/* SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-2.0-only
**  Copyright (c) 1996,1997,1998 Ralf S. Engelschall <rse@engelschall.com>
*/

#include "eperl.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define min(a, b) ((a) < (b) ? (a) : (b))


/*
**  check if the line is a valid HTTP header line
*/
static bool HTTP_IsHeaderLine(const char *beg, size_t len)
{
    char *colon = memchr(beg, ':', len);
    if (!colon)
        return false;
    size_t validlen = strspn(beg, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_");
    return (size_t)(colon - beg) == validlen;
}

/*
**  check if there a particular HTTP headerline exists
*/
static bool HTTP_HeaderLineExists(const char *cpBuf, const char *name)
{
    size_t n = strlen(name);
    const char *cp2 = NULL, *cp2a;
    if ((cp2a = strstr(cpBuf, "\n\n")) != NULL)
        cp2 = cp2a;
    if ((cp2a = strstr(cpBuf, "\r\n\r\n")) != NULL && (cp2 == NULL || cp2a < cp2))
        cp2 = cp2a;
    if (cp2 != NULL) {
        for (const char *cp1 = cpBuf; cp1 < cp2-1; ) {
            const char *cp3 = strchr(cp1, '\n');
            if ((size_t)(cp3-cp1) > n+1 && HTTP_IsHeaderLine(cp1, cp3-cp1))
                if (strncasecmp(cp1, name, n) == 0)
                    return true;
            cp1 = cp3+1;
        }
        return false;
    }
    return false;
}

/*
**  print a standard HTTP reponse of header lines
*/
size_t HTTP_PrintResponseHeaders(const char *cpBuf)
{
    size_t skip = 0;
    const char *statend;
    if (  (!strncmp(cpBuf, "HTTP/1.0 ", strlen("HTTP/1.0 "))
        || !strncmp(cpBuf, "HTTP/1.1 ", strlen("HTTP/1.1 ")))
        && (cpBuf[ 9] >= '1' && cpBuf[ 9] <= '5')
        && (cpBuf[10] >= '0' && cpBuf[10] <= '9')
        && (cpBuf[11] >= '0' && cpBuf[11] <= '9')
        && (cpBuf[12] == ' ')
        && ((statend = strchr(cpBuf + 12, '\n')) != NULL)) {
        /* found HTTP status code */
        printf("%.*s\r\n", (int)(statend - cpBuf - (*(statend - 1) == '\r')), cpBuf);
        skip = statend - cpBuf + 1;
    } else {
        /* no HTTP status code */
        printf("%s 200 OK\r\n", getenv("SERVER_PROTOCOL") ?: "HTTP/1.0");
    }

    if (!HTTP_HeaderLineExists(cpBuf, "Server"))
        printf("Server: %s ePerl/%s Perl/%s\r\n", getenv("SERVER_SOFTWARE") ?: "unknown-server/0.0", EPERL_VERSION_SHORT, AC_perl_vers);

    if (!HTTP_HeaderLineExists(cpBuf, "Date"))
        printf("Date: %.24s\r\n", ctime(&(time_t){time(NULL)}));

    if (!HTTP_HeaderLineExists(cpBuf, "Connection"))
        printf("Connection: close\r\n");

    return skip;
}

/*
**  check if there is a valid HTTP header
*/
bool HTTP_HeadersExists(const char *cpBuf)
{
    const char *cp2 = NULL, *cp2a;
    if ((cp2a = strstr(cpBuf, "\n\n")) != NULL)
        cp2 = cp2a;
    if ((cp2a = strstr(cpBuf, "\r\n\r\n")) != NULL && (cp2 == NULL || cp2a < cp2))
        cp2 = cp2a;
    if (cp2 != NULL) {
        for (const char *cp1 = cpBuf; cp1 < cp2-1; ) {
            const char *cp3 = strchr(cp1, '\n');
            if (!HTTP_IsHeaderLine(cp1, cp3 - cp1))
                return false;
            cp1 = cp3+1;
        }
        return true;
    }
    return false;
}


/*
**  extracts the host name from an url
*/
struct slice { char *data; size_t len; };
static struct slice HTTP_HostOfURL(char *url)
{
    char *cps = strstr(url, "//") + 2, *cpe;
    for (cpe = cps; *cpe != '/' && *cpe != ':' && *cpe != '\0'; cpe++)
        ;
    return (struct slice){ .data = cps, .len = cpe-cps };
}

/*
**  extracts the port from an url
*/
static struct slice HTTP_PortOfURL(char *url)
{
    char *cps = strstr(url, "//") + 2, *cpe;
    for ( ; *cps != '/' && *cps != ':' && *cps != '\0'; cps++)
        ;
    if (*cps == ':') {
        cps++;
        for (cpe = cps; *cpe != '/' && *cpe != '\0'; cpe++)
            ;
        return (struct slice){ .data = cps, .len = cpe-cps };
    }
    else
        return (struct slice){ .data = "80", .len = 2 };
}

/*
**  extracts path from an url
*/
static const char *HTTP_FileOfURL(const char *url)
{
    return (strchr(strstr(url, "//") + 2, '/') ?: "/") + 1;
}

/*
**  open an URL as a file descriptor
*/
FILE *HTTP_openURLasFP(const char *url_arg)
{
    struct addrinfo *ai;
    char buf[8 * 1024];
    struct slice host, port;
    const char *file;
    char *cp;
    int s;

    char *url = (char *)url_arg;
    bool url_rw = false;
redirected:
    host = HTTP_HostOfURL(url);
    port = HTTP_PortOfURL(url);
    file = HTTP_FileOfURL(url);

    /* get the host name */
    if (host.data[host.len]) {
        if (!url_rw) {
            host.len = min(sizeof(buf) - 128, host.len);
            host.data = memcpy(buf, host.data, host.len);
        }
        host.data[host.len] = '\0';
    }
    if (port.data[port.len]) {
        if (!url_rw) {
            port.len = min(126, port.len);
            port.data = memcpy(buf + sizeof(buf) - 127, port.data, port.len);
        }
        port.data[port.len] = '\0';
    }
    if (getaddrinfo(host.data, port.data, NULL, &ai) != 0)
        return NULL;
    if ((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
        freeaddrinfo(ai);
        return NULL;
    }
    if (connect(s, ai->ai_addr, ai->ai_addrlen) == -1) {
        freeaddrinfo(ai);
        close(s);
        return NULL;
    }
    freeaddrinfo(ai);

    /* send HTTP/1.0 request */
    FILE *fp = fdopen(s, "r+");
    fprintf(fp,
        "GET /%s HTTP/1.0\r\n"
        "Host: %s:%s\r\n"
        "User-Agent: ePerl/%s\r\n"
        "\r\n",
        file, host.data, port.data, EPERL_VERSION_SHORT);
    fflush(fp);

    /* read the HTTP response line and check for 200 OK response */
    if (fgets(buf, sizeof(buf), fp) == NULL) {
    fclose:
        fclose(fp);
        return NULL;
    }
    if (strncmp(buf, "HTTP/1.", 7) || (buf[7] != '0' && buf[7] != '1'))
        goto fclose;
    for (cp = buf+8; *cp == ' ' || *cp == '\t'; cp++)
        ;
    if (strncmp(cp, "200", 3 /* OK */) != 0) {
        if (strncmp(cp, "301", 3 /* MOVED PERMANENTLY */) != 0 ||
            strncmp(cp, "302", 3 /* MOVED TEMPORARILY */) != 0   ) {
            /* we try to determine the new URL from
               the HTTP header 'Location' and restart from
               the beginning if an URL is found */
            while (fgets(buf, sizeof(buf), fp) != NULL) {
                if ((*buf == '\n' && *(buf+1) == '\0') ||
                    (*buf == '\n' && *(buf+1) == '\r' && *(buf+2) == '\0') ||
                    (*buf == '\r' && *(buf+1) == '\n' && *(buf+2) == '\0'))
                    break;
                if (strncasecmp(buf, "Location:", 9) == 0) {
                    char *newurl = buf + 9;
                    newurl += strspn(newurl, " \t");
                    newurl[strcspn(newurl, " \t\r\n")] = '\0';
                    fclose(fp);
                    if (strncmp(newurl, "http://", 7))
                        return NULL;
                    url    = newurl;
                    url_rw = true;
                    goto redirected;
                }
            }
            return NULL;
        }
        goto fclose;
    }

    /* now read until a blank line, i.e. skip HTTP headers */
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        if ((*buf == '\n' && *(buf+1) == '\0') ||
            (*buf == '\n' && *(buf+1) == '\r' && *(buf+2) == '\0') ||
            (*buf == '\r' && *(buf+1) == '\n' && *(buf+2) == '\0'))
            break;
    }

    /* return the (still open) FILE pointer */
    return fp;
}
