diff --git a/.gitignore b/.gitignore index e5f91ea..832be35 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ example_large examples/example .cluster/ vsr_boot_marker +*.pem +*.exe +.claude/ \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index b3ed195..0000000 --- a/Makefile +++ /dev/null @@ -1,81 +0,0 @@ -CC ?= gcc -CFLAGS ?= -Wall -Wextra -ggdb -O0 -AR ?= ar - -INCLUDES = -Iquakey/include -Iinclude -I. - -# ---- Library (client-side only) ---- - -LIB_SRCS = src/client.c src/tcp.c src/byte_queue.c src/message.c src/basic.c -LIB_OBJS = $(LIB_SRCS:.c=.o) - -STATIC_LIB = libtoastyfs.a -SHARED_LIB = libtoastyfs.so - -# ---- Server binary ---- - -SERVER_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ - src/tcp.c src/server.c src/main.c src/log.c src/client_table.c \ - src/chunk_store.c src/metadata.c - -# ---- Client binary (random test client) ---- - -CLIENT_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ - src/tcp.c src/server.c src/client.c src/random_client.c src/main.c \ - src/log.c src/client_table.c src/chunk_store.c src/metadata.c - -# ---- Simulation binary ---- - -SIM_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ - src/tcp.c src/server.c src/client.c src/random_client.c src/main.c \ - src/log.c src/client_table.c src/invariant_checker.c src/chunk_store.c \ - src/metadata.c quakey/src/mockfs.c quakey/src/quakey.c - -# ---- Default target ---- - -all: $(STATIC_LIB) $(SHARED_LIB) toastyfs toastyfs_client toastyfs_simulation - -# ---- Library targets ---- - -lib: $(STATIC_LIB) $(SHARED_LIB) - -$(STATIC_LIB): $(LIB_OBJS) - $(AR) rcs $@ $^ - -$(SHARED_LIB): $(LIB_SRCS) - $(CC) $(CFLAGS) -shared -fPIC $(INCLUDES) $^ -o $@ - -src/%.o: src/%.c - $(CC) $(CFLAGS) $(INCLUDES) -fPIC -c $< -o $@ - -# ---- Binary targets ---- - -toastyfs: $(SERVER_SRCS) - $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_SERVER $^ -o $@ - -toastyfs_client: $(CLIENT_SRCS) - $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_CLIENT $^ -o $@ - -toastyfs_simulation: $(SIM_SRCS) - $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_SIMULATION -DFAULT_INJECTION $^ -o $@ - -# ---- Install ---- - -PREFIX ?= /usr/local -LIBDIR ?= $(PREFIX)/lib -INCLUDEDIR?= $(PREFIX)/include - -install: $(STATIC_LIB) $(SHARED_LIB) - install -d $(DESTDIR)$(LIBDIR) - install -d $(DESTDIR)$(INCLUDEDIR) - install -m 644 $(STATIC_LIB) $(DESTDIR)$(LIBDIR)/ - install -m 755 $(SHARED_LIB) $(DESTDIR)$(LIBDIR)/ - install -m 644 include/toastyfs.h $(DESTDIR)$(INCLUDEDIR)/ - -# ---- Clean ---- - -clean: - rm -f $(LIB_OBJS) $(STATIC_LIB) $(SHARED_LIB) - rm -f toastyfs toastyfs_client toastyfs_simulation - -.PHONY: all lib clean install diff --git a/README.md b/README.md deleted file mode 100644 index 99fd286..0000000 --- a/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# ToastyFS - -ToastyFS is a distributed and fault-tolerant object storage. - -WARNING: I'm still finishing up :) Log compaction and persistence are missing. - -## Getting Started - -First, build ToastyFS by running the build script: - -```sh -./build.sh -``` - -This will produce three executables: `toastyfs`, `toastyfs_random_client` and `toastyfs_simulation`. - -The `toastyfs` is the one you need to run. The other executables are for testing purposes. - -You can pick a cluster size based on how many faults you want it to handle. Let's start with a cluster size of 3 which will allow one node to crash at a given time. - -Run three instances of `toastyfs` while specifying to each one the addresses of the others: - -```sh -./toastyfs --addr 127.0.0.1:8081 --peer 127.0.0.1:8082 --peer 127.0.0.1:8083 -./toastyfs --addr 127.0.0.1:8082 --peer 127.0.0.1:8081 --peer 127.0.0.1:8083 -./toastyfs --addr 127.0.0.1:8083 --peer 127.0.0.1:8081 --peer 127.0.0.1:8082 -``` - -The cluster is now working. - -To upload/download/delete objects, you need to compile a client program which needs the toasty client library. - -To build the library, run the following, which will generate the `libtoasty.a` and `libtoasty.so` files: - -```sh -Makefile -``` - -You can use the following example program which will upload a simple object: - -```c -#include -#include -#include -#include -#include - -int main(void) -{ - srand(time(NULL)); - - char addr1[] = "127.0.0.1:8081"; - char addr2[] = "127.0.0.1:8082"; - char addr3[] = "127.0.0.1:8083"; - char *addrs[] = { addr1, addr2, addr3 }; - - printf("Connecting to cluster...\n"); - ToastyFS *tfs = toastyfs_init(rand(), addrs, 3); - if (tfs == NULL) { - fprintf(stderr, "toastyfs_init failed\n"); - return 1; - } - - char key[] = "hello"; - char data[] = "world"; - - printf("PUT key=\"%s\" data=\"%s\" (%d bytes)\n", key, data, (int)strlen(data)); - - ToastyFS_Result res; - int ret = toastyfs_put(tfs, key, strlen(key), data, strlen(data), &res); - if (ret < 0) { - fprintf(stderr, "toastyfs_put returned %d\n", ret); - toastyfs_free(tfs); - return 1; - } - - printf("Done.\n"); - toastyfs_free(tfs); - return 0; -} -``` - -Save it as `example_client.c` and compile it by running: - -``` -gcc example_client.c libtoasty.a -o example -``` - -By running the client while the cluster is running, the object will be created! - -## Fault Tolerance - -ToastyFS allows any minority of nodes to be dead at any given time and continue running without issues. Generally speaking, if your system has 2f+1 nodes, it can work without f of them. - -## Testing - -ToastyFS is tested with Deterministic Simulation Testing (DST). - -The entire system (server nodes and clients) is simulated in the memory of a single process deterministically. Faults (node crashes, network partitons) and latencies are pseudo-randonly injected in the simulation such that a variety of scenarios are tested but in a way that can be perfectly reproduced by using the same simulation seed from which all random events are chosen. - -After each node schedulation, the simulator inspects all node states at the same instant and verifies that none of the system's invariant were broken. - -You can build and run the simulation by doing: - -``` -./build.sh -./toastyfs_simulation -``` - diff --git a/TODO.txt b/TODO.txt index f8f67c1..97aa35f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,4 @@ [ ] Add log compaction [ ] Add log persistence -[ ] Add chunk locations to metadata \ No newline at end of file +[ ] Add chunk locations to metadata +[ ] Remove sender_idx fields in messages since it's provided by the message header \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..2cb122e --- /dev/null +++ b/build.bat @@ -0,0 +1,41 @@ +@echo off +setlocal + +set INCLUDES=-Iquakey/include -Iinclude -I. +set CFLAGS=-Wall -Wextra -ggdb -O0 + +set LIB_CORE=lib/basic.c lib/byte_queue.c lib/tcp.c +set LIB_FULL=%LIB_CORE% lib/file_system.c lib/message.c +set LIB_TLS=%LIB_FULL% lib/tls_schannel.c +set LIB_HTTP=%LIB_TLS% lib/http_parse.c lib/http_server.c + +set SRC_SERVER=src/server.c src/log.c src/client_table.c src/chunk_store.c src/metadata.c + +set QUAKEY=quakey/src/mockfs.c quakey/src/quakey.c + +rem 1. Simulation +echo Building simulation... +gcc %LIB_FULL% %SRC_SERVER% src/client.c src/random_client.c src/main.c src/invariant_checker.c %QUAKEY% -o toastyfs_simulation.exe %INCLUDES% %CFLAGS% -DMAIN_SIMULATION -DFAULT_INJECTION -lws2_32 +if errorlevel 1 goto :error + +rem 2. Server +echo Building server... +gcc %LIB_TLS% %SRC_SERVER% src/main.c -o toastyfs_server.exe %INCLUDES% %CFLAGS% -DMAIN_SERVER -DTLS_ENABLED -DTLS_SCHANNEL -lws2_32 -lsecur32 -lcrypt32 -lncrypt +if errorlevel 1 goto :error + +rem 3. Random client +echo Building random client... +gcc %LIB_CORE% src/client.c src/random_client.c src/main.c -o toastyfs_random_client.exe %INCLUDES% %CFLAGS% -DMAIN_CLIENT -lws2_32 +if errorlevel 1 goto :error + +rem 4. HTTP proxy +echo Building http proxy... +gcc %LIB_HTTP% src/client.c src/http_proxy.c src/main.c -o toastyfs_http_proxy.exe %INCLUDES% %CFLAGS% -DMAIN_HTTP_PROXY -DTLS_ENABLED -DTLS_SCHANNEL -lws2_32 -lsecur32 -lcrypt32 -lncrypt +if errorlevel 1 goto :error + +echo Done. +goto :eof + +:error +echo Build failed. +exit /b 1 diff --git a/build.sh b/build.sh index 71e6133..8dcef19 100644 --- a/build.sh +++ b/build.sh @@ -1,3 +1,45 @@ -gcc src/basic.c src/file_system.c src/byte_queue.c src/message.c src/tcp.c src/server.c src/client.c src/random_client.c src/main.c src/log.c src/client_table.c src/invariant_checker.c src/chunk_store.c src/metadata.c quakey/src/mockfs.c quakey/src/quakey.c -o toastyfs_simulation -Iquakey/include -Iinclude -I. -Wall -Wextra -ggdb -O0 -DMAIN_SIMULATION -DFAULT_INJECTION -gcc src/basic.c src/file_system.c src/byte_queue.c src/message.c src/tcp.c src/server.c src/main.c src/log.c src/client_table.c src/chunk_store.c src/metadata.c -o toastyfs -Iquakey/include -Iinclude -I. -Wall -Wextra -ggdb -O0 -DMAIN_SERVER -gcc src/basic.c src/file_system.c src/byte_queue.c src/message.c src/tcp.c src/server.c src/client.c src/random_client.c src/main.c src/log.c src/client_table.c src/chunk_store.c src/metadata.c -o toastyfs_random_client -Iquakey/include -Iinclude -I. -Wall -Wextra -ggdb -O0 -DMAIN_CLIENT +#!/bin/bash +set -e + +INCLUDES="-Iquakey/include -Iinclude -I." +CFLAGS="-Wall -Wextra -ggdb -O0" + +LIB_CORE="lib/basic.c lib/byte_queue.c lib/tcp.c" +LIB_FULL="$LIB_CORE lib/file_system.c lib/message.c" +LIB_TLS="$LIB_FULL lib/tls_openssl.c" +LIB_HTTP="$LIB_TLS lib/http_parse.c lib/http_server.c" + +SRC_SERVER="src/server.c src/log.c src/client_table.c src/chunk_store.c src/metadata.c" + +QUAKEY="quakey/src/mockfs.c quakey/src/quakey.c" + +# 1. Simulation +echo "Building simulation..." +gcc $LIB_FULL \ + $SRC_SERVER src/client.c src/random_client.c src/main.c src/invariant_checker.c \ + $QUAKEY \ + -o toastyfs_simulation \ + $INCLUDES $CFLAGS -DMAIN_SIMULATION -DFAULT_INJECTION + +# 2. Server +echo "Building server..." +gcc $LIB_TLS \ + $SRC_SERVER src/main.c \ + -o toastyfs_server \ + $INCLUDES $CFLAGS -DMAIN_SERVER -DTLS_ENABLED -DTLS_OPENSSL -lssl -lcrypto + +# 3. Random client +echo "Building random client..." +gcc $LIB_CORE lib/message.c \ + src/client.c src/random_client.c src/main.c \ + -o toastyfs_random_client \ + $INCLUDES $CFLAGS -DMAIN_CLIENT + +# 4. HTTP proxy +echo "Building http proxy..." +gcc $LIB_HTTP \ + src/client.c src/http_proxy.c src/main.c \ + -o toastyfs_http_proxy \ + $INCLUDES $CFLAGS -DMAIN_HTTP_PROXY -DTLS_ENABLED -DTLS_OPENSSL -lssl -lcrypto + +echo "Done." diff --git a/cluster.sh b/cluster.sh deleted file mode 100755 index 0d8800e..0000000 --- a/cluster.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -# -# cluster.sh - Manage a local 3-node ToastyFS cluster. -# -# Usage: -# ./cluster.sh up Build the server and start 3 nodes -# ./cluster.sh down Stop all nodes -# ./cluster.sh status Show which nodes are running -# ./cluster.sh logs [1|2|3] Tail logs (all nodes, or one) -# ./cluster.sh run Build the library, compile -# against it, start the cluster, run the -# binary, then tear down the cluster. -# -# The three server nodes listen on: -# 127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8083 -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CLUSTER_DIR="$SCRIPT_DIR/.cluster" -ADDRS=("127.0.0.1:8081" "127.0.0.1:8082" "127.0.0.1:8083") - -usage() { - sed -n '3,14s/^# \?//p' "$0" - exit 1 -} - -cluster_up() { - echo "==> Building server..." - make -C "$SCRIPT_DIR" toastyfs - - mkdir -p "$CLUSTER_DIR" - - for i in 1 2 3; do - pidfile="$CLUSTER_DIR/node${i}.pid" - if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then - echo " Node $i already running (pid $(cat "$pidfile"))" - continue - fi - - node_dir="$CLUSTER_DIR/node${i}" - mkdir -p "$node_dir" - - # Build argument list: --addr for self, --peer for the others. - args=() - for j in 1 2 3; do - idx=$((j - 1)) - if [ "$j" -eq "$i" ]; then - args+=(--addr "${ADDRS[$idx]}") - else - args+=(--peer "${ADDRS[$idx]}") - fi - done - - ( - cd "$node_dir" - # ServerState is ~40 MB (MetaStore), which exceeds the - # default 8 MB stack. Raise the limit for the server. - ulimit -s unlimited - "$SCRIPT_DIR/toastyfs" "${args[@]}" \ - >> "$CLUSTER_DIR/node${i}.log" 2>&1 & - echo $! > "$pidfile" - ) - echo " Node $i started (pid $(cat "$pidfile")) on ${ADDRS[$((i-1))]}" - done - echo "==> Cluster is running." -} - -cluster_down() { - if [ ! -d "$CLUSTER_DIR" ]; then - echo "No cluster state found." - return - fi - - for i in 1 2 3; do - pidfile="$CLUSTER_DIR/node${i}.pid" - if [ -f "$pidfile" ]; then - pid="$(cat "$pidfile")" - if kill -0 "$pid" 2>/dev/null; then - kill "$pid" 2>/dev/null || true - echo " Node $i stopped (pid $pid)" - else - echo " Node $i was not running" - fi - rm -f "$pidfile" - fi - done - - # Clean up node data directories so fresh starts don't trigger - # the crash-recovery path (vsr_boot_marker). - rm -rf "$CLUSTER_DIR" - echo "==> Cluster stopped." -} - -cluster_status() { - if [ ! -d "$CLUSTER_DIR" ]; then - echo "No cluster state found. Run './cluster.sh up' first." - return - fi - - printf "%-8s %-8s %-20s %s\n" "NODE" "PID" "ADDRESS" "STATUS" - for i in 1 2 3; do - pidfile="$CLUSTER_DIR/node${i}.pid" - addr="${ADDRS[$((i-1))]}" - if [ -f "$pidfile" ]; then - pid="$(cat "$pidfile")" - if kill -0 "$pid" 2>/dev/null; then - printf "%-8s %-8s %-20s %s\n" "node$i" "$pid" "$addr" "running" - else - printf "%-8s %-8s %-20s %s\n" "node$i" "$pid" "$addr" "dead" - fi - else - printf "%-8s %-8s %-20s %s\n" "node$i" "-" "$addr" "not started" - fi - done -} - -cluster_logs() { - if [ $# -gt 0 ]; then - logfile="$CLUSTER_DIR/node${1}.log" - if [ ! -f "$logfile" ]; then - echo "No log file for node $1." - return 1 - fi - tail -f "$logfile" - else - tail -f "$CLUSTER_DIR"/node*.log - fi -} - -cluster_run() { - local src="$1" - local bin - bin="$(basename "${src%.c}")" - - echo "==> Building libtoastyfs..." - make -C "$SCRIPT_DIR" lib - - echo "==> Compiling $src..." - ${CC:-gcc} -Wall -Wextra -o "$bin" "$src" \ - -I"$SCRIPT_DIR/include" \ - -L"$SCRIPT_DIR" \ - -ltoastyfs - - echo "==> Starting cluster..." - cluster_up - # Give the servers a moment to elect a leader. - sleep 2 - - echo "==> Running ./$bin" - echo "---" - LD_LIBRARY_PATH="$SCRIPT_DIR:${LD_LIBRARY_PATH:-}" "./$bin" || true - echo "---" - - echo "==> Stopping cluster..." - cluster_down -} - -if [ $# -lt 1 ]; then - usage -fi - -case "$1" in - up) cluster_up ;; - down) cluster_down ;; - status) cluster_status ;; - logs) shift; cluster_logs "$@" ;; - run) - if [ $# -lt 2 ]; then - echo "Error: 'run' requires a source file argument." >&2 - usage - fi - cluster_run "$2" - ;; - *) usage ;; -esac diff --git a/src/basic.c b/lib/basic.c similarity index 96% rename from src/basic.c rename to lib/basic.c index b1f706f..c15078b 100644 --- a/src/basic.c +++ b/lib/basic.c @@ -71,10 +71,7 @@ int deadline_to_timeout(Time deadline, Time current_time) { if (deadline == INVALID_TIME) return -1; - if (deadline <= current_time) - return 0; - // Round up so sub-millisecond deadlines become 1ms, not 0 - return CEIL(deadline - current_time, 1000000); + return (deadline - current_time) / 1000000; } bool getargb(int argc, char **argv, char *name) diff --git a/src/basic.h b/lib/basic.h similarity index 96% rename from src/basic.h rename to lib/basic.h index 7ab2fa2..dfb641f 100644 --- a/src/basic.h +++ b/lib/basic.h @@ -37,7 +37,6 @@ typedef uint64_t Time; #define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) #define MAX(X, Y) ((X) > (Y) ? (X) : (Y)) -#define CEIL(X, Y) (((X) + (Y) - 1) / (Y)) #define UNREACHABLE __builtin_trap(); diff --git a/src/byte_queue.c b/lib/byte_queue.c similarity index 98% rename from src/byte_queue.c rename to lib/byte_queue.c index bb807d6..aa8767f 100644 --- a/src/byte_queue.c +++ b/lib/byte_queue.c @@ -108,6 +108,11 @@ void byte_queue_read_ack(ByteQueue *queue, uint32_t num) } } +bool byte_queue_reading(ByteQueue *queue) +{ + return (queue->flags & BYTE_QUEUE_READ) != 0; +} + ByteView byte_queue_write_buf(ByteQueue *queue) { if ((queue->flags & BYTE_QUEUE_ERROR) || queue->data == NULL) diff --git a/src/byte_queue.h b/lib/byte_queue.h similarity index 96% rename from src/byte_queue.h rename to lib/byte_queue.h index 5330dce..0684db0 100644 --- a/src/byte_queue.h +++ b/lib/byte_queue.h @@ -39,6 +39,7 @@ int byte_queue_full(ByteQueue *queue); ByteView byte_queue_read_buf(ByteQueue *queue); void byte_queue_read_ack(ByteQueue *queue, uint32_t num); +bool byte_queue_reading(ByteQueue *queue); ByteView byte_queue_write_buf(ByteQueue *queue); void byte_queue_write_ack(ByteQueue *queue, uint32_t num); diff --git a/src/file_system.c b/lib/file_system.c similarity index 98% rename from src/file_system.c rename to lib/file_system.c index 5acea6f..f476b29 100644 --- a/src/file_system.c +++ b/lib/file_system.c @@ -36,7 +36,7 @@ int file_open(string path, Handle *fd) memcpy(zt, path.ptr, path.len); zt[path.len] = '\0'; - int ret = open(zt, O_RDWR | O_CREAT, 0644); + int ret = open(zt, O_RDWR | O_CREAT | O_APPEND, 0644); if (ret < 0) return -1; @@ -293,7 +293,7 @@ int get_full_path(string path, char *dst) #endif #ifdef _WIN32 - if (_fullpath(path_zt, dst, PATH_MAX) == NULL) + if (_fullpath(dst, path_zt, PATH_MAX) == NULL) return -1; #endif diff --git a/src/file_system.h b/lib/file_system.h similarity index 100% rename from src/file_system.h rename to lib/file_system.h diff --git a/lib/http_parse.c b/lib/http_parse.c new file mode 100644 index 0000000..ffadb6c --- /dev/null +++ b/lib/http_parse.c @@ -0,0 +1,1633 @@ +#if defined(MAIN_SIMULATION) || defined(MAIN_TEST) +#define QUAKEY_ENABLE_MOCKS +#endif + +#include +#include + +#include "http_parse.h" + +#define CHTTP_COUNT(arr) ((int)(sizeof(arr) / sizeof((arr)[0]))) + +static bool chttp_streq(string a, string b) +{ + if (a.len != b.len) return false; + return memcmp(a.ptr, b.ptr, a.len) == 0; +} + +static bool chttp_strcase(string a, string b) +{ + if (a.len != b.len) return false; + for (int i = 0; i < a.len; i++) { + char ca = a.ptr[i]; + char cb = b.ptr[i]; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return false; + } + return true; +} + +static string chttp_trim(string s) +{ + while (s.len > 0 && (s.ptr[0] == ' ' || s.ptr[0] == '\t')) { + s.ptr++; + s.len--; + } + while (s.len > 0 && (s.ptr[s.len-1] == ' ' || s.ptr[s.len-1] == '\t')) { + s.len--; + } + return s; +} + +// From RFC 9112 +// request-target = origin-form +// / absolute-form +// / authority-form +// / asterisk-form +// origin-form = absolute-path [ "?" query ] +// absolute-form = absolute-URI +// authority-form = uri-host ":" port +// asterisk-form = "*" +// +// From RFC 9110 +// URI-reference = +// absolute-URI = +// relative-part = +// authority = +// uri-host = +// port = +// path-abempty = +// segment = +// query = +// +// absolute-path = 1*( "/" segment ) +// partial-URI = relative-part [ "?" query ] +// +// From RFC 3986: +// segment = *pchar +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// pct-encoded = "%" HEXDIG HEXDIG +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// query = *( pchar / "/" / "?" ) +// absolute-URI = scheme ":" hier-part [ "?" query ] +// hier-part = "//" authority path-abempty +// / path-absolute +// / path-rootless +// / path-empty +// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + +typedef struct { + char *src; + int len; + int cur; +} Scanner; + +static int is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int is_alpha(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +static int is_hex_digit(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +// From RFC 3986: +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +static int is_sub_delim(char c) +{ + return c == '!' || c == '$' || c == '&' || c == '\'' + || c == '(' || c == ')' || c == '*' || c == '+' + || c == ',' || c == ';' || c == '='; +} + +// From RFC 3986: +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +static int is_unreserved(char c) +{ + return is_alpha(c) || is_digit(c) + || c == '-' || c == '.' + || c == '_' || c == '~'; +} + +// From RFC 3986: +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +static int is_pchar(char c) +{ + return is_unreserved(c) || is_sub_delim(c) || c == ':' || c == '@'; +} + +static int is_tchar(char c) +{ + return is_digit(c) || is_alpha(c) + || c == '!' || c == '#' || c == '$' + || c == '%' || c == '&' || c == '\'' + || c == '*' || c == '+' || c == '-' + || c == '.' || c == '^' || c == '_' + || c == '~'; +} + +static int is_vchar(char c) +{ + return c >= ' ' && c <= '~'; +} + +#define CONSUME_OPTIONAL_SEQUENCE(scanner, func) \ + while ((scanner)->cur < (scanner)->len && (func)((scanner)->src[(scanner)->cur])) \ + (scanner)->cur++; + +static int +consume_absolute_path(Scanner *s) +{ + if (s->cur == s->len || s->src[s->cur] != '/') + return -1; // ERROR + s->cur++; + + for (;;) { + + CONSUME_OPTIONAL_SEQUENCE(s, is_pchar); + + if (s->cur == s->len || s->src[s->cur] != '/') + break; + s->cur++; + } + + return 0; +} + +// If abempty=1: +// path-abempty = *( "/" segment ) +// else: +// path-absolute = "/" [ segment-nz *( "/" segment ) ] +// path-rootless = segment-nz *( "/" segment ) +// path-empty = 0 +static int parse_path(Scanner *s, string *path, int abempty) +{ + int start = s->cur; + + if (abempty) { + + // path-abempty + while (s->cur < s->len && s->src[s->cur] == '/') { + do + s->cur++; + while (s->cur < s->len && is_pchar(s->src[s->cur])); + } + + } else if (s->cur < s->len && (s->src[s->cur] == '/')) { + + // path-absolute + s->cur++; + if (s->cur < s->len && is_pchar(s->src[s->cur])) { + s->cur++; + for (;;) { + + CONSUME_OPTIONAL_SEQUENCE(s, is_pchar); + + if (s->cur == s->len || s->src[s->cur] != '/') + break; + s->cur++; + } + } + + } else if (s->cur < s->len && is_pchar(s->src[s->cur])) { + + // path-rootless + s->cur++; + for (;;) { + + CONSUME_OPTIONAL_SEQUENCE(s, is_pchar) + + if (s->cur == s->len || s->src[s->cur] != '/') + break; + s->cur++; + } + + } else { + // path->empty + // (do nothing) + } + + *path = (string) { + s->src + start, + s->cur - start, + }; + if (path->len == 0) + path->ptr = NULL; + + return 0; +} + +// RFC 3986: +// query = *( pchar / "/" / "?" ) +static int is_query(char c) +{ + return is_pchar(c) || c == '/' || c == '?'; +} + +// RFC 3986: +// fragment = *( pchar / "/" / "?" ) +static int is_fragment(char c) +{ + return is_pchar(c) || c == '/' || c == '?'; +} + +static int little_endian(void) +{ + uint16_t x = 1; + return *((uint8_t*) &x); +} + +static void invert_bytes(void *p, int len) +{ + char *c = p; + for (int i = 0; i < len/2; i++) { + char tmp = c[i]; + c[i] = c[len-i-1]; + c[len-i-1] = tmp; + } +} + +static int parse_ipv4(Scanner *s, CHTTP_IPv4 *ipv4) +{ + unsigned int out = 0; + int i = 0; + for (;;) { + + if (s->cur == s->len || !is_digit(s->src[s->cur])) + return -1; + + int b = 0; + do { + int x = s->src[s->cur++] - '0'; + if (b > (UINT8_MAX - x) / 10) + return -1; + b = b * 10 + x; + } while (s->cur < s->len && is_digit(s->src[s->cur])); + + out <<= 8; + out |= (unsigned char) b; + + i++; + if (i == 4) + break; + + if (s->cur == s->len || s->src[s->cur] != '.') + return -1; + s->cur++; + } + + if (little_endian()) + invert_bytes(&out, 4); + + ipv4->data = out; + return 0; +} + +static int hex_digit_to_int(char c) +{ + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= '0' && c <= '9') return c - '0'; + return -1; +} + +static int parse_ipv6_comp(Scanner *s) +{ + unsigned short buf; + + if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) + return -1; + buf = hex_digit_to_int(s->src[s->cur]); + s->cur++; + + if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) + return buf; + buf <<= 4; + buf |= hex_digit_to_int(s->src[s->cur]); + s->cur++; + + if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) + return buf; + buf <<= 4; + buf |= hex_digit_to_int(s->src[s->cur]); + s->cur++; + + if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) + return buf; + buf <<= 4; + buf |= hex_digit_to_int(s->src[s->cur]); + s->cur++; + + return (int) buf; +} + +static int parse_ipv6(Scanner *s, CHTTP_IPv6 *ipv6) +{ + unsigned short head[8]; + unsigned short tail[8]; + int head_len = 0; + int tail_len = 0; + + if (s->len - s->cur > 1 + && s->src[s->cur+0] == ':' + && s->src[s->cur+1] == ':') + s->cur += 2; + else { + + for (;;) { + + int ret = parse_ipv6_comp(s); + if (ret < 0) return ret; + + head[head_len++] = (unsigned short) ret; + if (head_len == 8) break; + + if (s->cur == s->len || s->src[s->cur] != ':') + return -1; + s->cur++; + + if (s->cur < s->len && s->src[s->cur] == ':') { + s->cur++; + break; + } + } + } + + if (head_len < 8) { + while (s->cur < s->len && is_hex_digit(s->src[s->cur])) { + + int ret = parse_ipv6_comp(s); + if (ret < 0) return ret; + + tail[tail_len++] = (unsigned short) ret; + if (head_len + tail_len == 8) break; + + if (s->cur == s->len || s->src[s->cur] != ':') + break; + s->cur++; + } + } + + for (int i = 0; i < head_len; i++) + ipv6->data[i] = head[i]; + + for (int i = 0; i < 8 - head_len - tail_len; i++) + ipv6->data[head_len + i] = 0; + + for (int i = 0; i < tail_len; i++) + ipv6->data[8 - tail_len + i] = tail[i]; + + if (little_endian()) + for (int i = 0; i < 8; i++) + invert_bytes(&ipv6->data[i], 2); + + return 0; +} + +// From RFC 3986: +// reg-name = *( unreserved / pct-encoded / sub-delims ) +static int is_regname(char c) +{ + return is_unreserved(c) || is_sub_delim(c); +} + +static int parse_regname(Scanner *s, string *regname) +{ + if (s->cur == s->len || !is_regname(s->src[s->cur])) + return -1; + int start = s->cur; + do + s->cur++; + while (s->cur < s->len && is_regname(s->src[s->cur])); + regname->ptr = s->src + start; + regname->len = s->cur - start; + return 0; +} + +static int parse_host(Scanner *s, CHTTP_Host *host) +{ + int ret; + if (s->cur < s->len && s->src[s->cur] == '[') { + + s->cur++; + + int start = s->cur; + CHTTP_IPv6 ipv6; + ret = parse_ipv6(s, &ipv6); + if (ret < 0) return ret; + + host->mode = CHTTP_HOST_MODE_IPV6; + host->ipv6 = ipv6; + host->text = (string) { s->src + start, s->cur - start }; + + if (s->cur == s->len || s->src[s->cur] != ']') + return -1; + s->cur++; + + } else { + + int start = s->cur; + CHTTP_IPv4 ipv4; + ret = parse_ipv4(s, &ipv4); + if (ret >= 0) { + host->mode = CHTTP_HOST_MODE_IPV4; + host->ipv4 = ipv4; + } else { + s->cur = start; + + string regname; + ret = parse_regname(s, ®name); + if (ret < 0) return ret; + + host->mode = CHTTP_HOST_MODE_NAME; + host->name = regname; + } + host->text = (string) { s->src + start, s->cur - start }; + } + + return 0; +} + +// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) +static int is_scheme_head(char c) +{ + return is_alpha(c); +} + +static int is_scheme_body(char c) +{ + return is_alpha(c) + || is_digit(c) + || c == '+' + || c == '-' + || c == '.'; +} + +// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +// Note: percent-encoded characters (%XX) are not currently validated +static int is_userinfo(char c) +{ + return is_unreserved(c) || is_sub_delim(c) || c == ':'; +} + +// authority = [ userinfo "@" ] host [ ":" port ] +static int parse_authority(Scanner *s, CHTTP_Authority *authority) +{ + string userinfo; + { + int start = s->cur; + + CONSUME_OPTIONAL_SEQUENCE(s, is_userinfo); + + if (s->cur < s->len && s->src[s->cur] == '@') { + userinfo = (string) { + s->src + start, + s->cur - start + }; + s->cur++; + } else { + // Rollback + s->cur = start; + userinfo = (string) {NULL, 0}; + } + } + + CHTTP_Host host; + { + int ret = parse_host(s, &host); + if (ret < 0) + return ret; + } + + int port = 0; + if (s->cur < s->len && s->src[s->cur] == ':') { + s->cur++; + if (s->cur < s->len && is_digit(s->src[s->cur])) { + port = s->src[s->cur++] - '0'; + while (s->cur < s->len && is_digit(s->src[s->cur])) { + int x = s->src[s->cur++] - '0'; + if (port > (UINT16_MAX - x) / 10) + return -1; // ERROR: Port too big + port = port * 10 + x; + } + } + } + + authority->userinfo = userinfo; + authority->host = host; + authority->port = port; + return 0; +} + +static int parse_uri(Scanner *s, CHTTP_URL *url, int allow_fragment) +{ + string scheme = {0}; + { + int start = s->cur; + if (s->cur == s->len || !is_scheme_head(s->src[s->cur])) + return -1; // ERROR: Missing scheme + do + s->cur++; + while (s->cur < s->len && is_scheme_body(s->src[s->cur])); + scheme = (string) { + s->src + start, + s->cur - start, + }; + + if (s->cur == s->len || s->src[s->cur] != ':') + return -1; // ERROR: Missing ':' after scheme + s->cur++; + } + + int abempty = 0; + CHTTP_Authority authority = {0}; + if (s->len - s->cur > 1 + && s->src[s->cur+0] == '/' + && s->src[s->cur+1] == '/') { + + s->cur += 2; + + int ret = parse_authority(s, &authority); + if (ret < 0) return ret; + + abempty = 1; + } + + string path; + int ret = parse_path(s, &path, abempty); + if (ret < 0) return ret; + + string query = {0}; + if (s->cur < s->len && s->src[s->cur] == '?') { + int start = s->cur; + do + s->cur++; + while (s->cur < s->len && is_query(s->src[s->cur])); + query = (string) { + s->src + start, + s->cur - start, + }; + } + + string fragment = {0}; + if (allow_fragment && s->cur < s->len && s->src[s->cur] == '#') { + int start = s->cur; + do + s->cur++; + while (s->cur < s->len && is_fragment(s->src[s->cur])); + fragment = (string) { + s->src + start, + s->cur - start, + }; + } + + url->scheme = scheme; + url->authority = authority; + url->path = path; + url->query = query; + url->fragment = fragment; + + return 1; +} + +// authority-form = host ":" port +// host = IP-literal / IPv4address / reg-name +// IP-literal = "[" ( IPv6address / IPvFuture ) "]" +// reg-name = *( unreserved / pct-encoded / sub-delims ) +static int parse_authority_form(Scanner *s, CHTTP_Host *host, int *port) +{ + int ret; + + ret = parse_host(s, host); + if (ret < 0) return ret; + + // Default port value + *port = 0; + + if (s->cur == s->len || s->src[s->cur] != ':') + return 0; // No port + s->cur++; + + if (s->cur == s->len || !is_digit(s->src[s->cur])) + return 0; // No port + + int buf = 0; + do { + int x = s->src[s->cur++] - '0'; + if (buf > (UINT16_MAX - x) / 10) + return -1; // ERROR + buf = buf * 10 + x; + } while (s->cur < s->len && is_digit(s->src[s->cur])); + + *port = buf; + return 0; +} + +static int parse_origin_form(Scanner *s, string *path, string *query) +{ + int ret, start; + + start = s->cur; + ret = consume_absolute_path(s); + if (ret < 0) return ret; + *path = (string) { s->src + start, s->cur - start }; + + if (s->cur < s->len && s->src[s->cur] == '?') { + start = s->cur; + do + s->cur++; + while (s->cur < s->len && is_query(s->src[s->cur])); + *query = (string) { s->src + start, s->cur - start }; + } else + *query = (string) { NULL, 0 }; + + return 0; +} + +static int parse_asterisk_form(Scanner *s) +{ + if (s->len - s->cur < 2 + || s->src[s->cur+0] != '*' + || s->src[s->cur+1] != ' ') + return -1; + s->cur++; + return 0; +} + +static int parse_request_target(Scanner *s, CHTTP_URL *url) +{ + int ret; + + memset(url, 0, sizeof(CHTTP_URL)); + + // asterisk-form + ret = parse_asterisk_form(s); + if (ret >= 0) return ret; + + ret = parse_uri(s, url, 0); + if (ret >= 0) return ret; + + ret = parse_authority_form(s, &url->authority.host, &url->authority.port); + if (ret >= 0) return ret; + + ret = parse_origin_form(s, &url->path, &url->query); + if (ret >= 0) return ret; + + return -1; +} + +bool consume_str(Scanner *scan, string token) +{ + assert(token.len > 0); + + if (token.len > scan->len - scan->cur) + return false; + + for (int i = 0; i < token.len; i++) + if (scan->src[scan->cur + i] != token.ptr[i]) + return false; + + scan->cur += token.len; + return true; +} + +static int is_header_body(char c) +{ + return is_vchar(c) || c == ' ' || c == '\t'; +} + +static int parse_headers(Scanner *s, CHTTP_Header *headers, int max_headers) +{ + int num_headers = 0; + while (!consume_str(s, S("\r\n"))) { + + // RFC 9112: + // field-line = field-name ":" OWS field-value OWS + // + // RFC 9110: + // field-value = *field-content + // field-content = field-vchar + // [ 1*( SP / HTAB / field-vchar ) field-vchar ] + // field-vchar = VCHAR / obs-text + // obs-text = %x80-FF + + int start; + + if (s->cur == s->len || !is_tchar(s->src[s->cur])) + return -1; // ERROR + start = s->cur; + do + s->cur++; + while (s->cur < s->len && is_tchar(s->src[s->cur])); + string name = { s->src + start, s->cur - start }; + + if (s->cur == s->len || s->src[s->cur] != ':') + return -1; // ERROR + s->cur++; + + start = s->cur; + CONSUME_OPTIONAL_SEQUENCE(s, is_header_body); + string body = { s->src + start, s->cur - start }; + body = chttp_trim(body); + + if (num_headers < max_headers) + headers[num_headers++] = (CHTTP_Header) { name, body }; + + if (!consume_str(s, S("\r\n"))) { + return -1; + } + } + + return num_headers; +} + +typedef enum { + TRANSFER_ENCODING_OPTION_CHUNKED, + TRANSFER_ENCODING_OPTION_COMPRESS, + TRANSFER_ENCODING_OPTION_DEFLATE, + TRANSFER_ENCODING_OPTION_GZIP, +} TransferEncodingOption; + +static bool is_space(char c) +{ + return c == ' ' || c == '\t'; +} + +static int +parse_transfer_encoding(string src, TransferEncodingOption *dst, int max) +{ + Scanner s = { src.ptr, src.len, 0 }; + + int num = 0; + for (;;) { + + CONSUME_OPTIONAL_SEQUENCE(&s, is_space); + + TransferEncodingOption opt; + if (0) {} + else if (consume_str(&s, S("chunked"))) opt = TRANSFER_ENCODING_OPTION_CHUNKED; + else if (consume_str(&s, S("compress"))) opt = TRANSFER_ENCODING_OPTION_COMPRESS; + else if (consume_str(&s, S("deflate"))) opt = TRANSFER_ENCODING_OPTION_DEFLATE; + else if (consume_str(&s, S("gzip"))) opt = TRANSFER_ENCODING_OPTION_GZIP; + else return -1; // Invalid option + + if (num == max) + return -1; // Too many options + dst[num++] = opt; + + CONSUME_OPTIONAL_SEQUENCE(&s, is_space); + + if (s.cur == s.len) + break; + + if (s.src[s.cur] != ',') + return -1; // Missing comma separator + } + + return num; +} + +static int +parse_content_length(const char *src, int len, uint64_t *out) +{ + int cur = 0; + while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) + cur++; + + if (cur == len || !is_digit(src[cur])) + return -1; + + uint64_t buf = 0; + do { + int d = src[cur++] - '0'; + if (buf > (UINT64_MAX - d) / 10) + return -1; + buf = buf * 10 + d; + } while (cur < len && is_digit(src[cur])); + + *out = buf; + return 0; +} + +static int parse_body(Scanner *s, + CHTTP_Header *headers, int num_headers, + string *body, bool body_expected) +{ + + // RFC 9112 section 6: + // The presence of a message body in a request is signaled by a Content-Length or + // Transfer-Encoding header field. Request message framing is independent of method + // semantics. + + int header_index = chttp_find_header(headers, num_headers, S("Transfer-Encoding")); + if (header_index != -1) { + + // RFC 9112 section 6.1: + // A server MAY reject a request that contains both Content-Length and Transfer-Encoding + // or process such a request in accordance with the Transfer-Encoding alone. Regardless, + // the server MUST close the connection after responding to such a request to avoid the + // potential attacks. + if (chttp_find_header(headers, num_headers, S("Content-Length")) != -1) + return -1; + + string value = headers[header_index].value; + + // RFC 9112 section 6.1: + // If any transfer coding other than chunked is applied to a request's content, the + // sender MUST apply chunked as the final transfer coding to ensure that the message + // is properly framed. If any transfer coding other than chunked is applied to a + // response's content, the sender MUST either apply chunked as the final transfer + // coding or terminate the message by closing the connection. + + TransferEncodingOption opts[8]; + int num = parse_transfer_encoding(value, opts, CHTTP_COUNT(opts)); + if (num != 1 || opts[0] != TRANSFER_ENCODING_OPTION_CHUNKED) + return -1; + + string chunks_maybe[128]; + string *chunks = chunks_maybe; + int num_chunks = 0; + int max_chunks = CHTTP_COUNT(chunks_maybe); + + #define FREE_CHUNK_LIST \ + if (chunks != chunks_maybe) \ + free(chunks); + + char *content_start = s->src + s->cur; + + for (;;) { + + // RFC 9112 section 7.1: + // The chunked transfer coding wraps content in order to transfer it as a series of chunks, + // each with its own size indicator, followed by an OPTIONAL trailer section containing + // trailer fields. + + if (s->cur == s->len) { + FREE_CHUNK_LIST + return 0; // Incomplete request + } + + if (!is_hex_digit(s->src[s->cur])) { + FREE_CHUNK_LIST + return -1; + } + + int chunk_len = 0; + + do { + char c = s->src[s->cur++]; + int n = hex_digit_to_int(c); + if (chunk_len > (INT_MAX - n) / 16) { + FREE_CHUNK_LIST + return -1; // overflow + } + chunk_len = chunk_len * 16 + n; + } while (s->cur < s->len && is_hex_digit(s->src[s->cur])); + + if (s->cur == s->len) { + FREE_CHUNK_LIST + return 0; // Incomplete request + } + if (s->src[s->cur] != '\r') { + FREE_CHUNK_LIST + return -1; + } + s->cur++; + + if (s->cur == s->len) { + FREE_CHUNK_LIST + return 0; + } + if (s->src[s->cur] != '\n') { + FREE_CHUNK_LIST + return -1; + } + s->cur++; + + char *chunk_ptr = s->src + s->cur; + + if (chunk_len > s->len - s->cur) { + FREE_CHUNK_LIST + return 0; // Incomplete request + } + s->cur += chunk_len; + + if (s->cur == s->len) + return 0; // Incomplete request + if (s->src[s->cur] != '\r') { + FREE_CHUNK_LIST + return -1; + } + s->cur++; + + if (s->cur == s->len) { + FREE_CHUNK_LIST + return 0; // Incomplete request + } + if (s->src[s->cur] != '\n') { + FREE_CHUNK_LIST + return -1; + } + s->cur++; + + if (chunk_len == 0) + break; + + if (num_chunks == max_chunks) { + + max_chunks *= 2; + + string *new_chunks = malloc(max_chunks * sizeof(string)); + if (new_chunks == NULL) { + if (chunks != chunks_maybe) + free(chunks); + return -1; + } + + for (int i = 0; i < num_chunks; i++) + new_chunks[i] = chunks[i]; + + if (chunks != chunks_maybe) + free(chunks); + + chunks = new_chunks; + } + chunks[num_chunks++] = (string) { chunk_ptr, chunk_len }; + } + + char *content_ptr = content_start; + for (int i = 0; i < num_chunks; i++) { + memmove(content_ptr, chunks[i].ptr, chunks[i].len); + content_ptr += chunks[i].len; + } + + *body = (string) { + content_start, + content_ptr - content_start + }; + + if (chunks != chunks_maybe) + free(chunks); + + return 1; + } + + // RFC 9112 section 6.3: + // If a valid Content-Length header field is present without Transfer-Encoding, + // its decimal value defines the expected message body length in octets. + + header_index = chttp_find_header(headers, num_headers, S("Content-Length")); + if (header_index != -1) { + + // Have Content-Length + string value = headers[header_index].value; + + uint64_t tmp; + if (parse_content_length(value.ptr, value.len, &tmp) < 0) + return -1; + if (tmp > INT_MAX) + return -1; + int len = (int) tmp; + + if (len > s->len - s->cur) + return 0; // Incomplete request + + *body = (string) { s->src + s->cur, len }; + + s->cur += len; + return 1; + } + + // No Content-Length or Transfer-Encoding + if (body_expected) return -1; + + *body = (string) { NULL, 0 }; + return 1; +} + +static int contains_head(char *src, int len) +{ + int cur = 0; + while (len - cur > 3) { + if (src[cur+0] == '\r' && + src[cur+1] == '\n' && + src[cur+2] == '\r' && + src[cur+3] == '\n') + return 1; + cur++; + } + return 0; +} + +static int parse_request(Scanner *s, CHTTP_Request *req) +{ + if (!contains_head(s->src + s->cur, s->len - s->cur)) + return 0; + + req->secure = false; + + if (0) {} + else if (consume_str(s, S("GET "))) req->method = CHTTP_METHOD_GET; + else if (consume_str(s, S("POST "))) req->method = CHTTP_METHOD_POST; + else if (consume_str(s, S("PUT "))) req->method = CHTTP_METHOD_PUT; + else if (consume_str(s, S("HEAD "))) req->method = CHTTP_METHOD_HEAD; + else if (consume_str(s, S("DELETE "))) req->method = CHTTP_METHOD_DELETE; + else if (consume_str(s, S("CONNECT "))) req->method = CHTTP_METHOD_CONNECT; + else if (consume_str(s, S("OPTIONS "))) req->method = CHTTP_METHOD_OPTIONS; + else if (consume_str(s, S("TRACE "))) req->method = CHTTP_METHOD_TRACE; + else if (consume_str(s, S("PATCH "))) req->method = CHTTP_METHOD_PATCH; + else return -1; + + { + Scanner s2 = *s; + int peek = s->cur; + while (peek < s->len && s->src[peek] != ' ') + peek++; + if (peek == s->len) + return -1; + s2.len = peek; + + int ret = parse_request_target(&s2, &req->url); + if (ret < 0) return ret; + + s->cur = s2.cur; + } + + if (consume_str(s, S(" HTTP/1.1\r\n"))) { + req->minor = 1; + } else if (consume_str(s, S(" HTTP/1.0\r\n")) || consume_str(s, S(" HTTP/1\r\n"))) { + req->minor = 0; + } else { + return -1; + } + + int num_headers = parse_headers(s, req->headers, CHTTP_MAX_HEADERS); + if (num_headers < 0) + return num_headers; + req->num_headers = num_headers; + + // Request methods that typically don't have a body + bool body_expected = true; + if (req->method == CHTTP_METHOD_GET || + req->method == CHTTP_METHOD_HEAD || + req->method == CHTTP_METHOD_DELETE || + req->method == CHTTP_METHOD_OPTIONS || + req->method == CHTTP_METHOD_TRACE) + body_expected = false; + + return parse_body(s, req->headers, req->num_headers, &req->body, body_expected); +} + +int chttp_find_header(CHTTP_Header *headers, int num_headers, string name) +{ + for (int i = 0; i < num_headers; i++) + if (chttp_strcase(name, headers[i].name)) + return i; + return -1; +} + +static int parse_response(Scanner *s, CHTTP_Response *res) +{ + if (!contains_head(s->src + s->cur, s->len - s->cur)) + return 0; + + if (consume_str(s, S("HTTP/1.1 "))) { + res->minor = 1; + } else if (consume_str(s, S("HTTP/1.0 ")) || consume_str(s, S("HTTP/1 "))) { + res->minor = 0; + } else { + return -1; + } + + if (s->len - s->cur < 4 + || !is_digit(s->src[s->cur+0]) + || !is_digit(s->src[s->cur+1]) + || !is_digit(s->src[s->cur+2]) + || s->src[s->cur+3] != ' ') + return -1; + res->status = + (s->src[s->cur+0] - '0') * 100 + + (s->src[s->cur+1] - '0') * 10 + + (s->src[s->cur+2] - '0') * 1; + s->cur += 4; + + // Parse reason phrase: HTAB / SP / VCHAR / obs-text + // Note: obs-text (obsolete text, octets 0x80-0xFF) is not validated + while (s->cur < s->len && ( + s->src[s->cur] == '\t' || + s->src[s->cur] == ' ' || + is_vchar(s->src[s->cur]))) + s->cur++; + + if (s->len - s->cur < 2 + || s->src[s->cur+0] != '\r' + || s->src[s->cur+1] != '\n') + return -1; + s->cur += 2; + + int num_headers = parse_headers(s, res->headers, CHTTP_MAX_HEADERS); + if (num_headers < 0) + return num_headers; + res->num_headers = num_headers; + + // Responses with certain status codes don't have a body: + // - 1xx (Informational) + // - 204 (No Content) + // - 304 (Not Modified) + // Note: HEAD responses also don't have a body, but we can't determine + // that here without access to the request method + bool body_expected = true; + if ((res->status >= 100 && res->status < 200) || + res->status == 204 || + res->status == 304) + body_expected = false; + + return parse_body(s, res->headers, res->num_headers, &res->body, body_expected); +} + +int chttp_parse_ipv4(char *src, int len, CHTTP_IPv4 *ipv4) +{ + Scanner s = {src, len, 0}; + int ret = parse_ipv4(&s, ipv4); + if (ret < 0) return ret; + return s.cur; +} + +int chttp_parse_ipv6(char *src, int len, CHTTP_IPv6 *ipv6) +{ + Scanner s = {src, len, 0}; + int ret = parse_ipv6(&s, ipv6); + if (ret < 0) return ret; + return s.cur; +} + +int chttp_parse_url(char *src, int len, CHTTP_URL *url) +{ + Scanner s = {src, len, 0}; + int ret = parse_uri(&s, url, 1); + if (ret == 1) + return s.cur; + return ret; +} + +int chttp_parse_request(char *src, int len, CHTTP_Request *req) +{ + Scanner s = {src, len, 0}; + int ret = parse_request(&s, req); + if (ret == 1) + return s.cur; + return ret; +} + +int chttp_parse_response(char *src, int len, CHTTP_Response *res) +{ + Scanner s = {src, len, 0}; + int ret = parse_response(&s, res); + if (ret == 1) + return s.cur; + return ret; +} + +string chttp_get_cookie(CHTTP_Request *req, string name) +{ + // Simple cookie parsing - does not handle quoted values or special characters + // See RFC 6265 for full cookie specification + + for (int i = 0; i < req->num_headers; i++) { + + if (!chttp_strcase(req->headers[i].name, S("Cookie"))) + continue; + + char *src = req->headers[i].value.ptr; + int len = req->headers[i].value.len; + int cur = 0; + + // Cookie: name1=value1; name2=value2; name3=value3 + + for (;;) { + + while (cur < len && src[cur] == ' ') + cur++; + + int off = cur; + while (cur < len && src[cur] != '=') + cur++; + + string cookie_name = { src + off, cur - off }; + + if (cur == len) + break; + cur++; + + off = cur; + while (cur < len && src[cur] != ';') + cur++; + + string cookie_value = { src + off, cur - off }; + + if (chttp_streq(name, cookie_name)) + return cookie_value; + + if (cur == len) + break; + cur++; + } + } + + return S(""); +} + +string chttp_get_param(string body, string str, char *mem, int cap) +{ + // This is just a best-effort implementation + + char *src = body.ptr; + int len = body.len; + int cur = 0; + + if (cur < len && src[cur] == '?') + cur++; + + while (cur < len) { + + string name; + { + int off = cur; + while (cur < len && src[cur] != '=' && src[cur] != '&') + cur++; + name = (string) { src + off, cur - off }; + } + + string body = S(""); + if (cur < len) { + cur++; + if (src[cur-1] == '=') { + int off = cur; + while (cur < len && src[cur] != '&') + cur++; + body = (string) { src + off, cur - off }; + + if (cur < len) + cur++; + } + } + + if (chttp_streq(str, name)) { + + bool percent_encoded = false; + for (int i = 0; i < body.len; i++) + if (body.ptr[i] == '+' || body.ptr[i] == '%') { + percent_encoded = true; + break; + } + + if (!percent_encoded) + return body; + + if (body.len > cap) + return (string) { NULL, 0 }; + + string decoded = { mem, 0 }; + for (int i = 0; i < body.len; i++) { + + char c = body.ptr[i]; + if (c == '+') + c = ' '; + else { + if (body.ptr[i] == '%') { + if (body.len - i < 3 + || !is_hex_digit(body.ptr[i+1]) + || !is_hex_digit(body.ptr[i+2])) + return (string) { NULL, 0 }; + + int h = hex_digit_to_int(body.ptr[i+1]); + int l = hex_digit_to_int(body.ptr[i+2]); + c = (h << 4) | l; + + i += 2; + } + } + + decoded.ptr[decoded.len++] = c; + } + + return decoded; + } + } + + return S(""); +} + +int chttp_get_param_i(string body, string str) +{ + char buf[128]; + string out = chttp_get_param(body, str, buf, (int) sizeof(buf)); + if (out.len == 0 || !is_digit(out.ptr[0])) + return -1; + + int cur = 0; + int res = 0; + do { + int d = out.ptr[cur++] - '0'; + if (res > (INT_MAX - d) / 10) + return -1; + res = res * 10 + d; + } while (cur < out.len && is_digit(out.ptr[cur])); + + return res; +} + +bool chttp_match_host(CHTTP_Request *req, string domain, int port) +{ + int idx = chttp_find_header(req->headers, req->num_headers, S("Host")); + if (idx < 0) + return false; + + char tmp[1<<8]; + if (port > -1 && port != 80) { + int ret = snprintf(tmp, sizeof(tmp), "%.*s:%d", domain.len, domain.ptr, port); + assert(ret > 0); + domain = (string) { tmp, ret }; + } + + string host = req->headers[idx].value; + return chttp_strcase(host, domain); +} + + +// , :: GMT +static int parse_date(Scanner *s, CHTTP_Date *out) +{ + struct { string str; CHTTP_WeekDay val; } week_day_table[] = { + { S("Mon, "), CHTTP_WEEKDAY_MON }, + { S("Tue, "), CHTTP_WEEKDAY_TUE }, + { S("Wed, "), CHTTP_WEEKDAY_WED }, + { S("Thu, "), CHTTP_WEEKDAY_THU }, + { S("Fri, "), CHTTP_WEEKDAY_FRI }, + { S("Sat, "), CHTTP_WEEKDAY_SAT }, + { S("Sun, "), CHTTP_WEEKDAY_SUN }, + }; + + bool found = false; + for (int i = 0; i < CHTTP_COUNT(week_day_table); i++) + if (consume_str(s, week_day_table[i].str)) { + out->week_day = week_day_table[i].val; + found = true; + break; + } + if (!found) + return -1; + + if (1 >= s->len - s->cur + || !is_digit(s->src[s->cur+0]) + || !is_digit(s->src[s->cur+1])) + return -1; + out->day + = (s->src[s->cur+0] - '0') * 10 + + (s->src[s->cur+1] - '0') * 1; + s->cur += 2; + + struct { string str; CHTTP_Month val; } month_table[] = { + { S(" Jan "), CHTTP_MONTH_JAN }, + { S(" Feb "), CHTTP_MONTH_FEB }, + { S(" Mar "), CHTTP_MONTH_MAR }, + { S(" Apr "), CHTTP_MONTH_APR }, + { S(" May "), CHTTP_MONTH_MAY }, + { S(" Jun "), CHTTP_MONTH_JUN }, + { S(" Jul "), CHTTP_MONTH_JUL }, + { S(" Aug "), CHTTP_MONTH_AUG }, + { S(" Sep "), CHTTP_MONTH_SEP }, + { S(" Oct "), CHTTP_MONTH_OCT }, + { S(" Nov "), CHTTP_MONTH_NOV }, + { S(" Dec "), CHTTP_MONTH_DEC }, + }; + + found = false; + for (int i = 0; i < CHTTP_COUNT(month_table); i++) + if (consume_str(s, month_table[i].str)) { + out->month = month_table[i].val; + found = true; + break; + } + if (!found) + return -1; + + if (3 >= s->len - s->cur + || !is_digit(s->src[s->cur+0]) + || !is_digit(s->src[s->cur+1]) + || !is_digit(s->src[s->cur+2]) + || !is_digit(s->src[s->cur+3])) + return -1; + out->year + = (s->src[s->cur+0] - '0') * 1000 + + (s->src[s->cur+1] - '0') * 100 + + (s->src[s->cur+2] - '0') * 10 + + (s->src[s->cur+3] - '0') * 1; + s->cur += 4; + + if (s->cur == s->len || s->src[s->cur] != ' ') + return -1; + s->cur++; + + if (7 >= s->len - s->cur + || !is_digit(s->src[s->cur+0]) + || !is_digit(s->src[s->cur+1]) + || s->src[s->cur+2] != ':' + || !is_digit(s->src[s->cur+3]) + || !is_digit(s->src[s->cur+4]) + || s->src[s->cur+5] != ':' + || !is_digit(s->src[s->cur+6]) + || !is_digit(s->src[s->cur+7]) + || s->src[s->cur+8] != ' ' + || s->src[s->cur+9] != 'G' + || s->src[s->cur+10] != 'M' + || s->src[s->cur+11] != 'T') + return -1; + out->hour + = (s->src[s->cur+0] - '0') * 10 + + (s->src[s->cur+1] - '0') * 1; + out->minute + = (s->src[s->cur+3] - '0') * 10 + + (s->src[s->cur+4] - '0') * 1; + out->second + = (s->src[s->cur+6] - '0') * 10 + + (s->src[s->cur+7] - '0') * 1; + s->cur += 12; + return 0; +} + +// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E +// ; US-ASCII characters excluding CTLs, +// ; whitespace, DQUOTE, comma, semicolon, +// ; and backslash +static bool is_cookie_octet(char c) +{ + return c == 0x21 || + (c >= 0x23 && c <= 0x2B) || + (c >= 0x2D && c <= 0x3A) || + (c >= 0x3C && c <= 0x5B) || + (c >= 0x5D && c <= 0x7E); +} + +int chttp_parse_set_cookie(string str, CHTTP_SetCookie *out) +{ + Scanner s = { str.ptr, str.len, 0 }; + + // cookie-name = token + if (s.cur == s.len || !is_tchar(s.src[s.cur])) + return -1; + int off = s.cur; + do + s.cur++; + while (s.cur < s.len && is_tchar(s.src[s.cur])); + out->name = (string) { s.src + off, s.cur - off }; + + // cookie-pair = cookie-name "=" cookie-value + if (s.cur == s.len || s.src[s.cur] != '=') + return -1; + s.cur++; + + // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + if (s.cur < s.len && s.src[s.cur] == '"') { + s.cur++; // Consume opening double quote + int off = s.cur; + while (s.cur < s.len && is_cookie_octet(s.src[s.cur])) + s.cur++; + if (s.cur == s.len || s.src[s.cur] != '"') + return -1; // Missing closing double quote + out->value = (string) { s.src + off, s.cur - off }; + s.cur++; // Consume closing double quote + } else { + int off = s.cur; + while (s.cur < s.len && is_cookie_octet(s.src[s.cur])) + s.cur++; + out->value = (string) { s.src + off, s.cur - off }; + } + + // *( ";" SP cookie-av ) + // + // cookie-av = expires-av / max-age-av / domain-av / + // path-av / secure-av / httponly-av / + // extension-av + out->secure = false; + out->chttp_only = false; + out->have_date = false; + out->have_max_age = false; + out->have_domain = false; + out->have_path = false; + while (consume_str(&s, S("; "))) { + if (consume_str(&s, S("Expires="))) { + + // expires-av = "Expires=" sane-cookie-date + if (parse_date(&s, &out->date) < 0) + return -1; + out->have_date = true; + + } else if (consume_str(&s, S("Max-Age="))) { + + // max-age-av = "Max-Age=" non-zero-digit *DIGIT + + uint32_t value = 0; + if (s.cur == s.len || !is_digit(s.src[s.cur])) + return -1; + do { + int d = s.src[s.cur++] - '0'; + if (value > (UINT32_MAX - d) / 10) + return -1; + value = value * 10 + d; + } while (s.cur < s.len && is_digit(s.src[s.cur])); + + out->have_max_age = true; + out->max_age = value; + + } else if (consume_str(&s, S("Domain="))) { + + // domain-av = "Domain=" domain-value + // domain-value = + // ; defined in RFC 1034, Section 3.5 + // + // From RFC 1034: + // ::=