Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

SFTP::opendir()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 12
nop 1
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Andreas Fischer <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author hkjolhede <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Lennart Rosam <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Ross Nicoll <[email protected]>
13
 * @author SA <[email protected]>
14
 * @author Senorsen <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @copyright Copyright (c) 2018, ownCloud GmbH
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
namespace OCA\Files_External\Lib\Storage;
35
use Icewind\Streams\IteratorDirectory;
36
use Icewind\Streams\RetryWrapper;
37
use phpseclib\Net\SFTP\Stream;
38
39
/**
40
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
41
* provide access to SFTP servers.
42
*/
43
class SFTP extends \OCP\Files\Storage\StorageAdapter {
44
	private $host;
45
	private $user;
46
	private $root;
47
	private $port = 22;
48
49
	private $auth;
50
51
	/**
52
	* @var SFTP
53
	*/
54
	protected $client;
55
56
	/**
57
	 * @param string $host protocol://server:port
58
	 * @return array [$server, $port]
59
	 */
60
	private function splitHost($host) {
61
		$input = $host;
62
		if (\strpos($host, '://') === false) {
63
			// add a protocol to fix parse_url behavior with ipv6
64
			$host = 'http://' . $host;
65
		}
66
67
		$parsed = \parse_url($host);
68
		if (\is_array($parsed) && isset($parsed['port'])) {
69
			return [$parsed['host'], $parsed['port']];
70
		} elseif (\is_array($parsed)) {
71
			return [$parsed['host'], 22];
72
		} else {
73
			return [$input, 22];
74
		}
75
	}
76
77
	/**
78
	 * {@inheritdoc}
79
	 */
80
	public function __construct($params) {
81
		// Register sftp://
82
		Stream::register();
83
84
		$parsedHost =  $this->splitHost($params['host']);
85
86
		$this->host = $parsedHost[0];
87
		$this->port = $parsedHost[1];
88
89
		if (!isset($params['user'])) {
90
			throw new \UnexpectedValueException('no authentication parameters specified');
91
		}
92
		$this->user = $params['user'];
93
94
		if (isset($params['public_key_auth'])) {
95
			$this->auth = $params['public_key_auth'];
96
		} elseif (isset($params['password'])) {
97
			$this->auth = $params['password'];
98
		} else {
99
			throw new \UnexpectedValueException('no authentication parameters specified');
100
		}
101
102
		$this->root
103
			= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
104
105
		if ($this->root[0] != '/') {
106
			$this->root = '/' . $this->root;
107
		}
108
109
		if (\substr($this->root, -1, 1) != '/') {
110
			$this->root .= '/';
111
		}
112
	}
113
114
	/**
115
	 * Returns the connection.
116
	 *
117
	 * @return \phpseclib\Net\SFTP connected client instance
118
	 * @throws \Exception when the connection failed
119
	 */
120
	public function getConnection() {
121
		if ($this->client !== null) {
122
			return $this->client;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->client; (OCA\Files_External\Lib\Storage\SFTP) is incompatible with the return type documented by OCA\Files_External\Lib\Storage\SFTP::getConnection of type phpseclib\Net\SFTP.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
123
		}
124
125
		$hostKeys = $this->readHostKeys();
126
		$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \phpseclib\Net\SFTP($this->host, $this->port) of type object<phpseclib\Net\SFTP> is incompatible with the declared type object<OCA\Files_External\Lib\Storage\SFTP> of property $client.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
127
128
		// The SSH Host Key MUST be verified before login().
129
		$currentHostKey = $this->client->getServerPublicHostKey();
130
		if (\array_key_exists($this->host, $hostKeys)) {
131
			if ($hostKeys[$this->host] != $currentHostKey) {
132
				throw new \Exception('Host public key does not match known key');
133
			}
134
		} else {
135
			$hostKeys[$this->host] = $currentHostKey;
136
			$this->writeHostKeys($hostKeys);
137
		}
138
139
		if (!$this->client->login($this->user, $this->auth)) {
140
			throw new \Exception('Login failed');
141
		}
142
		return $this->client;
143
	}
144
145
	/**
146
	 * {@inheritdoc}
147
	 */
148
	public function test() {
149
		if (
150
			!isset($this->host)
151
			|| !isset($this->user)
152
		) {
153
			return false;
154
		}
155
		return $this->getConnection()->nlist() !== false;
156
	}
157
158
	/**
159
	 * {@inheritdoc}
160
	 */
161
	public function getId() {
162
		$id = 'sftp::' . $this->user . '@' . $this->host;
163
		if ($this->port !== 22) {
164
			$id .= ':' . $this->port;
165
		}
166
		// note: this will double the root slash,
167
		// we should not change it to keep compatible with
168
		// old storage ids
169
		$id .= '/' . $this->root;
170
		return $id;
171
	}
172
173
	/**
174
	 * @return string
175
	 */
176
	public function getHost() {
177
		return $this->host;
178
	}
179
180
	/**
181
	 * @return string
182
	 */
183
	public function getRoot() {
184
		return $this->root;
185
	}
186
187
	/**
188
	 * @return mixed
189
	 */
190
	public function getUser() {
191
		return $this->user;
192
	}
193
194
	/**
195
	 * @param string $path
196
	 * @return string
197
	 */
198
	private function absPath($path) {
199
		return $this->root . $this->cleanPath($path);
200
	}
201
202
	/**
203
	 * @return string|false
204
	 */
205
	private function hostKeysPath() {
206
		try {
207
			$storage_view = \OCP\Files::getStorage('files_external');
208
			if ($storage_view) {
209
				return \OC::$server->getConfig()->getSystemValue('datadirectory') .
210
					$storage_view->getAbsolutePath('') .
211
					'ssh_hostKeys';
212
			}
213
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
214
		}
215
		return false;
216
	}
217
218
	/**
219
	 * @param $keys
220
	 * @return bool
221
	 */
222
	protected function writeHostKeys($keys) {
223
		try {
224
			$keyPath = $this->hostKeysPath();
225
			if ($keyPath && \file_exists($keyPath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keyPath of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
226
				$fp = \fopen($keyPath, 'w');
227
				foreach ($keys as $host => $key) {
228
					\fwrite($fp, $host . '::' . $key . "\n");
229
				}
230
				\fclose($fp);
231
				return true;
232
			}
233
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
234
		}
235
		return false;
236
	}
237
238
	/**
239
	 * @return array
240
	 */
241
	protected function readHostKeys() {
242
		try {
243
			$keyPath = $this->hostKeysPath();
244
			if (\file_exists($keyPath)) {
245
				$hosts = [];
246
				$keys = [];
247
				$lines = \file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
248
				if ($lines) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lines of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
249
					foreach ($lines as $line) {
250
						$hostKeyArray = \explode("::", $line, 2);
251
						if (\count($hostKeyArray) == 2) {
252
							$hosts[] = $hostKeyArray[0];
253
							$keys[] = $hostKeyArray[1];
254
						}
255
					}
256
					return \array_combine($hosts, $keys);
257
				}
258
			}
259
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
260
		}
261
		return [];
262
	}
263
264
	/**
265
	 * {@inheritdoc}
266
	 */
267
	public function mkdir($path) {
268
		try {
269
			return $this->getConnection()->mkdir($this->absPath($path));
270
		} catch (\Exception $e) {
271
			return false;
272
		}
273
	}
274
275
	/**
276
	 * {@inheritdoc}
277
	 */
278
	public function rmdir($path) {
279
		try {
280
			$result = $this->getConnection()->delete($this->absPath($path), true);
281
			// workaround: stray stat cache entry when deleting empty folders
282
			// see https://github.com/phpseclib/phpseclib/issues/706
283
			$this->getConnection()->clearStatCache();
284
			return $result;
285
		} catch (\Exception $e) {
286
			return false;
287
		}
288
	}
289
290
	/**
291
	 * {@inheritdoc}
292
	 */
293
	public function opendir($path) {
294
		try {
295
			$list = $this->getConnection()->nlist($this->absPath($path));
296
			if ($list === false) {
297
				return false;
298
			}
299
300
			$id = \md5('sftp:' . $path);
301
			$dirStream = [];
302
			foreach ($list as $file) {
303
				if ($file != '.' && $file != '..') {
304
					$dirStream[] = $file;
305
				}
306
			}
307
			return IteratorDirectory::wrap($dirStream);
308
		} catch (\Exception $e) {
309
			return false;
310
		}
311
	}
312
313
	/**
314
	 * {@inheritdoc}
315
	 */
316
	public function filetype($path) {
317
		try {
318
			$stat = $this->getConnection()->stat($this->absPath($path));
319
			if ($stat['type'] == NET_SFTP_TYPE_REGULAR) {
320
				return 'file';
321
			}
322
323
			if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) {
324
				return 'dir';
325
			}
326
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
327
		}
328
		return false;
329
	}
