Completed
Push — master ( 7a70a8...c15aec )
by Frank
02:13
created

SftpAdapter::setHostFingerprint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
namespace League\Flysystem\Sftp;
4
5
use InvalidArgumentException;
6
use League\Flysystem\Adapter\AbstractFtpAdapter;
7
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
8
use League\Flysystem\AdapterInterface;
9
use League\Flysystem\Config;
10
use League\Flysystem\Util;
11
use LogicException;
12
use phpseclib\Net\SFTP;
13
use phpseclib\Crypt\RSA;
14
use phpseclib\System\SSH\Agent;
15
use RuntimeException;
16
17
class SftpAdapter extends AbstractFtpAdapter
18
{
19
    use StreamedCopyTrait;
20
21
    /**
22
     * @var int
23
     */
24
    protected $port = 22;
25
26
    /**
27
     * @var string
28
     */
29
    protected $hostFingerprint;
30
31
    /**
32
     * @var string
33
     */
34
    protected $privatekey;
35
36
    /**
37
     * @var bool
38
     */
39
    protected $useAgent = false;
40
41
    /**
42
     * @var Agent
43
     */
44
    private $agent;
45
46
    /**
47
     * @var array
48
     */
49
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
50
51
    /**
52
     * @var array
53
     */
54
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
55
56
    /**
57
     * @var int
58
     */
59
    protected $directoryPerm = 0744;
60
61
    /**
62
     * Prefix a path.
63
     *
64
     * @param string $path
65
     *
66
     * @return string
67
     */
68 6
    protected function prefix($path)
69
    {
70 6
        return $this->root.ltrim($path, $this->separator);
71
    }
72
73
    /**
74
     * Set the finger print of the public key of the host you are connecting to.
75
     *
76
     * If the key does not match the server identification, the connection will
77
     * be aborted.
78
     *
79
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
80
     *
81
     * @return $this
82
     */
83 6
    public function setHostFingerprint($fingerprint)
84
    {
85 6
        $this->hostFingerprint = $fingerprint;
86
87 6
        return $this;
88
    }
89
90
    /**
91
     * Set the private key (string or path to local file).
92
     *
93
     * @param string $key
94
     *
95
     * @return $this
96
     */
97 9
    public function setPrivateKey($key)
98
    {
99 9
        $this->privatekey = $key;
100
101 9
        return $this;
102
    }
103
104
    /**
105
     * @param boolean $useAgent
106
     *
107
     * @return $this
108
     */
109
    public function setUseAgent($useAgent)
110
    {
111
        $this->useAgent = (bool) $useAgent;
112
113
        return $this;
114
    }
115
116
    /**
117
     * @param Agent $agent
118
     *
119
     * @return $this
120
     */
121
    public function setAgent(Agent $agent)
122
    {
123
        $this->agent = $agent;
124
125
        return $this;
126
    }
127
128
    /**
129
     * Set permissions for new directory
130
     *
131
     * @param int $directoryPerm
132
     *
133
     * @return $this
134
     */
135 6
    public function setDirectoryPerm($directoryPerm)
136
    {
137 6
        $this->directoryPerm = $directoryPerm;
138
139 6
        return $this;
140
    }
141
142
    /**
143
     * Get permissions for new directory
144
     *
145
     * @return int
146
     */
147 3
    public function getDirectoryPerm()
148
    {
149 3
        return $this->directoryPerm;
150
    }
151
152
    /**
153
     * Inject the SFTP instance.
154
     *
155
     * @param SFTP $connection
156
     *
157
     * @return $this
158
     */
159 30
    public function setNetSftpConnection(SFTP $connection)
160 2
    {
161 30
        $this->connection = $connection;
162
163 30
        return $this;
164
    }
165
166
    /**
167
     * Connect.
168
     */
169 21
    public function connect()
170
    {
171 21
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
172 21
        $this->login();
173 15
        $this->setConnectionRoot();
174 12
    }
175
176
    /**
177
     * Login.
178
     *
179
     * @throws LogicException
180
     */
181 21
    protected function login()
182
    {
183 21
        if ($this->hostFingerprint) {
184 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($this->connection->getServerPublicHostKey());
185
186 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
187 3
                throw new LogicException('The authenticity of host '.$this->host.' can\'t be established.');
188
            }
189 2
        }
190
191 18
        $authentication = $this->getAuthentication();
192
193 18
        if (! $this->connection->login($this->username, $authentication)) {
194 3
            throw new LogicException('Could not login with username: '.$this->username.', host: '.$this->host);
195
        }
196
197 15
        if ($authentication instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib\System\SSH\Agent does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
198
            $authentication->startSSHForwarding($this->connection);
199
        }
200 15
    }
