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