xref: /PHP-8.1/ext/standard/ftp_fopen_wrapper.c (revision 1d369a87)
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 #if 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 #if 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