1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace RedirectionIO\Client\Sdk; |
4
|
|
|
|
5
|
|
|
use Psr\Log\LoggerInterface; |
6
|
|
|
use Psr\Log\NullLogger; |
7
|
|
|
use RedirectionIO\Client\Sdk\Exception\AgentNotFoundException; |
8
|
|
|
use RedirectionIO\Client\Sdk\Exception\BadConfigurationException; |
9
|
|
|
use RedirectionIO\Client\Sdk\Exception\ExceptionInterface; |
10
|
|
|
use RedirectionIO\Client\Sdk\HttpMessage\RedirectResponse; |
11
|
|
|
use RedirectionIO\Client\Sdk\HttpMessage\Request; |
12
|
|
|
use RedirectionIO\Client\Sdk\HttpMessage\Response; |
13
|
|
|
|
14
|
|
|
class Client |
15
|
|
|
{ |
16
|
|
|
private $connections; |
17
|
|
|
private $timeout; |
18
|
|
|
private $debug; |
19
|
|
|
private $logger; |
20
|
|
|
private $currentConnection; |
21
|
|
|
private $currentConnectionName; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @param int $timeout |
25
|
|
|
* @param bool $debug |
26
|
|
|
*/ |
27
|
13 |
|
public function __construct(array $connections, $timeout = 10000, $debug = false, LoggerInterface $logger = null) |
28
|
|
|
{ |
29
|
13 |
|
if (!$connections) { |
|
|
|
|
30
|
1 |
|
throw new BadConfigurationException('At least one connection is required.'); |
31
|
|
|
} |
32
|
|
|
|
33
|
13 |
|
foreach ($connections as $name => $connection) { |
34
|
13 |
|
$this->connections[$name] = [ |
35
|
13 |
|
'remote_socket' => $connection, |
36
|
13 |
|
'retries' => 2, |
37
|
|
|
]; |
38
|
13 |
|
} |
39
|
|
|
|
40
|
13 |
|
$this->timeout = $timeout; |
41
|
13 |
|
$this->debug = $debug; |
42
|
13 |
|
$this->logger = $logger ?: new NullLogger(); |
43
|
13 |
|
} |
44
|
|
|
|
45
|
9 |
|
public function findRedirect(Request $request) |
46
|
|
|
{ |
47
|
|
|
$requestContext = [ |
48
|
9 |
|
'host' => $request->getHost(), |
49
|
9 |
|
'request_uri' => $request->getPath(), |
50
|
9 |
|
'user_agent' => $request->getUserAgent(), |
51
|
9 |
|
'referer' => $request->getReferer(), |
52
|
9 |
|
'scheme' => $request->getScheme(), |
53
|
9 |
|
'use_json' => true, |
54
|
9 |
|
]; |
55
|
|
|
|
56
|
|
|
try { |
57
|
9 |
|
$agentResponse = $this->request('GET', $requestContext); |
58
|
9 |
|
} catch (ExceptionInterface $exception) { |
59
|
3 |
|
if ($this->debug) { |
60
|
1 |
|
throw $exception; |
61
|
|
|
} |
62
|
|
|
|
63
|
2 |
|
return null; |
64
|
|
|
} |
65
|
|
|
|
66
|
7 |
|
if (0 === strlen($agentResponse)) { |
67
|
1 |
|
return null; |
68
|
|
|
} |
69
|
|
|
|
70
|
6 |
|
$json = json_decode($agentResponse, true); |
71
|
|
|
|
72
|
6 |
|
if (null === $json) { |
73
|
|
|
if ($this->debug) { |
74
|
|
|
throw new \ErrorException(sprintf('Impossible to decode the JSON (%s). Content: "%s"', json_last_error_msg(), $agentResponse)); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
return null; |
78
|
|
|
} |
79
|
|
|
|
80
|
6 |
|
return 410 == $json['status_code'] |
81
|
6 |
|
? new Response(410) |
82
|
6 |
|
: new RedirectResponse($json['location'], (int) $json['status_code']); |
83
|
|
|
} |
84
|
|
|
|
85
|
3 |
|
public function log(Request $request, Response $response) |
86
|
|
|
{ |
87
|
|
|
$responseContext = [ |
88
|
3 |
|
'status_code' => $response->getStatusCode(), |
89
|
3 |
|
'host' => $request->getHost(), |
90
|
3 |
|
'request_uri' => $request->getPath(), |
91
|
3 |
|
'user_agent' => $request->getUserAgent(), |
92
|
3 |
|
'referer' => $request->getReferer(), |
93
|
3 |
|
'scheme' => $request->getScheme(), |
94
|
3 |
|
'use_json' => true, |
95
|
3 |
|
]; |
96
|
|
|
|
97
|
|
|
try { |
98
|
3 |
|
return (bool) $this->request('LOG', $responseContext); |
99
|
2 |
|
} catch (ExceptionInterface $exception) { |
100
|
2 |
|
if ($this->debug) { |
101
|
1 |
|
throw $exception; |
102
|
|
|
} |
103
|
|
|
|
104
|
1 |
|
return false; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
12 |
|
private function request($command, $context) |
109
|
|
|
{ |
110
|
12 |
|
$connection = $this->getConnection(); |
111
|
|
|
|
112
|
8 |
|
$content = $command.' '.json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n"; |
113
|
8 |
|
$sent = $this->box('doSend', false, [$connection, $content]); |
114
|
|
|
|
115
|
|
|
// if the pipe is broken, `fwrite` will throw a Notice |
116
|
8 |
|
if (false === $sent) { |
117
|
|
|
$this->logger->debug('Impossible to send content to the connection.', [ |
118
|
1 |
|
'options' => $this->connections[$this->currentConnectionName], |
119
|
|
|
]); |
120
|
|
|
|
121
|
|
|
--$this->connections[$this->currentConnectionName]['retries']; |
122
|
|
|
$this->currentConnection = null; |
123
|
|
|
|
124
|
|
|
return $this->request($command, $context); |
125
|
|
|
} |
126
|
|
|
|
127
|
8 |
|
$received = $this->box('doGet', false, [$connection]); |
128
|
|
|
|
129
|
|
|
// false: the persistent connection is stale |
130
|
8 |
|
if (false === $received) { |
131
|
1 |
|
$this->logger->debug('Impossible to get content from the connection.', [ |
132
|
1 |
|
'options' => $this->connections[$this->currentConnectionName], |
133
|
1 |
|
]); |
134
|
|
|
|
135
|
1 |
|
--$this->connections[$this->currentConnectionName]['retries']; |
136
|
1 |
|
$this->currentConnection = null; |
137
|
|
|
|
138
|
1 |
|
return $this->request($command, $context); |
139
|
|
|
} |
140
|
|
|
|
141
|
8 |
|
return trim($received); |
142
|
|
|
} |
143
|
|
|
|
144
|
12 |
|
private function getConnection() |
145
|
|
|
{ |
146
|
12 |
|
if (null !== $this->currentConnection) { |
147
|
2 |
|
return $this->currentConnection; |
148
|
|
|
} |
149
|
|
|
|
150
|
12 |
|
foreach ($this->connections as $name => $connection) { |
151
|
12 |
|
if ($connection['retries'] <= 0) { |
152
|
1 |
|
continue; |
153
|
|
|
} |
154
|
|
|
|
155
|
12 |
|
$this->logger->debug('New connection chosen. Trying to connect.', [ |
156
|
12 |
|
'connection' => $connection, |
157
|
12 |
|
'name' => $name, |
158
|
12 |
|
]); |
159
|
|
|
|
160
|
12 |
|
$connection = $this->box('doConnect', false, [$connection]); |
161
|
|
|
|
162
|
12 |
|
if (false === $connection) { |
163
|
6 |
|
$this->logger->debug('Impossible to connect to the connection.', [ |
164
|
6 |
|
'connection' => $connection, |
165
|
6 |
|
'name' => $name, |
166
|
6 |
|
]); |
167
|
|
|
|
168
|
6 |
|
$this->connections[$name]['retries'] = 0; |
169
|
|
|
|
170
|
6 |
|
continue; |
171
|
|
|
} |
172
|
|
|
|
173
|
8 |
|
$this->logger->debug('New connection approved.', [ |
174
|
8 |
|
'connection' => $connection, |
175
|
8 |
|
'name' => $name, |
176
|
8 |
|
]); |
177
|
|
|
|
178
|
8 |
|
stream_set_timeout($connection, 0, $this->timeout); |
179
|
|
|
|
180
|
8 |
|
$this->currentConnection = $connection; |
181
|
8 |
|
$this->currentConnectionName = $name; |
182
|
|
|
|
183
|
8 |
|
return $connection; |
184
|
5 |
|
} |
185
|
|
|
|
186
|
5 |
|
$this->logger->error('Can not find an agent.', [ |
187
|
5 |
|
'connections_options' => $this->connections, |
188
|
5 |
|
]); |
189
|
|
|
|
190
|
5 |
|
throw new AgentNotFoundException(); |
191
|
|
|
} |
192
|
|
|
|
193
|
12 |
|
private function doConnect($options) |
194
|
|
|
{ |
195
|
12 |
|
return stream_socket_client( |
196
|
12 |
|
$options['remote_socket'], |
197
|
12 |
|
$errNo, |
198
|
12 |
|
$errMsg, |
199
|
12 |
|
1, // This value is not used but it should not be 0 |
200
|
12 |
|
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT |
201
|
12 |
|
); |
202
|
|
|
} |
203
|
|
|
|
204
|
8 |
|
private function doSend($connection, $content) |
205
|
|
|
{ |
206
|
8 |
|
return fwrite($connection, $content); |
207
|
|
|
} |
208
|
|
|
|
209
|
8 |
|
private function doGet($connection) |
210
|
|
|
{ |
211
|
8 |
|
return fgets($connection); |
212
|
|
|
} |
213
|
|
|
|
214
|
12 |
|
private function box($method, $defaultReturnValue = null, array $args = []) |
215
|
|
|
{ |
216
|
12 |
|
set_error_handler(__CLASS__.'::handleInternalError'); |
217
|
|
|
|
218
|
|
|
try { |
219
|
12 |
|
$returnValue = call_user_func_array([$this, $method], $args); |
220
|
12 |
|
} catch (\ErrorException $exception) { |
221
|
6 |
|
$returnValue = $defaultReturnValue; |
222
|
|
|
|
223
|
6 |
|
$this->logger->warning('Impossible to execute a boxed called.', [ |
224
|
6 |
|
'method' => $method, |
225
|
6 |
|
'default_return_value' => $defaultReturnValue, |
226
|
6 |
|
'args' => $args, |
227
|
6 |
|
'exception' => $exception, |
228
|
6 |
|
]); |
229
|
|
|
} |
230
|
|
|
|
231
|
12 |
|
restore_error_handler(); |
232
|
|
|
|
233
|
12 |
|
return $returnValue; |
234
|
|
|
} |
235
|
|
|
|
236
|
6 |
|
private static function handleInternalError($type, $message, $file, $line) |
237
|
|
|
{ |
238
|
6 |
|
throw new \ErrorException($message, 0, $type, $file, $line); |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.