Completed
Push — master ( ca493a...4c38d1 )
by Morris
132:28 queued 111:40
created

SFTP::getRoot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 Ross Nicoll <[email protected]>
16
 * @author SA <[email protected]>
17
 * @author Senorsen <[email protected]>
18
 * @author Vincent Petry <[email protected]>
19
 *
20
 * @license AGPL-3.0
21
 *
22
 * This code is free software: you can redistribute it and/or modify
23
 * it under the terms of the GNU Affero General Public License, version 3,
24
 * as published by the Free Software Foundation.
25
 *
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
 * GNU Affero General Public License for more details.
30
 *
31
 * You should have received a copy of the GNU Affero General Public License, version 3,
32
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33
 *
34
 */
35
namespace OCA\Files_External\Lib\Storage;
36
use Icewind\Streams\IteratorDirectory;
37
use Icewind\Streams\RetryWrapper;
38
use phpseclib\Net\SFTP\Stream;
39
40
/**
41
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
42
* provide access to SFTP servers.
43
*/
44
class SFTP extends \OC\Files\Storage\Common {
45
	private $host;
46
	private $user;
47
	private $root;
48
	private $port = 22;
49
50
	private $auth;
51
52
	/**
53
	 * @var \phpseclib\Net\SFTP
54
	 */
55
	protected $client;
56
57
	/**
58
	 * @param string $host protocol://server:port
59
	 * @return array [$server, $port]
60
	 */
61
	private function splitHost($host) {
62
		$input = $host;
63
		if (strpos($host, '://') === false) {
64
			// add a protocol to fix parse_url behavior with ipv6
65
			$host = 'http://' . $host;
66
		}
67
68
		$parsed = parse_url($host);
69
		if(is_array($parsed) && isset($parsed['port'])) {
70
			return [$parsed['host'], $parsed['port']];
71
		} else if (is_array($parsed)) {
72
			return [$parsed['host'], 22];
73
		} else {
74
			return [$input, 22];
75
		}
76
	}
77
78
	/**
79
	 * {@inheritdoc}
80
	 */
81
	public function __construct($params) {
82
		// Register sftp://
83
		Stream::register();
84
85
		$parsedHost =  $this->splitHost($params['host']);
86
87
		$this->host = $parsedHost[0];
88
		$this->port = $parsedHost[1];
89
90
		if (!isset($params['user'])) {
91
			throw new \UnexpectedValueException('no authentication parameters specified');
92
		}
93
		$this->user = $params['user'];
94
95
		if (isset($params['public_key_auth'])) {
96
			$this->auth = $params['public_key_auth'];
97
		} elseif (isset($params['password'])) {
98
			$this->auth = $params['password'];
99
		} else {
100
			throw new \UnexpectedValueException('no authentication parameters specified');
101
		}
102
103
		$this->root
104
			= isset($params['root']) ? $this->cleanPath($params['root']) : '/';
105
106
		$this->root = '/' . ltrim($this->root, '/');
107
		$this->root = rtrim($this->root, '/') . '/';
108
	}
109
110
	/**
111
	 * Returns the connection.
112
	 *
113
	 * @return \phpseclib\Net\SFTP connected client instance
114
	 * @throws \Exception when the connection failed
115
	 */
116
	public function getConnection() {
117
		if (!is_null($this->client)) {
118
			return $this->client;
119
		}
120
121
		$hostKeys = $this->readHostKeys();
122
		$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
123
124
		// The SSH Host Key MUST be verified before login().
125
		$currentHostKey = $this->client->getServerPublicHostKey();
126
		if (array_key_exists($this->host, $hostKeys)) {
127
			if ($hostKeys[$this->host] !== $currentHostKey) {
128
				throw new \Exception('Host public key does not match known key');
129
			}
130
		} else {
131
			$hostKeys[$this->host] = $currentHostKey;
132
			$this->writeHostKeys($hostKeys);
133
		}
134
135
		if (!$this->client->login($this->user, $this->auth)) {
136
			throw new \Exception('Login failed');
137
		}
138
		return $this->client;
139
	}
140
141
	/**
142
	 * {@inheritdoc}
143
	 */
144
	public function test() {
145
		if (
146
			!isset($this->host)
147
			|| !isset($this->user)
148
		) {
149
			return false;
150
		}
151
		return $this->getConnection()->nlist() !== false;
152
	}
153
154
	/**
155
	 * {@inheritdoc}
156
	 */
157
	public function getId(){
158
		$id = 'sftp::' . $this->user . '@' . $this->host;
159
		if ($this->port !== 22) {
160
			$id .= ':' . $this->port;
161
		}
162
		// note: this will double the root slash,
163
		// we should not change it to keep compatible with
164
		// old storage ids
165
		$id .= '/' . $this->root;
166
		return $id;
167
	}
168
169
	/**
170
	 * @return string
171
	 */
172
	public function getHost() {
173
		return $this->host;
174
	}
175
176
	/**
177
	 * @return string
178
	 */
179
	public function getRoot() {
180
		return $this->root;
181
	}
182
183
	/**
184
	 * @return mixed
185
	 */
186
	public function getUser() {
187
		return $this->user;
188
	}
189
190
	/**
191
	 * @param string $path
192
	 * @return string
193
	 */
194
	private function absPath($path) {
195
		return $this->root . $this->cleanPath($path);
196
	}
197
198
	/**
199
	 * @return string|false
200
	 */
201
	private function hostKeysPath() {
202
		try {
203
			$storage_view = \OCP\Files::getStorage('files_external');
204 View Code Duplication
			if ($storage_view) {
205
				return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') .
206
					$storage_view->getAbsolutePath('') .
207
					'ssh_hostKeys';
208
			}
209
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
210
		}
211
		return false;
212
	}
213
214
	/**
215
	 * @param $keys
216
	 * @return bool
217
	 */
218
	protected function writeHostKeys($keys) {
219
		try {
220
			$keyPath = $this->hostKeysPath();
221
			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...
222
				$fp = fopen($keyPath, 'w');
223
				foreach ($keys as $host => $key) {
224
					fwrite($fp, $host . '::' . $key . "\n");
225
				}
226
				fclose($fp);
227
				return true;
228
			}
229
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
230
		}
231
		return false;
232
	}
