#include "system.h"

#if !defined(HAVE_CONFIG_H)
#define HAVE_MACHINE_TYPES_H 1
#define HAVE_ALLOCA_H 1
#define HAVE_NETINET_IN_SYSTM_H 1
#define HAVE_SYS_SOCKET_H 1
#endif

#ifndef __LCLINT__
#if HAVE_MACHINE_TYPES_H
# include <machine/types.h>
#endif
#endif

#if HAVE_NETINET_IN_SYSTM_H
# include <sys/types.h>
# include <netinet/in_systm.h>
#endif

#if ! HAVE_HERRNO
extern int h_errno;
#endif

#include <stdarg.h>

#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <arpa/telnet.h>

#include "rpmlib.h"
#include "rpmio.h"

#if !defined(HAVE_INET_ATON)
int inet_aton(const char *cp, struct in_addr *inp);
#endif

#define TIMEOUT_SECS 60
#define BUFFER_SIZE 4096

#ifndef IPPORT_FTP
# define IPPORT_FTP 21
#endif

#if defined(USE_ALT_DNS) && USE_ALT_DNS 
#include "dns.h"
#endif

#include "url.h"
#include "ftp.h"

static int ftpDebug = 0;
static int ftpTimeoutSecs = TIMEOUT_SECS;
static int httpTimeoutSecs = TIMEOUT_SECS;

static rpmCallbackFunction	urlNotify = NULL;
static void *			urlNotifyData = NULL;
static int			urlNotifyCount = -1;

void urlSetCallback(rpmCallbackFunction notify, void *notifyData, int notifyCount) {
    urlNotify = notify;
    urlNotifyData = notifyData;
    urlNotifyCount = (notifyCount >= 0) ? notifyCount : 4096;
}

static int checkResponse(int fd, int secs, int *ecp, char ** str) {
    static char buf[BUFFER_SIZE + 1];
    int bufLength = 0; 
    fd_set emptySet, readSet;
    char *se, *s;
    struct timeval timeout;
    int bytesRead, rc = 0;
    int doesContinue = 1;
    char errorCode[4];
 
    errorCode[0] = '\0';
    
    do {
	/*
	 * XXX In order to preserve both getFile and getFd methods with
	 * XXX HTTP, the response is read 1 char at a time with breaks on
	 * XXX newlines.
	 */
	do {
	    FD_ZERO(&emptySet);
	    FD_ZERO(&readSet);
	    FD_SET(fd, &readSet);

	    timeout.tv_sec = secs;
	    timeout.tv_usec = 0;
    
	    rc = select(fd + 1, &readSet, &emptySet, &emptySet, &timeout);
	    if (rc < 1) {
		if (rc == 0) 
		    return FTPERR_BAD_SERVER_RESPONSE;
		else
		    rc = FTPERR_UNKNOWN;
	    } else
		rc = 0;

	    s = buf + bufLength;
	    if ((bytesRead = read(fd, s, 1)) != 1)
		return FTPERR_SERVER_IO_ERROR;
	    bufLength += bytesRead;
	    buf[bufLength] = '\0';
	} while (bufLength < sizeof(buf) && *s != '\n');

	/*
	 * Divide the response into lines. Skip continuation lines.
	 */
	s = se = buf;
	while (*se != '\0') {
		while (*se && *se != '\n') se++;

		if (se > s && se[-1] == '\r')
		   se[-1] = '\0';
		if (*se == '\0')
			break;

		/* HTTP header termination on empty line */
		if (*s == '\0') {
			doesContinue = 0;
			break;
		}
		*se++ = '\0';

		/* HTTP: look for "HTTP/1.1 123 ..." */
		if (!strncmp(s, "HTTP", 4)) {
			char *e;
			if ((e = strchr(s, ' ')) != NULL) {
			    e++;
			    if (strchr("0123456789", *e))
				strncpy(errorCode, e, 3);
			    errorCode[3] = '\0';
			}
			s = se;
			continue;
		}

		/* FTP: look for "123-" and/or "123 " */
		if (strchr("0123456789", *s)) {
			if (errorCode[0]) {
			    if (!strncmp(s, errorCode, 3) && s[3] == ' ')
				doesContinue = 0;
			} else {
			    strncpy(errorCode, s, 3);
			    errorCode[3] = '\0';
			    if (s[3] != '-') {
				doesContinue = 0;
			    } 
			}
		}
		s = se;
	}

	if (doesContinue && se > s) {
	    bufLength = se - s - 1;
	    if (s != buf)
		memcpy(buf, s, bufLength);
	} else {
	    bufLength = 0;
	}
    } while (doesContinue && !rc);

if (ftpDebug)
fprintf(stderr, "<- %s\n", buf);

    if (str)	*str = buf;
    if (ecp)	*ecp = atoi(errorCode);

    return rc;
}

