Completed
Pull Request — master (#20)
by
unknown
01:46
created

Connection::getCompressedResponse()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 5
Bugs 4 Features 1
Metric Value
c 5
b 4
f 1
dl 0
loc 36
ccs 0
cts 22
cp 0
rs 8.439
cc 6
eloc 19
nc 10
nop 1
crap 42
1
<?php
2
3
/*
4
 * This file is part of the NNTP library.
5
 *
6
 * (c) Robin van der Vleuten <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Rvdv\Nntp\Connection;
13
14
use Rvdv\Nntp\Command\CommandInterface;
15
use Rvdv\Nntp\Exception\InvalidArgumentException;
16
use Rvdv\Nntp\Exception\RuntimeException;
17
use Rvdv\Nntp\Response\MultiLineResponse;
18
use Rvdv\Nntp\Response\Response;
19
20
/**
21
 * @author Robin van der Vleuten <[email protected]>
22
 */
23
class Connection implements ConnectionInterface
24
{
25
    /**
26
     * @var string
27
     */
28
    private $host;
29
30
    /**
31
     * @var int
32
     */
33
    private $port;
34
35
    /**
36
     * @var bool
37
     */
38
    private $secure;
39
40
    /**
41
     * @var resource
42
     */
43
    private $socket;
44
45
    /**
46
     * @var int
47
     */
48
    private $timeout;
49
50
    /**
51
     * Constructor.
52
     *
53
     * @param string $host    The hostname of the NNTP server.
54
     * @param int    $port    The port of the NNTP server.
55
     * @param bool   $secure  A bool indicating if a secure connection should be established.
56
     * @param int    $timeout The socket timeout in seconds.
57
     */
58 2
    public function __construct($host, $port, $secure = false, $timeout = 15)
59
    {
60 2
        $this->host = $host;
61 2
        $this->port = $port;
62 2
        $this->secure = $secure;
63 2
        $this->timeout = $timeout;
64 2
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 2
    public function connect()
70
    {
71 2
        $address = gethostbyname($this->host);
72 2
        $url = $this->getSocketUrl($address);
73
74 2
        if (!$this->socket = @stream_socket_client($url, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT)) {
75 1
            throw new RuntimeException(sprintf('Connection to %s:%d failed: %s', $address, $this->port, $errstr), $errno);
76
        }
77
78 1
        if ($this->secure) {
79
			stream_context_set_option($this->socket, 'ssl', 'peer_name', $this->host);
80
            stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
81
        }
82
83 1
        stream_set_blocking($this->socket, 0);
84
85 1
        return $this->getResponse();
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function disconnect()
92
    {
93
        if (is_resource($this->socket)) {
94
            stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
95
96
            return fclose($this->socket);
97
        }
98
99
        return false;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function sendCommand(CommandInterface $command)
106
    {
107
        $commandString = $command->execute();
108
109
        // NNTP/RFC977 only allows command up to 512 (-2 \r\n) chars.
110
        if (!strlen($commandString) > 510) {
111
            throw new InvalidArgumentException('Failed to write to socket: command exceeded 510 characters');
112
        }
113
114
        if (!@fwrite($this->socket, $commandString."\r\n")) {
115
            throw new RuntimeException('Failed to write to socket');
116
        }
117
118
        $response = $this->getResponse();
119
120
        if ($command->isMultiLine() && ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 399)) {
121
            $response = $command->isCompressed() ? $this->getCompressedResponse($response) : $this->getMultiLineResponse($response);
122
        }
123
124
        if (in_array($response->getStatusCode(), array(Response::COMMAND_UNKNOWN, Response::COMMAND_UNAVAILABLE))) {
125
            throw new RuntimeException('Sent command is either unknown or unavailable on server');
126
        }
127
128
        $expectedResponseCodes = $command->getExpectedResponseCodes();
129
130
        // Check if we received a response expected by the command.
131
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
132
            throw new RuntimeException(sprintf(
133
                'Unexpected response received: [%d] %s',
134
                $response->getStatusCode(),
135
                $response->getMessage()
136
            ));
137
        }
138
139
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
140
        if (!is_callable(array($command, $expectedResponseHandler))) {
141
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
142
        }
143
144
        $command->setResponse($response);
145
        $command->$expectedResponseHandler($response);
146
147
        return $command;
148
    }
149
    
150
    public function sendArticle(CommandInterface $command)
151
    {
152
        $commandString = $command->execute();
153
154
        if (!@fwrite($this->socket, $commandString."\r\n.\r\n")) {
155
            throw new RuntimeException('Failed to write to socket');
156
        }
157
158
        $response = $this->getResponse();
159
160
        $expectedResponseCodes = $command->getExpectedResponseCodes();
161
162
        // Check if we received a response expected by the command.
163
        if (!isset($expectedResponseCodes[$response->getStatusCode()])) {
164
            throw new RuntimeException(sprintf(
165
                'Unexpected response received: [%d] %s',
166
                $response->getStatusCode(),
167
                $response->getMessage()
168
            ));
169
        }
170
171
        $expectedResponseHandler = $expectedResponseCodes[$response->getStatusCode()];
172
        if (!is_callable(array($command, $expectedResponseHandler))) {
173
            throw new RuntimeException(sprintf('Response handler (%s) is not callable method on given command object', $expectedResponseHandler));
174
        }
175
176
        $command->setResponse($response);
177
        $command->$expectedResponseHandler($response);
178
179
        return $command;
180
    }
181
182 1
    protected function getResponse()
183
    {
184 1
        $buffer = '';
185
186 1
        while (!feof($this->socket)) {
187 1
            $buffer .= @fgets($this->socket, 1024);
188
189 1
            if ("\r\n" === substr($buffer, -2)) {
190 1
                break;
191
            }
192
193
            if ($buffer === false) {
194
                $this->disconnect();
195
                throw new RuntimeException('Incorrect data received from buffer');
196
            }
197
        }
198
199 1
        return Response::createFromString($buffer);
200
    }
201
202
    public function getMultiLineResponse(Response $response)
203
    {
204
        $buffer = '';
205
206
        while (!feof($this->socket)) {
207
            $buffer .= @fgets($this->socket, 1024);
208
209
            if ("\n.\r\n" === substr($buffer, -4)) {
210
                break;
211
            }
212
213
            if ($buffer === false) {
214
                $this->disconnect();
215
                throw new RuntimeException('Incorrect data received from buffer');
216
            }
217
        }
218
219
        $lines = explode("\r\n", trim($buffer));
220
        if (end($lines) === '.') {
221
            array_pop($lines);
222
        }
223
224
        $lines = array_filter($lines);
225
        $lines = \SplFixedArray::fromArray($lines);
226
227
        return new MultiLineResponse($response, $lines);
228
    }
229
230
    public function getCompressedResponse(Response $response)
231
    {
232
        // Determine encoding by fetching first line.
233
        $line = @fread($this->socket, 1024);
234
235
        $uncompressed = '';
236
237
        while (!feof($this->socket)) {
238
            $buffer = @fread($this->socket, 32768);
239
240
            if (strlen($buffer) === 0) {
241
                $uncompressed = @gzuncompress($line);
242
243
                if ($uncompressed !== false) {
244
                    break;
245
                }
246
            }
247
248
            if ($buffer === false) {
249
                $this->disconnect();
250
                throw new RuntimeException('Incorrect data received from buffer');
251
            }
252
253
            $line .= $buffer;
254
        }
255
256
        $lines = explode("\r\n", trim($uncompressed));
257
        if (end($lines) === '.') {
258
            array_pop($lines);
259
        }
260
261
        $lines = array_filter($lines);
262
        $lines = \SplFixedArray::fromArray($lines);
263
264
        return new MultiLineResponse($response, $lines);
265
    }
266
267
    /**
268
     * @param string $address
269
     */
270 2
    protected function getSocketUrl($address)
271
    {
272 2
        if (strpos($address, ':') !== false) {
273
            // enclose IPv6 addresses in square brackets before appending port
274
            $address = '['.$address.']';
275
        }
276
277 2
        return sprintf('tcp://%s:%s', $address, $this->port);
278
    }
279
}
280