├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── examples ├── files │ ├── archive.zip │ ├── clean.txt │ └── infected.txt └── index.php └── src ├── Driver ├── AbstractDriver.php ├── ClamdDriver.php ├── ClamdRemoteDriver.php ├── ClamscanDriver.php ├── DriverFactory.php └── DriverInterface.php ├── Exception ├── ConfigurationException.php ├── RuntimeException.php └── SocketException.php ├── Result.php ├── ResultInterface.php ├── Scanner.php ├── ScannerInterface.php ├── Socket ├── Socket.php ├── SocketFactory.php └── SocketInterface.php └── Traits └── GetOptionTrait.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .idea 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 vanagnostos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-clamav 2 | php-clamav is a PHP interface to clamd / clamscan that allows you to scan files and directories using ClamAV. 3 | 4 | Examples 5 | ======== 6 | 7 | ```PHP 8 | 'clamscan', 'executable' => '/usr/local/bin/clamdscan']; 18 | 19 | // Scan using clamd on local host, clamd must have access to the scanned files. 20 | // $config = ['driver' => 'clamd_local', 'socket' => '/usr/local/var/run/clamav/clamd.sock']; 21 | // $config = ['driver' => 'clamd_local', 'host' => '127.0.0.1', 'port' => 3310]; 22 | 23 | // Scan using clamd on remote host, directory scan is not supported. 24 | // Files will be send over the network so large files could be an issue. 25 | // $config = ['driver' => 'clamd_remote', 'host' => '127.0.0.1', 'port' => 3310]; 26 | 27 | $clamd = new \Avasil\ClamAv\Scanner($config); 28 | 29 | echo 'Ping: ', ($clamd->ping() ? 'Ok' : 'Failed'), '
'; 30 | 31 | echo 'ClamAv Version: ', $clamd->version(), '
'; 32 | 33 | $toScan = [ 34 | '../examples/files/clean.txt', 35 | '../examples/files/infected.txt', 36 | '../examples/files/', 37 | 'Lorem Ipsum Dolor', 38 | 'Lorem Ipsum Dolor X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' 39 | ]; 40 | 41 | foreach ($toScan as $f) { 42 | if (file_exists($f)) { 43 | echo 'Scanning ', $f, '
'; 44 | $result = $clamd->scan($f); 45 | } else { 46 | echo 'Scanning buffer', '
'; 47 | $result = $clamd->scanBuffer($f); 48 | } 49 | if ($result->isClean()) { 50 | echo ' - ', $result->getTarget(), ' is clean', '
'; 51 | } else { 52 | foreach ($result->getInfected() as $file => $virus) { 53 | echo ' - ', $file, ' is infected with ', $virus, '
'; 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | **This should output something like:** 60 | 61 | > Ping: Ok 62 | > ClamAv Version: ClamAV 0.99.2/21473/Thu Mar 24 20:25:24 2016 63 | > Scanning ../examples/files/clean.txt 64 | > \- ../examples/files/clean.txt is clean 65 | > Scanning ../examples/files/infected.txt 66 | > \- ../examples/files/infected.txt is infected with Eicar-Test-Signature 67 | > Scanning ../examples/files/ 68 | > \- ../examples/files/infected.txt is infected with Eicar-Test-Signature 69 | > \- ../examples/files/archive.zip is infected with Eicar-Test-Signature 70 | > Scanning buffer 71 | > \- buffer is clean 72 | > Scanning buffer 73 | > \- biffer is infected with Eicar-Test-Signature -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanagnostos/php-clamav", 3 | "description": "A PHP interface to clamd / clamscan.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Vasil Anagnostos", 8 | "email": "vasil.a@me.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Avasil\\ClamAv\\": "src/" 14 | } 15 | }, 16 | "require": { 17 | "php": ">=5.6" 18 | } 19 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "f913f071a0f8ae60418d699d475cff2e", 8 | "content-hash": "dd66d6371a944b0f48fdde57a14c30b6", 9 | "packages": [], 10 | "packages-dev": [], 11 | "aliases": [], 12 | "minimum-stability": "stable", 13 | "stability-flags": [], 14 | "prefer-stable": false, 15 | "prefer-lowest": false, 16 | "platform": { 17 | "php": ">=5.6" 18 | }, 19 | "platform-dev": [] 20 | } 21 | -------------------------------------------------------------------------------- /examples/files/archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanagnostos/php-clamav/1f7ee082e8eb9cf8a03c4cdf4e8f9d573a9e86b7/examples/files/archive.zip -------------------------------------------------------------------------------- /examples/files/clean.txt: -------------------------------------------------------------------------------- 1 | I love trains -------------------------------------------------------------------------------- /examples/files/infected.txt: -------------------------------------------------------------------------------- 1 | Lorem Ipsum Dolor X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* -------------------------------------------------------------------------------- /examples/index.php: -------------------------------------------------------------------------------- 1 | 'clamscan', 'executable' => '/usr/local/bin/clamdscan']; 11 | 12 | // Scan using clamd on local host, clamd must have access to the scanned files. 13 | // $config = ['driver' => 'clamd_local', 'socket' => '/usr/local/var/run/clamav/clamd.sock']; 14 | // $config = ['driver' => 'clamd_local', 'host' => '127.0.0.1', 'port' => 3310]; 15 | 16 | // Scan using clamd on remote host, directory scan is not supported. 17 | // Files will be send over the network so large files could be an issue. 18 | // $config = ['driver' => 'clamd_remote', 'host' => '127.0.0.1', 'port' => 3310]; 19 | 20 | $clamd = new \Avasil\ClamAv\Scanner($config); 21 | 22 | echo 'Ping: ', ($clamd->ping() ? 'Ok' : 'Failed'), '
'; 23 | 24 | echo 'ClamAv Version: ', $clamd->version(), '
'; 25 | 26 | $toScan = [ 27 | '../examples/files/clean.txt', 28 | '../examples/files/infected.txt', 29 | '../examples/files/', 30 | 'Lorem Ipsum Dolor', 31 | 'Lorem Ipsum Dolor X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' 32 | ]; 33 | 34 | foreach ($toScan as $f) { 35 | if (file_exists($f)) { 36 | echo 'Scanning ', $f, '
'; 37 | $result = $clamd->scan($f); 38 | } else { 39 | echo 'Scanning buffer', '
'; 40 | $result = $clamd->scanBuffer($f); 41 | } 42 | if ($result->isClean()) { 43 | echo ' - ', $result->getTarget(), ' is clean', '
'; 44 | } else { 45 | foreach ($result->getInfected() as $file => $virus) { 46 | echo ' - ', $file, ' is infected with ', $virus, '
'; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Driver/AbstractDriver.php: -------------------------------------------------------------------------------- 1 | options = $options; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Driver/ClamdDriver.php: -------------------------------------------------------------------------------- 1 | sendCommand('PING'); 47 | return trim($this->getResponse()) === 'PONG'; 48 | } 49 | 50 | /** 51 | * version is used to receive the version of Clamd 52 | * @return string 53 | */ 54 | public function version() 55 | { 56 | $this->sendCommand('VERSION'); 57 | return trim($this->getResponse()); 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function scan($path) 64 | { 65 | if (is_dir($path)) { 66 | $command = 'CONTSCAN'; 67 | } else { 68 | $command = 'SCAN'; 69 | } 70 | 71 | $this->sendCommand($command . ' ' . $path); 72 | 73 | $result = $this->getResponse(); 74 | 75 | return $this->filterScanResult($result); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function scanBuffer($buffer) 82 | { 83 | $this->sendCommand('INSTREAM'); 84 | 85 | $this->getSocket()->streamData($buffer); 86 | 87 | $result = $this->getResponse(); 88 | 89 | if (false != ($filtered = $this->filterScanResult($result))) { 90 | $filtered[0] = preg_replace('/^stream:/', 'buffer:', $filtered[0]); 91 | } 92 | 93 | return $filtered; 94 | } 95 | 96 | /** 97 | * @param string $command 98 | * @return int|false 99 | */ 100 | protected function sendCommand($command) 101 | { 102 | return $this->sendRequest(sprintf(static::COMMAND, $command)); 103 | } 104 | 105 | /** 106 | * @param SocketInterface $socket 107 | */ 108 | public function setSocket(SocketInterface $socket) 109 | { 110 | $this->socket = $socket; 111 | } 112 | 113 | /** 114 | * @return SocketInterface 115 | */ 116 | protected function getSocket() 117 | { 118 | if (!$this->socket) { 119 | if ($this->getOption('socket')) { // socket set in config 120 | $options = [ 121 | 'socket' => $this->getOption('socket') 122 | ]; 123 | } elseif ($this->getOption('host')) { // host set in config 124 | $options = [ 125 | 'host' => $this->getOption('host'), 126 | 'port' => $this->getOption('port', static::PORT) 127 | ]; 128 | } else { // use defaults 129 | $options = [ 130 | 'socket' => $this->getOption('socket', static::SOCKET_PATH), 131 | 'host' => $this->getOption('host', static::HOST), 132 | 'port' => $this->getOption('port', static::PORT) 133 | ]; 134 | } 135 | $this->socket = SocketFactory::create($options); 136 | } 137 | 138 | return $this->socket; 139 | } 140 | 141 | /** 142 | * @param $data 143 | * @param int $flags 144 | * @return false|int 145 | * @throws RuntimeException 146 | */ 147 | protected function sendRequest($data, $flags = 0) 148 | { 149 | if (false == ($bytes = $this->getSocket()->send($data, $flags))) { 150 | throw new RuntimeException('Cannot write to socket'); 151 | } 152 | return $bytes; 153 | } 154 | 155 | /** 156 | * @param int $flags 157 | * @return string|false 158 | */ 159 | protected function getResponse($flags = MSG_WAITALL) 160 | { 161 | $data = $this->getSocket()->receive($flags); 162 | $this->getSocket()->close(); 163 | return $data; 164 | } 165 | 166 | /** 167 | * @param string $result 168 | * @param string $filter 169 | * @return array 170 | */ 171 | protected function filterScanResult($result, $filter = 'FOUND') 172 | { 173 | $result = explode("\n", $result); 174 | $result = array_filter($result); 175 | 176 | $list = []; 177 | foreach ($result as $line) { 178 | if (substr($line, -5) === $filter) { 179 | $list[] = $line; 180 | } 181 | } 182 | return $list; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Driver/ClamdRemoteDriver.php: -------------------------------------------------------------------------------- 1 | sendCommand('INSTREAM'); 38 | 39 | $resource = fopen($path, 'r'); 40 | 41 | $this->getSocket()->streamResource($resource); 42 | 43 | fclose($resource); 44 | 45 | $result = $this->getResponse(); 46 | 47 | if (false != ($filtered = $this->filterScanResult($result))) { 48 | $filtered[0] = preg_replace('/^stream:/', $path . ':', $filtered[0]); 49 | } 50 | 51 | return $filtered; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Driver/ClamscanDriver.php: -------------------------------------------------------------------------------- 1 | getExecutable())) { 43 | throw new ConfigurationException( 44 | $this->getExecutable() ? 45 | sprintf('%s is not valid executable file', $this->getExecutable()) : 46 | 'Executable required, please check your config.' 47 | ); 48 | } 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function ping() 55 | { 56 | return !!$this->version(); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function version() 63 | { 64 | exec($this->getExecutable() . ' -V', $out, $return); 65 | if (!$return) { 66 | return $out[0]; 67 | } 68 | return ''; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | * @throws RuntimeException 74 | */ 75 | public function scan($path) 76 | { 77 | $safe_path = escapeshellarg($path); 78 | 79 | // Reset the values. 80 | $return = -1; 81 | 82 | $cmd = $this->getExecutable() . ' ' . sprintf($this->getCommand(), $safe_path); 83 | 84 | // Execute the command. 85 | exec($cmd, $out, $return); 86 | 87 | return $this->parseResults($return, $out); 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | * @throws RuntimeException 93 | */ 94 | public function scanBuffer($buffer) 95 | { 96 | $descriptorSpec = array( 97 | 0 => array("pipe", "r"), // stdin is a pipe that clamscan will read from 98 | 1 => array("pipe", "w"), // stdout is a pipe that clamscan will write to 99 | ); 100 | 101 | $cmd = $this->getExecutable() . ' ' . sprintf($this->getCommand(), '-'); 102 | 103 | $process = @ proc_open($cmd, $descriptorSpec, $pipes); 104 | 105 | if (!is_resource($process)) { 106 | throw new RuntimeException('Failed to open a process file pointer'); 107 | } 108 | 109 | // write data to stdin 110 | fwrite($pipes[0], $buffer); 111 | fclose($pipes[0]); 112 | 113 | // get response 114 | $out = stream_get_contents($pipes[1]); 115 | fclose($pipes[1]); 116 | 117 | // get return value and close 118 | $return = proc_close($process); 119 | 120 | if (false != ($parsed = $this->parseResults($return, explode("\n", $out)))) { 121 | $parsed[0] = preg_replace('/^stream:/', 'buffer:', $parsed[0]); 122 | } 123 | 124 | return $parsed; 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | protected function getExecutable() 131 | { 132 | return $this->getOption('executable', static::EXECUTABLE); 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | protected function getCommand() 139 | { 140 | return $this->getOption('command', static::COMMAND); 141 | } 142 | 143 | /** 144 | * @return int 145 | */ 146 | protected function getInfected() 147 | { 148 | return $this->getOption('infected', static::INFECTED); 149 | } 150 | 151 | /** 152 | * @return int 153 | */ 154 | protected function getClean() 155 | { 156 | return $this->getOption('clean', static::CLEAN); 157 | } 158 | 159 | /** 160 | * @param int $return 161 | * @param array $out 162 | * @return array 163 | */ 164 | private function parseResults($return, array $out) 165 | { 166 | $result = []; 167 | if ($return == $this->getInfected()) { 168 | foreach ($out as $infected) { 169 | if (empty($infected)) { 170 | break; 171 | } 172 | $result[] = $infected; 173 | } 174 | } 175 | 176 | return $result; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Driver/DriverFactory.php: -------------------------------------------------------------------------------- 1 | ClamscanDriver::class, 18 | 'clamd_local' => ClamdDriver::class, 19 | 'clamd_remote' => ClamdRemoteDriver::class, 20 | 'default' => ClamscanDriver::class, 21 | ]; 22 | 23 | /** 24 | * @inheritdoc 25 | * @throws ConfigurationException 26 | */ 27 | public static function create(array $config) 28 | { 29 | if (empty($config['driver'])) { 30 | throw new ConfigurationException('ClamAV driver required, please check your config.'); 31 | } 32 | 33 | if (!array_key_exists($config['driver'], static::DRIVERS)) { 34 | throw new ConfigurationException( 35 | sprintf( 36 | 'Invalid driver "%s" specified. Available options are: %s', 37 | $config['driver'], 38 | join(', ', array_keys(static::DRIVERS)) 39 | ) 40 | ); 41 | } 42 | 43 | $driver = static::DRIVERS[$config['driver']]; 44 | return new $driver($config); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Driver/DriverInterface.php: -------------------------------------------------------------------------------- 1 | errorCode = $socketErrorCode; 23 | if (!$message) { 24 | $message = socket_strerror($this->errorCode); 25 | } 26 | parent::__construct($message); 27 | } 28 | 29 | /** 30 | * Get socket error (returned from 'socket_last_error') 31 | * @return int 32 | */ 33 | public function getErrorCode() 34 | { 35 | return $this->errorCode; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | target = $target; 28 | $this->infected = $infected; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function isClean() 35 | { 36 | return !$this->isInfected(); 37 | } 38 | 39 | /** 40 | * @inheritdoc 41 | */ 42 | public function isInfected() 43 | { 44 | return count($this->infected); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function getInfected() 51 | { 52 | return $this->infected; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function getTarget() 59 | { 60 | return $this->target; 61 | } 62 | 63 | /** 64 | * @param string $file 65 | * @param string $virus 66 | */ 67 | public function addInfected($file, $virus) 68 | { 69 | $this->infected[$file] = $virus; 70 | } 71 | 72 | /** 73 | * @param array $infected 74 | */ 75 | public function setInfected($infected) 76 | { 77 | $this->infected = $infected; 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function __toString() 84 | { 85 | $str = []; 86 | foreach ($this->infected as $k => $v) { 87 | $str[] = $k . ': ' . $v; 88 | } 89 | return join(PHP_EOL, $str); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ResultInterface.php: -------------------------------------------------------------------------------- 1 | 'default' 20 | ]; 21 | 22 | /** 23 | * Scanner constructor. 24 | * @param array $options 25 | */ 26 | public function __construct(array $options = array()) 27 | { 28 | $this->options = array_merge($this->options, $options); 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function ping() 35 | { 36 | return $this->getDriver()->ping(); 37 | } 38 | 39 | /** 40 | * @inheritdoc 41 | */ 42 | public function version() 43 | { 44 | return $this->getDriver()->version(); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | * @throws RuntimeException 50 | */ 51 | public function scan($path) 52 | { 53 | if (!is_readable($path)) { 54 | throw new RuntimeException( 55 | sprintf('"%s" does not exist or is not readable.') 56 | ); 57 | } 58 | 59 | // make sure clamav works with real paths 60 | $real_path = realpath($path); 61 | 62 | return $this->parseResults( 63 | $path, 64 | $this->getDriver()->scan($real_path) 65 | ); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | * @throws RuntimeException 71 | */ 72 | public function scanBuffer($buffer) 73 | { 74 | if (!is_scalar($buffer) && (!is_object($buffer) || !method_exists($buffer, '__toString'))) { 75 | throw new RuntimeException( 76 | sprintf('Expected scalar value, received %s', gettype($buffer)) 77 | ); 78 | } 79 | 80 | return $this->parseResults( 81 | 'buffer', 82 | $this->getDriver()->scanBuffer($buffer) 83 | ); 84 | } 85 | 86 | /** 87 | * @return DriverInterface 88 | */ 89 | public function getDriver() 90 | { 91 | if (!$this->driver) { 92 | $this->driver = DriverFactory::create($this->options); 93 | } 94 | return $this->driver; 95 | } 96 | 97 | /** 98 | * @param DriverInterface $driver 99 | */ 100 | public function setDriver($driver) 101 | { 102 | $this->driver = $driver; 103 | } 104 | 105 | /** 106 | * @param $path 107 | * @param array $infected 108 | * @return ResultInterface 109 | */ 110 | protected function parseResults($path, array $infected) 111 | { 112 | $result = new Result($path); 113 | 114 | foreach ($infected as $line) { 115 | list($file, $virus) = explode(':', $line); 116 | $result->addInfected($file, preg_replace('/ FOUND$/', '', $virus)); 117 | } 118 | 119 | return $result; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ScannerInterface.php: -------------------------------------------------------------------------------- 1 | host = $host; 49 | } 50 | 51 | /** 52 | * @param int $port 53 | */ 54 | public function setPort($port) 55 | { 56 | $this->port = $port; 57 | } 58 | 59 | /** 60 | * @param int $path 61 | */ 62 | public function setPath($path) 63 | { 64 | $this->path = $path; 65 | } 66 | 67 | /** 68 | * @return void 69 | */ 70 | public function close() 71 | { 72 | if (is_resource($this->socket)) { 73 | socket_close($this->socket); 74 | $this->socket = null; 75 | } 76 | } 77 | 78 | /** 79 | * @param $data 80 | * @param int $flags 81 | * @return false|int 82 | */ 83 | public function send($data, $flags = 0) 84 | { 85 | $this->reconnect(); 86 | 87 | return socket_send($this->socket, $data, strlen($data), $flags); 88 | } 89 | 90 | /** 91 | * @param $resource 92 | * @return false|int 93 | */ 94 | public function streamResource($resource) 95 | { 96 | $this->reconnect(); 97 | 98 | $result = 0; 99 | while ($chunk = fread($resource, self::BYTES_WRITE)) { 100 | $result += $this->sendChunk($chunk); 101 | } 102 | 103 | $result += $this->endStream(); 104 | 105 | return $result; 106 | } 107 | 108 | /** 109 | * @param $data 110 | * @return false|int 111 | */ 112 | public function streamData($data) 113 | { 114 | $this->reconnect(); 115 | 116 | $result = 0; 117 | $left = $data; 118 | while (strlen($left) > 0) { 119 | $chunk = substr($left, 0, self::BYTES_WRITE); 120 | $left = substr($left, self::BYTES_WRITE); 121 | $result += $this->sendChunk($chunk); 122 | } 123 | 124 | $result += $this->endStream(); 125 | 126 | return $result; 127 | } 128 | 129 | /** 130 | * @param int $flags 131 | * @return string|false 132 | * @throws RuntimeException 133 | */ 134 | public function receive($flags = MSG_WAITALL) 135 | { 136 | // $this->reconnect(); 137 | if (!is_resource($this->socket)) { 138 | throw new RuntimeException('Socket is currently closed'); 139 | } 140 | 141 | $data = ''; 142 | while ($bytes = socket_recv($this->socket, $chunk, self::BYTES_READ, $flags)) { 143 | $data .= $chunk; 144 | } 145 | 146 | return $data; 147 | } 148 | 149 | /** 150 | * @param $chunk 151 | * @return false|int 152 | */ 153 | private function sendChunk($chunk) 154 | { 155 | $size = pack('N', strlen($chunk)); 156 | // size packet 157 | $result = $this->send($size); 158 | // data packet 159 | $result += $this->send($chunk); 160 | return $result; 161 | } 162 | 163 | /** 164 | * @return false|int 165 | */ 166 | private function endStream() 167 | { 168 | $packet = pack('N', 0); 169 | return $this->send($packet); 170 | } 171 | 172 | /** 173 | * @return void 174 | */ 175 | private function connect() 176 | { 177 | $this->socket = $this->path ? 178 | $this->getUnixSocket() : 179 | $this->getInetSocket(); 180 | } 181 | 182 | /** 183 | * @return resource 184 | * @throws SocketException 185 | */ 186 | private function getInetSocket() 187 | { 188 | $socket = @socket_create(AF_INET, SOCK_STREAM, 0); 189 | if ($socket === false) { 190 | throw new SocketException('', socket_last_error()); 191 | } 192 | $hasError = @ socket_connect( 193 | $socket, 194 | $this->host, 195 | $this->port 196 | ); 197 | if ($hasError === false) { 198 | throw new SocketException('', socket_last_error()); 199 | } 200 | return $socket; 201 | } 202 | 203 | /** 204 | * @return resource 205 | * @throws SocketException 206 | */ 207 | private function getUnixSocket() 208 | { 209 | $socket = @ socket_create(AF_UNIX, SOCK_STREAM, 0); 210 | if ($socket === false) { 211 | throw new SocketException('', socket_last_error()); 212 | } 213 | $hasError = @ socket_connect($socket, $this->path); 214 | if ($hasError === false) { 215 | $errorCode = socket_last_error(); 216 | $errorMessage = socket_strerror($errorCode); 217 | throw new SocketException($errorMessage, $errorCode); 218 | } 219 | return $socket; 220 | } 221 | 222 | /** 223 | * @return void 224 | */ 225 | private function reconnect() 226 | { 227 | if (!is_resource($this->socket)) { 228 | $this->connect(); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Socket/SocketFactory.php: -------------------------------------------------------------------------------- 1 | setHost($options['host']); 29 | $instance->setPort($options['port']); 30 | } else { 31 | if (!is_readable($options['socket'])) { 32 | throw new ConfigurationException( 33 | sprintf('Socket "%s" does not exist or is not readable.', $options['socket']) 34 | ); 35 | } 36 | $instance->setPath($options['socket']); 37 | } 38 | return $instance; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Socket/SocketInterface.php: -------------------------------------------------------------------------------- 1 | options[$key]) ? $this->options[$key] : $default; 18 | } 19 | } 20 | --------------------------------------------------------------------------------