├── .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 |
--------------------------------------------------------------------------------