/*
 * Copyright (c) 2006 Kazuaki ODA.
 * Copyright (c) 2006 Mode2 inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the company nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ctr_fetch.c 436 2006-11-11 08:52:58Z oda $
 */

#include <stdlib.h>
#include "apr_network_io.h"
#include "apr_strings.h"
#include "apr_uri.h"
#include "apr_version.h"
#include "ctr_fetch.h"
#include "ctr_util.h"

#define CTR_SOCK_BUF_SZ         4096
#define CTR_SOCK_LINEBUF_INISZ  256
#define CTR_HTTP_BODYBUF_SZ     4096

typedef struct _ctr_socket ctr_socket_t;

struct _ctr_socket {
    apr_socket_t *rawsock;
    char *buf;
    apr_size_t bufsz;
    char *bufp;
    apr_size_t bufleft;
};

static apr_status_t ctr_socket_new(ctr_socket_t **sock, int family, int type,
                                   int protocol, apr_pool_t *pool);
static apr_status_t ctr_socket_timeout_set(ctr_socket_t *sock, int second);
static apr_status_t ctr_socket_connect(ctr_socket_t *sock, apr_sockaddr_t *sa);
static apr_status_t ctr_socket_str_write(ctr_socket_t *sock, const char *s);
static apr_status_t ctr_socket_char_read(ctr_socket_t *sock, char *c);
static apr_status_t ctr_socket_read(ctr_socket_t *sock, char *buf,
                                    apr_size_t *bufsz);
static apr_status_t ctr_socket_line_read(ctr_socket_t *sock, char **s,
                                         apr_pool_t *pool);
static apr_status_t ctr_socket_close(ctr_socket_t *sock);

char *ctr_http_fetch(const char *uristr, const char *uagent, apr_file_t *fd,
                     apr_pool_t *pool)
{
    apr_uri_t uri;
    apr_sockaddr_t *sa;
    ctr_socket_t *sock;
    char *request;
    char *line;
    char *tkn, *last;
    int httpstat;
    char *location;
    char *buf;
    apr_size_t bufsz;
    apr_status_t rv;

    if (apr_uri_parse(pool, uristr, &uri) != APR_SUCCESS)
        return "uri is invalid";

    if (strcmp(uri.scheme, "http") != 0)
        return "uri scheme is not \"http\"";

    if (uri.port == 0)
        uri.port = 80;

    if (apr_sockaddr_info_get(&sa, uri.hostname, APR_UNSPEC, uri.port,
                              APR_IPV4_ADDR_OK, pool) != APR_SUCCESS)
        return "failed to lookup server address";

    if (ctr_socket_new(&sock, sa->family, SOCK_STREAM, 0, pool) != APR_SUCCESS)
        return "failed to create client socket";

    ctr_socket_timeout_set(sock, 5);

    if (ctr_socket_connect(sock, sa) != APR_SUCCESS) {
        ctr_socket_close(sock);
        return "failed to connect to server";
    }

    request = apr_psprintf(pool,
                           "GET %s%s%s HTTP/1.0\r\n"
                           "Host: %s\r\n"
                           "%s%s%s"
                           "\r\n",
                           uri.path,
                           uri.query ? "?" : "",
                           uri.query ? uri.query : "",
                           uri.hostname,
                           uagent ? "User-Agent: " : "",
                           uagent ? uagent : "",
                           uagent ? "\r\n" : "");
    if (ctr_socket_str_write(sock, request) != APR_SUCCESS) {
        ctr_socket_close(sock);
        return "failed to send http request";
    }

    /*
     * Read HTTP status line.
     */
    if (ctr_socket_line_read(sock, &line, pool) != APR_SUCCESS) {
        ctr_socket_close(sock);
        return "failed to get http response status";
    }
    tkn = apr_strtok(line, " ", &last);
    if ((tkn = apr_strtok(NULL, " ", &last)) == NULL) {
        ctr_socket_close(sock);
        return "failed to get http response status";
    }
    httpstat = atoi(tkn);

    if (httpstat != 200 && httpstat != 301 && httpstat != 302) {
        ctr_socket_close(sock);
        return apr_psprintf(pool, "http status is [%d]", httpstat);
    }

    /*
     * Read HTTP headers.
     */
    location = NULL;
    for (;;) {
        if ((rv = ctr_socket_line_read(sock, &line, pool)) != APR_SUCCESS)
            break;

        if (ctr_startwith(line, "Location:")) {
            location = line += 9;       /* 9 is length of "Location:" */
            location = ctr_trim(location);
        } else if (strcmp(line, "\r\n") == 0)
            break;
    }
    if (rv != APR_SUCCESS && rv != APR_EOF) {
        ctr_socket_close(sock);
        return "failed to read http response headers";
    }

    if (httpstat == 301 || httpstat == 302) {
        /*
         * This is a redirection response.
         */
        ctr_socket_close(sock);

        if (location == NULL)
            return "redirection has no location header";

        return ctr_http_fetch(location, uagent, fd, pool);
    }

    /*
     * Read HTTP body and write it to a file.
     */
    buf = apr_palloc(pool, CTR_HTTP_BODYBUF_SZ);
    for (;;) {
        bufsz = CTR_HTTP_BODYBUF_SZ;
        if ((rv = ctr_socket_read(sock, buf, &bufsz)) != APR_SUCCESS)
            break;

        if (apr_file_write(fd, buf, &bufsz) != APR_SUCCESS) {
            ctr_socket_close(sock);
            return "failed to write http response body to file";
        }
    }

    ctr_socket_close(sock);

    if (rv != APR_EOF)
        return "failed to read http response body";

    return NULL;
}

