Completed
Pull Request — master (#41)
by Joel
02:40
created

Client::determineRemoteFromRequest()   B

Complexity

Conditions 8
Paths 17

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 8.048

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 11
cp 0.9091
rs 8.4444
c 0
b 0
f 0
cc 8
nc 17
nop 1
crap 8.048
1
<?php
2
3
namespace Http\Client\Socket;
4
5
use Http\Client\HttpClient;
6
use Http\Client\Socket\Exception\ConnectionException;
7
use Http\Client\Socket\Exception\InvalidRequestException;
8
use Http\Client\Socket\Exception\SSLConnectionException;
9
use Http\Client\Socket\Exception\TimeoutException;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Symfony\Component\OptionsResolver\Options;
13
use Symfony\Component\OptionsResolver\OptionsResolver;
14
15
/**
16
 * Socket Http Client.
17
 *
18
 * Use stream and socket capabilities of the core of PHP to send HTTP requests
19
 *
20
 * @author Joel Wurtz <[email protected]>
21
 */
22
class Client implements HttpClient
23
{
24
    use RequestWriter;
25
    use ResponseReader;
26
27
    private $config = [
28
        'remote_socket' => null,
29
        'timeout' => null,
30
        'stream_context_options' => [],
31
        'stream_context_param' => [],
32
        'ssl' => null,
33
        'write_buffer_size' => 8192,
34
        'ssl_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
35
    ];
36
37
    private $hasAsync;
38
39
    /**
40
     * Constructor.
41
     *
42
     * @param array $config {
43
     *
44
     *    @var string $remote_socket          Remote entrypoint (can be a tcp or unix domain address)
45
     *    @var int    $timeout                Timeout before canceling request
46
     *    @var array  $stream_context_options Context options as defined in the PHP documentation
47
     *    @var array  $stream_context_param   Context params as defined in the PHP documentation
48
     *    @var bool   $ssl                    Use ssl, default to scheme from request, false if not present
49
     *    @var int    $write_buffer_size      Buffer when writing the request body, defaults to 8192
50
     *    @var int    $ssl_method             Crypto method for ssl/tls, see PHP doc, defaults to STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
51
     * }
52
     */
53 64
    public function __construct($config1 = [], $config2 = null, array $config = [])
0 ignored issues
show
Unused Code introduced by
The parameter $config2 is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
54
    {
55 64
        $this->hasAsync = PHP_VERSION_ID >= 70300 && \extension_loaded('async');
56
57 64
        if (\is_array($config1)) {
58 64
            $this->config = $this->configure($config1);
59
60 64
            return;
61
        }
62
63
        @trigger_error(E_USER_DEPRECATED, 'Passing a Psr\Http\Message\ResponseFactoryInterface and a Psr\Http\Message\StreamFactoryInterface to SocketClient is deprecated, and will be removed in 3.0, you should only pass config options.');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
64
65
        $this->config = $this->configure($config);
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 64
    public function sendRequest(RequestInterface $request): ResponseInterface
72
    {
73 64
        $remote = $this->config['remote_socket'];
74 64
        $useSsl = $this->config['ssl'];
75
76 64
        if (!$request->hasHeader('Connection')) {
77 13
            $request = $request->withHeader('Connection', 'close');
78
        }
79
80 64
        if (null === $remote) {
81 55
            $remote = $this->determineRemoteFromRequest($request);
82
        }
83
84 63
        if (null === $useSsl) {
85 58
            $useSsl = ('https' === $request->getUri()->getScheme());
86
        }
87
88 63
        $socket = $this->createSocket($request, $remote, $useSsl);
89
90
        try {
91 59
            $this->writeRequest($socket, $request, $this->config['write_buffer_size']);
92 59
            $response = $this->readResponse($request, $socket);
93 1
        } catch (\Exception $e) {
94 1
            $this->closeSocket($socket);
95
96 1
            throw $e;
97
        }
98
99 58
        return $response;
100
    }
101
102
    /**
103
     * Create the socket to write request and read response on it.
104
     *
105
     * @param RequestInterface $request Request for
106
     * @param string           $remote  Entrypoint for the connection
107
     * @param bool             $useSsl  Whether to use ssl or not
108
     *
109
     * @throws ConnectionException|SSLConnectionException When the connection fail
110
     *
111
     * @return resource Socket resource
112
     */
113 63
    protected function createSocket(RequestInterface $request, string $remote, bool $useSsl)
114
    {
115 63
        $errNo = null;
116 63
        $errMsg = null;
117 63
        $socket = @stream_socket_client($remote, $errNo, $errMsg, floor($this->config['timeout'] / 1000), STREAM_CLIENT_CONNECT, $this->config['stream_context']);
118
119 63
        if (false === $socket) {
120 3
            if (110 === $errNo) {
121 1
                throw new TimeoutException($errMsg, $request);
122
            }
123
124 2
            throw new ConnectionException($errMsg, $request);
125
        }
126
127 60
        stream_set_timeout($socket, floor($this->config['timeout'] / 1000), $this->config['timeout'] % 1000);
128
129 60
        if ($useSsl && false === @stream_socket_enable_crypto($socket, true, $this->config['ssl_method'])) {
130 1
            throw new SSLConnectionException(sprintf('Cannot enable tls: %s', error_get_last()['message']), $request);
131
        }
132
133 59
        return $socket;
134
    }
135
136
    /**
137
     * Close the socket, used when having an error.
138
     *
139
     * @param resource $socket
140
     */
141 1
    protected function closeSocket($socket)
142
    {
143 1
        fclose($socket);
144 1
    }
145
146
    /**
147
     * Return configuration for the socket client.
148
     *
149
     * @param array $config Configuration from user
150
     *
151
     * @return array Configuration resolved
152
     */
153 64
    protected function configure(array $config = [])
154
    {
155 64
        $resolver = new OptionsResolver();
156 64
        $resolver->setDefaults($this->config);
157 64
        $resolver->setDefault('stream_context', function (Options $options) {
158 64
            return stream_context_create($options['stream_context_options'], $options['stream_context_param']);
159 64
        });
160
161 64
        $resolver->setDefault('timeout', ini_get('default_socket_timeout') * 1000);
162
163 64
        $resolver->setAllowedTypes('stream_context_options', 'array');
164 64
        $resolver->setAllowedTypes('stream_context_param', 'array');
165 64
        $resolver->setAllowedTypes('stream_context', 'resource');
166 64
        $resolver->setAllowedTypes('ssl', ['bool', 'null']);
167
168 64
        return $resolver->resolve($config);
169
    }
170
171
    /**
172
     * Return remote socket from the request.
173
     *
174
     *
175
     * @throws InvalidRequestException When no remote can be determined from the request
176
     *
177
     * @return string
178
     */
179 55
    private function determineRemoteFromRequest(RequestInterface $request)
180
    {
181 55
        if (!$request->hasHeader('Host') && '' === $request->getUri()->getHost()) {
182 1
            throw new InvalidRequestException('Remote is not defined and we cannot determine a connection endpoint for this request (no Host header)', $request);
183
        }
184
185 54
        $host = $request->getUri()->getHost();
186 54
        $port = $request->getUri()->getPort() ?: ('https' === $request->getUri()->getScheme() ? 443 : 80);
187 54
        $endpoint = sprintf('%s:%s', $host, $port);
188
189
        // If use the host header if present for the endpoint
190 54
        if (empty($host) && $request->hasHeader('Host')) {
191 1
            $endpoint = $request->getHeaderLine('Host');
192
        }
193
194 54
        if ($this->hasAsync) {
195
            return sprintf('async-tcp://%s', $endpoint);
196
        }
197
198 54
        return sprintf('tcp://%s', $endpoint);
199
    }
200
}
201