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'); |
|
|
|
|
217
|
|
|
if ($storage_view) { |
|
|
|
|
218
|
|
|
return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . |
219
|
|
|
$storage_view->getAbsolutePath('') . |
220
|
|
|
'ssh_hostKeys'; |
221
|
|
|
} |
222
|
|
|
} catch (\Exception $e) { |
|
|
|
|
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) { |
|
|
|
|
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); |
|
|
|
|
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
} catch (\Exception $e) { |
|
|
|
|
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); |
|
|
|
|
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) { |
|
|
|
|
329
|
|
|
return 'file'; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
if ((int) $stat['type'] === NET_SFTP_TYPE_DIRECTORY) { |
|
|
|
|
333
|
|
|
return 'dir'; |
334
|
|
|
} |
335
|
|
|
} catch (\Exception $e) { |
|
|
|
|
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) { |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.