233
234
	/**
235
	 * @return array
236
	 */
237
	protected function readHostKeys() {
238
		try {
239
			$keyPath = $this->hostKeysPath();
240
			if (file_exists($keyPath)) {
241
				$hosts = array();
242
				$keys = array();
243
				$lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
244
				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...
245
					foreach ($lines as $line) {
246
						$hostKeyArray = explode("::", $line, 2);
247
						if (count($hostKeyArray) === 2) {
248
							$hosts[] = $hostKeyArray[0];
249
							$keys[] = $hostKeyArray[1];
250
						}
251
					}
252
					return array_combine($hosts, $keys);
253
				}
254
			}
255
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
256
		}
257
		return array();
258
	}
259
260
	/**
261
	 * {@inheritdoc}
262
	 */
263
	public function mkdir($path) {
264
		try {
265
			return $this->getConnection()->mkdir($this->absPath($path));
266
		} catch (\Exception $e) {
267
			return false;
268
		}
269
	}
270
271
	/**
272
	 * {@inheritdoc}
273
	 */
274
	public function rmdir($path) {
275
		try {
276
			$result = $this->getConnection()->delete($this->absPath($path), true);
277
			// workaround: stray stat cache entry when deleting empty folders
278
			// see https://github.com/phpseclib/phpseclib/issues/706
279
			$this->getConnection()->clearStatCache();
280
			return $result;
281
		} catch (\Exception $e) {
282
			return false;
283
		}
284
	}
285
286
	/**
287
	 * {@inheritdoc}
288
	 */
289
	public function opendir($path) {
290
		try {
291
			$list = $this->getConnection()->nlist($this->absPath($path));
292
			if ($list === false) {
293
				return false;
294
			}
295
296
			$id = md5('sftp:' . $path);
0 ignored issues
show
Unused Code introduced by
$id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
297
			$dirStream = array();
298
			foreach($list as $file) {
299
				if ($file !== '.' && $file !== '..') {
300
					$dirStream[] = $file;
301
				}
302
			}
303
			return IteratorDirectory::wrap($dirStream);
304
		} catch(\Exception $e) {
305
			return false;
306
		}
307
	}
