Passed
Pull Request — master (#2)
by Stephen
05:45
created

Requests_Transport_fsockopen::request()   F

Complexity

Conditions 49
Paths > 20000

Size

Total Lines 235
Code Lines 137

Duplication

Lines 7
Ratio 2.98 %

Importance

Changes 0
Metric Value
cc 49
eloc 137
nc 429496.7295
nop 4
dl 7
loc 235
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
				// 'CN_match' => $host,
80
				'capture_peer_cert' => true
81
			);
82
			$verifyname = true;
83
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;
89
				}
90
			}
91
92
			if (isset($options['verify'])) {
93
				if ($options['verify'] === false) {
94
					$context_options['verify_peer'] = false;
95
				}
96
				elseif (is_string($options['verify'])) {
97
					$context_options['cafile'] = $options['verify'];
98
				}
99
			}
100
101
			if (isset($options['verifyname']) && $options['verifyname'] === false) {
102
				$context_options['verify_peer_name'] = false;
103
				$verifyname = false;
104
			}
105
106
			stream_context_set_option($context, array('ssl' => $context_options));
107
		}
108
		else {
109
			$remote_socket = 'tcp://' . $host;
110
		}
111
112
		$this->max_bytes = $options['max_bytes'];
113
114
		if (!isset($url_parts['port'])) {
115
			$url_parts['port'] = 80;
116
		}
117
		$remote_socket .= ':' . $url_parts['port'];
118
119
		set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
120
121
		$options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
122
123
		$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
124
125
		restore_error_handler();
126
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');
129
		}
130
131
		if (!$socket) {
132
			if ($errno === 0) {
133
				// Connection issue
134
				throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
135
			}
136
137
			throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
138
		}
139
140
		$data_format = $options['data_format'];
141
142
		if ($data_format === 'query') {
143
			$path = self::format_get($url_parts, $data);
144
			$data = '';
145
		}
146
		else {
147
			$path = self::format_get($url_parts, array());
148
		}
149
150
		$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
151
152
		$request_body = '';
153
		$out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']);
154
155
		if ($options['type'] !== Requests::TRACE) {
156
			if (is_array($data)) {
157
				$request_body = http_build_query($data, null, '&');
158
			}
159
			else {
160
				$request_body = $data;
161
			}
162
163
			if (!empty($data)) {
164
				if (!isset($case_insensitive_headers['Content-Length'])) {
165
					$headers['Content-Length'] = strlen($request_body);
166
				}
167
168
				if (!isset($case_insensitive_headers['Content-Type'])) {
169
					$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
170
				}
171
			}
172
		}
173
174
		if (!isset($case_insensitive_headers['Host'])) {
175
			$out .= sprintf('Host: %s', $url_parts['host']);
176
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'];
179
			}
180
			$out .= "\r\n";
181
		}
182
183
		if (!isset($case_insensitive_headers['User-Agent'])) {
184
			$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
185
		}
186
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);
190
		}
191
192
		$headers = Requests::flatten($headers);
193
194
		if (!empty($headers)) {
195
			$out .= implode($headers, "\r\n") . "\r\n";
196
		}
197
198
		$options['hooks']->dispatch('fsockopen.after_headers', array(&$out));
199
200
		if (substr($out, -2) !== "\r\n") {
201
			$out .= "\r\n";
202
		}
203
204
		if (!isset($case_insensitive_headers['Connection'])) {
205
			$out .= "Connection: Close\r\n";
206
		}
207
208
		$out .= "\r\n" . $request_body;
209
210
		$options['hooks']->dispatch('fsockopen.before_send', array(&$out));
211
212
		fwrite($socket, $out);
213
		$options['hooks']->dispatch('fsockopen.after_send', array($out));
214
215
		if (!$options['blocking']) {
216
			fclose($socket);
217
			$fake_headers = '';
218
			$options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
219
			return '';
220
		}
221
222
		$timeout_sec = (int) floor($options['timeout']);
223
		if ($timeout_sec == $options['timeout']) {
224
			$timeout_msec = 0;
225
		}
226
		else {
227
			$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
228
		}
229
		stream_set_timeout($socket, $timeout_sec, $timeout_msec);
230
231
		$response = $body = $headers = '';
232
		$this->info = stream_get_meta_data($socket);
233
		$size = 0;
234
		$doingbody = false;
235
		$download = false;
236
		if ($options['filename']) {
237
			$download = fopen($options['filename'], 'wb');
238
		}
239
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');
244
			}
245
246
			$block = fread($socket, Requests::BUFFER_SIZE);
