/* webclient.c - Connecting to remote http sites
 *
 * Copyright (C) 2005, 2007  Oskar Liljeblad
 *
 * 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; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <config.h>
#include <sys/socket.h>		/* POSIX */
#include <netinet/in.h>		/* POSIX */
#include <netdb.h>		/* POSIX */
#include <inttypes.h>		/* POSIX */
#include <unistd.h>		/* POSIX */
#include <sys/time.h>		/* ? */
#include <time.h>		/* POSIX */
#include <stdio.h>		/* C89 */
#include "gettext.h"            /* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "quote.h"              /* Gnulib */
#include "quotearg.h"           /* Gnulib */
#include "full-write.h"		/* Gnulib */
#include "c-ctype.h"		/* Gnulib */
#include "xvasprintf.h"		/* Gnulib */
#include "xalloc.h"		/* Gnulib */
#include "minmax.h"		/* Gnulib */
#include "getaddrinfo.h"	/* Gnulib */
#include "gmediaserver.h"
#include "hmap.h"
#include "strutil.h"

#define MIN_HTTP_READ 512
#define DEFAULT_LINE_LENGTH 120
#define SKIP_BUF_SIZE 1024

/* Return -1 on error, 0 on premature EOF, 1 otherwise */
ssize_t
http_skip(HTTPResult *result, size_t count)
{
    char skipbuf[SKIP_BUF_SIZE];
    ssize_t res;

    while (count > 0) {
        res = http_read(result, skipbuf, MIN(count, SKIP_BUF_SIZE));
        if (res <= 0)
            return res;
        count -= res;
    }

    return 1;
}

ssize_t
http_read(HTTPResult *result, void *buf, size_t count)
{
    ssize_t copylen;
    ssize_t readlen;

    /* Get all data from local buffer if possible. */
    copylen = result->buftail - result->bufhead;
    if (count <= copylen) {
        memcpy(buf, result->buf + result->bufhead, count);
        result->bufhead += count;
        return count;
    }
    /* Get some data from local buffer if possible. */
    if (copylen > 0) {
        memcpy(buf, result->buf + result->bufhead, copylen);
        result->bufhead = result->buftail = 0;
        count -= copylen;
        buf += copylen;
    }

    /* If we got here, then result->buf is empty. */
    if (count < MIN_HTTP_READ) {
        readlen = read(result->fd, result->buf, MIN_HTTP_READ);
        if (readlen <= 0)
            return copylen == 0 ? readlen : copylen;
        if (count < readlen) {
            memcpy(buf, result->buf, count);
            result->bufhead = count;
            result->buftail = readlen;
            return copylen + count;
        }
        memcpy(buf, result->buf, readlen);
        result->bufhead = result->buftail = 0;
        return copylen + readlen;
    }
    readlen = read(result->fd, buf, count);
    if (readlen <= 0)
        return copylen == 0 ? readlen : copylen;
    return copylen + readlen;
}

static ssize_t
http_get_line(HTTPResult *result, char **line, size_t *linelen)
{
    ssize_t cur_len = 0;

    if (*line == NULL) {
        *linelen = DEFAULT_LINE_LENGTH;
        *line = malloc(*linelen);
        if (*line == NULL)
            return -1;
    }

    for (;;) {
        ssize_t res;
        int ch = 0;

        res = http_read(result, &ch, 1);
        if (res < 0)
            return -1;
        if (res == 0)
            break;
        if (cur_len + 1 >= *linelen) {
            size_t new_len = 2 * (cur_len + 1) + 1;
            char *new_line;

            if (new_len < cur_len) {
                errno = ENOMEM;
                return -1;
            }
            new_line = realloc(*line, new_len);
            if (new_line == NULL)
                return -1;
            *line = new_line;
            *linelen = new_len;
        }

        (*line)[cur_len] = ch;
        cur_len++;
        if (ch == '\n')
            break;
    }

    (*line)[cur_len] = '\0';
    return cur_len;
}

void
http_result_free(HTTPResult *result)
{
    if (result != NULL) {
        if (result->buf != NULL)
            free(result->buf);
        if (result->headers != NULL) {
            hmap_foreach_key(result->headers, free);
            hmap_free(result->headers);
        }
        if (result->fd != -1)
            close(result->fd); /* Ignore errors */
        free(result);
    }
}

