Completed
Pull Request — master (#113)
by
unknown
01:10
created

SftpAdapter::getPrivateKey()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.074

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
rs 9.8666
c 0
b 0
f 0
cc 4
nc 4
nop 0
crap 4.074
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 phpseclib3\Crypt\PublicKeyLoader;
12
use phpseclib3\Crypt\RSA;
13
use phpseclib3\Net\SFTP;
14
use phpseclib3\System\SSH\Agent;
15
16
class SftpAdapter extends AbstractFtpAdapter
17
{
18
    use StreamedCopyTrait;
19
20
    /**
21
     * @var SFTP
22
     */
23
    protected $connection;
24
25
    /**
26
     * @var int
27
     */
28
    protected $port = 22;
29
30
    /**
31
     * @var string
32
     */
33
    protected $hostFingerprint;
34
35
    /**
36
     * @var string
37
     */
38
    protected $privateKey;
39
40
    /**
41
     * @var bool
42
     */
43
    protected $useAgent = false;
44
45
    /**
46
     * @var Agent
47
     */
48
    private $agent;
49
50
    /**
51
     * @var array
52
     */
53
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'passphrase', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
54
55
    /**
56
     * @var array
57
     */
58
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
59
60
    /**
61
     * @var int
62
     */
63
    protected $directoryPerm = 0744;
64
65
    /**
66
     * @var string
67
     */
68
    private $passphrase;
69
70
    /**
71
     * Prefix a path.
72
     *
73
     * @param string $path
74
     *
75
     * @return string
76
     */
77 9
    protected function prefix($path)
78
    {
79 9
        return $this->root.ltrim($path, $this->separator);
80
    }
81
82
    /**
83
     * Set the finger print of the public key of the host you are connecting to.
84
     *
85
     * If the key does not match the server identification, the connection will
86
     * be aborted.
87
     *
88
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
89
     *
90
     * @return $this
91
     */
92 9
    public function setHostFingerprint($fingerprint)
93
    {
94 9
        $this->hostFingerprint = $fingerprint;
95
96 9
        return $this;
97
    }
98
99
    /**
100
     * Set the private key (string or path to local file).
101
     *
102
     * @param string $key
103
     *
104
     * @return $this
105
     */
106 12
    public function setPrivateKey($key)
107
    {
108 12
        $this->privateKey = $key;
109
110 12
        return $this;
111
    }
112
113
    /**
114
     * Set the passphrase for the privatekey.
115
     *
116
     * @param string $passphrase
117
     *
118
     * @return $this
119
     */
120 6
    public function setPassphrase($passphrase)
121
    {
122 6
        $this->passphrase = $passphrase;
123
124 6
        return $this;
125
    }
126
127
    /**
128
     * @param boolean $useAgent
129
     *
130
     * @return $this
131
     */
132
    public function setUseAgent($useAgent)
133
    {
134
        $this->useAgent = (bool) $useAgent;
135
136
        return $this;
137
    }
138
139
    /**
140
     * @param Agent $agent
141
     *
142
     * @return $this
143
     */
144
    public function setAgent(Agent $agent)
145
    {
146
        $this->agent = $agent;
147
148
        return $this;
149
    }
150
151
    /**
152
     * Set permissions for new directory
153
     *
154
     * @param int $directoryPerm
155
     *
156
     * @return $this
157
     */
158 6
    public function setDirectoryPerm($directoryPerm)
159
    {
160 6
        $this->directoryPerm = $directoryPerm;
161
162 6
        return $this;
163
    }
164
165
    /**
166
     * Get permissions for new directory
167
     *
168
     * @return int
169
     */
170 3
    public function getDirectoryPerm()
171
    {
172 3
        return $this->directoryPerm;
173
    }
174
175
    /**
176
     * Inject the SFTP instance.
177
     *
178
     * @param SFTP $connection
179
     *
180
     * @return $this
181
     */
182 36
    public function setNetSftpConnection(SFTP $connection)
183
    {
184 36
        $this->connection = $connection;
185
186 36
        return $this;
187
    }
188
189
    /**
190
     * Connect.
191
     */
192 27
    public function connect()
193
    {
194 27
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
195 27
        $this->connection->disableStatCache();
196 27
        $this->login();
197 18
        $this->setConnectionRoot();
198 15
    }
199
200
    /**
201
     * Login.
202
     *
203
     * @throws ConnectionErrorException
204
     */
205 27
    protected function login()
206
    {
207 27
        if ($this->hostFingerprint) {
208 9
            $publicKey = $this->connection->getServerPublicHostKey();
209
210 9
            if ($publicKey === false) {
211 3
                throw new ConnectionErrorException('Could not connect to server to verify public key.');
212
            }
213
214 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($publicKey);
215
216 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
217 3
                throw new ConnectionErrorException('The authenticity of host '.$this->host.' can\'t be established.');
218
            }
219
        }
220
221 21
        $authentication = $this->getAuthentication();
222
223
224 21
        if ($this->connection->login($this->getUsername(), $authentication)) {
225 15
            goto past_login;
226
        }
227
228
        // try double authentication, key is already given so now give password
229 6
        if ($authentication instanceof RSA && $this->connection->login($this->getUsername(), $this->getPassword())) {
0 ignored issues
show
Bug introduced by
The class phpseclib3\Crypt\RSA 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...
230 3
            goto past_login;
231
        }
232
233 3
        throw new ConnectionErrorException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
234
235
        past_login:
236
237 18
        if ($authentication instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib3\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...
238
            $authentication->startSSHForwarding($this->connection);
239
        }
240 18
    }
241
242
    /**
243
     * Convert the SSH RSA public key into a hex formatted fingerprint.
244
     *
245
     * @param string $publickey
246
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
247
     */
248 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
249
    {
250 6
        $content = explode(' ', $publickey, 3);
251 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
252
    }
253
254
    /**
255
     * Set the connection root.
256
     *
257
     * @throws InvalidRootException
258
     */
259 18
    protected function setConnectionRoot()
260
    {
261 18
        $root = $this->getRoot();
262
263 18
        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...
264 12
            return;
265
        }
266
267 6
        if (! $this->connection->chdir($root)) {
268 3
            throw new InvalidRootException('Root is invalid or does not exist: '.$root);
269
        }
270
271 3
        $this->setRoot($this->connection->pwd());
272 3
    }
