blob: 37c134553140f2cb152b9269ad72aa134e9d1a8b [file] [log] [blame]
Nico Huberee52fbc2023-06-24 11:52:57 +00001<?php
2/*
3 * SimpleID
4 *
5 * Copyright (C) Kelvin Mo 2007-9
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public
18 * License along with this program; if not, write to the Free
19 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 *
21 * $Id$
22 */
23
24/**
25 * Functions for making and processing HTTP requests.
26 *
27 * @package simpleid
28 * @since 0.7
29 * @filesource
30 */
31
32/**
33 * The user agent to use during HTTP requests.
34 */
35define('SIMPLEHTTP_USER_AGENT', 'SimpleHTTP/' . substr('$Rev$', 6, -2));
36
37/**
38 * Performs an HTTP request.
39 *
40 * Communication with the web server is conducted using libcurl where possible.
41 * Where libcurl does not exist, then sockets will be used.
42 *
43 * Note that the request must be properly prepared before passing onto this function.
44 * For example, for POST requests, the Content-Type and Content-Length headers must be
45 * included in $headers.
46 *
47 * @param string $url the URL
48 * @param array $headers HTTP headers containing name => value pairs
49 * @param string $body the request body
50 * @param string $method the HTTP request method
51 * @param int $retry the maximum number of redirects allowed
52 * @return array containing keys 'error-code' (for communication errors), 'error'
53 * (for communication errors), 'data' (content returned), 'code' (the HTTP status code), 'http-error'
54 * (if the HTTP status code is not 200 or 304), 'protocol' (the HTTP protocol in the response),
55 * 'headers' (an array of return headers in lowercase),
56 * 'content-type' (the HTTP content-type returned)
57 */
58function http_make_request($url, $headers = array(), $body = NULL, $method = 'GET', $retry = 3) {
59 // If CURL is available, we use it
60 if (extension_loaded('curl')) {
61 $response = _http_make_request_curl($url, $headers, $body, $method, $retry);
62 } else {
63 $response = _http_make_request_fsock($url, $headers, $body, $method, $retry);
64 }
65
66 if (!isset($response['error-code'])) {
67 $valid_codes = array(
68 100, 101,
69 200, 201, 202, 203, 204, 205, 206,
70 300, 301, 302, 303, 304, 305, 307,
71 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417,
72 500, 501, 502, 503, 504, 505
73 );
74
75 // RFC 2616 states that all unknown HTTP codes must be treated the same as the
76 // base code in their class.
77 if (!in_array($response['code'], $valid_codes)) {
78 $response['code'] = floor($response['code'] / 100) * 100;
79 }
80
81 if (($response['code'] != 200) && ($response['code'] != 304)) {
82 $response['http-error'] = $response['code'];
83 }
84
85 }
86
87 return $response;
88}
89
90/**
91 * Returns the protocols currently supported for making remote requests.
92 *
93 * If libcurl is used, this function returns a list of protocols supported by the
94 * included build of the library. If libcurl is not used, then HTTP is the
95 * only protocol supported.
96 *
97 * @return array an array of protocols
98 */
99function http_protocols() {
100 if (extension_loaded('curl')) {
101 $curl_version = curl_version();
102 return $curl_version['protocols'];
103 } else {
104 return array('http');
105 }
106}
107
108/**
109 * Performs an HTTP request using libcurl.
110 *
111 * @param string $url the URL
112 * @param array $headers HTTP headers containing name => value pairs
113 * @param string $body the request body
114 * @param string $method the HTTP request method
115 * @param int $retry the maximum number of redirects allowed
116 * @return array containing keys 'error-code' (for communication errors), 'error'
117 * (for communication errors), 'data' (content returned), 'code' (the HTTP status code), 'http-error'
118 * (if the HTTP status code is not 200 or 304), 'headers' (an array of return headers),
119 * 'content-type' (the HTTP content-type returned)
120 */
121function _http_make_request_curl($url, $headers = array(), $body = NULL, $method = 'GET', $retry = 3) {
122 // CURLOPT_FOLLOWLOCATION only works when safe mode is off or when open_basedir is set
123 // In these instances we will need to follow redirects manually
124 $manual_redirect = ((@ini_get('safe_mode') === 1) // safe mode
125 || (strtolower(@ini_get('safe_mode')) == 'on') // safe mode
126 || (@ini_get('open_basedir') != false)); // open_basedir
127
128 $version = curl_version();
129
130 $curl = curl_init($url);
131
132 if (version_compare($version['version'], '7.10.5', '>=')) {
133 curl_setopt($curl, CURLOPT_ENCODING, '');
134 }
135
136 if (!$manual_redirect) curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
137
138 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
139 curl_setopt($curl, CURLOPT_MAXREDIRS, $retry);
140 curl_setopt($curl, CURLOPT_HTTPHEADER, array(implode("\n", $headers) . "\n"));
141 curl_setopt($curl, CURLOPT_USERAGENT, SIMPLEHTTP_USER_AGENT);
142
143 curl_setopt($curl, CURLOPT_TIMEOUT, 20);
144 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 20);
145
146 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
147 curl_setopt($curl, CURLOPT_HEADER, true);
148
149 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
150 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
151
152 if ($body != NULL) curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
153
154 $response = curl_exec($curl);
155
156 if (($response === FALSE) && ((curl_errno($curl) == 23) || (curl_errno($curl) == 61))) {
157 curl_setopt($curl, CURLOPT_ENCODING, 'none');
158 $response = curl_exec($curl);
159 }
160
161 if ($response === FALSE) {
162 $result = array();
163 $result['error-code'] = curl_errno($curl);
164 $result['error'] = curl_error($curl);
165 } else {
166 $result['code'] = curl_getinfo($curl, CURLINFO_HTTP_CODE);
167 $result['url'] = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
168 $result['content-type'] = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
169
170 // Parse response.
171 $result['raw'] = $response;
172
173 $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
174 $result['data'] = substr($response, $header_size);
175
176 $response_headers = substr($response, 0, $header_size - 4);
177
178 // In case where redirect occurs, we want the last set of headers
179 $header_blocks = explode("\r\n\r\n", $response_headers);
180 $header_block = array_pop($header_blocks);
181
182 $result = array_merge($result, _http_parse_headers($header_block, TRUE));
183
184 // If we are in safe mode, we need to process redirects manually
185 if ($manual_redirect && (($result['code'] == 301) || ($result['code'] == 302) || ($result['code'] == 307))) {
186 if ($retry == 0) {
187 // Too many times, return an error
188 $result['error-code'] = 47;
189 $result['error'] = 'Too many redirects';
190 } else {
191 curl_close($curl);
192 return _http_make_request_curl($result['headers']['location'], $headers, $body, $method, $retry - 1);
193 }
194 }
195 }
196
197 curl_close($curl);
198
199 return $result;
200}
201
202/**
203 * Performs an HTTP request using sockets.
204 *
205 * @param string $url the URL
206 * @param array $headers HTTP headers containing name => value pairs
207 * @param string $body the request body
208 * @param string $method the HTTP request method
209 * @param int $retry the maximum number of redirects allowed
210 * @return array containing keys 'error-code' (for communication errors), 'error'
211 * (for communication errors), 'data' (content returned), 'code' (the HTTP status code), 'http-error'
212 * (if the HTTP status code is not 200 or 304), 'headers' (an array of return headers),
213 * 'content-type' (the HTTP content-type returned)
214 */
215function _http_make_request_fsock($url, $headers = array(), $body = NULL, $method = 'GET', $retry = 3) {
216 $result = array();
217
218 $parts = parse_url($url);
219
220 if (!isset($parts)) {
221 $result['error-code'] = 3;
222 $result['error'] = 'URL not properly formatted';
223 return $result;
224 }
225
226 if ($parts['scheme'] == 'http') {
227 $port = isset($parts['port']) ? $parts['port'] : 80;
228 $host = $parts['host'];
229 } elseif ($parts['scheme'] == 'https') {
230 $port = isset($parts['port']) ? $parts['port'] : 443;
231 $host = 'ssl://' . $parts['host'];
232 } else {
233 $result['error-code'] = 1;
234 $result['error'] = 'Unsupported protocol';
235 }
236
237 $fp = @fsockopen($host, $port, $errno, $errstr, 15);
238
239 if (!$fp) {
240 $result['error-code'] = 7;
241 $result['error'] = "Cannot connect: Error $errno:" . trim($errstr);
242 return $result;
243 }
244
245 if (isset($parts['path'])) {
246 $path = $url_parts['path'];
247 if (isset($parts['query'])) $path .= '?' . $url_parts['query'];
248 } else {
249 $path = '/';
250 }
251
252 $headers = array_merge(
253 array(
254 'Host' => $parts['host'],
255 'User-Agent' => SIMPLEHTTP_USER_AGENT,
256 'Connection' => 'close'
257 ),
258 $headers
259 );
260
261 if (isset($parts['user']) && isset($parts['pass'])) {
262 $headers['Authorization'] = 'Basic '. base64_encode($parts['user'] . (!empty($parts['pass']) ? ":". $parts['pass'] : ''));
263 }
264
265 $request = $method . ' '. $path ." HTTP/1.0\r\n";
266
267 $keys = array_keys($headers);
268 for ($i = 0; $i < count($keys); $i++) {
269 $request .= $keys[$i] . ': ' . $headers[$keys[$i]] . "\r\n";
270 }
271
272 // End of headers - separator
273 $request .= "\r\n";
274
275 if ($body != NULL) $request .= $body;
276
277 fwrite($fp, $request);
278
279 // Fetch response.
280 $response = '';
281 while (!feof($fp) && $chunk = fread($fp, 1024)) {
282 $response .= $chunk;
283 }
284 fclose($fp);
285
286 // Parse response.
287 list($header_block, $result['data']) = explode("\r\n\r\n", $response, 2);
288
289 $result = array_merge($result, _http_parse_headers($header_block, FALSE));
290
291 // Process redirects
292 if (($result['code'] == 301) || ($result['code'] == 302) || ($result['code'] == 307)) {
293 if ($retry == 0) {
294 // Too many times, return an error
295 $result['error-code'] = 47;
296 $result['error'] = 'Too many redirects';
297 } else {
298 $result = _http_make_request_fsock($result['headers']['location'], $headers, $body, $method, $retry - 1);
299 }
300 }
301
302 $result['url'] = $url;
303 return $result;
304}
305
306/**
307 * Parses HTTP response headers.
308 *
309 * @param string $header_block the unparsed header block
310 * @param bool $curl if true, use simplified parsing as libcurl already parses
311 * the headers
312 * @return an array containing the following keys: 'protocol' (the HTTP protocol in the response),
313 * 'headers' (an array of return headers in lowercase). If $curl is false, additional
314 * parsing is done for 'code' and 'content-type'
315 */
316function _http_parse_headers($header_block, $curl) {
317 $headers = array();
318 $result = array();
319
320 // Split the status line from the rest of the message header
321 list($status, $header_block) = preg_split("/\r\n|\n|\r/", $header_block, 2);
322
323 // RFC 2616, section 4.2: Header fields can be extended over multiple lines
324 // by preceding each extra line with at least one space or tab. So we need
325 // to join them...
326 $header_block = preg_replace('/(\r\n|\n|\r)( |\t)+/', '', $header_block);
327
328 // Then split them to get the fields
329 $fields = preg_split("/\r\n|\n|\r/", $header_block);
330
331 // Parse the status line
332 list($protocol, $code, $reason) = explode(' ', trim($status), 3);
333
334 $result['protocol'] = $protocol;
335 if (!$curl) $result['code'] = $code;
336
337 // Parse headers.
338 while ($field = trim(array_shift($fields))) {
339 list($header, $value) = explode(':', $field, 2);
340
341 // Headers are case insensitive
342 $header = strtolower($header);
343
344 if (isset($headers[$header])) {
345 // RFC 2616, section 4.2: Multiple headers with the same field
346 // name is the same as a concatenating all the headers in a single
347 // header, separated by commas.
348 $headers[$header] .= ','. trim($value);
349 } else {
350 $headers[$header] = trim($value);
351 }
352
353 if (!$curl && (strtolower($header) == 'content-type')) $result['content-type'] = $value;
354 }
355
356 $result['headers'] = $headers;
357 return $result;
358}
359?>