PhpHttpRequest::getCertOptions()   C
last analyzed

Complexity

Conditions 7
Paths 15

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 24
nc 15
nop 0
dl 0
loc 39
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 */
20
21
class PhpHttpRequest extends MWHttpRequest {
22
23
	private $fopenErrors = [];
24
25
	/**
26
	 * @param string $url
27
	 * @return string
28
	 */
29
	protected function urlToTcp( $url ) {
30
		$parsedUrl = parse_url( $url );
31
32
		return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
33
	}
34
35
	/**
36
	 * Returns an array with a 'capath' or 'cafile' key
37
	 * that is suitable to be merged into the 'ssl' sub-array of
38
	 * a stream context options array.
39
	 * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
40
	 * default CA bundle if PHP supports that, or searches a few standard locations.
41
	 * @return array
42
	 * @throws DomainException
43
	 */
44
	protected function getCertOptions() {
45
		$certOptions = [];
46
		$certLocations = [];
47
		if ( $this->caInfo ) {
48
			$certLocations = [ 'manual' => $this->caInfo ];
49
		} elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
50
			// @codingStandardsIgnoreStart Generic.Files.LineLength
51
			// Default locations, based on
52
			// https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
53
			// PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
54
			// PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
55
			// so we should leave capath/cafile empty there.
56
			// @codingStandardsIgnoreEnd
57
			$certLocations = array_filter( [
58
				getenv( 'SSL_CERT_DIR' ),
59
				getenv( 'SSL_CERT_PATH' ),
60
				'/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
61
				'/etc/ssl/certs',  # Debian et al
62
				'/etc/pki/tls/certs/ca-bundle.trust.crt',
63
				'/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
64
				'/System/Library/OpenSSL', # OSX
65
			] );
66
		}
67
68
		foreach ( $certLocations as $key => $cert ) {
69
			if ( is_dir( $cert ) ) {
70
				$certOptions['capath'] = $cert;
71
				break;
72
			} elseif ( is_file( $cert ) ) {
73
				$certOptions['cafile'] = $cert;
74
				break;
75
			} elseif ( $key === 'manual' ) {
76
				// fail more loudly if a cert path was manually configured and it is not valid
77
				throw new DomainException( "Invalid CA info passed: $cert" );
78
			}
79
		}
80
81
		return $certOptions;
82
	}
83
84
	/**
85
	 * Custom error handler for dealing with fopen() errors.
86
	 * fopen() tends to fire multiple errors in succession, and the last one
87
	 * is completely useless (something like "fopen: failed to open stream")
88
	 * so normal methods of handling errors programmatically
89
	 * like get_last_error() don't work.
90
	 */
91
	public function errorHandler( $errno, $errstr ) {
92
		$n = count( $this->fopenErrors ) + 1;
93
		$this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
94
	}
95
96
	public function execute() {
97
98
		parent::execute();
99
100
		if ( is_array( $this->postData ) ) {
101
			$this->postData = wfArrayToCgi( $this->postData );
102
		}
103
104
		if ( $this->parsedUrl['scheme'] != 'http'
105
			&& $this->parsedUrl['scheme'] != 'https' ) {
106
			$this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
107
		}
108
109
		$this->reqHeaders['Accept'] = "*/*";
110
		$this->reqHeaders['Connection'] = 'Close';
111
		if ( $this->method == 'POST' ) {
112
			// Required for HTTP 1.0 POSTs
113
			$this->reqHeaders['Content-Length'] = strlen( $this->postData );
114
			if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
115
				$this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
116
			}
117
		}
118
119
		// Set up PHP stream context
120
		$options = [
121
			'http' => [
122
				'method' => $this->method,
123
				'header' => implode( "\r\n", $this->getHeaderList() ),
124
				'protocol_version' => '1.1',
125
				'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
126
				'ignore_errors' => true,
127
				'timeout' => $this->timeout,
128
				// Curl options in case curlwrappers are installed
129
				'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
130
				'curl_verify_ssl_peer' => $this->sslVerifyCert,
131
			],
132
			'ssl' => [
133
				'verify_peer' => $this->sslVerifyCert,
134
				'SNI_enabled' => true,
135
				'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
136
				'disable_compression' => true,
137
			],
138
		];