273
274
    /**
275
     * Get the password, either the private key or a plain text password.
276
     *
277
     * @return Agent|RSA|string
278
     */
279 24
    public function getAuthentication()
280
    {
281 24
        if ($this->useAgent) {
282
            return $this->getAgent();
283
        }
284
285 24
        if ($this->privateKey) {
286 6
            return $this->getPrivateKey();
287
        }
288
289 18
        return $this->getPassword();
290
    }
291
292
    /**
293
     * Get the private key with the password or private key contents.
294
     *
295
     * @return RSA
296
     */
297 12
    public function getPrivateKey()
298
    {
299 12
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
300 12
            $this->privateKey = file_get_contents($this->privateKey);
301
        }
302
303 12
        if ($password = $this->getPassphrase()) {
304 12
            return PublicKeyLoader::load($this->privateKey, $password);
305
        }
306
307
        return PublicKeyLoader::load($this->privateKey);
308
    }
309
310
    /**
311
     * @return string
312
     */
313 21
    public function getPassphrase()
314
    {
315 21
        if ($this->passphrase === null) {
316
            //Added for backward compatibility
317 15
            return $this->getPassword();
318
        }
319 6
        return $this->passphrase;
320
    }
321
322
    /**
323
     * @return Agent|bool
324
     */
325
    public function getAgent()
326
    {
327
        if ( ! $this->agent instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib3\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...
328
            $this->agent = new Agent();
329
        }
330
331
        return $this->agent;
332
    }
333
334
    /**
335
     * List the contents of a directory.
336
     *
337
     * @param string $directory
338
     * @param bool   $recursive
339
     *
340
     * @return array
341
     */
342 9
    protected function listDirectoryContents($directory, $recursive = true)
343
    {
344 9
        $result = [];
345 9
        $connection = $this->getConnection();
346 9
        $location = $this->prefix($directory);
347 9
        $listing = $connection->rawlist($location);
348
349 9
        if ($listing === false) {
350 3
            return [];
351
        }
352
353 9
        foreach ($listing as $filename => $object) {
354
            // When directory entries have a numeric filename they are changed to int
355 9
            $filename = (string) $filename;
356 9
            if (in_array($filename, ['.', '..'])) {
357 3
                continue;
358
            }
359
360 9
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
361 9
            $result[] = $this->normalizeListingObject($path, $object);
362
363 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
364 7
                $result = array_merge($result, $this->listDirectoryContents($path));
365
            }
366
        }
367
368 9
        return $result;
369
    }
370
371
    /**
372
     * Normalize a listing response.
373
     *
374
     * @param string $path
375
     * @param array  $object
376
     *
377
     * @return array
378
     */
379 9
    protected function normalizeListingObject($path, array $object)
380
    {
381 9
        $permissions = $this->normalizePermissions($object['permissions']);
382 9
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
383
384 9
        $timestamp = $object['mtime'];
385
386 9
        if ($type === 'dir') {
387 9
            return compact('path', 'timestamp', 'type');
388
        }
389
390 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
391 6
        $size = (int) $object['size'];
392
393 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
394
    }
395
396
    /**
397
     * Disconnect.
398
     */
399 18
    public function disconnect()
400
    {
401 18
        $this->connection = null;
402 18
    }
403
404
    /**
405
     * @inheritdoc
406
     */
407 6
    public function write($path, $contents, Config $config)
408
    {
409 6
        if ($this->upload($path, $contents, $config) === false) {
410 6
            return false;
411
        }
412
413 6
        return compact('contents', 'path');
414
    }
