Nico Huber | ee52fbc | 2023-06-24 11:52:57 +0000 | [diff] [blame^] | 1 | <?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 | */ |
| 35 | define('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 | */ |
| 58 | function 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 | */ |
| 99 | function 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 | */ |
| 121 | function _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 | */ |
| 215 | function _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 | */ |
| 316 | function _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 | ?> |