Completed
Pull Request — master (#14)
by Antonio J.
06:07
created

Client::closeSocket()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Http\Client\Socket;
4
5
use Http\Client\Exception\NetworkException;
6
use Http\Client\HttpClient;
7
use Http\Discovery\MessageFactoryDiscovery;
8
use Http\Message\ResponseFactory;
9
use Psr\Http\Message\RequestInterface;
10
use Symfony\Component\OptionsResolver\Options;
11
use Symfony\Component\OptionsResolver\OptionsResolver;
12
13
/**
14
 * Socket Http Client.
15
 *
16
 * Use stream and socket capabilities of the core of PHP to send HTTP requests
17
 *
18
 * @author Joel Wurtz <[email protected]>
19
 */
20
class Client implements HttpClient
21
{
22
    use RequestWriter;
23
    use ResponseReader;
24
25
    private $config = [
26
        'remote_socket' => null,
27
        'timeout' => null,
28
        'stream_context_options' => [],
29
        'stream_context_param' => [],
30
        'ssl' => null,
31
        'write_buffer_size' => 8192,
32
        'ssl_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
33
    ];
34
35
    /**
36
     * Constructor.
37
     *
38
     * @param ResponseFactory $responseFactory Response factory for creating response
39
     * @param array           $config          {
40
     *
41
     *    @var string $remote_socket          Remote entrypoint (can be a tcp or unix domain address)
42
     *    @var int    $timeout                Timeout before canceling request
43
     *    @var array  $stream_context_options Context options as defined in the PHP documentation
44
     *    @var array  $stream_context_param   Context params as defined in the PHP documentation
45
     *    @var bool   $ssl                    Use ssl, default to scheme from request, false if not present
46
     *    @var int    $write_buffer_size      Buffer when writing the request body, defaults to 8192
47
     *    @var int    $ssl_method             Crypto method for ssl/tls, see PHP doc, defaults to STREAM_CRYPTO_METHOD_TLS_CLIENT
48
     * }
49
     */
50 64
    public function __construct(ResponseFactory $responseFactory = null, array $config = [])
51
    {
52 64
        if (null === $responseFactory) {
53 53
            $responseFactory = MessageFactoryDiscovery::find();
54 53
        }
55
56 64
        $this->responseFactory = $responseFactory;
57 64
        $this->config = $this->configure($config);
58 64
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 64
    public function sendRequest(RequestInterface $request)
64
    {
65 64
        $remote = $this->config['remote_socket'];
66 64
        $useSsl = $this->config['ssl'];
67
68 64
        if (!$request->hasHeader('Connection')) {
69 11
            $request = $request->withHeader('Connection', 'close');
70 11
        }
71
72 64
        if (null === $remote) {
73 58
            $remote = $this->determineRemoteFromRequest($request);
74 57
        }
75
76 63
        if (null === $useSsl) {
77 61
            $useSsl = ($request->getUri()->getScheme() === 'https');
78 61
        }
79
80 63
        $socket = $this->createSocket($request, $remote, $useSsl);
81
82
        try {
83 59
            $this->writeRequest($socket, $request, $this->config['write_buffer_size']);
84 59
            $response = $this->readResponse($request, $socket);
85 59
        } catch (\Exception $e) {
86 1
            $this->closeSocket($socket);
87
88 1
            throw $e;
89
        }
90
91 58
        return $response;
92
    }
93
94
    /**
95
     * Create the socket to write request and read response on it.
96
     *
97
     * @param RequestInterface $request Request for
98
     * @param string           $remote  Entrypoint for the connection
99
     * @param bool             $useSsl  Whether to use ssl or not
100
     *
101
     * @throws NetworkException When the connection fail
102
     *
103
     * @return resource Socket resource
104
     */
105 63
    protected function createSocket(RequestInterface $request, $remote, $useSsl)
106
    {
107 63
        $errNo = null;
108 63
        $errMsg = null;
109 63
        $socket = @stream_socket_client($remote, $errNo, $errMsg, floor($this->config['timeout'] / 1000), STREAM_CLIENT_CONNECT, $this->config['stream_context']);
110
111 63
        if (false === $socket) {
112 3
            throw new NetworkException($errMsg, $request);
113
        }
114
115 60
        stream_set_timeout($socket, floor($this->config['timeout'] / 1000), $this->config['timeout'] % 1000);
116
117 60
        if ($useSsl) {
118 3
            if (false === @stream_socket_enable_crypto($socket, true, $this->config['ssl_method'])) {
119 1
                throw new NetworkException(sprintf('Cannot enable tls: %s', error_get_last()['message']), $request);
120
            }
121 2
        }
122
123 59
        return $socket;
124
    }
125
126
    /**
127
     * Close the socket, used when having an error.
128
     *
129
     * @param resource $socket
130
     */
131 1
    protected function closeSocket($socket)
132
    {
133 1
        fclose($socket);
134 1
    }
135
136
    /**
137
     * Return configuration for the socket client.
138
     *
139
     * @param array $config Configuration from user
140
     *
141
     * @return array Configuration resolved
142
     */
143 64
    protected function configure(array $config = [])
144
    {
145 64
        $resolver = new OptionsResolver();
146 64
        $resolver->setDefaults($this->config);
147 64
        $resolver->setDefault('stream_context', function (Options $options) {
148 64
            return stream_context_create($options['stream_context_options'], $options['stream_context_param']);
149 64
        });
150
151 64
        $resolver->setDefault('timeout', ini_get('default_socket_timeout') * 1000);
152
153 64
        $resolver->setAllowedTypes('stream_context_options', 'array');
154 64
        $resolver->setAllowedTypes('stream_context_param', 'array');
155 64
        $resolver->setAllowedTypes('stream_context', 'resource');
156 64
        $resolver->setAllowedTypes('ssl', ['bool', 'null']);
157
158 64
        return $resolver->resolve($config);
159
    }
160
161
    /**
162
     * Return remote socket from the request.
163
     *
164
     * @param RequestInterface $request
165
     *
166
     * @throws NetworkException When no remote can be determined from the request
167
     *
168
     * @return string
169
     */
170 58
    private function determineRemoteFromRequest(RequestInterface $request)
171
    {
172 58
        if ($request->getUri()->getHost() == '' && !$request->hasHeader('Host')) {
173 1
            throw new NetworkException('Cannot find connection endpoint for this request', $request);
174
        }
175
176 57
        $host = $request->getUri()->getHost();
177 57
        $port = $request->getUri()->getPort() ?: ($request->getUri()->getScheme() == 'https' ? 443 : 80);
178 57
        $endpoint = sprintf('%s:%s', $host, $port);
179
180
        // If use the host header if present for the endpoint
181 57
        if (empty($host) && $request->hasHeader('Host')) {
182 1
            $endpoint = $request->getHeaderLine('Host');
183 1
        }
184
185 57
        return sprintf('tcp://%s', $endpoint);
186
    }
187
}
188