static int ftpCheckResponse(urlinfo *u, char ** str) {
    int ec = 0;
    int rc =  checkResponse(u->ftpControl, ftpTimeoutSecs, &ec, str);

    switch (ec) {
    case 550:
	return FTPERR_FILE_NOT_FOUND;
	break;
    case 552:
	return FTPERR_NIC_ABORT_IN_PROGRESS;
	break;
    default:
	if (ec >= 400 && ec <= 599)
	    return FTPERR_BAD_SERVER_RESPONSE;
	break;
    }
    return rc;
}

static int ftpCommand(urlinfo *u, char * command, ...) {
    va_list ap;
    int len;
    char * s;
    char * buf;

    va_start(ap, command);
    len = strlen(command) + 2;
    s = va_arg(ap, char *);
    while (s) {
	len += strlen(s) + 1;
	s = va_arg(ap, char *);
    }
    va_end(ap);

    buf = alloca(len + 1);

    va_start(ap, command);
    strcpy(buf, command);
    strcat(buf, " ");
    s = va_arg(ap, char *);
    while (s) {
	strcat(buf, s);
	strcat(buf, " ");
	s = va_arg(ap, char *);
    }
    va_end(ap);

    buf[len - 2] = '\r';
    buf[len - 1] = '\n';
    buf[len] = '\0';

if (ftpDebug)
fprintf(stderr, "-> %s", buf);
    if (write(u->ftpControl, buf, len) != len) {
        return FTPERR_SERVER_IO_ERROR;
    }

    return ftpCheckResponse(u, NULL);
}

#if !defined(USE_ALT_DNS) || !USE_ALT_DNS 
static int mygethostbyname(const char * host, struct in_addr * address) {
    struct hostent * hostinfo;

    hostinfo = gethostbyname(host);
    if (!hostinfo) return 1;

    memcpy(address, hostinfo->h_addr_list[0], hostinfo->h_length);
    return 0;
}
#endif

static int getHostAddress(const char * host, struct in_addr * address) {
    if (isdigit(host[0])) {
      if (!inet_aton(host, address)) {
	  return FTPERR_BAD_HOST_ADDR;
      }
    } else {
      if (mygethostbyname(host, address)) {
	  errno = h_errno;
	  return FTPERR_BAD_HOSTNAME;
      }
    }
    
    return 0;
}

static int tcpConnect(const char *host, int port)
{
    struct sockaddr_in sin;
    int sock = -1;
    int rc;

    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);
    sin.sin_addr.s_addr = INADDR_ANY;
    
  do {
    if ((rc = getHostAddress(host, &sin.sin_addr)) < 0)
	break;

    if ((sock = socket(sin.sin_family, SOCK_STREAM, IPPROTO_IP)) < 0) {
        rc = FTPERR_FAILED_CONNECT;
	break;
    }

    if (connect(sock, (struct sockaddr *) &sin, sizeof(sin))) {
        rc = FTPERR_FAILED_CONNECT;
	break;
    }
  } while (0);

    if (rc < 0 && sock >= 0) {
	close(sock);
	return rc;
    }

if (ftpDebug)
fprintf(stderr,"++ connect %s:%d on fd %d\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), sock);

    return sock;
}