247
			if (!$doingbody) {
248
				$response .= $block;
249
				if (strpos($response, "\r\n\r\n")) {
250
					list($headers, $block) = explode("\r\n\r\n", $response, 2);
251
					$doingbody = true;
252
				}
253
			}
254
255
			// Are we in body mode now?
256
			if ($doingbody) {
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) {
262
						continue;
263
					}
264
					if (($size + $data_length) > $this->max_bytes) {
265
						// Limit the length
266
						$limited_length = ($this->max_bytes - $size);
267
						$block = substr($block, 0, $limited_length);
268
					}
269
				}
270
271
				$size += strlen($block);
272
				if ($download) {
273
					fwrite($download, $block);
274
				}
275
				else {
276
					$body .= $block;
277
				}
278
			}
279
		}
280
		$this->headers = $headers;
281
282
		if ($download) {
283
			fclose($download);
284
		}
285
		else {
286
			$this->headers .= "\r\n\r\n" . $body;
287
		}
288
		fclose($socket);
289
290
		$options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info));
291
		return $this->headers;
292
	}
293
294
	/**
295
	 * Send multiple requests simultaneously
296
	 *
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)
300
	 */
301
	public function request_multiple($requests, $options) {
302
		$responses = array();
303
		$class = get_class($this);
304
		foreach ($requests as $id => $request) {
305
			try {
306
				$handler = new $class();
307
				$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
308
309
				$request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
310
			}
311
			catch (Requests_Exception $e) {
312
				$responses[$id] = $e;
313
			}
314
315
			if (!is_string($responses[$id])) {
316
				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
317
			}
318
		}
319
320
		return $responses;
321
	}
322
323
	/**
324
	 * Retrieve the encodings we can accept
325
	 *
326
	 * @return string Accept-Encoding header value
327
	 */
328
	protected static function accept_encoding() {
329
		$type = array();
330
		if (function_exists('gzinflate')) {
331
			$type[] = 'deflate;q=1.0';
332
		}
333
334
		if (function_exists('gzuncompress')) {
335
			$type[] = 'compress;q=0.5';
336
		}
337
338
		$type[] = 'gzip;q=0.5';
339
340
		return implode(', ', $type);
341
	}
342
343
	/**
344
	 * Format a URL given GET data
345
	 *
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
349
	 */
350
	protected static function format_get($url_parts, $data) {
351
		if (!empty($data)) {
352
			if (empty($url_parts['query'])) {
353
				$url_parts['query'] = '';
354
			}
355
356
			$url_parts['query'] .= '&' . http_build_query($data, null, '&');
357
			$url_parts['query'] = trim($url_parts['query'], '&');
358
		}
359
		if (isset($url_parts['path'])) {
360
			if (isset($url_parts['query'])) {
361
				$get = $url_parts['path'] . '?' . $url_parts['query'];
362
			}
363
			else {
364
				$get = $url_parts['path'];
365
			}
366
		}
367
		else {
368
			$get = '/';
369
		}
370
		return $get;
371
	}
372
373
	/**
374
	 * Error handler for stream_socket_client()
375
	 *
376
	 * @param int $errno Error number (e.g. E_WARNING)
377
	 * @param string $errstr Error message
378
	 */
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
383
			return false;
384
		}
385
386
		$this->connect_error .= $errstr . "\n";
387
		return true;
388
	}
389
390
	/**
391
	 * Verify the certificate against common name and subject alternative names
392
	 *
393
	 * Unfortunately, PHP doesn't check the certificate against the alternative
394
	 * names, leading things like 'https://www.github.com/' to be invalid.
395
	 * Instead
396
	 *
397
	 * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
398
	 *
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
403
	 * @return bool
404
	 */
405
	public function verify_certificate_from_context($host, $context) {
406
		$meta = stream_context_get_options($context);
407
408
		// If we don't have SSL options, then we couldn't make the connection at
409
		// all
410
		if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
411
			throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
412
		}
413
414
		$cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
415
416
		return Requests_SSL::verify_certificate($host, $cert);
417
	}
418
419
	/**
420
	 * Whether this transport is valid
421
	 *
422
	 * @codeCoverageIgnore
423
	 * @return boolean True if the transport is valid, false otherwise.
424
	 */
425
	public static function test($capabilities = array()) {
426
		if (!function_exists('fsockopen')) {
427
			return false;
428
		}
429
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')) {
433
				return false;
434
			}
435
436
			// Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
437
			if (defined('HHVM_VERSION')) {
438
				return false;
439
			}
440
		}
441
442
		return true;
443
	}
444
}
445