/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "Licence").
 * You may not use this file except in compliance with the Licence.
 *
 * You can obtain a copy of the licence at RISC OS path @.^.LICENCE
 * or http://www.riscosdev.com/lanman98/LICENCE.CDDL
 * See the Licence for the specific language governing permissions
 * and limitations under the Licence.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the Licence file.
 *
 * CDDL HEADER END
 *
 * Portions Copyright 2026 RISC OS Developments
 * SPDX-License-Identifier: CDDL-1.0
 */
/*
 * HTTP/1.1 request framing and response decoding. No automatic redirects:
 * credentials and modifying requests must never migrate to another origin. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include "webhttp.h"
#ifdef WEB_HOST_TEST
#include "webtest.h"
#else
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/tcp.h"
#include "LanMan98BaseLib/thread.h"
#include "kernel.h"
#include "swis.h"
#endif

/* Platform adapter, separately testable from framing. */
#include "webio.h"

static int equal(const char *a, const char *b)
{
    while(*a && *b)
        if(tolower((unsigned char)*a++) != tolower((unsigned char)*b++)) return 0;
    return *a == *b;
}

/* URI syntax is ASCII, regardless of the current alphabet or locale. */
static int ascii_alnum(int c)
{
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
           (c >= '0' && c <= '9');
}

static int starts(const char *s, const char *prefix)
{
    while(*prefix)
        if(tolower((unsigned char)*s++) != (unsigned char)*prefix++) return 0;
    return 1;
}

int WebParseURL(const char *s, web_url *u)
{
    const char *p, *end, *colon;
    unsigned long port;
    int n;
    memset(u, 0, sizeof(*u));
    if(starts(s, "https://")) {
#ifdef WEB_HTTP_ONLY
        Error("This build supports WebDAV over HTTP only");
#else
        u->secure = 1; p = s + 8;
#endif
    }
    else if(starts(s, "http://")) p = s + 7;
    else return 0;
    end = strchr(p, '/');
    if(!end) end = p + strlen(p);
    colon = NULL;
    for(s = p; s < end; s++)
    {
        if(*s == ':') { if(colon) return 0; colon = s; }
        else if(!ascii_alnum((unsigned char)*s) && *s != '.' && *s != '-') return 0;
    }
    n = (int)((colon ? colon : end) - p);
    if(n < 1 || n > 253) return 0;
    memcpy(u->host, p, n);
    u->port = u->secure ? 443 : 80;
    if(colon)
    {
        port = 0;
        if(colon + 1 == end) return 0;
        for(p = colon + 1; p < end; p++)
        {
            if(!isdigit((unsigned char)*p)) return 0;
            port = port * 10 + (unsigned long)(*p - '0');
            if(port > 65535) return 0;
        }
        if(!port) return 0;
        u->port = (int)port;
    }
    if(strlen(end) >= sizeof(u->base)) return 0;
    for(p = end; *p; p++)
    {
        if((unsigned char)*p <= 32 || (unsigned char)*p >= 127 ||
           *p == '#' || *p == '?' || *p == '\\') return 0;
        if(*p == '%')
        {
            if(!p[1] || !p[2] || !isxdigit((unsigned char)p[1]) ||
               !isxdigit((unsigned char)p[2])) return 0;
            p += 2;
        }
    }
    strcpy(u->base, *end ? end : "/");
    n = strlen(u->base);
    while(n > 1 && u->base[n-1] == '/') u->base[--n] = 0;
    return 1;
}

int WebEscapePath(const char *s, char *out, int size)
{
    static const char hex[] = "0123456789ABCDEF";
    int n = 0, c;
    const char *p, *segment;
    /* Dot segments name the current/parent collection in HTTP. They cannot
       represent literal files and must not escape the selected collection. */
    p = s;
    while(*p)
    {
        segment = p;
        while(*p && *p != '/' && *p != '\\') p++;
        if((p-segment == 1 && segment[0] == '.') ||
           (p-segment == 2 && segment[0] == '.' && segment[1] == '.')) return 0;
        if(*p) p++;
    }
    while((c = (unsigned char)*s++) != 0)
    {
        if(c == '\\') c = '/';
        if(ascii_alnum(c) || c == '/' || c == '-' || c == '_' || c == '.' || c == '~')
        {
            if(n + 1 >= size) return 0;
            out[n++] = c;
        }
        else
        {
            if(n + 3 >= size) return 0;
            out[n++] = '%'; out[n++] = hex[c >> 4]; out[n++] = hex[c & 15];
        }
    }
    if(n >= size) return 0;
    out[n] = 0;
    return 1;
}