139
140
		if ( $this->proxy ) {
141
			$options['http']['proxy'] = $this->urlToTcp( $this->proxy );
142
			$options['http']['request_fulluri'] = true;
143
		}
144
145
		if ( $this->postData ) {
146
			$options['http']['content'] = $this->postData;
147
		}
148
149
		if ( $this->sslVerifyHost ) {
150
			// PHP 5.6.0 deprecates CN_match, in favour of peer_name which
151
			// actually checks SubjectAltName properly.
152
			if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
153
				$options['ssl']['peer_name'] = $this->parsedUrl['host'];
154
			} else {
155
				$options['ssl']['CN_match'] = $this->parsedUrl['host'];
156
			}
157
		}
158
159
		$options['ssl'] += $this->getCertOptions();
160
161
		$context = stream_context_create( $options );
162
163
		$this->headerList = [];
164
		$reqCount = 0;
165
		$url = $this->url;
166
167
		$result = [];
168
169
		if ( $this->profiler ) {
170
			$profileSection = $this->profiler->scopedProfileIn(
171
				__METHOD__ . '-' . $this->profileName
172
			);
173
		}
174
		do {
175
			$reqCount++;
176
			$this->fopenErrors = [];
177
			set_error_handler( [ $this, 'errorHandler' ] );
178
			$fh = fopen( $url, "r", false, $context );
179
			restore_error_handler();
180
181
			if ( !$fh ) {
182
				// HACK for instant commons.
183
				// If we are contacting (commons|upload).wikimedia.org
184
				// try again with CN_match for en.wikipedia.org
185
				// as php does not handle SubjectAltName properly
186
				// prior to "peer_name" option in php 5.6
187
				if ( isset( $options['ssl']['CN_match'] )
188
					&& ( $options['ssl']['CN_match'] === 'commons.wikimedia.org'
189
						|| $options['ssl']['CN_match'] === 'upload.wikimedia.org' )
190
				) {
191
					$options['ssl']['CN_match'] = 'en.wikipedia.org';
192
					$context = stream_context_create( $options );
193
					continue;
194
				}
195
				break;
196
			}
197
198
			$result = stream_get_meta_data( $fh );
199
			$this->headerList = $result['wrapper_data'];
200
			$this->parseHeader();
201
202
			if ( !$this->followRedirects ) {
203
				break;
204
			}
205
206
			# Handle manual redirection
207
			if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
208
				break;
209
			}
210
			# Check security of URL
211
			$url = $this->getResponseHeader( "Location" );
212
213
			if ( !Http::isValidURI( $url ) ) {
214
				$this->logger->debug( __METHOD__ . ": insecure redirection\n" );
215
				break;
216
			}
217
		} while ( true );
218
		if ( $this->profiler ) {
219
			$this->profiler->scopedProfileOut( $profileSection );
0 ignored issues
show
Bug introduced by
It seems like $profileSection defined by $this->profiler->scopedP...' . $this->profileName) on line 170 can also be of type object<Wikimedia\ScopedCallback>; however, Profiler::scopedProfileOut() does only seem to accept null|object<SectionProfileCallback>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
220
		}
221
222
		$this->setStatus();
223
224
		if ( $fh === false ) {
225
			if ( $this->fopenErrors ) {
226
				$this->logger->warning( __CLASS__
227
					. ': error opening connection: {errstr1}', $this->fopenErrors );
228
			}
229
			$this->status->fatal( 'http-request-error' );
230
			return $this->status;
231
		}
232
233
		if ( $result['timed_out'] ) {
234
			$this->status->fatal( 'http-timed-out', $this->url );
235
			return $this->status;
236
		}
237
238
		// If everything went OK, or we received some error code
239
		// get the response body content.
240
		if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
241
			while ( !feof( $fh ) ) {
242
				$buf = fread( $fh, 8192 );
243
244
				if ( $buf === false ) {
245
					$this->status->fatal( 'http-read-error' );
246
					break;
247
				}
248
249
				if ( strlen( $buf ) ) {
250
					call_user_func( $this->callback, $fh, $buf );
251
				}
252
			}
253
		}
254
		fclose( $fh );
255
256
		return $this->status;
257
	}
258
}
259