int httpOpen(urlinfo *u)
{
    int sock;
    const char *host;
    const char *path;
    int port;
    char *buf;
    size_t len;

    if (u == NULL || ((host = (u->proxyh ? u->proxyh : u->host)) == NULL))
	return FTPERR_BAD_HOSTNAME;

    if ((port = (u->proxyp > 0 ? u->proxyp : u->port)) < 0) port = 80;

    path = (u->proxyh || u->proxyp > 0) ? u->url : u->path;
    if ((sock = tcpConnect(host, port)) < 0)
	return sock;

    len = strlen(path) + sizeof("GET  HTTP 1.0\r\n\r\n");
    buf = alloca(len);
    strcpy(buf, "GET ");
    strcat(buf, path);
    strcat(buf, " HTTP 1.0\r\n");
    strcat(buf, "\r\n");

    if (write(sock, buf, len) != len) {
	close(sock);
	return FTPERR_SERVER_IO_ERROR;
    }

if (ftpDebug)
fprintf(stderr, "-> %s", buf);

  { int ec = 0;
    int rc;
    rc = checkResponse(sock, httpTimeoutSecs, &ec, NULL);

    switch (ec) {
    default:
	if (rc == 0 && ec != 200)	/* not HTTP_OK */
	    rc = FTPERR_FILE_NOT_FOUND;
	break;
    }

    if (rc < 0) {
	close(sock);
	return rc;
    }
  }

    return sock;
}

int ftpOpen(urlinfo *u)
{
    const char * host;
    const char * user;
    const char * password;
    int port;
    int rc;

    if (u == NULL || ((host = (u->proxyh ? u->proxyh : u->host)) == NULL))
	return FTPERR_BAD_HOSTNAME;

    if ((port = (u->proxyp > 0 ? u->proxyp : u->port)) < 0) port = IPPORT_FTP;

    if ((user = (u->proxyu ? u->proxyu : u->user)) == NULL)
	user = "anonymous";

    if ((password = u->password) == NULL) {
	if (getuid()) {
	    struct passwd * pw = getpwuid(getuid());
	    char *myp = alloca(strlen(pw->pw_name) + sizeof("@"));
	    strcpy(myp, pw->pw_name);
	    strcat(myp, "@");
	    password = myp;
	} else {
	    password = "root@";
	}
    }

    if ((u->ftpControl = tcpConnect(host, port)) < 0)
	return u->ftpControl;

    /* ftpCheckResponse() assumes the socket is nonblocking */
    if (fcntl(u->ftpControl, F_SETFL, O_NONBLOCK)) {
	close(u->ftpControl);
	u->ftpControl = -1;
        return FTPERR_FAILED_CONNECT;
    }

    if ((rc = ftpCheckResponse(u, NULL))) {
        return rc;     
    }

    if ((rc = ftpCommand(u, "USER", user, NULL))) {
	close(u->ftpControl);
	u->ftpControl = -1;
	return rc;
    }

    if ((rc = ftpCommand(u, "PASS", password, NULL))) {
	close(u->ftpControl);
	u->ftpControl = -1;
	return rc;
    }

    if ((rc = ftpCommand(u, "TYPE", "I", NULL))) {
	close(u->ftpControl);
	u->ftpControl = -1;
	return rc;
    }

    return u->ftpControl;
}

