/* ----------------------------------------------------------------------- *
 *   
 *   Copyright 2009-2011 Intel Corporation; author: H. Peter Anvin
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 *   Boston MA 02110-1301, USA; either version 2 of the License, or
 *   (at your option) any later version; incorporated herein by reference.
 *
 * ----------------------------------------------------------------------- */

/*
 * ftp.c
 */
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <minmax.h>
#include <sys/cpu.h>
#include <netinet/in.h>
#include <lwip/api.h>
#include "core.h"
#include "fs.h"
#include "pxe.h"
#include "thread.h"
#include "url.h"

static int ftp_cmd_response(struct inode *inode, const char *cmd,
			    const char *cmd_arg,
			    uint8_t *pasv_data, int *pn_ptr)
{
    struct pxe_pvt_inode *socket = PVT(inode);
    int c;
    int pos, code;
    int pb, pn;
    bool ps;
    bool first_line, done;
    err_t err;
    char cmd_buf[4096];
    int cmd_len;
    const char *p;
    char *q;

    if (cmd) {
	cmd_len = strlcpy(cmd_buf, cmd, sizeof cmd_buf);
	if (cmd_len >= sizeof cmd_buf - 3)
	    return -1;
	q = cmd_buf + cmd_len;

	if (cmd_arg) {
	    p = cmd_arg;

	    *q++ = ' ';
	    cmd_len++;
	    while (*p) {
		if (++cmd_len < sizeof cmd_buf) *q++ = *p;
		if (*p == '\r')
		    if (++cmd_len < sizeof cmd_buf) *q++ = '\0';
		p++;
	    }

	    if (cmd_len >= sizeof cmd_buf - 2)
		return -1;
	}

	*q++ = '\r';
	*q++ = '\n';
	cmd_len += 2;

	err = netconn_write(socket->net.lwip.conn, cmd_buf, cmd_len, NETCONN_COPY);
	if (err)
	    return -1;
    }

    pos = code = pn = pb = 0;
    ps = false;
    first_line = true;
    done = false;

    while ((c = pxe_getc(inode)) >= 0) {
	if (c == '\n') {
	    if (done) {
		if (pn) {
		    pn += ps;
		    if (pn_ptr)
			*pn_ptr = pn;
		}
		return code;
	    }
	    pos = code = 0;
	    first_line = false;
	    continue;
	}

	switch (pos++) {
	case 0:
	case 1:
	case 2:
	    if (c < '0' || c > '9') {
		if (first_line)
		    return -1;
		else
		    pos = 4;	/* Skip this line */
	    } else {
		code = (code*10) + (c - '0');
	    }
	    break;

	case 3:
	    pn = pb = 0;
	    ps = false;
	    if (c == ' ')
		done = true;
	    else if (c == '-')
		done = false;
	    else if (first_line)
		return -1;
	    else
		done = false;
	    break;

	default:
	    if (pasv_data) {
		if (c >= '0' && c <= '9') {
		    pb = (pb*10) + (c-'0');
		    if (pn < 6)
			pasv_data[pn] = pb;
		    ps = true;
		} else if (c == ',') {
		    pn++;
		    pb = 0;
		    ps = false;
		} else if (pn) {
		    pn += ps;
		    if (pn_ptr)
			*pn_ptr = pn;
		    pn = pb = 0;
		    ps = false;
		}
	    }
	    break;
	}
    }

    return -1;
}

static void ftp_free(struct inode *inode)
{
    struct pxe_pvt_inode *socket = PVT(inode);

    if (socket->ctl) {
	tcp_close_file(socket->ctl);
	free_socket(socket->ctl);
	socket->ctl = NULL;
    }
    tcp_close_file(inode);
}

