connect_error_handler()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 2
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * fsockopen HTTP transport
4
 *
5
 * @package Requests
6
 * @subpackage Transport
7
 */
8
9
/**
10
 * fsockopen HTTP transport
11
 *
12
 * @package Requests
13
 * @subpackage Transport
14
 */
15
class Requests_Transport_fsockopen implements Requests_Transport {
16
	/**
17
	 * Second to microsecond conversion
18
	 *
19
	 * @var integer
20
	 */
21
	const SECOND_IN_MICROSECONDS = 1000000;
22
23
	/**
24
	 * Raw HTTP data
25
	 *
26
	 * @var string
27
	 */
28
	public $headers = '';
29
30
	/**
31
	 * Stream metadata
32
	 *
33
	 * @var array Associative array of properties, see {@see https://secure.php.net/stream_get_meta_data}
34
	 */
35
	public $info;
36
37
	/**
38
	 * What's the maximum number of bytes we should keep?
39
	 *
40
	 * @var int|bool Byte count, or false if no limit.
41
	 */
42
	protected $max_bytes = false;
43
44
	protected $connect_error = '';
45
46
	/**
47
	 * Perform a request
48
	 *
49
	 * @throws Requests_Exception On failure to connect to socket (`fsockopenerror`)
50
	 * @throws Requests_Exception On socket timeout (`timeout`)
51
	 *
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
57
	 */
58
	public function request($url, $headers = array(), $data = array(), $options = array()) {
59
		$options['hooks']->dispatch('fsockopen.before_request');
60
61
		$url_parts = parse_url($url);
62
		if (empty($url_parts)) {
63
			throw new Requests_Exception('Invalid URL.', 'invalidurl', $url);
64
		}
65
		$host                     = $url_parts['host'];
66
		$context                  = stream_context_create();
67
		$verifyname               = false;
68
		$case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers);
69
70
		// HTTPS support
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;
75
			}
76
77
			$context_options = array(
78
				'verify_peer'       => true,
79
				'capture_peer_cert' => true,
80
			);
81
			$verifyname      = true;
82
83
			// SNI, if enabled (OpenSSL >=0.9.8j)
84
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound
85
			if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
86
				$context_options['SNI_enabled'] = true;
87 View Code Duplication
				if (isset($options['verifyname']) && $options['verifyname'] === false) {
88
					$context_options['SNI_enabled'] = false;
89
				}
90
			}
91
92
			if (isset($options['verify'])) {
93
				if ($options['verify'] === false) {
94
					$context_options['verify_peer']      = false;
95
					$context_options['verify_peer_name'] = false;
96
					$verifyname                          = false;
97
				}
98
				elseif (is_string($options['verify'])) {
99
					$context_options['cafile'] = $options['verify'];
100
				}
101
			}
102
103 View Code Duplication
			if (isset($options['verifyname']) && $options['verifyname'] === false) {
104
				$context_options['verify_peer_name'] = false;
105
				$verifyname                          = false;
106
			}
107
108
			stream_context_set_option($context, array('ssl' => $context_options));
109
		}
110
		else {
111
			$remote_socket = 'tcp://' . $host;
112
		}
113
114
		$this->max_bytes = $options['max_bytes'];
115
116
		if (!isset($url_parts['port'])) {
117
			$url_parts['port'] = 80;
118
		}
119
		$remote_socket .= ':' . $url_parts['port'];
120
121
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
122
		set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
123
124
		$options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
125
126
		$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
127
128
		restore_error_handler();
129
130
		if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
131
			throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
132
		}
133
134
		if (!$socket) {
135
			if ($errno === 0) {
136
				// Connection issue
137
				throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
138
			}
139
140
			throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
141
		}
142
143
		$data_format = $options['data_format'];
