/*
   +----------------------------------------------------------------------+
   | PHP Version 4                                                        |
   +----------------------------------------------------------------------+
   | Copyright (c) 1997-2007 The PHP Group                                |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
   | Authors: Rasmus Lerdorf <rasmus@php.net>                             |
   |          Jim Winstead <jimw@php.net>                                 |
   |          Hartmut Holzgraefe <hholzgra@php.net>                       |
   +----------------------------------------------------------------------+
 */
/* $Id: ftp_fopen_wrapper.c,v 1.38.2.8.2.6 2007/01/01 09:46:48 sebastian Exp $ */

#include "php.h"
#include "php_globals.h"
#include "php_network.h"

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#ifdef PHP_WIN32
#include <windows.h>
#include <winsock.h>
#define O_RDONLY _O_RDONLY
#include "win32/param.h"
#else
#include <sys/param.h>
#endif

#include "php_standard.h"

#include <sys/types.h>
#if HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif

#ifdef PHP_WIN32
#include <winsock.h>
#elif defined(NETWARE) && defined(USE_WINSOCK)
#include <novsock2.h>
#else
#include <netinet/in.h>
#include <netdb.h>
#if HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#endif

#if defined(PHP_WIN32) || defined(__riscos__) || defined(NETWARE)
#undef AF_UNIX
#endif

#if defined(AF_UNIX)
#include <sys/un.h>
#endif

#include "php_fopen_wrappers.h"


static inline int get_ftp_result(php_stream *stream, char *buffer, size_t buffer_size TSRMLS_DC)
{
	while (php_stream_gets(stream, buffer, buffer_size-1) &&
		   !(isdigit((int) buffer[0]) && isdigit((int) buffer[1]) &&
			 isdigit((int) buffer[2]) && buffer[3] == ' '));
	return strtol(buffer, NULL, 10);
}
#define GET_FTP_RESULT(stream)	get_ftp_result((stream), tmp_line, sizeof(tmp_line) TSRMLS_CC)

#define FTPS_ENCRYPT_DATA 1

static int php_stream_ftp_stream_stat(php_stream_wrapper *wrapper,
		php_stream *stream,
		php_stream_statbuf *ssb
		TSRMLS_DC)
{
	/* For now, we return with a failure code to prevent the underlying
	 * file's details from being used instead. */
	return -1;
}


static int php_stream_ftp_stream_close(php_stream_wrapper *wrapper,
		php_stream *stream
		TSRMLS_DC)
{
	php_stream *controlstream = (php_stream *)stream->wrapperdata;
	
	if (controlstream) {
		php_stream_write_string(controlstream, "QUIT\r\n");
		php_stream_close(controlstream);
		stream->wrapperdata = NULL;
	}
	return 0;
}


static php_stream_wrapper_ops ftp_stream_wops = {
	php_stream_url_wrap_ftp,
	php_stream_ftp_stream_close, /* stream_close */
	php_stream_ftp_stream_stat,
	NULL, /* stat_url */
	NULL, /* opendir */
	"FTP"
};

php_stream_wrapper php_stream_ftp_wrapper =	{
	&ftp_stream_wops,
	NULL,
	1 /* is_url */
};


/* {{{ php_fopen_url_wrap_ftp
 */
php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, char *path, char *mode, int options, char **opened_path, php_stream_context *context STREAMS_DC TSRMLS_DC)
{
	php_stream *stream=NULL, *datastream=NULL;
	php_url *resource=NULL;
	char tmp_line[512];
	char ip[sizeof("123.123.123.123")];
	unsigned short portno;
	char *scratch;
	int result;
	int i, use_ssl, tmp_len;
#ifdef HAVE_OPENSSL_EXT	
	int use_ssl_on_data=0;
	php_stream *reuseid=NULL;
#endif	
	char *tpath, *ttpath, *hoststart=NULL;
	size_t file_size = 0;

	tmp_line[0] = '\0';

	if (strchr(mode, 'a') || strchr(mode, '+')) {
		php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "FTP does not support simultaneous read/write connections.");
		return NULL;
	}
	
	resource = php_url_parse((char *) path);
	if (resource == NULL || resource->path == NULL)
		return NULL;

	use_ssl = resource->scheme && (strlen(resource->scheme) > 3) && resource->scheme[3] == 's';

	/* use port 21 if one wasn't specified */
	if (resource->port == 0)
		resource->port = 21;
	
	stream = php_stream_sock_open_host(resource->host, resource->port, SOCK_STREAM, NULL, 0);
	if (stream == NULL) {
		result = 0; /* silence */
		goto errexit;
	}

	php_stream_context_set(stream, context);
	php_stream_notify_info(context, PHP_STREAM_NOTIFY_CONNECT, NULL, 0);

	/* Start talking to ftp server */
	result = GET_FTP_RESULT(stream);
	if (result > 299 || result < 200) {
		php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
		goto errexit;
	}