static char *basic(const char *user, const char *pass)
{
    static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    char *raw, *out;
    size_t n, i, k;
    unsigned int v;
    if(strlen(user) > 1024 || strlen(pass) > 1024 || strchr(user, ':'))
        Error("Invalid WebDAV credentials");
    n = strlen(user) + strlen(pass) + 1;
    raw = Malloc(n + 1);
    sprintf(raw, "%s:%s", user, pass);
    ExceptTry { out = Malloc(((n+2)/3)*4+1); }
    ExceptCatch { memset(raw,0,n); Free(raw); ExceptRethrow(); }
    k = 0;
    for(i = 0; i < n; i += 3)
    {
        v = (unsigned char)raw[i] << 16;
        if(i+1 < n) v |= (unsigned char)raw[i+1] << 8;
        if(i+2 < n) v |= (unsigned char)raw[i+2];
        out[k++] = alphabet[v >> 18]; out[k++] = alphabet[(v >> 12) & 63];
        out[k++] = i+1 < n ? alphabet[(v >> 6) & 63] : '=';
        out[k++] = i+2 < n ? alphabet[v & 63] : '=';
    }
    out[k] = 0;
    memset(raw, 0, n); Free(raw);
    return out;
}

typedef struct {
    web_io io;
    /* Large 64KB network transfer buffer to maximize throughput over TLS */
    unsigned char buffer[65536];
    int pos, end;
    long response_limit;
    /* Set where the response ended for lack of bytes rather than bad
       framing, so a read that never started can be asked for again. */
    int eof;
} reader;

static int byte(reader *r)
{
    if(r->pos == r->end)
    {
        r->end = WebIORead(&r->io, r->buffer, sizeof(r->buffer));
        r->pos = 0;
        if(r->end <= 0) return -1;
    }
    return r->buffer[r->pos++];
}

static int line(reader *r, char *s, int size)
{
    int c, n = 0;
    while((c = byte(r)) != -1)
    {
        if(c == '\r')
        {
            if(byte(r) != '\n') Error("Invalid HTTP line ending");
            s[n] = 0; return n;
        }
        if(c == '\n' || c == 0 || n >= size-1) Error("Invalid or oversized HTTP header");
        s[n++] = c;
    }
    r->eof = 1;
    Error("WebDAV server closed an incomplete response");
    return 0;
}

static unsigned long number(const char *s, int base, const char **end)
{
    unsigned long n = 0;
    int d, count = 0;
    while(*s)
    {
        d = isdigit((unsigned char)*s) ? *s-'0' :
            (base == 16 && isxdigit((unsigned char)*s) ? tolower((unsigned char)*s)-'a'+10 : -1);
        if(d < 0 || d >= base) break;
        if(n > (0x7fffffffUL - d)/base) Error("HTTP length exceeds the filing system limit");
        n = n*base+d; s++; count++;
    }
    if(!count) Error("Missing HTTP length");
    *end = s;
    return n;
}

static void body(reader *r, FILE *out, long n, long *total)
{
    int take;
    if(n < 0 || *total > r->response_limit || n > r->response_limit - *total)
        Error("WebDAV response exceeds the size limit");
    while(n > 0)
    {
        if(r->pos == r->end)
        {
            r->end = WebIORead(&r->io, r->buffer, sizeof(r->buffer));
            r->pos = 0;
            if(r->end <= 0)
            {
                r->eof = 1;
                Error("WebDAV response body was truncated");
            }
        }
        take = r->end-r->pos;
        if(take > n) take = (int)n;
        if(*total > 0x7fffffffL-take) Error("WebDAV file is too large");
        if(out && fwrite(r->buffer+r->pos, 1, take, out) != (size_t)take)
            Error("Unable to save WebDAV response");
        r->pos += take; n -= take; *total += take;
    }
}

static void keep_ahead(web_io *io, reader *r)
{
    int n = r->end - r->pos;
    if(n <= 0) return;
    if(io->ahead) Free(io->ahead);
    io->ahead = Malloc(n);
    memcpy(io->ahead, r->buffer + r->pos, n);
    io->ahead_pos = 0;
    io->ahead_end = n;
    r->pos = r->end;
}