415
416
    /**
417
     * @inheritdoc
418
     */
419 6
    public function writeStream($path, $resource, Config $config)
420
    {
421 6
        if ($this->upload($path, $resource, $config) === false) {
422 6
            return false;
423
        }
424
425 6
        return compact('path');
426
    }
427
428
    /**
429
     * Upload a file.
430
     *
431
     * @param string          $path
432
     * @param string|resource $contents
433
     * @param Config          $config
434
     * @return bool
435
     */
436 12
    public function upload($path, $contents, Config $config)
437
    {
438 12
        $connection = $this->getConnection();
439 12
        $this->ensureDirectory(Util::dirname($path));
440 12
        $config = Util::ensureConfig($config);
441
442 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
443 12
            return false;
444
        }
445
446 12
        if ($config && $visibility = $config->get('visibility')) {
447 6
            $this->setVisibility($path, $visibility);
448
        }
449
450 12
        return true;
451
    }
452
453
    /**
454
     * @inheritdoc
455
     */
456 6
    public function read($path)
457
    {
458 6
        $connection = $this->getConnection();
459
460 6
        if (($contents = $connection->get($path)) === false) {
461 6
            return false;
462
        }
463
464 6
        return compact('contents', 'path');
465
    }
466
467
    /**
468
     * @inheritdoc
469
     */
470 3
    public function readStream($path)
471
    {
472 3
        $stream = tmpfile();
473 3
        $connection = $this->getConnection();
474
475 3
        if ($connection->get($path, $stream) === false) {
476 3
            fclose($stream);
477 3
            return false;
478
        }
479
480 3
        rewind($stream);
481
482 3
        return compact('stream', 'path');
483
    }
484
485
    /**
486
     * @inheritdoc
487
     */
488 3
    public function update($path, $contents, Config $config)
489
    {
490 3
        return $this->write($path, $contents, $config);
491
    }
492
493
    /**
494
     * @inheritdoc
495
     */
496 3
    public function updateStream($path, $contents, Config $config)
497
    {
498 3
        return $this->writeStream($path, $contents, $config);
499
    }
500
501
    /**
502
     * @inheritdoc
503
     */
504 3
    public function delete($path)
505
    {
506 3
        $connection = $this->getConnection();
507
508 3
        return $connection->delete($path);
509
    }
510
511
    /**
512
     * @inheritdoc
513
     */
514 3
    public function rename($path, $newpath)
515
    {
516 3
        $connection = $this->getConnection();
517
518 3
        return $connection->rename($path, $newpath);
519
    }
520
521
    /**
522
     * @inheritdoc
523
     */
524 3
    public function deleteDir($dirname)
525
    {
526 3
        $connection = $this->getConnection();
527
528 3
        return $connection->delete($dirname, true);
529
    }
530
531
    /**
532
     * @inheritdoc
533
     */
534 42
    public function has($path)
535
    {
536 42
        return $this->getMetadata($path);
537
    }
538
539
    /**
540
     * @inheritdoc
541
     */
542 50
    public function getMetadata($path)
543
    {
544 50
        $connection = $this->getConnection();
545 50
        $info = $connection->stat($path);
546
547 50
        if ($info === false) {
548 12
            return false;
549
        }
550
551 41
        $result = Util::map($info, $this->statMap);
552 41
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
553 41
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
554
555 41
        return $result;
556
    }
557
558
    /**
559
     * @inheritdoc
560
     */
561 6
    public function getTimestamp($path)
562
    {
563 6
        return $this->getMetadata($path);
564
    }
565
566
    /**
567
     * @inheritdoc
568
     */
569 3
    public function getMimetype($path)
570
    {
571 3
        if (! $data = $this->read($path)) {
572 3
            return false;
573
        }
574
575 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
576
577 3
        return $data;
578
    }
579
580
    /**
581
     * @inheritdoc
582
     */
583 3
    public function createDir($dirname, Config $config)
584
    {
585 3
        $connection = $this->getConnection();
586
587 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
588 3
            return false;
589
        }
590
591 3
        return ['path' => $dirname];
592
    }
593
594
    /**
595
     * @inheritdoc
596
     */
597 6
    public function getVisibility($path)
598
    {
599 6
        return $this->getMetadata($path);
600
    }
601
602
    /**
603
     * @inheritdoc
604
     */
605 12
    public function setVisibility($path, $visibility)
606
    {
607 12
        $visibility = ucfirst($visibility);
608
609 12
        if (! isset($this->{'perm'.$visibility})) {
610 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
611
        }
612
613 9
        $connection = $this->getConnection();
614
615 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
616
    }
617
618
    /**
619
     * @inheritdoc
620
     */
621 31
    public function isConnected()
622
    {
623 31
        if ($this->connection instanceof SFTP && $this->connection->isConnected()) {
0 ignored issues
show
Bug introduced by
The class phpseclib3\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...
624 28
            return true;
625
        }
626
627 3
        return false;
628
    }
629
}
630