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