#ifdef HAVE_OPENSSL_EXT
	if (use_ssl)	{
	
		/* send the AUTH TLS request name */
		php_stream_write_string(stream, "AUTH TLS\r\n");

		/* get the response */
		result = GET_FTP_RESULT(stream);
		if (result != 234) {
			/* AUTH TLS not supported try AUTH SSL */
			php_stream_write_string(stream, "AUTH SSL\r\n");
			
			/* get the response */
			result = GET_FTP_RESULT(stream);
			if (result != 334) {
				use_ssl = 0;
			} else {
				/* we must reuse the old SSL session id */
				/* if we talk to an old ftpd-ssl */
				reuseid = stream;
			}
		} else {
			/* encrypt data etc */


		}

	}
	
	if (use_ssl) {
		if (use_ssl && php_stream_sock_ssl_activate(stream, 1) == FAILURE)	{
			php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unable to activate SSL mode");
			php_stream_close(stream);
			stream = NULL;
			goto errexit;
		}
	
		/* set PBSZ to 0 */
		php_stream_write_string(stream, "PBSZ 0\r\n");

		/* ignore the response */
		result = GET_FTP_RESULT(stream);
		
		/* set data connection protection level */
#if FTPS_ENCRYPT_DATA
		php_stream_write_string(stream, "PROT P\r\n");

		/* get the response */
		result = GET_FTP_RESULT(stream);
		use_ssl_on_data = (result >= 200 && result<=299) || reuseid;
#else
		php_stream_write_string(stream, "PROT C\r\n");

		/* get the response */
		result = GET_FTP_RESULT(stream);
#endif
	}

#endif

