#include <syslinux/sysappend.h>
#include <ctype.h>
#include <lwip/api.h>
#include "pxe.h"
#include "version.h"
#include "url.h"
#include "net.h"

#define HTTP_PORT	80

static bool is_tspecial(int ch)
{
    bool tspecial = false;
    switch(ch) {
    case '(':  case ')':  case '<':  case '>':  case '@':
    case ',':  case ';':  case ':':  case '\\': case '"':
    case '/':  case '[':  case ']':  case '?':  case '=':
    case '{':  case '}':  case ' ':  case '\t':
	tspecial = true;
	break;
    }
    return tspecial;
}

static bool is_ctl(int ch)
{
    return ch < 0x20;
}

static bool is_token(int ch)
{
    /* Can by antying except a ctl character or a tspecial */
    return !is_ctl(ch) && !is_tspecial(ch);
}

static bool append_ch(char *str, size_t size, size_t *pos, int ch)
{
    bool success = true;
    if ((*pos + 1) >= size) {
	*pos = 0;
	success = false;
    } else {
	str[*pos] = ch;
	str[*pos + 1] = '\0';
	*pos += 1;
    }
    return success;
}

static size_t cookie_len, header_len;
static char *cookie_buf, *header_buf;

__export uint32_t SendCookies = -1UL; /* Send all cookies */

static size_t http_do_bake_cookies(char *q)
{
    static const char uchexchar[16] = "0123456789ABCDEF";
    int i;
    size_t n = 0;
    const char *p;
    char c;
    bool first = true;
    uint32_t mask = SendCookies;

    for (i = 0; i < SYSAPPEND_MAX; i++) {
	if ((mask & 1) && (p = sysappend_strings[i])) {
	    if (first) {
		if (q) {
		    strcpy(q, "Cookie: ");
		    q += 8;
		}
		n += 8;
		first = false;
	    }
	    if (q) {
		strcpy(q, "_Syslinux_");
		q += 10;
	    }
	    n += 10;
	    /* Copy string up to and including '=' */
	    do {
		c = *p++;
		if (q)
		    *q++ = c;
		n++;
	    } while (c != '=');
	    while ((c = *p++)) {
		if (c == ' ') {
		    if (q)
			*q++ = '+';
		    n++;
		} else if (is_token(c)) {
		    if (q)
			*q++ = c;
		    n++;
		} else {
		    if (q) {
			*q++ = '%';
			*q++ = uchexchar[c >> 4];
			*q++ = uchexchar[c & 15];
		    }
		    n += 3;
		}
	    }
	    if (q)
		*q++ = ';';
	    n++;
	}
	mask >>= 1;
    }
    if (!first) {
	if (q) {
	    *q++ = '\r';
	    *q++ = '\n';
	}
	n += 2;
    }
    if (q)
	*q = '\0';
    
    return n;
}

void http_bake_cookies(void)
{
    if (cookie_buf)
	free(cookie_buf);

    cookie_len = http_do_bake_cookies(NULL);
    cookie_buf = malloc(cookie_len+1);
    if (!cookie_buf) {
	cookie_len = 0;
	return;
    }

    if (header_buf)
	free(header_buf);

    header_len = cookie_len + 6*FILENAME_MAX + 256;
    header_buf = malloc(header_len);
    if (!header_buf) {
	header_len = 0;
	return;			/* Uh-oh... */
    }

    http_do_bake_cookies(cookie_buf);
}

static const struct pxe_conn_ops http_conn_ops = {
    .fill_buffer	= core_tcp_fill_buffer,
    .close		= core_tcp_close_file,
    .readdir		= http_readdir,
};