HTTPResult *
http_query(const char *method, const char *urlstr, bool keep_open)
{
    struct addrinfo hints;
    struct addrinfo *addrinfo;
    int fd;
    int c;
    size_t linesize;
    ssize_t linelen;
    char *line;
    char service[6]; /* will hold an uint16_t string */
    HTTPResult *result;
    URL *url;

    url = parse_simple_url(urlstr);
    if (url == NULL) {
        warn(_("%s: invalid URL\n"), urlstr);
        return NULL;
    }

    snprintf(service, sizeof(service), "%" PRIu16, url->port); /* " */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    c = getaddrinfo(url->hostname, service, &hints, &addrinfo);
    if (c != 0) {
        warn(_("%s: cannot look up host address: %s\n"), url->hostname, gai_strerror(c));
        free_url(url);
        return NULL;
    }

    /* XXX: try multiple addresses? see
     * http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=getaddrinfo
     * use addrinfo->ai_next.
     */

    fd = socket(addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol);
    if (fd < 0) {
        warn(_("%s: cannot create socket: %s\n"), url->hostname, errstr);
        freeaddrinfo(addrinfo); /* errstr must come above, free may clobber errno */
        free_url(url);
        return NULL;
    }
    freeaddrinfo(addrinfo);

    result = xmalloc(sizeof(HTTPResult));
    result->headers = NULL;
    result->fd = fd;
    result->bufsize = MIN_HTTP_READ;
    result->buf = xmalloc(result->bufsize);

    if (connect(fd, addrinfo->ai_addr, addrinfo->ai_addrlen) < 0) {
        warn(_("%s: cannot connect: %s\n"), url->hostname, errstr);
        http_result_free(result);
        free_url(url);
        return NULL;
    }

    line = xasprintf("%s %s HTTP/1.0\r\nUser-Agent: %s %s\r\n\r\n", method, url->path, PACKAGE, VERSION);
    linelen = strlen(line);
    if (full_write(fd, line, linelen) < linelen) {
        warn(_("%s: cannot send: %s\n"), url->hostname, errstr);
        free(line);
        http_result_free(result);
        free_url(url);
        return NULL;
    }
    free(line);

    result->bufhead = 0;
    result->buftail = 0;

    line = NULL;
    linelen = http_get_line(result, &line, &linesize);
    if (linelen < 0) {
        warn(_("%s: cannot read: %s\n"), url->hostname, errstr);
        free(line);
        http_result_free(result);
        free_url(url);
        return NULL;
    }
    if (linelen == 0) {
        free(line);
        close(fd); /* Ignore errors */
        result->result = -1;
        result->fd = -1;
        free_url(url);
        return result;
    }

    if (strncmp(line, "ICY ", 4) == 0) {
        c = 3;
        if (!c_isdigit(line[c+1])
              || !c_isdigit(line[c+2])
              || !c_isdigit(line[c+3])
              || line[c+4] != ' ') {
            warn(_("%s: invalid response from ICY server\n"), url->hostname);
            free(line);
            http_result_free(result);
            free_url(url);
            return NULL;
        }
        result->icy_server = true;
    } else {
        for (c = 7; c < linelen && c_isdigit(line[c]); c++);
        if (strncmp(line, "HTTP/1.", 7) != 0
              || c >= linelen
              || line[c] != ' '
              || !c_isdigit(line[c+1])
              || !c_isdigit(line[c+2])
              || !c_isdigit(line[c+3])
              || line[c+4] != ' ') {
            warn(_("%s: invalid response from HTTP server\n"), url->hostname);
            free(line);
            http_result_free(result);
            free_url(url);
            return NULL;
        }
        result->icy_server = true;
    }

    result->result = (line[c+1]-'0')*100 + (line[c+2]-'0')*10 + (line[c+3]-'0');
    if (result->result != 200) {
        warn(_("%s: HTTP request failed (code %d)\n"), url->hostname, result->result);
        free(line);
        http_result_free(result);
        free_url(url);
        return NULL;
    }

    result->headers = hmap_new();
    hmap_set_hash_fn(result->headers, (hash_fn_t) strcasehash);
    hmap_set_compare_fn(result->headers, (comparison_fn_t) strcasecmp);
    for (;;) {
        char *value;
        char *key;

        /* if pendlen == NULL, read line, else line = pendline */

        linelen = http_get_line(result, &line, &linesize);
        if (linelen < 0) {
            warn(_("%s: cannot read: %s\n"), url->hostname, errstr);
            free(line);
            http_result_free(result);
            free_url(url);
            return NULL;
        }
        chomp(line, linelen);

        if (line[0] == '\0')
            break; /* end of headers */

        value = strstr(line, ":");
        if (value == NULL /*|| (value[1] != ' ' &&  value[1] != '\t' && value[1] != '\0')*/) {
            warn(_("%s: invalid headers from HTTP/ICY server\n"), url->hostname);
            free(line);
            http_result_free(result);
            free_url(url);
            return NULL;
        }
        
        /* read new line. if starts with SP or HT, then append data. otherwise, put in pendline and continue; */
        
        /*if (value[1] == '\0') {*/
          /* XXX: Handle  LWS=[CRLF] 1*( SP | HT ) */
        /*} else {*/
            key = xstrdup(line);
            key[value - line] = '\0';
            for (value++; c_isspace(*value); value++);
            hmap_put(result->headers, key, key + (value - line));
        /*}*/
    }

    free(line);
    if (!keep_open) {
        close(fd); /* Ignore errors */
        result->fd = -1;
    }
    free_url(url);
    return result;
}

bool 
parse_http_date(const char *s, time_t *timeptr)
{
    struct tm tm;
    char *res;
    time_t t;

    if (strlen(s) < 24)
        return false;

    if (s[3] == ',') {
        res = strptime(s, "%a, %d %b %Y %H:%M:%S GMT", &tm); /* RFC 1123 */
    } else if (s[3] == ' ') {
        res = strptime(s, "%A, %d-%b-%y %H:%M:%S GMT", &tm); /* asctime */
    } else {	
        res = strptime(s, "%a %b %d %H:%M:%S %Y", &tm); /* RFC 850 */
    }

    if (res == NULL || *res != '\0')
        return false;

    tm.tm_zone = "GMT";
    t = mktime(&tm);
    if (t == (time_t) -1)
        return false;

    *timeptr = t;
    return true;
}
