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 php_url_free(resource);
303
304 if (stream) {
305 php_stream_close(stream);
306 }
307
308 return NULL;
309 }
310 /* }}} */
311
312 /* {{{ php_fopen_do_pasv */
php_fopen_do_pasv(php_stream * stream,char * ip,size_t ip_size,char ** phoststart)313 static unsigned short php_fopen_do_pasv(php_stream *stream, char *ip, size_t ip_size, char **phoststart)
314 {
315 char tmp_line[512];
316 int result, i;
317 unsigned short portno;
318 char *tpath, *ttpath, *hoststart=NULL;
319
320 #ifdef HAVE_IPV6
321 /* We try EPSV first, needed for IPv6 and works on some IPv4 servers */
322 php_stream_write_string(stream, "EPSV\r\n");
323 result = GET_FTP_RESULT(stream);
324
325 /* check if we got a 229 response */
326 if (result != 229) {
327 #endif
328 /* EPSV failed, let's try PASV */
329 php_stream_write_string(stream, "PASV\r\n");
330 result = GET_FTP_RESULT(stream);
331
332 /* make sure we got a 227 response */
333 if (result != 227) {
334 return 0;
335 }
336
337 /* parse pasv command (129, 80, 95, 25, 13, 221) */
338 tpath = tmp_line;
339 /* skip over the "227 Some message " part */
340 for (tpath += 4; *tpath && !isdigit((int) *tpath); tpath++);
341 if (!*tpath) {
342 return 0;
343 }
344 /* skip over the host ip, to get the port */
345 hoststart = tpath;
346 for (i = 0; i < 4; i++) {
347 for (; isdigit((int) *tpath); tpath++);
348 if (*tpath != ',') {
349 return 0;
350 }
351 *tpath='.';
352 tpath++;
353 }
354 tpath[-1] = '\0';
355 memcpy(ip, hoststart, ip_size);
356 ip[ip_size-1] = '\0';
357 hoststart = ip;
358
359 /* pull out the MSB of the port */
360 portno = (unsigned short) strtoul(tpath, &ttpath, 10) * 256;
361 if (ttpath == NULL) {
362 /* didn't get correct response from PASV */
363 return 0;
364 }
365 tpath = ttpath;
366 if (*tpath != ',') {
367 return 0;
368 }
369 tpath++;
370 /* pull out the LSB of the port */
371 portno += (unsigned short) strtoul(tpath, &ttpath, 10);
372 #ifdef HAVE_IPV6
373 } else {
374 /* parse epsv command (|||6446|) */
375 for (i = 0, tpath = tmp_line + 4; *tpath; tpath++) {
376 if (*tpath == '|') {
377 i++;
378 if (i == 3)
379 break;
380 }
381 }
382 if (i < 3) {
383 return 0;
384 }
385 /* pull out the port */
386 portno = (unsigned short) strtoul(tpath + 1, &ttpath, 10);
387 }
388 #endif
389 if (ttpath == NULL) {
390 /* didn't get correct response from EPSV/PASV */
391 return 0;
392 }
393
394 if (phoststart) {
395 *phoststart = hoststart;
396 }
397
398 return portno;
399 }
400 /* }}} */
401
402 /* {{{ 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)403 php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, const char *path, const char *mode,
404 int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
405 {
406 php_stream *stream = NULL, *datastream = NULL;
407 php_url *resource = NULL;
408 char tmp_line[512];
409 char ip[sizeof("123.123.123.123")];
410 unsigned short portno;
411 char *hoststart = NULL;
412 int result = 0, use_ssl, use_ssl_on_data=0;
413 php_stream *reuseid=NULL;
414 size_t file_size = 0;
415 zval *tmpzval;
416 bool allow_overwrite = 0;
417 int8_t read_write = 0;
418 char *transport;
419 int transport_len;
420 zend_string *error_message = NULL;
421
422 tmp_line[0] = '\0';
423
424 if (strpbrk(mode, "r+")) {
425 read_write = 1; /* Open for reading */
426 }
427 if (strpbrk(mode, "wa+")) {
428 if (read_write) {
429 php_stream_wrapper_log_error(wrapper, options, "FTP does not support simultaneous read/write connections");
430 return NULL;
431 }
432 if (strchr(mode, 'a')) {
433 read_write = 3; /* Open for Appending */
434 } else {
435 read_write = 2; /* Open for writing */
436 }
437 }
438 if (!read_write) {
439 /* No mode specified? */
440 php_stream_wrapper_log_error(wrapper, options, "Unknown file open mode");
441 return NULL;
442 }
443
444 if (context &&
445 (tmpzval = php_stream_context_get_option(context, "ftp", "proxy")) != NULL) {
446 if (read_write == 1) {
447 /* Use http wrapper to proxy ftp request */
448 return php_stream_url_wrap_http(wrapper, path, mode, options, opened_path, context STREAMS_CC);
449 } else {
450 /* ftp proxy is read-only */
451 php_stream_wrapper_log_error(wrapper, options, "FTP proxy may only be used in read mode");
452 return NULL;
453 }
454 }
455
456 stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
457 if (!stream) {
458 goto errexit;
459 }
460
461 /* set the connection to be binary */
462 php_stream_write_string(stream, "TYPE I\r\n");
463 result = GET_FTP_RESULT(stream);
464 if (result > 299 || result < 200)
465 goto errexit;
466
467 /* find out the size of the file (verifying it exists) */
468 php_stream_printf(stream, "SIZE %s\r\n", ZSTR_VAL(resource->path));
469
470 /* read the response */
471 result = GET_FTP_RESULT(stream);
472 if (read_write == 1) {
473 /* Read Mode */
474 char *sizestr;
475
476 /* when reading file, it must exist */
477 if (result > 299 || result < 200) {
478 errno = ENOENT;
479 goto errexit;
480 }
481
482 sizestr = strchr(tmp_line, ' ');
483 if (sizestr) {
484 sizestr++;
485 file_size = atoi(sizestr);
486 php_stream_notify_file_size(context, file_size, tmp_line, result);
487 }
488 } else if (read_write == 2) {
489 /* when writing file (but not appending), it must NOT exist, unless a context option exists which allows it */
490 if (context && (tmpzval = php_stream_context_get_option(context, "ftp", "overwrite")) != NULL) {
491 allow_overwrite = zend_is_true(tmpzval);
492 }
493 if (result <= 299 && result >= 200) {
494 if (allow_overwrite) {
495 /* Context permits overwriting file,
496 so we just delete whatever's there in preparation */
497 php_stream_printf(stream, "DELE %s\r\n", ZSTR_VAL(resource->path));
498 result = GET_FTP_RESULT(stream);
499 if (result >= 300 || result <= 199) {
500 goto errexit;
501 }
502 } else {
503 php_stream_wrapper_log_error(wrapper, options, "Remote file already exists and overwrite context option not specified");
504 errno = EEXIST;
505 goto errexit;
506 }
507 }
508 }
509
510 /* set up the passive connection */
511 portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
512
513 if (!portno) {
514 goto errexit;
515 }
516
517 /* Send RETR/STOR command */
518 if (read_write == 1) {
519 /* set resume position if applicable */
520 if (context &&
521 (tmpzval = php_stream_context_get_option(context, "ftp", "resume_pos")) != NULL &&
522 Z_TYPE_P(tmpzval) == IS_LONG &&
523 Z_LVAL_P(tmpzval) > 0) {
524 php_stream_printf(stream, "REST " ZEND_LONG_FMT "\r\n", Z_LVAL_P(tmpzval));
525 result = GET_FTP_RESULT(stream);
526 if (result < 300 || result > 399) {
527 php_stream_wrapper_log_error(wrapper, options, "Unable to resume from offset " ZEND_LONG_FMT, Z_LVAL_P(tmpzval));
528 goto errexit;
529 }
530 }
531
532 /* retrieve file */
533 memcpy(tmp_line, "RETR", sizeof("RETR"));
534 } else if (read_write == 2) {
535 /* Write new file */
536 memcpy(tmp_line, "STOR", sizeof("STOR"));
537 } else {
538 /* Append */
539 memcpy(tmp_line, "APPE", sizeof("APPE"));
540 }
541 php_stream_printf(stream, "%s %s\r\n", tmp_line, (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
542
543 /* open the data channel */
544 if (hoststart == NULL) {
545 hoststart = ZSTR_VAL(resource->host);
546 }
547 transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", hoststart, portno);
548 datastream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, &error_message, NULL);
549 efree(transport);
550 if (datastream == NULL) {
551 tmp_line[0]='\0';
552 goto errexit;
553 }
554
555 result = GET_FTP_RESULT(stream);
556 if (result != 150 && result != 125) {
557 /* Could not retrieve or send the file
558 * this data will only be sent to us after connection on the data port was initiated.
559 */
560 php_stream_close(datastream);
561 datastream = NULL;
562 goto errexit;
563 }
564
565 php_stream_context_set(datastream, context);
566 php_stream_notify_progress_init(context, 0, file_size);
567
568 if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
569 STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
570 php_stream_xport_crypto_enable(datastream, 1) < 0)) {
571
572 php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
573 php_stream_close(datastream);
574 datastream = NULL;
575 tmp_line[0]='\0';
576 goto errexit;
577 }
578
579 /* remember control stream */
580 datastream->wrapperthis = stream;
581
582 php_url_free(resource);
583 return datastream;
584
585 errexit:
586 if (resource) {
587 php_url_free(resource);
588 }
589 if (stream) {
590 php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
591 php_stream_close(stream);
592 }
593 if (tmp_line[0] != '\0')
594 php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
595
596 if (error_message) {
597 php_stream_wrapper_log_error(wrapper, options, "Failed to set up data channel: %s", ZSTR_VAL(error_message));
598 zend_string_release(error_message);
599 }
600 return NULL;
601 }
602 /* }}} */
603
604 /* {{{ php_ftp_dirsteam_read */
php_ftp_dirstream_read(php_stream * stream,char * buf,size_t count)605 static ssize_t php_ftp_dirstream_read(php_stream *stream, char *buf, size_t count)
606 {
607 php_stream_dirent *ent = (php_stream_dirent *)buf;
608 php_stream *innerstream;
609 size_t tmp_len;
610 zend_string *basename;
611
612 innerstream = ((php_ftp_dirstream_data *)stream->abstract)->datastream;
613
614 if (count != sizeof(php_stream_dirent)) {
615 return -1;
616 }
617
618 if (php_stream_eof(innerstream)) {
619 return 0;
620 }
621
622 if (!php_stream_get_line(innerstream, ent->d_name, sizeof(ent->d_name), &tmp_len)) {
623 return -1;
624 }
625
626 basename = php_basename(ent->d_name, tmp_len, NULL, 0);
627
628 tmp_len = MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1);
629 memcpy(ent->d_name, ZSTR_VAL(basename), tmp_len);
630 ent->d_name[tmp_len - 1] = '\0';
631 zend_string_release_ex(basename, 0);
632 ent->d_type = DT_UNKNOWN;
633
634 /* Trim off trailing whitespace characters */
635 while (tmp_len > 0 &&
636 (ent->d_name[tmp_len - 1] == '\n' || ent->d_name[tmp_len - 1] == '\r' ||
637 ent->d_name[tmp_len - 1] == '\t' || ent->d_name[tmp_len - 1] == ' ')) {
638 ent->d_name[--tmp_len] = '\0';
639 }
640
641 return sizeof(php_stream_dirent);
642 }
643 /* }}} */
644
645 /* {{{ php_ftp_dirstream_close */
php_ftp_dirstream_close(php_stream * stream,int close_handle)646 static int php_ftp_dirstream_close(php_stream *stream, int close_handle)
647 {
648 php_ftp_dirstream_data *data = stream->abstract;
649
650 /* close control connection */
651 if (data->controlstream) {
652 php_stream_close(data->controlstream);
653 data->controlstream = NULL;
654 }
655 /* close data connection */
656 php_stream_close(data->datastream);
657 data->datastream = NULL;
658
659 efree(data);
660 stream->abstract = NULL;
661
662 return 0;
663 }
664 /* }}} */
665
666 /* ftp dirstreams only need to support read and close operations,
667 They can't be rewound because the underlying ftp stream can't be rewound. */
668 static const php_stream_ops php_ftp_dirstream_ops = {
669 NULL, /* write */
670 php_ftp_dirstream_read, /* read */
671 php_ftp_dirstream_close, /* close */
672 NULL, /* flush */
673 "ftpdir",
674 NULL, /* rewind */
675 NULL, /* cast */
676 NULL, /* stat */
677 NULL /* set option */
678 };
679
680 /* {{{ 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)681 php_stream * php_stream_ftp_opendir(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
682 zend_string **opened_path, php_stream_context *context STREAMS_DC)
683 {
684 php_stream *stream, *reuseid, *datastream = NULL;
685 php_ftp_dirstream_data *dirsdata;
686 php_url *resource = NULL;
687 int result = 0, use_ssl, use_ssl_on_data = 0;
688 char *hoststart = NULL, tmp_line[512];
689 char ip[sizeof("123.123.123.123")];
690 unsigned short portno;
691
692 tmp_line[0] = '\0';
693
694 stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
695 if (!stream) {
696 goto opendir_errexit;
697 }
698
699 /* set the connection to be ascii */
700 php_stream_write_string(stream, "TYPE A\r\n");
701 result = GET_FTP_RESULT(stream);
702 if (result > 299 || result < 200)
703 goto opendir_errexit;
704
705 // tmp_line isn't relevant after the php_fopen_do_pasv().
706 tmp_line[0] = '\0';
707
708 /* set up the passive connection */
709 portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
710
711 if (!portno) {
712 goto opendir_errexit;
713 }
714
715 /* open the data channel */
716 if (hoststart == NULL) {
717 hoststart = ZSTR_VAL(resource->host);
718 }
719
720 datastream = php_stream_sock_open_host(hoststart, portno, SOCK_STREAM, 0, 0);
721 if (datastream == NULL) {
722 goto opendir_errexit;
723 }
724
725 php_stream_printf(stream, "NLST %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
726
727 result = GET_FTP_RESULT(stream);
728 if (result != 150 && result != 125) {
729 /* Could not retrieve or send the file
730 * this data will only be sent to us after connection on the data port was initiated.
731 */
732 php_stream_close(datastream);
733 datastream = NULL;
734 goto opendir_errexit;
735 }
736
737 php_stream_context_set(datastream, context);
738 if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
739 STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
740 php_stream_xport_crypto_enable(datastream, 1) < 0)) {
741
742 php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
743 php_stream_close(datastream);
744 datastream = NULL;
745 goto opendir_errexit;
746 }
747
748 php_url_free(resource);
749
750 dirsdata = emalloc(sizeof *dirsdata);
751 dirsdata->datastream = datastream;
752 dirsdata->controlstream = stream;
753 dirsdata->dirstream = php_stream_alloc(&php_ftp_dirstream_ops, dirsdata, 0, mode);
754
755 return dirsdata->dirstream;
756
757 opendir_errexit:
758 if (resource) {
759 php_url_free(resource);
760 }
761 if (stream) {
762 php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
763 php_stream_close(stream);
764 }
765 if (tmp_line[0] != '\0') {
766 php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
767 }
768 return NULL;
769 }
770 /* }}} */
771
772 /* {{{ 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)773 static int php_stream_ftp_url_stat(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context)
774 {
775 php_stream *stream = NULL;
776 php_url *resource = NULL;
777 int result;
778 char tmp_line[512];
779
780 /* If ssb is NULL then someone is misbehaving */
781 if (!ssb) return -1;
782
783 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
784 if (!stream) {
785 goto stat_errexit;
786 }
787
788 ssb->sb.st_mode = 0644; /* FTP won't give us a valid mode, so approximate one based on being readable */
789 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) */
790 result = GET_FTP_RESULT(stream);
791 if (result < 200 || result > 299) {
792 ssb->sb.st_mode |= S_IFREG;
793 } else {
794 ssb->sb.st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
795 }
796
797 php_stream_write_string(stream, "TYPE I\r\n"); /* we need this since some servers refuse to accept SIZE command in ASCII mode */
798
799 result = GET_FTP_RESULT(stream);
800
801 if(result < 200 || result > 299) {
802 goto stat_errexit;
803 }
804
805 php_stream_printf(stream, "SIZE %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
806 result = GET_FTP_RESULT(stream);
807 if (result < 200 || result > 299) {
808 /* Failure either means it doesn't exist
809 or it's a directory and this server
810 fails on listing directory sizes */
811 if (ssb->sb.st_mode & S_IFDIR) {
812 ssb->sb.st_size = 0;
813 } else {
814 goto stat_errexit;
815 }
816 } else {
817 ssb->sb.st_size = atoi(tmp_line + 4);
818 }
819
820 php_stream_printf(stream, "MDTM %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
821 result = GET_FTP_RESULT(stream);
822 if (result == 213) {
823 char *p = tmp_line + 4;
824 int n;
825 struct tm tm, tmbuf, *gmt;
826 time_t stamp;
827
828 while ((size_t)(p - tmp_line) < sizeof(tmp_line) && !isdigit(*p)) {
829 p++;
830 }
831
832 if ((size_t)(p - tmp_line) > sizeof(tmp_line)) {
833 goto mdtm_error;
834 }
835
836 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);
837 if (n != 6) {
838 goto mdtm_error;
839 }
840
841 tm.tm_year -= 1900;
842 tm.tm_mon--;
843 tm.tm_isdst = -1;
844
845 /* figure out the GMT offset */
846 stamp = time(NULL);
847 gmt = php_gmtime_r(&stamp, &tmbuf);
848 if (!gmt) {
849 goto mdtm_error;
850 }
851 gmt->tm_isdst = -1;
852
853 /* apply the GMT offset */
854 tm.tm_sec += (long)(stamp - mktime(gmt));
855 tm.tm_isdst = gmt->tm_isdst;
856
857 ssb->sb.st_mtime = mktime(&tm);
858 } else {
859 /* error or unsupported command */
860 mdtm_error:
861 ssb->sb.st_mtime = -1;
862 }
863
864 ssb->sb.st_ino = 0; /* Unknown values */
865 ssb->sb.st_dev = 0;
866 ssb->sb.st_uid = 0;
867 ssb->sb.st_gid = 0;
868 ssb->sb.st_atime = -1;
869 ssb->sb.st_ctime = -1;
870
871 ssb->sb.st_nlink = 1;
872 ssb->sb.st_rdev = -1;
873 #ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
874 ssb->sb.st_blksize = 4096; /* Guess since FTP won't expose this information */
875 #ifdef HAVE_STRUCT_STAT_ST_BLOCKS
876 ssb->sb.st_blocks = (int)((4095 + ssb->sb.st_size) / ssb->sb.st_blksize); /* emulate ceil */
877 #endif
878 #endif
879 php_stream_close(stream);
880 php_url_free(resource);
881 return 0;
882
883 stat_errexit:
884 if (resource) {
885 php_url_free(resource);
886 }
887 if (stream) {
888 php_stream_close(stream);
889 }
890 return -1;
891 }
892 /* }}} */
893
894 /* {{{ php_stream_ftp_unlink */
php_stream_ftp_unlink(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)895 static int php_stream_ftp_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
896 {
897 php_stream *stream = NULL;
898 php_url *resource = NULL;
899 int result;
900 char tmp_line[512];
901
902 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
903 if (!stream) {
904 if (options & REPORT_ERRORS) {
905 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
906 }
907 goto unlink_errexit;
908 }
909
910 if (resource->path == NULL) {
911 if (options & REPORT_ERRORS) {
912 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
913 }
914 goto unlink_errexit;
915 }
916
917 /* Attempt to delete the file */
918 php_stream_printf(stream, "DELE %s\r\n", ZSTR_VAL(resource->path));
919
920 result = GET_FTP_RESULT(stream);
921 if (result < 200 || result > 299) {
922 if (options & REPORT_ERRORS) {
923 php_error_docref(NULL, E_WARNING, "Error Deleting file: %s", tmp_line);
924 }
925 goto unlink_errexit;
926 }
927
928 php_url_free(resource);
929 php_stream_close(stream);
930 return 1;
931
932 unlink_errexit:
933 if (resource) {
934 php_url_free(resource);
935 }
936 if (stream) {
937 php_stream_close(stream);
938 }
939 return 0;
940 }
941 /* }}} */
942
943 /* {{{ 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)944 static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context)
945 {
946 php_stream *stream = NULL;
947 php_url *resource_from = NULL, *resource_to = NULL;
948 int result;
949 char tmp_line[512];
950
951 resource_from = php_url_parse(url_from);
952 resource_to = php_url_parse(url_to);
953 /* Must be same scheme (ftp/ftp or ftps/ftps), same host, and same port
954 (or a 21/0 0/21 combination which is also "same")
955 Also require paths to/from */
956 if (!resource_from ||
957 !resource_to ||
958 !resource_from->scheme ||
959 !resource_to->scheme ||
960 !zend_string_equals(resource_from->scheme, resource_to->scheme) ||
961 !resource_from->host ||
962 !resource_to->host ||
963 !zend_string_equals(resource_from->host, resource_to->host) ||
964 (resource_from->port != resource_to->port &&
965 resource_from->port * resource_to->port != 0 &&
966 resource_from->port + resource_to->port != 21) ||
967 !resource_from->path ||
968 !resource_to->path) {
969 goto rename_errexit;
970 }
971
972 stream = php_ftp_fopen_connect(wrapper, url_from, "r", 0, NULL, context, NULL, NULL, NULL, NULL);
973 if (!stream) {
974 if (options & REPORT_ERRORS) {
975 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", ZSTR_VAL(resource_from->host));
976 }
977 goto rename_errexit;
978 }
979
980 /* Rename FROM */
981 php_stream_printf(stream, "RNFR %s\r\n", ZSTR_VAL(resource_from->path));
982
983 result = GET_FTP_RESULT(stream);
984 if (result < 300 || result > 399) {
985 if (options & REPORT_ERRORS) {
986 php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
987 }
988 goto rename_errexit;
989 }
990
991 /* Rename TO */
992 php_stream_printf(stream, "RNTO %s\r\n", ZSTR_VAL(resource_to->path));
993
994 result = GET_FTP_RESULT(stream);
995 if (result < 200 || result > 299) {
996 if (options & REPORT_ERRORS) {
997 php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
998 }
999 goto rename_errexit;
1000 }
1001
1002 php_url_free(resource_from);
1003 php_url_free(resource_to);
1004 php_stream_close(stream);
1005 return 1;
1006
1007 rename_errexit:
1008 if (resource_from) {
1009 php_url_free(resource_from);
1010 }
1011 if (resource_to) {
1012 php_url_free(resource_to);
1013 }
1014 if (stream) {
1015 php_stream_close(stream);
1016 }
1017 return 0;
1018 }
1019 /* }}} */
1020
1021 /* {{{ php_stream_ftp_mkdir */
php_stream_ftp_mkdir(php_stream_wrapper * wrapper,const char * url,int mode,int options,php_stream_context * context)1022 static int php_stream_ftp_mkdir(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context)
1023 {
1024 php_stream *stream = NULL;
1025 php_url *resource = NULL;
1026 int result, recursive = options & PHP_STREAM_MKDIR_RECURSIVE;
1027 char tmp_line[512];
1028
1029 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
1030 if (!stream) {
1031 if (options & REPORT_ERRORS) {
1032 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
1033 }
1034 goto mkdir_errexit;
1035 }
1036
1037 if (resource->path == NULL) {
1038 if (options & REPORT_ERRORS) {
1039 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
1040 }
1041 goto mkdir_errexit;
1042 }
1043
1044 if (!recursive) {
1045 php_stream_printf(stream, "MKD %s\r\n", ZSTR_VAL(resource->path));
1046 result = GET_FTP_RESULT(stream);
1047 } else {
1048 /* we look for directory separator from the end of string, thus hopefully reducing our work load */
1049 char *p, *e, *buf;
1050
1051 buf = estrndup(ZSTR_VAL(resource->path), ZSTR_LEN(resource->path));
1052 e = buf + ZSTR_LEN(resource->path);
1053
1054 /* find a top level directory we need to create */
1055 while ((p = strrchr(buf, '/'))) {
1056 *p = '\0';
1057 php_stream_printf(stream, "CWD %s\r\n", strlen(buf) ? buf : "/");
1058 result = GET_FTP_RESULT(stream);
1059 if (result >= 200 && result <= 299) {
1060 *p = '/';
1061 break;
1062 }
1063 }
1064
1065 php_stream_printf(stream, "MKD %s\r\n", strlen(buf) ? buf : "/");
1066 result = GET_FTP_RESULT(stream);
1067
1068 if (result >= 200 && result <= 299) {
1069 if (!p) {
1070 p = buf;
1071 }
1072 /* create any needed directories if the creation of the 1st directory worked */
1073 while (p != e) {
1074 if (*p == '\0' && *(p + 1) != '\0') {
1075 *p = '/';
1076 php_stream_printf(stream, "MKD %s\r\n", buf);
1077 result = GET_FTP_RESULT(stream);
1078 if (result < 200 || result > 299) {
1079 if (options & REPORT_ERRORS) {
1080 php_error_docref(NULL, E_WARNING, "%s", tmp_line);
1081 }
1082 break;
1083 }
1084 }
1085 ++p;
1086 }
1087 }
1088
1089 efree(buf);
1090 }
1091
1092 php_url_free(resource);
1093 php_stream_close(stream);
1094
1095 if (result < 200 || result > 299) {
1096 /* Failure */
1097 return 0;
1098 }
1099
1100 return 1;
1101
1102 mkdir_errexit:
1103 if (resource) {
1104 php_url_free(resource);
1105 }
1106 if (stream) {
1107 php_stream_close(stream);
1108 }
1109 return 0;
1110 }
1111 /* }}} */
1112
1113 /* {{{ php_stream_ftp_rmdir */
php_stream_ftp_rmdir(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)1114 static int php_stream_ftp_rmdir(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
1115 {
1116 php_stream *stream = NULL;
1117 php_url *resource = NULL;
1118 int result;
1119 char tmp_line[512];
1120
1121 stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
1122 if (!stream) {
1123 if (options & REPORT_ERRORS) {
1124 php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
1125 }
1126 goto rmdir_errexit;
1127 }
1128
1129 if (resource->path == NULL) {
1130 if (options & REPORT_ERRORS) {
1131 php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
1132 }
1133 goto rmdir_errexit;
1134 }
1135
1136 php_stream_printf(stream, "RMD %s\r\n", ZSTR_VAL(resource->path));
1137 result = GET_FTP_RESULT(stream);
1138
1139 if (result < 200 || result > 299) {
1140 if (options & REPORT_ERRORS) {
1141 php_error_docref(NULL, E_WARNING, "%s", tmp_line);
1142 }
1143 goto rmdir_errexit;
1144 }
1145
1146 php_url_free(resource);
1147 php_stream_close(stream);
1148
1149 return 1;
1150
1151 rmdir_errexit:
1152 if (resource) {
1153 php_url_free(resource);
1154 }
1155 if (stream) {
1156 php_stream_close(stream);
1157 }
1158 return 0;
1159 }
1160 /* }}} */
1161
1162 static const php_stream_wrapper_ops ftp_stream_wops = {
1163 php_stream_url_wrap_ftp,
1164 php_stream_ftp_stream_close, /* stream_close */
1165 php_stream_ftp_stream_stat,
1166 php_stream_ftp_url_stat, /* stat_url */
1167 php_stream_ftp_opendir, /* opendir */
1168 "ftp",
1169 php_stream_ftp_unlink, /* unlink */
1170 php_stream_ftp_rename, /* rename */
1171 php_stream_ftp_mkdir, /* mkdir */
1172 php_stream_ftp_rmdir, /* rmdir */
1173 NULL
1174 };
1175
1176 PHPAPI const php_stream_wrapper php_stream_ftp_wrapper = {
1177 &ftp_stream_wops,
1178 NULL,
1179 1 /* is_url */
1180 };
1181