144
145
		if ($data_format === 'query') {
146
			$path = self::format_get($url_parts, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 58 can also be of type string; however, Requests_Transport_fsockopen::format_get() does only seem to accept array|object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
147
			$data = '';
148
		}
149
		else {
150
			$path = self::format_get($url_parts, array());
151
		}
152
153
		$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
154
155
		$request_body = '';
156
		$out          = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']);
157
158
		if ($options['type'] !== Requests::TRACE) {
159
			if (is_array($data)) {
160
				$request_body = http_build_query($data, '', '&');
161
			}
162
			else {
163
				$request_body = $data;
164
			}
165
166
			// Always include Content-length on POST requests to prevent
167
			// 411 errors from some servers when the body is empty.
168
			if (!empty($data) || $options['type'] === Requests::POST) {
169
				if (!isset($case_insensitive_headers['Content-Length'])) {
170
					$headers['Content-Length'] = strlen($request_body);
171
				}
172
173
				if (!isset($case_insensitive_headers['Content-Type'])) {
174
					$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
175
				}
176
			}
177
		}
178
179
		if (!isset($case_insensitive_headers['Host'])) {
180
			$out .= sprintf('Host: %s', $url_parts['host']);
181
182
			if ((strtolower($url_parts['scheme']) === 'http' && $url_parts['port'] !== 80) || (strtolower($url_parts['scheme']) === 'https' && $url_parts['port'] !== 443)) {
183
				$out .= ':' . $url_parts['port'];
184
			}
185
			$out .= "\r\n";
186
		}
187
188
		if (!isset($case_insensitive_headers['User-Agent'])) {
189
			$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
190
		}
191
192
		$accept_encoding = $this->accept_encoding();
193
		if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
194
			$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
195
		}
196
197
		$headers = Requests::flatten($headers);
198
199
		if (!empty($headers)) {
200
			$out .= implode("\r\n", $headers) . "\r\n";
201
		}
202
203
		$options['hooks']->dispatch('fsockopen.after_headers', array(&$out));
204
205
		if (substr($out, -2) !== "\r\n") {
206
			$out .= "\r\n";
207
		}
208
209
		if (!isset($case_insensitive_headers['Connection'])) {
210
			$out .= "Connection: Close\r\n";
211
		}
212
213
		$out .= "\r\n" . $request_body;
214
215
		$options['hooks']->dispatch('fsockopen.before_send', array(&$out));
216
217
		fwrite($socket, $out);
218
		$options['hooks']->dispatch('fsockopen.after_send', array($out));
219
220
		if (!$options['blocking']) {
221
			fclose($socket);
222
			$fake_headers = '';
223
			$options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
224
			return '';
225
		}
226
227
		$timeout_sec = (int) floor($options['timeout']);
228
		if ($timeout_sec === $options['timeout']) {
229
			$timeout_msec = 0;
230
		}
231
		else {
232
			$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
233
		}
234
		stream_set_timeout($socket, $timeout_sec, $timeout_msec);
235
236
		$response   = '';
237
		$body       = '';
238
		$headers    = '';
239
		$this->info = stream_get_meta_data($socket);
240
		$size       = 0;
241
		$doingbody  = false;
242
		$download   = false;
243
		if ($options['filename']) {
244
			$download = fopen($options['filename'], 'wb');
245
		}
246
247
		while (!feof($socket)) {
248
			$this->info = stream_get_meta_data($socket);
249
			if ($this->info['timed_out']) {
250
				throw new Requests_Exception('fsocket timed out', 'timeout');
251
			}
252
253
			$block = fread($socket, Requests::BUFFER_SIZE);
254
			if (!$doingbody) {
255
				$response .= $block;
256
				if (strpos($response, "\r\n\r\n")) {
257
					list($headers, $block) = explode("\r\n\r\n", $response, 2);
258
					$doingbody             = true;
259
				}
260
			}
261
262
			// Are we in body mode now?
263
			if ($doingbody) {
264
				$options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes));
265
				$data_length = strlen($block);
266
				if ($this->max_bytes) {
267
					// Have we already hit a limit?
268
					if ($size === $this->max_bytes) {
269
						continue;
270
					}
271
					if (($size + $data_length) > $this->max_bytes) {
272
						// Limit the length
273
						$limited_length = ($this->max_bytes - $size);
274
						$block          = substr($block, 0, $limited_length);
275
					}
276
				}
277
278
				$size += strlen($block);
279
				if ($download) {
280
					fwrite($download, $block);
281
				}
282
				else {
283
					$body .= $block;
284
				}
285
			}
286
		}
287
		$this->headers = $headers;
288
289
		if ($download) {
290
			fclose($download);
291
		}
