HPCloud-PHP  1.2.0
PHP bindings for HPCloud and OpenStack services.
 All Classes Namespaces Files Functions Variables Pages
CURLTransport.php
Go to the documentation of this file.
1 <?php
2 /* ============================================================================
3 (c) Copyright 2012 Hewlett-Packard Development Company, L.P.
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
8 the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ============================================================================ */
22 /**
23  * @file
24  * Implements a transporter with CURL.
25  */
26 
27 namespace HPCloud\Transport;
28 
29 use \HPCloud\Bootstrap;
30 
31 /**
32  * Provide HTTP transport with CURL.
33  *
34  * You should choose the Curl backend if...
35  *
36  * - You KNOW Curl support is compiled into your PHP version
37  * - You do not like the built-in PHP HTTP handler
38  * - Performance is a big deal to you
39  * - You will be sending large objects (>2M)
40  * - Or PHP stream wrappers for URLs are not supported on your system.
41  *
42  * CURL is demonstrably faster than the built-in PHP HTTP handling, so
43  * ths library gives a performance boost. Error reporting is slightly
44  * better too.
45  *
46  * But the real strong point to Curl is that it can take file objects
47  * and send them over HTTP without having to buffer them into strings
48  * first. This saves memory and processing.
49  *
50  * The only downside to Curl is that it is not available on all hosts.
51  * Some installations of PHP do not compile support.
52  */
53 class CURLTransport implements Transporter {
54 
55 
56  const HTTP_USER_AGENT_SUFFIX = ' (c93c0a) CURL/1.0';
57 
58  protected $curlInst = NULL;
59  /**
60  * The curl_multi instance.
61  *
62  * By using curl_multi to wrap CURL requests, we can re-use the same
63  * connection for multiple requests. This has tremendous value for
64  * cases where several transactions occur in short order.
65  */
66  protected $multi = NULL;
67 
68  public function __destruct() {
69  // Destroy the multi handle.
70  if (!empty($this->multi)) {
71  curl_multi_close($this->multi);
72  }
73  }
74 
75  /*
76  public function curl($uri) {
77  //if (empty($this->curlInst)) {
78  $this->curlInst = curl_init();
79  //}
80  curl_setopt($this->curlInst, CURLOPT_URL, $uri);
81  return $this->curlInst;
82  }
83  */
84 
85  public function doRequest($uri, $method = 'GET', $headers = array(), $body = NULL) {
86 
87  $in = NULL;
88  if (!empty($body)) {
89  // For whatever reason, CURL seems to want POST request data to be
90  // a string, not a file handle. So we adjust. PUT, on the other hand,
91  // needs to be in a file handle.
92  if ($method == 'POST') {
93  $in = $body;
94  }
95  else {
96  // First we turn our body into a temp-backed buffer.
97  $in = fopen('php://temp', 'wr', FALSE);
98  fwrite($in, $body, strlen($body));
99  rewind($in);
100  }
101  }
102  return $this->handleDoRequest($uri, $method, $headers, $in);
103  //return $this->handleDoRequest($uri, $method, $headers, $body);
104 
105  }
106 
107  public function doRequestWithResource($uri, $method, $headers, $resource) {
108  if (is_string($resource)) {
109  $in = open($resource, 'rb', FALSE);
110  }
111  else {
112  // FIXME: Is there a better way?
113  // There is a bug(?) in CURL which prevents it
114  // from writing the same stream twice. But we
115  // need to be able to flush a file multiple times.
116  // So we have to create a new temp buffer for each
117  // write operation.
118  $in = fopen('php://temp', 'rb+'); //tmpfile();
119  stream_copy_to_stream($resource, $in);
120  rewind($in);
121  }
122  return $this->handleDoRequest($uri, $method, $headers, $in);
123  }
124 
125  /**
126  * Internal workhorse.
127  */
128  protected function handleDoRequest($uri, $method, $headers, $in = NULL) {
129 
130  // XXX: I don't like this, but I'm getting bug reports that mistakenly
131  // assume this library is broken, when in fact CURL is not installed.
132  if (!function_exists('curl_init')) {
133  throw new \HPCloud\Exception('The CURL library is not available.');
134  }
135 
136  //syslog(LOG_WARNING, "Real Operation: $method $uri");
137 
138  //$urlParts = parse_url($uri);
139  $opts = array(
140  CURLOPT_USERAGENT => self::HTTP_USER_AGENT . self::HTTP_USER_AGENT_SUFFIX,
141  // CURLOPT_RETURNTRANSFER => TRUE, // Make curl_exec return the results.
142  // CURLOPT_BINARYTRANSFER => TRUE, // Raw output if RETURNTRANSFER is TRUE.
143 
144  // Timeout if the remote has not connected in 30 sec.
145  //CURLOPT_CONNECTTIMEOUT => 30,
146 
147  // If this is set, CURL will auto-deflate any encoding it can.
148  // CURLOPT_ENCODING => '',
149 
150  // Later, we may want to do this to support range-based
151  // fetching of large objects.
152  // CURLOPT_RANGE => 'X-Y',
153 
154  // Limit curl to only these protos.
155  // CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
156 
157  // (Re-)set defaults.
158  //CURLOPT_POSTFIELDS => NULL,
159  //CURLOPT_INFILE => NULL,
160  //CURLOPT_INFILESIZE => NULL,
161  );
162 
163 
164  // Write to in-mem handle backed by a temp file.
165  $out = fopen('php://temp', 'wb+');
166  $headerFile = fopen('php://temp', 'w+');
167 
168  $curl = curl_init($uri);
169  //$curl = $this->curl($uri);
170 
171  // Set method
172  $this->determineMethod($curl, $method);
173 
174  // Set the upload
175  $copy = NULL;
176 
177  // If we get a string, we send the string
178  // data.
179  if (is_string($in)) {
180  //curl_setopt($curl, CURLOPT_POSTFIELDS, $in);
181  $opts[CURLOPT_POSTFIELDS] = $in;
182  if (!isset($headers['Content-Length'])) {
183  $headers['Content-Length'] = strlen($in);
184  }
185  }
186  // If we get a resource, we treat it like a stream
187  // and pass it into CURL as a file.
188  elseif (is_resource($in)) {
189  //curl_setopt($curl, CURLOPT_INFILE, $in);
190  $opts[CURLOPT_INFILE] = $in;
191 
192  // Tell CURL about the content length if we know it.
193  if (!empty($headers['Content-Length'])) {
194  //curl_setopt($curl, CURLOPT_INFILESIZE, $headers['Content-Length']);
195  $opts[CURLOPT_INFILESIZE] = $headers['Content-Length'];
196  unset($headers['Content-Length']);
197  }
198  }
199 
200  // Set headers.
201  $this->setHeaders($curl, $headers);
202 
203  // Get the output.
204  //curl_setopt($curl, CURLOPT_FILE, $out);
205  $opts[CURLOPT_FILE] = $out;
206 
207  // We need to capture the headers, too.
208  //curl_setopt($curl, CURLOPT_WRITEHEADER, $headerFile);
209  $opts[CURLOPT_WRITEHEADER] = $headerFile;
210 
211 
212  if (Bootstrap::hasConfig('transport.debug')) {
213  $debug = Bootstrap::config('transport.debug', NULL);
214  //curl_setopt($curl, CURLOPT_VERBOSE, (int) $debug);
215  $opts[CURLOPT_VERBOSE] = (int) $debug;
216  }
217 
218  if (Bootstrap::hasConfig('transport.timeout')) {
219  //curl_setopt($curl, CURLOPT_TIMEOUT, (int) Bootstrap::config('transport.timeout'));
220  $opts[CURLOPT_TIMEOUT] = (int) Bootstrap::config('transport.timeout');
221  }
222 
223  if (Bootstrap::hasConfig('transport.ssl.verify')) {
224  $validate = (boolean) Bootstrap::config('transport.ssl.verify', TRUE);
225  //curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $validate);
226  $opts[CURLOPT_SSL_VERIFYPEER] = $validate;
227  }
228 
229  // PROXY settings
230  $proxyMap = array(
231  'proxy' => CURLOPT_PROXY,
232  'proxy.tunnel' => CURLOPT_HTTPPROXYTUNNEL,
233  'proxy.auth' => CURLOPT_PROXYAUTH,
234  'proxy.port' => CURLOPT_PROXYPORT,
235  'proxy.type' => CURLOPT_PROXYTYPE,
236  'proxy.userpwd' => CURLOPT_PROXYUSERPWD,
237  );
238  foreach ($proxyMap as $conf => $opt) {
239  if (Bootstrap::hasConfig($conf)) {
240  $val = Bootstrap::config($conf);
241  $opts[$opt] = $val;
242  }
243  }
244 
245 
246  // Set all of the curl opts and then execute.
247  curl_setopt_array($curl, $opts);
248  $ret = $this->execCurl($curl);//curl_exec($curl);
249  $info = curl_getinfo($curl);
250  $status = $info['http_code'];
251 
252  rewind($headerFile);
253  $responseHeaders = $this->fetchHeaders($headerFile);
254  fclose($headerFile);
255 
256  if (!$ret || $status < 200 || $status > 299) {
257  if (empty($responseHeaders)) {
258  $err = 'Unknown (non-HTTP) error: ' . $status;
259  }
260  else {
261  $err = $responseHeaders[0];
262  }
263  //rewind($out);
264  //fwrite(STDERR, stream_get_contents($out));
265  Response::failure($status, $err, $info['url'], $method, $info);
266  }
267 
268 
269  rewind($out);
270  // Now we need to build a response.
271  $resp = new Response($out, $info, $responseHeaders);
272 
273  //curl_close($curl);
274  if (is_resource($copy)) {
275  fclose($copy);
276  }
277 
278  return $resp;
279  }
280 
281 
282  /**
283  * Poor man's connection pooling.
284  *
285  * Instead of using curl_exec(), we use curl_multi_* to
286  * handle the processing The CURL multi library tracks connections, and
287  * basically provides connection sharing across requests. So two requests made to
288  * the same server will use the same connection (even when they are executed
289  * separately) assuming that the remote server supports this.
290  *
291  * We've noticed that this improves performance substantially, especially since
292  * SSL requests only require the SSL handshake once.
293  *
294  * @param resource $handle
295  * A CURL handle from curl_init().
296  * @retval boolean
297  * @return boolean
298  * Returns a boolean value indicating whether or not CURL could process the
299  * request.
300  */
301  protected function execCurl($handle) {
302  if (empty($this->multi)) {
303  $multi = curl_multi_init();
304  $this->multi = $multi;
305  // fwrite(STDOUT, "Creating MULTI handle.\n");
306  }
307  else {
308  // fwrite(STDOUT, "Reusing MULTI handle.\n");
309  $multi = $this->multi;
310  }
311  curl_multi_add_handle($multi, $handle);
312 
313  // Initialize all of the listeners
314  $active = NULL;
315  do {
316  $ret = curl_multi_exec($multi, $active);
317  } while ($ret == CURLM_CALL_MULTI_PERFORM);
318 
319  while ($active && $ret == CURLM_OK) {
320  if (curl_multi_select($multi) != -1) {
321  do {
322  $mrc = curl_multi_exec($multi, $active);
323  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
324  }
325  }
326 
327  curl_multi_remove_handle($multi, $handle);
328  //curl_multi_close($multi);
329 
330  return TRUE;
331 
332  }
333 
334  /**
335  * This function reads the header file into an array.
336  *
337  * This format mataches the format returned by the stream handlers, so
338  * we can re-use the header parsing logic in Response.
339  *
340  * @param resource $file
341  * A file pointer to the file that has the headers.
342  * @retval array
343  * @return array
344  * An array of headers, one header per line.
345  */
346  protected function fetchHeaders($file) {
347  $buffer = array();
348  while ($header = fgets($file)) {
349  $header = trim($header);
350  if ($header == 'HTTP/1.1 100 Continue') {
351  // Obey the command.
352  continue;
353  }
354  if (!empty($header)) {
355  $buffer[] = $header;
356  }
357  }
358  return $buffer;
359  }
360 
361  /**
362  * Set the appropriate constant on the CURL object.
363  *
364  * Curl handles method name setting in a slightly counter-intuitive
365  * way, so we have a special function for setting the method
366  * correctly. Note that since we do not POST as www-form-*, we
367  * use a custom post.
368  *
369  * @param resource $curl
370  * A curl object.
371  * @param string $method
372  * An HTTP method name.
373  */
374  protected function determineMethod($curl, $method) {
375  $method = strtoupper($method);
376 
377  switch ($method) {
378  case 'GET':
379  curl_setopt($curl, CURLOPT_HTTPGET, TRUE);
380  break;
381  case 'HEAD':
382  curl_setopt($curl, CURLOPT_NOBODY, TRUE);
383  break;
384 
385  // Put is problematic: Some PUT requests might not have
386  // a body.
387  case 'PUT':
388  curl_setopt($curl, CURLOPT_PUT, TRUE);
389  break;
390 
391  // We use customrequest for post because we are
392  // not submitting form data.
393  case 'POST':
394  case 'DELETE':
395  case 'COPY':
396  default:
397  curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
398  }
399 
400  }
401 
402  public function setHeaders($curl, $headers) {
403  $buffer = array();
404  $format = '%s: %s';
405 
406  foreach ($headers as $name => $value) {
407  $buffer[] = sprintf($format, $name, $value);
408  }
409 
410  curl_setopt($curl, CURLOPT_HTTPHEADER, $buffer);
411 
412  return $this;
413  }
414 }