/src/php-src/ext/standard/http_fopen_wrapper.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | +----------------------------------------------------------------------+ |
3 | | | Copyright (c) The PHP Group | |
4 | | +----------------------------------------------------------------------+ |
5 | | | This source file is subject to version 3.01 of the PHP license, | |
6 | | | that is bundled with this package in the file LICENSE, and is | |
7 | | | available through the world-wide-web at the following url: | |
8 | | | https://www.php.net/license/3_01.txt | |
9 | | | If you did not receive a copy of the PHP license and are unable to | |
10 | | | obtain it through the world-wide-web, please send a note to | |
11 | | | license@php.net so we can mail you a copy immediately. | |
12 | | +----------------------------------------------------------------------+ |
13 | | | Authors: Rasmus Lerdorf <rasmus@php.net> | |
14 | | | Jim Winstead <jimw@php.net> | |
15 | | | Hartmut Holzgraefe <hholzgra@php.net> | |
16 | | | Wez Furlong <wez@thebrainroom.com> | |
17 | | | Sara Golemon <pollita@php.net> | |
18 | | +----------------------------------------------------------------------+ |
19 | | */ |
20 | | |
21 | | #include "php.h" |
22 | | #include "php_globals.h" |
23 | | #include "php_streams.h" |
24 | | #include "php_network.h" |
25 | | #include "php_ini.h" |
26 | | #include "ext/standard/basic_functions.h" |
27 | | #include "zend_smart_str.h" |
28 | | |
29 | | #include <stdio.h> |
30 | | #include <stdlib.h> |
31 | | #include <errno.h> |
32 | | #include <sys/types.h> |
33 | | #include <sys/stat.h> |
34 | | #include <fcntl.h> |
35 | | |
36 | | #ifdef PHP_WIN32 |
37 | | #define O_RDONLY _O_RDONLY |
38 | | #include "win32/param.h" |
39 | | #else |
40 | | #include <sys/param.h> |
41 | | #endif |
42 | | |
43 | | #include "php_standard.h" |
44 | | |
45 | | #include <sys/types.h> |
46 | | #ifdef HAVE_SYS_SOCKET_H |
47 | | #include <sys/socket.h> |
48 | | #endif |
49 | | |
50 | | #ifdef PHP_WIN32 |
51 | | #include <winsock2.h> |
52 | | #else |
53 | | #include <netinet/in.h> |
54 | | #include <netdb.h> |
55 | | #ifdef HAVE_ARPA_INET_H |
56 | | #include <arpa/inet.h> |
57 | | #endif |
58 | | #endif |
59 | | |
60 | | #if defined(PHP_WIN32) || defined(__riscos__) |
61 | | #undef AF_UNIX |
62 | | #endif |
63 | | |
64 | | #if defined(AF_UNIX) |
65 | | #include <sys/un.h> |
66 | | #endif |
67 | | |
68 | | #include "php_fopen_wrappers.h" |
69 | | |
70 | | #define HTTP_HEADER_BLOCK_SIZE 1024 |
71 | 0 | #define HTTP_HEADER_MAX_LOCATION_SIZE 8182 /* 8192 - 10 (size of "Location: ") */ |
72 | 0 | #define PHP_URL_REDIRECT_MAX 20 |
73 | 0 | #define HTTP_HEADER_USER_AGENT 1 |
74 | 0 | #define HTTP_HEADER_HOST 2 |
75 | 0 | #define HTTP_HEADER_AUTH 4 |
76 | 0 | #define HTTP_HEADER_FROM 8 |
77 | 0 | #define HTTP_HEADER_CONTENT_LENGTH 16 |
78 | 0 | #define HTTP_HEADER_TYPE 32 |
79 | 0 | #define HTTP_HEADER_CONNECTION 64 |
80 | | |
81 | 0 | #define HTTP_WRAPPER_HEADER_INIT 1 |
82 | 0 | #define HTTP_WRAPPER_REDIRECTED 2 |
83 | 0 | #define HTTP_WRAPPER_KEEP_METHOD 4 |
84 | | |
85 | | static inline void strip_header(char *header_bag, char *lc_header_bag, |
86 | | const char *lc_header_name) |
87 | 0 | { |
88 | 0 | char *lc_header_start = strstr(lc_header_bag, lc_header_name); |
89 | 0 | if (lc_header_start |
90 | 0 | && (lc_header_start == lc_header_bag || *(lc_header_start-1) == '\n') |
91 | 0 | ) { |
92 | 0 | char *header_start = header_bag + (lc_header_start - lc_header_bag); |
93 | 0 | char *lc_eol = strchr(lc_header_start, '\n'); |
94 | |
|
95 | 0 | if (lc_eol) { |
96 | 0 | char *eol = header_start + (lc_eol - lc_header_start); |
97 | 0 | size_t eollen = strlen(lc_eol); |
98 | |
|
99 | 0 | memmove(lc_header_start, lc_eol+1, eollen); |
100 | 0 | memmove(header_start, eol+1, eollen); |
101 | 0 | } else { |
102 | 0 | *lc_header_start = '\0'; |
103 | 0 | *header_start = '\0'; |
104 | 0 | } |
105 | 0 | } |
106 | 0 | } |
107 | | |
108 | 0 | static bool check_has_header(const char *headers, const char *header) { |
109 | 0 | const char *s = headers; |
110 | 0 | while ((s = strstr(s, header))) { |
111 | 0 | if (s == headers || (*(s-1) == '\n' && *(s-2) == '\r')) { |
112 | 0 | return 1; |
113 | 0 | } |
114 | 0 | s++; |
115 | 0 | } |
116 | 0 | return 0; |
117 | 0 | } |
118 | | |
119 | | static zend_result php_stream_handle_proxy_authorization_header(const char *s, smart_str *header) |
120 | 0 | { |
121 | 0 | const char *p; |
122 | |
|
123 | 0 | do { |
124 | 0 | while (*s == ' ' || *s == '\t') s++; |
125 | 0 | p = s; |
126 | 0 | while (*p != 0 && *p != ':' && *p != '\r' && *p !='\n') p++; |
127 | 0 | if (*p == ':') { |
128 | 0 | p++; |
129 | 0 | if (p - s == sizeof("Proxy-Authorization:") - 1 && |
130 | 0 | zend_binary_strcasecmp(s, sizeof("Proxy-Authorization:") - 1, |
131 | 0 | "Proxy-Authorization:", sizeof("Proxy-Authorization:") - 1) == 0) { |
132 | 0 | while (*p != 0 && *p != '\r' && *p !='\n') p++; |
133 | 0 | smart_str_appendl(header, s, p - s); |
134 | 0 | smart_str_appendl(header, "\r\n", sizeof("\r\n")-1); |
135 | 0 | return SUCCESS; |
136 | 0 | } else { |
137 | 0 | while (*p != 0 && *p != '\r' && *p !='\n') p++; |
138 | 0 | } |
139 | 0 | } |
140 | 0 | s = p; |
141 | 0 | while (*s == '\r' || *s == '\n') s++; |
142 | 0 | } while (*s != 0); |
143 | | |
144 | 0 | return FAILURE; |
145 | 0 | } |
146 | | |
147 | | typedef struct _php_stream_http_response_header_info { |
148 | | php_stream_filter *transfer_encoding; |
149 | | size_t file_size; |
150 | | bool error; |
151 | | bool follow_location; |
152 | | char *location; |
153 | | size_t location_len; |
154 | | } php_stream_http_response_header_info; |
155 | | |
156 | | static void php_stream_http_response_header_info_init( |
157 | | php_stream_http_response_header_info *header_info) |
158 | 0 | { |
159 | 0 | memset(header_info, 0, sizeof(php_stream_http_response_header_info)); |
160 | 0 | header_info->follow_location = 1; |
161 | 0 | } |
162 | | |
163 | | /* Trim white spaces from response header line and update its length */ |
164 | | static bool php_stream_http_response_header_trim(char *http_header_line, |
165 | | size_t *http_header_line_length) |
166 | 0 | { |
167 | 0 | char *http_header_line_end = http_header_line + *http_header_line_length - 1; |
168 | 0 | while (http_header_line_end >= http_header_line && |
169 | 0 | (*http_header_line_end == '\n' || *http_header_line_end == '\r')) { |
170 | 0 | http_header_line_end--; |
171 | 0 | } |
172 | | |
173 | | /* The primary definition of an HTTP header in RFC 7230 states: |
174 | | * > Each header field consists of a case-insensitive field name followed |
175 | | * > by a colon (":"), optional leading whitespace, the field value, and |
176 | | * > optional trailing whitespace. */ |
177 | | |
178 | | /* Strip trailing whitespace */ |
179 | 0 | bool space_trim = (*http_header_line_end == ' ' || *http_header_line_end == '\t'); |
180 | 0 | if (space_trim) { |
181 | 0 | do { |
182 | 0 | http_header_line_end--; |
183 | 0 | } while (http_header_line_end >= http_header_line && |
184 | 0 | (*http_header_line_end == ' ' || *http_header_line_end == '\t')); |
185 | 0 | } |
186 | 0 | http_header_line_end++; |
187 | 0 | *http_header_line_end = '\0'; |
188 | 0 | *http_header_line_length = http_header_line_end - http_header_line; |
189 | |
|
190 | 0 | return space_trim; |
191 | 0 | } |
192 | | |
193 | | /* Process folding headers of the current line and if there are none, parse last full response |
194 | | * header line. It returns NULL if the last header is finished, otherwise it returns updated |
195 | | * last header line. */ |
196 | | static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *wrapper, |
197 | | php_stream *stream, php_stream_context *context, int options, |
198 | | zend_string *last_header_line_str, char *header_line, size_t *header_line_length, |
199 | | int response_code, zval *response_header, |
200 | | php_stream_http_response_header_info *header_info) |
201 | 0 | { |
202 | 0 | char *last_header_line = ZSTR_VAL(last_header_line_str); |
203 | 0 | size_t last_header_line_length = ZSTR_LEN(last_header_line_str); |
204 | 0 | char *last_header_line_end = ZSTR_VAL(last_header_line_str) + ZSTR_LEN(last_header_line_str) - 1; |
205 | | |
206 | | /* Process non empty header line. */ |
207 | 0 | if (header_line && (*header_line != '\n' && *header_line != '\r')) { |
208 | | /* Removing trailing white spaces. */ |
209 | 0 | if (php_stream_http_response_header_trim(header_line, header_line_length) && |
210 | 0 | *header_line_length == 0) { |
211 | | /* Only spaces so treat as an empty folding header. */ |
212 | 0 | return last_header_line_str; |
213 | 0 | } |
214 | | |
215 | | /* Process folding headers if starting with a space or a tab. */ |
216 | 0 | if (header_line && (*header_line == ' ' || *header_line == '\t')) { |
217 | 0 | char *http_folded_header_line = header_line; |
218 | 0 | size_t http_folded_header_line_length = *header_line_length; |
219 | | /* Remove the leading white spaces. */ |
220 | 0 | while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') { |
221 | 0 | http_folded_header_line++; |
222 | 0 | http_folded_header_line_length--; |
223 | 0 | } |
224 | | /* It has to have some characters because it would get returned after the call |
225 | | * php_stream_http_response_header_trim above. */ |
226 | 0 | ZEND_ASSERT(http_folded_header_line_length > 0); |
227 | | /* Concatenate last header line, space and current header line. */ |
228 | 0 | zend_string *extended_header_str = zend_string_concat3( |
229 | 0 | last_header_line, last_header_line_length, |
230 | 0 | " ", 1, |
231 | 0 | http_folded_header_line, http_folded_header_line_length); |
232 | 0 | zend_string_efree(last_header_line_str); |
233 | 0 | last_header_line_str = extended_header_str; |
234 | | /* Return new header line. */ |
235 | 0 | return last_header_line_str; |
236 | 0 | } |
237 | 0 | } |
238 | | |
239 | | /* Find header separator position. */ |
240 | 0 | char *last_header_value = memchr(last_header_line, ':', last_header_line_length); |
241 | 0 | if (last_header_value) { |
242 | | /* Verify there is no space in header name */ |
243 | 0 | char *last_header_name = last_header_line + 1; |
244 | 0 | while (last_header_name < last_header_value) { |
245 | 0 | if (*last_header_name == ' ' || *last_header_name == '\t') { |
246 | 0 | header_info->error = true; |
247 | 0 | php_stream_wrapper_log_error(wrapper, options, |
248 | 0 | "HTTP invalid response format (space in header name)!"); |
249 | 0 | zend_string_efree(last_header_line_str); |
250 | 0 | return NULL; |
251 | 0 | } |
252 | 0 | ++last_header_name; |
253 | 0 | } |
254 | | |
255 | 0 | last_header_value++; /* Skip ':'. */ |
256 | | |
257 | | /* Strip leading whitespace. */ |
258 | 0 | while (last_header_value < last_header_line_end |
259 | 0 | && (*last_header_value == ' ' || *last_header_value == '\t')) { |
260 | 0 | last_header_value++; |
261 | 0 | } |
262 | 0 | } else { |
263 | | /* There is no colon which means invalid response so error. */ |
264 | 0 | header_info->error = true; |
265 | 0 | php_stream_wrapper_log_error(wrapper, options, |
266 | 0 | "HTTP invalid response format (no colon in header line)!"); |
267 | 0 | zend_string_efree(last_header_line_str); |
268 | 0 | return NULL; |
269 | 0 | } |
270 | | |
271 | 0 | bool store_header = true; |
272 | 0 | zval *tmpzval = NULL; |
273 | |
|
274 | 0 | if (!strncasecmp(last_header_line, "Location:", sizeof("Location:")-1)) { |
275 | | /* Check if the location should be followed. */ |
276 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) { |
277 | 0 | header_info->follow_location = zval_is_true(tmpzval); |
278 | 0 | } else if (!((response_code >= 300 && response_code < 304) |
279 | 0 | || 307 == response_code || 308 == response_code)) { |
280 | | /* The redirection should not be automatic if follow_location is not set and |
281 | | * response_code not in (300, 301, 302, 303 and 307) |
282 | | * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1 |
283 | | * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ |
284 | 0 | header_info->follow_location = 0; |
285 | 0 | } |
286 | 0 | size_t last_header_value_len = strlen(last_header_value); |
287 | 0 | if (last_header_value_len > HTTP_HEADER_MAX_LOCATION_SIZE) { |
288 | 0 | header_info->error = true; |
289 | 0 | php_stream_wrapper_log_error(wrapper, options, |
290 | 0 | "HTTP Location header size is over the limit of %d bytes", |
291 | 0 | HTTP_HEADER_MAX_LOCATION_SIZE); |
292 | 0 | zend_string_efree(last_header_line_str); |
293 | 0 | return NULL; |
294 | 0 | } |
295 | 0 | if (header_info->location_len == 0) { |
296 | 0 | header_info->location = emalloc(last_header_value_len + 1); |
297 | 0 | } else if (header_info->location_len <= last_header_value_len) { |
298 | 0 | header_info->location = erealloc(header_info->location, last_header_value_len + 1); |
299 | 0 | } |
300 | 0 | header_info->location_len = last_header_value_len; |
301 | 0 | memcpy(header_info->location, last_header_value, last_header_value_len + 1); |
302 | 0 | } else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { |
303 | 0 | php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0); |
304 | 0 | } else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { |
305 | | /* https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length */ |
306 | 0 | const char *ptr = last_header_value; |
307 | | /* must contain only digits, no + or - symbols */ |
308 | 0 | if (*ptr >= '0' && *ptr <= '9') { |
309 | 0 | char *endptr = NULL; |
310 | 0 | size_t parsed = ZEND_STRTOUL(ptr, &endptr, 10); |
311 | | /* check whether there was no garbage in the header value and the conversion was successful */ |
312 | 0 | if (endptr && !*endptr) { |
313 | | /* truncate for 32-bit such that no negative file sizes occur */ |
314 | 0 | header_info->file_size = MIN(parsed, ZEND_LONG_MAX); |
315 | 0 | php_stream_notify_file_size(context, header_info->file_size, last_header_line, 0); |
316 | 0 | } |
317 | 0 | } |
318 | 0 | } else if ( |
319 | 0 | !strncasecmp(last_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1) |
320 | 0 | && !strncasecmp(last_header_value, "Chunked", sizeof("Chunked")-1) |
321 | 0 | ) { |
322 | | /* Create filter to decode response body. */ |
323 | 0 | if (!(options & STREAM_ONLY_GET_HEADERS)) { |
324 | 0 | bool decode = true; |
325 | |
|
326 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) { |
327 | 0 | decode = zend_is_true(tmpzval); |
328 | 0 | } |
329 | 0 | if (decode) { |
330 | 0 | if (header_info->transfer_encoding != NULL) { |
331 | | /* Prevent a memory leak in case there are more transfer-encoding headers. */ |
332 | 0 | php_stream_filter_free(header_info->transfer_encoding); |
333 | 0 | } |
334 | 0 | header_info->transfer_encoding = php_stream_filter_create( |
335 | 0 | "dechunk", NULL, php_stream_is_persistent(stream)); |
336 | 0 | if (header_info->transfer_encoding != NULL) { |
337 | | /* Do not store transfer-encoding header. */ |
338 | 0 | store_header = false; |
339 | 0 | } |
340 | 0 | } |
341 | 0 | } |
342 | 0 | } |
343 | | |
344 | 0 | if (store_header) { |
345 | 0 | zval http_header; |
346 | 0 | ZVAL_NEW_STR(&http_header, last_header_line_str); |
347 | 0 | zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header); |
348 | 0 | } else { |
349 | 0 | zend_string_efree(last_header_line_str); |
350 | 0 | } |
351 | |
|
352 | 0 | return NULL; |
353 | 0 | } |
354 | | |
355 | | static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, |
356 | | const char *path, const char *mode, int options, zend_string **opened_path, |
357 | | php_stream_context *context, int redirect_max, int flags, |
358 | | zval *response_header STREAMS_DC) /* {{{ */ |
359 | 0 | { |
360 | 0 | php_stream *stream = NULL; |
361 | 0 | php_url *resource = NULL; |
362 | 0 | int use_ssl; |
363 | 0 | int use_proxy = 0; |
364 | 0 | zend_string *tmp = NULL; |
365 | 0 | char *ua_str = NULL; |
366 | 0 | zval *ua_zval = NULL, *tmpzval = NULL, ssl_proxy_peer_name; |
367 | 0 | int reqok = 0; |
368 | 0 | char *http_header_line = NULL; |
369 | 0 | zend_string *last_header_line_str = NULL; |
370 | 0 | php_stream_http_response_header_info header_info; |
371 | 0 | char tmp_line[128]; |
372 | 0 | size_t chunk_size = 0; |
373 | 0 | int eol_detect = 0; |
374 | 0 | zend_string *transport_string; |
375 | 0 | zend_string *errstr = NULL; |
376 | 0 | int have_header = 0; |
377 | 0 | bool request_fulluri = false, ignore_errors = false; |
378 | 0 | struct timeval timeout; |
379 | 0 | char *user_headers = NULL; |
380 | 0 | int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0); |
381 | 0 | int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0); |
382 | 0 | int redirect_keep_method = ((flags & HTTP_WRAPPER_KEEP_METHOD) != 0); |
383 | 0 | int response_code; |
384 | 0 | smart_str req_buf = {0}; |
385 | 0 | bool custom_request_method; |
386 | |
|
387 | 0 | tmp_line[0] = '\0'; |
388 | |
|
389 | 0 | if (redirect_max < 1) { |
390 | 0 | php_stream_wrapper_log_error(wrapper, options, "Redirection limit reached, aborting"); |
391 | 0 | return NULL; |
392 | 0 | } |
393 | | |
394 | 0 | resource = php_url_parse(path); |
395 | 0 | if (resource == NULL) { |
396 | 0 | return NULL; |
397 | 0 | } |
398 | | |
399 | 0 | ZEND_ASSERT(resource->scheme); |
400 | 0 | if (!zend_string_equals_literal_ci(resource->scheme, "http") && |
401 | 0 | !zend_string_equals_literal_ci(resource->scheme, "https")) { |
402 | 0 | if (!context || |
403 | 0 | (tmpzval = php_stream_context_get_option(context, wrapper->wops->label, "proxy")) == NULL || |
404 | 0 | Z_TYPE_P(tmpzval) != IS_STRING || |
405 | 0 | Z_STRLEN_P(tmpzval) == 0) { |
406 | 0 | php_url_free(resource); |
407 | 0 | return php_stream_open_wrapper_ex(path, mode, REPORT_ERRORS, NULL, context); |
408 | 0 | } |
409 | | /* Called from a non-http wrapper with http proxying requested (i.e. ftp) */ |
410 | 0 | request_fulluri = true; |
411 | 0 | use_ssl = 0; |
412 | 0 | use_proxy = 1; |
413 | 0 | transport_string = zend_string_copy(Z_STR_P(tmpzval)); |
414 | 0 | } else { |
415 | | /* Normal http request (possibly with proxy) */ |
416 | |
|
417 | 0 | if (strpbrk(mode, "awx+")) { |
418 | 0 | php_stream_wrapper_log_error(wrapper, options, "HTTP wrapper does not support writeable connections"); |
419 | 0 | php_url_free(resource); |
420 | 0 | return NULL; |
421 | 0 | } |
422 | | |
423 | | /* Should we send the entire path in the request line, default to no. */ |
424 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "request_fulluri")) != NULL) { |
425 | 0 | request_fulluri = zend_is_true(tmpzval); |
426 | 0 | } |
427 | |
|
428 | 0 | use_ssl = (ZSTR_LEN(resource->scheme) > 4) && ZSTR_VAL(resource->scheme)[4] == 's'; |
429 | | /* choose default ports */ |
430 | 0 | if (use_ssl && resource->port == 0) |
431 | 0 | resource->port = 443; |
432 | 0 | else if (resource->port == 0) |
433 | 0 | resource->port = 80; |
434 | |
|
435 | 0 | if (context && |
436 | 0 | (tmpzval = php_stream_context_get_option(context, wrapper->wops->label, "proxy")) != NULL && |
437 | 0 | Z_TYPE_P(tmpzval) == IS_STRING && |
438 | 0 | Z_STRLEN_P(tmpzval) > 0) { |
439 | 0 | use_proxy = 1; |
440 | 0 | transport_string = zend_string_copy(Z_STR_P(tmpzval)); |
441 | 0 | } else { |
442 | 0 | transport_string = zend_strpprintf(0, "%s://%s:%d", use_ssl ? "ssl" : "tcp", ZSTR_VAL(resource->host), resource->port); |
443 | 0 | } |
444 | 0 | } |
445 | | |
446 | 0 | if (request_fulluri && (strchr(path, '\n') != NULL || strchr(path, '\r') != NULL)) { |
447 | 0 | php_stream_wrapper_log_error(wrapper, options, "HTTP wrapper full URI path does not allow CR or LF characters"); |
448 | 0 | php_url_free(resource); |
449 | 0 | zend_string_release(transport_string); |
450 | 0 | return NULL; |
451 | 0 | } |
452 | | |
453 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, wrapper->wops->label, "timeout")) != NULL) { |
454 | 0 | double d = zval_get_double(tmpzval); |
455 | 0 | #ifndef PHP_WIN32 |
456 | 0 | const double timeoutmax = (double) PHP_TIMEOUT_ULL_MAX / 1000000.0; |
457 | | #else |
458 | | const double timeoutmax = (double) LONG_MAX / 1000000.0; |
459 | | #endif |
460 | |
|
461 | 0 | if (d > timeoutmax) { |
462 | 0 | php_stream_wrapper_log_error(wrapper, options, "timeout must be lower than " ZEND_ULONG_FMT, (zend_ulong)timeoutmax); |
463 | 0 | zend_string_release(transport_string); |
464 | 0 | php_url_free(resource); |
465 | 0 | return NULL; |
466 | 0 | } |
467 | 0 | #ifndef PHP_WIN32 |
468 | 0 | timeout.tv_sec = (time_t) d; |
469 | 0 | timeout.tv_usec = (size_t) ((d - timeout.tv_sec) * 1000000); |
470 | | #else |
471 | | timeout.tv_sec = (long) d; |
472 | | timeout.tv_usec = (long) ((d - timeout.tv_sec) * 1000000); |
473 | | #endif |
474 | 0 | } else { |
475 | 0 | #ifndef PHP_WIN32 |
476 | 0 | timeout.tv_sec = FG(default_socket_timeout); |
477 | | #else |
478 | | timeout.tv_sec = (long)FG(default_socket_timeout); |
479 | | #endif |
480 | 0 | timeout.tv_usec = 0; |
481 | 0 | } |
482 | | |
483 | 0 | stream = php_stream_xport_create(ZSTR_VAL(transport_string), ZSTR_LEN(transport_string), options, |
484 | 0 | STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, |
485 | 0 | NULL, &timeout, context, &errstr, NULL); |
486 | |
|
487 | 0 | if (stream) { |
488 | 0 | php_stream_set_option(stream, PHP_STREAM_OPTION_READ_TIMEOUT, 0, &timeout); |
489 | 0 | } |
490 | |
|
491 | 0 | if (errstr) { |
492 | 0 | php_stream_wrapper_log_error(wrapper, options, "%s", ZSTR_VAL(errstr)); |
493 | 0 | zend_string_release_ex(errstr, 0); |
494 | 0 | errstr = NULL; |
495 | 0 | } |
496 | |
|
497 | 0 | zend_string_release(transport_string); |
498 | |
|
499 | 0 | if (stream && use_proxy && use_ssl) { |
500 | 0 | smart_str header = {0}; |
501 | 0 | bool reset_ssl_peer_name = false; |
502 | | |
503 | | /* Set peer_name or name verification will try to use the proxy server name */ |
504 | 0 | if (!context || (tmpzval = php_stream_context_get_option(context, "ssl", "peer_name")) == NULL) { |
505 | 0 | ZVAL_STR_COPY(&ssl_proxy_peer_name, resource->host); |
506 | 0 | php_stream_context_set_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name", &ssl_proxy_peer_name); |
507 | 0 | zval_ptr_dtor(&ssl_proxy_peer_name); |
508 | 0 | reset_ssl_peer_name = true; |
509 | 0 | } |
510 | |
|
511 | 0 | smart_str_appendl(&header, "CONNECT ", sizeof("CONNECT ")-1); |
512 | 0 | smart_str_appends(&header, ZSTR_VAL(resource->host)); |
513 | 0 | smart_str_appendc(&header, ':'); |
514 | 0 | smart_str_append_unsigned(&header, resource->port); |
515 | 0 | smart_str_appendl(&header, " HTTP/1.0\r\n", sizeof(" HTTP/1.0\r\n")-1); |
516 | | |
517 | | /* check if we have Proxy-Authorization header */ |
518 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "header")) != NULL) { |
519 | 0 | const char *s; |
520 | |
|
521 | 0 | if (Z_TYPE_P(tmpzval) == IS_ARRAY) { |
522 | 0 | zval *tmpheader = NULL; |
523 | |
|
524 | 0 | ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(tmpzval), tmpheader) { |
525 | 0 | if (Z_TYPE_P(tmpheader) == IS_STRING) { |
526 | 0 | s = Z_STRVAL_P(tmpheader); |
527 | 0 | if (php_stream_handle_proxy_authorization_header(s, &header) == SUCCESS) { |
528 | 0 | goto finish; |
529 | 0 | } |
530 | 0 | } |
531 | 0 | } ZEND_HASH_FOREACH_END(); |
532 | 0 | } else if (Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval)) { |
533 | 0 | s = Z_STRVAL_P(tmpzval); |
534 | 0 | if (php_stream_handle_proxy_authorization_header(s, &header) == SUCCESS) { |
535 | 0 | goto finish; |
536 | 0 | } |
537 | 0 | } |
538 | 0 | } |
539 | 0 | finish: |
540 | 0 | smart_str_appendl(&header, "\r\n", sizeof("\r\n")-1); |
541 | |
|
542 | 0 | if (php_stream_write(stream, ZSTR_VAL(header.s), ZSTR_LEN(header.s)) != ZSTR_LEN(header.s)) { |
543 | 0 | php_stream_wrapper_log_error(wrapper, options, "Cannot connect to HTTPS server through proxy"); |
544 | 0 | php_stream_close(stream); |
545 | 0 | stream = NULL; |
546 | 0 | } |
547 | 0 | smart_str_free(&header); |
548 | |
|
549 | 0 | if (stream) { |
550 | 0 | char header_line[HTTP_HEADER_BLOCK_SIZE]; |
551 | | |
552 | | /* get response header */ |
553 | 0 | while (php_stream_gets(stream, header_line, HTTP_HEADER_BLOCK_SIZE-1) != NULL) { |
554 | 0 | if (header_line[0] == '\n' || |
555 | 0 | header_line[0] == '\r' || |
556 | 0 | header_line[0] == '\0') { |
557 | 0 | break; |
558 | 0 | } |
559 | 0 | } |
560 | 0 | } |
561 | | |
562 | | /* enable SSL transport layer */ |
563 | 0 | if (stream) { |
564 | 0 | if (php_stream_xport_crypto_setup(stream, STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 || |
565 | 0 | php_stream_xport_crypto_enable(stream, 1) < 0) { |
566 | 0 | php_stream_wrapper_log_error(wrapper, options, "Cannot connect to HTTPS server through proxy"); |
567 | 0 | php_stream_close(stream); |
568 | 0 | stream = NULL; |
569 | 0 | } |
570 | 0 | } |
571 | |
|
572 | 0 | if (reset_ssl_peer_name) { |
573 | 0 | php_stream_context_unset_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name"); |
574 | 0 | } |
575 | 0 | } |
576 | | |
577 | 0 | php_stream_http_response_header_info_init(&header_info); |
578 | |
|
579 | 0 | if (stream == NULL) |
580 | 0 | goto out; |
581 | | |
582 | | /* avoid buffering issues while reading header */ |
583 | 0 | if (options & STREAM_WILL_CAST) |
584 | 0 | chunk_size = php_stream_set_chunk_size(stream, 1); |
585 | | |
586 | | /* avoid problems with auto-detecting when reading the headers -> the headers |
587 | | * are always in canonical \r\n format */ |
588 | 0 | eol_detect = stream->flags & (PHP_STREAM_FLAG_DETECT_EOL | PHP_STREAM_FLAG_EOL_MAC); |
589 | 0 | stream->flags &= ~(PHP_STREAM_FLAG_DETECT_EOL | PHP_STREAM_FLAG_EOL_MAC); |
590 | |
|
591 | 0 | php_stream_context_set(stream, context); |
592 | |
|
593 | 0 | php_stream_notify_info(context, PHP_STREAM_NOTIFY_CONNECT, NULL, 0); |
594 | |
|
595 | 0 | if (header_init && context && (tmpzval = php_stream_context_get_option(context, "http", "max_redirects")) != NULL) { |
596 | 0 | redirect_max = (int)zval_get_long(tmpzval); |
597 | 0 | } |
598 | |
|
599 | 0 | custom_request_method = 0; |
600 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "method")) != NULL) { |
601 | 0 | if (Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval) > 0) { |
602 | | /* As per the RFC, automatically redirected requests MUST NOT use other methods than |
603 | | * GET and HEAD unless it can be confirmed by the user. */ |
604 | 0 | if (!redirected || redirect_keep_method |
605 | 0 | || zend_string_equals_literal(Z_STR_P(tmpzval), "GET") |
606 | 0 | || zend_string_equals_literal(Z_STR_P(tmpzval), "HEAD") |
607 | 0 | ) { |
608 | 0 | custom_request_method = 1; |
609 | 0 | smart_str_append(&req_buf, Z_STR_P(tmpzval)); |
610 | 0 | smart_str_appendc(&req_buf, ' '); |
611 | 0 | } |
612 | 0 | } |
613 | 0 | } |
614 | |
|
615 | 0 | if (!custom_request_method) { |
616 | 0 | smart_str_appends(&req_buf, "GET "); |
617 | 0 | } |
618 | |
|
619 | 0 | if (request_fulluri) { |
620 | | /* Ask for everything */ |
621 | 0 | smart_str_appends(&req_buf, path); |
622 | 0 | } else { |
623 | | /* Send the traditional /path/to/file?query_string */ |
624 | | |
625 | | /* file */ |
626 | 0 | if (resource->path && ZSTR_LEN(resource->path)) { |
627 | 0 | smart_str_appends(&req_buf, ZSTR_VAL(resource->path)); |
628 | 0 | } else { |
629 | 0 | smart_str_appendc(&req_buf, '/'); |
630 | 0 | } |
631 | | |
632 | | /* query string */ |
633 | 0 | if (resource->query) { |
634 | 0 | smart_str_appendc(&req_buf, '?'); |
635 | 0 | smart_str_appends(&req_buf, ZSTR_VAL(resource->query)); |
636 | 0 | } |
637 | 0 | } |
638 | | |
639 | | /* protocol version we are speaking */ |
640 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "protocol_version")) != NULL) { |
641 | 0 | char *protocol_version; |
642 | 0 | spprintf(&protocol_version, 0, "%.1F", zval_get_double(tmpzval)); |
643 | |
|
644 | 0 | smart_str_appends(&req_buf, " HTTP/"); |
645 | 0 | smart_str_appends(&req_buf, protocol_version); |
646 | 0 | smart_str_appends(&req_buf, "\r\n"); |
647 | 0 | efree(protocol_version); |
648 | 0 | } else { |
649 | 0 | smart_str_appends(&req_buf, " HTTP/1.1\r\n"); |
650 | 0 | } |
651 | |
|
652 | 0 | if (context && (tmpzval = php_stream_context_get_option(context, "http", "header")) != NULL) { |
653 | 0 | tmp = NULL; |
654 | |
|
655 | 0 | if (Z_TYPE_P(tmpzval) == IS_ARRAY) { |
656 | 0 | zval *tmpheader = NULL; |
657 | 0 | smart_str tmpstr = {0}; |
658 | |
|
659 | 0 | ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(tmpzval), tmpheader) { |
660 | 0 | if (Z_TYPE_P(tmpheader) == IS_STRING) { |
661 | 0 | smart_str_append(&tmpstr, Z_STR_P(tmpheader)); |
662 | 0 | smart_str_appendl(&tmpstr, "\r\n", sizeof("\r\n") - 1); |
663 | 0 | } |
664 | 0 | } ZEND_HASH_FOREACH_END(); |
665 | 0 | smart_str_0(&tmpstr); |
666 | | /* Remove newlines and spaces from start and end. there's at least one extra \r\n at the end that needs to go. */ |
667 | 0 | if (tmpstr.s) { |
668 | 0 | tmp = php_trim(tmpstr.s, NULL, 0, 3); |
669 | 0 | smart_str_free(&tmpstr); |
670 | 0 | } |
671 | 0 | } else if (Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval)) { |
672 | | /* Remove newlines and spaces from start and end php_trim will estrndup() */ |
673 | 0 | tmp = php_trim(Z_STR_P(tmpzval), NULL, 0, 3); |
674 | 0 | } |
675 | 0 | if (tmp && ZSTR_LEN(tmp)) { |
676 | 0 | char *s; |
677 | 0 | char *t; |
678 | |
|
679 | 0 | user_headers = estrndup(ZSTR_VAL(tmp), ZSTR_LEN(tmp)); |
680 | |
|
681 | 0 | if (ZSTR_IS_INTERNED(tmp)) { |
682 | 0 | tmp = zend_string_init(ZSTR_VAL(tmp), ZSTR_LEN(tmp), 0); |
683 | 0 | } else if (GC_REFCOUNT(tmp) > 1) { |
684 | 0 | GC_DELREF(tmp); |
685 | 0 | tmp = zend_string_init(ZSTR_VAL(tmp), ZSTR_LEN(tmp), 0); |
686 | 0 | } |
687 | | |
688 | | /* Make lowercase for easy comparison against 'standard' headers */ |
689 | 0 | zend_str_tolower(ZSTR_VAL(tmp), ZSTR_LEN(tmp)); |
690 | 0 | t = ZSTR_VAL(tmp); |
691 | |
|
692 | 0 | if (!header_init && !redirect_keep_method) { |
693 | | /* strip POST headers on redirect */ |
694 | 0 | strip_header(user_headers, t, "content-length:"); |
695 | 0 | strip_header(user_headers, t, "content-type:"); |
696 | 0 | } |
697 | |
|
698 | 0 | if (check_has_header(t, "user-agent:")) { |
699 | 0 | have_header |= HTTP_HEADER_USER_AGENT; |
700 | 0 | } |
701 | 0 | if (check_has_header(t, "host:")) { |
702 | 0 | have_header |= HTTP_HEADER_HOST; |
703 | 0 | } |
704 | 0 | if (check_has_header(t, "from:")) { |
705 | 0 | have_header |= HTTP_HEADER_FROM; |
706 | 0 | } |
707 | 0 | if (check_has_header(t, "authorization:")) { |
708 | 0 | have_header |= HTTP_HEADER_AUTH; |
709 | 0 | } |
710 | 0 | if (check_has_header(t, "content-length:")) { |
711 | 0 | have_header |= HTTP_HEADER_CONTENT_LENGTH; |
712 | 0 | } |
713 | 0 | if (check_has_header(t, "content-type:")) { |
714 | 0 | have_header |= HTTP_HEADER_TYPE; |
715 | 0 | } |
716 | 0 | if (check_has_header(t, "connection:")) { |
717 | 0 | have_header |= HTTP_HEADER_CONNECTION; |
718 | 0 | } |
719 | | |
720 | | /* remove Proxy-Authorization header */ |
721 | 0 | if (use_proxy && use_ssl && (s = strstr(t, "proxy-authorization:")) && |
722 | 0 | (s == t || *(s-1) == '\n')) { |
723 | 0 | char *p = s + sizeof("proxy-authorization:") - 1; |
724 | |
|
725 | 0 | while (s > t && (*(s-1) == ' ' || *(s-1) == '\t')) s--; |
726 | 0 | while (*p != 0 && *p != '\r' && *p != '\n') p++; |
727 | 0 | while (*p == '\r' || *p == '\n') p++; |
728 | 0 | if (*p == 0) { |
729 | 0 | if (s == t) { |
730 | 0 | efree(user_headers); |
731 | 0 | user_headers = NULL; |
732 | 0 | } else { |
733 | 0 | while (s > t && (*(s-1) == '\r' || *(s-1) == '\n')) s--; |
734 | 0 | user_headers[s - t] = 0; |
735 | 0 | } |
736 | 0 | } else { |
737 | 0 | memmove(user_headers + (s - t), user_headers + (p - t), strlen(p) + 1); |
738 | 0 | } |
739 | 0 | } |
740 | |
|
741 | 0 | } |
742 | 0 | if (tmp) { |
743 | 0 | zend_string_release_ex(tmp, 0); |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | | /* auth header if it was specified */ |
748 | 0 | if (((have_header & HTTP_HEADER_AUTH) == 0) && resource->user) { |
749 | | /* make scratch large enough to hold the whole URL (over-estimate) */ |
750 | 0 | size_t scratch_len = strlen(path) + 1; |
751 | 0 | char *scratch = emalloc(scratch_len); |
752 | 0 | zend_string *stmp; |
753 | | |
754 | | /* decode the strings first */ |
755 | 0 | php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user)); |
756 | |
|
757 | 0 | strcpy(scratch, ZSTR_VAL(resource->user)); |
758 | 0 | strcat(scratch, ":"); |
759 | | |
760 | | /* Note: password is optional! */ |
761 | 0 | if (resource->pass) { |
762 | 0 | php_url_decode(ZSTR_VAL(resource->pass), ZSTR_LEN(resource->pass)); |
763 | 0 | strcat(scratch, ZSTR_VAL(resource->pass)); |
764 | 0 | } |
765 | |
|
766 | 0 | stmp = php_base64_encode((unsigned char*)scratch, strlen(scratch)); |
767 | |
|
768 | 0 | smart_str_appends(&req_buf, "Authorization: Basic "); |
769 | 0 | smart_str_appends(&req_buf, ZSTR_VAL(stmp)); |
770 | 0 | smart_str_appends(&req_buf, "\r\n"); |
771 | |
|
772 | 0 | php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, NULL, 0); |
773 | |
|
774 | 0 | zend_string_free(stmp); |
775 | 0 | efree(scratch); |
776 | 0 | } |
777 | | |
778 | | /* if the user has configured who they are, send a From: line */ |
779 | 0 | if (!(have_header & HTTP_HEADER_FROM) && FG(from_address)) { |
780 | 0 | smart_str_appends(&req_buf, "From: "); |
781 | 0 | smart_str_appends(&req_buf, FG(from_address)); |
782 | 0 | smart_str_appends(&req_buf, "\r\n"); |
783 | 0 | } |
784 | | |
785 | | /* Send Host: header so name-based virtual hosts work */ |
786 | 0 | if ((have_header & HTTP_HEADER_HOST) == 0) { |
787 | 0 | smart_str_appends(&req_buf, "Host: "); |
788 | 0 | smart_str_appends(&req_buf, ZSTR_VAL(resource->host)); |
789 | 0 | if ((use_ssl && resource->port != 443 && resource->port != 0) || |
790 | 0 | (!use_ssl && resource->port != 80 && resource->port != 0)) { |
791 | 0 | smart_str_appendc(&req_buf, ':'); |
792 | 0 | smart_str_append_unsigned(&req_buf, resource->port); |
793 | 0 | } |
794 | 0 | smart_str_appends(&req_buf, "\r\n"); |
795 | 0 | } |
796 | | |
797 | | /* Send a Connection: close header to avoid hanging when the server |
798 | | * interprets the RFC literally and establishes a keep-alive connection, |
799 | | * unless the user specifically requests something else by specifying a |
800 | | * Connection header in the context options. Send that header even for |
801 | | * HTTP/1.0 to avoid issues when the server respond with an HTTP/1.1 |
802 | | * keep-alive response, which is the preferred response type. */ |
803 | 0 | if ((have_header & HTTP_HEADER_CONNECTION) == 0) { |
804 | 0 | smart_str_appends(&req_buf, "Connection: close\r\n"); |
805 | 0 | } |
806 | |
|
807 | 0 | if (context && |
808 | 0 | (ua_zval = php_stream_context_get_option(context, "http", "user_agent")) != NULL && |
809 | 0 | Z_TYPE_P(ua_zval) == IS_STRING) { |
810 | 0 | ua_str = Z_STRVAL_P(ua_zval); |
811 | 0 | } else if (FG(user_agent)) { |
812 | 0 | ua_str = FG(user_agent); |
813 | 0 | } |
814 | |
|
815 | 0 | if (((have_header & HTTP_HEADER_USER_AGENT) == 0) && ua_str) { |
816 | 0 | #define _UA_HEADER "User-Agent: %s\r\n" |
817 | 0 | char *ua; |
818 | 0 | size_t ua_len; |
819 | |
|
820 | 0 | ua_len = sizeof(_UA_HEADER) + strlen(ua_str); |
821 | | |
822 | | /* ensure the header is only sent if user_agent is not blank */ |
823 | 0 | if (ua_len > sizeof(_UA_HEADER)) { |
824 | 0 | ua = emalloc(ua_len + 1); |
825 | 0 | if ((ua_len = slprintf(ua, ua_len, _UA_HEADER, ua_str)) > 0) { |
826 | 0 | ua[ua_len] = 0; |
827 | 0 | smart_str_appendl(&req_buf, ua, ua_len); |
828 | 0 | } else { |
829 | 0 | php_error_docref(NULL, E_WARNING, "Cannot construct User-agent header"); |
830 | 0 | } |
831 | 0 | efree(ua); |
832 | 0 | } |
833 | 0 | } |
834 | |
|
835 | 0 | if (user_headers) { |
836 | | /* A bit weird, but some servers require that Content-Length be sent prior to Content-Type for POST |
837 | | * see bug #44603 for details. Since Content-Type maybe part of user's headers we need to do this check first. |
838 | | */ |
839 | 0 | if ( |
840 | 0 | (header_init || redirect_keep_method) && |
841 | 0 | context && |
842 | 0 | !(have_header & HTTP_HEADER_CONTENT_LENGTH) && |
843 | 0 | (tmpzval = php_stream_context_get_option(context, "http", "content")) != NULL && |
844 | 0 | Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval) > 0 |
845 | 0 | ) { |
846 | 0 | smart_str_appends(&req_buf, "Content-Length: "); |
847 | 0 | smart_str_append_unsigned(&req_buf, Z_STRLEN_P(tmpzval)); |
848 | 0 | smart_str_appends(&req_buf, "\r\n"); |
849 | 0 | have_header |= HTTP_HEADER_CONTENT_LENGTH; |
850 | 0 | } |
851 | |
|
852 | 0 | smart_str_appends(&req_buf, user_headers); |
853 | 0 | smart_str_appends(&req_buf, "\r\n"); |
854 | 0 | efree(user_headers); |
855 | 0 | } |
856 | | |
857 | | /* Request content, such as for POST requests */ |
858 | 0 | if ((header_init || redirect_keep_method) && context && |
859 | 0 | (tmpzval = php_stream_context_get_option(context, "http", "content")) != NULL && |
860 | 0 | Z_TYPE_P(tmpzval) == IS_STRING && Z_STRLEN_P(tmpzval) > 0) { |
861 | 0 | if (!(have_header & HTTP_HEADER_CONTENT_LENGTH)) { |
862 | 0 | smart_str_appends(&req_buf, "Content-Length: "); |
863 | 0 | smart_str_append_unsigned(&req_buf, Z_STRLEN_P(tmpzval)); |
864 | 0 | smart_str_appends(&req_buf, "\r\n"); |
865 | 0 | } |
866 | 0 | if (!(have_header & HTTP_HEADER_TYPE)) { |
867 | 0 | smart_str_appends(&req_buf, "Content-Type: application/x-www-form-urlencoded\r\n"); |
868 | 0 | php_error_docref(NULL, E_NOTICE, "Content-type not specified assuming application/x-www-form-urlencoded"); |
869 | 0 | } |
870 | 0 | smart_str_appends(&req_buf, "\r\n"); |
871 | 0 | smart_str_appendl(&req_buf, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval)); |
872 | 0 | } else { |
873 | 0 | smart_str_appends(&req_buf, "\r\n"); |
874 | 0 | } |
875 | | |
876 | | /* send it */ |
877 | 0 | php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s)); |
878 | |
|
879 | 0 | if (Z_ISUNDEF_P(response_header)) { |
880 | 0 | array_init(response_header); |
881 | 0 | } |
882 | |
|
883 | 0 | { |
884 | | /* get response header */ |
885 | 0 | size_t tmp_line_len; |
886 | 0 | if (!php_stream_eof(stream) && |
887 | 0 | php_stream_get_line(stream, tmp_line, sizeof(tmp_line) - 1, &tmp_line_len) != NULL) { |
888 | 0 | zval http_response; |
889 | |
|
890 | 0 | if (tmp_line_len > 9) { |
891 | 0 | response_code = atoi(tmp_line + 9); |
892 | 0 | } else { |
893 | 0 | response_code = 0; |
894 | 0 | } |
895 | 0 | if (context && NULL != (tmpzval = php_stream_context_get_option(context, "http", "ignore_errors"))) { |
896 | 0 | ignore_errors = zend_is_true(tmpzval); |
897 | 0 | } |
898 | | /* when we request only the header, don't fail even on error codes */ |
899 | 0 | if ((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) { |
900 | 0 | reqok = 1; |
901 | 0 | } |
902 | | |
903 | | /* status codes of 1xx are "informational", and will be followed by a real response |
904 | | * e.g "100 Continue". RFC 7231 states that unexpected 1xx status MUST be parsed, |
905 | | * and MAY be ignored. As such, we need to skip ahead to the "real" status*/ |
906 | 0 | if (response_code >= 100 && response_code < 200 && response_code != 101) { |
907 | | /* consume lines until we find a line starting 'HTTP/1' */ |
908 | 0 | while ( |
909 | 0 | !php_stream_eof(stream) |
910 | 0 | && php_stream_get_line(stream, tmp_line, sizeof(tmp_line) - 1, &tmp_line_len) != NULL |
911 | 0 | && ( tmp_line_len < sizeof("HTTP/1") - 1 || strncasecmp(tmp_line, "HTTP/1", sizeof("HTTP/1") - 1) ) |
912 | 0 | ); |
913 | |
|
914 | 0 | if (tmp_line_len > 9) { |
915 | 0 | response_code = atoi(tmp_line + 9); |
916 | 0 | } else { |
917 | 0 | response_code = 0; |
918 | 0 | } |
919 | 0 | } |
920 | | /* all status codes in the 2xx range are defined by the specification as successful; |
921 | | * all status codes in the 3xx range are for redirection, and so also should never |
922 | | * fail */ |
923 | 0 | if (response_code >= 200 && response_code < 400) { |
924 | 0 | reqok = 1; |
925 | 0 | } else { |
926 | 0 | switch(response_code) { |
927 | 0 | case 403: |
928 | 0 | php_stream_notify_error(context, PHP_STREAM_NOTIFY_AUTH_RESULT, |
929 | 0 | tmp_line, response_code); |
930 | 0 | break; |
931 | 0 | default: |
932 | | /* safety net in the event tmp_line == NULL */ |
933 | 0 | if (!tmp_line_len) { |
934 | 0 | tmp_line[0] = '\0'; |
935 | 0 | } |
936 | 0 | php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, |
937 | 0 | tmp_line, response_code); |
938 | 0 | } |
939 | 0 | } |
940 | 0 | if (tmp_line_len >= 1 && tmp_line[tmp_line_len - 1] == '\n') { |
941 | 0 | --tmp_line_len; |
942 | 0 | if (tmp_line_len >= 1 &&tmp_line[tmp_line_len - 1] == '\r') { |
943 | 0 | --tmp_line_len; |
944 | 0 | } |
945 | 0 | } else { |
946 | | // read and discard rest of status line |
947 | 0 | char *line = php_stream_get_line(stream, NULL, 0, NULL); |
948 | 0 | efree(line); |
949 | 0 | } |
950 | 0 | ZVAL_STRINGL(&http_response, tmp_line, tmp_line_len); |
951 | 0 | zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_response); |
952 | 0 | } else { |
953 | 0 | php_stream_close(stream); |
954 | 0 | stream = NULL; |
955 | 0 | php_stream_wrapper_log_error(wrapper, options, "HTTP request failed!"); |
956 | 0 | goto out; |
957 | 0 | } |
958 | 0 | } |
959 | | |
960 | | /* read past HTTP headers */ |
961 | 0 | while (!php_stream_eof(stream)) { |
962 | 0 | size_t http_header_line_length; |
963 | |
|
964 | 0 | if (http_header_line != NULL) { |
965 | 0 | efree(http_header_line); |
966 | 0 | } |
967 | 0 | if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length))) { |
968 | 0 | bool last_line; |
969 | 0 | if (*http_header_line == '\r') { |
970 | 0 | if (http_header_line[1] != '\n') { |
971 | 0 | php_stream_close(stream); |
972 | 0 | stream = NULL; |
973 | 0 | php_stream_wrapper_log_error(wrapper, options, |
974 | 0 | "HTTP invalid header name (cannot start with CR character)!"); |
975 | 0 | goto out; |
976 | 0 | } |
977 | 0 | last_line = true; |
978 | 0 | } else if (*http_header_line == '\n') { |
979 | 0 | last_line = true; |
980 | 0 | } else { |
981 | 0 | last_line = false; |
982 | 0 | } |
983 | | |
984 | 0 | if (last_header_line_str != NULL) { |
985 | | /* Parse last header line. */ |
986 | 0 | last_header_line_str = php_stream_http_response_headers_parse(wrapper, stream, |
987 | 0 | context, options, last_header_line_str, http_header_line, |
988 | 0 | &http_header_line_length, response_code, response_header, &header_info); |
989 | 0 | if (EXPECTED(last_header_line_str == NULL)) { |
990 | 0 | if (UNEXPECTED(header_info.error)) { |
991 | 0 | php_stream_close(stream); |
992 | 0 | stream = NULL; |
993 | 0 | goto out; |
994 | 0 | } |
995 | 0 | } else { |
996 | | /* Folding header present so continue. */ |
997 | 0 | continue; |
998 | 0 | } |
999 | 0 | } else if (!last_line) { |
1000 | | /* The first line cannot start with spaces. */ |
1001 | 0 | if (*http_header_line == ' ' || *http_header_line == '\t') { |
1002 | 0 | php_stream_close(stream); |
1003 | 0 | stream = NULL; |
1004 | 0 | php_stream_wrapper_log_error(wrapper, options, |
1005 | 0 | "HTTP invalid response format (folding header at the start)!"); |
1006 | 0 | goto out; |
1007 | 0 | } |
1008 | | /* Trim the first line if it is not the last line. */ |
1009 | 0 | php_stream_http_response_header_trim(http_header_line, &http_header_line_length); |
1010 | 0 | } |
1011 | 0 | if (last_line) { |
1012 | | /* For the last line the last header line must be NULL. */ |
1013 | 0 | ZEND_ASSERT(last_header_line_str == NULL); |
1014 | 0 | break; |
1015 | 0 | } |
1016 | | /* Save current line as the last line so it gets parsed in the next round. */ |
1017 | 0 | last_header_line_str = zend_string_init(http_header_line, http_header_line_length, 0); |
1018 | 0 | } else { |
1019 | 0 | break; |
1020 | 0 | } |
1021 | 0 | } |
1022 | | |
1023 | | /* If the stream was closed early, we still want to process the last line to keep BC. */ |
1024 | 0 | if (last_header_line_str != NULL) { |
1025 | 0 | php_stream_http_response_headers_parse(wrapper, stream, context, options, |
1026 | 0 | last_header_line_str, NULL, NULL, response_code, response_header, &header_info); |
1027 | 0 | } |
1028 | |
|
1029 | 0 | if (!reqok || (header_info.location != NULL && header_info.follow_location)) { |
1030 | 0 | if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { |
1031 | 0 | goto out; |
1032 | 0 | } |
1033 | | |
1034 | 0 | if (header_info.location != NULL) |
1035 | 0 | php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0); |
1036 | |
|
1037 | 0 | php_stream_close(stream); |
1038 | 0 | stream = NULL; |
1039 | |
|
1040 | 0 | if (header_info.transfer_encoding) { |
1041 | 0 | php_stream_filter_free(header_info.transfer_encoding); |
1042 | 0 | header_info.transfer_encoding = NULL; |
1043 | 0 | } |
1044 | |
|
1045 | 0 | if (header_info.location != NULL) { |
1046 | |
|
1047 | 0 | char *new_path = NULL; |
1048 | |
|
1049 | 0 | if (strlen(header_info.location) < 8 || |
1050 | 0 | (strncasecmp(header_info.location, "http://", sizeof("http://")-1) && |
1051 | 0 | strncasecmp(header_info.location, "https://", sizeof("https://")-1) && |
1052 | 0 | strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && |
1053 | 0 | strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1))) |
1054 | 0 | { |
1055 | 0 | char *loc_path = NULL; |
1056 | 0 | if (*header_info.location != '/') { |
1057 | 0 | if (*(header_info.location+1) != '\0' && resource->path) { |
1058 | 0 | char *s = strrchr(ZSTR_VAL(resource->path), '/'); |
1059 | 0 | if (!s) { |
1060 | 0 | s = ZSTR_VAL(resource->path); |
1061 | 0 | if (!ZSTR_LEN(resource->path)) { |
1062 | 0 | zend_string_release_ex(resource->path, 0); |
1063 | 0 | resource->path = ZSTR_INIT_LITERAL("/", 0); |
1064 | 0 | s = ZSTR_VAL(resource->path); |
1065 | 0 | } else { |
1066 | 0 | *s = '/'; |
1067 | 0 | } |
1068 | 0 | } |
1069 | 0 | s[1] = '\0'; |
1070 | 0 | if (resource->path && |
1071 | 0 | ZSTR_VAL(resource->path)[0] == '/' && |
1072 | 0 | ZSTR_VAL(resource->path)[1] == '\0') { |
1073 | 0 | spprintf(&loc_path, 0, "%s%s", ZSTR_VAL(resource->path), header_info.location); |
1074 | 0 | } else { |
1075 | 0 | spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location); |
1076 | 0 | } |
1077 | 0 | } else { |
1078 | 0 | spprintf(&loc_path, 0, "/%s", header_info.location); |
1079 | 0 | } |
1080 | 0 | } else { |
1081 | 0 | loc_path = header_info.location; |
1082 | 0 | header_info.location = NULL; |
1083 | 0 | } |
1084 | 0 | if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) { |
1085 | 0 | spprintf(&new_path, 0, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), |
1086 | 0 | ZSTR_VAL(resource->host), resource->port, loc_path); |
1087 | 0 | } else { |
1088 | 0 | spprintf(&new_path, 0, "%s://%s%s", ZSTR_VAL(resource->scheme), |
1089 | 0 | ZSTR_VAL(resource->host), loc_path); |
1090 | 0 | } |
1091 | 0 | efree(loc_path); |
1092 | 0 | } else { |
1093 | 0 | new_path = header_info.location; |
1094 | 0 | header_info.location = NULL; |
1095 | 0 | } |
1096 | |
|
1097 | 0 | php_url_free(resource); |
1098 | | /* check for invalid redirection URLs */ |
1099 | 0 | if ((resource = php_url_parse(new_path)) == NULL) { |
1100 | 0 | php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); |
1101 | 0 | efree(new_path); |
1102 | 0 | goto out; |
1103 | 0 | } |
1104 | | |
1105 | 0 | #define CHECK_FOR_CNTRL_CHARS(val) { \ |
1106 | 0 | if (val) { \ |
1107 | 0 | unsigned char *s, *e; \ |
1108 | 0 | ZSTR_LEN(val) = php_url_decode(ZSTR_VAL(val), ZSTR_LEN(val)); \ |
1109 | 0 | s = (unsigned char*)ZSTR_VAL(val); e = s + ZSTR_LEN(val); \ |
1110 | 0 | while (s < e) { \ |
1111 | 0 | if (iscntrl(*s)) { \ |
1112 | 0 | php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); \ |
1113 | 0 | efree(new_path); \ |
1114 | 0 | goto out; \ |
1115 | 0 | } \ |
1116 | 0 | s++; \ |
1117 | 0 | } \ |
1118 | 0 | } \ |
1119 | 0 | } |
1120 | | /* check for control characters in login, password & path */ |
1121 | 0 | if (strncasecmp(new_path, "http://", sizeof("http://") - 1) || strncasecmp(new_path, "https://", sizeof("https://") - 1)) { |
1122 | 0 | CHECK_FOR_CNTRL_CHARS(resource->user); |
1123 | 0 | CHECK_FOR_CNTRL_CHARS(resource->pass); |
1124 | 0 | CHECK_FOR_CNTRL_CHARS(resource->path); |
1125 | 0 | } |
1126 | 0 | int new_flags = HTTP_WRAPPER_REDIRECTED; |
1127 | 0 | if (response_code == 307 || response_code == 308) { |
1128 | | /* RFC 7538 specifies that status code 308 does not allow changing the request method from POST to GET. |
1129 | | * RFC 7231 does the same for status code 307. |
1130 | | * To keep consistency between POST and PATCH requests, we'll also not change the request method from PATCH to GET, even though it's allowed it's not mandated by the RFC. */ |
1131 | 0 | new_flags |= HTTP_WRAPPER_KEEP_METHOD; |
1132 | 0 | } |
1133 | 0 | stream = php_stream_url_wrap_http_ex( |
1134 | 0 | wrapper, new_path, mode, options, opened_path, context, |
1135 | 0 | --redirect_max, new_flags, response_header STREAMS_CC); |
1136 | 0 | efree(new_path); |
1137 | 0 | } else { |
1138 | 0 | php_stream_wrapper_log_error(wrapper, options, "HTTP request failed! %s", tmp_line); |
1139 | 0 | } |
1140 | 0 | } |
1141 | 0 | out: |
1142 | |
|
1143 | 0 | smart_str_free(&req_buf); |
1144 | |
|
1145 | 0 | if (http_header_line) { |
1146 | 0 | efree(http_header_line); |
1147 | 0 | } |
1148 | |
|
1149 | 0 | if (header_info.location != NULL) { |
1150 | 0 | efree(header_info.location); |
1151 | 0 | } |
1152 | |
|
1153 | 0 | if (resource) { |
1154 | 0 | php_url_free(resource); |
1155 | 0 | } |
1156 | |
|
1157 | 0 | if (stream) { |
1158 | 0 | if (header_init) { |
1159 | 0 | ZVAL_COPY(&stream->wrapperdata, response_header); |
1160 | 0 | } |
1161 | 0 | php_stream_notify_progress_init(context, 0, header_info.file_size); |
1162 | | |
1163 | | /* Restore original chunk size now that we're done with headers */ |
1164 | 0 | if (options & STREAM_WILL_CAST) |
1165 | 0 | php_stream_set_chunk_size(stream, (int)chunk_size); |
1166 | | |
1167 | | /* restore the users auto-detect-line-endings setting */ |
1168 | 0 | stream->flags |= eol_detect; |
1169 | | |
1170 | | /* as far as streams are concerned, we are now at the start of |
1171 | | * the stream */ |
1172 | 0 | stream->position = 0; |
1173 | | |
1174 | | /* restore mode */ |
1175 | 0 | strlcpy(stream->mode, mode, sizeof(stream->mode)); |
1176 | |
|
1177 | 0 | if (header_info.transfer_encoding) { |
1178 | 0 | php_stream_filter_append(&stream->readfilters, header_info.transfer_encoding); |
1179 | 0 | } |
1180 | | |
1181 | | /* It's possible that the server already sent in more data than just the headers. |
1182 | | * We account for this by adjusting the progress counter by the difference of |
1183 | | * already read header data and the body. */ |
1184 | 0 | if (stream->writepos > stream->readpos) { |
1185 | 0 | php_stream_notify_progress_increment(context, stream->writepos - stream->readpos, 0); |
1186 | 0 | } |
1187 | 0 | } |
1188 | |
|
1189 | 0 | return stream; |
1190 | 0 | } |
1191 | | /* }}} */ |
1192 | | |
1193 | | php_stream *php_stream_url_wrap_http(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */ |
1194 | 0 | { |
1195 | 0 | php_stream *stream; |
1196 | 0 | zval headers; |
1197 | |
|
1198 | 0 | ZVAL_UNDEF(&headers); |
1199 | |
|
1200 | 0 | zval_ptr_dtor(&BG(last_http_headers)); |
1201 | 0 | ZVAL_UNDEF(&BG(last_http_headers)); |
1202 | |
|
1203 | 0 | stream = php_stream_url_wrap_http_ex( |
1204 | 0 | wrapper, path, mode, options, opened_path, context, |
1205 | 0 | PHP_URL_REDIRECT_MAX, HTTP_WRAPPER_HEADER_INIT, &headers STREAMS_CC); |
1206 | |
|
1207 | 0 | if (!Z_ISUNDEF(headers)) { |
1208 | 0 | ZVAL_COPY(&BG(last_http_headers), &headers); |
1209 | |
|
1210 | 0 | if (FAILURE == zend_set_local_var_str( |
1211 | 0 | "http_response_header", sizeof("http_response_header")-1, &headers, 0)) { |
1212 | 0 | zval_ptr_dtor(&headers); |
1213 | 0 | } |
1214 | 0 | } |
1215 | |
|
1216 | 0 | return stream; |
1217 | 0 | } |
1218 | | /* }}} */ |
1219 | | |
1220 | | static int php_stream_http_stream_stat(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb) /* {{{ */ |
1221 | 0 | { |
1222 | | /* one day, we could fill in the details based on Date: and Content-Length: |
1223 | | * headers. For now, we return with a failure code to prevent the underlying |
1224 | | * file's details from being used instead. */ |
1225 | 0 | return -1; |
1226 | 0 | } |
1227 | | /* }}} */ |
1228 | | |
1229 | | static const php_stream_wrapper_ops http_stream_wops = { |
1230 | | php_stream_url_wrap_http, |
1231 | | NULL, /* stream_close */ |
1232 | | php_stream_http_stream_stat, |
1233 | | NULL, /* stat_url */ |
1234 | | NULL, /* opendir */ |
1235 | | "http", |
1236 | | NULL, /* unlink */ |
1237 | | NULL, /* rename */ |
1238 | | NULL, /* mkdir */ |
1239 | | NULL, /* rmdir */ |
1240 | | NULL |
1241 | | }; |
1242 | | |
1243 | | PHPAPI const php_stream_wrapper php_stream_http_wrapper = { |
1244 | | &http_stream_wops, |
1245 | | NULL, |
1246 | | 1 /* is_url */ |
1247 | | }; |