static int copyData(FD_t sfd, FD_t tfd) {
    char buf[BUFFER_SIZE];
    fd_set emptySet, readSet;
    struct timeval timeout;
    int bytesRead;
    int bytesCopied = 0;
    int rc;
    int notifier = -1;

    if (urlNotify) {
	(*urlNotify) (NULL, RPMCALLBACK_INST_OPEN_FILE,
		0, 0, NULL, urlNotifyData);
    }
    
    while (1) {
	FD_ZERO(&emptySet);
	FD_ZERO(&readSet);
	FD_SET(fdFileno(sfd), &readSet);

	timeout.tv_sec = ftpTimeoutSecs;
	timeout.tv_usec = 0;
    
	rc = select(fdFileno(sfd) + 1, &readSet, &emptySet, &emptySet, &timeout);
	if (rc == 0) {
	    rc = FTPERR_SERVER_TIMEOUT;
	    break;
	} else if (rc < 0) {
	    rc = FTPERR_UNKNOWN;
	    break;
	}

	bytesRead = fdRead(sfd, buf, sizeof(buf));
	if (bytesRead == 0) {
	    rc = 0;
	    break;
	}

	if (fdWrite(tfd, buf, bytesRead) != bytesRead) {
	    rc = FTPERR_FILE_IO_ERROR;
	    break;
	}
	
	bytesCopied += bytesRead;
	if (urlNotify && urlNotifyCount > 0) {
	    int n = bytesCopied/urlNotifyCount;
	    if (n != notifier) {
		(*urlNotify) (NULL, RPMCALLBACK_INST_PROGRESS,
			bytesCopied, 0, NULL, urlNotifyData);
		notifier = n;
	    }
	}
    }

if (ftpDebug)
fprintf(stderr, "++ copied %d bytes: %s\n", bytesCopied, ftpStrerror(rc));

    if (urlNotify) {
	(*urlNotify) (NULL, RPMCALLBACK_INST_OPEN_FILE,
		bytesCopied, bytesCopied, NULL, urlNotifyData);
    }
    
    fdClose(sfd);
    return rc;
}

int ftpAbort(FD_t fd) {
    urlinfo *u = (urlinfo *)fd->fd_url;
    char buf[BUFFER_SIZE];
    int rc;
    int tosecs = ftpTimeoutSecs;

if (ftpDebug)
fprintf(stderr, "-> ABOR\n");

    sprintf(buf, "%c%c%c", IAC, IP, IAC);
    send(u->ftpControl, buf, 3, MSG_OOB);
    sprintf(buf, "%cABOR\r\n", DM);
    if (write(u->ftpControl, buf, 7) != 7) {
        return FTPERR_SERVER_IO_ERROR;
    }
    if (fdFileno(fd) >= 0) {
	while(read(fdFileno(fd), buf, sizeof(buf)) > 0)
	    ;
    }

    ftpTimeoutSecs = 10;
    if ((rc = ftpCheckResponse(u, NULL)) == FTPERR_NIC_ABORT_IN_PROGRESS) {
	rc = ftpCheckResponse(u, NULL);
    }
    rc = ftpCheckResponse(u, NULL);
    ftpTimeoutSecs = tosecs;

    if (fdFileno(fd) >= 0)
	fdClose(fd);
    return 0;
}

static int ftpGetFileDone(urlinfo *u) {
    if (u->ftpGetFileDoneNeeded && ftpCheckResponse(u, NULL))
	return FTPERR_BAD_SERVER_RESPONSE;
    u->ftpGetFileDoneNeeded = 0;
    return 0;
}

