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