Completed
Push — master ( 54d5ab...279d39 )
by Filippo
02:32
created

SocketAdapter::writeRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 1
eloc 7
nc 1
nop 1
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
}