3 * fsockopen HTTP transport
6 * @subpackage Transport
10 * fsockopen HTTP transport
13 * @subpackage Transport
15 class Requests_Transport_fsockopen implements Requests_Transport {
17 * Second to microsecond conversion
21 const SECOND_IN_MICROSECONDS = 1000000;
33 * @var array Associative array of properties, see {@see http://php.net/stream_get_meta_data}
38 * What's the maximum number of bytes we should keep?
40 * @var int|bool Byte count, or false if no limit.
42 protected $max_bytes = false;
44 protected $connect_error = '';
49 * @throws Requests_Exception On failure to connect to socket (`fsockopenerror`)
50 * @throws Requests_Exception On socket timeout (`timeout`)
52 * @param string $url URL to request
53 * @param array $headers Associative array of request headers
54 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
55 * @param array $options Request options, see {@see Requests::response()} for documentation
56 * @return string Raw HTTP result
58 public function request($url, $headers = array(), $data = array(), $options = array()) {
59 $options['hooks']->dispatch('fsockopen.before_request');
61 $url_parts = parse_url($url);
62 $host = $url_parts['host'];
63 $context = stream_context_create();
67 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
68 $remote_socket = 'ssl://' . $host;
69 $url_parts['port'] = 443;
71 $context_options = array(
72 'verify_peer' => true,
73 // 'CN_match' => $host,
74 'capture_peer_cert' => true
78 // SNI, if enabled (OpenSSL >=0.9.8j)
79 if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
80 $context_options['SNI_enabled'] = true;
81 if (isset($options['verifyname']) && $options['verifyname'] === false) {
82 $context_options['SNI_enabled'] = false;
86 if (isset($options['verify'])) {
87 if ($options['verify'] === false) {
88 $context_options['verify_peer'] = false;
89 } elseif (is_string($options['verify'])) {
90 $context_options['cafile'] = $options['verify'];
94 if (isset($options['verifyname']) && $options['verifyname'] === false) {
98 stream_context_set_option($context, array('ssl' => $context_options));
101 $remote_socket = 'tcp://' . $host;
104 $this->max_bytes = $options['max_bytes'];
106 $proxy = isset( $options['proxy'] );
107 $proxy_auth = $proxy && isset( $options['proxy_username'] ) && isset( $options['proxy_password'] );
109 if (!isset($url_parts['port'])) {
110 $url_parts['port'] = 80;
112 $remote_socket .= ':' . $url_parts['port'];
114 set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
116 $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
118 $fp = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
120 restore_error_handler();
123 if (!$this->verify_certificate_from_context($host, $context)) {
124 throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
131 throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
134 throw new Requests_Exception($errstr, 'fsockopenerror');
141 switch ($options['type']) {
144 case Requests::PATCH:
145 if (isset($url_parts['path'])) {
146 $path = $url_parts['path'];
147 if (isset($url_parts['query'])) {
148 $path .= '?' . $url_parts['query'];
155 $options['hooks']->dispatch( 'fsockopen.remote_host_path', array( &$path, $url ) );
156 $out = $options['type'] . " $path HTTP/1.0\r\n";
158 if (is_array($data)) {
159 $request_body = http_build_query($data, null, '&');
162 $request_body = $data;
164 if (empty($headers['Content-Length'])) {
165 $headers['Content-Length'] = strlen($request_body);
167 if (empty($headers['Content-Type'])) {
168 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
173 case Requests::DELETE:
174 $path = self::format_get($url_parts, $data);
175 $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
176 $out = $options['type'] . " $path HTTP/1.0\r\n";
179 $out .= "Host: {$url_parts['host']}";
181 if ($url_parts['port'] !== 80) {
182 $out .= ":{$url_parts['port']}";
186 $out .= "User-Agent: {$options['useragent']}\r\n";
187 $accept_encoding = $this->accept_encoding();
188 if (!empty($accept_encoding)) {
189 $out .= "Accept-Encoding: $accept_encoding\r\n";
192 $headers = Requests::flatten($headers);
194 if (!empty($headers)) {
195 $out .= implode($headers, "\r\n") . "\r\n";
198 $options['hooks']->dispatch('fsockopen.after_headers', array(&$out));
200 if (substr($out, -2) !== "\r\n") {
204 $out .= "Connection: Close\r\n\r\n" . $request_body;
206 $options['hooks']->dispatch('fsockopen.before_send', array(&$out));
209 $options['hooks']->dispatch('fsockopen.after_send', array(&$fake_headers));
211 if (!$options['blocking']) {
214 $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
218 $timeout_sec = (int) floor($options['timeout']);
219 $timeout_msec = $timeout_sec == $options['timeout'] ? 0 : self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
220 stream_set_timeout($fp, $timeout_sec, $timeout_msec);
222 $this->info = stream_get_meta_data($fp);
224 $response = $body = $headers = '';
225 $this->info = stream_get_meta_data($fp);
229 if ($options['filename']) {
230 $download = fopen($options['filename'], 'wb');
234 $this->info = stream_get_meta_data($fp);
235 if ($this->info['timed_out']) {
236 throw new Requests_Exception('fsocket timed out', 'timeout');
239 $block = fread($fp, Requests::BUFFER_SIZE);
242 if (strpos($response, "\r\n\r\n")) {
243 list($headers, $block) = explode("\r\n\r\n", $response, 2);
248 // Are we in body mode now?
250 $data_length = strlen($block);
251 if ($this->max_bytes) {
252 // Have we already hit a limit?
253 if ($size === $this->max_bytes) {
256 if (($size + $data_length) > $this->max_bytes) {
258 $limited_length = ($this->max_bytes - $size);
259 $block = substr($block, 0, $limited_length);
263 $size += strlen($block);
265 fwrite($download, $block);
272 $this->headers = $headers;
278 $this->headers .= "\r\n\r\n" . $body;
282 $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers));
283 return $this->headers;
287 * Send multiple requests simultaneously
289 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
290 * @param array $options Global options, see {@see Requests::response()} for documentation
291 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
293 public function request_multiple($requests, $options) {
294 $responses = array();
295 $class = get_class($this);
296 foreach ($requests as $id => $request) {
298 $handler = new $class();
299 $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
301 $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
303 catch (Requests_Exception $e) {
304 $responses[$id] = $e;
307 if (!is_string($responses[$id])) {
308 $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
316 * Retrieve the encodings we can accept
318 * @return string Accept-Encoding header value
320 protected static function accept_encoding() {
322 if (function_exists('gzinflate')) {
323 $type[] = 'deflate;q=1.0';
326 if (function_exists('gzuncompress')) {
327 $type[] = 'compress;q=0.5';
330 $type[] = 'gzip;q=0.5';
332 return implode(', ', $type);
336 * Format a URL given GET data
338 * @param array $url_parts
339 * @param array|object $data Data to build query using, see {@see http://php.net/http_build_query}
340 * @return string URL with data
342 protected static function format_get($url_parts, $data) {
344 if (empty($url_parts['query']))
345 $url_parts['query'] = '';
347 $url_parts['query'] .= '&' . http_build_query($data, null, '&');
348 $url_parts['query'] = trim($url_parts['query'], '&');
350 if (isset($url_parts['path'])) {
351 if (isset($url_parts['query'])) {
352 $get = $url_parts['path'] . '?' . $url_parts['query'];
355 $get = $url_parts['path'];
365 * Error handler for stream_socket_client()
367 * @param int $errno Error number (e.g. E_WARNING)
368 * @param string $errstr Error message
370 public function connect_error_handler($errno, $errstr) {
371 // Double-check we can handle it
372 if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
373 // Return false to indicate the default error handler should engage
377 $this->connect_error .= $errstr . "\n";
382 * Verify the certificate against common name and subject alternative names
384 * Unfortunately, PHP doesn't check the certificate against the alternative
385 * names, leading things like 'https://www.github.com/' to be invalid.
388 * @see http://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
390 * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
391 * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
392 * @param string $host Host name to verify against
393 * @param resource $context Stream context
396 public function verify_certificate_from_context($host, $context) {
397 $meta = stream_context_get_options($context);
399 // If we don't have SSL options, then we couldn't make the connection at
401 if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
402 throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
405 $cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
407 return Requests_SSL::verify_certificate($host, $cert);
411 * Whether this transport is valid
413 * @codeCoverageIgnore
414 * @return boolean True if the transport is valid, false otherwise.
416 public static function test($capabilities = array()) {
417 if (!function_exists('fsockopen'))
420 // If needed, check that streams support SSL
421 if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) {
422 if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse'))
425 // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
426 if (defined('HHVM_VERSION'))