|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* @file SocketAdapter.php |
|
5
|
|
|
* @brief This file contains the SocketAdapter class. |
|
6
|
|
|
* @details |
|
7
|
|
|
* @author Filippo F. Fadda |
|
8
|
|
|
*/ |
|
9
|
|
|
|
|
10
|
|
|
|
|
11
|
|
|
namespace EoC\Adapter; |
|
12
|
|
|
|
|
13
|
|
|
|
|
14
|
|
|
use EoC\Message\Message; |
|
15
|
|
|
use EoC\Message\Request; |
|
16
|
|
|
use EoC\Message\Response; |
|
17
|
|
|
use EoC\Hook\IChunkHook; |
|
18
|
|
|
|
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @brief An HTTP 1.1 client using raw sockets. |
|
22
|
|
|
* @details This client is using HTTP/1.1 version.\n |
|
23
|
|
|
* Encoding is made according RFC 3986, using rawurlencode().\n |
|
24
|
|
|
* It supports 100-continue, chunked responses, persistent connections, etc. |
|
25
|
|
|
* @nosubgrouping |
|
26
|
|
|
*/ |
|
27
|
|
|
class SocketAdapter extends AbstractAdapter { |
|
28
|
|
|
|
|
29
|
|
|
//! HTTP protocol version. |
|
30
|
|
|
const HTTP_VERSION = "HTTP/1.1"; |
|
31
|
|
|
|
|
32
|
|
|
//! Buffer dimension. |
|
33
|
|
|
const BUFFER_LENGTH = 8192; |
|
34
|
|
|
|
|
35
|
|
|
//! Maximum period to wait before the response is sent. |
|
36
|
|
|
const DEFAULT_TIMEOUT = 60000; |
|
37
|
|
|
|
|
38
|
|
|
protected static $defaultSocketTimeout; |
|
39
|
|
|
|
|
40
|
|
|
// Socket connection timeout in seconds, specified by a float. |
|
41
|
|
|
protected $timeout; |
|
42
|
|
|
|
|
43
|
|
|
// Socket handle. |
|
44
|
|
|
protected $handle; |
|
45
|
|
|
|
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @copydoc AbstractAdapter::__construct() |
|
49
|
|
|
* @param[in] bool $persistent (optional) When `true` the client uses a persistent connection. |
|
50
|
|
|
*/ |
|
51
|
|
|
public function __construct($server = parent::DEFAULT_SERVER, $userName = "", $password = "", $persistent = TRUE) { |
|
52
|
|
|
$this->initialize(); |
|
53
|
|
|
|
|
54
|
|
|
parent::__construct($server, $userName, $password); |
|
55
|
|
|
|
|
56
|
|
|
$this->timeout = static::$defaultSocketTimeout; |
|
57
|
|
|
|
|
58
|
|
|
// Establishes a connection within the server. |
|
59
|
|
|
if ($persistent) |
|
60
|
|
|
$this->handle = @pfsockopen($this->scheme.$this->host, $this->port, $errno, $errstr, $this->timeout); |
|
61
|
|
|
else |
|
62
|
|
|
$this->handle = @fsockopen($this->scheme.$this->host, $this->port, $errno, $errstr, $this->timeout); |
|
63
|
|
|
|
|
64
|
|
|
if (!is_resource($this->handle)) |
|
65
|
|
|
throw new \ErrorException($errstr, $errno); |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
|
|
69
|
|
|
/** |
|
70
|
|
|
* @brief Closes the file pointer. |
|
71
|
|
|
*/ |
|
72
|
|
|
public function __destruct() { |
|
73
|
|
|
//@fclose($this->handle); |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
|
|
77
|
|
|
/** |
|
78
|
|
|
* @copydoc AbstractAdapter::initialize() |
|
79
|
|
|
*/ |
|
80
|
|
|
public function initialize() { |
|
81
|
|
|
|
|
82
|
|
|
if (!static::$initialized) { |
|
83
|
|
|
static::$initialized = TRUE; |
|
84
|
|
|
|
|
85
|
|
|
// If PHP is not properly recognizing the line endings when reading files either on or created by a Macintosh |
|
86
|
|
|
// computer, enabling the auto_detect_line_endings run-time configuration option may help resolve the problem. |
|
87
|
|
|
ini_set("auto_detect_line_endings", TRUE); |
|
88
|
|
|
|
|
89
|
|
|
// By default the default_socket_timeout php.ini setting is used. |
|
90
|
|
|
static::$defaultSocketTimeout = ini_get("default_socket_timeout"); |
|
91
|
|
|
} |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
|
|
95
|
|
|
/** |
|
96
|
|
|
* @brief Writes the entire request over the socket. |
|
97
|
|
|
* @param[in] Request $request A request. |
|
98
|
|
|
*/ |
|
99
|
|
|
protected function writeRequest(Request $request) { |
|
100
|
|
|
$command = $request->getMethod()." ".$request->getPath().$request->getQueryString()." ".self::HTTP_VERSION; |
|
101
|
|
|
|
|
102
|
|
|
// Writes the request over the socket. |
|
103
|
|
|
fputs($this->handle, $command.Message::CRLF); |
|
104
|
|
|
fputs($this->handle, $request->getHeaderAsString().Message::CRLF); |
|
105
|
|
|
fputs($this->handle, Message::CRLF); |
|
106
|
|
|
fputs($this->handle, $request->getBody()); |
|
107
|
|
|
fputs($this->handle, Message::CRLF); |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
* @brief Reads the the status code and the header of the response. |
|
113
|
|
|
* @return string |
|
114
|
|
|
*/ |
|
115
|
|
|
protected function readResponseStatusCodeAndHeader() { |
|
116
|
|
|
$statusCodeAndHeader = ""; |
|
117
|
|
|
|
|
118
|
|
View Code Duplication |
while (!feof($this->handle)) { |
|
|
|
|
|
|
119
|
|
|
// We use fgets() because it stops reading at first newline or buffer length, depends which one is reached first. |
|
120
|
|
|
$buffer = fgets($this->handle, self::BUFFER_LENGTH); |
|
121
|
|
|
|
|
122
|
|
|
// Adds the buffer to the header. |
|
123
|
|
|
$statusCodeAndHeader .= $buffer; |
|
124
|
|
|
|
|
125
|
|
|
// The header is separated from the body by a newline, so we break when we read it. |
|
126
|
|
|
if ($buffer == Message::CRLF) |
|
127
|
|
|
break; |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
return $statusCodeAndHeader; |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* @brief Reads the entity-body of a chunked response. |
|
135
|
|
|
* @see http://www.jmarshall.com/easy/http/#http1.1c2 |
|
136
|
|
|
* @param[in] IChunkHook $chunkHook The chunk's hook. |
|
137
|
|
|
*/ |
|
138
|
|
|
protected function readChunkedResponseBody($chunkHook) { |
|
139
|
|
|
$body = ""; |
|
140
|
|
|
|
|
141
|
|
|
while (!feof($this->handle)) { |
|
142
|
|
|
// Gets the line which has the length of this chunk. |
|
143
|
|
|
$line = fgets($this->handle, self::BUFFER_LENGTH); |
|
144
|
|
|
|
|
145
|
|
|
// If it's only a newline, this normally means it's read the total amount of data requested minus the newline |
|
146
|
|
|
// continue to next loop to make sure we're done. |
|
147
|
|
|
if ($line == Message::CRLF) |
|
148
|
|
|
continue; |
|
149
|
|
|
|
|
150
|
|
|
// The length of the block is expressed in hexadecimal. |
|
151
|
|
|
$length = hexdec($line); |
|
152
|
|
|
|
|
153
|
|
|
if (!is_int($length)) |
|
154
|
|
|
throw new \RuntimeException("The response doesn't seem chunk encoded."); |
|
155
|
|
|
|
|
156
|
|
|
// Zero is sent when at the end of the chunks or the end of the stream. |
|
157
|
|
|
if ($length < 1) |
|
158
|
|
|
break; |
|
159
|
|
|
|
|
160
|
|
|
// Reads the chunk. |
|
161
|
|
|
// When reading from network streams or pipes, such as those returned when reading remote files or from popen() |
|
162
|
|
|
// and proc_open(), reading will stop after a new packet is available. This means that we must collect the data |
|
163
|
|
|
// together in chunks. So, we can't pass to the fread() the entire length because it could return less data than |
|
164
|
|
|
// expected. We have to read, instead, the standard buffer length, and concatenate the read chunks. |
|
165
|
|
|
$buffer = ""; |
|
166
|
|
|
|
|
167
|
|
|
while ($length > 0) { |
|
168
|
|
|
$size = min(self::BUFFER_LENGTH, $length); |
|
169
|
|
|
$data = fread($this->handle, $size); |
|
170
|
|
|
|
|
171
|
|
|
if (strlen($data) == 0) |
|
172
|
|
|
break; // EOF |
|
173
|
|
|
|
|
174
|
|
|
$buffer .= $data; |
|
175
|
|
|
$length -= strlen($data); |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
// If a function has been hooked, calls it, else just adds the buffer to the body. |
|
179
|
|
|
if (is_null($chunkHook)) |
|
180
|
|
|
$body .= $buffer; |
|
181
|
|
|
else |
|
182
|
|
|
$chunkHook->process($buffer); |
|
183
|
|
|
} |
|
184
|
|
|
|
|
185
|
|
|
// A chunk response might have some footer, but CouchDB doesn't use them, so we simply ignore them. |
|
186
|
|
View Code Duplication |
while (!feof($this->handle)) { |
|
|
|
|
|
|
187
|
|
|
// We use fgets() because it stops reading at first newline or buffer length, depends which one is reached first. |
|
188
|
|
|
$buffer = fgets($this->handle, self::BUFFER_LENGTH); |
|
189
|
|
|
|
|
190
|
|
|
// The chunk response ends with a newline, so we break when we read it. |
|
191
|
|
|
if ($buffer == Message::CRLF) |
|
192
|
|
|
break; |
|
193
|
|
|
} |
|
194
|
|
|
|
|
195
|
|
|
return $body; |
|
196
|
|
|
} |
|
197
|
|
|
|
|
198
|
|
|
|
|
199
|
|
|
/** |
|
200
|
|
|
* @brief Reads the entity-body of a standard response. |
|
201
|
|
|
* @param[in] Response $response The response. |
|
202
|
|
|
* @return string |
|
203
|
|
|
*/ |
|
204
|
|
|
protected function readStandardResponseBody(Response $response) { |
|
205
|
|
|
$body = ""; |
|
206
|
|
|
|
|
207
|
|
|
// Retrieves the body length from the header. |
|
208
|
|
|
$length = (int)$response->getHeaderFieldValue(Response::CONTENT_LENGTH_HF); |
|
209
|
|
|
|
|
210
|
|
|
// The response should have a body, if not we have finished. |
|
211
|
|
|
if ($length > 0) { |
|
212
|
|
|
$bytes = 0; |
|
213
|
|
|
|
|
214
|
|
|
while (!feof($this->handle)) { |
|
215
|
|
|
$buffer = fgets($this->handle); |
|
216
|
|
|
$body .= $buffer; |
|
217
|
|
|
$bytes += strlen($buffer); |
|
218
|
|
|
|
|
219
|
|
|
if ($bytes >= $length) |
|
220
|
|
|
break; |
|
221
|
|
|
} |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
return $body; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
|
|
228
|
|
|
/** |
|
229
|
|
|
* @brief Reads the entity-body. |
|
230
|
|
|
* @param[in] Response $response The response. |
|
231
|
|
|
* @param[in] IChunkHook $chunkHook (optional) The chunk's hook. |
|
232
|
|
|
* @return string |
|
233
|
|
|
*/ |
|
234
|
|
|
protected function readResponseBody(Response $response, $chunkHook) { |
|
235
|
|
|
if ($response->getHeaderFieldValue(Response::TRANSFER_ENCODING_HF) == "chunked") |
|
236
|
|
|
return $this->readChunkedResponseBody($chunkHook); |
|
237
|
|
|
else |
|
238
|
|
|
return $this->readStandardResponseBody($response); |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* @copydoc AbstractAdapter::send() |
|
244
|
|
|
*/ |
|
245
|
|
|
public function send(Request $request, IChunkHook $chunkHook = NULL) { |
|
246
|
|
|
$request->setHeaderField(Request::HOST_HF, $this->host.":".$this->port); |
|
247
|
|
|
|
|
248
|
|
|
if (!empty($this->userName)) |
|
249
|
|
|
$request->setBasicAuth($this->userName, $this->password); |
|
250
|
|
|
|
|
251
|
|
|
// Sets the Content-Length header only when the given request has a message body. |
|
252
|
|
|
if ($request->hasBody()) |
|
253
|
|
|
$request->setHeaderField(Message::CONTENT_LENGTH_HF, $request->getBodyLength()); |
|
254
|
|
|
|
|
255
|
|
|
// Writes the request over the socket. |
|
256
|
|
|
$this->writeRequest($request); |
|
257
|
|
|
|
|
258
|
|
|
// Creates the Response object. |
|
259
|
|
|
$response = new Response($this->readResponseStatusCodeAndHeader()); |
|
260
|
|
|
|
|
261
|
|
|
// Assigns the body to the response, if any is present. |
|
262
|
|
|
$response->setBody($this->readResponseBody($response, $chunkHook)); |
|
263
|
|
|
|
|
264
|
|
|
return $response; |
|
265
|
|
|
} |
|
266
|
|
|
|
|
267
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.