Coverage Report

Created: 2026-10-28 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
};