void http_open(struct url_info *url, int flags, struct inode *inode,
	       const char **redir)
{
    struct pxe_pvt_inode *socket = PVT(inode);
    int header_bytes;
    const char *next;
    char field_name[20];
    char field_value[1024];
    size_t field_name_len, field_value_len;
    enum state {
	st_httpver,
	st_stcode,
	st_skipline,
	st_fieldfirst,
	st_fieldname,
	st_fieldvalue,
	st_skip_fieldname,
	st_skip_fieldvalue,
	st_eoh,
    } state;
    static char location[FILENAME_MAX];
    uint32_t content_length; /* same as inode->size */
    size_t response_size;
    int status;
    int pos;
    int err;

    (void)flags;

    if (!header_buf)
	return;			/* http is broken... */

    /* This is a straightforward TCP connection after headers */
    socket->ops = &http_conn_ops;

    /* Reset all of the variables */
    inode->size = content_length = -1;

    /* Start the http connection */
    err = core_tcp_open(socket);
    if (err)
        return;

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

    err = core_tcp_connect(socket, url->ip, url->port);
    if (err)
	goto fail;

    strcpy(header_buf, "GET /");
    header_bytes = 5;
    header_bytes += url_escape_unsafe(header_buf+5, url->path,
				      header_len - 5);
    if (header_bytes >= header_len)
	goto fail;		/* Buffer overflow */
    header_bytes += snprintf(header_buf + header_bytes,
			     header_len - header_bytes,
			     " HTTP/1.0\r\n"
			     "Host: %s\r\n"
			     "User-Agent: Syslinux/" VERSION_STR "\r\n"
			     "Connection: close\r\n"
			     "%s"
			     "\r\n",
			     url->host, cookie_buf ? cookie_buf : "");
    if (header_bytes >= header_len)
	goto fail;		/* Buffer overflow */

    err = core_tcp_write(socket, header_buf, header_bytes, false);
    if (err)
	goto fail;

    /* Parse the HTTP header */
    state = st_httpver;
    pos = 0;
    status = 0;
    response_size = 0;
    field_value_len = 0;
    field_name_len = 0;

    while (state != st_eoh) {
	int ch = pxe_getc(inode);
	/* Eof before I finish paring the header */
	if (ch == -1)
	    goto fail;
#if 0
        printf("%c", ch);
#endif
	response_size++;
	if (ch == '\r' || ch == '\0')
	    continue;
	switch (state) {
	case st_httpver:
	    if (ch == ' ') {
		state = st_stcode;
		pos = 0;
	    }
	    break;

	case st_stcode:
	    if (ch < '0' || ch > '9')
	       goto fail;
	    status = (status*10) + (ch - '0');
	    if (++pos == 3)
		state = st_skipline;
	    break;

	case st_skipline:
	    if (ch == '\n')
		state = st_fieldfirst;
	    break;

	case st_fieldfirst:
	    if (ch == '\n')
		state = st_eoh;
	    else if (isspace(ch)) {
		/* A continuation line */
		state = st_fieldvalue;
		goto fieldvalue;
	    }
	    else if (is_token(ch)) {
		/* Process the previous field before starting on the next one */
		if (strcasecmp(field_name, "Content-Length") == 0) {
		    next = field_value;
		    /* Skip leading whitespace */
		    while (isspace(*next))
			next++;
		    content_length = 0;
		    for (;(*next >= '0' && *next <= '9'); next++) {
			if ((content_length * 10) < content_length)
			    break;
			content_length = (content_length * 10) + (*next - '0');
		    }
		    /* In the case of overflow or other error ignore
		     * Content-Length.
		     */
		    if (*next)
			content_length = -1;
		}
		else if (strcasecmp(field_name, "Location") == 0) {
		    next = field_value;
		    /* Skip leading whitespace */
		    while (isspace(*next))
			next++;
		    strlcpy(location, next, sizeof location);
		}
		/* Start the field name and field value afress */
		field_name_len = 1;
		field_name[0] = ch;
		field_name[1] = '\0';
		field_value_len = 0;
		field_value[0] = '\0';
		state = st_fieldname;
	    }
	    else /* Bogus try to recover */
		state = st_skipline;
	    break;

	case st_fieldname:
	    if (ch == ':' ) {
		state = st_fieldvalue;
	    }
	    else if (is_token(ch)) {
		if (!append_ch(field_name, sizeof field_name, &field_name_len, ch))
		    state = st_skip_fieldname;
	    }
	    /* Bogus cases try to recover */
	    else if (ch == '\n')
		state = st_fieldfirst;
	    else
		state = st_skipline;
	    break;

	 case st_fieldvalue:
	    if (ch == '\n')
		state = st_fieldfirst;
	    else {
	    fieldvalue:
		if (!append_ch(field_value, sizeof field_value, &field_value_len, ch))
		    state = st_skip_fieldvalue;
	    }
	    break;

	/* For valid fields whose names are longer than I choose to support. */
	case st_skip_fieldname:
	    if (ch == ':')
		state = st_skip_fieldvalue;
	    else if (is_token(ch))
		state = st_skip_fieldname;
	    /* Bogus cases try to recover */
	    else if (ch == '\n')
		state = st_fieldfirst;
	    else
		state = st_skipline;
	    break;

	/* For valid fields whose bodies are longer than I choose to support. */
	case st_skip_fieldvalue:
	    if (ch == '\n')
		state = st_fieldfirst;
	    break;

	case st_eoh:
	   break; /* Should never happen */
	}
    }

    if (state != st_eoh)
	status = 0;

    switch (status) {
    case 200:
	/*
	 * All OK, need to mark header data consumed and set up a file
	 * structure...
	 */
	/* Treat the remainder of the bytes as data */
	socket->tftp_filepos -= response_size;
	break;
    case 301:
    case 302:
    case 303:
    case 307:
	/* A redirect */
	if (!location[0])
	    goto fail;
	*redir = location;
	goto fail;
    default:
	goto fail;
	break;
    }
    return;
fail:
    inode->size = 0;
    core_tcp_close_file(inode);
    return;
}
