Passed
Push — master ( 3c5692...34cd49 )
by Roeland
13:23 queued 11s
created

SFTP::uploadFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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