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