static void ftp_close_file(struct inode *inode)
{
    struct pxe_pvt_inode *socket  = PVT(inode);
    struct pxe_pvt_inode *ctlsock;
    int resp;

    ctlsock = socket->ctl ? PVT(socket->ctl) : NULL;
    if (ctlsock->net.lwip.conn) {
	resp = ftp_cmd_response(socket->ctl, "QUIT", NULL, NULL, NULL);
	while (resp == 226) {
	    resp = ftp_cmd_response(socket->ctl, NULL, NULL, NULL, NULL);
	}
    }
    ftp_free(inode);
}

static const struct pxe_conn_ops ftp_conn_ops = {
    .fill_buffer	= tcp_fill_buffer,
    .close		= ftp_close_file,
    .readdir		= ftp_readdir,
};

void ftp_open(struct url_info *url, int flags, struct inode *inode,
	      const char **redir)
{
    struct pxe_pvt_inode *socket = PVT(inode);
    struct pxe_pvt_inode *ctlsock;
    struct ip_addr addr;
    uint8_t pasv_data[6];
    int pasv_bytes;
    int resp;
    err_t err;

    (void)redir;		/* FTP does not redirect */

    inode->size = 0;

    if (!url->port)
	url->port = 21;

    url_unescape(url->path, 0);

    socket->ops = &ftp_conn_ops;

    /* Set up the control connection */
    socket->ctl = alloc_inode(inode->fs, 0, sizeof(struct pxe_pvt_inode));
    if (!socket->ctl)
	return;
    ctlsock = PVT(socket->ctl);
    ctlsock->ops = &tcp_conn_ops; /* The control connection is just TCP */
    ctlsock->net.lwip.conn = netconn_new(NETCONN_TCP);
    if (!ctlsock->net.lwip.conn)
	goto err_free;
    addr.addr = url->ip;
    err = netconn_connect(ctlsock->net.lwip.conn, &addr, url->port);
    if (err)
	goto err_delete;

    do {
	resp = ftp_cmd_response(socket->ctl, NULL, NULL, NULL, NULL);
    } while (resp == 120);
    if (resp != 220)
	goto err_disconnect;

    if (!url->user)
	url->user = "anonymous";
    if (!url->passwd)
	url->passwd = "syslinux@";

    resp = ftp_cmd_response(socket->ctl, "USER", url->user, NULL, NULL);
    if (resp != 202 && resp != 230) {
	if (resp != 331)
	    goto err_disconnect;

	resp = ftp_cmd_response(socket->ctl, "PASS", url->passwd, NULL, NULL);
	if (resp != 230)
	    goto err_disconnect;
    }

    if (!(flags & O_DIRECTORY)) {
	resp = ftp_cmd_response(socket->ctl, "TYPE", "I", NULL, NULL);
	if (resp != 200)
	    goto err_disconnect;
    }

    resp = ftp_cmd_response(socket->ctl, "PASV", NULL, pasv_data, &pasv_bytes);
    if (resp != 227 || pasv_bytes != 6)
	goto err_disconnect;

    socket->net.lwip.conn = netconn_new(NETCONN_TCP);
    if (!socket->net.lwip.conn)
	goto err_disconnect;
    err = netconn_connect(socket->net.lwip.conn, (struct ip_addr *)&pasv_data[0],
			  ntohs(*(uint16_t *)&pasv_data[4]));
    if (err)
	goto err_disconnect;

    resp = ftp_cmd_response(socket->ctl,
			    (flags & O_DIRECTORY) ? "LIST" : "RETR",
			    url->path, NULL, NULL);
    if (resp != 125 && resp != 150)
	goto err_disconnect;

    inode->size = -1;
    return;			/* Sucess! */

err_disconnect:
    if (ctlsock->net.lwip.conn)
	netconn_write(ctlsock->net.lwip.conn, "QUIT\r\n", 6, NETCONN_NOCOPY);
    if (socket->net.lwip.conn)
	netconn_delete(socket->net.lwip.conn);
    if (ctlsock->net.lwip.buf)
	netbuf_delete(ctlsock->net.lwip.buf);
err_delete:
    if (ctlsock->net.lwip.conn)
	netconn_delete(ctlsock->net.lwip.conn);
err_free:
    free_socket(socket->ctl);
}
