Passed
Push — master ( 24d0fb...bc411e )
by Roeland
13:34 queued 12s
created

SFTP::fopen()   C

Complexity

Conditions 17
Paths 64

Size

Total Lines 35
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 31
c 0
b 0
f 0
nc 64
nop 2
dl 0
loc 35
rs 5.2166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Andreas Fischer <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author hkjolhede <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Jörn Friedrich Dreyer <[email protected]>
10
 * @author Lennart Rosam <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Robin McCorkell <[email protected]>
15
 * @author Roeland Jago Douma <[email protected]>
16
 * @author Ross Nicoll <[email protected]>
17
 * @author SA <[email protected]>
18
 * @author Senorsen <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
37
namespace OCA\Files_External\Lib\Storage;
38
use Icewind\Streams\IteratorDirectory;
39
use Icewind\Streams\RetryWrapper;
40
use phpseclib\Net\SFTP\Stream;
41
42
/**
43
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
44
* provide access to SFTP servers.
45
*/
46
class SFTP extends \OC\Files\Storage\Common {
47
	private $host;
48
	private $user;
49
	private $root;
50
	private $port = 22;
51
52
	private $auth = [];
53
54
	/**
55
	 * @var \phpseclib\Net\SFTP
56
	 */
57
	protected $client;
58
59
	/**
60
	 * @param string $host protocol://server:port
61
	 * @return array [$server, $port]
62
	 */
63
	private function splitHost($host) {
64
		$input = $host;
65
		if (strpos($host, '://') === false) {
66
			// add a protocol to fix parse_url behavior with ipv6
67
			$host = 'http://' . $host;
68
		}
69
70
		$parsed = parse_url($host);
71
		if(is_array($parsed) && isset($parsed['port'])) {
72
			return [$parsed['host'], $parsed['port']];
73
		} else if (is_array($parsed)) {
74
			return [$parsed['host'], 22];
75
		} else {
76
			return [$input, 22];
77
		}
78
	}
79
80
	/**
81
	 * {@inheritdoc}
82
	 */
83
	public function __construct($params) {
84
		// Register sftp://
85
		Stream::register();
86
87
		$parsedHost =  $this->splitHost($params['host']);
88
89
		$this->host = $parsedHost[0];
90
		$this->port = $parsedHost[1];
91
92
		if (!isset($params['user'])) {
93
			throw new \UnexpectedValueException('no authentication parameters specified');
94
		}
95
		$this->user = $params['user'];
96
97
		if (isset($params['public_key_auth'])) {
98
			$this->auth[] = $params['public_key_auth'];
99
		}
100
		if (isset($params['password']) && $params['password'] !== '') {
101
			$this->auth[] = $params['password'];
102
		}
103
104
		if ($this->auth === []) {
105
			throw new \UnexpectedValueException('no authentication parameters specified');
106
		}
107
108
		$this->root
109
			= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
110
111
		$this->root = '/' . ltrim($this->root, '/');
112
		$this->root = rtrim($this->root, '/') . '/';
113
	}
114
115
	/**
116
	 * Returns the connection.
117
	 *
118
	 * @return \phpseclib\Net\SFTP connected client instance
119
	 * @throws \Exception when the connection failed
120
	 */
121
	public function getConnection() {
122
		if (!is_null($this->client)) {
123
			return $this->client;
124
		}
125
126
		$hostKeys = $this->readHostKeys();
127
		$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
128
129
		// The SSH Host Key MUST be verified before login().
130
		$currentHostKey = $this->client->getServerPublicHostKey();
131
		if (array_key_exists($this->host, $hostKeys)) {
132
			if ($hostKeys[$this->host] !== $currentHostKey) {
133
				throw new \Exception('Host public key does not match known key');
134
			}
135
		} else {
136
			$hostKeys[$this->host] = $currentHostKey;
137
			$this->writeHostKeys($hostKeys);
138
		}
139
140
		$login = false;
141
		foreach ($this->auth as $auth) {
142
			$login = $this->client->login($this->user, $auth);
143
			if ($login === true) {
144
				break;
145
			}
146
		}
147
148
		if ($login === false) {
149
			throw new \Exception('Login failed');
150
		}
151
		return $this->client;
152
	}
153
154
	/**
155
	 * {@inheritdoc}
156
	 */
157
	public function test() {
158
		if (
159
			!isset($this->host)
160
			|| !isset($this->user)
161
		) {
162
			return false;
163
		}
164
		return $this->getConnection()->nlist() !== false;
165
	}
166
167
	/**
168
	 * {@inheritdoc}
169
	 */
170
	public function getId(){
171
		$id = 'sftp::' . $this->user . '@' . $this->host;
172
		if ($this->port !== 22) {
173
			$id .= ':' . $this->port;
174
		}
175
		// note: this will double the root slash,
176
		// we should not change it to keep compatible with
177
		// old storage ids
178
		$id .= '/' . $this->root;
179
		return $id;
180
	}
181
182
	/**
183
	 * @return string
184
	 */
185
	public function getHost() {
186
		return $this->host;
187
	}
188
189
	/**
190
	 * @return string
191
	 */
192
	public function getRoot() {
193
		return $this->root;
194
	}
195
196
	/**
197
	 * @return mixed
198
	 */
199
	public function getUser() {
200
		return $this->user;
201
	}
202
203
	/**
204
	 * @param string $path
205
	 * @return string
206
	 */
207
	private function absPath($path) {
208
		return $this->root . $this->cleanPath($path);
209
	}
210
211
	/**
212
	 * @return string|false
213
	 */
214
	private function hostKeysPath() {
215
		try {
216
			$storage_view = \OCP\Files::getStorage('files_external');
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Files::getStorage() has been deprecated: 14.0.0 use IAppData instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

216
			$storage_view = /** @scrutinizer ignore-deprecated */ \OCP\Files::getStorage('files_external');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
217
			if ($storage_view) {
0 ignored issues
show
introduced by
$storage_view is of type OC\Files\View, thus it always evaluated to true.
Loading history...
218
				return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') .
219
					$storage_view->getAbsolutePath('') .
220
					'ssh_hostKeys';
221
			}
222
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
223
		}
224
		return false;
225
	}
226
227
	/**
228
	 * @param $keys
229
	 * @return bool
230
	 */
231
	protected function writeHostKeys($keys) {
232
		try {
233
			$keyPath = $this->hostKeysPath();
234
			if ($keyPath && file_exists($keyPath)) {
235
				$fp = fopen($keyPath, 'w');
236
				foreach ($keys as $host => $key) {
237
					fwrite($fp, $host . '::' . $key . "\n");
238
				}
239
				fclose($fp);
240
				return true;
241
			}
242
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
243
		}
244
		return false;
245
	}
246
247
	/**
248
	 * @return array
249
	 */
250
	protected function readHostKeys() {
251
		try {
252
			$keyPath = $this->hostKeysPath();
253
			if (file_exists($keyPath)) {
254
				$hosts = array();
255
				$keys = array();
256
				$lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
257
				if ($lines) {
258
					foreach ($lines as $line) {
259
						$hostKeyArray = explode("::", $line, 2);
260
						if (count($hostKeyArray) === 2) {
261
							$hosts[] = $hostKeyArray[0];
262
							$keys[] = $hostKeyArray[1];
263
						}
264
					}
265
					return array_combine($hosts, $keys);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($hosts, $keys) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

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