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 Vincent Petry <[email protected]> |
15
|
|
|
* |
16
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc. |
17
|
|
|
* @license AGPL-3.0 |
18
|
|
|
* |
19
|
|
|
* This code is free software: you can redistribute it and/or modify |
20
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
21
|
|
|
* as published by the Free Software Foundation. |
22
|
|
|
* |
23
|
|
|
* This program is distributed in the hope that it will be useful, |
24
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
25
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
26
|
|
|
* GNU Affero General Public License for more details. |
27
|
|
|
* |
28
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
29
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
30
|
|
|
* |
31
|
|
|
*/ |
32
|
|
|
namespace OCA\Files_External\Lib\Storage; |
33
|
|
|
use Icewind\Streams\IteratorDirectory; |
34
|
|
|
|
35
|
|
|
use Icewind\Streams\RetryWrapper; |
36
|
|
|
use phpseclib\Net\SFTP\Stream; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to |
40
|
|
|
* provide access to SFTP servers. |
41
|
|
|
*/ |
42
|
|
|
class SFTP extends \OC\Files\Storage\Common { |
43
|
|
|
private $host; |
44
|
|
|
private $user; |
45
|
|
|
private $root; |
46
|
|
|
private $port = 22; |
47
|
|
|
|
48
|
|
|
private $auth; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var SFTP |
52
|
|
|
*/ |
53
|
|
|
protected $client; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @param string $host protocol://server:port |
57
|
|
|
* @return array [$server, $port] |
58
|
|
|
*/ |
59
|
|
|
private function splitHost($host) { |
60
|
|
|
$input = $host; |
61
|
|
|
if (strpos($host, '://') === false) { |
62
|
|
|
// add a protocol to fix parse_url behavior with ipv6 |
63
|
|
|
$host = 'http://' . $host; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$parsed = parse_url($host); |
67
|
|
|
if(is_array($parsed) && isset($parsed['port'])) { |
68
|
|
|
return [$parsed['host'], $parsed['port']]; |
69
|
|
|
} else if (is_array($parsed)) { |
70
|
|
|
return [$parsed['host'], 22]; |
71
|
|
|
} else { |
72
|
|
|
return [$input, 22]; |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* {@inheritdoc} |
78
|
|
|
*/ |
79
|
|
|
public function __construct($params) { |
80
|
|
|
// Register sftp:// |
81
|
|
|
Stream::register(); |
82
|
|
|
|
83
|
|
|
$parsedHost = $this->splitHost($params['host']); |
84
|
|
|
|
85
|
|
|
$this->host = $parsedHost[0]; |
86
|
|
|
$this->port = $parsedHost[1]; |
87
|
|
|
|
88
|
|
|
if (!isset($params['user'])) { |
89
|
|
|
throw new \UnexpectedValueException('no authentication parameters specified'); |
90
|
|
|
} |
91
|
|
|
$this->user = $params['user']; |
92
|
|
|
|
93
|
|
|
if (isset($params['public_key_auth'])) { |
94
|
|
|
$this->auth = $params['public_key_auth']; |
95
|
|
|
} elseif (isset($params['password'])) { |
96
|
|
|
$this->auth = $params['password']; |
97
|
|
|
} else { |
98
|
|
|
throw new \UnexpectedValueException('no authentication parameters specified'); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$this->root |
102
|
|
|
= isset($params['root']) ? $this->cleanPath($params['root']) : '/'; |
103
|
|
|
|
104
|
|
|
if ($this->root[0] != '/') { |
105
|
|
|
$this->root = '/' . $this->root; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
if (substr($this->root, -1, 1) != '/') { |
109
|
|
|
$this->root .= '/'; |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Returns the connection. |
115
|
|
|
* |
116
|
|
|
* @return \phpseclib\Net\SFTP connected client instance |
117
|
|
|
* @throws \Exception when the connection failed |
118
|
|
|
*/ |
119
|
|
|
public function getConnection() { |
120
|
|
|
if (!is_null($this->client)) { |
121
|
|
|
return $this->client; |
|
|
|
|
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$hostKeys = $this->readHostKeys(); |
125
|
|
|
$this->client = new \phpseclib\Net\SFTP($this->host, $this->port); |
|
|
|
|
126
|
|
|
|
127
|
|
|
// The SSH Host Key MUST be verified before login(). |
128
|
|
|
$currentHostKey = $this->client->getServerPublicHostKey(); |
129
|
|
|
if (array_key_exists($this->host, $hostKeys)) { |
130
|
|
|
if ($hostKeys[$this->host] != $currentHostKey) { |
131
|
|
|
throw new \Exception('Host public key does not match known key'); |
132
|
|
|
} |
133
|
|
|
} else { |
134
|
|
|
$hostKeys[$this->host] = $currentHostKey; |
135
|
|
|
$this->writeHostKeys($hostKeys); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
if (!$this->client->login($this->user, $this->auth)) { |
139
|
|
|
throw new \Exception('Login failed'); |
140
|
|
|
} |
141
|
|
|
return $this->client; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* {@inheritdoc} |
146
|
|
|
*/ |
147
|
|
|
public function test() { |
148
|
|
|
if ( |
149
|
|
|
!isset($this->host) |
150
|
|
|
|| !isset($this->user) |
151
|
|
|
) { |
152
|
|
|
return false; |
153
|
|
|
} |
154
|
|
|
return $this->getConnection()->nlist() !== false; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* {@inheritdoc} |
159
|
|
|
*/ |
160
|
|
|
public function getId(){ |
161
|
|
|
$id = 'sftp::' . $this->user . '@' . $this->host; |
162
|
|
|
if ($this->port !== 22) { |
163
|
|
|
$id .= ':' . $this->port; |
164
|
|
|
} |
165
|
|
|
// note: this will double the root slash, |
166
|
|
|
// we should not change it to keep compatible with |
167
|
|
|
// old storage ids |
168
|
|
|
$id .= '/' . $this->root; |
169
|
|
|
return $id; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* @return string |
174
|
|
|
*/ |
175
|
|
|
public function getHost() { |
176
|
|
|
return $this->host; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @return string |
181
|
|
|
*/ |
182
|
|
|
public function getRoot() { |
183
|
|
|
return $this->root; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* @return mixed |
188
|
|
|
*/ |
189
|
|
|
public function getUser() { |
190
|
|
|
return $this->user; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @param string $path |
195
|
|
|
* @return string |
196
|
|
|
*/ |
197
|
|
|
private function absPath($path) { |
198
|
|
|
return $this->root . $this->cleanPath($path); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @return string|false |
203
|
|
|
*/ |
204
|
|
|
private function hostKeysPath() { |
205
|
|
|
try { |
206
|
|
|
$storage_view = \OCP\Files::getStorage('files_external'); |
207
|
|
|
if ($storage_view) { |
208
|
|
|
return \OC::$server->getConfig()->getSystemValue('datadirectory') . |
209
|
|
|
$storage_view->getAbsolutePath('') . |
210
|
|
|
'ssh_hostKeys'; |
211
|
|
|
} |
212
|
|
|
} catch (\Exception $e) { |
|
|
|
|
213
|
|
|
} |
214
|
|
|
return false; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* @param $keys |
219
|
|
|
* @return bool |
220
|
|
|
*/ |
221
|
|
|
protected function writeHostKeys($keys) { |
222
|
|
|
try { |
223
|
|
|
$keyPath = $this->hostKeysPath(); |
224
|
|
|
if ($keyPath && file_exists($keyPath)) { |
|
|
|
|
225
|
|
|
$fp = fopen($keyPath, 'w'); |
226
|
|
|
foreach ($keys as $host => $key) { |
227
|
|
|
fwrite($fp, $host . '::' . $key . "\n"); |
228
|
|
|
} |
229
|
|
|
fclose($fp); |
230
|
|
|
return true; |
231
|
|
|
} |
232
|
|
|
} catch (\Exception $e) { |
|
|
|
|
233
|
|
|
} |
234
|
|
|
return false; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @return array |
239
|
|
|
*/ |
240
|
|
|
protected function readHostKeys() { |
241
|
|
|
try { |
242
|
|
|
$keyPath = $this->hostKeysPath(); |
243
|
|
|
if (file_exists($keyPath)) { |
244
|
|
|
$hosts = array(); |
245
|
|
|
$keys = array(); |
246
|
|
|
$lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); |
247
|
|
|
if ($lines) { |
|
|
|
|
248
|
|
|
foreach ($lines as $line) { |
249
|
|
|
$hostKeyArray = explode("::", $line, 2); |
250
|
|
|
if (count($hostKeyArray) == 2) { |
251
|
|
|
$hosts[] = $hostKeyArray[0]; |
252
|
|
|
$keys[] = $hostKeyArray[1]; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
return array_combine($hosts, $keys); |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
} catch (\Exception $e) { |
|
|
|
|
259
|
|
|
} |
260
|
|
|
return array(); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* {@inheritdoc} |
265
|
|
|
*/ |
266
|
|
|
public function mkdir($path) { |
267
|
|
|
try { |
268
|
|
|
return $this->getConnection()->mkdir($this->absPath($path)); |
269
|
|
|
} catch (\Exception $e) { |
270
|
|
|
return false; |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* {@inheritdoc} |
276
|
|
|
*/ |
277
|
|
|
public function rmdir($path) { |
278
|
|
|
try { |
279
|
|
|
$result = $this->getConnection()->delete($this->absPath($path), true); |
280
|
|
|
// workaround: stray stat cache entry when deleting empty folders |
281
|
|
|
// see https://github.com/phpseclib/phpseclib/issues/706 |
282
|
|
|
$this->getConnection()->clearStatCache(); |
283
|
|
|
return $result; |
284
|
|
|
} catch (\Exception $e) { |
285
|
|
|
return false; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* {@inheritdoc} |
291
|
|
|
*/ |
292
|
|
|
public function opendir($path) { |
293
|
|
|
try { |
294
|
|
|
$list = $this->getConnection()->nlist($this->absPath($path)); |
295
|
|
|
if ($list === false) { |
296
|
|
|
return false; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
$id = md5('sftp:' . $path); |
|
|
|
|
300
|
|
|
$dirStream = array(); |
301
|
|
|
foreach($list as $file) { |
302
|
|
|
if ($file != '.' && $file != '..') { |
303
|
|
|
$dirStream[] = $file; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
return IteratorDirectory::wrap($dirStream); |
307
|
|
|
} catch(\Exception $e) { |
308
|
|
|
return false; |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* {@inheritdoc} |
314
|
|
|
*/ |
315
|
|
|
public function filetype($path) { |
316
|
|
|
try { |
317
|
|
|
$stat = $this->getConnection()->stat($this->absPath($path)); |
318
|
|
|
if ($stat['type'] == NET_SFTP_TYPE_REGULAR) { |
319
|
|
|
return 'file'; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { |
323
|
|
|
return 'dir'; |
324
|
|
|
} |
325
|
|
|
} catch (\Exception $e) { |
|
|
|
|
326
|
|
|
|
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
|
|
|
case 'w': |
366
|
|
|
case 'wb': |
367
|
|
|
case 'a': |
368
|
|
|
case 'ab': |
369
|
|
|
case 'r+': |
370
|
|
|
case 'w+': |
371
|
|
|
case 'wb+': |
372
|
|
|
case 'a+': |
373
|
|
|
case 'x': |
374
|
|
|
case 'x+': |
375
|
|
|
case 'c': |
376
|
|
View Code Duplication |
case 'c+': |
|
|
|
|
377
|
|
|
$context = stream_context_create(array('sftp' => array('session' => $this->getConnection()))); |
378
|
|
|
$handle = fopen($this->constructUrl($path), $mode, false, $context); |
379
|
|
|
return RetryWrapper::wrap($handle); |
380
|
|
|
} |
381
|
|
|
} catch (\Exception $e) { |
|
|
|
|
382
|
|
|
} |
383
|
|
|
return false; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* {@inheritdoc} |
388
|
|
|
*/ |
389
|
|
|
public function touch($path, $mtime=null) { |
390
|
|
|
try { |
391
|
|
|
if (!is_null($mtime)) { |
392
|
|
|
return false; |
393
|
|
|
} |
394
|
|
|
if (!$this->file_exists($path)) { |
395
|
|
|
$this->getConnection()->put($this->absPath($path), ''); |
396
|
|
|
} else { |
397
|
|
|
return false; |
398
|
|
|
} |
399
|
|
|
} catch (\Exception $e) { |
400
|
|
|
return false; |
401
|
|
|
} |
402
|
|
|
return true; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* @param string $path |
407
|
|
|
* @param string $target |
408
|
|
|
* @throws \Exception |
409
|
|
|
*/ |
410
|
|
|
public function getFile($path, $target) { |
411
|
|
|
$this->getConnection()->get($path, $target); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* @param string $path |
416
|
|
|
* @param string $target |
417
|
|
|
* @throws \Exception |
418
|
|
|
*/ |
419
|
|
|
public function uploadFile($path, $target) { |
420
|
|
|
$this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* {@inheritdoc} |
425
|
|
|
*/ |
426
|
|
|
public function rename($source, $target) { |
427
|
|
|
try { |
428
|
|
|
if (!$this->is_dir($target) && $this->file_exists($target)) { |
429
|
|
|
$this->unlink($target); |
430
|
|
|
} |
431
|
|
|
return $this->getConnection()->rename( |
432
|
|
|
$this->absPath($source), |
433
|
|
|
$this->absPath($target) |
434
|
|
|
); |
435
|
|
|
} catch (\Exception $e) { |
436
|
|
|
return false; |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* {@inheritdoc} |
442
|
|
|
*/ |
443
|
|
|
public function stat($path) { |
444
|
|
|
try { |
445
|
|
|
$stat = $this->getConnection()->stat($this->absPath($path)); |
446
|
|
|
|
447
|
|
|
$mtime = $stat ? $stat['mtime'] : -1; |
448
|
|
|
$size = $stat ? $stat['size'] : 0; |
449
|
|
|
|
450
|
|
|
return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1); |
451
|
|
|
} catch (\Exception $e) { |
452
|
|
|
return false; |
453
|
|
|
} |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* @param string $path |
458
|
|
|
* @return string |
459
|
|
|
*/ |
460
|
|
|
public function constructUrl($path) { |
461
|
|
|
// Do not pass the password here. We want to use the Net_SFTP object |
462
|
|
|
// supplied via stream context or fail. We only supply username and |
463
|
|
|
// hostname because this might show up in logs (they are not used). |
464
|
|
|
$url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path; |
465
|
|
|
return $url; |
466
|
|
|
} |
467
|
|
|
} |
468
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.