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