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