330
331
	/**
332
	 * {@inheritdoc}
333
	 */
334
	public function file_exists($path) {
335
		try {
336
			return $this->getConnection()->stat($this->absPath($path)) !== false;
337
		} catch (\Exception $e) {
338
			return false;
339
		}
340
	}
341
342
	/**
343
	 * {@inheritdoc}
344
	 */
345
	public function unlink($path) {
346
		try {
347
			return $this->getConnection()->delete($this->absPath($path), true);
348
		} catch (\Exception $e) {
349
			return false;
350
		}
351
	}
352
353
	/**
354
	 * {@inheritdoc}
355
	 */
356
	public function fopen($path, $mode) {
357
		try {
358
			$absPath = $this->absPath($path);
359
			switch ($mode) {
360
				case 'r':
361
				case 'rb':
362
					if (!$this->file_exists($path)) {
363
						return false;
364
					}
365
					// no break
366
				case 'w':
367
				case 'wb':
368
				case 'a':
369
				case 'ab':
370
				case 'r+':
371
				case 'w+':
372
				case 'wb+':
373
				case 'a+':
374
				case 'x':
375
				case 'x+':
376
				case 'c':
377
				case 'c+':
378
					$context = \stream_context_create(['sftp' => ['session' => $this->getConnection()]]);
379
					$handle = \fopen($this->constructUrl($path), $mode, false, $context);
380
					return RetryWrapper::wrap($handle);
381
			}
382
		} catch (\Exception $e) {
383
		}
384
		return false;
385
	}
