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 https://secure.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 if (empty($url_parts)) {
63 throw new Requests_Exception('Invalid URL.', 'invalidurl', $url);
65 $host = $url_parts['host'];
66 $context = stream_context_create();
68 $case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers);
71 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
72 $remote_socket = 'ssl://' . $host;
73 if (!isset($url_parts['port'])) {
74 $url_parts['port'] = 443;
77 $context_options = array(
78 'verify_peer' => true,
79 // 'CN_match' => $host,
80 'capture_peer_cert' => true
84 // SNI, if enabled (OpenSSL >=0.9.8j)
85 if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
86 $context_options['SNI_enabled'] = true;
87 if (isset($options['verifyname']) && $options['verifyname'] === false) {
88 $context_options['SNI_enabled'] = false;
92 if (isset($options['verify'])) {
93 if ($options['verify'] === false) {
94 $context_options['verify_peer'] = false;
96 elseif (is_string($options['verify'])) {
97 $context_options['cafile'] = $options['verify'];
101 if (isset($options['verifyname']) && $options['verifyname'] === false) {
102 $context_options['verify_peer_name'] = false;
106 stream_context_set_option($context, array('ssl' => $context_options));
109 $remote_socket = 'tcp://' . $host;
112 $this->max_bytes = $options['max_bytes'];
114 if (!isset($url_parts['port'])) {
115 $url_parts['port'] = 80;
117 $remote_socket .= ':' . $url_parts['port'];
119 set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
121 $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
123 $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
125 restore_error_handler();
127 if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
128 throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
134 throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
137 throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
140 $data_format = $options['data_format'];
142 if ($data_format === 'query') {
143 $path = self::format_get($url_parts, $data);
147 $path = self::format_get($url_parts, array());
150 $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
153 $out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']);
155 if ($options['type'] !== Requests::TRACE) {
156 if (is_array($data)) {
157 $request_body = http_build_query($data, null, '&');
160 $request_body = $data;
164 if (!isset($case_insensitive_headers['Content-Length'])) {
165 $headers['Content-Length'] = strlen($request_body);
168 if (!isset($case_insensitive_headers['Content-Type'])) {
169 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
174 if (!isset($case_insensitive_headers['Host'])) {
175 $out .= sprintf('Host: %s', $url_parts['host']);
177 if (( 'http' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 80 ) || ( 'https' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 443 )) {
178 $out .= ':' . $url_parts['port'];
183 if (!isset($case_insensitive_headers['User-Agent'])) {
184 $out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
187 $accept_encoding = $this->accept_encoding();
188 if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
189 $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
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 if (!isset($case_insensitive_headers['Connection'])) {
205 $out .= "Connection: Close\r\n";
208 $out .= "\r\n" . $request_body;
210 $options['hooks']->dispatch('fsockopen.before_send', array(&$out));
212 fwrite($socket, $out);
213 $options['hooks']->dispatch('fsockopen.after_send', array($out));
215 if (!$options['blocking']) {
218 $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
222 $timeout_sec = (int) floor($options['timeout']);
223 if ($timeout_sec == $options['timeout']) {
227 $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
229 stream_set_timeout($socket, $timeout_sec, $timeout_msec);
231 $response = $body = $headers = '';
232 $this->info = stream_get_meta_data($socket);
236 if ($options['filename']) {
237 $download = fopen($options['filename'], 'wb');
240 while (!feof($socket)) {
241 $this->info = stream_get_meta_data($socket);
242 if ($this->info['timed_out']) {
243 throw new Requests_Exception('fsocket timed out', 'timeout');
246 $block = fread($socket, Requests::BUFFER_SIZE);
249 if (strpos($response, "\r\n\r\n")) {
250 list($headers, $block) = explode("\r\n\r\n", $response, 2);
255 // Are we in body mode now?
257 $options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes));
258 $data_length = strlen($block);
259 if ($this->max_bytes) {
260 // Have we already hit a limit?
261 if ($size === $this->max_bytes) {
264 if (($size + $data_length) > $this->max_bytes) {
266 $limited_length = ($this->max_bytes - $size);
267 $block = substr($block, 0, $limited_length);
271 $size += strlen($block);
273 fwrite($download, $block);
280 $this->headers = $headers;
286 $this->headers .= "\r\n\r\n" . $body;
290 $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info));
291 return $this->headers;
295 * Send multiple requests simultaneously
297 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
298 * @param array $options Global options, see {@see Requests::response()} for documentation
299 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
301 public function request_multiple($requests, $options) {
302 $responses = array();
303 $class = get_class($this);
304 foreach ($requests as $id => $request) {
306 $handler = new $class();
307 $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
309 $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
311 catch (Requests_Exception $e) {
312 $responses[$id] = $e;
315 if (!is_string($responses[$id])) {
316 $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
324 * Retrieve the encodings we can accept
326 * @return string Accept-Encoding header value
328 protected static function accept_encoding() {
330 if (function_exists('gzinflate')) {
331 $type[] = 'deflate;q=1.0';
334 if (function_exists('gzuncompress')) {
335 $type[] = 'compress;q=0.5';
338 $type[] = 'gzip;q=0.5';
340 return implode(', ', $type);
344 * Format a URL given GET data
346 * @param array $url_parts
347 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
348 * @return string URL with data
350 protected static function format_get($url_parts, $data) {
352 if (empty($url_parts['query'])) {
353 $url_parts['query'] = '';
356 $url_parts['query'] .= '&' . http_build_query($data, null, '&');
357 $url_parts['query'] = trim($url_parts['query'], '&');
359 if (isset($url_parts['path'])) {
360 if (isset($url_parts['query'])) {
361 $get = $url_parts['path'] . '?' . $url_parts['query'];
364 $get = $url_parts['path'];
374 * Error handler for stream_socket_client()
376 * @param int $errno Error number (e.g. E_WARNING)
377 * @param string $errstr Error message
379 public function connect_error_handler($errno, $errstr) {
380 // Double-check we can handle it
381 if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
382 // Return false to indicate the default error handler should engage
386 $this->connect_error .= $errstr . "\n";
391 * Verify the certificate against common name and subject alternative names
393 * Unfortunately, PHP doesn't check the certificate against the alternative
394 * names, leading things like 'https://www.github.com/' to be invalid.
397 * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
399 * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
400 * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
401 * @param string $host Host name to verify against
402 * @param resource $context Stream context
405 public function verify_certificate_from_context($host, $context) {
406 $meta = stream_context_get_options($context);
408 // If we don't have SSL options, then we couldn't make the connection at
410 if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
411 throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
414 $cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
416 return Requests_SSL::verify_certificate($host, $cert);
420 * Whether this transport is valid
422 * @codeCoverageIgnore
423 * @return boolean True if the transport is valid, false otherwise.
425 public static function test($capabilities = array()) {
426 if (!function_exists('fsockopen')) {
430 // If needed, check that streams support SSL
431 if (isset($capabilities['ssl']) && $capabilities['ssl']) {
432 if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
436 // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
437 if (defined('HHVM_VERSION')) {