int ftpGetFileDesc(FD_t fd)
{
    urlinfo *u;
    const char *remotename;
    struct sockaddr_in dataAddress;
    int i, j;
    char * passReply;
    char * chptr;
    char * retrCommand;
    int rc;

    u = (urlinfo *)fd->fd_url;
    remotename = u->path;

/*
 * XXX When ftpGetFileDesc() is called, there may be a lurking
 * XXX transfer complete message (if ftpGetFileDone() was not
 * XXX called to clear that message). Clear that message now.
 */

    if (u->ftpGetFileDoneNeeded)
	rc = ftpGetFileDone(u);

if (ftpDebug)
fprintf(stderr, "-> PASV\n");
    if (write(u->ftpControl, "PASV\r\n", 6) != 6) {
        return FTPERR_SERVER_IO_ERROR;
    }
    if ((rc = ftpCheckResponse(u, &passReply)))
	return FTPERR_PASSIVE_ERROR;

    chptr = passReply;
    while (*chptr && *chptr != '(') chptr++;
    if (*chptr != '(') return FTPERR_PASSIVE_ERROR; 
    chptr++;
    passReply = chptr;
    while (*chptr && *chptr != ')') chptr++;
    if (*chptr != ')') return FTPERR_PASSIVE_ERROR;
    *chptr-- = '\0';

    while (*chptr && *chptr != ',') chptr--;
    if (*chptr != ',') return FTPERR_PASSIVE_ERROR;
    chptr--;
    while (*chptr && *chptr != ',') chptr--;
    if (*chptr != ',') return FTPERR_PASSIVE_ERROR;
    *chptr++ = '\0';
    
    /* now passReply points to the IP portion, and chptr points to the
       port number portion */

    dataAddress.sin_family = AF_INET;
    if (sscanf(chptr, "%d,%d", &i, &j) != 2) {
	return FTPERR_PASSIVE_ERROR;
    }
    dataAddress.sin_port = htons((i << 8) + j);

    chptr = passReply;
    while (*chptr++) {
	if (*chptr == ',') *chptr = '.';
    }

    if (!inet_aton(passReply, &dataAddress.sin_addr)) 
	return FTPERR_PASSIVE_ERROR;

    fd->fd_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (fdFileno(fd) < 0) {
        return FTPERR_FAILED_CONNECT;
    }

    retrCommand = alloca(strlen(remotename) + 20);
    sprintf(retrCommand, "RETR %s\r\n", remotename);
    i = strlen(retrCommand);
   
    while (connect(fdFileno(fd), (struct sockaddr *) &dataAddress, 
	        sizeof(dataAddress)) < 0) {
	if (errno == EINTR)
	    continue;
	close(fd->fd_fd);
	fd->fd_fd = -1;
        return FTPERR_FAILED_DATA_CONNECT;
    }

if (ftpDebug)
fprintf(stderr, "-> %s", retrCommand);
    if (write(u->ftpControl, retrCommand, i) != i) {
        return FTPERR_SERVER_IO_ERROR;
    }

    if ((rc = ftpCheckResponse(u, NULL))) {
	close(fd->fd_fd);
	fd->fd_fd = -1;
	return rc;
    }

    u ->ftpGetFileDoneNeeded = 1;
    return 0;
}

int httpGetFile(FD_t sfd, FD_t tfd) {
    return copyData(sfd, tfd);
}

int ftpGetFile(FD_t sfd, FD_t tfd)
{
    urlinfo *u;
    int rc;

    /* XXX sfd will be freed by copyData -- grab sfd->fd_url now */
    u = (urlinfo *)sfd->fd_url;

    /* XXX normally sfd = ufdOpen(...) and this code does not execute */
    if (fdFileno(sfd) < 0 && (rc = ftpGetFileDesc(sfd)) < 0) {
	fdClose(sfd);
	return rc;
    }

    rc = copyData(sfd, tfd);
    if (rc < 0)
	return rc;

    return ftpGetFileDone(u);
}

int ftpClose(FD_t fd) {
    int fdno = ((urlinfo *)fd->fd_url)->ftpControl;
    if (fdno >= 0)
	close(fdno);
    return 0;
}

const char *ftpStrerror(int errorNumber) {
  switch (errorNumber) {
    case 0:
	return _("Success");

    case FTPERR_BAD_SERVER_RESPONSE:
      return _("Bad server response");

    case FTPERR_SERVER_IO_ERROR:
      return _("Server IO error");

    case FTPERR_SERVER_TIMEOUT:
      return _("Server timeout");

    case FTPERR_BAD_HOST_ADDR:
      return _("Unable to lookup server host address");

    case FTPERR_BAD_HOSTNAME:
      return _("Unable to lookup server host name");

    case FTPERR_FAILED_CONNECT:
      return _("Failed to connect to server");

    case FTPERR_FAILED_DATA_CONNECT:
      return _("Failed to establish data connection to server");

    case FTPERR_FILE_IO_ERROR:
      return _("IO error to local file");

    case FTPERR_PASSIVE_ERROR:
      return _("Error setting remote server to passive mode");

    case FTPERR_FILE_NOT_FOUND:
      return _("File not found on server");

    case FTPERR_NIC_ABORT_IN_PROGRESS:
      return _("Abort in progress");

    case FTPERR_UNKNOWN:
    default:
      return _("Unknown or unexpected error");
  }
}