201
202
    /**
203
     * Convert the SSH RSA public key into a hex formatted fingerprint.
204
     *
205
     * @param string $publickey
206
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
207
     */
208 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
209
    {
210 6
        $content = explode(' ', $publickey, 3);
211 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
212
    }
213
214
    /**
215
     * Set the connection root.
216
     */
217 15
    protected function setConnectionRoot()
218
    {
219 15
        $root = $this->getRoot();
220
221 15
        if (! $root) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null 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 9
            return;
223
        }
224
225 6
        if (! $this->connection->chdir($root)) {
226 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
227
        }
228 3
        $this->root = $this->connection->pwd() . $this->separator;
229 3
    }
230
231
    /**
232
     * Get the password, either the private key or a plain text password.
233
     *
234
     * @return Agent|RSA|string
235
     */
236 21
    public function getAuthentication()
237
    {
238 21
        if ($this->useAgent) {
239
            return $this->getAgent();
240
        }
241
242 21
        if ($this->privatekey) {
243 3
            return $this->getPrivateKey();
244
        }
245
246 18
        return $this->getPassword();
247
    }
248
249
    /**
250
     * Get the password, a plain text password.
251
     *
252
     * @return string
253
     */
254 18
    public function getPassword()
255
    {
256 18
        return $this->password;
257
    }
258
259
    /**
260
     * Get the private get with the password or private key contents.
261
     *
262
     * @return RSA
263
     */
264 9
    public function getPrivateKey()
265
    {
266 9
        if (@is_file($this->privatekey)) {
267 3
            $this->privatekey = file_get_contents($this->privatekey);
268 2
        }
269
270 9
        $key = new RSA();
271
272 9
        if ($this->password) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->password of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
273 9
            $key->setPassword($this->password);
274 6
        }
275
276 9
        $key->loadKey($this->privatekey);
277
278 9
        return $key;
279
    }
280
281
    /**
282
     * @return Agent|bool
283
     */
284
    public function getAgent()
285
    {
286
        if ( ! $this->agent instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib\System\SSH\Agent does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
287
            $this->agent = new Agent();
288
        }
289
290
        return $this->agent;
291
    }
292
293
    /**
294
     * List the contents of a directory.
295
     *
296
     * @param string $directory
297
     * @param bool   $recursive
298
     *
299
     * @return array
300
     */
301 6
    protected function listDirectoryContents($directory, $recursive = true)
302
    {
303 6
        $result = [];
304 6
        $connection = $this->getConnection();
305 6
        $location = $this->prefix($directory);
306 6
        $listing = $connection->rawlist($location);
307
308 6
        if ($listing === false) {
309 3
            return [];
310
        }
311
312 6
        foreach ($listing as $filename => $object) {
313 6
            if (in_array($filename, ['.', '..'])) {
314 3
                continue;
315
            }
316
317 6
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
318 6
            $result[] = $this->normalizeListingObject($path, $object);
319
320 6
            if ($recursive && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
321 4
                $result = array_merge($result, $this->listDirectoryContents($path));
322 2
            }
323 4
        }
324
325 6
        return $result;
326
    }
327
328
    /**
329
     * Normalize a listing response.
330
     *
331
     * @param string $path
332
     * @param array  $object
333
     *
334
     * @return array
335
     */
336 6
    protected function normalizeListingObject($path, array $object)
337
    {
338 6
        $permissions = $this->normalizePermissions($object['permissions']);
339 6
        $type = ($object['type'] === 1) ? 'file' : 'dir' ;
340 6
        $timestamp = $object['mtime'];
341
342 6
        if ($type === 'dir') {
343 6
            return compact('path', 'timestamp', 'type');
344
        }
345
346 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
347 6
        $size = (int) $object['size'];
348
349 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
350
    }
351
352
    /**
353
     * Disconnect.
354
     */
355 15
    public function disconnect()
356
    {
357 15
        $this->connection = null;
358 15
    }
359
360
    /**
361
     * @inheritdoc
362
     */
363 6
    public function write($path, $contents, Config $config)
364
    {
365 6
        if ($this->upload($path, $contents, $config) === false) {
366 6
            return false;
367
        }
368
369 6
        return compact('contents', 'visibility', 'path');
370
    }
371
372
    /**
373
     * @inheritdoc
374
     */
375 6
    public function writeStream($path, $resource, Config $config)
376
    {
377 6
        if ($this->upload($path, $resource, $config) === false) {
378 6
            return false;
379
        }
380
381 6
        return compact('visibility', 'path');
382
    }
