/src/php-src/ext/standard/php_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 | | +----------------------------------------------------------------------+ |
15 | | */ |
16 | | |
17 | | #include <stdio.h> |
18 | | #include <stdlib.h> |
19 | | #ifdef HAVE_UNISTD_H |
20 | | # include <unistd.h> |
21 | | #endif |
22 | | |
23 | | #include "php.h" |
24 | | #include "php_globals.h" |
25 | | #include "php_standard.h" |
26 | | #include "php_memory_streams.h" |
27 | | #include "php_fopen_wrappers.h" |
28 | | #include "SAPI.h" |
29 | | |
30 | | static ssize_t php_stream_output_write(php_stream *stream, const char *buf, size_t count) /* {{{ */ |
31 | 0 | { |
32 | 0 | PHPWRITE(buf, count); |
33 | 0 | return count; |
34 | 0 | } |
35 | | /* }}} */ |
36 | | |
37 | | static ssize_t php_stream_output_read(php_stream *stream, char *buf, size_t count) /* {{{ */ |
38 | 0 | { |
39 | 0 | stream->eof = 1; |
40 | 0 | return -1; |
41 | 0 | } |
42 | | /* }}} */ |
43 | | |
44 | | static int php_stream_output_close(php_stream *stream, int close_handle) /* {{{ */ |
45 | 0 | { |
46 | 0 | return 0; |
47 | 0 | } |
48 | | /* }}} */ |
49 | | |
50 | | static const php_stream_ops php_stream_output_ops = { |
51 | | php_stream_output_write, |
52 | | php_stream_output_read, |
53 | | php_stream_output_close, |
54 | | NULL, /* flush */ |
55 | | "Output", |
56 | | NULL, /* seek */ |
57 | | NULL, /* cast */ |
58 | | NULL, /* stat */ |
59 | | NULL /* set_option */ |
60 | | }; |
61 | | |
62 | | typedef struct php_stream_input { /* {{{ */ |
63 | | php_stream *body; |
64 | | zend_off_t position; |
65 | | } php_stream_input_t; |
66 | | /* }}} */ |
67 | | |
68 | | static ssize_t php_stream_input_write(php_stream *stream, const char *buf, size_t count) /* {{{ */ |
69 | 0 | { |
70 | 0 | return -1; |
71 | 0 | } |
72 | | /* }}} */ |
73 | | |
74 | | static ssize_t php_stream_input_read(php_stream *stream, char *buf, size_t count) /* {{{ */ |
75 | 0 | { |
76 | 0 | php_stream_input_t *input = stream->abstract; |
77 | 0 | ssize_t read; |
78 | |
|
79 | 0 | if (!SG(post_read) && SG(read_post_bytes) < (int64_t)(input->position + count)) { |
80 | | /* read requested data from SAPI */ |
81 | 0 | size_t read_bytes = sapi_read_post_block(buf, count); |
82 | |
|
83 | 0 | if (read_bytes > 0) { |
84 | 0 | php_stream_seek(input->body, 0, SEEK_END); |
85 | 0 | php_stream_write(input->body, buf, read_bytes); |
86 | 0 | } |
87 | 0 | } |
88 | |
|
89 | 0 | if (!input->body->readfilters.head) { |
90 | | /* If the input stream contains filters, it's not really seekable. The |
91 | | input->position is likely to be wrong for unfiltered data. */ |
92 | 0 | php_stream_seek(input->body, input->position, SEEK_SET); |
93 | 0 | } |
94 | 0 | read = php_stream_read(input->body, buf, count); |
95 | |
|
96 | 0 | if (!read || read == (size_t) -1) { |
97 | 0 | stream->eof = 1; |
98 | 0 | } else { |
99 | 0 | input->position += read; |
100 | 0 | } |
101 | |
|
102 | 0 | return read; |
103 | 0 | } |
104 | | /* }}} */ |
105 | | |
106 | | static int php_stream_input_close(php_stream *stream, int close_handle) /* {{{ */ |
107 | 0 | { |
108 | 0 | efree(stream->abstract); |
109 | 0 | stream->abstract = NULL; |
110 | |
|
111 | 0 | return 0; |
112 | 0 | } |
113 | | /* }}} */ |
114 | | |
115 | | static int php_stream_input_flush(php_stream *stream) /* {{{ */ |
116 | 0 | { |
117 | 0 | return -1; |
118 | 0 | } |
119 | | /* }}} */ |
120 | | |
121 | | static int php_stream_input_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset) /* {{{ */ |
122 | 0 | { |
123 | 0 | php_stream_input_t *input = stream->abstract; |
124 | |
|
125 | 0 | if (input->body) { |
126 | 0 | int sought = php_stream_seek(input->body, offset, whence); |
127 | 0 | *newoffset = input->position = (input->body)->position; |
128 | 0 | return sought; |
129 | 0 | } |
130 | | |
131 | 0 | return -1; |
132 | 0 | } |
133 | | /* }}} */ |
134 | | |
135 | | static const php_stream_ops php_stream_input_ops = { |
136 | | php_stream_input_write, |
137 | | php_stream_input_read, |
138 | | php_stream_input_close, |
139 | | php_stream_input_flush, |
140 | | "Input", |
141 | | php_stream_input_seek, |
142 | | NULL, /* cast */ |
143 | | NULL, /* stat */ |
144 | | NULL /* set_option */ |
145 | | }; |
146 | | |
147 | | static const zend_long max_filter_count_default = 16; |
148 | | |
149 | | static zend_result php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain, php_stream_context *context) /* {{{ */ |
150 | 171 | { |
151 | 171 | char *p, *token = NULL; |
152 | 171 | php_stream_filter *temp_filter; |
153 | | |
154 | 171 | zend_long max_filter_count = max_filter_count_default; |
155 | 171 | bool max_filter_count_configured = false; |
156 | 171 | if (context != NULL) { |
157 | 0 | zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count"); |
158 | 0 | if (option_val) { |
159 | 0 | max_filter_count = zval_get_long(option_val); |
160 | 0 | max_filter_count_configured = true; |
161 | 0 | } |
162 | 0 | } |
163 | | |
164 | 171 | p = php_strtok_r(filterlist, "|", &token); |
165 | 351 | while (p) { |
166 | 180 | zend_long read_count = read_chain ? stream->readfilters.num_filters : 0; |
167 | 180 | zend_long write_count = write_chain ? stream->writefilters.num_filters : 0; |
168 | | |
169 | 180 | if (read_count == max_filter_count || write_count == max_filter_count) { |
170 | 0 | if (max_filter_count_configured) { |
171 | 0 | return FAILURE; |
172 | 0 | } else { |
173 | | // No max_filter_count configured; raise deprecation error if over default |
174 | 0 | zend_error(E_DEPRECATED, "Using more than " ZEND_LONG_FMT " filters in a php://filter URL is deprecated, " |
175 | 0 | "set this limit using the stream context option max_filter_count, or use stream_filter_append", max_filter_count_default); |
176 | 0 | } |
177 | 0 | } |
178 | | |
179 | 180 | php_url_decode(p, strlen(p)); |
180 | 180 | if (read_chain) { |
181 | 180 | if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) { |
182 | 68 | php_stream_filter_append(&stream->readfilters, temp_filter); |
183 | 112 | } else { |
184 | 112 | php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS, |
185 | 112 | CreateFailed, |
186 | 112 | "Unable to create filter (%s)", p); |
187 | 112 | } |
188 | 180 | } |
189 | 180 | if (write_chain) { |
190 | 0 | if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) { |
191 | 0 | php_stream_filter_append(&stream->writefilters, temp_filter); |
192 | 0 | } else { |
193 | 0 | php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS, |
194 | 0 | CreateFailed, |
195 | 0 | "Unable to create filter (%s)", p); |
196 | 0 | } |
197 | 0 | } |
198 | 180 | p = php_strtok_r(NULL, "|", &token); |
199 | 180 | } |
200 | 171 | return SUCCESS; |
201 | 171 | } |
202 | | /* }}} */ |
203 | | |
204 | | static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, |
205 | | zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */ |
206 | 80 | { |
207 | 80 | int fd = -1; |
208 | 80 | int mode_rw = 0; |
209 | 80 | php_stream * stream = NULL; |
210 | 80 | char *p, *token = NULL, *pathdup; |
211 | 80 | zend_long max_memory; |
212 | 80 | FILE *file = NULL; |
213 | | #ifdef PHP_WIN32 |
214 | | int pipe_requested = 0; |
215 | | #endif |
216 | | |
217 | 80 | if (!strncasecmp(path, "php://", 6)) { |
218 | 80 | path += 6; |
219 | 80 | } |
220 | | |
221 | 80 | if (!strncasecmp(path, "temp", 4)) { |
222 | 0 | path += 4; |
223 | 0 | max_memory = PHP_STREAM_MAX_MEM; |
224 | 0 | if (!strncasecmp(path, "/maxmemory:", 11)) { |
225 | 0 | path += 11; |
226 | 0 | max_memory = ZEND_STRTOL(path, NULL, 10); |
227 | 0 | if (max_memory < 0) { |
228 | 0 | zend_argument_value_error(2, "must be greater than or equal to 0"); |
229 | 0 | return NULL; |
230 | 0 | } |
231 | 0 | } |
232 | 0 | mode_rw = php_stream_mode_from_str(mode); |
233 | 0 | return php_stream_temp_create(mode_rw, max_memory); |
234 | 0 | } |
235 | | |
236 | 80 | if (!strcasecmp(path, "memory")) { |
237 | 0 | mode_rw = php_stream_mode_from_str(mode); |
238 | 0 | return php_stream_memory_create(mode_rw); |
239 | 0 | } |
240 | | |
241 | 80 | if (!strcasecmp(path, "output")) { |
242 | 0 | return php_stream_alloc(&php_stream_output_ops, NULL, 0, "wb"); |
243 | 0 | } |
244 | | |
245 | 80 | if (!strcasecmp(path, "input")) { |
246 | 0 | php_stream_input_t *input; |
247 | |
|
248 | 0 | if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { |
249 | 0 | php_stream_wrapper_warn(wrapper, context, options, |
250 | 0 | Disabled, |
251 | 0 | "URL file-access is disabled in the server configuration"); |
252 | 0 | return NULL; |
253 | 0 | } |
254 | | |
255 | 0 | input = ecalloc(1, sizeof(*input)); |
256 | 0 | if ((input->body = SG(request_info).request_body)) { |
257 | 0 | php_stream_rewind(input->body); |
258 | 0 | } else { |
259 | 0 | input->body = php_stream_temp_create_ex(TEMP_STREAM_DEFAULT, SAPI_POST_BLOCK_SIZE, PG(upload_tmp_dir)); |
260 | 0 | SG(request_info).request_body = input->body; |
261 | 0 | } |
262 | |
|
263 | 0 | return php_stream_alloc(&php_stream_input_ops, input, 0, "rb"); |
264 | 0 | } |
265 | | |
266 | 80 | if (!strcasecmp(path, "stdin")) { |
267 | 0 | if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { |
268 | 0 | php_stream_wrapper_warn(wrapper, context, options, |
269 | 0 | Disabled, |
270 | 0 | "URL file-access is disabled in the server configuration"); |
271 | 0 | return NULL; |
272 | 0 | } |
273 | 0 | if (!strcmp(sapi_module.name, "cli")) { |
274 | 0 | static int cli_in = 0; |
275 | 0 | fd = STDIN_FILENO; |
276 | 0 | if (cli_in) { |
277 | 0 | fd = dup(fd); |
278 | 0 | } else { |
279 | 0 | cli_in = 1; |
280 | 0 | file = stdin; |
281 | 0 | } |
282 | 0 | } else { |
283 | 0 | fd = dup(STDIN_FILENO); |
284 | 0 | } |
285 | | #ifdef PHP_WIN32 |
286 | | pipe_requested = 1; |
287 | | #endif |
288 | 80 | } else if (!strcasecmp(path, "stdout")) { |
289 | 0 | if (!strcmp(sapi_module.name, "cli")) { |
290 | 0 | static int cli_out = 0; |
291 | 0 | fd = STDOUT_FILENO; |
292 | 0 | if (cli_out++) { |
293 | 0 | fd = dup(fd); |
294 | 0 | } else { |
295 | 0 | cli_out = 1; |
296 | 0 | file = stdout; |
297 | 0 | } |
298 | 0 | } else { |
299 | 0 | fd = dup(STDOUT_FILENO); |
300 | 0 | } |
301 | | #ifdef PHP_WIN32 |
302 | | pipe_requested = 1; |
303 | | #endif |
304 | 80 | } else if (!strcasecmp(path, "stderr")) { |
305 | 0 | if (!strcmp(sapi_module.name, "cli")) { |
306 | 0 | static int cli_err = 0; |
307 | 0 | fd = STDERR_FILENO; |
308 | 0 | if (cli_err++) { |
309 | 0 | fd = dup(fd); |
310 | 0 | } else { |
311 | 0 | cli_err = 1; |
312 | 0 | file = stderr; |
313 | 0 | } |
314 | 0 | } else { |
315 | 0 | fd = dup(STDERR_FILENO); |
316 | 0 | } |
317 | | #ifdef PHP_WIN32 |
318 | | pipe_requested = 1; |
319 | | #endif |
320 | 80 | } else if (!strncasecmp(path, "fd/", 3)) { |
321 | 0 | const char *start; |
322 | 0 | char *end; |
323 | 0 | zend_long fildes_ori; |
324 | 0 | int dtablesize; |
325 | |
|
326 | 0 | if (strcmp(sapi_module.name, "cli")) { |
327 | 0 | php_stream_wrapper_warn(wrapper, context, options, |
328 | 0 | Disabled, |
329 | 0 | "Direct access to file descriptors is only available from command-line PHP"); |
330 | 0 | return NULL; |
331 | 0 | } |
332 | | |
333 | 0 | if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { |
334 | 0 | php_stream_wrapper_warn(wrapper, context, options, |
335 | 0 | Disabled, |
336 | 0 | "URL file-access is disabled in the server configuration"); |
337 | 0 | return NULL; |
338 | 0 | } |
339 | | |
340 | 0 | start = &path[3]; |
341 | 0 | fildes_ori = ZEND_STRTOL(start, &end, 10); |
342 | 0 | if (end == start || *end != '\0') { |
343 | 0 | php_stream_wrapper_log_warn(wrapper, context, options, |
344 | 0 | InvalidUrl, |
345 | 0 | "php://fd/ stream must be specified in the form php://fd/<orig fd>"); |
346 | 0 | return NULL; |
347 | 0 | } |
348 | | |
349 | 0 | #ifdef HAVE_UNISTD_H |
350 | 0 | dtablesize = getdtablesize(); |
351 | | #else |
352 | | dtablesize = INT_MAX; |
353 | | #endif |
354 | |
|
355 | 0 | if (fildes_ori < 0 || fildes_ori >= dtablesize) { |
356 | 0 | php_stream_wrapper_log_warn(wrapper, context, options, |
357 | 0 | InvalidParam, |
358 | 0 | "The file descriptors must be non-negative numbers smaller than %d", dtablesize); |
359 | 0 | return NULL; |
360 | 0 | } |
361 | | |
362 | 0 | fd = dup((int)fildes_ori); |
363 | 0 | if (fd == -1) { |
364 | 0 | php_stream_wrapper_log_warn(wrapper, context, options, |
365 | 0 | DupFailed, |
366 | 0 | "Error duping file descriptor " ZEND_LONG_FMT "; possibly it doesn't exist: " |
367 | 0 | "[%d]: %s", fildes_ori, errno, strerror(errno)); |
368 | 0 | return NULL; |
369 | 0 | } |
370 | 80 | } else if (!strncasecmp(path, "filter/", 7)) { |
371 | | /* Save time/memory when chain isn't specified */ |
372 | 71 | if (strchr(mode, 'r') || strchr(mode, '+')) { |
373 | 71 | mode_rw |= PHP_STREAM_FILTER_READ; |
374 | 71 | } |
375 | 71 | if (strchr(mode, 'w') || strchr(mode, '+') || strchr(mode, 'a')) { |
376 | 0 | mode_rw |= PHP_STREAM_FILTER_WRITE; |
377 | 0 | } |
378 | 71 | pathdup = estrndup(path + 6, strlen(path + 6)); |
379 | 71 | p = strstr(pathdup, "/resource="); |
380 | 71 | if (!p) { |
381 | 4 | zend_throw_error(NULL, "No URL resource specified"); |
382 | 4 | efree(pathdup); |
383 | 4 | return NULL; |
384 | 4 | } |
385 | | |
386 | 67 | if (!(stream = php_stream_open_wrapper_ex(p + 10, mode, options, opened_path, context))) { |
387 | 7 | efree(pathdup); |
388 | 7 | return NULL; |
389 | 7 | } |
390 | | |
391 | 60 | zend_result safl_result = SUCCESS; |
392 | 60 | *p = '\0'; |
393 | | |
394 | 60 | p = php_strtok_r(pathdup + 1, "/", &token); |
395 | 231 | while (p) { |
396 | 171 | if (!strncasecmp(p, "read=", 5)) { |
397 | 82 | safl_result = php_stream_apply_filter_list(stream, p + 5, 1, 0, context); |
398 | 89 | } else if (!strncasecmp(p, "write=", 6)) { |
399 | 0 | safl_result = php_stream_apply_filter_list(stream, p + 6, 0, 1, context); |
400 | 89 | } else { |
401 | 89 | safl_result = php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, context); |
402 | 89 | } |
403 | | |
404 | 171 | if (safl_result == FAILURE) { |
405 | 0 | break; |
406 | 0 | } |
407 | | |
408 | 171 | p = php_strtok_r(NULL, "/", &token); |
409 | 171 | } |
410 | 60 | efree(pathdup); |
411 | | |
412 | 60 | if (EG(exception)) { |
413 | 0 | php_stream_close(stream); |
414 | 0 | return NULL; |
415 | 0 | } |
416 | | |
417 | 60 | if (safl_result == FAILURE) { |
418 | 0 | php_stream_wrapper_log_warn(wrapper, context, options, |
419 | 0 | PathTooLong, "too many filters"); |
420 | 0 | php_stream_close(stream); |
421 | 0 | return NULL; |
422 | 0 | } |
423 | | |
424 | 60 | return stream; |
425 | 60 | } else { |
426 | | /* invalid php://thingy */ |
427 | 9 | php_stream_wrapper_warn(wrapper, context, options, |
428 | 9 | InvalidUrl, "Invalid php:// URL specified"); |
429 | 9 | return NULL; |
430 | 9 | } |
431 | | |
432 | | /* must be stdin, stderr or stdout */ |
433 | 0 | if (fd == -1) { |
434 | | /* failed to dup */ |
435 | 0 | return NULL; |
436 | 0 | } |
437 | | |
438 | 0 | #if defined(S_IFSOCK) && !defined(PHP_WIN32) |
439 | 0 | do { |
440 | 0 | zend_stat_t st = {0}; |
441 | 0 | memset(&st, 0, sizeof(st)); |
442 | 0 | if (zend_fstat(fd, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) { |
443 | 0 | stream = php_stream_sock_open_from_socket(fd, NULL); |
444 | 0 | if (stream) { |
445 | 0 | stream->ops = &php_stream_socket_ops; |
446 | 0 | return stream; |
447 | 0 | } |
448 | 0 | } |
449 | 0 | } while (0); |
450 | 0 | #endif |
451 | | |
452 | 0 | if (file) { |
453 | 0 | stream = php_stream_fopen_from_file(file, mode); |
454 | 0 | } else { |
455 | 0 | stream = php_stream_fopen_from_fd(fd, mode, NULL); |
456 | 0 | if (stream == NULL) { |
457 | 0 | close(fd); |
458 | 0 | } |
459 | 0 | } |
460 | |
|
461 | | #ifdef PHP_WIN32 |
462 | | if (pipe_requested && stream && context) { |
463 | | zval *blocking_pipes = php_stream_context_get_option(context, "pipe", "blocking"); |
464 | | if (blocking_pipes) { |
465 | | php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, zval_get_long(blocking_pipes), NULL); |
466 | | } |
467 | | } |
468 | | #endif |
469 | 0 | return stream; |
470 | 0 | } |
471 | | /* }}} */ |
472 | | |
473 | | static const php_stream_wrapper_ops php_stdio_wops = { |
474 | | php_stream_url_wrap_php, |
475 | | NULL, /* close */ |
476 | | NULL, /* fstat */ |
477 | | NULL, /* stat */ |
478 | | NULL, /* opendir */ |
479 | | "PHP", |
480 | | NULL, /* unlink */ |
481 | | NULL, /* rename */ |
482 | | NULL, /* mkdir */ |
483 | | NULL, /* rmdir */ |
484 | | NULL |
485 | | }; |
486 | | |
487 | | PHPAPI const php_stream_wrapper php_stream_php_wrapper = { |
488 | | &php_stdio_wops, |
489 | | NULL, |
490 | | 0, /* is_url */ |
491 | | }; |