386
387
	/**
388
	 * {@inheritdoc}
389
	 */
390
	public function touch($path, $mtime=null) {
391
		try {
392
			if ($mtime !== null) {
393
				return false;
394
			}
395
			if (!$this->file_exists($path)) {
396
				$this->getConnection()->put($this->absPath($path), '');
397
			} else {
398
				return false;
399
			}
400
		} catch (\Exception $e) {
401
			return false;
402
		}
403
		return true;
404
	}
405
406
	/**
407
	 * @param string $path
408
	 * @param string $target
409
	 * @throws \Exception
410
	 */
411
	public function getFile($path, $target) {
412
		$this->getConnection()->get($path, $target);
413
	}
414
415
	/**
416
	 * @param string $path
417
	 * @param string $target
418
	 * @throws \Exception
419
	 */
420
	public function uploadFile($path, $target) {
421
		$this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE);
422
	}
423
424
	/**
425
	 * {@inheritdoc}
426
	 */
427
	public function rename($source, $target) {
428
		try {
429
			if ($this->file_exists($target)) {
430
				$this->unlink($target);
431
			}
432
			return $this->getConnection()->rename(
433
				$this->absPath($source),
434
				$this->absPath($target)
435
			);
436
		} catch (\Exception $e) {
437
			return false;
438
		}
439
	}
440
441
	/**
442
	 * {@inheritdoc}
443
	 */
444
	public function stat($path) {
445
		try {
446
			$stat = $this->getConnection()->stat($this->absPath($path));
447
448
			$mtime = $stat ? $stat['mtime'] : -1;
449
			$size = $stat ? $stat['size'] : 0;
450
451
			return ['mtime' => $mtime, 'size' => $size, 'ctime' => -1];
452
		} catch (\Exception $e) {
453
			return false;
454
		}
455
	}
456
457
	/**
458
	 * @param string $path
459
	 * @return string
460
	 */
461
	public function constructUrl($path) {
462
		// Do not pass the password here. We want to use the Net_SFTP object
463
		// supplied via stream context or fail. We only supply username and
464
		// hostname because this might show up in logs (they are not used).
465
		$url = 'sftp://' . \urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
466
		return $url;
467
	}
468
}
469