static apr_status_t ctr_socket_new(ctr_socket_t **sock, int family, int type,
                                   int protocol, apr_pool_t *pool)
{
    ctr_socket_t *newsock;
    apr_status_t rv;

    newsock = apr_palloc(pool, sizeof(*newsock));

#if APR_MAJOR_VERSION == 0
    if ((rv = apr_socket_create_ex(&newsock->rawsock, family, type, protocol,
                                   pool)) != APR_SUCCESS)
#else
    if ((rv = apr_socket_create(&newsock->rawsock, family, type, protocol,
                                pool)) != APR_SUCCESS)
#endif
        return rv;

    newsock->bufsz = CTR_SOCK_BUF_SZ;
    newsock->buf = apr_palloc(pool, newsock->bufsz);
    newsock->bufp = newsock->buf;
    newsock->bufleft = 0;

    *sock = newsock;
    return APR_SUCCESS;
}

static apr_status_t ctr_socket_timeout_set(ctr_socket_t *sock, int second)
{
    return apr_socket_timeout_set(sock->rawsock, second * APR_USEC_PER_SEC);
}

static apr_status_t ctr_socket_connect(ctr_socket_t *sock, apr_sockaddr_t *sa)
{
    return apr_socket_connect(sock->rawsock, sa);
}

static apr_status_t ctr_socket_str_write(ctr_socket_t *sock, const char *s)
{
    apr_size_t len;

    len = strlen(s);
    return apr_socket_send(sock->rawsock, s, &len);
}

static apr_status_t ctr_socket_char_read(ctr_socket_t *sock, char *c)
{
    apr_status_t rv;

    if (sock->bufleft == 0) {
        apr_size_t readbytes;

        readbytes = sock->bufsz;
        if ((rv = apr_socket_recv(sock->rawsock, sock->buf,
                                  &readbytes)) != APR_SUCCESS)
            return rv;

        sock->bufleft = readbytes;
        sock->bufp = sock->buf;
    }

    *c = *(sock->bufp++);
    sock->bufleft--;

    return APR_SUCCESS;
}

static apr_status_t ctr_socket_read(ctr_socket_t *sock, char *buf,
                                    apr_size_t *bufsz)
{
    apr_size_t readbytes, bufleft;
    apr_status_t rv;

    readbytes = 0;
    bufleft = *bufsz;
    rv = APR_SUCCESS;

    while (bufleft > 0) {
        if ((rv = ctr_socket_char_read(sock, buf)) != APR_SUCCESS)
            break;

        readbytes++;
        bufleft--;
        buf++;
    }

    if (rv == APR_SUCCESS || (rv == APR_EOF && readbytes > 0)) {
        *bufsz = readbytes;
        return APR_SUCCESS;
    } else
        return rv;
}

static apr_status_t ctr_socket_line_read(ctr_socket_t *sock, char **s,
                                         apr_pool_t *pool)
{
    char c;
    char *buf;
    apr_size_t bufsz;
    char *bufp;
    apr_size_t bufleft;
    apr_status_t rv;

    bufsz = CTR_SOCK_LINEBUF_INISZ;
    buf = apr_palloc(pool, bufsz);
    bufp = buf;
    bufleft = bufsz;

    for (;;) {
        if ((rv = ctr_socket_char_read(sock, &c)) != APR_SUCCESS)
            break;

        if (bufleft <= 1) {
            /* Expand buffer */
            char *newbuf;

            newbuf = apr_palloc(pool, bufsz * 2);
            memcpy(newbuf, buf, bufsz);
            bufp = newbuf + (bufp - buf);
            buf = newbuf;
            bufleft += bufsz;
            bufsz *= 2;
        }

        *bufp++ = c;
        bufleft--;

        if (c == '\n')
            break;
    }

    if (rv == APR_SUCCESS || (rv == APR_EOF && bufp > buf)) {
        *bufp = '\0';
        *s = buf;
        return APR_SUCCESS;
    } else
        return rv;
}

static apr_status_t ctr_socket_close(ctr_socket_t *sock)
{
    return apr_socket_close(sock->rawsock);
}
