HttpClientTrait::checkConnection()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.8333
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
1
<?php
2
/**
3
 * This file is part of the PHP Generics package.
4
 *
5
 * @package Generics
6
 */
7
namespace Generics\Client;
8
9
use Generics\Streams\InputOutputStream;
10
use Generics\Streams\InputStream;
11
use Generics\Streams\MemoryStream;
12
use Generics\Socket\Endpoint;
13
use Generics\Socket\SocketException;
14
15
/**
16
 * This trait provides common http(s) client functionality
17
 *
18
 * @author Maik Greubel <[email protected]>
19
 */
20
trait HttpClientTrait
21
{
22
    use HttpHeadersTrait;
23
24
    /**
25
     * The query string
26
     *
27
     * @var string
28
     */
29
    private $queryString;
30
31
    /**
32
     * The payload
33
     *
34
     * @var MemoryStream
35
     */
36
    private $payload;
37
38
    /**
39
     * The HTTP protocol version
40
     *
41
     * @var string
42
     */
43
    private $protocol;
44
45
    /**
46
     * Path to file on server (excluding endpoint address)
47
     *
48
     * @var string
49
     */
50
    private $path;
51
52
    /**
53
     * When the connection times out (in seconds)
54
     *
55
     * @var int
56
     */
57
    private $timeout;
58
59
    /**
60
     * Load headers from remote and return it
61
     *
62
     * @return array
63
     */
64 2
    public function retrieveHeaders(): array
65
    {
66 2
        $this->setHeader('Connection', 'close');
67 2
        $this->setHeader('Accept', '');
68 2
        $this->setHeader('Accept-Language', '');
69 2
        $this->setHeader('User-Agent', '');
70
        
71 2
        $savedProto = $this->protocol;
72 2
        $this->protocol = 'HTTP/1.0';
73 2
        $this->request('HEAD');
74 2
        $this->protocol = $savedProto;
75
        
76 2
        return $this->getHeaders();
77
    }
78
79
    /**
80
     *
81
     * {@inheritdoc}
82
     * @see \Generics\Streams\HttpStream::appendPayload()
83
     */
84 2
    public function appendPayload(InputStream $payload)
85
    {
86 2
        while ($payload->ready()) {
87 2
            $this->payload->write($payload->read(1024));
88
        }
89 2
    }
90
91
    /**
92
     *
93
     * {@inheritdoc}
94
     * @see \Generics\Streams\HttpStream::getPayload()
95
     */
96 8
    public function getPayload(): InputOutputStream
97
    {
98 8
        return $this->payload;
99
    }
100
101
    /**
102
     * Set connection timeout in seconds
103
     *
104
     * @param int $timeout
105
     */
106 25
    public function setTimeout($timeout)
107
    {
108 25
        $timeout = intval($timeout);
109 25
        if ($timeout < 1 || $timeout > 60) {
110 2
            $timeout = 5;
111
        }
112 25
        $this->timeout = $timeout;
113 25
    }
114
115
    /**
116
     *
117
     * {@inheritdoc}
118
     * @see \Generics\Resettable::reset()
119
     */
120 25
    public function reset()
121
    {
122 25
        if (null == $this->payload) {
123 25
            $this->payload = new MemoryStream();
124
        }
125 25
        $this->payload->reset();
126 25
    }
127
128
    /**
129
     * Prepare the request buffer
130
     *
131
     * @param string $requestType
132
     * @return \Generics\Streams\MemoryStream
133
     * @throws \Generics\Streams\StreamException
134
     */
135 22
    private function prepareRequest($requestType): MemoryStream
136
    {
137 22
        $ms = new MemoryStream();
138
        
139
        // First send the request type
140 22
        $ms->interpolate("{rqtype} {path}{query} {proto}\r\n", array(
141 22
            'rqtype' => $requestType,
142 22
            'path' => $this->path,
143 22
            'proto' => $this->protocol,
144 22
            'query' => (strlen($this->queryString) ? '?' . $this->queryString : '')
145
        ));
146
        
147
        // Add the host part
148 22
        $ms->interpolate("Host: {host}\r\n", array(
149 22
            'host' => $this->getEndpoint()
150 22
                ->getAddress()
151
        ));
152
        
153 22
        $this->adjustHeaders($requestType);
154
        
155
        // Add all existing headers
156 22
        foreach ($this->getHeaders() as $headerName => $headerValue) {
157 22
            if (isset($headerValue) && strlen($headerValue) > 0) {
158 22
                $ms->interpolate("{headerName}: {headerValue}\r\n", array(
159 22
                    'headerName' => $headerName,
160 22
                    'headerValue' => $headerValue
161
                ));
162
            }
163
        }
164
        
165 22
        $ms->write("\r\n");
166
        
167 22
        return $ms;
168
    }
169
170
    /**
171
     * Set the query string
172
     *
173
     * @param string $queryString
174
     */
175 25
    public function setQueryString(string $queryString)
176
    {
177 25
        $this->queryString = $queryString;
178 25
    }
179
180
    /**
181
     * Retrieve and parse the response
182
     *
183
     * @param string $requestType
184
     * @throws \Generics\Client\HttpException
185
     * @throws \Generics\Socket\SocketException
186
     * @throws \Generics\Streams\StreamException
187
     */
188 20
    private function retrieveAndParseResponse($requestType)
189
    {
190 20
        $this->payload = new MemoryStream();
191 20
        $this->headers = array();
192
        
193 20
        $delimiterFound = false;
194
        
195 20
        $tmp = "";
196 20
        $numBytes = 1;
197 20
        $start = time();
198 20
        while (true) {
199 20
            if (! $this->checkConnection($start)) {
200 20
                continue;
201
            }
202
            
203 18
            $c = $this->read($numBytes);
204
            
205 18
            if ($c == null) {
206
                break;
207
            }
208
            
209 18
            $start = time(); // we have readen something => adjust timeout start point
210 18
            $tmp .= $c;
211
            
212 18
            if (! $delimiterFound) {
213 18
                $this->handleHeader($delimiterFound, $numBytes, $tmp);
214
            }
215
            
216 18
            if ($delimiterFound) {
217 18
                if ($requestType == 'HEAD') {
218
                    // Header readen, in type HEAD it is now time to leave
219 4
                    break;
220
                }
221
                
222
                // delimiter already found, append to payload
223 14
                $this->payload->write($tmp);
224 14
                $tmp = "";
225
                
226 14
                if ($this->checkContentLengthExceeded()) {
227 14
                    break;
228
                }
229
            }
230
        }
231
        
232 18
        $size = $this->payload->count();
233 18
        if ($size == 0) {
234 4
            return;
235
        }
236
        // Set pointer to start
237 14
        $this->payload->reset();
238
        
239 14
        $mayCompressed = $this->payload->read($size);
240 14
        switch ($this->getContentEncoding()) {
241 14
            case 'gzip':
242 2
                $uncompressed = gzdecode(strstr($mayCompressed, "\x1f\x8b"));
243 2
                $this->payload->flush();
244 2
                $this->payload->write($uncompressed);
245 2
                break;
246
            
247 12
            case 'deflate':
248 2
                $uncompressed = gzuncompress($mayCompressed);
249 2
                $this->payload->flush();
250 2
                $this->payload->write($uncompressed);
251 2
                break;
252
            
253
            default:
254
                // nothing
255 10
                break;
256
        }
257 14
        $this->payload->reset();
258 14
    }
259
260
    /**
261
     * Append the payload buffer to the request buffer
262
     *
263
     * @param MemoryStream $ms
264
     * @return MemoryStream
265
     * @throws \Generics\Streams\StreamException
266
     * @throws \Generics\ResetException
267
     */
268 22
    private function appendPayloadToRequest(MemoryStream $ms): MemoryStream
269
    {
270 22
        $this->payload->reset();
271
        
272 22
        while ($this->payload->ready()) {
273 22
            $ms->write($this->payload->read(1024));
274
        }
275
        
276 22
        $ms->reset();
277
        
278 22
        return $ms;
279
    }
280
281
    /**
282
     * Handle a header line
283
     *
284
     * All parameters by reference, which means the the values can be
285
     * modified during execution of this method.
286
     *
287
     * @param boolean $delimiterFound
288
     *            Whether the delimiter for end of header section was found
289
     * @param int $numBytes
290
     *            The number of bytes to read from remote
291
     * @param string $tmp
292
     *            The current readen line
293
     */
294 18
    private function handleHeader(&$delimiterFound, &$numBytes, &$tmp)
295
    {
296 18
        if ($tmp == "\r\n") {
297 18
            $numBytes = $this->adjustNumbytes($numBytes);
298 18
            $delimiterFound = true;
299 18
            $tmp = "";
300 18
            return;
301
        }
302
        
303 18
        if (substr($tmp, - 2, 2) == "\r\n") {
304 18
            $this->addParsedHeader($tmp);
305 18
            $tmp = "";
306
        }
307 18
    }
308
309
    /**
310
     * Perform the request
311
     *
312
     * @param string $requestType
313
     */
314 22
    private function requestImpl(string $requestType)
315
    {
316 22
        if ($requestType == 'HEAD') {
317 4
            $this->setTimeout(1); // Don't wait too long on simple head
318
        }
319
        
320 22
        $ms = $this->prepareRequest($requestType);
321
        
322 22
        $ms = $this->appendPayloadToRequest($ms);
323
        
324 22
        if (! $this->isConnected()) {
325 20
            $this->connect();
326
        }
327
        
328 21
        while ($ms->ready()) {
329 21
            $this->write($ms->read(1024));
330
        }
331
        
332 20
        $this->retrieveAndParseResponse($requestType);
333
        
334 18
        if ($this->getHeader('Connection') == 'close') {
335 6
            $this->disconnect();
336
        }
337 18
    }
338
339
    /**
340
     * Check the connection availability
341
     *
342
     * @param int $start
343
     *            Timestamp when read request attempt starts
344
     * @throws HttpException
345
     * @return bool
346
     */
347 20
    private function checkConnection($start): bool
348
    {
349 20
        if (! $this->ready()) {
350 20
            if (time() - $start > $this->timeout) {
351 2
                $this->disconnect();
352 2
                throw new HttpException("Connection timed out!");
353
            }
354
            
355 20
            return false;
356
        }
357
        
358 18
        return true;
359
    }
360
361
    /**
362
     * Check whether the readen bytes amount has reached the
363
     * content length amount
364
     *
365
     * @return bool
366
     */
367 14
    private function checkContentLengthExceeded(): bool
368
    {
369 14
        if (isset($this->headers['Content-Length'])) {
370 14
            if ($this->payload->count() >= $this->headers['Content-Length']) {
371 14
                return true;
372
            }
373
        }
374 14
        return false;
375
    }
376
377
    /**
378
     * Set the used protocol
379
     *
380
     * @param string $protocol
381
     */
382 25
    private function setProtocol(string $protocol)
383
    {
384 25
        $this->protocol = $protocol;
385 25
    }
386
387
    /**
388
     * Set the path on remote server
389
     *
390
     * @param string $path
391
     */
392 25
    private function setPath(string $path)
393
    {
394 25
        $this->path = $path;
395 25
    }
396
397
    /**
398
     *
399
     * {@inheritdoc}
400
     * @see \Generics\Streams\HttpStream::request()
401
     */
402
    abstract public function request(string $requestType);
403
404
    /**
405
     * Get the socket endpoint
406
     *
407
     * @return \Generics\Socket\Endpoint
408
     */
409
    abstract public function getEndpoint(): Endpoint;
410
411
    /**
412
     *
413
     * {@inheritdoc}
414
     * @see \Generics\Streams\InputStream::read()
415
     */
416
    abstract public function read($length = 1, $offset = null): string;
417
418
    /**
419
     * Whether the client is connected
420
     *
421
     * @return bool
422
     */
423
    abstract public function isConnected(): bool;
424
    
425
    /**
426
     * Connect to remote endpoint
427
     *
428
     * @throws SocketException
429
     */
430
    abstract public function connect();
431
    
432
    /**
433
     * Disconnects the socket
434
     *
435
     * @throws SocketException
436
     */
437
    abstract public function disconnect();
438
    
439
    /**
440
     *
441
     * {@inheritdoc}
442
     * @see \Generics\Streams\OutputStream::write()
443
     */
444
    abstract public function write($buffer);
445
    
446
    /**
447
     *
448
     * {@inheritdoc}
449
     * @see \Generics\Streams\Stream::ready()
450
     */
451
    abstract public function ready(): bool;
452
}
453