#define PHP_FTP_CNTRL_CHK(val, val_len, err_msg) {	\
	unsigned char *s = val, *e = s + val_len;	\
	while (s < e) {	\
		if (iscntrl(*s)) {	\
			php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, err_msg, val);	\
			goto errexit;	\
		}	\
		s++;	\
	}	\
}

	/* send the user name */
	if (resource->user != NULL) {
		tmp_len = php_raw_url_decode(resource->user, strlen(resource->user));
		
		PHP_FTP_CNTRL_CHK(resource->user, tmp_len, "Invalid login %s")
		
		php_stream_printf(stream TSRMLS_CC, "USER %s\r\n", resource->user);
	} else {
		php_stream_write_string(stream, "USER anonymous\r\n");
	}
	
	/* get the response */
	result = GET_FTP_RESULT(stream);
	
	/* if a password is required, send it */
	if (result >= 300 && result <= 399) {
		php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, tmp_line, 0);

		if (resource->pass != NULL) {
			tmp_len = php_raw_url_decode(resource->pass, strlen(resource->pass));
			
			PHP_FTP_CNTRL_CHK(resource->pass, tmp_len, "Invalid password %s")
			
			php_stream_printf(stream TSRMLS_CC, "PASS %s\r\n", resource->pass);
		} else {
			/* if the user has configured who they are,
			   send that as the password */
			if (cfg_get_string("from", &scratch) == SUCCESS) {
				php_stream_printf(stream TSRMLS_CC, "PASS %s\r\n", scratch);
			} else {
				php_stream_write_string(stream, "PASS anonymous\r\n");
			}
		}
		
		/* read the response */
		result = GET_FTP_RESULT(stream);

		if (result > 299 || result < 200) {
			php_stream_notify_error(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
		} else {
			php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
		}
	}
	if (result > 299 || result < 200)
		goto errexit;
	
	/* set the connection to be binary */
	php_stream_write_string(stream, "TYPE I\r\n");
	result = GET_FTP_RESULT(stream);
	if (result > 299 || result < 200)
		goto errexit;
	
	/* find out the size of the file (verifying it exists) */
	php_stream_printf(stream TSRMLS_CC, "SIZE %s\r\n", resource->path);
	
	/* read the response */
	result = GET_FTP_RESULT(stream);
	if (mode[0] == 'r') {
		char *sizestr;
		
		/* when reading file, it must exist */
		if (result > 299 || result < 200) {
			errno = ENOENT;
			goto errexit;
		}
		
		sizestr = strchr(tmp_line, ' ');
		if (sizestr) {
			sizestr++;
			file_size = atoi(sizestr);
			php_stream_notify_file_size(context, file_size, tmp_line, result);
		}	
	} else {
		/* when writing file, it must NOT exist */
		if (result <= 299 && result >= 200) {
			errno = EEXIST;
			goto errexit;
		}
	}

	/* set up the passive connection */

	/* We try EPSV first, needed for IPv6 and works on some IPv4 servers */
	php_stream_write_string(stream, "EPSV\r\n");
	result = GET_FTP_RESULT(stream);

	/* check if we got a 229 response */
	if (result != 229) {
		/* EPSV failed, let's try PASV */
		php_stream_write_string(stream, "PASV\r\n");
		result = GET_FTP_RESULT(stream);
		
		/* make sure we got a 227 response */
		if (result != 227)
			goto errexit;

		/* parse pasv command (129, 80, 95, 25, 13, 221) */
		tpath = tmp_line;
		/* skip over the "227 Some message " part */
		for (tpath += 4; *tpath && !isdigit((int) *tpath); tpath++);
		if (!*tpath)
			goto errexit;
		/* skip over the host ip, to get the port */
		hoststart = tpath;
		for (i = 0; i < 4; i++) {
			for (; isdigit((int) *tpath); tpath++);
			if (*tpath != ',')
				goto errexit;
			*tpath='.';	
			tpath++;
		}
		tpath[-1] = '\0';
		memcpy(ip, hoststart, sizeof(ip));
		ip[sizeof(ip)-1] = '\0';
		hoststart = ip;
		
		/* pull out the MSB of the port */
		portno = (unsigned short) strtoul(tpath, &ttpath, 10) * 256;
		if (ttpath == NULL) {
			/* didn't get correct response from PASV */
			goto errexit;
		}
		tpath = ttpath;
		if (*tpath != ',')
			goto errexit;
		tpath++;
		/* pull out the LSB of the port */
		portno += (unsigned short) strtoul(tpath, &ttpath, 10);
	} else {
		/* parse epsv command (|||6446|) */
		for (i = 0, tpath = tmp_line + 4; *tpath; tpath++) {
			if (*tpath == '|') {
				i++;
				if (i == 3)
					break;
			}
		}
		if (i < 3)
			goto errexit;
		/* pull out the port */
		portno = (unsigned short) strtoul(tpath + 1, &ttpath, 10);
	}
	
	if (ttpath == NULL) {
		/* didn't get correct response from EPSV/PASV */
		goto errexit;
	}
	
	php_stream_printf(stream TSRMLS_CC, "%s %s\r\n", (mode[0] == 'r' ? "RETR" : "STOR"), (resource->path != NULL ? resource->path : "/"));
	
	/* open the data channel */
	if (hoststart == NULL) {
		hoststart = resource->host;
	}
	datastream = php_stream_sock_open_host(hoststart, portno, SOCK_STREAM, 0, 0);
	if (datastream == NULL) {
		goto errexit;
	}

	result = GET_FTP_RESULT(stream);
	if (result != 150 && result != 125) {
		/* Could not retrieve or send the file 
		 * this data will only be sent to us after connection on the data port was initiated.
		 */
		php_stream_close(datastream);
		datastream = NULL;
		goto errexit;	
	}
		
	php_stream_context_set(datastream, context);
	php_stream_notify_progress_init(context, 0, file_size);

#ifdef HAVE_OPENSSL_EXT
	if (use_ssl_on_data && php_stream_sock_ssl_activate_with_method(datastream, 1, SSLv23_method(), reuseid TSRMLS_CC) == FAILURE)	{
		php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unable to activate SSL mode");
		php_stream_close(datastream);
		datastream = NULL;
		goto errexit;
	}
#endif

	/* remember control stream */	
	datastream->wrapperdata = (zval *)stream;

	php_url_free(resource);
	return datastream;

 errexit:
	php_url_free(resource);
	if (stream) {
		php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
		php_stream_close(stream);
	}
	if (tmp_line[0] != '\0')
		php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "FTP server reports %s", tmp_line);
	return NULL;
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: sw=4 ts=4 fdm=marker
 * vim<600: sw=4 ts=4
 */