292
		else {
293
			$this->headers .= "\r\n\r\n" . $body;
294
		}
295
		fclose($socket);
296
297
		$options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info));
298
		return $this->headers;
299
	}
300
301
	/**
302
	 * Send multiple requests simultaneously
303
	 *
304
	 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
305
	 * @param array $options Global options, see {@see Requests::response()} for documentation
306
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
307
	 */
308
	public function request_multiple($requests, $options) {
309
		$responses = array();
310
		$class     = get_class($this);
311
		foreach ($requests as $id => $request) {
312
			try {
313
				$handler        = new $class();
314
				$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
315
316
				$request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
317
			}
318
			catch (Requests_Exception $e) {
319
				$responses[$id] = $e;
320
			}
321
322 View Code Duplication
			if (!is_string($responses[$id])) {
323
				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
324
			}
325
		}
326
327
		return $responses;
328
	}
329
330
	/**
331
	 * Retrieve the encodings we can accept
332
	 *
333
	 * @return string Accept-Encoding header value
334
	 */
335
	protected static function accept_encoding() {
336
		$type = array();
337
		if (function_exists('gzinflate')) {
338
			$type[] = 'deflate;q=1.0';
339
		}
340
341
		if (function_exists('gzuncompress')) {
342
			$type[] = 'compress;q=0.5';
343
		}
344
345
		$type[] = 'gzip;q=0.5';
346
347
		return implode(', ', $type);
348
	}
349
350
	/**
351
	 * Format a URL given GET data
352
	 *
353
	 * @param array $url_parts
354
	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
355
	 * @return string URL with data
356
	 */
357
	protected static function format_get($url_parts, $data) {
358
		if (!empty($data)) {
359
			if (empty($url_parts['query'])) {
360
				$url_parts['query'] = '';
361
			}
362
363
			$url_parts['query'] .= '&' . http_build_query($data, '', '&');
364
			$url_parts['query']  = trim($url_parts['query'], '&');
365
		}
366
		if (isset($url_parts['path'])) {
367
			if (isset($url_parts['query'])) {
368
				$get = $url_parts['path'] . '?' . $url_parts['query'];
369
			}
370
			else {
371
				$get = $url_parts['path'];
372
			}
373
		}
374
		else {
375
			$get = '/';
376
		}
377
		return $get;
378
	}
379
380
	/**
381
	 * Error handler for stream_socket_client()
382
	 *
383
	 * @param int $errno Error number (e.g. E_WARNING)
384
	 * @param string $errstr Error message
385
	 */
386
	public function connect_error_handler($errno, $errstr) {
387
		// Double-check we can handle it
388
		if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
389
			// Return false to indicate the default error handler should engage
390
			return false;
391
		}
392
393
		$this->connect_error .= $errstr . "\n";
394
		return true;
395
	}
396
397
	/**
398
	 * Verify the certificate against common name and subject alternative names
399
	 *
400
	 * Unfortunately, PHP doesn't check the certificate against the alternative
401
	 * names, leading things like 'https://www.github.com/' to be invalid.
402
	 * Instead
403
	 *
404
	 * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
405
	 *
406
	 * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
407
	 * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
408
	 * @param string $host Host name to verify against
409
	 * @param resource $context Stream context
410
	 * @return bool
411
	 */
412
	public function verify_certificate_from_context($host, $context) {
413
		$meta = stream_context_get_options($context);
414
415
		// If we don't have SSL options, then we couldn't make the connection at
416
		// all
417
		if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
418
			throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
419
		}
420
421
		$cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
422
423
		return Requests_SSL::verify_certificate($host, $cert);
424
	}
425
426
	/**
427
	 * Whether this transport is valid
428
	 *
429
	 * @codeCoverageIgnore
430
	 * @return boolean True if the transport is valid, false otherwise.
431
	 */
432
	public static function test($capabilities = array()) {
433
		if (!function_exists('fsockopen')) {
434
			return false;
435
		}
436
437
		// If needed, check that streams support SSL
438
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
439
			if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
440
				return false;
441
			}
442
443
			// Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
444
			if (defined('HHVM_VERSION')) {
445
				return false;
446
			}
447
		}
448
449
		return true;
450
	}
451
}
452