void WebRequest(const web_url *url, const char *user, const char *pass,
                const char *method, const char *path, const char *extra,
                FILE *input, long length, FILE *output, web_reply *reply,
                web_io *keep)
{
    reader *r;
    char *header, *auth, *value, *colon;
    const char *end;
    long content, chunk, sent;
    int n, total, chunked, interim, i, have_etag, attempt, retry;
    int http11, close_conn, framed, reused;
    unsigned char *block;
    if(length < 0 || length > 0x7fffffffL || strlen(path) >= WEB_PATH_MAX || path[0] != '/')
        Error("Invalid WebDAV request");
    for(i = 0; path[i]; i++)
        if((unsigned char)path[i] <= 32 || (unsigned char)path[i] >= 127)
            Error("Unescaped WebDAV path");
    if(strlen(extra) > 8192) Error("WebDAV request headers too large");
    r = Malloc(sizeof(*r)); memset(r, 0, sizeof(*r));
    if(keep) r->io = *keep;
    else WebIOInit(&r->io);
    r->response_limit = (!strcmp(method,"PROPFIND") || !strcmp(method,"PROPPATCH"))
                        ? WEB_XML_MAX : 0x7fffffffL;
    block = r->buffer;
    header = NULL; auth = NULL;
    memset(reply, 0, sizeof(*reply));
    for(attempt = 0; ; attempt++)
    {
    reused = 0;
    ExceptTry
    {
        header = Malloc(WEB_HEADER_MAX);
        if(*user || *pass) auth = basic(user, pass);
        reused = keep && (r->io.tcp || r->io.tls != -1);
        WebIOOpen(&r->io, url);
        if(r->io.forward_proxy && !url->secure)
            n = sprintf(header, "%s http://%s:%d%s HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: LanMan98-WebDAV\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nContent-Length: %ld\r\n%s", method, url->host, url->port, path, url->host, url->port, length, extra);
        else
            n = sprintf(header, "%s %s HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: LanMan98-WebDAV\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nContent-Length: %ld\r\n%s", method, path, url->host, url->port, length, extra);
        if(auth) n += sprintf(header+n, "Authorization: Basic %s\r\n", auth);
        strcpy(header+n, "\r\n"); n += 2;
        WebIOWrite(&r->io, header, n);
        sent = 0;
        while(sent < length)
        {
            n = length-sent > (long)sizeof(r->buffer) ? sizeof(r->buffer) : (int)(length-sent);
            if(!input || fread(block, 1, n, input) != (size_t)n) Error("Unable to read WebDAV upload");
            WebIOWrite(&r->io, block, n); sent += n;
        }
        interim = 0;
        do
        {
            n = line(r, header, WEB_HEADER_MAX);
            http11 = !strncmp(header, "HTTP/1.1 ", 9);
            close_conn = 0;
            if(n < 12 || (strncmp(header,"HTTP/1.1 ",9) && strncmp(header,"HTTP/1.0 ",9)) ||
               !isdigit((unsigned char)header[9]) || !isdigit((unsigned char)header[10]) ||
               !isdigit((unsigned char)header[11]) || (header[12] && header[12] != ' '))
                Error("Invalid HTTP status line");
            reply->status = (header[9]-'0')*100+(header[10]-'0')*10+header[11]-'0';
            reply->etag[0] = 0;
            content = -1; chunked = 0; total = n; have_etag = 0;
            while((n = line(r, header, WEB_HEADER_MAX)) != 0)
            {
                total += n+2;
                if(total > WEB_HEADER_MAX) Error("HTTP headers exceed the limit");
                colon = strchr(header, ':');
                if(!colon || colon == header || header[0] == ' ' || header[0] == '\t')
                    Error("Malformed HTTP header");
                for(i = 0; header+i < colon; i++)
                    if(!((header[i] >= 'A' && header[i] <= 'Z') ||
                         (header[i] >= 'a' && header[i] <= 'z') ||
                         (header[i] >= '0' && header[i] <= '9') ||
                         strchr("!#$%&'*+-.^_`|~",header[i])))
                        Error("Invalid HTTP header name");
                *colon++ = 0; value = colon;
                while(*value == ' ' || *value == '\t') value++;
                n = strlen(value);
                while(n && (value[n-1] == ' ' || value[n-1] == '\t')) value[--n] = 0;
                if(equal(header, "Content-Length"))
                {
                    chunk = number(value, 10, &end);
                    if(*end || (content != -1 && content != chunk)) Error("Conflicting HTTP lengths");
                    content = chunk;
                }
                else if(equal(header, "Transfer-Encoding"))
                {
                    if(chunked || !equal(value, "chunked")) Error("Unsupported HTTP transfer encoding");
                    chunked = 1;
                }
                else if(equal(header, "Connection") && equal(value, "close"))
                    close_conn = 1;
                else if(equal(header, "Content-Encoding") && !equal(value,"identity"))
                    Error("Unexpected compressed WebDAV response");
                else if(equal(header, "ETag"))
                {
                    if(have_etag) Error("Duplicate HTTP ETag");
                    have_etag = 1;
                    if(strlen(value) >= sizeof(reply->etag)) Error("Oversized HTTP ETag");
                    strcpy(reply->etag,value);
                }
            }
            if(++interim > 8) Error("Too many interim HTTP responses");
            if(reply->status == 101) Error("HTTP protocol switching is not supported");
        } while(reply->status < 200);
        if(chunked && content != -1) Error("Ambiguous HTTP response framing");
        if(reply->status != 204 && reply->status != 304 && strcmp(method,"HEAD"))
        {
            if(chunked)
            {
                do
                {
                    line(r, header, WEB_HEADER_MAX);
                    chunk = number(header,16,&end);
                    if(*end && *end != ';') Error("Invalid HTTP chunk size");
                    if(chunk)
                    {
                        body(r,output,chunk,&reply->length);
                        if(byte(r) != '\r' || byte(r) != '\n') Error("Invalid HTTP chunk boundary");
                    }
                } while(chunk);
                total = 0;
                while((n = line(r,header,WEB_HEADER_MAX)) != 0)
                    if((total += n+2) > WEB_HEADER_MAX) Error("Oversized HTTP trailers");
            }
            else if(content >= 0) body(r,output,content,&reply->length);
            else
            {
                while((n = byte(r)) != -1)
                {
                    r->pos--;
                    body(r,output,(long)r->end-r->pos,&reply->length);
                }
            }
        }
        if(output && fflush(output)) Error("Unable to flush WebDAV response");
        framed = chunked || content >= 0 || reply->status == 204 ||
                 reply->status == 304 || !strcmp(method, "HEAD");
        if(!(keep && http11 && !close_conn && framed && !r->eof))
        {
            WebIOClose(&r->io);
            if(keep) WebIOInit(keep);
        }
        else
        {
            keep_ahead(&r->io, r);
            *keep = r->io;
        }
    }
    ExceptCatch
    {
        _kernel_oserror *caught = ExceptCaught();

        WebIssue("HTTP %s %s failed on %s connection (attempt %d): %s",
                 method, path, reused ? "reused" : "new", attempt + 1,
                 caught && caught->errmess[0] ? caught->errmess : "no error given");
        WebIOClose(&r->io);
        if(keep) WebIOInit(keep);
        if(auth) { memset(auth,0,strlen(auth)); Free(auth); auth = NULL; }
        if(header) { Free(header); header = NULL; }
        /* A kept connection the server has already dropped fails on the
           write, with no reply. The request, including a PUT, is sent
           once more on a new connection. A read that dies before any
           reply is repeated only for PROPFIND, GET and HEAD. */
        retry = (attempt == 0) && reply->length == 0 && reply->status == 0 &&
                (!input || !fseek(input, 0, SEEK_SET)) &&
                (!output || !fseek(output, 0, SEEK_SET)) &&
                (reused ||
                 (r->eof && (!strcmp(method,"PROPFIND") ||
                             !strcmp(method,"GET") || !strcmp(method,"HEAD"))));
        if(!retry) { Free(r); ExceptRethrow(); }
        WebIOInit(&r->io);
        r->pos = 0; r->end = 0; r->eof = 0;
        memset(reply, 0, sizeof(*reply));
        continue;
    }
    break;
    }
    if(auth) { memset(auth,0,strlen(auth)); Free(auth); }
    Free(header); Free(r);
}