308
309
	/**
310
	 * {@inheritdoc}
311
	 */
312
	public function filetype($path) {
313
		try {
314
			$stat = $this->getConnection()->stat($this->absPath($path));
315
			if ((int) $stat['type'] === NET_SFTP_TYPE_REGULAR) {
316
				return 'file';
317
			}
318
319
			if ((int) $stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
320
				return 'dir';
321
			}
322
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
323
324
		}
325
		return false;
326
	}
327
328
	/**
329
	 * {@inheritdoc}
330
	 */
331
	public function file_exists($path) {
332
		try {
333
			return $this->getConnection()->stat($this->absPath($path)) !== false;
334
		} catch (\Exception $e) {
335
			return false;
336
		}
337
	}
338
339
	/**
340
	 * {@inheritdoc}
341
	 */
342
	public function unlink($path) {
343
		try {
344
			return $this->getConnection()->delete($this->absPath($path), true);
345
		} catch (\Exception $e) {
346
			return false;
347
		}
348
	}
349
350
	/**
351
	 * {@inheritdoc}
352
	 */
353
	public function fopen($path, $mode) {
354
		try {
355
			$absPath = $this->absPath($path);
0 ignored issues
show
Unused Code introduced by
$absPath is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
356
			switch($mode) {
357
				case 'r':
358
				case 'rb':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
359
					if ( !$this->file_exists($path)) {
360
						return false;
361
					}
362
				case 'w':
363
				case 'wb':
364
				case 'a':
365
				case 'ab':
366
				case 'r+':
367
				case 'w+':
368
				case 'wb+':
369
				case 'a+':
370
				case 'x':
371
				case 'x+':
372
				case 'c':
373 View Code Duplication
				case 'c+':
374
					$context = stream_context_create(array('sftp' => array('session' => $this->getConnection())));
375
					$handle = fopen($this->constructUrl($path), $mode, false, $context);
376
					return RetryWrapper::wrap($handle);
377
			}
378
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
379
		}
380
		return false;
381
	}
382
383
	/**
384
	 * {@inheritdoc}
385
	 */
386
	public function touch($path, $mtime=null) {
387
		try {
388
			if (!is_null($mtime)) {
389
				return false;
390
			}
391
			if (!$this->file_exists($path)) {
392
				$this->getConnection()->put($this->absPath($path), '');
393
			} else {
394
				return false;
395
			}
396
		} catch (\Exception $e) {
397
			return false;
398
		}
399
		return true;
400
	}
401
402
	/**
403
	 * @param string $path
404
	 * @param string $target
405
	 * @throws \Exception
406
	 */
407
	public function getFile($path, $target) {
408
		$this->getConnection()->get($path, $target);
409
	}
410
411
	/**
412
	 * @param string $path
413
	 * @param string $target
414
	 * @throws \Exception
415
	 */
416
	public function uploadFile($path, $target) {
417
		$this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE);
418
	}
419
420
	/**
421
	 * {@inheritdoc}
422
	 */
423
	public function rename($source, $target) {
424
		try {
425
			if ($this->file_exists($target)) {
426
				$this->unlink($target);
427
			}
428
			return $this->getConnection()->rename(
429
				$this->absPath($source),
430
				$this->absPath($target)
431
			);
432
		} catch (\Exception $e) {
433
			return false;
434
		}
435
	}
436
437
	/**
438
	 * {@inheritdoc}
439
	 */
440
	public function stat($path) {
441
		try {
442
			$stat = $this->getConnection()->stat($this->absPath($path));
443
444
			$mtime = $stat ? $stat['mtime'] : -1;
445
			$size = $stat ? $stat['size'] : 0;
446
447
			return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1);
448
		} catch (\Exception $e) {
449
			return false;
450
		}
451
	}
452
453
	/**
454
	 * @param string $path
455
	 * @return string
456
	 */
457
	public function constructUrl($path) {
458
		// Do not pass the password here. We want to use the Net_SFTP object
459
		// supplied via stream context or fail. We only supply username and
460
		// hostname because this might show up in logs (they are not used).
461
		$url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
462
		return $url;
463
	}
464
}
465