383
384
    /**
385
     * Upload a file.
386
     *
387
     * @param string          $path
388
     * @param string|resource $contents
389
     * @param Config          $config
390
     * @return bool
391
     */
392 12
    public function upload($path, $contents, Config $config)
393
    {
394 12
        $connection = $this->getConnection();
395 12
        $this->ensureDirectory(Util::dirname($path));
396 12
        $config = Util::ensureConfig($config);
397
398 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
399 12
            return false;
400
        }
401
402 12
        if ($config && $visibility = $config->get('visibility')) {
403 6
            $this->setVisibility($path, $visibility);
404 4
        }
405
406 12
        return true;
407
    }
408
409
    /**
410
     * @inheritdoc
411
     */
412 6
    public function read($path)
413
    {
414 6
        $connection = $this->getConnection();
415
416 6
        if (($contents = $connection->get($path)) === false) {
417 6
            return false;
418
        }
419
420 6
        return compact('contents');
421
    }
422
423
    /**
424
     * @inheritdoc
425
     */
426 3
    public function readStream($path)
427
    {
428 3
        $stream = tmpfile();
429 3
        $connection = $this->getConnection();
430
431 3
        if ($connection->get($path, $stream) === false) {
432 3
            fclose($stream);
433 3
            return false;
434
        }
435
436 3
        rewind($stream);
437
438 3
        return compact('stream');
439
    }
440
441
    /**
442
     * @inheritdoc
443
     */
444 3
    public function update($path, $contents, Config $config)
445
    {
446 3
        return $this->write($path, $contents, $config);
447
    }
448
449
    /**
450
     * @inheritdoc
451
     */
452 3
    public function updateStream($path, $contents, Config $config)
453
    {
454 3
        return $this->writeStream($path, $contents, $config);
455
    }
456
457
    /**
458
     * @inheritdoc
459
     */
460 3
    public function delete($path)
461
    {
462 3
        $connection = $this->getConnection();
463
464 3
        return $connection->delete($path);
465
    }
466
467
    /**
468
     * @inheritdoc
469
     */
470 3
    public function rename($path, $newpath)
471
    {
472 3
        $connection = $this->getConnection();
473
474 3
        return $connection->rename($path, $newpath);
475
    }
476
477
    /**
478
     * @inheritdoc
479
     */
480 3
    public function deleteDir($dirname)
481
    {
482 3
        $connection = $this->getConnection();
483
484 3
        return $connection->delete($dirname, true);
485
    }
486
487
    /**
488
     * @inheritdoc
489
     */
490 39
    public function has($path)
491
    {
492 39
        return $this->getMetadata($path);
493
    }
494
495
    /**
496
     * @inheritdoc
497
     */
498 48
    public function getMetadata($path)
499
    {
500 48
        $connection = $this->getConnection();
501 48
        $info = $connection->stat($path);
502
503 48
        if ($info === false) {
504 12
            return false;
505
        }
506
507 39
        $result = Util::map($info, $this->statMap);
508 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
509 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
510
511 39
        return $result;
512
    }
513
514
    /**
515
     * @inheritdoc
516
     */
517 6
    public function getTimestamp($path)
518
    {
519 6
        return $this->getMetadata($path);
520 2
    }
521
522
    /**
523
     * @inheritdoc
524
     */
525 3
    public function getMimetype($path)
526
    {
527 3
        if (! $data = $this->read($path)) {
528 3
            return false;
529
        }
530
531 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
532
533 3
        return $data;
534
    }
535
536
    /**
537
     * @inheritdoc
538
     */
539 3
    public function createDir($dirname, Config $config)
540
    {
541 3
        $connection = $this->getConnection();
542
543 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
544 3
            return false;
545
        }
546
547 3
        return ['path' => $dirname];
548
    }
549
550
    /**
551
     * @inheritdoc
552
     */
553 6
    public function getVisibility($path)
554
    {
555 6
        return $this->getMetadata($path);
556
    }
557
558
    /**
559
     * @inheritdoc
560
     */
561 12
    public function setVisibility($path, $visibility)
562
    {
563 12
        $visibility = ucfirst($visibility);
564
565 12
        if (! isset($this->{'perm'.$visibility})) {
566 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
567
        }
568
569 9
        $connection = $this->getConnection();
570
571 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
572
    }
573
574
    /**
575
     * @inheritdoc
576
     */
577 6
    public function isConnected()
578
    {
579 6
        if ($this->connection instanceof SFTP && $this->connection->isConnected()) {
0 ignored issues
show
Bug introduced by
The class phpseclib\Net\SFTP does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
580 3
            return true;
581
        }
582
583 3
        return false;
584
    }
585
}
586