├── .noninteractive ├── .gitattributes ├── test ├── TModule │ ├── _Data │ │ ├── file_write.txt │ │ └── file_read.txt │ └── SSH2Test.php ├── Callback.php ├── _Simulation │ ├── EventCollection.php │ ├── Event.php │ ├── SimulationInterface.php │ └── Simulation.php ├── bootstrap.php ├── TUnit │ ├── Auth │ │ ├── SSH2AgentTest.php │ │ ├── SSH2NoneTest.php │ │ ├── SSH2PasswordTest.php │ │ ├── SSH2PublicKeyFileTest.php │ │ └── SSH2HostBasedFileTest.php │ ├── SSH2ConfigTest.php │ ├── Driver │ │ ├── SftpTest.php │ │ └── ShellTest.php │ └── SSH2Test.php ├── TModule.php └── TUnit.php ├── example ├── _file_read.txt ├── _file_write.txt ├── ssh_async_single_command.php ├── sftp_async_read.php ├── sftp_async_write.php └── ssh_async_multi_command.php ├── .gitignore ├── CHANGELOG.md ├── src └── SSH │ ├── SSH2ResourceInterface.php │ ├── SSH2AuthInterface.php │ ├── SSH2ConfigInterface.php │ ├── Auth │ ├── SSH2None.php │ ├── SSH2Agent.php │ ├── SSH2Password.php │ ├── SSH2PublicKeyFile.php │ └── SSH2HostBasedFile.php │ ├── SSH2Config.php │ ├── SSH2DriverInterface.php │ ├── SSH2Interface.php │ ├── SSH2.php │ └── Driver │ ├── Sftp.php │ ├── Sftp │ └── SftpResource.php │ ├── Shell │ └── ShellResource.php │ └── Shell.php ├── phpunit.xml ├── LICENSE ├── .travis.yml ├── CONTRIBUTING.md ├── composer.json ├── .scrutinizer.yml └── README.md /.noninteractive: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /test/TModule/_Data/file_write.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/_file_read.txt: -------------------------------------------------------------------------------- 1 | DAZZLE 2 | IS 3 | AWESOME! 4 | -------------------------------------------------------------------------------- /example/_file_write.txt: -------------------------------------------------------------------------------- 1 | DAZZLE 2 | IS 3 | AWESOME! 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | composer.phar -------------------------------------------------------------------------------- /test/TModule/_Data/file_read.txt: -------------------------------------------------------------------------------- 1 | DAZZLE 2 | IS 3 | AWESOME 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | This changelog references the relevant changes, bug and security fixes done. 4 | -------------------------------------------------------------------------------- /test/Callback.php: -------------------------------------------------------------------------------- 1 | username = $username; 23 | } 24 | 25 | /** 26 | * @override 27 | * @inheritDoc 28 | */ 29 | public function authenticate($conn) 30 | { 31 | return true === @ssh2_auth_none($conn, $this->username); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/_Simulation/Event.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->data = $data; 25 | } 26 | 27 | /** 28 | * @return mixed 29 | */ 30 | public function name() 31 | { 32 | return $this->name; 33 | } 34 | 35 | /** 36 | * @return mixed[] 37 | */ 38 | public function data() 39 | { 40 | return $this->data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/SSH/Auth/SSH2Agent.php: -------------------------------------------------------------------------------- 1 | username = $username; 25 | } 26 | 27 | /** 28 | * @override 29 | * @inheritDoc 30 | */ 31 | public function authenticate($conn) 32 | { 33 | return @ssh2_auth_agent( 34 | $conn, 35 | $this->username 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/TUnit/Auth/SSH2AgentTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SSH2AuthInterface::class, $auth); 20 | $this->assertAttributeEquals($user, 'username', $auth); 21 | } 22 | 23 | /** 24 | * 25 | */ 26 | public function testDestructor_DoesNotThrowException() 27 | { 28 | $user = 'user'; 29 | $auth = new SSH2Agent($user); 30 | unset($auth); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/TUnit/Auth/SSH2NoneTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SSH2AuthInterface::class, $auth); 20 | $this->assertAttributeEquals($user, 'username', $auth); 21 | } 22 | 23 | /** 24 | * 25 | */ 26 | public function testDestructor_DoesNotThrowException() 27 | { 28 | $user = 'user'; 29 | $auth = new SSH2None($user); 30 | unset($auth); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SSH/Auth/SSH2Password.php: -------------------------------------------------------------------------------- 1 | username = $username; 29 | $this->password = $password; 30 | } 31 | 32 | /** 33 | * @override 34 | * @inheritDoc 35 | */ 36 | public function authenticate($conn) 37 | { 38 | return @ssh2_auth_password($conn, $this->username, $this->password); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/TUnit/Auth/SSH2PasswordTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SSH2AuthInterface::class, $auth); 21 | $this->assertAttributeEquals($user, 'username', $auth); 22 | $this->assertAttributeEquals($pass, 'password', $auth); 23 | } 24 | 25 | /** 26 | * 27 | */ 28 | public function testDestructor_DoesNotThrowException() 29 | { 30 | $user = 'user'; 31 | $pass = 'pass'; 32 | $auth = new SSH2Password($user, $pass); 33 | unset($auth); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test/TModule 21 | 22 | 23 | 24 | test/TUnit 25 | 26 | 27 | 28 | 29 | 30 | src 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Kamil Jamróz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/SSH/SSH2Config.php: -------------------------------------------------------------------------------- 1 | host = $host; 30 | $this->port = $port; 31 | $this->methods = $methods; 32 | } 33 | 34 | /** 35 | * @override 36 | * @inheritDoc 37 | */ 38 | public function getHost() 39 | { 40 | return $this->host; 41 | } 42 | 43 | /** 44 | * @override 45 | * @inheritDoc 46 | */ 47 | public function getPort() 48 | { 49 | return $this->port; 50 | } 51 | 52 | /** 53 | * @override 54 | * @inheritDoc 55 | */ 56 | public function getMethods() 57 | { 58 | return $this->methods; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | sudo: required 5 | 6 | php: 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | 11 | before_install: 12 | - sudo ln -s /home/travis/.phpenv/versions/$(phpenv version-name)/bin/phpize /usr/bin/ 13 | - sudo ln -s /home/travis/.phpenv/versions/$(phpenv version-name)/bin/php-config /usr/bin/ 14 | - export PHP_MAJOR="$(echo $TRAVIS_PHP_VERSION | cut -d '.' -f 1,2)" 15 | 16 | install: 17 | - sudo apt-get -qq update 18 | - bash build-ci/install_prereqs_$PHP_MAJOR.sh 19 | - travis_retry composer self-update 20 | - travis_retry composer install --prefer-source --no-interaction 21 | - php -m 22 | 23 | before_script: 24 | - ulimit -c unlimited -S || true 25 | - echo '/tmp/core_%e.%p' | sudo tee /proc/sys/kernel/core_pattern &> /dev/null 26 | 27 | script: 28 | - vendor/bin/phpunit -d memory_limit=1024M --coverage-text --coverage-clover=coverage.clover 29 | 30 | after_script: 31 | - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi 32 | - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 33 | 34 | after_failure: 35 | - bash build-ci/install_failure.sh 36 | 37 | addons: 38 | apt: 39 | packages: 40 | - gdb 41 | -------------------------------------------------------------------------------- /test/_Simulation/SimulationInterface.php: -------------------------------------------------------------------------------- 1 | vendor/bin/phpunit 19 | ``` 20 | -------------------------------------------------------------------------------- /test/TUnit/Auth/SSH2PublicKeyFileTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SSH2AuthInterface::class, $auth); 24 | $this->assertAttributeEquals($user, 'username', $auth); 25 | $this->assertAttributeEquals($publicKey, 'publicKeyFile', $auth); 26 | $this->assertAttributeEquals($privateKey, 'privateKeyFile', $auth); 27 | $this->assertAttributeEquals($passPhrase, 'passPhrase', $auth); 28 | } 29 | 30 | /** 31 | * 32 | */ 33 | public function testDestructor_DoesNotThrowException() 34 | { 35 | $user = 'user'; 36 | $publicKey = 'path/public.key'; 37 | $privateKey = 'path/private.key'; 38 | $passPhrase = 'passPhrase'; 39 | 40 | $auth = new SSH2PublicKeyFile($user, $publicKey, $privateKey, $passPhrase); 41 | unset($auth); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dazzle-php/ssh", 3 | "description": "Dazzle Asynchronous SSH.", 4 | "keywords": [ 5 | "dazzle", "dazzle-php", "async", "asynchronous", "ssh", "ssh2", "driver" 6 | ], 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/dazzle-php/ssh/issues", 10 | "source": "https://github.com/dazzle-php/ssh" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Kamil Jamroz", 15 | "homepage": "https://github.com/khelle" 16 | }, 17 | { 18 | "name": "The contributors", 19 | "homepage": "http://github.com/dazzle-php/ssh/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.6.7", 24 | "dazzle-php/event": "0.5.*", 25 | "dazzle-php/loop": "0.5.*", 26 | "dazzle-php/stream": "0.5.*", 27 | "dazzle-php/throwable": "0.5.*", 28 | "dazzle-php/util": "0.5.*" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": ">=4.8.0 <5.4.0" 32 | }, 33 | "suggest": { 34 | "ext-ssh2": "*" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Dazzle\\SSH\\": "src/SSH", 39 | "Dazzle\\SSH\\Test\\": "test" 40 | } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "0.5-dev" 45 | } 46 | }, 47 | "minimum-stability": "dev", 48 | "prefer-stable": true 49 | } 50 | -------------------------------------------------------------------------------- /src/SSH/SSH2DriverInterface.php: -------------------------------------------------------------------------------- 1 | username = $username; 41 | $this->publicKeyFile = $publicKeyFile; 42 | $this->privateKeyFile = $privateKeyFile; 43 | $this->passPhrase = $passPhrase; 44 | } 45 | 46 | /** 47 | * @override 48 | * @inheritDoc 49 | */ 50 | public function authenticate($conn) 51 | { 52 | return @ssh2_auth_pubkey_file( 53 | $conn, 54 | $this->username, 55 | $this->publicKeyFile, 56 | $this->privateKeyFile, 57 | $this->passPhrase 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SSH/SSH2Interface.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SSH2HostBasedFile::class, $auth); 25 | $this->assertAttributeEquals($user, 'username', $auth); 26 | $this->assertAttributeEquals($host, 'hostname', $auth); 27 | $this->assertAttributeEquals($publicKey, 'publicKeyFile', $auth); 28 | $this->assertAttributeEquals($privateKey, 'privateKeyFile', $auth); 29 | $this->assertAttributeEquals($passPhrase, 'passPhrase', $auth); 30 | $this->assertAttributeEquals($localUser, 'localUsername', $auth); 31 | } 32 | 33 | /** 34 | * 35 | */ 36 | public function testDestructor_DoesNotThrowThrowable() 37 | { 38 | $user = 'user'; 39 | $host = 'example.com'; 40 | $publicKey = 'path/public.key'; 41 | $privateKey = 'path/private.key'; 42 | $passPhrase = 'passPhrase'; 43 | $localUser = 'localUser'; 44 | 45 | $auth = new SSH2HostBasedFile($user, $host, $publicKey, $privateKey, $passPhrase, $localUser); 46 | unset($auth); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/TUnit/SSH2ConfigTest.php: -------------------------------------------------------------------------------- 1 | assertAttributeEquals('localhost', 'host', $config); 18 | $this->assertAttributeEquals(22, 'port', $config); 19 | $this->assertAttributeEquals([], 'methods', $config); 20 | } 21 | 22 | /** 23 | * 24 | */ 25 | public function testConstructor_UsesPassedArguments() 26 | { 27 | $config = new SSH2Config($host = 'A', $port = 50, $methods = [ 'method' => 'option' ]); 28 | 29 | $this->assertAttributeEquals($host, 'host', $config); 30 | $this->assertAttributeEquals($port, 'port', $config); 31 | $this->assertAttributeEquals($methods, 'methods', $config); 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | public function testDestructor_DoesNotThrowThrowable() 38 | { 39 | $config = new SSH2Config(); 40 | unset($config); 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | public function testApiGetHost_ReturnsHost() 47 | { 48 | $config = new SSH2Config($host = 'A'); 49 | $this->assertSame($host, $config->getHost()); 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | public function testApiGetPort_ReturnsPort() 56 | { 57 | $config = new SSH2Config('A', $port = 50); 58 | $this->assertSame($port, $config->getPort()); 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | public function testApiGetMethods_ReturnsMethods() 65 | { 66 | $config = new SSH2Config('A', 50, $methods = [ 'something' => 'value' ]); 67 | $this->assertSame($methods, $config->getMethods()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/SSH/Auth/SSH2HostBasedFile.php: -------------------------------------------------------------------------------- 1 | username = $username; 53 | $this->hostname = $hostname; 54 | $this->publicKeyFile = $publicKeyFile; 55 | $this->privateKeyFile = $privateKeyFile; 56 | $this->passPhrase = $passPhrase; 57 | $this->localUsername = $localUsername; 58 | } 59 | 60 | /** 61 | * @override 62 | * @inheritDoc 63 | */ 64 | public function authenticate($conn) 65 | { 66 | return @ssh2_auth_hostbased_file( 67 | $conn, 68 | $this->username, 69 | $this->hostname, 70 | $this->publicKeyFile, 71 | $this->privateKeyFile, 72 | $this->passPhrase, 73 | $this->localUsername 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/ssh_async_single_command.php: -------------------------------------------------------------------------------- 1 | on('connect:shell', function(SSH2DriverInterface $shell) use($ssh2, $loop) { 31 | echo "# CONNECTED SHELL\n"; 32 | 33 | $buffer = ''; 34 | $command = $shell->open(); 35 | $command->write('ls -la'); 36 | $command->on('data', function(SSH2ResourceInterface $command, $data) use(&$buffer) { 37 | $buffer .= $data; 38 | }); 39 | $command->on('end', function(SSH2ResourceInterface $command) use(&$buffer) { 40 | echo "# COMMAND RETURNED:\n"; 41 | echo $buffer; 42 | $command->close(); 43 | }); 44 | $command->on('close', function(SSH2ResourceInterface $command) use($shell) { 45 | $shell->disconnect(); 46 | }); 47 | }); 48 | 49 | $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($ssh2) { 50 | echo "# DISCONNECTED SHELL\n"; 51 | $ssh2->disconnect(); 52 | }); 53 | 54 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 55 | echo "# CONNECTED\n"; 56 | $ssh2->createDriver(SSH2::DRIVER_SHELL) 57 | ->connect(); 58 | }); 59 | 60 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 61 | echo "# DISCONNECTED\n"; 62 | $loop->stop(); 63 | }); 64 | 65 | $loop->onTick(function() use($ssh2) { 66 | $ssh2->connect(); 67 | }); 68 | 69 | $loop->start(); 70 | -------------------------------------------------------------------------------- /example/sftp_async_read.php: -------------------------------------------------------------------------------- 1 | on('connect:sftp', function(SSH2DriverInterface $sftp) use($loop, $ssh2) { 31 | echo "# CONNECTED SFTP\n"; 32 | 33 | $buffer = ''; 34 | $file = $sftp->open(__DIR__ . '/_file_read.txt', 'r+'); 35 | $file->read(); 36 | $file->on('data', function(SSH2ResourceInterface $file, $data) use(&$buffer) { 37 | $buffer .= $data; 38 | }); 39 | $file->on('end', function(SSH2ResourceInterface $file) use(&$buffer) { 40 | echo "# FOLLOWING LINES WERE READ FROM FILE:\n"; 41 | echo $buffer; 42 | $file->close(); 43 | }); 44 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 45 | echo "# FILE HAS BEEN CLOSED\n"; 46 | $sftp->disconnect(); 47 | }); 48 | }); 49 | 50 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($ssh2) { 51 | echo "# DISCONNECTED SFTP\n"; 52 | $ssh2->disconnect(); 53 | }); 54 | 55 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 56 | echo "# CONNECTED\n"; 57 | $ssh2->createDriver(SSH2::DRIVER_SFTP) 58 | ->connect(); 59 | }); 60 | 61 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 62 | echo "# DISCONNECTED\n"; 63 | $loop->stop(); 64 | }); 65 | 66 | $loop->onTick(function() use($ssh2) { 67 | $ssh2->connect(); 68 | }); 69 | 70 | $loop->start(); 71 | -------------------------------------------------------------------------------- /example/sftp_async_write.php: -------------------------------------------------------------------------------- 1 | on('connect:sftp', function(SSH2DriverInterface $sftp) use($loop, $ssh2) { 31 | echo "# CONNECTED SFTP\n"; 32 | 33 | $lines = [ "DAZZLE\n", "IS\n", "AWESOME!\n" ]; 34 | $linesPointer = 0; 35 | 36 | $file = $sftp->open(__DIR__ . '/_file_write.txt', 'w+'); 37 | $file->write(); 38 | $file->on('drain', function(SSH2ResourceInterface $file) use(&$lines, &$linesPointer) { 39 | echo "# PART OF THE DATA HAS BEEN WRITTEN\n"; 40 | if ($linesPointer < count($lines)) { 41 | $file->write($lines[$linesPointer++]); 42 | } 43 | }); 44 | $file->on('finish', function(SSH2ResourceInterface $file) { 45 | echo "# FINISHED WRITING\n"; 46 | $file->close(); 47 | }); 48 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 49 | echo "# FILE HAS BEEN CLOSED\n"; 50 | $sftp->disconnect(); 51 | }); 52 | }); 53 | 54 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($ssh2) { 55 | echo "# DISCONNECTED SFTP\n"; 56 | $ssh2->disconnect(); 57 | }); 58 | 59 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 60 | echo "# CONNECTED\n"; 61 | $ssh2->createDriver(SSH2::DRIVER_SFTP) 62 | ->connect(); 63 | }); 64 | 65 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 66 | echo "# DISCONNECTED\n"; 67 | $loop->stop(); 68 | }); 69 | 70 | $loop->onTick(function() use($ssh2) { 71 | $ssh2->connect(); 72 | }); 73 | 74 | $loop->start(); 75 | -------------------------------------------------------------------------------- /example/ssh_async_multi_command.php: -------------------------------------------------------------------------------- 1 | open(); 26 | $command->write($shellCommand); 27 | $command->on('data', function(SSH2ResourceInterface $command, $data) use(&$buffer) { 28 | $buffer .= $data; 29 | }); 30 | $command->on('end', function(SSH2ResourceInterface $command) use(&$buffer) { 31 | echo "# COMMAND RETURNED:\n"; 32 | echo $buffer; 33 | $command->close(); 34 | }); 35 | } 36 | 37 | $user = getenv('TEST_USER') ? getenv('TEST_USER') : 'Dazzle'; 38 | $pass = getenv('TEST_PASS') ? getenv('TEST_PASS') : 'Dazzle-1234'; 39 | 40 | $loop = new Loop(new SelectLoop); 41 | $auth = new SSH2Password($user, $pass); 42 | $config = new SSH2Config(); 43 | $ssh2 = new SSH2($auth, $config, $loop); 44 | 45 | $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($ssh2, $loop) { 46 | echo "# CONNECTED SHELL\n"; 47 | 48 | $commands = [ 'ls -la', 'pwd' ]; 49 | $commandsRemain = count($commands); 50 | 51 | $shell->on('resource:close', function(SSH2DriverInterface $shell) use(&$commandsRemain) { 52 | if (--$commandsRemain === 0) { 53 | $shell->disconnect(); 54 | } 55 | }); 56 | 57 | foreach ($commands as $command) { 58 | executeCommand($shell, $command); 59 | } 60 | }); 61 | 62 | $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($ssh2) { 63 | echo "# DISCONNECTED SHELL\n"; 64 | $ssh2->disconnect(); 65 | }); 66 | 67 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 68 | echo "# CONNECTED\n"; 69 | $ssh2->createDriver(SSH2::DRIVER_SHELL) 70 | ->connect(); 71 | }); 72 | 73 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 74 | echo "# DISCONNECTED\n"; 75 | $loop->stop(); 76 | }); 77 | 78 | $loop->onTick(function() use($ssh2) { 79 | $ssh2->connect(); 80 | }); 81 | 82 | $loop->start(); 83 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - 'build/*' 4 | - 'build-ci/*' 5 | - 'example/*' 6 | - 'example-bench/*' 7 | - 'test/*' 8 | - 'vendor/*' 9 | 10 | checks: 11 | php: 12 | return_doc_comments: true 13 | remove_extra_empty_lines: true 14 | 15 | coding_style: 16 | php: 17 | indentation: 18 | general: 19 | use_tabs: false 20 | size: 4 21 | switch: 22 | indent_case: true 23 | spaces: 24 | general: 25 | linefeed_character: newline 26 | before_parentheses: 27 | function_declaration: false 28 | closure_definition: false 29 | function_call: false 30 | if: true 31 | for: true 32 | while: true 33 | switch: true 34 | catch: true 35 | array_initializer: false 36 | around_operators: 37 | assignment: true 38 | logical: true 39 | equality: true 40 | relational: true 41 | bitwise: true 42 | additive: true 43 | multiplicative: true 44 | shift: true 45 | unary_additive: false 46 | concatenation: true 47 | negation: false 48 | before_left_brace: 49 | class: true 50 | function: true 51 | if: true 52 | else: true 53 | for: true 54 | while: true 55 | do: true 56 | switch: true 57 | try: true 58 | catch: true 59 | finally: true 60 | before_keywords: 61 | else: true 62 | while: true 63 | catch: true 64 | finally: true 65 | within: 66 | brackets: false 67 | array_initializer: false 68 | grouping: false 69 | function_call: false 70 | function_declaration: false 71 | if: false 72 | for: false 73 | while: false 74 | switch: false 75 | catch: false 76 | type_cast: false 77 | ternary_operator: 78 | before_condition: true 79 | after_condition: true 80 | before_alternative: true 81 | after_alternative: true 82 | in_short_version: false 83 | other: 84 | before_comma: false 85 | after_comma: true 86 | before_semicolon: false 87 | after_semicolon: true 88 | after_type_cast: true 89 | braces: 90 | classes_functions: 91 | class: new-line 92 | function: new-line 93 | closure: end-of-line 94 | if: 95 | opening: new-line 96 | always: false 97 | else_on_new_line: true 98 | for: 99 | opening: new-line 100 | always: true 101 | while: 102 | opening: new-line 103 | always: true 104 | do_while: 105 | opening: new-line 106 | always: true 107 | while_on_new_line: true 108 | switch: 109 | opening: new-line 110 | try: 111 | opening: new-line 112 | catch_on_new_line: true 113 | finally_on_new_line: true 114 | upper_lower_casing: 115 | keywords: 116 | general: lower 117 | constants: 118 | true_false_null: lower 119 | 120 | tools: 121 | external_code_coverage: 122 | timeout: 1800 123 | runs: 1 124 | php_code_coverage: false -------------------------------------------------------------------------------- /test/TModule.php: -------------------------------------------------------------------------------- 1 | loop = null; 46 | $this->sim = null; 47 | } 48 | 49 | /** 50 | * 51 | */ 52 | public function tearDown() 53 | { 54 | unset($this->sim); 55 | unset($this->loop); 56 | } 57 | 58 | /** 59 | * @return LoopInterface|null 60 | */ 61 | public function getLoop() 62 | { 63 | return $this->loop; 64 | } 65 | 66 | /** 67 | * Run test scenario as simulation. 68 | * 69 | * @param callable(Simulation) $scenario 70 | * @return TModule 71 | */ 72 | public function simulate(callable $scenario) 73 | { 74 | try 75 | { 76 | $this->loop = new Loop(new SelectLoop); 77 | $this->loop->erase(true); 78 | 79 | $this->sim = new Simulation($this->loop); 80 | $this->sim->setScenario($scenario); 81 | $this->sim->begin(); 82 | } 83 | catch (Exception $ex) 84 | { 85 | $this->fail($ex->getMessage()); 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param $events 93 | * @param int $flags 94 | * @return TModule 95 | */ 96 | public function expect($events, $flags = Simulation::EVENTS_COMPARE_IN_ORDER) 97 | { 98 | $expectedEvents = []; 99 | 100 | foreach ($events as $event) 101 | { 102 | $data = isset($event[1]) ? $event[1] : []; 103 | $expectedEvents[] = new Event($event[0], $data); 104 | } 105 | 106 | $this->assertEvents( 107 | $this->sim->getExpectations(), 108 | $expectedEvents, 109 | $flags 110 | ); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param Event[] $actualEvents 117 | * @param Event[] $expectedEvents 118 | * @param int $flags 119 | */ 120 | public function assertEvents($actualEvents = [], $expectedEvents = [], $flags = Simulation::EVENTS_COMPARE_IN_ORDER) 121 | { 122 | $count = max(count($actualEvents), count($expectedEvents)); 123 | 124 | if ($flags === Simulation::EVENTS_COMPARE_RANDOMLY) 125 | { 126 | sort($actualEvents); 127 | sort($expectedEvents); 128 | } 129 | 130 | for ($i=0; $i<$count; $i++) 131 | { 132 | if (!isset($actualEvents[$i])) 133 | { 134 | $this->fail( 135 | sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, $expectedEvents[$i]->name(), 'null') 136 | ); 137 | } 138 | else if (!isset($expectedEvents[$i])) 139 | { 140 | $this->fail( 141 | sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, 'null', $actualEvents[$i]->name()) 142 | ); 143 | } 144 | 145 | $actualEvent = $actualEvents[$i]; 146 | $expectedEvent = $expectedEvents[$i]; 147 | 148 | $this->assertSame( 149 | $expectedEvent->name(), 150 | $actualEvent->name(), 151 | sprintf(self::MSG_EVENT_NAME_ASSERTION_FAILED, $i) 152 | ); 153 | $this->assertSame( 154 | $expectedEvent->data(), 155 | $actualEvent->data(), 156 | sprintf(self::MSG_EVENT_DATA_ASSERTION_FAILED, $i) 157 | ); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/TUnit.php: -------------------------------------------------------------------------------- 1 | exactly(2); 34 | } 35 | 36 | /** 37 | * Creates a callback that must be called $amount times or the test will fail. 38 | * 39 | * @param $amount 40 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 41 | */ 42 | public function expectCallableExactly($amount) 43 | { 44 | $mock = $this->createCallableMock(); 45 | $mock 46 | ->expects($this->exactly($amount)) 47 | ->method('__invoke'); 48 | 49 | return $mock; 50 | } 51 | 52 | /** 53 | * Creates a callback that must be called once. 54 | * 55 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 56 | */ 57 | public function expectCallableOnce() 58 | { 59 | $mock = $this->createCallableMock(); 60 | $mock 61 | ->expects($this->once()) 62 | ->method('__invoke'); 63 | 64 | return $mock; 65 | } 66 | 67 | /** 68 | * Creates a callback that must be called twice. 69 | * 70 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 71 | */ 72 | public function expectCallableTwice() 73 | { 74 | $mock = $this->createCallableMock(); 75 | $mock 76 | ->expects($this->exactly(2)) 77 | ->method('__invoke'); 78 | 79 | return $mock; 80 | } 81 | 82 | /** 83 | * Creates a callable that must not be called once. 84 | * 85 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 86 | */ 87 | public function expectCallableNever() 88 | { 89 | $mock = $this->createCallableMock(); 90 | $mock 91 | ->expects($this->never()) 92 | ->method('__invoke'); 93 | 94 | return $mock; 95 | } 96 | 97 | /** 98 | * Creates a callable mock. 99 | * 100 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 101 | */ 102 | public function createCallableMock() 103 | { 104 | return $this->getMock(Callback::class); 105 | } 106 | 107 | /** 108 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 109 | */ 110 | public function createLoopMock() 111 | { 112 | return $this->getMock('Dazzle\Loop\LoopInterface'); 113 | } 114 | 115 | /** 116 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 117 | */ 118 | public function createWritableLoopMock() 119 | { 120 | $loop = $this->createLoopMock(); 121 | $loop 122 | ->expects($this->once()) 123 | ->method('addWriteStream') 124 | ->will($this->returnCallback(function($stream, $listener) { 125 | call_user_func($listener, $stream); 126 | })); 127 | 128 | return $loop; 129 | } 130 | 131 | /** 132 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 133 | */ 134 | public function createReadableLoopMock() 135 | { 136 | $loop = $this->createLoopMock(); 137 | $loop 138 | ->expects($this->once()) 139 | ->method('addReadStream') 140 | ->will($this->returnCallback(function($stream, $listener) { 141 | call_user_func($listener, $stream); 142 | })); 143 | 144 | return $loop; 145 | } 146 | 147 | /** 148 | * Check if protected property exists. 149 | * 150 | * @param object $object 151 | * @param string $property 152 | * @return bool 153 | */ 154 | public function existsProtectedProperty($object, $property) 155 | { 156 | $reflection = new ReflectionClass($object); 157 | return $reflection->hasProperty($property); 158 | } 159 | 160 | /** 161 | * Get protected property from given object via reflection. 162 | * 163 | * @param object $object 164 | * @param string $property 165 | * @return mixed 166 | */ 167 | public function getProtectedProperty($object, $property) 168 | { 169 | $reflection = new ReflectionClass($object); 170 | $reflection_property = $reflection->getProperty($property); 171 | $reflection_property->setAccessible(true); 172 | 173 | return $reflection_property->getValue($object); 174 | } 175 | 176 | /** 177 | * Set protected property on a given object via reflection. 178 | * 179 | * @param object $object 180 | * @param string $property 181 | * @param mixed $value 182 | * @return object 183 | */ 184 | public function setProtectedProperty($object, $property, $value) 185 | { 186 | $reflection = new ReflectionClass($object); 187 | $reflection_property = $reflection->getProperty($property); 188 | $reflection_property->setAccessible(true); 189 | $reflection_property->setValue($object, $value); 190 | 191 | return $object; 192 | } 193 | 194 | /** 195 | * Call protected method on a given object via reflection. 196 | * 197 | * @param object|string $objectOrClass 198 | * @param string $method 199 | * @param mixed[] $args 200 | * @return mixed 201 | */ 202 | public function callProtectedMethod($objectOrClass, $method, $args = []) 203 | { 204 | $reflection = new ReflectionClass($objectOrClass); 205 | $reflectionMethod = $reflection->getMethod($method); 206 | $reflectionMethod->setAccessible(true); 207 | $reflectionTarget = is_object($objectOrClass) ? $objectOrClass : null; 208 | 209 | return $reflectionMethod->invokeArgs($reflectionTarget, $args); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /test/_Simulation/Simulation.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 65 | $this->scenario = function() {}; 66 | $this->events = []; 67 | $this->failureMessage = null; 68 | $this->startCallback = function() {}; 69 | $this->stopCallback = function() {}; 70 | $this->stopFlags = false; 71 | } 72 | 73 | /** 74 | * 75 | */ 76 | public function __destruct() 77 | { 78 | $this->stop(); 79 | 80 | unset($loop); 81 | unset($this->scenario); 82 | unset($this->events); 83 | unset($this->failureMessage); 84 | unset($this->startCallback); 85 | unset($this->stopCallback); 86 | unset($this->stopFlags); 87 | } 88 | 89 | /** 90 | * @param callable(SimulationInterface) $scenario 91 | */ 92 | public function setScenario(callable $scenario) 93 | { 94 | $this->scenario = $scenario; 95 | } 96 | 97 | /** 98 | * @return callable(SimulationInterface)|null $scenario 99 | */ 100 | public function getScenario() 101 | { 102 | return $this->scenario; 103 | } 104 | 105 | /** 106 | * @return LoopInterface 107 | */ 108 | public function getLoop() 109 | { 110 | return $this->loop; 111 | } 112 | 113 | /** 114 | * 115 | */ 116 | public function begin() 117 | { 118 | $this->start(); 119 | } 120 | 121 | /** 122 | * 123 | */ 124 | public function done() 125 | { 126 | $this->stop(); 127 | } 128 | 129 | /** 130 | * 131 | */ 132 | public function fail($message) 133 | { 134 | $this->failureMessage = $message; 135 | $this->stop(); 136 | } 137 | 138 | /** 139 | * @param mixed $expected 140 | * @param mixed $actual 141 | * @param string $message 142 | */ 143 | public function assertSame($expected, $actual, $message = "Assertion failed, expected \"%s\" got \"%s\"") 144 | { 145 | if ($expected !== $actual) 146 | { 147 | $stringExpected = (is_object($expected) || is_array($expected)) ? json_encode($expected) : (string) $expected; 148 | $stringActual = (is_object($actual) || is_array($actual)) ? json_encode($actual) : (string) $actual; 149 | 150 | $this->fail(sprintf($message, $stringExpected, $stringActual)); 151 | } 152 | } 153 | 154 | /** 155 | * @param string $name 156 | * @param mixed $data 157 | */ 158 | public function expect($name, $data = []) 159 | { 160 | $this->events[] = new Event($name, $data); 161 | } 162 | 163 | /** 164 | * @return Event[] 165 | */ 166 | public function getExpectations() 167 | { 168 | return $this->events; 169 | } 170 | 171 | /** 172 | * @param callable $callable 173 | */ 174 | public function onStart(callable $callable) 175 | { 176 | $this->startCallback = $callable; 177 | } 178 | 179 | /** 180 | * @param callable $callable 181 | */ 182 | public function onStop(callable $callable) 183 | { 184 | $this->stopCallback = $callable; 185 | } 186 | 187 | /** 188 | * @param string $model 189 | * @param mixed[] $config 190 | * @return object 191 | */ 192 | public function reflect($model, $config = []) 193 | { 194 | foreach ($config as $key=>$value) 195 | { 196 | if ($value === 'Dazzle\Loop\Loop' || $value === 'Dazzle\Loop\LoopInterface') 197 | { 198 | $config[$key] = $this->getLoop(); 199 | } 200 | } 201 | 202 | return (new ReflectionClass($model))->newInstanceArgs($config); 203 | } 204 | 205 | /** 206 | * @throws Exception 207 | */ 208 | private function start() 209 | { 210 | $sim = $this; 211 | 212 | $scenario = $this->scenario; 213 | $scenario($sim); 214 | 215 | if ($this->stopFlags === true) 216 | { 217 | return; 218 | } 219 | 220 | $onStart = $this->startCallback; 221 | $loop = $this->loop; 222 | 223 | $loop->onStart(function() use($sim, $onStart) { 224 | $onStart($sim); 225 | }); 226 | $loop->addTimer(5, function() use($sim) { 227 | $sim->fail('Timeout for test has been reached.'); 228 | }); 229 | 230 | $loop->start(); 231 | 232 | if ($sim->failureMessage !== null) 233 | { 234 | throw new Exception($sim->failureMessage); 235 | } 236 | } 237 | 238 | /** 239 | * 240 | */ 241 | private function stop() 242 | { 243 | if ($this->loop !== null && $this->loop->isRunning()) 244 | { 245 | $this->loop->stop(); 246 | } 247 | 248 | if ($this->stopFlags === false) 249 | { 250 | $callable = $this->stopCallback; 251 | $callable($this); 252 | } 253 | 254 | $this->stopFlags = true; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/SSH/SSH2.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 75 | $this->config = $config; 76 | $this->loop = $loop; 77 | 78 | $this->host = $config->getHost(); 79 | $this->port = $config->getPort(); 80 | $this->methods = $config->getMethods(); 81 | 82 | $this->conn = null; 83 | $this->drivers = []; 84 | } 85 | 86 | /** 87 | * 88 | */ 89 | public function __destruct() 90 | { 91 | $this->disconnect(); 92 | $this->destructEventEmitterTrait(); 93 | } 94 | 95 | /** 96 | * @override 97 | * @inheritDoc 98 | */ 99 | public function connect() 100 | { 101 | if ($this->conn !== null) 102 | { 103 | return; 104 | } 105 | 106 | $this->conn = $this->createConnection($this->host, $this->port, $this->methods, []); 107 | 108 | if (!$this->conn || !is_resource($this->conn)) 109 | { 110 | $this->emit('error', [ $this, new ExecutionException('SSH2 connection could not be established.') ]); 111 | return; 112 | } 113 | 114 | if (!$this->auth->authenticate($this->conn)) 115 | { 116 | $this->emit('error', [ $this, new ExecutionException('SSH2 connection could not be authenticated.') ]); 117 | return; 118 | } 119 | 120 | $this->emit('connect', [ $this ]); 121 | } 122 | 123 | /** 124 | * @override 125 | * @inheritDoc 126 | */ 127 | public function disconnect() 128 | { 129 | if ($this->conn === null || !is_resource($this->conn)) 130 | { 131 | return; 132 | } 133 | 134 | foreach ($this->drivers as $driver) 135 | { 136 | $driver->disconnect(); 137 | } 138 | 139 | foreach ($this->drivers as $driver) 140 | { 141 | $driver->removeListener('connect', [ $this, 'handleConnect' ]); 142 | $driver->removeListener('disconnect', [ $this, 'handleDisconnect' ]); 143 | $driver->removeListener('error', [ $this, 'handleError' ]); 144 | } 145 | 146 | $this->conn = null; 147 | $this->drivers = []; 148 | 149 | $this->emit('disconnect', [ $this ]); 150 | } 151 | 152 | /** 153 | * @override 154 | * @inheritDoc 155 | */ 156 | public function isConnected() 157 | { 158 | return $this->conn !== null && is_resource($this->conn); 159 | } 160 | 161 | /** 162 | * @override 163 | * @inheritDoc 164 | */ 165 | public function createDriver($name) 166 | { 167 | if (isset($this->drivers[$name])) 168 | { 169 | return $this->drivers[$name]; 170 | } 171 | 172 | if (!$this->isConnected()) 173 | { 174 | throw new ExecutionException("The driver can be created only after the connection has been established!"); 175 | } 176 | 177 | switch ($name) 178 | { 179 | case self::DRIVER_SHELL: 180 | $driver = new Shell($this, $this->conn); 181 | break; 182 | 183 | case self::DRIVER_SFTP: 184 | $driver = new Sftp($this, $this->conn); 185 | break; 186 | 187 | default: 188 | throw new InvalidArgumentException("The driver [$name] is not supported."); 189 | } 190 | 191 | $driver->on('connect', [ $this, 'handleConnect' ]); 192 | $driver->on('disconnect', [ $this, 'handleDisconnect' ]); 193 | $driver->on('error', [ $this, 'handleError' ]); 194 | 195 | $this->drivers[$name] = $driver; 196 | 197 | return $driver; 198 | } 199 | 200 | /** 201 | * @internal 202 | * @param SSH2DriverInterface $driver 203 | */ 204 | public function handleConnect(SSH2DriverInterface $driver) 205 | { 206 | $this->emit('connect:' . $driver->getName(), [ $driver ]); 207 | } 208 | 209 | /** 210 | * @internal 211 | * @param SSH2DriverInterface $driver 212 | */ 213 | public function handleDisconnect(SSH2DriverInterface $driver) 214 | { 215 | $this->emit('disconnect:' . $driver->getName(), [ $driver ]); 216 | } 217 | 218 | /** 219 | * @internal 220 | * @param SSH2DriverInterface $driver 221 | * @param Error|Exception $ex 222 | */ 223 | public function handleError(SSH2DriverInterface $driver, $ex) 224 | { 225 | $this->emit('error:' . $driver->getName(), [ $driver, $ex ]); 226 | $this->emit('error', [ $this, $ex ]); 227 | } 228 | 229 | /** 230 | * Create SSH2 connection. 231 | * 232 | * @param string $host 233 | * @param int $port 234 | * @param mixed[] $methods 235 | * @param callable[] $callbacks 236 | * @return resource 237 | */ 238 | protected function createConnection($host, $port, $methods, $callbacks) 239 | { 240 | return @ssh2_connect($host, $port, $methods, $callbacks); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/SSH/Driver/Sftp.php: -------------------------------------------------------------------------------- 1 | ssh2 = $ssh2; 86 | $this->conn = $conn; 87 | $this->interval = $interval; 88 | 89 | $this->loop = $ssh2->getLoop(); 90 | 91 | $this->resource = null; 92 | $this->resources = []; 93 | $this->paused = true; 94 | 95 | $this->timer = null; 96 | $this->resourcesCounter = 0; 97 | $this->buffer = ''; 98 | $this->prefix = ''; 99 | 100 | $this->resume(); 101 | } 102 | 103 | /** 104 | * 105 | */ 106 | public function __destruct() 107 | { 108 | $this->disconnect(); 109 | 110 | 111 | } 112 | 113 | /** 114 | * @override 115 | * @inheritDoc 116 | */ 117 | public function getName() 118 | { 119 | return 'sftp'; 120 | } 121 | 122 | /** 123 | * @override 124 | * @inheritDoc 125 | */ 126 | public function connect() 127 | { 128 | if ($this->resource !== null) 129 | { 130 | return; 131 | } 132 | 133 | $resource = $this->createConnection($this->conn); 134 | 135 | if (!$resource || !is_resource($resource)) 136 | { 137 | $this->emit('error', [ $this, new ExecutionException('SSH2:Sftp could not be connected.') ]); 138 | return; 139 | } 140 | 141 | $this->resource = $resource; 142 | 143 | $this->emit('connect', [ $this ]); 144 | } 145 | 146 | /** 147 | * @override 148 | * @inheritDoc 149 | */ 150 | public function disconnect() 151 | { 152 | if ($this->resource === null || !is_resource($this->resource)) 153 | { 154 | return; 155 | } 156 | 157 | $this->pause(); 158 | 159 | foreach ($this->resources as $resource) 160 | { 161 | $resource->close(); 162 | } 163 | 164 | $this->handleDisconnect(); 165 | $this->emit('disconnect', [ $this ]); 166 | } 167 | 168 | /** 169 | * @override 170 | * @inheritDoc 171 | */ 172 | public function isConnected() 173 | { 174 | return $this->resource !== null && is_resource($this->resource); 175 | } 176 | 177 | /** 178 | * @override 179 | * @inheritDoc 180 | */ 181 | public function pause() 182 | { 183 | if (!$this->paused) 184 | { 185 | $this->paused = true; 186 | 187 | if ($this->timer !== null) 188 | { 189 | $this->timer->cancel(); 190 | $this->timer = null; 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * @override 197 | * @inheritDoc 198 | */ 199 | public function resume() 200 | { 201 | if ($this->paused) 202 | { 203 | $this->paused = false; 204 | 205 | if ($this->timer === null) 206 | { 207 | $this->timer = $this->loop->addPeriodicTimer($this->interval, [ $this, 'handleHeartbeat' ]); 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * @override 214 | * @inheritDoc 215 | */ 216 | public function isPaused() 217 | { 218 | return $this->paused; 219 | } 220 | 221 | /** 222 | * @override 223 | * @inheritDoc 224 | */ 225 | public function open($resource = null, $flags = 'r') 226 | { 227 | if (!$this->isConnected()) 228 | { 229 | throw new ResourceUndefinedException('Tried to open resource before establishing SSH2 connection!'); 230 | } 231 | 232 | $stream = @fopen("ssh2.sftp://" . $this->resource . $resource, $flags); 233 | 234 | if (!$stream) 235 | { 236 | throw new ResourceUndefinedException("Access to SFTP resource [$resource] denied!"); 237 | } 238 | 239 | $resource = new SftpResource($this, $stream); 240 | $resource->on('open', function(SSH2ResourceInterface $resource) { 241 | $this->emit('resource:open', [ $this, $resource ]); 242 | }); 243 | $resource->on('close', function(SSH2ResourceInterface $resource) { 244 | $this->removeResource($resource->getId()); 245 | $this->emit('resource:close', [ $this, $resource ]); 246 | }); 247 | 248 | $this->resources[$resource->getId()] = $resource; 249 | $this->resourcesCounter++; 250 | $this->resume(); 251 | 252 | return $resource; 253 | } 254 | 255 | /** 256 | * Determine whether connection is still open. 257 | * 258 | * @internal 259 | */ 260 | public function handleHeartbeat() 261 | { 262 | $fp = @fopen("ssh2.sftp://" . $this->resource . "/.", "r"); 263 | 264 | if (!$fp || !is_resource($fp)) 265 | { 266 | return $this->ssh2->disconnect(); 267 | } 268 | 269 | fclose($fp); 270 | 271 | $this->handleData(); 272 | } 273 | 274 | /** 275 | * Handle data. 276 | * 277 | * @internal 278 | */ 279 | public function handleData() 280 | { 281 | if ($this->paused || $this->resourcesCounter === 0) 282 | { 283 | return; 284 | } 285 | 286 | // handle all reading 287 | foreach ($this->resources as $resource) 288 | { 289 | if (!$resource->isPaused() && $resource->isReadable()) 290 | { 291 | $resource->handleRead(); 292 | } 293 | } 294 | 295 | // handle all writing 296 | foreach ($this->resources as $resource) 297 | { 298 | if (!$resource->isPaused() && $resource->isWritable()) 299 | { 300 | $resource->handleWrite(); 301 | } 302 | } 303 | } 304 | 305 | /** 306 | * 307 | */ 308 | protected function handleDisconnect() 309 | { 310 | @fclose($this->resource); 311 | $this->resource = null; 312 | } 313 | 314 | /** 315 | * @param resource $conn 316 | * @return resource 317 | */ 318 | protected function createConnection($conn) 319 | { 320 | return @ssh2_shell($conn); 321 | } 322 | 323 | /** 324 | * Remove resource from known collection. 325 | * 326 | * @param string $prefix 327 | */ 328 | private function removeResource($prefix) 329 | { 330 | if (!isset($this->resources[$prefix])) 331 | { 332 | return; 333 | } 334 | 335 | unset($this->resources[$prefix]); 336 | $this->resourcesCounter--; 337 | 338 | if ($this->resourcesCounter === 0) 339 | { 340 | $this->pause(); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/SSH/Driver/Sftp/SftpResource.php: -------------------------------------------------------------------------------- 1 | loop = $driver->getLoop(); 54 | $this->writing = false; 55 | $this->reading = false; 56 | $this->readingStarted = false; 57 | $this->paused = false; 58 | $this->buffer = new Buffer(); 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getId() 65 | { 66 | return (string) $this->getResourceId(); 67 | } 68 | 69 | /** 70 | * @override 71 | * @inheritDoc 72 | */ 73 | public function isPaused() 74 | { 75 | return $this->paused; 76 | } 77 | 78 | /** 79 | * @override 80 | * @inheritDoc 81 | */ 82 | public function setBufferSize($bufferSize) 83 | { 84 | $this->bufferSize = $bufferSize; 85 | } 86 | 87 | /** 88 | * @override 89 | * @inheritDoc 90 | */ 91 | public function getBufferSize() 92 | { 93 | return $this->bufferSize; 94 | } 95 | 96 | /** 97 | * @override 98 | * @inheritDoc 99 | */ 100 | public function pause() 101 | { 102 | if (!$this->paused) 103 | { 104 | $this->paused = true; 105 | $this->writing = false; 106 | $this->reading = false; 107 | } 108 | } 109 | 110 | /** 111 | * @override 112 | * @inheritDoc 113 | */ 114 | public function resume() 115 | { 116 | if (($this->writable || $this->readable) && $this->paused) 117 | { 118 | $this->paused = false; 119 | 120 | if ($this->readable && $this->readingStarted) 121 | { 122 | $this->reading = true; 123 | } 124 | 125 | if ($this->writable && $this->buffer->isEmpty() === false) 126 | { 127 | $this->writing = true; 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * @override 134 | * @inheritDoc 135 | */ 136 | public function write($text = '') 137 | { 138 | if (!$this->writable) 139 | { 140 | return $this->throwAndEmitException( 141 | new WriteException('Stream is no longer writable.') 142 | ); 143 | } 144 | 145 | $this->buffer->push($text); 146 | 147 | if (!$this->writing && !$this->paused) 148 | { 149 | $this->writing = true; 150 | } 151 | 152 | return $this->buffer->length() < $this->bufferSize; 153 | } 154 | 155 | /** 156 | * @override 157 | * @inheritDoc 158 | */ 159 | public function read($length = null) 160 | { 161 | if (!$this->readable) 162 | { 163 | return $this->throwAndEmitException( 164 | new ReadException('Stream is no longer readable.') 165 | ); 166 | } 167 | 168 | if (!$this->reading && !$this->paused) 169 | { 170 | $this->reading = true; 171 | $this->readingStarted = true; 172 | } 173 | 174 | return ''; 175 | } 176 | 177 | /** 178 | * @override 179 | * @inheritDoc 180 | */ 181 | public function close() 182 | { 183 | if ($this->closing) 184 | { 185 | return; 186 | } 187 | 188 | $this->closing = true; 189 | $this->readable = false; 190 | $this->writable = false; 191 | 192 | if ($this->buffer->isEmpty() === false) 193 | { 194 | $this->writeEnd(); 195 | } 196 | 197 | $this->emit('close', [ $this ]); 198 | $this->handleClose(); 199 | $this->emit('done', [ $this ]); 200 | } 201 | 202 | /** 203 | * Handle the outcoming stream. 204 | * 205 | * @internal 206 | */ 207 | public function handleWrite() 208 | { 209 | try 210 | { 211 | if (!$this->writing) 212 | { 213 | return; 214 | } 215 | 216 | $text = $this->buffer->peek(); 217 | 218 | if ($text !== '') 219 | { 220 | $sent = fwrite($this->resource, $text); 221 | } 222 | else 223 | { 224 | $sent = 0; 225 | } 226 | 227 | if ($text !== '' && !$sent) 228 | { 229 | $this->emit('error', [ $this, new WriteException('Error occurred while writing to the stream resource.') ]); 230 | return; 231 | } 232 | 233 | $lenBefore = strlen($text); 234 | $lenAfter = $lenBefore - $sent; 235 | $this->buffer->remove($sent); 236 | 237 | $this->emit('drain', [ $this ]); 238 | 239 | if ($lenAfter === 0 && $this->buffer->isEmpty()) 240 | { 241 | $this->writing = false; 242 | $this->emit('finish', [ $this ]); 243 | } 244 | } 245 | catch (Error $ex) 246 | { 247 | $this->emit('error', [ $this, $ex ]); 248 | } 249 | catch (Exception $ex) 250 | { 251 | $this->emit('error', [ $this, $ex ]); 252 | } 253 | } 254 | 255 | /** 256 | * Handle the incoming stream. 257 | * 258 | * @internal 259 | */ 260 | public function handleRead() 261 | { 262 | try 263 | { 264 | if (!$this->reading) 265 | { 266 | return; 267 | } 268 | 269 | $length = $this->bufferSize; 270 | $ret = fread($this->resource, $length); 271 | 272 | if ($ret === false) 273 | { 274 | $this->emit('error', [ $this, new ReadException('Error occurred while reading from the stream resource.') ]); 275 | return; 276 | } 277 | 278 | if ($ret !== '') 279 | { 280 | $this->emit('data', [ $this, $ret ]); 281 | 282 | if (strlen($ret) < $length) 283 | { 284 | $this->reading = false; 285 | $this->emit('end', [ $this ]); 286 | } 287 | } 288 | } 289 | catch (Error $ex) 290 | { 291 | $this->emit('error', [ $this, $ex ]); 292 | } 293 | catch (Exception $ex) 294 | { 295 | $this->emit('error', [ $this, $ex ]); 296 | } 297 | } 298 | 299 | /** 300 | * Handle close. 301 | * 302 | * @internal 303 | */ 304 | public function handleClose() 305 | { 306 | $this->pause(); 307 | 308 | parent::handleClose(); 309 | } 310 | 311 | /** 312 | * 313 | */ 314 | private function writeEnd() 315 | { 316 | do 317 | { 318 | try 319 | { 320 | $sent = fwrite($this->resource, $this->buffer->peek()); 321 | $this->buffer->remove($sent); 322 | } 323 | catch (Error $ex) 324 | { 325 | $sent = 0; 326 | } 327 | catch (Exception $ex) 328 | { 329 | $sent = 0; 330 | } 331 | } 332 | while (is_resource($this->resource) && $sent > 0 && !$this->buffer->isEmpty()); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dazzle Async SSH 2 | 3 | [![Build Status](https://travis-ci.org/dazzle-php/ssh.svg)](https://travis-ci.org/dazzle-php/ssh) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/dazzle-php/ssh/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/dazzle-php/ssh/?branch=master) 5 | [![Code Quality](https://scrutinizer-ci.com/g/dazzle-php/ssh/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/dazzle-php/ssh/?branch=master) 6 | [![Latest Stable Version](https://poser.pugx.org/dazzle-php/ssh/v/stable)](https://packagist.org/packages/dazzle-php/ssh) 7 | [![Latest Unstable Version](https://poser.pugx.org/dazzle-php/ssh/v/unstable)](https://packagist.org/packages/dazzle-php/ssh) 8 | [![License](https://poser.pugx.org/dazzle-php/ssh/license)](https://packagist.org/packages/dazzle-php/ssh/license) 9 | 10 | > **Note:** This repository is part of [Dazzle Project](https://github.com/dazzle-php/dazzle) - the next-gen library for PHP. The project's purpose is to provide PHP developers with a set of complete tools to build functional async applications. Please, make sure you read the attached README carefully and it is guaranteed you will be surprised how easy to use and powerful it is. In the meantime, you might want to check out the rest of our async libraries in [Dazzle repository](https://github.com/dazzle-php) for the full extent of Dazzle experience. 11 | 12 |
13 |

14 | 15 |

16 | 17 | ## Description 18 | 19 | Dazzle SSH is a component that provides consistent interface for PHP SSH2 extension and allows asynchronous writing and reading. 20 | 21 | ## Feature Highlights 22 | 23 | Dazzle SSH features: 24 | 25 | * OOP abstraction for PHP SSH2 extension, 26 | * Support for variety of authorization methods, 27 | * Asynchronous SSH2 commands, 28 | * Asynchronous operations on files via SFTP, 29 | * ...and more. 30 | 31 | ## Provided Example(s) 32 | 33 | ### Executing commands 34 | 35 | ```php 36 | $loop = new Loop(new SelectLoop); 37 | $auth = new SSH2Password($user, $pass); 38 | $config = new SSH2Config(); 39 | $ssh2 = new SSH2($auth, $config, $loop); 40 | 41 | $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($ssh2, $loop) { 42 | echo "# CONNECTED SHELL\n"; 43 | 44 | $buffer = ''; 45 | $command = $shell->open(); 46 | $command->write('ls -la'); 47 | $command->on('data', function(SSH2ResourceInterface $command, $data) use(&$buffer) { 48 | $buffer .= $data; 49 | }); 50 | $command->on('end', function(SSH2ResourceInterface $command) use(&$buffer) { 51 | echo "# COMMAND RETURNED:\n"; 52 | echo $buffer; 53 | $command->close(); 54 | }); 55 | $command->on('close', function(SSH2ResourceInterface $command) use($shell) { 56 | $shell->disconnect(); 57 | }); 58 | }); 59 | 60 | $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($ssh2) { 61 | echo "# DISCONNECTED SHELL\n"; 62 | $ssh2->disconnect(); 63 | }); 64 | 65 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 66 | echo "# CONNECTED\n"; 67 | $ssh2->createDriver(SSH2::DRIVER_SHELL) 68 | ->connect(); 69 | }); 70 | 71 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 72 | echo "# DISCONNECTED\n"; 73 | $loop->stop(); 74 | }); 75 | 76 | $loop->onTick(function() use($ssh2) { 77 | $ssh2->connect(); 78 | }); 79 | 80 | $loop->start(); 81 | ``` 82 | 83 | ### Writing files 84 | 85 | ```php 86 | $loop = new Loop(new SelectLoop); 87 | $auth = new SSH2Password($user, $pass); 88 | $config = new SSH2Config(); 89 | $ssh2 = new SSH2($auth, $config, $loop); 90 | 91 | $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($loop, $ssh2) { 92 | echo "# CONNECTED SFTP\n"; 93 | 94 | $lines = [ "DAZZLE\n", "IS\n", "AWESOME!\n" ]; 95 | $linesPointer = 0; 96 | 97 | $file = $sftp->open(__DIR__ . '/_file_write.txt', 'w+'); 98 | $file->write(); 99 | $file->on('drain', function(SSH2ResourceInterface $file) use(&$lines, &$linesPointer) { 100 | echo "# PART OF THE DATA HAS BEEN WRITTEN\n"; 101 | if ($linesPointer < count($lines)) { 102 | $file->write($lines[$linesPointer++]); 103 | } 104 | }); 105 | $file->on('finish', function(SSH2ResourceInterface $file) { 106 | echo "# FINISHED WRITING\n"; 107 | $file->close(); 108 | }); 109 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 110 | echo "# FILE HAS BEEN CLOSED\n"; 111 | $sftp->disconnect(); 112 | }); 113 | }); 114 | 115 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($ssh2) { 116 | echo "# DISCONNECTED SFTP\n"; 117 | $ssh2->disconnect(); 118 | }); 119 | 120 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 121 | echo "# CONNECTED\n"; 122 | $ssh2->createDriver(SSH2::DRIVER_SFTP) 123 | ->connect(); 124 | }); 125 | 126 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 127 | echo "# DISCONNECTED\n"; 128 | $loop->stop(); 129 | }); 130 | 131 | $loop->onTick(function() use($ssh2) { 132 | $ssh2->connect(); 133 | }); 134 | 135 | $loop->start(); 136 | ``` 137 | 138 | ### Reading files 139 | 140 | ```php 141 | $loop = new Loop(new SelectLoop); 142 | $auth = new SSH2Password($user, $pass); 143 | $config = new SSH2Config(); 144 | $ssh2 = new SSH2($auth, $config, $loop); 145 | 146 | $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($loop, $ssh2) { 147 | echo "# CONNECTED SFTP\n"; 148 | 149 | $buffer = ''; 150 | $file = $sftp->open(__DIR__ . '/_file_read.txt', 'r+'); 151 | $file->read(); 152 | $file->on('data', function(SSH2ResourceInterface $file, $data) use(&$buffer) { 153 | $buffer .= $data; 154 | }); 155 | $file->on('end', function(SSH2ResourceInterface $file) use(&$buffer) { 156 | echo "# FOLLOWING LINES WERE READ FROM FILE:\n"; 157 | echo $buffer; 158 | $file->close(); 159 | }); 160 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 161 | echo "# FILE HAS BEEN CLOSED\n"; 162 | $sftp->disconnect(); 163 | }); 164 | }); 165 | 166 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($ssh2) { 167 | echo "# DISCONNECTED SFTP\n"; 168 | $ssh2->disconnect(); 169 | }); 170 | 171 | $ssh2->on('connect', function(SSH2Interface $ssh2) { 172 | echo "# CONNECTED\n"; 173 | $ssh2->createDriver(SSH2::DRIVER_SFTP) 174 | ->connect(); 175 | }); 176 | 177 | $ssh2->on('disconnect', function(SSH2Interface $ssh2) use($loop) { 178 | echo "# DISCONNECTED\n"; 179 | $loop->stop(); 180 | }); 181 | 182 | $loop->onTick(function() use($ssh2) { 183 | $ssh2->connect(); 184 | }); 185 | 186 | $loop->start(); 187 | ``` 188 | 189 | See more examples in **example directory**. 190 | 191 | ## Requirements 192 | 193 | Dazzle SSH requires: 194 | 195 | * PHP-5.6 or PHP-7.0+, 196 | * UNIX or Windows OS, 197 | * PHP SSH2 extension enabled. 198 | 199 | ## Installation 200 | 201 | To install this library make sure you have [composer](https://getcomposer.org/) installed, then run following command: 202 | 203 | ``` 204 | $> composer require dazzle-php/ssh 205 | ``` 206 | 207 | ## Tests 208 | 209 | Tests can be run via: 210 | 211 | ``` 212 | $> vendor/bin/phpunit -d memory_limit=1024M 213 | ``` 214 | 215 | ## Versioning 216 | 217 | Versioning of Dazzle libraries is being shared between all packages included in [Dazzle Project](https://github.com/dazzle-php/dazzle). That means the releases are being made concurrently for all of them. On one hand this might lead to "empty" releases for some packages at times, but don't worry. In the end it is far much easier for contributors to maintain and -- what's the most important -- much more straight-forward for users to understand the compatibility and inter-operability of the packages. 218 | 219 | ## Contributing 220 | 221 | Thank you for considering contributing to this repository! 222 | 223 | - The contribution guide can be found in the [contribution tips](https://github.com/dazzle-php/ssh/blob/master/CONTRIBUTING.md). 224 | - Open tickets can be found in [issues section](https://github.com/dazzle-php/ssh/issues). 225 | - Current contributors are listed in [graphs section](https://github.com/dazzle-php/ssh/graphs/contributors) 226 | - To contact the author(s) see the information attached in [composer.json](https://github.com/dazzle-php/ssh/blob/master/composer.json) file. 227 | 228 | ## License 229 | 230 | Dazzle SSH is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 231 | 232 |
233 |

234 | "Everything is possible. The impossible just takes longer." ― Dan Brown 235 |

236 | -------------------------------------------------------------------------------- /src/SSH/Driver/Shell/ShellResource.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 75 | $this->resource = $resource; 76 | 77 | $this->paused = true; 78 | $this->closing = false; 79 | $this->readable = true; 80 | $this->writable = true; 81 | 82 | $this->bufferSize = 4096; 83 | 84 | $this->prefix = md5(microtime()); 85 | $this->successSuffix = md5(microtime()); 86 | $this->failureSuffix = md5(microtime()); 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getId() 93 | { 94 | return $this->prefix; 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | public function getPrefix() 101 | { 102 | return $this->prefix; 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function getSuccessSuffix() 109 | { 110 | return $this->successSuffix; 111 | } 112 | 113 | /** 114 | * @return string 115 | */ 116 | public function getFailureSuffix() 117 | { 118 | return $this->failureSuffix; 119 | } 120 | 121 | /** 122 | * @override 123 | * @inheritDoc 124 | */ 125 | public function setLoop(LoopInterface $loop = null) 126 | { 127 | $this->driver->setLoop($loop); 128 | } 129 | 130 | /** 131 | * @override 132 | * @inheritDoc 133 | */ 134 | public function getLoop() 135 | { 136 | return $this->driver->getLoop(); 137 | } 138 | 139 | /** 140 | * @override 141 | * @inheritDoc 142 | */ 143 | public function pause() 144 | { 145 | if (!$this->paused) 146 | { 147 | $this->paused = true; 148 | } 149 | } 150 | 151 | /** 152 | * @override 153 | * @inheritDoc 154 | */ 155 | public function resume() 156 | { 157 | if ($this->paused) 158 | { 159 | $this->paused = false; 160 | } 161 | } 162 | 163 | /** 164 | * @override 165 | * @inheritDoc 166 | */ 167 | public function isPaused() 168 | { 169 | return $this->paused; 170 | } 171 | 172 | /** 173 | * @override 174 | * @inheritDoc 175 | */ 176 | public function getResource() 177 | { 178 | return $this->resource; 179 | } 180 | 181 | /** 182 | * @override 183 | * @inheritDoc 184 | */ 185 | public function getResourceId() 186 | { 187 | return (int) $this->resource; 188 | } 189 | 190 | /** 191 | * @override 192 | * @inheritDoc 193 | */ 194 | public function getMetadata() 195 | { 196 | return stream_get_meta_data($this->resource); 197 | } 198 | 199 | /** 200 | * @override 201 | * @inheritDoc 202 | */ 203 | public function getStreamType() 204 | { 205 | return $this->getMetadata()['stream_type']; 206 | } 207 | 208 | /** 209 | * @override 210 | * @inheritDoc 211 | */ 212 | public function getWrapperType() 213 | { 214 | return $this->getMetadata()['wrapper_type']; 215 | } 216 | 217 | /** 218 | * @override 219 | * @inheritDoc 220 | */ 221 | public function isOpen() 222 | { 223 | return !$this->closing; 224 | } 225 | 226 | /** 227 | * @override 228 | * @inheritDoc 229 | */ 230 | public function isSeekable() 231 | { 232 | return $this->getMetadata()['seekable']; 233 | } 234 | 235 | /** 236 | * @override 237 | * @inheritDoc 238 | */ 239 | public function tell() 240 | { 241 | throw new ReadException('Cannot tell offset of this kind of stream.'); 242 | } 243 | 244 | /** 245 | * @override 246 | * @inheritDoc 247 | */ 248 | public function seek($offset, $whence = SEEK_SET) 249 | { 250 | throw new WriteException('Cannot seek on this kind of stream.'); 251 | } 252 | 253 | /** 254 | * @override 255 | * @inheritDoc 256 | */ 257 | public function rewind() 258 | { 259 | throw new WriteException('Cannot rewind this kind of stream.'); 260 | } 261 | 262 | /** 263 | * @override 264 | * @inheritDoc 265 | */ 266 | public function close() 267 | { 268 | if ($this->closing) 269 | { 270 | return; 271 | } 272 | 273 | $this->closing = true; 274 | $this->readable = false; 275 | $this->writable = false; 276 | 277 | $this->emit('close', [ $this ]); 278 | $this->pause(); 279 | $this->emit('done', [ $this ]); 280 | } 281 | 282 | /** 283 | * @override 284 | * @inheritDoc 285 | */ 286 | public function isReadable() 287 | { 288 | return $this->readable; 289 | } 290 | 291 | /** 292 | * @override 293 | * @inheritDoc 294 | */ 295 | public function setBufferSize($bufferSize) 296 | { 297 | $this->bufferSize = $bufferSize; 298 | } 299 | 300 | /** 301 | * @override 302 | * @inheritDoc 303 | */ 304 | public function getBufferSize() 305 | { 306 | return $this->bufferSize; 307 | } 308 | 309 | /** 310 | * @override 311 | * @inheritDoc 312 | */ 313 | public function read($length = null) 314 | { 315 | if (!$this->readable) 316 | { 317 | return $this->throwAndEmitException( 318 | new ReadException('Stream is no longer readable.') 319 | ); 320 | } 321 | 322 | if ($length === null) 323 | { 324 | $length = $this->bufferSize; 325 | } 326 | 327 | $ret = fread($this->resource, $length); 328 | 329 | if ($ret === false) 330 | { 331 | return $this->throwAndEmitException( 332 | new ReadException('Cannot read stream.') 333 | ); 334 | } 335 | else if ($ret !== '') 336 | { 337 | $this->emit('data', [ $this, $ret ]); 338 | 339 | if (strlen($ret) < $length) 340 | { 341 | $this->emit('end', [ $this ]); 342 | } 343 | } 344 | 345 | return $ret; 346 | } 347 | 348 | /** 349 | * @override 350 | * @inheritDoc 351 | */ 352 | public function isWritable() 353 | { 354 | return $this->writable; 355 | } 356 | 357 | /** 358 | * @override 359 | * @inheritDoc 360 | */ 361 | public function write($text = '') 362 | { 363 | if (!$this->writable) 364 | { 365 | return $this->throwAndEmitException( 366 | new WriteException('Stream is no longer writable.') 367 | ); 368 | } 369 | 370 | $command = sprintf( 371 | "echo %s && %s && echo %s || echo %s\n", 372 | $this->prefix, 373 | $text, 374 | $this->successSuffix . ':$?', 375 | $this->failureSuffix . ':$?' 376 | ); 377 | 378 | $sent = fwrite($this->resource, $command); 379 | 380 | if ($sent === false) 381 | { 382 | return $this->throwAndEmitException( 383 | new WriteException('Error occurred while writing to the stream resource.') 384 | ); 385 | } 386 | 387 | $this->writable = false; // this is single-use stream only! 388 | $this->emit('drain', [ $this ]); 389 | $this->emit('finish', [ $this ]); 390 | 391 | return true; 392 | } 393 | 394 | /** 395 | * Emit error event and the throws it too. 396 | * 397 | * @param Error|Exception $ex 398 | * @return null 399 | * @throws Error|Exception 400 | */ 401 | protected function throwAndEmitException($ex) 402 | { 403 | $this->emit('error', [ $this, $ex ]); 404 | throw $ex; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/SSH/Driver/Shell.php: -------------------------------------------------------------------------------- 1 | ssh2 = $ssh2; 85 | $this->conn = $conn; 86 | $this->interval = $interval; 87 | 88 | $this->loop = $ssh2->getLoop(); 89 | 90 | $this->resource = null; 91 | $this->resources = []; 92 | $this->paused = true; 93 | 94 | $this->timer = null; 95 | $this->resourcesCounter = 0; 96 | $this->buffer = ''; 97 | $this->prefix = ''; 98 | 99 | $this->resume(); 100 | } 101 | 102 | /** 103 | * 104 | */ 105 | public function __destruct() 106 | { 107 | $this->disconnect(); 108 | } 109 | 110 | /** 111 | * @override 112 | * @inheritDoc 113 | */ 114 | public function getName() 115 | { 116 | return SSH2::DRIVER_SHELL; 117 | } 118 | 119 | /** 120 | * @override 121 | * @inheritDoc 122 | */ 123 | public function connect() 124 | { 125 | if ($this->resource !== null) 126 | { 127 | return; 128 | } 129 | 130 | $shell = $this->createConnection($this->conn); 131 | 132 | if (!$shell || !is_resource($shell)) 133 | { 134 | $this->emit('error', [ $this, new ExecutionException('SSH2:Shell could not be connected.') ]); 135 | return; 136 | } 137 | 138 | $this->resource = $shell; 139 | 140 | $this->emit('connect', [ $this ]); 141 | } 142 | 143 | /** 144 | * @override 145 | * @inheritDoc 146 | */ 147 | public function disconnect() 148 | { 149 | if ($this->resource === null || !is_resource($this->resource)) 150 | { 151 | return; 152 | } 153 | 154 | $this->pause(); 155 | 156 | foreach ($this->resources as $resource) 157 | { 158 | $resource->close(); 159 | } 160 | 161 | $this->handleDisconnect(); 162 | $this->emit('disconnect', [ $this ]); 163 | } 164 | 165 | /** 166 | * @override 167 | * @inheritDoc 168 | */ 169 | public function isConnected() 170 | { 171 | return $this->resource !== null && is_resource($this->resource); 172 | } 173 | 174 | /** 175 | * @override 176 | * @inheritDoc 177 | */ 178 | public function pause() 179 | { 180 | if (!$this->paused) 181 | { 182 | $this->paused = true; 183 | 184 | if ($this->timer !== null) 185 | { 186 | $this->timer->cancel(); 187 | $this->timer = null; 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * @override 194 | * @inheritDoc 195 | */ 196 | public function resume() 197 | { 198 | if ($this->paused) 199 | { 200 | $this->paused = false; 201 | 202 | if ($this->timer === null) 203 | { 204 | $this->timer = $this->loop->addPeriodicTimer($this->interval, [ $this, 'handleHeartbeat' ]); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * @override 211 | * @inheritDoc 212 | */ 213 | public function isPaused() 214 | { 215 | return $this->paused; 216 | } 217 | 218 | /** 219 | * @override 220 | * @inheritDoc 221 | */ 222 | public function open($resource = null, $flags = 'r') 223 | { 224 | if (!$this->isConnected()) 225 | { 226 | throw new ResourceUndefinedException('Tried to open resource before establishing SSH2 connection!'); 227 | } 228 | 229 | $resource = new ShellResource($this, $this->resource); 230 | $resource->on('open', function(SSH2ResourceInterface $resource) { 231 | $this->emit('resource:open', [ $this, $resource ]); 232 | }); 233 | $resource->on('close', function(SSH2ResourceInterface $resource) { 234 | $this->removeResource($resource->getId()); 235 | $this->emit('resource:close', [ $this, $resource ]); 236 | }); 237 | 238 | $this->resources[$resource->getId()] = $resource; 239 | $this->resourcesCounter++; 240 | $this->resume(); 241 | 242 | return $resource; 243 | } 244 | 245 | /** 246 | * Handle data. 247 | * 248 | * @internal 249 | */ 250 | public function handleHeartbeat() 251 | { 252 | if (fwrite($this->resource, "\n") === 0) 253 | { 254 | return $this->ssh2->disconnect(); 255 | } 256 | 257 | $this->handleRead(); 258 | } 259 | 260 | /** 261 | * Handle incoming data. 262 | * 263 | * @internal 264 | */ 265 | protected function handleRead() 266 | { 267 | if ($this->paused) 268 | { 269 | return; 270 | } 271 | 272 | $data = @fread($this->resource, static::BUFFER_SIZE); 273 | 274 | if ($data === false || $data === '') 275 | { 276 | return; 277 | } 278 | 279 | $this->buffer .= $data; 280 | 281 | while ($this->buffer !== '') 282 | { 283 | if ($this->prefix !== '' && !isset($this->resources[$this->prefix])) 284 | { 285 | $this->prefix = ''; 286 | } 287 | 288 | if ($this->prefix === '') 289 | { 290 | if (!preg_match('/([a-zA-Z0-9]{32})\r?\n(.*)/s', $this->buffer, $matches)) 291 | { 292 | return; 293 | } 294 | 295 | $this->prefix = $matches[1]; 296 | $this->buffer = $matches[2]; 297 | } 298 | 299 | $resource = $this->resources[$this->prefix]; 300 | $data = ''; 301 | $status = -1; 302 | $successSuffix = $resource->getSuccessSuffix(); 303 | $failureSuffix = $resource->getFailureSuffix(); 304 | 305 | $this->buffer = preg_replace_callback( 306 | sprintf('/(.*)(%s|%s):(\d*)\r?\n/s', $successSuffix, $failureSuffix), 307 | function($matches) use($resource, &$data, &$status) { 308 | $data = $matches[1]; 309 | $status = (int) $matches[3]; 310 | return ''; 311 | }, 312 | $this->buffer 313 | ); 314 | 315 | if ($status === -1) 316 | { 317 | $data = $this->buffer; 318 | $this->buffer = ''; 319 | } 320 | else 321 | { 322 | $this->removeResource($this->prefix); 323 | $this->prefix = ''; 324 | } 325 | 326 | $parts = str_split($data, $resource->getBufferSize()); 327 | unset($data); 328 | 329 | foreach ($parts as &$part) 330 | { 331 | $resource->emit('data', [ $resource, $part ]); 332 | } 333 | unset($parts); 334 | unset($part); 335 | 336 | if ($status === 0) 337 | { 338 | $resource->emit('end', [ $resource ]); 339 | $resource->close(); 340 | } 341 | else if ($status > 0) 342 | { 343 | $resource->emit('error', [ $resource, new ReadException($status) ]); 344 | $resource->close(); 345 | } 346 | } 347 | 348 | $this->handleRead(); 349 | } 350 | 351 | /** 352 | * 353 | */ 354 | protected function handleDisconnect() 355 | { 356 | @fclose($this->resource); 357 | $this->resource = null; 358 | } 359 | 360 | /** 361 | * @param resource $conn 362 | * @return resource 363 | */ 364 | protected function createConnection($conn) 365 | { 366 | return @ssh2_shell($conn); 367 | } 368 | 369 | /** 370 | * Remove resource from known collection. 371 | * 372 | * @param string $prefix 373 | */ 374 | private function removeResource($prefix) 375 | { 376 | if (!isset($this->resources[$prefix])) 377 | { 378 | return; 379 | } 380 | 381 | unset($this->resources[$prefix]); 382 | $this->resourcesCounter--; 383 | 384 | if ($this->resourcesCounter === 0) 385 | { 386 | $this->pause(); 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /test/TUnit/Driver/SftpTest.php: -------------------------------------------------------------------------------- 1 | createDriver(); 24 | $this->assertInstanceOf(SSH2DriverInterface::class, $driver); 25 | } 26 | 27 | /** 28 | * 29 | */ 30 | public function testDestructor_DoesNotThrowThrowable() 31 | { 32 | $driver = $this->createDriver(); 33 | unset($driver); 34 | } 35 | 36 | /** 37 | * 38 | */ 39 | public function testApiGetName_ReturnsSftpName() 40 | { 41 | $driver = $this->createDriver(); 42 | 43 | $this->assertSame(SSH2::DRIVER_SFTP, $driver->getName()); 44 | } 45 | 46 | /** 47 | * 48 | */ 49 | public function testApiConnect_DoesNothing_WhenConnectionIsEstablished() 50 | { 51 | $driver = $this->createDriver(); 52 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); 53 | 54 | $driver->on('error', $this->expectCallableNever()); 55 | $driver->on('connect', $this->expectCallableNever()); 56 | 57 | $driver->connect(); 58 | } 59 | 60 | /** 61 | * 62 | */ 63 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() 64 | { 65 | $driver = $this->createDriver([ 'createConnection' ]); 66 | $driver 67 | ->expects($this->once()) 68 | ->method('createConnection') 69 | ->will($this->returnValue(false)); 70 | 71 | $callback = $this->createCallableMock(); 72 | $callback 73 | ->expects($this->once()) 74 | ->method('__invoke') 75 | ->with($driver, $this->isInstanceOf(ExecutionException::class)); 76 | 77 | $driver->on('error', $callback); 78 | $driver->on('connect', $this->expectCallableNever()); 79 | 80 | $driver->connect(); 81 | } 82 | 83 | /** 84 | * 85 | */ 86 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNotResource() 87 | { 88 | $driver = $this->createDriver([ 'createConnection' ]); 89 | $driver 90 | ->expects($this->once()) 91 | ->method('createConnection') 92 | ->will($this->returnValue(true)); 93 | 94 | $callback = $this->createCallableMock(); 95 | $callback 96 | ->expects($this->once()) 97 | ->method('__invoke') 98 | ->with($driver, $this->isInstanceOf(ExecutionException::class)); 99 | 100 | $driver->on('error', $callback); 101 | $driver->on('connect', $this->expectCallableNever()); 102 | 103 | $driver->connect(); 104 | } 105 | 106 | /** 107 | * 108 | */ 109 | public function testApiConnect_EmitsConnectEvent() 110 | { 111 | $stream = fopen('php://memory', 'r'); 112 | 113 | $driver = $this->createDriver([ 'createConnection' ]); 114 | $driver 115 | ->expects($this->once()) 116 | ->method('createConnection') 117 | ->will($this->returnValue($stream)); 118 | 119 | $callback = $this->createCallableMock(); 120 | $callback 121 | ->expects($this->once()) 122 | ->method('__invoke') 123 | ->with($driver); 124 | 125 | $driver->on('error', $this->expectCallableNever()); 126 | $driver->on('connect', $callback); 127 | 128 | $driver->connect(); 129 | } 130 | 131 | /** 132 | * 133 | */ 134 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() 135 | { 136 | $driver = $this->createDriver(); 137 | 138 | $driver->on('disconnect', $this->expectCallableNever()); 139 | 140 | $driver->disconnect(); 141 | } 142 | 143 | /** 144 | * 145 | */ 146 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() 147 | { 148 | $driver = $this->createDriver(); 149 | $this->setProtectedProperty($driver, 'resource', true); 150 | 151 | $driver->on('disconnect', $this->expectCallableNever()); 152 | 153 | $driver->disconnect(); 154 | } 155 | 156 | /** 157 | * 158 | */ 159 | public function testApiDisconnect_EmitsDisconnectEvent() 160 | { 161 | $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); 162 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); 163 | 164 | $driver 165 | ->expects($this->once()) 166 | ->method('pause'); 167 | $driver 168 | ->expects($this->once()) 169 | ->method('handleDisconnect'); 170 | 171 | $callback = $this->createCallableMock(); 172 | $callback 173 | ->expects($this->once()) 174 | ->method('__invoke') 175 | ->with($driver); 176 | 177 | $driver->on('disconnect', $callback); 178 | 179 | $driver->disconnect(); 180 | } 181 | 182 | /** 183 | * 184 | */ 185 | public function testApiDisconnect_ClosesEachResource() 186 | { 187 | $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); 188 | 189 | $resource1 = $this->getMock(SftpResource::class, [], [], '', false); 190 | $resource1 191 | ->expects($this->once()) 192 | ->method('close'); 193 | 194 | $resource2 = $this->getMock(SftpResource::class, [], [], '', false); 195 | $resource1 196 | ->expects($this->once()) 197 | ->method('close'); 198 | 199 | $resources = [ $resource1, $resource2 ]; 200 | 201 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); 202 | $this->setProtectedProperty($driver, 'resources', $resources); 203 | 204 | $driver 205 | ->expects($this->once()) 206 | ->method('pause'); 207 | $driver 208 | ->expects($this->once()) 209 | ->method('handleDisconnect'); 210 | 211 | $driver->disconnect(); 212 | } 213 | 214 | /** 215 | * 216 | */ 217 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() 218 | { 219 | $driver = $this->createDriver(); 220 | 221 | $this->assertFalse($driver->isConnected()); 222 | } 223 | 224 | /** 225 | * 226 | */ 227 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() 228 | { 229 | $driver = $this->createDriver(); 230 | $this->setProtectedProperty($driver, 'resource', true); 231 | 232 | $this->assertFalse($driver->isConnected()); 233 | } 234 | 235 | /** 236 | * 237 | */ 238 | public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() 239 | { 240 | $driver = $this->createDriver(); 241 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); 242 | 243 | $this->assertTrue($driver->isConnected()); 244 | } 245 | 246 | /** 247 | * 248 | */ 249 | public function testApiResume_DoesNothing_WhenDriverIsNotPaused() 250 | { 251 | $driver = $this->createDriver(); 252 | $this->setProtectedProperty($driver, 'paused', false); 253 | 254 | $driver->resume(); 255 | 256 | $this->assertFalse($driver->isPaused()); 257 | } 258 | 259 | /** 260 | * 261 | */ 262 | public function testApiResume_ResumesDriver_WhenDriverIsPaused() 263 | { 264 | $driver = $this->createDriver(); 265 | $this->setProtectedProperty($driver, 'paused', true); 266 | 267 | $driver->resume(); 268 | 269 | $this->assertFalse($driver->isPaused()); 270 | } 271 | 272 | /** 273 | * 274 | */ 275 | public function testApiPause_PausesDriver_WhenDriverIsNotPaused() 276 | { 277 | $driver = $this->createDriver(); 278 | $this->setProtectedProperty($driver, 'paused', false); 279 | 280 | $driver->pause(); 281 | 282 | $this->assertTrue($driver->isPaused()); 283 | } 284 | 285 | /** 286 | * 287 | */ 288 | public function testApiPause_DoesNothing_WhenDriverIsPaused() 289 | { 290 | $driver = $this->createDriver(); 291 | $this->setProtectedProperty($driver, 'paused', true); 292 | 293 | $driver->pause(); 294 | 295 | $this->assertTrue($driver->isPaused()); 296 | } 297 | 298 | /** 299 | * 300 | */ 301 | public function testApiIsPaused_ReturnsFalse_WhenDriverIsNotPaused() 302 | { 303 | $driver = $this->createDriver(); 304 | $this->setProtectedProperty($driver, 'paused', false); 305 | 306 | $this->assertFalse($driver->isPaused()); 307 | } 308 | 309 | /** 310 | * 311 | */ 312 | public function testApiIsPaused_ReturnsTrue_WhenDriverIsPaused() 313 | { 314 | $driver = $this->createDriver(); 315 | $this->setProtectedProperty($driver, 'paused', true); 316 | 317 | $this->assertTrue($driver->isPaused()); 318 | } 319 | 320 | /** 321 | * Create Sftp driver. 322 | * 323 | * @param string[] $methods 324 | * @param mixed 325 | * @return Sftp|\PHPUnit_Framework_MockObject_MockObject 326 | */ 327 | public function createDriver($methods = null, $constructorParams = []) 328 | { 329 | if (isset($constructorParams['ssh2'])) 330 | { 331 | $ssh2 = $constructorParams['ssh2']; 332 | } 333 | else 334 | { 335 | $timer = $this->getMock(TimerInterface::class, [], [], '', false); 336 | $loop = $this->getMock(Loop::class, [ 'addPeriodicTimer' ], [], '', false); 337 | $loop 338 | ->expects($this->any()) 339 | ->method('addPeriodicTimer') 340 | ->will($this->returnValue($timer)); 341 | 342 | $ssh2 = $this->getMock(SSH2Interface::class, [], [], '', false); 343 | $ssh2 344 | ->expects($this->any()) 345 | ->method('getLoop') 346 | ->will($this->returnValue($loop)); 347 | } 348 | 349 | $conn = isset($constructorParams['conn']) ? $constructorParams['conn'] : fopen('php://memory', 'r+'); 350 | 351 | $mock = $this->getMock(Sftp::class, $methods, [ $ssh2, $conn ]); 352 | 353 | return $mock; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /test/TUnit/Driver/ShellTest.php: -------------------------------------------------------------------------------- 1 | createDriver(); 24 | $this->assertInstanceOf(SSH2DriverInterface::class, $driver); 25 | } 26 | 27 | /** 28 | * 29 | */ 30 | public function testDestructor_DoesNotThrowThrowable() 31 | { 32 | $driver = $this->createDriver(); 33 | unset($driver); 34 | } 35 | 36 | /** 37 | * 38 | */ 39 | public function testApiGetName_ReturnsShellName() 40 | { 41 | $driver = $this->createDriver(); 42 | 43 | $this->assertSame(SSH2::DRIVER_SHELL, $driver->getName()); 44 | } 45 | 46 | /** 47 | * 48 | */ 49 | public function testApiConnect_DoesNothing_WhenConnectionIsEstablished() 50 | { 51 | $driver = $this->createDriver(); 52 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); 53 | 54 | $driver->on('error', $this->expectCallableNever()); 55 | $driver->on('connect', $this->expectCallableNever()); 56 | 57 | $driver->connect(); 58 | } 59 | 60 | /** 61 | * 62 | */ 63 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() 64 | { 65 | $driver = $this->createDriver([ 'createConnection' ]); 66 | $driver 67 | ->expects($this->once()) 68 | ->method('createConnection') 69 | ->will($this->returnValue(false)); 70 | 71 | $callback = $this->createCallableMock(); 72 | $callback 73 | ->expects($this->once()) 74 | ->method('__invoke') 75 | ->with($driver, $this->isInstanceOf(ExecutionException::class)); 76 | 77 | $driver->on('error', $callback); 78 | $driver->on('connect', $this->expectCallableNever()); 79 | 80 | $driver->connect(); 81 | } 82 | 83 | /** 84 | * 85 | */ 86 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNotResource() 87 | { 88 | $driver = $this->createDriver([ 'createConnection' ]); 89 | $driver 90 | ->expects($this->once()) 91 | ->method('createConnection') 92 | ->will($this->returnValue(true)); 93 | 94 | $callback = $this->createCallableMock(); 95 | $callback 96 | ->expects($this->once()) 97 | ->method('__invoke') 98 | ->with($driver, $this->isInstanceOf(ExecutionException::class)); 99 | 100 | $driver->on('error', $callback); 101 | $driver->on('connect', $this->expectCallableNever()); 102 | 103 | $driver->connect(); 104 | } 105 | 106 | /** 107 | * 108 | */ 109 | public function testApiConnect_EmitsConnectEvent() 110 | { 111 | $stream = fopen('php://memory', 'r'); 112 | 113 | $driver = $this->createDriver([ 'createConnection' ]); 114 | $driver 115 | ->expects($this->once()) 116 | ->method('createConnection') 117 | ->will($this->returnValue($stream)); 118 | 119 | $callback = $this->createCallableMock(); 120 | $callback 121 | ->expects($this->once()) 122 | ->method('__invoke') 123 | ->with($driver); 124 | 125 | $driver->on('error', $this->expectCallableNever()); 126 | $driver->on('connect', $callback); 127 | 128 | $driver->connect(); 129 | } 130 | 131 | /** 132 | * 133 | */ 134 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() 135 | { 136 | $driver = $this->createDriver(); 137 | 138 | $driver->on('disconnect', $this->expectCallableNever()); 139 | 140 | $driver->disconnect(); 141 | } 142 | 143 | /** 144 | * 145 | */ 146 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() 147 | { 148 | $driver = $this->createDriver(); 149 | $this->setProtectedProperty($driver, 'resource', true); 150 | 151 | $driver->on('disconnect', $this->expectCallableNever()); 152 | 153 | $driver->disconnect(); 154 | } 155 | 156 | /** 157 | * 158 | */ 159 | public function testApiDisconnect_EmitsDisconnectEvent() 160 | { 161 | $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); 162 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); 163 | 164 | $driver 165 | ->expects($this->once()) 166 | ->method('pause'); 167 | $driver 168 | ->expects($this->once()) 169 | ->method('handleDisconnect'); 170 | 171 | $callback = $this->createCallableMock(); 172 | $callback 173 | ->expects($this->once()) 174 | ->method('__invoke') 175 | ->with($driver); 176 | 177 | $driver->on('disconnect', $callback); 178 | 179 | $driver->disconnect(); 180 | } 181 | 182 | /** 183 | * 184 | */ 185 | public function testApiDisconnect_ClosesEachResource() 186 | { 187 | $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); 188 | 189 | $resource1 = $this->getMock(ShellResource::class, [], [], '', false); 190 | $resource1 191 | ->expects($this->once()) 192 | ->method('close'); 193 | 194 | $resource2 = $this->getMock(ShellResource::class, [], [], '', false); 195 | $resource1 196 | ->expects($this->once()) 197 | ->method('close'); 198 | 199 | $resources = [ $resource1, $resource2 ]; 200 | 201 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); 202 | $this->setProtectedProperty($driver, 'resources', $resources); 203 | 204 | $driver 205 | ->expects($this->once()) 206 | ->method('pause'); 207 | $driver 208 | ->expects($this->once()) 209 | ->method('handleDisconnect'); 210 | 211 | $driver->disconnect(); 212 | } 213 | 214 | /** 215 | * 216 | */ 217 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() 218 | { 219 | $driver = $this->createDriver(); 220 | 221 | $this->assertFalse($driver->isConnected()); 222 | } 223 | 224 | /** 225 | * 226 | */ 227 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() 228 | { 229 | $driver = $this->createDriver(); 230 | $this->setProtectedProperty($driver, 'resource', true); 231 | 232 | $this->assertFalse($driver->isConnected()); 233 | } 234 | 235 | /** 236 | * 237 | */ 238 | public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() 239 | { 240 | $driver = $this->createDriver(); 241 | $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); 242 | 243 | $this->assertTrue($driver->isConnected()); 244 | } 245 | 246 | /** 247 | * 248 | */ 249 | public function testApiResume_DoesNothing_WhenDriverIsNotPaused() 250 | { 251 | $driver = $this->createDriver(); 252 | $this->setProtectedProperty($driver, 'paused', false); 253 | 254 | $driver->resume(); 255 | 256 | $this->assertFalse($driver->isPaused()); 257 | } 258 | 259 | /** 260 | * 261 | */ 262 | public function testApiResume_ResumesDriver_WhenDriverIsPaused() 263 | { 264 | $driver = $this->createDriver(); 265 | $this->setProtectedProperty($driver, 'paused', true); 266 | 267 | $driver->resume(); 268 | 269 | $this->assertFalse($driver->isPaused()); 270 | } 271 | 272 | /** 273 | * 274 | */ 275 | public function testApiPause_PausesDriver_WhenDriverIsNotPaused() 276 | { 277 | $driver = $this->createDriver(); 278 | $this->setProtectedProperty($driver, 'paused', false); 279 | 280 | $driver->pause(); 281 | 282 | $this->assertTrue($driver->isPaused()); 283 | } 284 | 285 | /** 286 | * 287 | */ 288 | public function testApiPause_DoesNothing_WhenDriverIsPaused() 289 | { 290 | $driver = $this->createDriver(); 291 | $this->setProtectedProperty($driver, 'paused', true); 292 | 293 | $driver->pause(); 294 | 295 | $this->assertTrue($driver->isPaused()); 296 | } 297 | 298 | /** 299 | * 300 | */ 301 | public function testApiIsPaused_ReturnsFalse_WhenDriverIsNotPaused() 302 | { 303 | $driver = $this->createDriver(); 304 | $this->setProtectedProperty($driver, 'paused', false); 305 | 306 | $this->assertFalse($driver->isPaused()); 307 | } 308 | 309 | /** 310 | * 311 | */ 312 | public function testApiIsPaused_ReturnsTrue_WhenDriverIsPaused() 313 | { 314 | $driver = $this->createDriver(); 315 | $this->setProtectedProperty($driver, 'paused', true); 316 | 317 | $this->assertTrue($driver->isPaused()); 318 | } 319 | 320 | /** 321 | * Create Shell driver. 322 | * 323 | * @param string[] $methods 324 | * @param mixed 325 | * @return Shell|\PHPUnit_Framework_MockObject_MockObject 326 | */ 327 | public function createDriver($methods = null, $constructorParams = []) 328 | { 329 | if (isset($constructorParams['ssh2'])) 330 | { 331 | $ssh2 = $constructorParams['ssh2']; 332 | } 333 | else 334 | { 335 | $timer = $this->getMock(TimerInterface::class, [], [], '', false); 336 | $loop = $this->getMock(Loop::class, [ 'addPeriodicTimer' ], [], '', false); 337 | $loop 338 | ->expects($this->any()) 339 | ->method('addPeriodicTimer') 340 | ->will($this->returnValue($timer)); 341 | 342 | $ssh2 = $this->getMock(SSH2Interface::class, [], [], '', false); 343 | $ssh2 344 | ->expects($this->any()) 345 | ->method('getLoop') 346 | ->will($this->returnValue($loop)); 347 | } 348 | 349 | $conn = isset($constructorParams['conn']) ? $constructorParams['conn'] : fopen('php://memory', 'r+'); 350 | 351 | $mock = $this->getMock(Shell::class, $methods, [ $ssh2, $conn ]); 352 | 353 | return $mock; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /test/TUnit/SSH2Test.php: -------------------------------------------------------------------------------- 1 | createSSH2(); 25 | $this->assertInstanceOf(SSH2Interface::class, $ssh2); 26 | } 27 | 28 | /** 29 | * 30 | */ 31 | public function testDestructor_DoesNotThrowThrowable() 32 | { 33 | $ssh2 = $this->createSSH2(); 34 | unset($ssh2); 35 | } 36 | 37 | /** 38 | * 39 | */ 40 | public function testApiConnect_DoesNothing_WhenConnectionIsNotNull() 41 | { 42 | $ssh2 = $this->createSSH2(); 43 | $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); 44 | 45 | $ssh2->on('error', $this->expectCallableNever()); 46 | $ssh2->on('connect', $this->expectCallableNever()); 47 | 48 | $ssh2->connect(); 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() 55 | { 56 | $ssh2 = $this->createSSH2([ 'createConnection' ]); 57 | $ssh2 58 | ->expects($this->once()) 59 | ->method('createConnection') 60 | ->will($this->returnValue(false)); 61 | 62 | $callback = $this->createCallableMock(); 63 | $callback 64 | ->expects($this->once()) 65 | ->method('__invoke') 66 | ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); 67 | 68 | $ssh2->on('error', $callback); 69 | $ssh2->on('connect', $this->expectCallableNever()); 70 | 71 | $ssh2->connect(); 72 | } 73 | 74 | /** 75 | * 76 | */ 77 | public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNonResource() 78 | { 79 | $ssh2 = $this->createSSH2([ 'createConnection' ]); 80 | $ssh2 81 | ->expects($this->once()) 82 | ->method('createConnection') 83 | ->will($this->returnValue(true)); 84 | 85 | $callback = $this->createCallableMock(); 86 | $callback 87 | ->expects($this->once()) 88 | ->method('__invoke') 89 | ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); 90 | 91 | $ssh2->on('error', $callback); 92 | $ssh2->on('connect', $this->expectCallableNever()); 93 | 94 | $ssh2->connect(); 95 | } 96 | 97 | /** 98 | * 99 | */ 100 | public function testApiConnect_EmitsErrorEvent_WhenAuthenticationIsInvalid() 101 | { 102 | $stream = fopen('php://memory', 'r'); 103 | 104 | $auth = $this->getMock(SSH2AuthInterface::class, [ 'authenticate' ], [], '', false); 105 | $auth 106 | ->expects($this->once()) 107 | ->method('authenticate') 108 | ->will($this->returnValue(false)); 109 | 110 | $ssh2 = $this->createSSH2([ 'createConnection' ], [ 'auth' => $auth ]); 111 | $ssh2 112 | ->expects($this->once()) 113 | ->method('createConnection') 114 | ->will($this->returnValue($stream)); 115 | 116 | $callback = $this->createCallableMock(); 117 | $callback 118 | ->expects($this->once()) 119 | ->method('__invoke') 120 | ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); 121 | 122 | $ssh2->on('error', $callback); 123 | $ssh2->on('connect', $this->expectCallableNever()); 124 | 125 | $ssh2->connect(); 126 | } 127 | 128 | /** 129 | * 130 | */ 131 | public function testApiConnect_EmitsConnectEvent() 132 | { 133 | $stream = fopen('php://memory', 'r'); 134 | 135 | $auth = $this->getMock(SSH2AuthInterface::class, [ 'authenticate' ], [], '', false); 136 | $auth 137 | ->expects($this->once()) 138 | ->method('authenticate') 139 | ->will($this->returnValue(true)); 140 | 141 | $ssh2 = $this->createSSH2([ 'createConnection' ], [ 'auth' => $auth ]); 142 | $ssh2 143 | ->expects($this->once()) 144 | ->method('createConnection') 145 | ->will($this->returnValue($stream)); 146 | 147 | $callback = $this->createCallableMock(); 148 | $callback 149 | ->expects($this->once()) 150 | ->method('__invoke') 151 | ->with($ssh2); 152 | 153 | $ssh2->on('error', $this->expectCallableNever()); 154 | $ssh2->on('connect', $callback); 155 | 156 | $ssh2->connect(); 157 | } 158 | 159 | /** 160 | * 161 | */ 162 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() 163 | { 164 | $ssh2 = $this->createSSH2(); 165 | 166 | $ssh2->on('disconnect', $this->expectCallableNever()); 167 | 168 | $ssh2->disconnect(); 169 | } 170 | 171 | /** 172 | * 173 | */ 174 | public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() 175 | { 176 | $ssh2 = $this->createSSH2(); 177 | $this->setProtectedProperty($ssh2, 'conn', true); 178 | 179 | $ssh2->on('disconnect', $this->expectCallableNever()); 180 | 181 | $ssh2->disconnect(); 182 | } 183 | 184 | /** 185 | * 186 | */ 187 | public function testApiDisconnect_EmitsDisconnectEvent() 188 | { 189 | $ssh2 = $this->createSSH2(); 190 | $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); 191 | 192 | $callback = $this->createCallableMock(); 193 | $callback 194 | ->expects($this->once()) 195 | ->method('__invoke') 196 | ->with($ssh2); 197 | 198 | $ssh2->on('disconnect', $callback); 199 | 200 | $ssh2->disconnect(); 201 | } 202 | 203 | /** 204 | * 205 | */ 206 | public function testApiDisconnect_CallsDisconnectMethodAndRemovesListeners_OnEachDriver() 207 | { 208 | $ssh2 = $this->createSSH2(); 209 | 210 | $stream = fopen('php://memory', 'r'); 211 | 212 | $driver1 = $this->getMock(SSH2DriverInterface::class, [], [], '', false); 213 | $driver1 214 | ->expects($this->once()) 215 | ->method('disconnect'); 216 | $driver1 217 | ->expects($this->exactly(3)) 218 | ->method('removeListener') 219 | ->with($this->isType('string'), $this->isType('callable')); 220 | 221 | $driver2 = $this->getMock(SSH2DriverInterface::class, [], [], '', false); 222 | $driver2 223 | ->expects($this->once()) 224 | ->method('disconnect'); 225 | $driver2 226 | ->expects($this->exactly(3)) 227 | ->method('removeListener') 228 | ->with($this->isType('string'), $this->isType('callable')); 229 | 230 | $drivers = [ 'd1' => $driver1, 'd2' => $driver2 ]; 231 | 232 | $this->setProtectedProperty($ssh2, 'conn', $stream); 233 | $this->setProtectedProperty($ssh2, 'drivers', $drivers); 234 | 235 | $callback = $this->createCallableMock(); 236 | $callback 237 | ->expects($this->once()) 238 | ->method('__invoke') 239 | ->with($ssh2); 240 | 241 | $ssh2->on('disconnect', $callback); 242 | 243 | $ssh2->disconnect(); 244 | } 245 | 246 | /** 247 | * 248 | */ 249 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() 250 | { 251 | $ssh2 = $this->createSSH2(); 252 | 253 | $this->assertFalse($ssh2->isConnected()); 254 | } 255 | 256 | /** 257 | * 258 | */ 259 | public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() 260 | { 261 | $ssh2 = $this->createSSH2(); 262 | $this->setProtectedProperty($ssh2, 'conn', true); 263 | 264 | $this->assertFalse($ssh2->isConnected()); 265 | } 266 | 267 | /** 268 | * 269 | */ 270 | public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() 271 | { 272 | $ssh2 = $this->createSSH2(); 273 | $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); 274 | 275 | $this->assertTrue($ssh2->isConnected()); 276 | } 277 | 278 | /** 279 | * 280 | */ 281 | public function testApiCreateDriver_ReturnsDriver_WhenDriverDoesExist() 282 | { 283 | $ssh2 = $this->createSSH2(); 284 | 285 | $driver = $this->getMock(SSH2DriverInterface::class, [], [], '', false); 286 | $drivers = [ 'name' => $driver ]; 287 | 288 | $this->setProtectedProperty($ssh2, 'drivers', $drivers); 289 | 290 | $this->assertSame($driver, $ssh2->createDriver('name')); 291 | } 292 | 293 | /** 294 | * 295 | */ 296 | public function testApiCreateDriver_ThrowsException_WhenConnectionIsNotEstablished() 297 | { 298 | $ssh2 = $this->createSSH2(); 299 | 300 | $this->setExpectedException(ExecutionException::class); 301 | $ssh2->createDriver('name'); 302 | } 303 | 304 | /** 305 | * 306 | */ 307 | public function testApiCreateDriver_ThrowsException_WhenInvalidDriverIsRequested() 308 | { 309 | $ssh2 = $this->createSSH2([ 'isConnected' ]); 310 | $ssh2 311 | ->expects($this->once()) 312 | ->method('isConnected') 313 | ->will($this->returnValue(true)); 314 | 315 | $this->setExpectedException(InvalidArgumentException::class); 316 | $ssh2->createDriver('invalidDriver'); 317 | } 318 | 319 | /** 320 | * 321 | */ 322 | public function testApiCreateDriver_CreatesDriver_WhenShellIsRequested() 323 | { 324 | $ssh2 = $this->createSSH2([ 'isConnected' ]); 325 | $ssh2 326 | ->expects($this->once()) 327 | ->method('isConnected') 328 | ->will($this->returnValue(true)); 329 | 330 | $driver = $ssh2->createDriver(SSH2::DRIVER_SHELL); 331 | 332 | $this->assertInstanceOf(SSH2DriverInterface::class, $driver); 333 | $this->assertInstanceOf(Shell::class, $driver); 334 | } 335 | 336 | /** 337 | * 338 | */ 339 | public function testApiCreateDriver_CreatesDriver_WhenSftpIsRequested() 340 | { 341 | $ssh2 = $this->createSSH2([ 'isConnected' ]); 342 | $ssh2 343 | ->expects($this->once()) 344 | ->method('isConnected') 345 | ->will($this->returnValue(true)); 346 | 347 | $driver = $ssh2->createDriver(SSH2::DRIVER_SFTP); 348 | 349 | $this->assertInstanceOf(SSH2DriverInterface::class, $driver); 350 | $this->assertInstanceOf(Sftp::class, $driver); 351 | } 352 | 353 | /** 354 | * Create SSH2 driver. 355 | * 356 | * @param string[]|null $methods 357 | * @param mixed 358 | * @return SSH2|\PHPUnit_Framework_MockObject_MockObject 359 | */ 360 | public function createSSH2($methods = null, $constructorParams = []) 361 | { 362 | $loop = isset($constructorParams['loop']) 363 | ? $constructorParams['loop'] 364 | : $this->getMock(LoopInterface::class, [], [], '', false); 365 | 366 | $auth = isset($constructorParams['auth']) 367 | ? $constructorParams['auth'] 368 | : $this->getMock(SSH2AuthInterface::class, [], [], '', false); 369 | 370 | $config = new SSH2Config(); 371 | 372 | return $this->getMock(SSH2::class, $methods, [ $auth, $config, $loop ]); 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /test/TModule/SSH2Test.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); 25 | } 26 | 27 | $sim = $this; 28 | $sim = $sim->simulate(function(SimulationInterface $sim) { 29 | $loop = $sim->getLoop(); 30 | $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); 31 | $config = new SSH2Config(); 32 | $ssh2 = new SSH2($auth, $config, $loop); 33 | 34 | $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { 35 | $sim->expect('connect'); 36 | $ssh->disconnect(); 37 | }); 38 | 39 | $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { 40 | $sim->expect('disconnect'); 41 | $sim->done(); 42 | }); 43 | 44 | $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { 45 | $sim->fail($ex->getMessage()); 46 | }); 47 | 48 | $sim->onStart(function() use($ssh2) { 49 | $ssh2->connect(); 50 | }); 51 | $sim->onStop(function() use($ssh2) { 52 | $ssh2->disconnect(); 53 | }); 54 | 55 | }); 56 | $sim = $sim->expect([ 57 | [ 'connect', [] ], 58 | [ 'disconnect', [] ] 59 | ]); 60 | } 61 | 62 | /** 63 | * 64 | */ 65 | public function testSSH2_ShellDriver_IsAbleToExecute_SingleLineCommand() 66 | { 67 | if (!extension_loaded('ssh2')) 68 | { 69 | $this->markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); 70 | } 71 | 72 | $sim = $this; 73 | $sim = $sim->simulate(function(SimulationInterface $sim) { 74 | $loop = $sim->getLoop(); 75 | $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); 76 | $config = new SSH2Config(); 77 | $ssh2 = new SSH2($auth, $config, $loop); 78 | 79 | $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($sim) { 80 | $sim->expect('connect:shell'); 81 | $buffer = ''; 82 | 83 | $command = $shell->open(); 84 | $command->write('echo "test"'); 85 | $command->on('data', function($command, $data) use(&$buffer) { 86 | $buffer .= $data; 87 | }); 88 | $command->on('end', function() use(&$buffer, $shell, $sim) { 89 | $sim->expect('buffer', [ $buffer ]); 90 | $shell->disconnect(); 91 | }); 92 | }); 93 | 94 | $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($sim, $ssh2) { 95 | $sim->expect('disconnect:shell'); 96 | $ssh2->disconnect(); 97 | }); 98 | 99 | $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { 100 | $sim->expect('connect'); 101 | $ssh->createDriver(SSH2::DRIVER_SHELL) 102 | ->connect(); 103 | }); 104 | 105 | $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { 106 | $sim->expect('disconnect'); 107 | $sim->done(); 108 | }); 109 | 110 | $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { 111 | $sim->fail($ex->getMessage()); 112 | }); 113 | 114 | $sim->onStart(function() use($ssh2) { 115 | $ssh2->connect(); 116 | }); 117 | $sim->onStop(function() use($ssh2) { 118 | $ssh2->disconnect(); 119 | }); 120 | }); 121 | $sim = $sim->expect([ 122 | [ 'connect', [] ], 123 | [ 'connect:shell', [] ], 124 | [ 'buffer', [ "test\r\n" ] ], 125 | [ 'disconnect:shell', [] ], 126 | [ 'disconnect', [] ] 127 | ]); 128 | } 129 | 130 | /** 131 | * 132 | */ 133 | public function testSSH2_ShellDriver_IsAbleToExecute_MultiLineCommand() 134 | { 135 | if (!extension_loaded('ssh2')) 136 | { 137 | $this->markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); 138 | } 139 | 140 | $sim = $this; 141 | $sim = $sim->simulate(function(SimulationInterface $sim) { 142 | $loop = $sim->getLoop(); 143 | $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); 144 | $config = new SSH2Config(); 145 | $ssh2 = new SSH2($auth, $config, $loop); 146 | 147 | $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($sim) { 148 | $sim->expect('connect:shell'); 149 | 150 | $buffer = ''; 151 | $command = $shell->open(); 152 | $command->write('printf "A\nB\nC\n"'); 153 | $command->on('data', function($command, $data) use(&$buffer) { 154 | $buffer .= $data; 155 | }); 156 | $command->on('end', function() use(&$buffer, $shell, $sim) { 157 | $sim->expect('buffer', [ $buffer ]); 158 | $shell->disconnect(); 159 | }); 160 | }); 161 | 162 | $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($sim, $ssh2) { 163 | $sim->expect('disconnect:shell'); 164 | $ssh2->disconnect(); 165 | }); 166 | 167 | $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { 168 | $sim->expect('connect'); 169 | $ssh->createDriver(SSH2::DRIVER_SHELL) 170 | ->connect(); 171 | }); 172 | 173 | $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { 174 | $sim->expect('disconnect'); 175 | $sim->done(); 176 | }); 177 | 178 | $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { 179 | $sim->fail($ex->getMessage()); 180 | }); 181 | 182 | $sim->onStart(function() use($ssh2) { 183 | $ssh2->connect(); 184 | }); 185 | $sim->onStop(function() use($ssh2) { 186 | $ssh2->disconnect(); 187 | }); 188 | }); 189 | $sim = $sim->expect([ 190 | [ 'connect', [] ], 191 | [ 'connect:shell', [] ], 192 | [ 'buffer', [ "A\r\nB\r\nC\r\n" ] ], 193 | [ 'disconnect:shell', [] ], 194 | [ 'disconnect', [] ] 195 | ]); 196 | } 197 | 198 | /** 199 | * 200 | */ 201 | public function testSSH2_SftpDriver_IsAbleToWriteFiles() 202 | { 203 | $this->markTestSkipped('It seems there are problems with setting this automation on Travis.'); 204 | 205 | $sim = $this; 206 | $sim = $sim->simulate(function(SimulationInterface $sim) { 207 | $loop = $sim->getLoop(); 208 | $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); 209 | $config = new SSH2Config(); 210 | $ssh2 = new SSH2($auth, $config, $loop); 211 | 212 | $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($sim) { 213 | $sim->expect('connect:sftp'); 214 | 215 | $lines = [ "DAZZLE\n", "IS\n", "AWESOME!\n" ]; 216 | $linesPointer = 0; 217 | 218 | $file = $sftp->open(__DIR__ . '/_Data/file_write.txt', 'w+'); 219 | $file->write(); 220 | $file->on('drain', function(SSH2ResourceInterface $file) use(&$lines, &$linesPointer) { 221 | if ($linesPointer < count($lines)) { 222 | $file->write($lines[$linesPointer++]); 223 | } 224 | }); 225 | $file->on('finish', function(SSH2ResourceInterface $file) { 226 | $file->close(); 227 | }); 228 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 229 | $sftp->disconnect(); 230 | }); 231 | }); 232 | 233 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($sim, $ssh2) { 234 | $sim->expect('disconnect:sftp'); 235 | $ssh2->disconnect(); 236 | }); 237 | 238 | $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { 239 | $sim->expect('connect'); 240 | $ssh->createDriver(SSH2::DRIVER_SFTP) 241 | ->connect(); 242 | }); 243 | 244 | $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { 245 | $sim->expect('disconnect'); 246 | $sim->done(); 247 | }); 248 | 249 | $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { 250 | $sim->fail($ex->getMessage()); 251 | }); 252 | 253 | $sim->onStart(function() use($ssh2) { 254 | $ssh2->connect(); 255 | }); 256 | $sim->onStop(function() use($ssh2) { 257 | $ssh2->disconnect(); 258 | }); 259 | }); 260 | $sim = $sim->expect([ 261 | [ 'connect', [] ], 262 | [ 'connect:sftp', [] ], 263 | [ 'disconnect:sftp', [] ], 264 | [ 'disconnect', [] ] 265 | ]); 266 | } 267 | 268 | /** 269 | * 270 | */ 271 | public function testSSH2_SftpDriver_IsAbleToReadFiles() 272 | { 273 | $this->markTestSkipped('It seems there are problems with setting this automation on Travis.'); 274 | 275 | $sim = $this; 276 | $sim = $sim->simulate(function(SimulationInterface $sim) { 277 | $loop = $sim->getLoop(); 278 | $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); 279 | $config = new SSH2Config(); 280 | $ssh2 = new SSH2($auth, $config, $loop); 281 | 282 | $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($sim) { 283 | $sim->expect('connect:sftp'); 284 | 285 | $buffer = ''; 286 | $file = $sftp->open(__DIR__ . '/_Data/file_read.txt', 'r+'); 287 | $file->read(); 288 | $file->on('data', function(SSH2ResourceInterface $file, $data) use(&$buffer) { 289 | $buffer .= $data; 290 | }); 291 | $file->on('end', function(SSH2ResourceInterface $file) use(&$buffer, $sim) { 292 | $sim->expect('buffer', [ $buffer ]); 293 | $file->close(); 294 | }); 295 | $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { 296 | $sftp->disconnect(); 297 | }); 298 | }); 299 | 300 | $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($sim, $ssh2) { 301 | $sim->expect('disconnect:sftp'); 302 | $ssh2->disconnect(); 303 | }); 304 | 305 | $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { 306 | $sim->expect('connect'); 307 | $ssh->createDriver(SSH2::DRIVER_SFTP) 308 | ->connect(); 309 | }); 310 | 311 | $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { 312 | $sim->expect('disconnect'); 313 | $sim->done(); 314 | }); 315 | 316 | $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { 317 | $sim->fail($ex->getMessage()); 318 | }); 319 | 320 | $sim->onStart(function() use($ssh2) { 321 | $ssh2->connect(); 322 | }); 323 | $sim->onStop(function() use($ssh2) { 324 | $ssh2->disconnect(); 325 | }); 326 | }); 327 | $sim = $sim->expect([ 328 | [ 'connect', [] ], 329 | [ 'connect:sftp', [] ], 330 | [ 'buffer', [ "DAZZLE\r\nIS\r\nAWESOME\r\n" ] ], 331 | [ 'disconnect:sftp', [] ], 332 | [ 'disconnect', [] ] 333 | ]); 334 | } 335 | } 336 | --------------------------------------------------------------------------------