1 /*
2 +----------------------------------------------------------------------+
3 | Copyright (c) The PHP Group |
4 +----------------------------------------------------------------------+
5 | This source file is subject to version 3.01 of the PHP license, |
6 | that is bundled with this package in the file LICENSE, and is |
7 | available through the world-wide-web at the following url: |
8 | https://www.php.net/license/3_01.txt |
9 | If you did not receive a copy of the PHP license and are unable to |
10 | obtain it through the world-wide-web, please send a note to |
11 | license@php.net so we can mail you a copy immediately. |
12 +----------------------------------------------------------------------+
13 | Authors: Rasmus Lerdorf <rasmus@php.net> |
14 | Jim Winstead <jimw@php.net> |
15 | Hartmut Holzgraefe <hholzgra@php.net> |
16 | Sara Golemon <pollita@php.net> |
17 +----------------------------------------------------------------------+
18 */
19
20 #include "php.h"
21 #include "php_globals.h"
22 #include "php_network.h"
23 #include "php_ini.h"
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <errno.h>
28 #include <sys/types.h>
29 #include <sys/stat.h>
30 #include <fcntl.h>
31
32 #ifdef PHP_WIN32
33 #include <winsock2.h>
34 #define O_RDONLY _O_RDONLY
35 #include "win32/param.h"
36 #else
37 #include <sys/param.h>
38 #endif
39
40 #include "php_standard.h"
41
42 #include <sys/types.h>
43 #ifdef HAVE_SYS_SOCKET_H
44 #include <sys/socket.h>
45 #endif
46
47 #ifdef PHP_WIN32
48 #include <winsock2.h>
49 #else
50 #include <netinet/in.h>
51 #include <netdb.h>
52 #ifdef HAVE_ARPA_INET_H
53 #include <arpa/inet.h>
54 #endif
55 #endif
56
57 #if defined(PHP_WIN32) || defined(__riscos__)
58 #undef AF_UNIX
59 #endif
60
61 #if defined(AF_UNIX)
62 #include <sys/un.h>
63 #endif
64
65 #include "php_fopen_wrappers.h"
66
67 #define FTPS_ENCRYPT_DATA 1
68 #define GET_FTP_RESULT(stream) get_ftp_result((stream), tmp_line, sizeof(tmp_line))
69
70 typedef struct _php_ftp_dirstream_data {
71 php_stream *datastream;
72 php_stream *controlstream;
73 php_stream *dirstream;
74 } php_ftp_dirstream_data;
75
76 /* {{{ get_ftp_result */
get_ftp_result(php_stream * stream,char * buffer,size_t buffer_size)77 static inline int get_ftp_result(php_stream *stream, char *buffer, size_t buffer_size)
78 {
79 buffer[0] = '\0'; /* in case read fails to read anything */
80 while (php_stream_gets(stream, buffer, buffer_size-1) &&
81 !(isdigit((int) buffer[0]) && isdigit((int) buffer[1]) &&
82 isdigit((int) buffer[2]) && buffer[3] == ' '));
83 return strtol(buffer, NULL, 10);
84 }
85 /* }}} */
86
87 /* {{{ php_stream_ftp_stream_stat */
php_stream_ftp_stream_stat(php_stream_wrapper * wrapper,php_stream * stream,php_stream_statbuf * ssb)88 static int php_stream_ftp_stream_stat(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb)
89 {
90 /* For now, we return with a failure code to prevent the underlying
91 * file's details from being used instead. */
92 return -1;
93 }
94 /* }}} */
95
96 /* {{{ php_stream_ftp_stream_close */
php_stream_ftp_stream_close(php_stream_wrapper * wrapper,php_stream * stream)97 static int php_stream_ftp_stream_close(php_stream_wrapper *wrapper, php_stream *stream)
98 {
99 php_stream *controlstream = stream->wrapperthis;
100 int ret = 0;
101
102 if (controlstream) {
103 if (strpbrk(stream->mode, "wa+")) {
104 char tmp_line[512];
105 int result;
106
107 /* For write modes close data stream first to signal EOF to server */
108 result = GET_FTP_RESULT(controlstream);
109 if (result != 226 && result != 250) {
110 php_error_docref(NULL, E_WARNING, "FTP server error %d:%s", result, tmp_line);
111 ret = EOF;
112 }
113 }
114
115 php_stream_write_string(controlstream, "QUIT\r\n");
116 php_stream_close(controlstream);
117 stream->wrapperthis = NULL;
118 }
119
120 return ret;
121 }
122 /* }}} */
123
124 /* {{{ php_ftp_fopen_connect */
php_ftp_fopen_connect(php_stream_wrapper * wrapper,const char * path,const char * mode,int options,zend_string ** opened_path,php_stream_context * context,php_stream ** preuseid,php_url ** presource,int * puse_ssl,int * puse_ssl_on_data)125 static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
126 zend_string **opened_path, php_stream_context *context, php_stream **preuseid,
127 php_url **presource, int *puse_ssl, int *puse_ssl_on_data)
128 {
129 php_stream *stream = NULL, *reuseid = NULL;
130 php_url *resource = NULL;
131 int result, use_ssl, use_ssl_on_data = 0;
132 char tmp_line[512];
133 char *transport;
134 int transport_len;
135
136 resource = php_url_parse(path);
137 if (resource == NULL || resource->path == NULL) {
138 if (resource && presource) {
139 *presource = resource;
140 }
141 return NULL;
142 }
143
144 use_ssl = resource->scheme && (ZSTR_LEN(resource->scheme) > 3) && ZSTR_VAL(resource->scheme)[3] == 's';
145
146 /* use port 21 if one wasn't specified */
147 if (resource->port == 0)
148 resource->port = 21;
149
150 transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", ZSTR_VAL(resource->host), resource->port);
151 stream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, NULL, NULL);
152 efree(transport);
153 if (stream == NULL) {
154 result = 0; /* silence */
155 goto connect_errexit;
156 }
157
158 php_stream_context_set(stream, context);
159 php_stream_notify_info(context, PHP_STREAM_NOTIFY_CONNECT, NULL, 0);
160
161 /* Start talking to ftp server */
162 result = GET_FTP_RESULT(stream);
163 if (result > 299 || result < 200) {
164 php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
165 goto connect_errexit;
166 }
167
168 if (use_ssl) {
169
170 /* send the AUTH TLS request name */
171 php_stream_write_string(stream, "AUTH TLS\r\n");
172
173 /* get the response */
174 result = GET_FTP_RESULT(stream);
175 if (result != 234) {
176 /* AUTH TLS not supported try AUTH SSL */
177 php_stream_write_string(stream, "AUTH SSL\r\n");
178
179 /* get the response */
180 result = GET_FTP_RESULT(stream);
181 if (result != 334) {
182 php_stream_wrapper_log_error(wrapper, options, "Server doesn't support FTPS.");
183 goto connect_errexit;
184 } else {
185 /* we must reuse the old SSL session id */
186 /* if we talk to an old ftpd-ssl */
187 reuseid = stream;
188 }
189 } else {
190 /* encrypt data etc */
191
192
193 }
194
195 }
196
197 if (use_ssl) {
198 if (php_stream_xport_crypto_setup(stream,
199 STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0
200 || php_stream_xport_crypto_enable(stream, 1) < 0) {
201 php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
202 php_stream_close(stream);
203 stream = NULL;
204 goto connect_errexit;
205 }
206
207 /* set PBSZ to 0 */
208 php_stream_write_string(stream, "PBSZ 0\r\n");
209
210 /* ignore the response */
211 result = GET_FTP_RESULT(stream);
212
213 /* set data connection protection level */
214 #if FTPS_ENCRYPT_DATA
215 php_stream_write_string(stream, "PROT P\r\n");
216
217 /* get the response */
218 result = GET_FTP_RESULT(stream);
219 use_ssl_on_data = (result >= 200 && result<=299) || reuseid;
220 #else
221 php_stream_write_string(stream, "PROT C\r\n");
222
223 /* get the response */
224 result = GET_FTP_RESULT(stream);
225 #endif
226 }
227
228 #define PHP_FTP_CNTRL_CHK(val, val_len, err_msg) { \
229 unsigned char *s = (unsigned char *) val, *e = (unsigned char *) s + val_len; \
230 while (s < e) { \
231 if (iscntrl(*s)) { \
232 php_stream_wrapper_log_error(wrapper, options, err_msg, val); \
233 goto connect_errexit; \
234 } \
235 s++; \
236 } \
237 }
238
239 /* send the user name */
240 if (resource->user != NULL) {
241 ZSTR_LEN(resource->user) = php_raw_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user));
242
243 PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user), "Invalid login %s")
244
245 php_stream_printf(stream, "USER %s\r\n", ZSTR_VAL(resource->user));
246 } else {
247 php_stream_write_string(stream, "USER anonymous\r\n");
248 }
249
250 /* get the response */
251 result = GET_FTP_RESULT(stream);
252
253 /* if a password is required, send it */
254 if (result >= 300 && result <= 399) {
255 php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, tmp_line, 0);
256
257 if (resource->pass != NULL) {
258 ZSTR_LEN(resource->pass) = php_raw_url_decode(ZSTR_VAL(resource->pass), ZSTR_LEN(resource->pass));
259
260 PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->pass), ZSTR_LEN(resource->pass), "Invalid password %s")
261
262 php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(resource->pass));
263 } else {
264 /* if the user has configured who they are,
265 send that as the password */
266 if (FG(from_address)) {
267 php_stream_printf(stream, "PASS %s\r\n", FG(from_address));
268 } else {
269 php_stream_write_string(stream, "PASS anonymous\r\n");
270 }
271 }
272
273 /* read the response */
274 result = GET_FTP_RESULT(stream);
275
276 if (result > 299 || result < 200) {
277 php_stream_notify_error(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
278 } else {
279 php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
280 }
281 }
282 if (result > 299 || result < 200) {
283 goto connect_errexit;
284 }
285
286 if (puse_ssl) {
287 *puse_ssl = use_ssl;
288 }
289 if (puse_ssl_on_data) {
290 *puse_ssl_on_data = use_ssl_on_data;
291 }
292 if (preuseid) {
293 *preuseid = reuseid;
294 }
295 if (presource) {
296 *presource = resource;
297 }
298
299 return stream;
300
301 connect_errexit:
302 if (resource) {
303 php_url_free(resource);
304 }
305
306 if (stream) {
307 php_stream_close(stream);
308 }
309
310 return NULL;
311 }
312 /* }}} */
313
314 /* {{{ php_fopen_do_pasv */
php_fopen_do_pasv(php_stream * stream,char * ip,size_t ip_size,char ** phoststart)315 static unsigned short php_fopen_do_pasv(php_stream *stream, char *ip, size_t ip_size, char **phoststart)
316 {
317 char tmp_line[512];
318 int result, i;
319 unsigned short portno;
320 char *tpath, *ttpath, *hoststart=NULL;
321
322 #ifdef HAVE_IPV6
323 /* We try EPSV first, needed for IPv6 and works on some IPv4 servers */
324 php_stream_write_string(stream, "EPSV\r\n");
325 result = GET_FTP_RESULT(stream);
326
327 /* check if we got a 229 response */
328 if (result != 229) {
329 #endif
330 /* EPSV failed, let's try PASV */
331 php_stream_write_string(stream, "PASV\r\n");
332 result = GET_FTP_RESULT(stream);
333
334 /* make sure we got a 227 response */
335 if (result != 227) {
336 return 0;
337 }
338
339 /* parse pasv command (129, 80, 95, 25, 13, 221) */
340 tpath = tmp_line;
341 /* skip over the "227 Some message " part */
342 for (tpath += 4; *tpath && !isdigit((int) *tpath); tpath++);
343 if (!*tpath) {
344 return 0;
345 }
346 /* skip over the host ip, to get the port */
347 hoststart = tpath;
348 for (i = 0; i < 4; i++) {
349 for (; isdigit((int) *tpath); tpath++);
350 if (*tpath != ',') {
351 return 0;
352 }
353 *tpath='.';
354 tpath++;
355 }
356 tpath[-1] = '\0';
357 memcpy(ip, hoststart, ip_size);
358 ip[ip_size-1] = '\0';
359 hoststart = ip;
360
361 /* pull out the MSB of the port */
362 portno = (unsigned short) strtoul(tpath, &ttpath, 10) * 256;
363 if (ttpath == NULL) {
364 /* didn't get correct response from PASV */
365 return 0;
366 }
367 tpath = ttpath;
368 if (*tpath != ',') {
369 return 0;
370 }
371 tpath++;
372 /* pull out the LSB of the port */
373 portno += (unsigned short) strtoul(tpath, &ttpath, 10);
374 #ifdef HAVE_IPV6
375 } else {
376 /* parse epsv command (|||6446|) */
377 for (i = 0, tpath = tmp_line + 4; *tpath; tpath++) {
378 if (*tpath == '|') {
379 i++;
380 if (i == 3)
381 break;
382 }
383 }
384 if (i < 3) {
385 return 0;
386 }
387 /* pull out the port */
388 portno = (unsigned short) strtoul(tpath + 1, &ttpath, 10);
389 }
390 #endif
391 if (ttpath == NULL) {
392 /* didn't get correct response from EPSV/PASV */
393 return 0;
394 }
395
396 if (phoststart) {
397 *phoststart = hoststart;
398 }
399
400 return portno;
401 }
402 /* }}} */
403
404 /* {{{ php_fopen_url_wrap_ftp */
php_stream_url_wrap_ftp(php_stream_wrapper * wrapper,const char * path,const char * mode,int options,zend_string ** opened_path,php_stream_context * context STREAMS_DC)405 php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, const char *path, const char *mode,
406 int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
407 {
408 php_stream *stream = NULL, *datastream = NULL;
409 php_url *resource = NULL;
410 char tmp_line[512];
411 char ip[sizeof("123.123.123.123")];
412 unsigned short portno;
413 char *hoststart = NULL;
414 int result = 0, use_ssl, use_ssl_on_data=0;
415 php_stream *reuseid=NULL;
416 size_t file_size = 0;
417 zval *tmpzval;
418 bool allow_overwrite = 0;
419 int8_t read_write = 0;
420 char *transport;
421 int transport_len;
422 zend_string *error_message = NULL;
423
424 tmp_line[0] = '\0';
425
426 if (strpbrk(mode, "r+")) {
427 read_write = 1; /* Open for reading */
428 }
429 if (strpbrk(mode, "wa+")) {
430 if (read_write) {
431 php_stream_wrapper_log_error(wrapper, options, "FTP does not support simultaneous read/write connections");
432 return NULL;
433 }
434 if (strchr(mode, 'a')) {
435 read_write = 3; /* Open for Appending */
436 } else {
437 read_write = 2; /* Open for writing */
438 }
439 }
440 if (!read_write) {
441 /* No mode specified? */
442 php_stream_wrapper_log_error(wrapper, options, "Unknown file open mode");
443 return NULL;
444 }
445
446 if (context &&
447 (tmpzval = php_stream_context_get_option(context, "ftp", "proxy")) != NULL) {
448 if (read_write == 1) {
449 /* Use http wrapper to proxy ftp request */
450 return php_stream_url_wrap_http(wrapper, path, mode, options, opened_path, context STREAMS_CC);
451 } else {
452 /* ftp proxy is read-only */
453 php_stream_wrapper_log_error(wrapper, options, "FTP proxy may only be used in read mode");
454 return NULL;
455 }
456 }
457
458 stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
459 if (!stream) {
460 goto errexit;
461 }
462
463 /* set the connection to be binary */
464 php_stream_write_string(stream, "TYPE I\r\n");
465 result = GET_FTP_RESULT(stream);
466 if (result > 299 || result < 200)
467 goto errexit;
468
469 /* find out the size of the file (verifying it exists) */
470 php_stream_printf(stream, "SIZE %s\r\n", ZSTR_VAL(resource->path));
471
472 /* read the response */
473 result = GET_FTP_RESULT(stream);
474 if (read_write == 1) {
475 /* Read Mode */
476 char *sizestr;
477
478 /* when reading file, it must exist */
479 if (result > 299 || result < 200) {
480 errno = ENOENT;
481 goto errexit;
482 }
483
484 sizestr = strchr(tmp_line, ' ');
485 if (sizestr) {
486 sizestr++;
487 file_size = atoi(sizestr);
488 php_stream_notify_file_size(context, file_size, tmp_line, result);
489 }
490 } else if (read_write == 2) {
491 /* when writing file (but not appending), it must NOT exist, unless a context option exists which allows it */
492 if (context && (tmpzval = php_stream_context_get_option(context, "ftp", "overwrite")) != NULL) {
493 allow_overwrite = zend_is_true(tmpzval);
494 }
495 if (result <= 299 && result >= 200) {
496 if (allow_overwrite) {
497 /* Context permits overwriting file,
498 so we just delete whatever's there in preparation */
499 php_stream_printf(stream, "DELE %s\r\n", ZSTR_VAL(resource->path));
500 result = GET_FTP_RESULT(stream);
501 if (result >= 300 || result <= 199) {
502 goto errexit;
503 }
504 } else {
505 php_stream_wrapper_log_error(wrapper, options, "Remote file already exists and overwrite context option not specified");
506 errno = EEXIST;
507 goto errexit;
508 }
509 }
510 }
511
512 /* set up the passive connection */
513 portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
514
515 if (!portno) {
516 goto errexit;
517 }
518
519 /* Send RETR/STOR command */
520 if (read_write == 1) {
521 /* set resume position if applicable */
522 if (context &&
523 (tmpzval = php_stream_context_get_option(context, "ftp", "resume_pos")) != NULL &&
524 Z_TYPE_P(tmpzval) == IS_LONG &&
525 Z_LVAL_P(tmpzval) > 0) {
526 php_stream_printf(stream, "REST " ZEND_LONG_FMT "\r\n", Z_LVAL_P(tmpzval));
527 result = GET_FTP_RESULT(stream);
528 if (result < 300 || result > 399) {
529 php_stream_wrapper_log_error(wrapper, options, "Unable to resume from offset " ZEND_LONG_FMT, Z_LVAL_P(tmpzval));
530 goto errexit;
531 }
532 }
533
534 /* retrieve file */
535 memcpy(tmp_line, "RETR", sizeof("RETR"));
536 } else if (read_write == 2) {
537 /* Write new file */
538 memcpy(tmp_line, "STOR", sizeof("STOR"));
539 } else {
540 /* Append */
541 memcpy(tmp_line, "APPE", sizeof("APPE"));
542 }
543 php_stream_printf(stream, "%s %s\r\n", tmp_line, (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
544
545 /* open the data channel */
546 if (hoststart == NULL) {
547 hoststart = ZSTR_VAL(resource->host);
548 }
549 transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", hoststart, portno);
550 datastream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, &error_message, NULL);
551 efree(transport);
552 if (datastream == NULL) {
553 tmp_line[0]='\0';
554 goto errexit;
555 }
556
557 result = GET_FTP_RESULT(stream);
558 if (result != 150 && result != 125) {
559 /* Could not retrieve or send the file
560 * this data will only be sent to us after connection on the data port was initiated.
561 */
562 php_stream_close(datastream);
563 datastream = NULL;
564 goto errexit;
565 }
566
567 php_stream_context_set(datastream, context);
568 php_stream_notify_progress_init(context, 0, file_size);
569
570 if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
571 STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
572 php_stream_xport_crypto_enable(datastream, 1) < 0)) {
573
574 php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
575 php_stream_close(datastream);
576 datastream = NULL;
577 tmp_line[0]='\0';
578 goto errexit;
579 }
580
581 /* remember control stream */
582 datastream->wrapperthis = stream;
583
584 php_url_free(resource);
585 return datastream;
586
587 errexit:
588 if (resource) {
589 php_url_free(resource);
590 }
591 if (stream) {
592 php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
593 php_stream_close(stream);
594 }
595 if (tmp_line[0] != '\0')
596 php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
597
598 if (error_message) {
599 php_stream_wrapper_log_error(wrapper, options, "Failed to set up data channel: %s", ZSTR_VAL(error_message));
600 zend_string_release(error_message);
601 }
602 return NULL;
603 }
604 /* }}} */
605
606 /* {{{ php_ftp_dirsteam_read */
php_ftp_dirstream_read(php_stream * stream,char * buf,size_t count)607 static ssize_t php_ftp_dirstream_read(php_stream *stream, char *buf, size_t count)
608 {
609 php_stream_dirent *ent = (php_stream_dirent *)buf;
610 php_stream *innerstream;
611 size_t tmp_len;
612 zend_string *basename;
613
614 innerstream = ((php_ftp_dirstream_data *)stream->abstract)->datastream;
615
616 if (count != sizeof(php_stream_dirent)) {
617 return -1;
618 }
619
620 if (php_stream_eof(innerstream)) {
621 return 0;
622 }
623
624 if (!php_stream_get_line(innerstream, ent->d_name, sizeof(ent->d_name), &tmp_len)) {
625 return -1;
626 }
627
628 basename = php_basename(ent->d_name, tmp_len, NULL, 0);
629
630 tmp_len = MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1);
631 memcpy(ent->d_name, ZSTR_VAL(basename), tmp_len);
632 ent->d_name[tmp_len - 1] = '\0';
633 zend_string_release_ex(basename, 0);
634
635 /* Trim off trailing whitespace characters */
636 while (tmp_len > 0 &&
637 (ent->d_name[tmp_len - 1] == '\n' || ent->d_name[tmp_len - 1] == '\r' ||
638 ent->d_name[tmp_len - 1] == '\t' || ent->d_name[tmp_len - 1] == ' ')) {
639 ent->d_name[--tmp_len] = '\0';
640 }
641
642 return sizeof(php_stream_dirent);
643 }
644 /* }}} */
645
646 /* {{{ php_ftp_dirstream_close */
php_ftp_dirstream_close(php_stream * stream,int close_handle)647 static int php_ftp_dirstream_close(php_stream *stream, int close_handle)
648 {
649 php_ftp_dirstream_data *data = stream->abstract;
650
651 /* close control connection */
652 if (data->controlstream) {
653 php_stream_close(data->controlstream);
654 data->controlstream = NULL;
655 }
656 /* close data connection */
657 php_stream_close(data->datastream);
658 data->datastream = NULL;
659
660 efree(data);
661 stream->abstract = NULL;
662
663 return 0;
664 }
665 /* }}} */
666
667 /* ftp dirstreams only need to support read and close operations,
668 They can't be rewound because the underlying ftp stream can't be rewound. */
669 static const php_stream_ops php_ftp_dirstream_ops = {
670 NULL, /* write */
671 php_ftp_dirstream_read, /* read */
672 php_ftp_dirstream_close, /* close */
673 NULL, /* flush */
674 "ftpdir",
675 NULL, /* rewind */
676 NULL, /* cast */
677 NULL, /* stat */
678 NULL /* set option */
679 };
680
681 /* {{{ php_stream_ftp_opendir */
php_stream_ftp_opendir(php_stream_wrapper * wrapper,const char * path,const char * mode,int options,zend_string ** opened_path,php_stream_context * context STREAMS_DC)682 php_stream * php_stream_ftp_opendir(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
683 zend_string **opened_path, php_stream_context *context STREAMS_DC)
684 {
685 php_stream *stream, *reuseid, *datastream = NULL;
686 php_ftp_dirstream_data *dirsdata;
687 php_url *resource = NULL;
688 int result = 0, use_ssl, use_ssl_on_data = 0;
689 char *hoststart = NULL, tmp_line[512];
690 char ip[sizeof("123.123.123.123")];
691 unsigned short portno;
692
693 tmp_line[0] = '\0';
694
695 stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
696 if (!stream) {
697 goto opendir_errexit;
698 }
699
700 /* set the connection to be ascii */
701 php_stream_write_string(stream, "TYPE A\r\n");
702 result = GET_FTP_RESULT(stream);
703 if (result > 299 || result < 200)
704 goto opendir_errexit;
705
706 // tmp_line isn't relevant after the php_fopen_do_pasv().
707 tmp_line[0] = '\0';
708
709 /* set up the passive connection */
710 portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
711
712 if (!portno) {
713 goto opendir_errexit;
714 }
715
716 /* open the data channel */
717 if (hoststart == NULL) {
718 hoststart = ZSTR_VAL(resource->host);
719 }
720
721 datastream = php_stream_sock_open_host(hoststart, portno, SOCK_STREAM, 0, 0);
722 if (datastream == NULL) {
723 goto opendir_errexit;
724 }
725
726 php_stream_printf(stream, "NLST %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
727
728 result = GET_FTP_RESULT(stream);
729 if (result != 150 && result != 125) {
730 /* Could not retrieve or send the file
731 * this data will only be sent to us after connection on the data port was initiated.
732 */
733 php_stream_close(datastream);
734 datastream = NULL;
735 goto opendir_errexit;
736 }
737
738 php_stream_context_set(datastream, context);
739 if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
740 STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
741 php_stream_xport_crypto_enable(datastream, 1) < 0)) {
742
743 php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
744 php_stream_close(datastream);
745 datastream = NULL;
746 goto opendir_errexit;
747 }
748
749 php_url_free(resource);
750
751 dirsdata = emalloc(sizeof *dirsdata);
752 dirsdata->datastream = datastream;
753 dirsdata->controlstream = stream;
754 dirsdata->dirstream = php_stream_alloc(&php_ftp_dirstream_ops, dirsdata, 0, mode);
755
756 return dirsdata->dirstream;
757
758 opendir_errexit:
759 if (resource) {
760 php_url_free(resource);
761 }
762 if (stream) {
763 php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
764 php_stream_close(stream);
765 }
766 if (tmp_line[0] != '\0') {
767 php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
768 }
769 return NULL;
770 }
771 /* }}} */
772
773 /* {{{ php_stream_ftp_url_stat */
php_stream_ftp_url_stat(php_stream_wrapper * wrapper,const char * url,int flags,php_stream_statbuf * ssb,php_stream_context * context)774 static int php_stream_ftp_url_stat(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context)
775 {
776 php_stream *stream = NULL;
777 php_url *resource = NULL;
778 int result;
779 char tmp_line[512];
780
781 /* If ssb is NULL then someone is misbehaving */
782 if (!ssb) return -1;
783
784 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
785 if (!stream) {
786 goto stat_errexit;
787 }
788
789 ssb->sb.st_mode = 0644; /* FTP won't give us a valid mode, so approximate one based on being readable */
790 php_stream_printf(stream, "CWD %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/")); /* If we can CWD to it, it's a directory (maybe a link, but we can't tell) */
791 result = GET_FTP_RESULT(stream);
792 if (result < 200 || result > 299) {
793 ssb->sb.st_mode |= S_IFREG;
794 } else {
795 ssb->sb.st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
796 }
797
798 php_stream_write_string(stream, "TYPE I\r\n"); /* we need this since some servers refuse to accept SIZE command in ASCII mode */
799
800 result = GET_FTP_RESULT(stream);
801
802 if(result < 200 || result > 299) {
803 goto stat_errexit;
804 }
805
806 php_stream_printf(stream, "SIZE %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
807 result = GET_FTP_RESULT(stream);
808 if (result < 200 || result > 299) {
809 /* Failure either means it doesn't exist
810 or it's a directory and this server
811 fails on listing directory sizes */
812 if (ssb->sb.st_mode & S_IFDIR) {
813 ssb->sb.st_size = 0;
814 } else {
815 goto stat_errexit;
816 }
817 } else {
818 ssb->sb.st_size = atoi(tmp_line + 4);
819 }
820
821 php_stream_printf(stream, "MDTM %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
822 result = GET_FTP_RESULT(stream);
823 if (result == 213) {
824 char *p = tmp_line + 4;
825 int n;
826 struct tm tm, tmbuf, *gmt;
827 time_t stamp;
828
829 while ((size_t)(p - tmp_line) < sizeof(tmp_line) && !isdigit(*p)) {
830 p++;
831 }
832
833 if ((size_t)(p - tmp_line) > sizeof(tmp_line)) {
834 goto mdtm_error;
835 }
836
837 n = sscanf(p, "%4d%2d%2d%2d%2d%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
838 if (n != 6) {
839 goto mdtm_error;
840 }
841
842 tm.tm_year -= 1900;
843 tm.tm_mon--;
844 tm.tm_isdst = -1;
845
846 /* figure out the GMT offset */
847 stamp = time(NULL);
848 gmt = php_gmtime_r(&stamp, &tmbuf);
849 if (!gmt) {
850 goto mdtm_error;
851 }
852 gmt->tm_isdst = -1;
853
854 /* apply the GMT offset */
855 tm.tm_sec += (long)(stamp - mktime(gmt));
856 tm.tm_isdst = gmt->tm_isdst;
857
858 ssb->sb.st_mtime = mktime(&tm);
859 } else {
860 /* error or unsupported command */
861 mdtm_error:
862 ssb->sb.st_mtime = -1;
863 }
864
865 ssb->sb.st_ino = 0; /* Unknown values */
866 ssb->sb.st_dev = 0;
867 ssb->sb.st_uid = 0;
868 ssb->sb.st_gid = 0;
869 ssb->sb.st_atime = -1;
870 ssb->sb.st_ctime = -1;
871
872 ssb->sb.st_nlink = 1;
873 ssb->sb.st_rdev = -1;
874 #ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
875 ssb->sb.st_blksize = 4096; /* Guess since FTP won't expose this information */
876 #ifdef HAVE_STRUCT_STAT_ST_BLOCKS
877 ssb->sb.st_blocks = (int)((4095 + ssb->sb.st_size) / ssb->sb.st_blksize); /* emulate ceil */
878 #endif
879 #endif
880 php_stream_close(stream);
881 php_url_free(resource);
882 return 0;
883
884 stat_errexit:
885 if (resource) {
886 php_url_free(resource);
887 }
888 if (stream) {
889 php_stream_close(stream);
890 }
891 return -1;
892 }
893 /* }}} */
894
895 /* {{{ php_stream_ftp_unlink */
php_stream_ftp_unlink(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)896 static int php_stream_ftp_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
897 {
898 php_stream *stream = NULL;
899 php_url *resource = NULL;
900 int result;
901 char tmp_line[512];
902
903 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
904 if (!stream) {
905 if (options & REPORT_ERRORS) {
906 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
907 }
908 goto unlink_errexit;
909 }
910
911 if (resource->path == NULL) {
912 if (options & REPORT_ERRORS) {
913 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
914 }
915 goto unlink_errexit;
916 }
917
918 /* Attempt to delete the file */
919 php_stream_printf(stream, "DELE %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
920
921 result = GET_FTP_RESULT(stream);
922 if (result < 200 || result > 299) {
923 if (options & REPORT_ERRORS) {
924 php_error_docref(NULL, E_WARNING, "Error Deleting file: %s", tmp_line);
925 }
926 goto unlink_errexit;
927 }
928
929 php_url_free(resource);
930 php_stream_close(stream);
931 return 1;
932
933 unlink_errexit:
934 if (resource) {
935 php_url_free(resource);
936 }
937 if (stream) {
938 php_stream_close(stream);
939 }
940 return 0;
941 }
942 /* }}} */
943
944 /* {{{ php_stream_ftp_rename */
php_stream_ftp_rename(php_stream_wrapper * wrapper,const char * url_from,const char * url_to,int options,php_stream_context * context)945 static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context)
946 {
947 php_stream *stream = NULL;
948 php_url *resource_from = NULL, *resource_to = NULL;
949 int result;
950 char tmp_line[512];
951
952 resource_from = php_url_parse(url_from);
953 resource_to = php_url_parse(url_to);
954 /* Must be same scheme (ftp/ftp or ftps/ftps), same host, and same port
955 (or a 21/0 0/21 combination which is also "same")
956 Also require paths to/from */
957 if (!resource_from ||
958 !resource_to ||
959 !resource_from->scheme ||
960 !resource_to->scheme ||
961 !zend_string_equals(resource_from->scheme, resource_to->scheme) ||
962 !resource_from->host ||
963 !resource_to->host ||
964 !zend_string_equals(resource_from->host, resource_to->host) ||
965 (resource_from->port != resource_to->port &&
966 resource_from->port * resource_to->port != 0 &&
967 resource_from->port + resource_to->port != 21) ||
968 !resource_from->path ||
969 !resource_to->path) {
970 goto rename_errexit;
971 }
972
973 stream = php_ftp_fopen_connect(wrapper, url_from, "r", 0, NULL, context, NULL, NULL, NULL, NULL);
974 if (!stream) {
975 if (options & REPORT_ERRORS) {
976 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", ZSTR_VAL(resource_from->host));
977 }
978 goto rename_errexit;
979 }
980
981 /* Rename FROM */
982 php_stream_printf(stream, "RNFR %s\r\n", (resource_from->path != NULL ? ZSTR_VAL(resource_from->path) : "/"));
983
984 result = GET_FTP_RESULT(stream);
985 if (result < 300 || result > 399) {
986 if (options & REPORT_ERRORS) {
987 php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
988 }
989 goto rename_errexit;
990 }
991
992 /* Rename TO */
993 php_stream_printf(stream, "RNTO %s\r\n", (resource_to->path != NULL ? ZSTR_VAL(resource_to->path) : "/"));
994
995 result = GET_FTP_RESULT(stream);
996 if (result < 200 || result > 299) {
997 if (options & REPORT_ERRORS) {
998 php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
999 }
1000 goto rename_errexit;
1001 }
1002
1003 php_url_free(resource_from);
1004 php_url_free(resource_to);
1005 php_stream_close(stream);
1006 return 1;
1007
1008 rename_errexit:
1009 if (resource_from) {
1010 php_url_free(resource_from);
1011 }
1012 if (resource_to) {
1013 php_url_free(resource_to);
1014 }
1015 if (stream) {
1016 php_stream_close(stream);
1017 }
1018 return 0;
1019 }
1020 /* }}} */
1021
1022 /* {{{ php_stream_ftp_mkdir */
php_stream_ftp_mkdir(php_stream_wrapper * wrapper,const char * url,int mode,int options,php_stream_context * context)1023 static int php_stream_ftp_mkdir(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context)
1024 {
1025 php_stream *stream = NULL;
1026 php_url *resource = NULL;
1027 int result, recursive = options & PHP_STREAM_MKDIR_RECURSIVE;
1028 char tmp_line[512];
1029
1030 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
1031 if (!stream) {
1032 if (options & REPORT_ERRORS) {
1033 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
1034 }
1035 goto mkdir_errexit;
1036 }
1037
1038 if (resource->path == NULL) {
1039 if (options & REPORT_ERRORS) {
1040 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
1041 }
1042 goto mkdir_errexit;
1043 }
1044
1045 if (!recursive) {
1046 php_stream_printf(stream, "MKD %s\r\n", ZSTR_VAL(resource->path));
1047 result = GET_FTP_RESULT(stream);
1048 } else {
1049 /* we look for directory separator from the end of string, thus hopefully reducing our work load */
1050 char *p, *e, *buf;
1051
1052 buf = estrndup(ZSTR_VAL(resource->path), ZSTR_LEN(resource->path));
1053 e = buf + ZSTR_LEN(resource->path);
1054
1055 /* find a top level directory we need to create */
1056 while ((p = strrchr(buf, '/'))) {
1057 *p = '\0';
1058 php_stream_printf(stream, "CWD %s\r\n", strlen(buf) ? buf : "/");
1059 result = GET_FTP_RESULT(stream);
1060 if (result >= 200 && result <= 299) {
1061 *p = '/';
1062 break;
1063 }
1064 }
1065
1066 php_stream_printf(stream, "MKD %s\r\n", strlen(buf) ? buf : "/");
1067 result = GET_FTP_RESULT(stream);
1068
1069 if (result >= 200 && result <= 299) {
1070 if (!p) {
1071 p = buf;
1072 }
1073 /* create any needed directories if the creation of the 1st directory worked */
1074 while (p != e) {
1075 if (*p == '\0' && *(p + 1) != '\0') {
1076 *p = '/';
1077 php_stream_printf(stream, "MKD %s\r\n", buf);
1078 result = GET_FTP_RESULT(stream);
1079 if (result < 200 || result > 299) {
1080 if (options & REPORT_ERRORS) {
1081 php_error_docref(NULL, E_WARNING, "%s", tmp_line);
1082 }
1083 break;
1084 }
1085 }
1086 ++p;
1087 }
1088 }
1089
1090 efree(buf);
1091 }
1092
1093 php_url_free(resource);
1094 php_stream_close(stream);
1095
1096 if (result < 200 || result > 299) {
1097 /* Failure */
1098 return 0;
1099 }
1100
1101 return 1;
1102
1103 mkdir_errexit:
1104 if (resource) {
1105 php_url_free(resource);
1106 }
1107 if (stream) {
1108 php_stream_close(stream);
1109 }
1110 return 0;
1111 }
1112 /* }}} */
1113
1114 /* {{{ php_stream_ftp_rmdir */
php_stream_ftp_rmdir(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)1115 static int php_stream_ftp_rmdir(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
1116 {
1117 php_stream *stream = NULL;
1118 php_url *resource = NULL;
1119 int result;
1120 char tmp_line[512];
1121
1122 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
1123 if (!stream) {
1124 if (options & REPORT_ERRORS) {
1125 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
1126 }
1127 goto rmdir_errexit;
1128 }
1129
1130 if (resource->path == NULL) {
1131 if (options & REPORT_ERRORS) {
1132 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
1133 }
1134 goto rmdir_errexit;
1135 }
1136
1137 php_stream_printf(stream, "RMD %s\r\n", ZSTR_VAL(resource->path));
1138 result = GET_FTP_RESULT(stream);
1139
1140 if (result < 200 || result > 299) {
1141 if (options & REPORT_ERRORS) {
1142 php_error_docref(NULL, E_WARNING, "%s", tmp_line);
1143 }
1144 goto rmdir_errexit;
1145 }
1146
1147 php_url_free(resource);
1148 php_stream_close(stream);
1149
1150 return 1;
1151
1152 rmdir_errexit:
1153 if (resource) {
1154 php_url_free(resource);
1155 }
1156 if (stream) {
1157 php_stream_close(stream);
1158 }
1159 return 0;
1160 }
1161 /* }}} */
1162
1163 static const php_stream_wrapper_ops ftp_stream_wops = {
1164 php_stream_url_wrap_ftp,
1165 php_stream_ftp_stream_close, /* stream_close */
1166 php_stream_ftp_stream_stat,
1167 php_stream_ftp_url_stat, /* stat_url */
1168 php_stream_ftp_opendir, /* opendir */
1169 "ftp",
1170 php_stream_ftp_unlink, /* unlink */
1171 php_stream_ftp_rename, /* rename */
1172 php_stream_ftp_mkdir, /* mkdir */
1173 php_stream_ftp_rmdir, /* rmdir */
1174 NULL
1175 };
1176
1177 PHPAPI const php_stream_wrapper php_stream_ftp_wrapper = {
1178 &ftp_stream_wops,
1179 NULL,
1180 1 /* is_url */
1181 };
1182