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