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