commit 2681801a1e9dddd805dd99f70fb7bc0da485f4ff Author: Chewing_Bever Date: Sat Jan 27 22:54:53 2024 +0100 chore: copy over original source files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4fb4fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.cache/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38b2b4a --- /dev/null +++ b/Makefile @@ -0,0 +1,128 @@ +# https://spin.atomicobject.com/2016/08/26/makefile-c-projects/ was a great +# base for this Makefile + +-include config.mk + +LIB := $(BUILD_DIR)/$(LIB_FILENAME) + +SRCS != find '$(SRC_DIR)' -iname '*.c' +SRCS_H != find include -iname '*.h' +SRCS_H_INTERNAL != find $(SRC_DIR) -iname '*.h' +SRCS_TEST != find '$(TEST_DIR)' -iname '*.c' + +OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) +OBJS_TEST := $(SRCS_TEST:%=$(BUILD_DIR)/%.o) +DEPS := $(SRCS:%=$(BUILD_DIR)/%.d) $(SRCS_TEST:%=$(BUILD_DIR)/%.d) + +BINS_TEST := $(OBJS_TEST:%.c.o=%) +TARGETS_TEST := $(BINS_TEST:%=test-%) +TARGETS_MEM_TEST := $(BINS_TEST:%=test-mem-%) + +_CFLAGS := $(addprefix -I,$(INC_DIRS)) $(CFLAGS) -Wall -Wextra + +.PHONY: all +all: lib + + +# =====COMPILATION===== +# Utility used by the CI to lint +.PHONY: objs +objs: $(OBJS) + +.PHONY: lib +lib: $(LIB) +$(LIB): $(OBJS) + ar -rcs $@ $(OBJS) + +$(BUILD_DIR)/$(SRC_DIR)/%.c.o: $(SRC_DIR)/%.c + mkdir -p $(dir $@) + $(CC) -c $(_CFLAGS) $< -o $@ + +# =====TESTING===== +.PHONY: test +test: $(TARGETS_TEST) + +.PHONY: test-mem +test-mem: $(TARGETS_MEM_TEST) + +.PHONY: $(TARGETS_TEST) +$(TARGETS_TEST): test-%: % + ./$^ + +.PHONY: $(TARGETS_MEM_TEST) +$(TARGETS_MEM_TEST): test-mem-%: % + valgrind --tool=memcheck --error-exitcode=1 --track-origins=yes --leak-check=full ./$^ + +.PHONY: build-test +build-test: $(BINS_TEST) + +$(BINS_TEST): %: %.c.o $(LIB) + $(CC) \ + $^ -o $@ + +# Along with the include directory, each test includes $(TEST_DIR) (which +# contains the acutest.h header file), and the src directory of the module it's +# testing. This allows tests to access internal methods, which aren't publicly +# exposed. +$(BUILD_DIR)/$(TEST_DIR)/%.c.o: $(TEST_DIR)/%.c + mkdir -p $(dir $@) + $(CC) $(_CFLAGS) -I$(TEST_DIR) \ + -I$(dir $(@:$(BUILD_DIR)/$(TEST_DIR)/%=$(SRC_DIR)/%)) \ + -c $< -o $@ + +# =====EXAMPLES===== +.PHONY: build-example +build-example: $(BINS_EXAMPLE) + +$(BINS_EXAMPLE): %: %.c.o $(LIB) + $(CC) \ + $^ -o $@ + +# Example binaries link the resulting library +$(BUILD_DIR)/$(EXAMPLE_DIR)/%.c.o: $(EXAMPLE_DIR)/%.c + mkdir -p $(dir $@) + $(CC) $(_CFLAGS) -I$(PUB_INC_DIR) -c $< -o $@ + +# =====MAINTENANCE===== +.PHONY: lint +lint: + clang-format -n --Werror \ + $(filter-out $(THIRDPARTY),$(SRCS)) \ + $(filter-out $(THIRDPARTY),$(SRCS_H)) \ + $(filter-out $(THIRDPARTY),$(SRCS_H_INTERNAL)) + +.PHONY: fmt +fmt: + clang-format -i \ + $(filter-out $(THIRDPARTY),$(SRCS)) \ + $(filter-out $(THIRDPARTY),$(SRCS_H)) \ + $(filter-out $(THIRDPARTY),$(SRCS_H_INTERNAL)) + +.PHONY: check +check: + mkdir -p $(BUILD_DIR)/cppcheck + cppcheck \ + $(addprefix -I,$(INC_DIRS)) \ + --cppcheck-build-dir=$(BUILD_DIR)/cppcheck \ + --error-exitcode=1 \ + --enable=warning,style \ + --inline-suppr \ + --check-level=exhaustive \ + --quiet \ + -j$(shell nproc) \ + $(filter-out $(THIRDPARTY),$(SRCS)) + +.PHONY: clean +clean: + rm -rf '$(BUILD_DIR)' + + +.PHONY: bear +bear: clean + bear -- make + bear --append -- make build-test + bear --append -- make build-example + + +# Make make aware of the .d files +-include $(DEPS) diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..7e55aff --- /dev/null +++ b/config.mk @@ -0,0 +1,16 @@ +LIB_FILENAME = liblnm.a + +BUILD_DIR = build +SRC_DIR = src +TEST_DIR = test +THIRDPARTY = src/picohttpparser.c include/picohttpparser.h + +PUB_INC_DIR = include +INC_DIRS = $(PUB_INC_DIR) src/_include + +# -MMD: generate a .d file for every source file. This file can be imported by +# make and makes make aware that a header file has been changed, ensuring an +# object file is also recompiled if only a header is changed. +# -MP: generate a dummy target for every header file (according to the docs it +# prevents some errors when removing header files) +CFLAGS ?= -MMD -MP -g diff --git a/include/lnm/common.h b/include/lnm/common.h new file mode 100644 index 0000000..8cc982e --- /dev/null +++ b/include/lnm/common.h @@ -0,0 +1,89 @@ +#ifndef LNM_COMMON +#define LNM_COMMON + +#include +#include +#include + +#define LNM_RES(x) \ + { \ + lnm_err res = x; \ + if (res != lnm_err_ok) \ + return res; \ + } + +#define LNM_RES2(x, e) \ + { \ + lnm_err res = x; \ + if (res != lnm_err_ok) { \ + e; \ + return res; \ + } \ + } + +#define LNM_MIN(x, y) ((x) < (y) ? (x) : (y)) +#define LNM_MAX(x, y) ((x) > (y) ? (x) : (y)) + +typedef enum lnm_err { + lnm_err_ok = 0, + lnm_err_failed_alloc, + lnm_err_failed_network, + lnm_err_failed_poll, + lnm_err_not_setup, + lnm_err_bad_regex, + lnm_err_not_found, +} lnm_err; + +typedef struct lnm_loop lnm_http_loop; + +typedef struct lnm_loop_conn lnm_http_conn; + +typedef struct lnm_http_step lnm_http_step; + +typedef struct lnm_http_route lnm_http_route; + +/** + * Returns whether the two strings are equal. + * + * @param s1 first string to compare + * @param s1_len length of `s1` + * @param s2 second string to compare + * @param s2_len length of `s2` + */ +bool lnm_strneq(const char *s1, size_t s1_len, const char *s2, size_t s2_len); + +/** + * Returns whether the two strings are equal, ignoring capitalisation. + * + * @param s1 first string to compare + * @param s1_len length of `s1` + * @param s2 second string to compare + * @param s2_len length of `s2` + */ +bool lnm_strnieq(const char *s1, size_t s1_len, const char *s2, size_t s2_len); + +/** + * Calculate integer exponentation. + * + * @param base + * @param power + */ +uint64_t lnm_ipow(uint64_t base, uint64_t power); + +/** + * Parse the given string into a number. + * + * @param s string to parse + * @param len length of s + * @return the parsed number, or 0 if the number is invalid + */ +uint64_t lnm_atoi(const char *s, size_t len); + +/** + * Calculate how many base 10 digits the given number consists of. + * + * @param num number to use + */ +uint64_t lnm_digits(uint64_t num); + +#endif diff --git a/include/lnm/http/consts.h b/include/lnm/http/consts.h new file mode 100644 index 0000000..f543bbb --- /dev/null +++ b/include/lnm/http/consts.h @@ -0,0 +1,98 @@ +#ifndef LNM_HTTP_CONSTS +#define LNM_HTTP_CONSTS + +#include + +extern const char *lnm_http_method_names[]; +extern const size_t lnm_http_method_names_len; + +typedef enum lnm_http_method { + lnm_http_method_get = 0, + lnm_http_method_post, + lnm_http_method_put, + lnm_http_method_patch, + lnm_http_method_delete, + lnm_http_method_head, +} lnm_http_method; + +extern const char *lnm_http_status_names[][32]; + +typedef enum lnm_http_status { + // 1xx + lnm_http_status_continue = 100, + lnm_http_status_switching_protocols = 101, + lnm_http_status_processing = 102, + lnm_http_status_early_hints = 103, + // 2xx + lnm_http_status_ok = 200, + lnm_http_status_created = 201, + lnm_http_status_accepted = 202, + lnm_http_status_non_authoritative_information = 203, + lnm_http_status_no_content = 204, + lnm_http_status_reset_content = 205, + lnm_http_status_partial_content = 206, + lnm_http_status_multi_status = 207, + lnm_http_status_already_reported = 208, + // 3xx + lnm_http_status_multiple_choices = 300, + lnm_http_status_moved_permanently = 301, + lnm_http_status_found = 302, + lnm_http_status_see_other = 303, + lnm_http_status_not_modified = 304, + lnm_http_status_temporary_redirect = 307, + lnm_http_status_permanent_redirect = 308, + // 4xx + lnm_http_status_bad_request = 400, + lnm_http_status_unauthorized = 401, + lnm_http_status_payment_required = 402, + lnm_http_status_forbidden = 403, + lnm_http_status_not_found = 404, + lnm_http_status_method_not_allowed = 405, + lnm_http_status_not_acceptable = 406, + lnm_http_status_proxy_authentication_required = 407, + lnm_http_status_request_timeout = 408, + lnm_http_status_conflict = 409, + lnm_http_status_gone = 410, + lnm_http_status_length_required = 411, + lnm_http_status_precondition_failed = 412, + lnm_http_status_content_too_large = 413, + lnm_http_status_uri_too_long = 414, + lnm_http_status_unsupported_media_type = 415, + lnm_http_status_range_not_satisfiable = 416, + lnm_http_status_expection_failed = 417, + lnm_http_status_im_a_teapot = 418, + lnm_http_status_misdirected_request = 421, + lnm_http_status_unprocessable_content = 422, + lnm_http_status_locked = 423, + lnm_http_status_failed_dependency = 424, + lnm_http_status_too_early = 425, + lnm_http_status_upgrade_required = 426, + lnm_http_status_precondition_required = 428, + lnm_http_status_too_many_requests = 429, + lnm_http_status_request_header_fields_too_large = 431, + // 5xx + lnm_http_status_internal_server_error = 500, + lnm_http_status_method_not_implemented = 501, + lnm_http_status_bad_gateway = 502, + lnm_http_status_service_unavailable = 503, + lnm_http_status_gateway_timeout = 504, + lnm_http_status_http_status_version_not_supported = 505, + lnm_http_status_variant_also_negotiates = 506, + lnm_http_status_insufficient_storage = 507, + lnm_http_status_loop_detected = 508, + lnm_http_status_not_extended = 510, + lnm_http_status_network_authentication_required = 511 +} lnm_http_status; + +extern const char *lnm_http_header_names[]; + +typedef enum lnm_http_header { + lnm_http_header_connection = 0, + lnm_http_header_location, + lnm_http_header_content_type, + lnm_http_header_content_disposition, + lnm_http_header_server, + lnm_http_header_content_length +} lnm_http_header; + +#endif diff --git a/include/lnm/http/loop.h b/include/lnm/http/loop.h new file mode 100644 index 0000000..8e36caa --- /dev/null +++ b/include/lnm/http/loop.h @@ -0,0 +1,141 @@ +#ifndef LNM_HTTP_LOOP +#define LNM_HTTP_LOOP + +#include + +#include "lnm/common.h" +#include "lnm/http/req.h" +#include "lnm/http/res.h" + +typedef enum lnm_http_step_err { + lnm_http_step_err_done = 0, + lnm_http_step_err_io_needed, + lnm_http_step_err_close, + lnm_http_step_err_res, +} lnm_http_step_err; + +typedef lnm_http_step_err (*lnm_http_step_fn)(lnm_http_conn *conn); + +typedef lnm_err (*lnm_http_ctx_init_fn)(void **c_ctx, void *gctx); + +typedef void (*lnm_http_ctx_reset_fn)(void *c_ctx); + +typedef void (*lnm_http_ctx_free_fn)(void *c_ctx); + +/** + * Initialize a new `lnm_http_loop`. + * + * @param out where to store pointer to new `lnm_http_loop` + */ +lnm_err lnm_http_loop_init(lnm_http_loop **out, void *c_gctx, + lnm_http_ctx_init_fn ctx_init, + lnm_http_ctx_reset_fn ctx_reset, + lnm_http_ctx_free_fn ctx_free); + +/** + * Initialize a new step. + * + * @param out where to store pointer to new `lnm_http_step` + * @param fn step function + */ +lnm_err lnm_http_step_init(lnm_http_step **out, lnm_http_step_fn fn); + +/** + * Append the given step fn to the step. + * + * @param out where to store pointer to new `lnm_http_step` + * @param step step to append new step to + * @param fn step function + */ +lnm_err lnm_http_step_append(lnm_http_step **out, lnm_http_step *step, + lnm_http_step_fn fn); + +/** + * Initialize a new route of type literal. + * + * @param out where to store pointer to new `lnm_http_route` + * @param path literal path to match + * @param step step to process request with + */ +lnm_err lnm_http_route_init_literal(lnm_http_route **out, + lnm_http_method method, const char *path, + lnm_http_step *step); + +/** + * Initialize a new route of type regex. + * + * @param out where to store pointer to new `lnm_http_route` + * @param pattern regex pattern + * @param regex_group_count how many regex groups are contained in the pattern + * @param step step to process request with + */ +lnm_err lnm_http_route_init_regex(lnm_http_route **out, lnm_http_method method, + const char *pattern, int regex_group_count, + lnm_http_step *step); + +/** + * Add a new route to the HTTP route. + * + * @param hl HTTP loop to modify + * @param route route to add + */ +lnm_err lnm_http_loop_route_add(lnm_http_loop *hl, lnm_http_route *route); + +lnm_err lnm_http_loop_run(lnm_http_loop *hl, uint16_t port, int thread_count); + +void lnm_http_loop_set_api_key(lnm_http_loop *hl, const char *api_key); + +void lnm_http_loop_set_server(lnm_http_loop *hl, const char *value); + +/** + * Represents what state an HTTP loop request is currently in. + */ +typedef enum lnm_http_loop_state { + // Parse the HTTP request + lnm_http_loop_state_parse_req = 0, + // Route the request + lnm_http_loop_state_route, + // Parse specific headers (e.g. Content-Length) + lnm_http_loop_state_parse_headers, + // Execute the various steps defined for the route + lnm_http_loop_state_steps, + // Add certain automatically added headers + lnm_http_loop_state_add_headers, + // Write the response status line + lnm_http_loop_state_write_status_line, + // Write the various response headers + lnm_http_loop_state_write_headers, + // Write the request body + lnm_http_loop_state_write_body, + // Clean up the request and reset the state for a next request + lnm_http_loop_state_finish, +} lnm_http_loop_state; + +typedef struct lnm_http_loop_gctx { + struct { + lnm_http_route **arr; + size_t len; + } routes; + lnm_http_ctx_init_fn ctx_init; + lnm_http_ctx_reset_fn ctx_reset; + lnm_http_ctx_free_fn ctx_free; + const char *api_key; + const char *server; + void *c; +} lnm_http_loop_gctx; + +typedef struct lnm_http_loop_ctx { + lnm_http_loop_state state; + lnm_http_req req; + lnm_http_res res; + lnm_http_route *route; + lnm_http_step *cur_step; + lnm_http_loop_gctx *g; + void *c; +} lnm_http_loop_ctx; + +lnm_http_step_err lnm_http_loop_step_body_to_buf(lnm_http_conn *conn); + +lnm_http_step_err lnm_http_loop_step_auth(lnm_http_conn *conn); + +#endif diff --git a/include/lnm/http/req.h b/include/lnm/http/req.h new file mode 100644 index 0000000..06f2743 --- /dev/null +++ b/include/lnm/http/req.h @@ -0,0 +1,111 @@ +#ifndef LNM_HTTP_REQ +#define LNM_HTTP_REQ + +#include +#include +#include + +#include "picohttpparser.h" + +#include "lnm/common.h" +#include "lnm/http/consts.h" + +#define LNM_HTTP_MAX_REQ_HEADERS 32 +#define LNM_HTTP_MAX_REGEX_GROUPS 4 + +typedef struct lnm_http_req_header { + struct { + size_t o; + size_t len; + } name; + struct { + size_t o; + size_t len; + } value; +} lnm_http_req_header; + +/** + * Represents the parsed HTTP request + */ +typedef struct lnm_http_req { + struct { + char *s; + size_t len; + bool owned; + } buf; + int minor_version; + lnm_http_method method; + struct { + size_t o; + size_t len; + regmatch_t groups[LNM_HTTP_MAX_REGEX_GROUPS]; + } path; + struct { + size_t o; + size_t len; + } query; + struct { + lnm_http_req_header arr[LNM_HTTP_MAX_REQ_HEADERS]; + size_t len; + } headers; + struct { + uint64_t expected_len; + uint64_t len; + char *buf; + bool owned; + } body; +} lnm_http_req; + +typedef enum lnm_http_parse_err { + lnm_http_parse_err_ok = 0, + lnm_http_parse_err_incomplete, + lnm_http_parse_err_invalid, + lnm_http_parse_err_unknown_method, +} lnm_http_parse_err; + +/** + * Try to parse the given buffer into an HTTP request. + * + * @param req request to store parsed data in + * @param buf buffer to parse; might be modified + * @param len length of buf + */ +lnm_http_parse_err lnm_http_req_parse(lnm_http_req *req, char *buf, size_t len); + +/** + * Reset the given request object, free'ing all its relevant parts and allowing + * it to be reused as a new object. + * + * @param req object to reset + */ +void lnm_http_req_reset(lnm_http_req *req); + +/** + * Retrieve a specific header from the request. + * + * Pointers retrieved from this function should never be used between step + * functions; simply request the header again if you need to. + * + * @param out where to write pointer to header value + * @param out_len where to store length of out value + * @param req request to look for header in + * @param type type of header to look for + */ +lnm_err lnm_http_req_header_get(const char **out, size_t *out_len, + lnm_http_req *req, lnm_http_header type); + +/** + * Retrieve a specific header from the request by specifying its name. + * + * Pointers retrieved from this function should never be used between step + * functions; simply request the header again if you need to. + * + * @param out where to write pointer to header value + * @param out_len where to store length of out value + * @param req request to look for header in + * @param name name of the header; matches case-insensitive + */ +lnm_err lnm_http_req_header_get_s(const char **out, size_t *out_len, + lnm_http_req *req, const char *name); + +#endif diff --git a/include/lnm/http/res.h b/include/lnm/http/res.h new file mode 100644 index 0000000..ee2a079 --- /dev/null +++ b/include/lnm/http/res.h @@ -0,0 +1,119 @@ +#ifndef LNM_HTTP_RES +#define LNM_HTTP_RES + +#include +#include + +#include "lnm/common.h" +#include "lnm/http/consts.h" + +typedef lnm_err (*data_fn)(uint64_t *written, char *buf, lnm_http_conn *conn, + uint64_t offset, uint64_t len); + +/** + * Linked list elements used to store the response headers + */ +typedef struct lnm_http_res_header { + struct { + char *s; + size_t len; + bool owned; + } name; + struct { + char *s; + size_t len; + bool owned; + } value; + struct lnm_http_res_header *next; +} lnm_http_res_header; + +typedef enum lnm_http_res_body_type { + lnm_http_res_body_type_file = 0, + lnm_http_res_body_type_buf, + lnm_http_res_body_type_fn, +} lnm_http_res_body_type; + +typedef struct lnm_http_res { + lnm_http_status status; + struct { + lnm_http_res_header *head; + lnm_http_res_header *current; + } headers; + struct { + struct { + char *buf; + FILE *f; + data_fn fn; + } data; + uint64_t len; + bool owned; + lnm_http_res_body_type type; + } body; + // General-purpose; meaning depends on the current state + uint64_t written; +} lnm_http_res; + +/** + * Add a new header of a known type to the response + * + * @param type type of header + * @param value null-terminated string containing the value of the header + * @param value_owned whether to take ownership of the value pointer; if false, + * free'ing the buffer is the caller's responsibility + */ +lnm_err lnm_http_res_add_header(lnm_http_res *res, lnm_http_header type, + char *value, bool value_owned); + +/** + * Add a new header of a known type to the response with a given value length. + * + * @param type type of header + * @param value string of length `value_len` containing the value of the header + * @param value_len length of value + * @param value_owned whether to take ownership of the value pointer; if false, + * free'ing the buffer is the caller's responsibility + */ +lnm_err lnm_http_res_add_header_len(lnm_http_res *res, lnm_http_header type, + char *value, size_t value_len, + bool value_owned); + +/** + * Set the request body to the given file pointer. + * + * @param res response to modify + * @param f file pointer to use as data + * @param len expected length of the file + * @param owned whether to take ownership of the file pointer + */ +void lnm_http_res_body_set_file(lnm_http_res *res, FILE *f, size_t len, + bool owned); + +/** + * Set the request body to the given buffer. + * + * @param res response to modify + * @param buf buffer to use as data + * @param len length of the buffer + * @param owned whether to take ownership of the file pointer + */ +void lnm_http_res_body_set_buf(lnm_http_res *res, char *buf, size_t len, + bool owned); + +/** + * Set the request body to be read from the given data function. + * + * @param res response to modify + * @param fn data reader function + * @param len expected length of the response + */ +void lnm_http_res_body_set_fn(lnm_http_res *res, data_fn fn, size_t len); + +/** + * Reset the given response object, properly free'ing any allocated buffers, + * allowing it to be reused for later connections. + * + * @param res res to reset + */ +void lnm_http_res_reset(lnm_http_res *res); + +#endif diff --git a/include/lnm/log.h b/include/lnm/log.h new file mode 100644 index 0000000..42f27de --- /dev/null +++ b/include/lnm/log.h @@ -0,0 +1,47 @@ +#ifndef LNM_LOG +#define LOG + +#include + +#include "lnm/common.h" + +typedef struct lnm_logger lnm_logger; + +typedef enum lnm_log_level { + lnm_log_level_debug = 0, + lnm_log_level_info, + lnm_log_level_notice, + lnm_log_level_warning, + lnm_log_level_error, + lnm_log_level_critical +} lnm_log_level; + +extern const char *lnm_log_level_names[]; + +/** + * Initialize the global logger. + */ +lnm_err lnm_log_init_global(); + +/** + * Register stdout as one of the streams for the global logger. + */ +lnm_err lnm_log_register_stdout(lnm_log_level level); + +void lnm_log(lnm_log_level level, const char *section, const char *fmt, ...) + __attribute__((format(printf, 3, 4))); + +#define lnm_ldebug(section, fmt, ...) \ + lnm_log(lnm_log_level_debug, section, fmt, __VA_ARGS__) +#define lnm_linfo(section, fmt, ...) \ + lnm_log(lnm_log_level_info, section, fmt, __VA_ARGS__) +#define lnm_lnotice(section, fmt, ...) \ + lnm_log(lnm_log_level_notice, section, fmt, __VA_ARGS__) +#define lnm_lwarning(section, fmt, ...) \ + lnm_log(lnm_log_level_warning, section, fmt, __VA_ARGS__) +#define lnm_lerror(section, fmt, ...) \ + lnm_log(lnm_log_level_error, section, fmt, __VA_ARGS__) +#define lnm_lcritical(section, fmt, ...) \ + lnm_log(lnm_log_level_critical, section, fmt, __VA_ARGS__) + +#endif diff --git a/include/lnm/loop.h b/include/lnm/loop.h new file mode 100644 index 0000000..7ca372c --- /dev/null +++ b/include/lnm/loop.h @@ -0,0 +1,54 @@ +#ifndef LNM_LOOP +#define LNM_LOOP + +#include +#include +#include + +#include "lnm/common.h" + +#define LNM_LOOP_BUF_SIZE 2048 + +typedef enum { + lnm_loop_state_req = 0, + lnm_loop_state_res, + lnm_loop_state_end, +} lnm_loop_state; + +typedef struct lnm_loop_conn { + int fd; + lnm_loop_state state; + void *ctx; + struct { + char buf[LNM_LOOP_BUF_SIZE]; + size_t size; + size_t read; + } r; + struct { + char buf[LNM_LOOP_BUF_SIZE]; + size_t size; + } w; +} lnm_loop_conn; + +typedef struct lnm_loop { + int listen_fd; + int epoll_fd; + atomic_int open; + void *gctx; + lnm_err (*ctx_init)(void **out, void *gctx); + void (*ctx_free)(void *ctx); + void (*data_read)(lnm_loop_conn *conn); + void (*data_write)(lnm_loop_conn *conn); +} lnm_loop; + +lnm_err lnm_loop_init(lnm_loop **out, void *gctx, + lnm_err (*ctx_init)(void **out, void *gctx), + void (*ctx_free)(void *ctx), + void (*data_read)(lnm_loop_conn *conn), + void (*data_write)(lnm_loop_conn *conn)); + +lnm_err lnm_loop_setup(lnm_loop *l, uint16_t port); + +lnm_err lnm_loop_run(lnm_loop *l, int thread_count); + +#endif diff --git a/include/picohttpparser.h b/include/picohttpparser.h new file mode 100644 index 0000000..07537cf --- /dev/null +++ b/include/picohttpparser.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef picohttpparser_h +#define picohttpparser_h + +#include + +#ifdef _MSC_VER +#define ssize_t intptr_t +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* contains name and value of a header (name == NULL if is a continuing line + * of a multiline header */ +struct phr_header { + const char *name; + size_t name_len; + const char *value; + size_t value_len; +}; + +/* returns number of bytes consumed if successful, -2 if request is partial, + * -1 if failed */ +int phr_parse_request(const char *buf, size_t len, const char **method, size_t *method_len, const char **path, size_t *path_len, + int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* ditto */ +int phr_parse_response(const char *_buf, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* ditto */ +int phr_parse_headers(const char *buf, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len); + +/* should be zero-filled before start */ +struct phr_chunked_decoder { + size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ + char consume_trailer; /* if trailing headers should be consumed */ + char _hex_count; + char _state; +}; + +/* the function rewrites the buffer given as (buf, bufsz) removing the chunked- + * encoding headers. When the function returns without an error, bufsz is + * updated to the length of the decoded data available. Applications should + * repeatedly call the function while it returns -2 (incomplete) every time + * supplying newly arrived data. If the end of the chunked-encoded data is + * found, the function returns a non-negative number indicating the number of + * octets left undecoded, that starts from the offset returned by `*bufsz`. + * Returns -1 on error. + */ +ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *bufsz); + +/* returns if the chunked decoder is in middle of chunked data */ +int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/_include/lnm/http/loop_internal.h b/src/_include/lnm/http/loop_internal.h new file mode 100644 index 0000000..4d9e865 --- /dev/null +++ b/src/_include/lnm/http/loop_internal.h @@ -0,0 +1,69 @@ +#ifndef LNM_HTTP_LOOP_INTERNAL +#define LNM_HTTP_LOOP_INTERNAL + +#include + +#include "lnm/http/loop.h" + +typedef struct lnm_http_step { + lnm_http_step_fn fn; + struct lnm_http_step *next; +} lnm_http_step; + +typedef enum lnm_http_route_type { + lnm_http_route_type_literal = 0, + lnm_http_route_type_regex, +} lnm_http_route_type; + +typedef struct lnm_http_route { + union { + regex_t *regex; + const char *s; + } route; + lnm_http_method method; + lnm_http_route_type type; + int regex_group_count; + lnm_http_step *step; +} lnm_http_route; + +/** + * Initialize a new empty route. + * + * @param out where to store pointer to new `lnm_http_route` + */ +lnm_err lnm_http_route_init(lnm_http_route **out); + +/** + * Initialize a first step. + * + * @param out where to store pointer to new `lnm_http_step` + * @param fn step function associated with the step + */ +lnm_err lnm_http_step_init(lnm_http_step **out, lnm_http_step_fn fn); + +/** + * Initialize a new global context object. + * + * @param out where to store pointer to new `lnm_http_loop_gctx` + */ +lnm_err lnm_http_loop_gctx_init(lnm_http_loop_gctx **out, void *c_gctx, + lnm_http_ctx_init_fn ctx_init, + lnm_http_ctx_reset_fn ctx_reset, + lnm_http_ctx_free_fn ctx_free); + +/** + * Initialize a new context. + * + * @param out where to store pointer to new object + * @param gctx global context for the loop + */ +lnm_err lnm_http_loop_ctx_init(lnm_http_loop_ctx **out, + lnm_http_loop_gctx *gctx); + +void lnm_http_loop_ctx_reset(lnm_http_loop_ctx *ctx); + +void lnm_http_loop_ctx_free(lnm_http_loop_ctx *ctx); + +void lnm_http_loop_process(lnm_http_conn *conn); + +#endif diff --git a/src/_include/lnm/log_internal.h b/src/_include/lnm/log_internal.h new file mode 100644 index 0000000..ebcd243 --- /dev/null +++ b/src/_include/lnm/log_internal.h @@ -0,0 +1,26 @@ +#ifndef LNM_LOG_INTERNAL +#define LNM_LOG_INTERNAL + +#include "lnm/log.h" + +typedef enum lnm_logger_stream_type { + lnm_logger_stream_type_file = 0 +} lnm_logger_stream_type; + +typedef struct lnm_logger_stream { + void *ptr; + lnm_logger_stream_type type; + lnm_log_level level; +} lnm_logger_stream; + +struct lnm_logger { + struct { + lnm_logger_stream **arr; + size_t len; + } streams; +}; + +lnm_err lnm_logger_stream_register(lnm_logger *logger, + lnm_logger_stream *stream); + +#endif diff --git a/src/_include/lnm/loop_internal.h b/src/_include/lnm/loop_internal.h new file mode 100644 index 0000000..a5e70a8 --- /dev/null +++ b/src/_include/lnm/loop_internal.h @@ -0,0 +1,14 @@ +#ifndef LNM_LOOP_INTERNAL +#define LNM_LOOP_INTERNAL + +#include "lnm/loop.h" + +lnm_err lnm_loop_conn_init(lnm_loop_conn **out, lnm_loop *l); + +void lnm_loop_conn_free(lnm_loop *l, lnm_loop_conn *conn); + +lnm_err lnm_loop_accept(lnm_loop *l); + +void lnm_loop_conn_io(lnm_loop *l, lnm_loop_conn *conn); + +#endif diff --git a/src/http/lnm_http_consts.c b/src/http/lnm_http_consts.c new file mode 100644 index 0000000..cfbee40 --- /dev/null +++ b/src/http/lnm_http_consts.c @@ -0,0 +1,101 @@ +#include "lnm/http/consts.h" + +const char *lnm_http_method_names[] = {"GET", "POST", "PUT", + "PATCH", "DELETE", "HEAD"}; +const size_t lnm_http_method_names_len = + sizeof(lnm_http_method_names) / sizeof(lnm_http_method_names[0]); + +// clang-format off + +const char *lnm_http_status_names[][32] = { + // 1xx + { + "Continue", // 100 + "Switching Protocols", // 101, + "Processing", // 102 + "Early Hints", // 103 + }, + // 2xx + { + "OK", // 200 + "Created", // 201 + "Accepted", // 202 + "Non-Authoritative Information", // 203 + "No Content", // 204 + "Reset Content", // 205 + "Partial Content", // 206 + "Multi-Status", // 207 + "Already Reported", // 208 + }, + // 3xx + { + "Multiple Choices", // 300 + "Moved Permanently", // 301 + "Found", // 302 + "See Other", // 303 + "Not Modified", // 304 + NULL, // 305 + NULL, // 306 + "Temporary Redirect", // 307 + "Permanent Redirect", // 308 + }, + // 4xx + { + "Bad Request", // 400 + "Unauthorized", // 401 + "Payment Required", // 402 + "Forbidden", // 403 + "Not Found", // 404 + "Method Not Allowed", // 405 + "Not Acceptable", // 406 + "Proxy Authentication Required", // 407 + "Request Timeout", // 408 + "Conflict", // 409 + "Gone", // 410 + "Length Required", // 411 + "Precondition Failed", // 412 + "Content Too Large", // 413 + "URI Too Long", // 414 + "Unsupported Media Type", // 415 + "Range Not Satisfiable", // 416 + "Expectation Failed", // 417 + "I'm a teapot", // 418 + NULL, // 419 + NULL, // 420 + "Misdirected Request", // 421 + "Unprocessable Content", // 422 + "Locked", // 423 + "Failed Dependency", // 424 + "Too Early", // 425 + "Upgrade Required", // 426 + NULL, // 427 + "Precondition Required", // 428 + "Too Many Requests", // 429 + NULL, // 430 + "Request Header Fields Too Large", // 431 + }, + // 5xx + { + "Internal Server Error", // 500 + "Not Implemented", // 501 + "Bad Gateway", // 502 + "Service Unavailable", // 503 + "Gateway Timeout", // 504 + "HTTP Version Not Supported", // 505 + "Variant Also Negotiates", // 506 + "Insufficient Storage", // 507 + "Loop Detected", // 508 + NULL, // 509 + "Not Extended", // 510 + "Network Authentication Required" // 511 + }, +}; + +const char *lnm_http_header_names[] = { + "Connection", + "Location", + "Content-Type", + "Content-Disposition", + "Server", + "Content-Length" +}; diff --git a/src/http/lnm_http_loop.c b/src/http/lnm_http_loop.c new file mode 100644 index 0000000..784126e --- /dev/null +++ b/src/http/lnm_http_loop.c @@ -0,0 +1,138 @@ +#include + +#include "lnm/common.h" +#include "lnm/http/loop.h" +#include "lnm/http/loop_internal.h" +#include "lnm/loop_internal.h" + +lnm_err lnm_http_loop_init(lnm_http_loop **out, void *c_gctx, + lnm_http_ctx_init_fn ctx_init, + lnm_http_ctx_reset_fn ctx_reset, + lnm_http_ctx_free_fn ctx_free) { + lnm_http_loop *hl = calloc(1, sizeof(lnm_http_loop)); + + if (hl == NULL) { + return lnm_err_failed_alloc; + } + + LNM_RES2(lnm_http_loop_gctx_init((lnm_http_loop_gctx **)&hl->gctx, c_gctx, + ctx_init, ctx_reset, ctx_free), + free(hl)); + + hl->data_read = lnm_http_loop_process; + hl->data_write = lnm_http_loop_process; + hl->ctx_init = (lnm_err(*)(void **, void *))lnm_http_loop_ctx_init; + hl->ctx_free = (void (*)(void *))lnm_http_loop_ctx_free; + *out = hl; + + return lnm_err_ok; +} + +lnm_err lnm_http_step_init(lnm_http_step **out, lnm_http_step_fn fn) { + lnm_http_step *step = calloc(1, sizeof(lnm_http_step)); + + if (step == NULL) { + return lnm_err_failed_alloc; + } + + step->fn = fn; + *out = step; + + return lnm_err_ok; +} + +lnm_err lnm_http_step_append(lnm_http_step **out, lnm_http_step *step, + lnm_http_step_fn fn) { + LNM_RES(lnm_http_step_init(out, fn)); + + if (step != NULL) { + step->next = *out; + } + + return lnm_err_ok; +} + +lnm_err lnm_http_route_init(lnm_http_route **out) { + lnm_http_route *route = calloc(1, sizeof(lnm_http_route)); + + if (route == NULL) { + return lnm_err_failed_alloc; + } + + *out = route; + + return lnm_err_ok; +} + +lnm_err lnm_http_route_init_literal(lnm_http_route **out, + lnm_http_method method, const char *path, + lnm_http_step *step) { + LNM_RES(lnm_http_route_init(out)); + + (*out)->type = lnm_http_route_type_literal; + (*out)->method = method; + (*out)->route.s = path; + (*out)->step = step; + + return lnm_err_ok; +} + +lnm_err lnm_http_route_init_regex(lnm_http_route **out, lnm_http_method method, + const char *pattern, int regex_group_count, + lnm_http_step *step) { + regex_t *regex = calloc(1, sizeof(regex_t)); + + if (regex == NULL) { + return lnm_err_failed_alloc; + } + + if (regcomp(regex, pattern, REG_EXTENDED) != 0) { + free(regex); + return lnm_err_bad_regex; + } + + LNM_RES2(lnm_http_route_init(out), free(regex)); + + (*out)->method = method; + (*out)->type = lnm_http_route_type_regex; + (*out)->route.regex = regex; + (*out)->regex_group_count = regex_group_count; + (*out)->step = step; + + return lnm_err_ok; +} + +lnm_err lnm_http_loop_route_add(lnm_http_loop *hl, lnm_http_route *route) { + lnm_http_loop_gctx *gctx = hl->gctx; + + lnm_http_route **new_routes = + gctx->routes.len > 0 + ? realloc(gctx->routes.arr, + (gctx->routes.len + 1) * sizeof(lnm_http_route *)) + : malloc(sizeof(lnm_http_route *)); + + if (new_routes == NULL) { + return lnm_err_failed_alloc; + } + + new_routes[gctx->routes.len] = route; + gctx->routes.arr = new_routes; + gctx->routes.len++; + + return lnm_err_ok; +} + +lnm_err lnm_http_loop_run(lnm_http_loop *hl, uint16_t port, int thread_count) { + LNM_RES(lnm_loop_setup(hl, port)); + return lnm_loop_run(hl, thread_count); +} + +void lnm_http_loop_set_api_key(lnm_http_loop *hl, const char *api_key) { + lnm_http_loop_gctx *gctx = hl->gctx; + gctx->api_key = api_key; +} + +void lnm_http_loop_set_server(lnm_http_loop *hl, const char *server) { + lnm_http_loop_gctx *gctx = hl->gctx; + gctx->server = server; +} diff --git a/src/http/lnm_http_loop_ctx.c b/src/http/lnm_http_loop_ctx.c new file mode 100644 index 0000000..87741e2 --- /dev/null +++ b/src/http/lnm_http_loop_ctx.c @@ -0,0 +1,55 @@ +#include "lnm/http/loop_internal.h" + +lnm_err lnm_http_loop_gctx_init(lnm_http_loop_gctx **out, void *c_gctx, + lnm_http_ctx_init_fn ctx_init, + lnm_http_ctx_reset_fn ctx_reset, + lnm_http_ctx_free_fn ctx_free) { + lnm_http_loop_gctx *gctx = calloc(1, sizeof(lnm_http_loop_gctx)); + + if (gctx == NULL) { + return lnm_err_failed_alloc; + } + + gctx->c = c_gctx; + gctx->ctx_init = ctx_init; + gctx->ctx_reset = ctx_reset; + gctx->ctx_free = ctx_free; + + *out = gctx; + + return lnm_err_ok; +} + +lnm_err lnm_http_loop_ctx_init(lnm_http_loop_ctx **out, + lnm_http_loop_gctx *gctx) { + lnm_http_loop_ctx *ctx = calloc(1, sizeof(lnm_http_loop_ctx)); + + if (ctx == NULL) { + return lnm_err_failed_alloc; + } + + LNM_RES2(gctx->ctx_init(&ctx->c, gctx), free(ctx)); + + ctx->g = gctx; + *out = ctx; + + return lnm_err_ok; +} + +void lnm_http_loop_ctx_reset(lnm_http_loop_ctx *ctx) { + ctx->g->ctx_reset(ctx->c); + + lnm_http_req_reset(&ctx->req); + lnm_http_res_reset(&ctx->res); + + ctx->route = NULL; + ctx->cur_step = NULL; +} + +void lnm_http_loop_ctx_free(lnm_http_loop_ctx *ctx) { + lnm_http_loop_ctx_reset(ctx); + + ctx->g->ctx_free(ctx->c); + + free(ctx); +} diff --git a/src/http/lnm_http_loop_process.c b/src/http/lnm_http_loop_process.c new file mode 100644 index 0000000..e16e18c --- /dev/null +++ b/src/http/lnm_http_loop_process.c @@ -0,0 +1,391 @@ +#include +#include +#include + +#include "lnm/http/consts.h" +#include "lnm/http/loop.h" +#include "lnm/http/loop_internal.h" +#include "lnm/http/req.h" +#include "lnm/log.h" +#include "lnm/loop.h" +#include "lnm/loop_internal.h" + +static const char *section = "http"; + +/* static const lnm_http_loop_state lnm_http_loop_state_first_req = + * lnm_http_loop_state_parse_req; */ +static const lnm_http_loop_state lnm_http_loop_state_first_res = + lnm_http_loop_state_add_headers; + +void lnm_http_loop_process_parse_req(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + + lnm_http_parse_err res = lnm_http_req_parse( + &ctx->req, &conn->r.buf[conn->r.read], conn->r.size - conn->r.read); + + switch (res) { + case lnm_http_parse_err_ok: + conn->r.read += ctx->req.buf.len; + ctx->state = lnm_http_loop_state_route; + + lnm_linfo(section, "%s %.*s HTTP/1.%i", + lnm_http_method_names[ctx->req.method], (int)ctx->req.path.len, + ctx->req.buf.s + ctx->req.path.o, ctx->req.minor_version); + break; + case lnm_http_parse_err_incomplete: + // If the request is already the size of the read buffer, we close the + // request. Otherwise, we wait for anything read + if (conn->r.size - conn->r.read == LNM_LOOP_BUF_SIZE) { + lnm_linfo(section, "Received request larger than buffer (%i bytes)", + LNM_LOOP_BUF_SIZE); + + conn->state = lnm_loop_state_end; + } + break; + case lnm_http_parse_err_invalid: + lnm_linfo(section, "%s", "Received invalid request"); + + conn->state = lnm_loop_state_end; + break; + case lnm_http_parse_err_unknown_method: + ctx->res.status = lnm_http_status_method_not_implemented; + ctx->state = lnm_http_loop_state_first_res; + break; + } +} + +void lnm_http_loop_process_route(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_loop_gctx *gctx = ctx->g; + + // 0: no match + // 1: matched route, but not method + // 2: fully matched route + int match_level = 0; + lnm_http_route *route; + + for (size_t i = 0; i < gctx->routes.len && match_level < 3; i++) { + route = gctx->routes.arr[i]; + bool matched_path = false; + + switch (route->type) { + case lnm_http_route_type_literal: + matched_path = strncmp(route->route.s, ctx->req.buf.s + ctx->req.path.o, + ctx->req.path.len) == 0; + break; + case lnm_http_route_type_regex: + matched_path = + regexec(route->route.regex, ctx->req.buf.s + ctx->req.path.o, + LNM_HTTP_MAX_REGEX_GROUPS, ctx->req.path.groups, 0) == 0; + break; + } + + // GET routes also automatically route HEAD requests + bool matched_method = route->method == ctx->req.method || + (route->method == lnm_http_method_get && + ctx->req.method == lnm_http_method_head); + int new_match_level = 2 * matched_path + matched_method; + + // Remember the previous match levels so we can return the correct status + // message + match_level = match_level < new_match_level ? new_match_level : match_level; + } + + switch (match_level) { + case 0: + case 1: + ctx->res.status = lnm_http_status_not_found; + ctx->state = lnm_http_loop_state_first_res; + break; + case 2: + ctx->res.status = lnm_http_status_method_not_allowed; + ctx->state = lnm_http_loop_state_first_res; + break; + case 3: + ctx->route = route; + ctx->cur_step = route->step; + ctx->state = lnm_http_loop_state_parse_headers; + break; + } +} + +void lnm_http_loop_process_parse_headers(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_req *req = &ctx->req; + + const char *value; + size_t value_len; + if (lnm_http_req_header_get(&value, &value_len, req, + lnm_http_header_content_length) == lnm_err_ok) { + req->body.expected_len = lnm_atoi(value, value_len); + } + + ctx->state = lnm_http_loop_state_steps; +} + +void lnm_http_loop_process_steps(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_step *step = NULL; + + // Loop until we either: + // - reach the end of the chain of steps, indicated by NULL + // - have a step that's waiting for I/O + while ((ctx->cur_step != NULL) && (step != ctx->cur_step)) { + step = ctx->cur_step; + + switch (step->fn(conn)) { + case lnm_http_step_err_done: + ctx->cur_step = ctx->cur_step->next; + break; + case lnm_http_step_err_io_needed: + break; + case lnm_http_step_err_close: + conn->state = lnm_loop_state_end; + break; + case lnm_http_step_err_res: + ctx->state = lnm_http_loop_state_first_res; + break; + } + } + + if (ctx->cur_step == NULL) { + ctx->state = lnm_http_loop_state_add_headers; + } +} + +void lnm_http_loop_state_process_add_headers(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_res *res = &ctx->res; + + uint64_t digits = lnm_digits(res->body.len); + char *buf = malloc(digits + 1); + + if (buf == NULL) { + conn->state = lnm_loop_state_end; + + return; + } + + sprintf(buf, "%lu", res->body.len); + lnm_http_res_add_header_len(res, lnm_http_header_content_length, buf, digits, + true); + + if (ctx->g->server != NULL) { + lnm_http_res_add_header(res, lnm_http_header_server, (char *)ctx->g->server, + false); + } + + if (res->status == 0) { + res->status = lnm_http_status_ok; + } + + lnm_linfo(section, "%i %s", res->status, + lnm_http_status_names[res->status / 100 - 1][res->status % 100]); + + ctx->state = lnm_http_loop_state_write_status_line; +} + +// This function is intentionally written inefficiently for now, as it will most +// likely only have to run once for each response +void lnm_http_loop_process_write_status_line(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_res *res = &ctx->res; + + const char *response_type_name = + lnm_http_status_names[res->status / 100 - 1][res->status % 100]; + + // First we calculate the size of the start of the header + size_t buf_size = + snprintf(NULL, 0, "HTTP/1.1 %i %s\n", res->status, response_type_name); + char buf[buf_size + 1]; + sprintf(buf, "HTTP/1.1 %i %s\n", res->status, response_type_name); + + size_t to_write = + LNM_MIN(buf_size - res->written, LNM_LOOP_BUF_SIZE - conn->w.size); + memcpy(&conn->w.buf[conn->w.size], &buf[res->written], to_write); + + conn->w.size += to_write; + res->written += to_write; + + if (res->written == buf_size) { + res->written = 0; + ctx->state = lnm_http_loop_state_write_headers; + } +} + +void lnm_http_loop_process_write_headers(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_res *res = &ctx->res; + + lnm_http_res_header *header; + + // Loop as long as we can still write new data and have headers to write + while ((conn->w.size < LNM_LOOP_BUF_SIZE) && + ((header = res->headers.current) != NULL)) { + size_t buf_len = header->name.len + 2 + header->value.len + 1; + + // Here, we also constantly calculate the entire buffer as we assume each + // header will be written in one go + char buf[buf_len]; + memcpy(buf, header->name.s, header->name.len); + memcpy(&buf[header->name.len + 2], header->value.s, header->value.len); + buf[header->name.len] = ':'; + buf[header->name.len + 1] = ' '; + buf[buf_len - 1] = '\n'; + + size_t to_write = + LNM_MIN(buf_len - res->written, LNM_LOOP_BUF_SIZE - conn->w.size); + memcpy(&conn->w.buf[conn->w.size], &buf[res->written], to_write); + + conn->w.size += to_write; + res->written += to_write; + + if (res->written == buf_len) { + res->written = 0; + res->headers.current = res->headers.current->next; + } + } + + // The headers should end with an additional newline. If there's no space left + // in the write buffer, we don't switch states so we can re-try this write + // later + if ((res->headers.current == NULL) && (conn->w.size < LNM_LOOP_BUF_SIZE)) { + conn->w.buf[conn->w.size] = '\n'; + conn->w.size++; + + // HEAD requests function exactly the same as GET requests, except that they + // skip the body writing part + ctx->state = + ctx->req.method != lnm_http_method_head && ctx->res.body.len > 0 + ? lnm_http_loop_state_write_body + : lnm_http_loop_state_finish; + } +} + +void lnm_http_loop_process_write_body(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_res *res = &ctx->res; + + size_t to_write = + LNM_MIN(res->body.len - res->written, LNM_LOOP_BUF_SIZE - conn->w.size); + size_t written = 0; + + switch (res->body.type) { + case lnm_http_res_body_type_buf: + memcpy(&conn->w.buf[conn->w.size], &res->body.data.buf[res->written], + to_write); + written = to_write; + break; + case lnm_http_res_body_type_file: + written = fread(&conn->w.buf[conn->w.size], 1, to_write, res->body.data.f); + + if ((written == 0) && (ferror(res->body.data.f) != 0)) { + ctx->state = lnm_http_loop_state_finish; + } + break; + case lnm_http_res_body_type_fn: + if (res->body.data.fn(&written, &conn->w.buf[conn->w.size], conn, + res->written, to_write) != lnm_err_ok) { + ctx->state = lnm_http_loop_state_finish; + } + break; + } + + conn->w.size += written; + res->written += written; + + if (res->written == res->body.len) { + ctx->state = lnm_http_loop_state_finish; + } +} + +void lnm_http_loop_process_finish(lnm_http_conn *conn) { + // First we ensure the write buffer is fully flushed + if (conn->w.size > 0) { + return; + } + + lnm_http_loop_ctx *ctx = conn->ctx; + lnm_http_loop_ctx_reset(ctx); + + ctx->state = lnm_http_loop_state_parse_req; +} + +void (*process_fns[])(lnm_http_conn *conn) = { + lnm_http_loop_process_parse_req, + lnm_http_loop_process_route, + lnm_http_loop_process_parse_headers, + lnm_http_loop_process_steps, + lnm_http_loop_state_process_add_headers, + lnm_http_loop_process_write_status_line, + lnm_http_loop_process_write_headers, + lnm_http_loop_process_write_body, + lnm_http_loop_process_finish, +}; + +lnm_loop_state state_map[] = { + // parse_req + lnm_loop_state_req, + // route + lnm_loop_state_req, + // parse_headers + lnm_loop_state_req, + // steps + lnm_loop_state_req, + // add_headers + lnm_loop_state_req, + // write_status_line + lnm_loop_state_res, + // write_headers + lnm_loop_state_res, + // write_body + lnm_loop_state_res, + // finish + lnm_loop_state_res, +}; + +void lnm_http_loop_process(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + + lnm_http_loop_state http_loop_state; + lnm_loop_state loop_state = conn->state; + + // We stop processing if: + // - the event loop state has been explicitely changed inside the executed + // step, as we need to switch to the other I/O loop + // - the event loop state needs to be changed because the next step should be + // run in another event loop state + // - the process fn returned without changing the HTTP loop state, indicating + // it's waiting for I/O + do { + http_loop_state = ctx->state; + + process_fns[http_loop_state](conn); + } while ((conn->state == state_map[ctx->state]) && + (http_loop_state != ctx->state)); + + // Check required to prevent overwriting manually set event loop state + conn->state = conn->state == loop_state ? state_map[ctx->state] : conn->state; + + // We move the request to a dedicated buffer if the read buffer needs to be + // reused + if ((conn->state == lnm_loop_state_req) && (conn->state == loop_state) && + (!ctx->req.buf.owned) && (ctx->req.buf.len > 0)) { + char *buf = malloc(ctx->req.buf.len); + + if (buf == NULL) { + lnm_lerror(section, + "Failed to allocate request buffer; closing connection %i", + conn->fd); + + conn->state = lnm_loop_state_end; + } else { + memcpy(buf, ctx->req.buf.s, ctx->req.buf.len); + ctx->req.buf.s = buf; + ctx->req.buf.owned = true; + + lnm_ldebug(section, "Allocated request buffer for connection %i", + conn->fd); + } + } +} diff --git a/src/http/lnm_http_loop_steps.c b/src/http/lnm_http_loop_steps.c new file mode 100644 index 0000000..2552e09 --- /dev/null +++ b/src/http/lnm_http_loop_steps.c @@ -0,0 +1,44 @@ +#include + +#include "lnm/http/consts.h" +#include "lnm/http/loop.h" +#include "lnm/loop.h" + +lnm_http_step_err lnm_http_loop_step_body_to_buf(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + + if (ctx->req.body.buf == NULL) { + ctx->req.body.buf = malloc(ctx->req.body.expected_len * sizeof(char)); + ctx->req.body.len = 0; + } + + size_t to_read = LNM_MIN(conn->r.size - conn->r.read, + ctx->req.body.expected_len - ctx->req.body.len); + memcpy(&ctx->req.body.buf[ctx->req.body.len], &conn->r.buf[conn->r.read], + to_read); + ctx->req.body.len += to_read; + conn->r.read += to_read; + + return ctx->req.body.len == ctx->req.body.expected_len + ? lnm_http_step_err_done + : lnm_http_step_err_io_needed; +} + +lnm_http_step_err lnm_http_loop_step_auth(lnm_http_conn *conn) { + lnm_http_loop_ctx *ctx = conn->ctx; + + // If there's no API key, requests are always authorized + bool authorized = ctx->g->api_key == NULL; + + const char *value; + size_t value_len; + + if (!authorized && lnm_http_req_header_get_s(&value, &value_len, &ctx->req, + "X-Api-Key") == lnm_err_ok) { + authorized = (value_len == strlen(ctx->g->api_key)) && + (memcmp(value, ctx->g->api_key, value_len) == 0); + } + + ctx->res.status = authorized ? ctx->res.status : lnm_http_status_unauthorized; + return authorized ? lnm_http_step_err_done : lnm_http_step_err_res; +} diff --git a/src/http/lnm_http_req.c b/src/http/lnm_http_req.c new file mode 100644 index 0000000..50be3af --- /dev/null +++ b/src/http/lnm_http_req.c @@ -0,0 +1,114 @@ +#include +#include + +#include "lnm/common.h" +#include "lnm/http/consts.h" +#include "lnm/http/loop.h" +#include "lnm/http/req.h" + +lnm_http_parse_err lnm_http_req_parse(lnm_http_req *req, char *buf, + size_t len) { + const char *method; + char *path; + size_t method_len, path_len; + size_t num_headers = LNM_HTTP_MAX_REQ_HEADERS; + struct phr_header headers[LNM_HTTP_MAX_REQ_HEADERS]; + + int req_len = phr_parse_request( + buf, len, &method, &method_len, (const char **)&path, &path_len, + &req->minor_version, headers, &num_headers, req->buf.len); + + if (req_len == -1) { + req->buf.len = len; + + return lnm_http_parse_err_invalid; + } else if (req_len == -2) { + return lnm_http_parse_err_incomplete; + } + + bool known_method = false; + + for (size_t i = 0; i < lnm_http_method_names_len && !known_method; i++) { + if (strncmp(method, lnm_http_method_names[i], method_len) == 0) { + req->method = i; + known_method = true; + } + } + + if (!known_method) { + return lnm_http_parse_err_unknown_method; + } + + // Path will always end with a newline, which we can safely set to nul + path[path_len] = '\0'; + char *question_mark = strchr(path, '?'); + + // Only store query if the path doesn't simply end with a question mark + if ((question_mark != NULL) && (path_len - (question_mark + 1 - path) > 0)) { + req->query.o = question_mark + 1 - buf; + req->query.len = path_len - (question_mark + 1 - path); + + path_len = question_mark - path; + + // All parsed strings should be null-terminated. This character is either a + // newline (if at the end of the path), or a question mark (if a query is + // present). + path[path_len] = '\0'; + } + + // Also migrate headers to offset-based + for (size_t i = 0; i < num_headers; i++) { + req->headers.arr[i].name.o = headers[i].name - buf; + req->headers.arr[i].name.len = headers[i].name_len; + req->headers.arr[i].value.o = headers[i].value - buf; + req->headers.arr[i].value.len = headers[i].value_len; + } + + req->headers.len = num_headers; + + req->path.len = path_len; + req->path.o = path - buf; + + req->buf.len = req_len; + req->buf.s = buf; + req->buf.owned = false; + + return lnm_http_parse_err_ok; +} + +void lnm_http_req_reset(lnm_http_req *req) { + if (req->body.owned) { + free(req->body.buf); + } + + if (req->buf.owned) { + free(req->buf.s); + } + + memset(req, 0, sizeof(lnm_http_req)); +} + +lnm_err lnm_http_req_header_get(const char **out, size_t *out_len, + lnm_http_req *req, lnm_http_header type) { + return lnm_http_req_header_get_s(out, out_len, req, + lnm_http_header_names[type]); +} + +lnm_err lnm_http_req_header_get_s(const char **out, size_t *out_len, + lnm_http_req *req, const char *name) { + size_t name_len = strlen(name); + + for (size_t i = 0; i < req->headers.len; i++) { + const lnm_http_req_header *header = &req->headers.arr[i]; + + if (lnm_strnieq(req->buf.s + header->name.o, header->name.len, name, + name_len)) { + *out = req->buf.s + header->value.o; + *out_len = header->value.len; + + return lnm_err_ok; + } + } + + return lnm_err_not_found; +} diff --git a/src/http/lnm_http_res.c b/src/http/lnm_http_res.c new file mode 100644 index 0000000..3f2b0c0 --- /dev/null +++ b/src/http/lnm_http_res.c @@ -0,0 +1,99 @@ +#include + +#include "lnm/http/res.h" + +lnm_err lnm_http_res_add_header(lnm_http_res *res, lnm_http_header type, + char *value, bool value_owned) { + return lnm_http_res_add_header_len(res, type, value, strlen(value), + value_owned); +} + +lnm_err lnm_http_res_add_header_len(lnm_http_res *res, lnm_http_header type, + char *value, size_t value_len, + bool value_owned) { + lnm_http_res_header *header = calloc(1, sizeof(lnm_http_res_header)); + + if (header == NULL) { + return lnm_err_failed_alloc; + } + + lnm_http_res_header **next_ptr = &res->headers.head; + + while ((*next_ptr) != NULL) { + next_ptr = &(*next_ptr)->next; + } + + *next_ptr = header; + + // Initialize the current pointer to the head of the linked list + if (res->headers.current == NULL) { + res->headers.current = header; + } + + header->name.s = (char *)lnm_http_header_names[type]; + header->name.len = strlen(lnm_http_header_names[type]); + header->name.owned = false; + + header->value.s = value; + header->value.len = value_len; + header->value.owned = value_owned; + + return lnm_err_ok; +} + +void lnm_http_res_body_set_file(lnm_http_res *res, FILE *f, size_t len, + bool owned) { + res->body.data.f = f; + res->body.len = len; + res->body.owned = owned; + res->body.type = lnm_http_res_body_type_file; +} + +void lnm_http_res_body_set_buf(lnm_http_res *res, char *buf, size_t len, + bool owned) { + res->body.data.buf = buf; + res->body.len = len; + res->body.owned = owned; + res->body.type = lnm_http_res_body_type_buf; +} + +void lnm_http_res_body_set_fn(lnm_http_res *res, data_fn fn, size_t len) { + res->body.data.fn = fn; + res->body.len = len; + res->body.type = lnm_http_res_body_type_fn; +} + +void lnm_http_res_reset(lnm_http_res *res) { + lnm_http_res_header *header = res->headers.head; + + while (header != NULL) { + lnm_http_res_header *next = header->next; + + if (header->name.owned) { + free(header->name.s); + } + + if (header->value.owned) { + free(header->value.s); + } + + free(header); + + header = next; + } + + if (res->body.owned) { + switch (res->body.type) { + case lnm_http_res_body_type_file: + fclose(res->body.data.f); + break; + case lnm_http_res_body_type_buf: + free(res->body.data.buf); + break; + case lnm_http_res_body_type_fn: + break; + } + } + + memset(res, 0, sizeof(lnm_http_res)); +} diff --git a/src/lnm_log.c b/src/lnm_log.c new file mode 100644 index 0000000..15fb00f --- /dev/null +++ b/src/lnm_log.c @@ -0,0 +1,86 @@ +#include +#include + +#include "lnm/common.h" +#include "lnm/log_internal.h" + +const char *lnm_log_level_names[] = {"DEBUG ", "INFO ", "NOTICE ", + "WARNING ", "ERROR ", "CRITICAL"}; + +lnm_logger *global_logger = NULL; + +lnm_err lnm_log_init_global() { + global_logger = calloc(1, sizeof(lnm_logger)); + + return global_logger == NULL ? lnm_err_failed_alloc : lnm_err_ok; +} + +lnm_err lnm_logger_stream_register(lnm_logger *logger, + lnm_logger_stream *stream) { + lnm_logger_stream **new = + logger->streams.len == 0 + ? malloc(sizeof(lnm_logger_stream *)) + : realloc(logger->streams.arr, + (logger->streams.len + 1) * sizeof(lnm_logger_stream *)); + + if (new == NULL) { + return lnm_err_failed_alloc; + } + + new[logger->streams.len] = stream; + logger->streams.arr = new; + logger->streams.len++; + + return lnm_err_ok; +} + +lnm_err lnm_log_register_stdout(lnm_log_level level) { + lnm_logger_stream *stream = malloc(sizeof(lnm_logger_stream)); + + if (stream == NULL) { + return lnm_err_failed_alloc; + } + + stream->type = lnm_logger_stream_type_file; + stream->ptr = stdout; + stream->level = level; + + LNM_RES2(lnm_logger_stream_register(global_logger, stream), free(stream)); + + return lnm_err_ok; +} + +void lnm_vlog(lnm_log_level level, const char *section, const char *fmt, + va_list ap) { + char date_str[32]; + + time_t now = time(NULL); + strftime(date_str, sizeof(date_str) - 1, "%Y-%m-%d %H:%M:%S", + localtime(&now)); + + for (size_t i = 0; i < global_logger->streams.len; i++) { + lnm_logger_stream *stream = global_logger->streams.arr[i]; + + if (level < stream->level) { + continue; + } + + switch (stream->type) { + case lnm_logger_stream_type_file: + fprintf(stream->ptr, "[%s][%s][%s] ", date_str, + lnm_log_level_names[level], section); + vfprintf(stream->ptr, fmt, ap); + fprintf(stream->ptr, "\n"); + break; + } + + va_end(ap); + } +} + +void lnm_log(lnm_log_level level, const char *section, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + lnm_vlog(level, section, fmt, ap); + va_end(ap); +} diff --git a/src/lnm_utils.c b/src/lnm_utils.c new file mode 100644 index 0000000..85094bd --- /dev/null +++ b/src/lnm_utils.c @@ -0,0 +1,57 @@ +#include +#include + +#include "lnm/common.h" + +bool lnm_strneq(const char *s1, size_t s1_len, const char *s2, size_t s2_len) { + return (s1_len == s2_len) && (memcmp(s1, s2, s1_len) == 0); +} + +bool lnm_strnieq(const char *s1, size_t s1_len, const char *s2, size_t s2_len) { + bool equal = s1_len == s2_len; + + for (size_t i = 0; i < s1_len && equal; i++) { + equal = s1[i] == s2[i] || + (('a' <= s1[i]) && (s1[i] <= 'z') && (s1[i] - 32 == s2[i])); + } + + return equal; +} + +uint64_t lnm_ipow(uint64_t base, uint64_t power) { + uint64_t res = 1; + + while (power > 0) { + res *= base; + power--; + } + + return res; +} + +uint64_t lnm_atoi(const char *s, size_t len) { + uint64_t res = 0; + + for (size_t i = 0; i < len; i++) { + if (s[i] < '0' || '9' < s[i]) { + return 0; + } + + uint64_t val = s[i] - '0'; + res += val * lnm_ipow(10, (len - 1) - i); + } + + return res; +} + +uint64_t lnm_digits(uint64_t num) { + int digits = 1; + + while (num > 9) { + digits++; + + num /= 10; + } + + return digits; +} diff --git a/src/loop/lnm_loop.c b/src/loop/lnm_loop.c new file mode 100644 index 0000000..6c9a854 --- /dev/null +++ b/src/loop/lnm_loop.c @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "lnm/common.h" +#include "lnm/log.h" +#include "lnm/loop.h" +#include "lnm/loop_internal.h" + +static const char *section = "loop"; + +lnm_err lnm_loop_init(lnm_loop **out, void *gctx, + lnm_err (*ctx_init)(void **out, void *gctx), + void (*ctx_free)(void *ctx), + void (*data_read)(lnm_loop_conn *conn), + void (*data_write)(lnm_loop_conn *conn)) { + lnm_loop *l = calloc(1, sizeof(lnm_loop)); + + if (l == NULL) { + return lnm_err_failed_alloc; + } + + l->gctx = gctx; + l->ctx_init = ctx_init; + l->ctx_free = ctx_free; + l->data_read = data_read; + l->data_write = data_write; + + *out = l; + + return lnm_err_ok; +} + +lnm_err lnm_loop_accept(lnm_loop *l) { + int conn_fd = accept(l->listen_fd, NULL, NULL); + + if (conn_fd < 0) { + lnm_lcritical(section, "accept failed: %i", conn_fd); + + return lnm_err_failed_network; + } + + // Set socket to non-blocking + int flags = fcntl(conn_fd, F_GETFL); + flags |= O_NONBLOCK; + fcntl(conn_fd, F_SETFL, flags); + + lnm_loop_conn *conn; + LNM_RES2(lnm_loop_conn_init(&conn, l), close(conn_fd)); + + conn->fd = conn_fd; + conn->state = lnm_loop_state_req; + + struct epoll_event event = {.data.ptr = conn, + .events = EPOLLIN | EPOLLET | EPOLLONESHOT}; + + epoll_ctl(l->epoll_fd, EPOLL_CTL_ADD, conn_fd, &event); + + l->open++; + + lnm_ldebug(section, "connection opened with fd %i", conn_fd); + + return lnm_err_ok; +} + +lnm_err lnm_loop_setup(lnm_loop *l, uint16_t port) { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + + if (listen_fd < 0) { + return lnm_err_failed_network; + } + + int val = 1; + int res = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int)); + + if (res < 0) { + return lnm_err_failed_network; + } + + struct sockaddr_in addr = {.sin_family = AF_INET, + .sin_port = ntohs(port), + .sin_addr.s_addr = ntohl(0)}; + + res = bind(listen_fd, (const struct sockaddr *)&addr, sizeof(addr)); + + if (res < 0) { + return lnm_err_failed_network; + } + + res = listen(listen_fd, SOMAXCONN); + + if (res < 0) { + return lnm_err_failed_network; + } + + int flags = fcntl(listen_fd, F_GETFL); + fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK); + + int epoll_fd = epoll_create1(0); + + if (epoll_fd < 0) { + return lnm_err_failed_network; + } + + struct epoll_event event = { + // The listening socket is marked using a NULL data field + .data.ptr = NULL, + .events = EPOLLIN | EPOLLET | EPOLLONESHOT}; + + res = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event); + + if (res < 0) { + return lnm_err_failed_network; + } + + l->listen_fd = listen_fd; + l->epoll_fd = epoll_fd; + + return lnm_err_ok; +} + +typedef struct lnm_loop_thread_args { + lnm_loop *l; + int id; + int thread_count; +} lnm_loop_thread_args; + +lnm_err lnm_loop_run_thread(lnm_loop_thread_args *args) { + lnm_loop *l = args->l; + int thread_id = args->id; + int thread_count = args->thread_count; + + struct epoll_event *events = calloc(1, sizeof(struct epoll_event)); + int events_cap = 1; + + if (events == NULL) { + return lnm_err_failed_alloc; + } + + lnm_linfo(section, "thread %i started", thread_id); + + struct epoll_event listen_event = { + .data.ptr = NULL, .events = EPOLLIN | EPOLLET | EPOLLONESHOT}; + + while (1) { + int polled = epoll_wait(l->epoll_fd, events, events_cap, -1); + lnm_ldebug(section, "polled (thread %i): %i", thread_id, polled); + + if (polled < 0) { + return lnm_err_failed_poll; + } + + for (int i = 0; i < polled; i++) { + if (events[i].data.ptr == NULL) { + lnm_loop_accept(l); + + epoll_ctl(l->epoll_fd, EPOLL_CTL_MOD, l->listen_fd, &listen_event); + } else { + lnm_loop_conn *conn = events[i].data.ptr; + lnm_loop_conn_io(l, conn); + + if (conn->state == lnm_loop_state_end) { + int conn_fd = conn->fd; + + lnm_loop_conn_free(l, conn); + close(conn_fd); + l->open--; + + epoll_ctl(l->epoll_fd, EPOLL_CTL_DEL, conn_fd, NULL); + + lnm_ldebug(section, "connection closed with fd %i", conn_fd); + } else { + struct epoll_event event = { + .data.ptr = conn, + .events = + (conn->state == lnm_loop_state_req ? EPOLLIN : EPOLLOUT) | + EPOLLET | EPOLLONESHOT}; + epoll_ctl(l->epoll_fd, EPOLL_CTL_MOD, conn->fd, &event); + } + } + } + + int open = l->open; + int cap_per_thread = + open + 1 > thread_count ? (open + 1) / thread_count : 1; + + if (cap_per_thread > events_cap) { + struct epoll_event *new_events = + malloc(cap_per_thread * sizeof(struct epoll_event)); + + if (new_events == NULL) { + return lnm_err_failed_alloc; + } + + free(events); + events = new_events; + events_cap = cap_per_thread; + } + } + + return lnm_err_ok; +} + +lnm_err lnm_loop_run(lnm_loop *l, int thread_count) { + if (l->epoll_fd == 0) { + return lnm_err_not_setup; + } + + lnm_loop_thread_args args[thread_count]; + + for (int i = 1; i < thread_count; i++) { + args[i].l = l; + args[i].id = i; + args[i].thread_count = thread_count; + + pthread_t thread; + pthread_create(&thread, NULL, (void *(*)(void *))lnm_loop_run_thread, + &args[i]); + } + + args[0].l = l; + args[0].id = 0; + args[0].thread_count = thread_count; + + lnm_loop_run_thread(&args[0]); + + return lnm_err_ok; +} diff --git a/src/loop/lnm_loop_conn.c b/src/loop/lnm_loop_conn.c new file mode 100644 index 0000000..49b86ad --- /dev/null +++ b/src/loop/lnm_loop_conn.c @@ -0,0 +1,22 @@ +#include "lnm/loop_internal.h" + +lnm_err lnm_loop_conn_init(lnm_loop_conn **out, lnm_loop *l) { + lnm_loop_conn *conn = calloc(1, sizeof(lnm_loop_conn)); + + if (conn == NULL) { + return lnm_err_failed_alloc; + } + + void *ctx; + LNM_RES2(l->ctx_init(&ctx, l->gctx), free(conn)); + + conn->ctx = ctx; + *out = conn; + + return lnm_err_ok; +} + +void lnm_loop_conn_free(lnm_loop *l, lnm_loop_conn *conn) { + l->ctx_free(conn->ctx); + free(conn); +} diff --git a/src/loop/lnm_loop_io.c b/src/loop/lnm_loop_io.c new file mode 100644 index 0000000..363d6af --- /dev/null +++ b/src/loop/lnm_loop_io.c @@ -0,0 +1,77 @@ +#include +#include +#include + +#include "lnm/loop.h" +#include "lnm/loop_internal.h" + +void lnm_loop_conn_io_req(lnm_loop *l, lnm_loop_conn *conn) { + do { + // Move remaining data to front of buffer + memmove(conn->r.buf, &conn->r.buf[conn->r.read], + conn->r.size - conn->r.read); + conn->r.size -= conn->r.read; + conn->r.read = 0; + + ssize_t res; + size_t cap = LNM_LOOP_BUF_SIZE - conn->r.size; + + do { + res = read(conn->fd, &conn->r.buf[conn->r.size], cap); + } while (res < 0 && errno == EINTR); + + // Read can't be performed without blocking; we come back later + if (res < 0 && errno == EAGAIN) { + return; + } + + if (res <= 0) { + conn->state = lnm_loop_state_end; + + return; + } + + conn->r.size += res; + l->data_read(conn); + } while (conn->state == lnm_loop_state_req); +} + +void lnm_loop_conn_io_res(lnm_loop *l, lnm_loop_conn *conn) { + do { + l->data_write(conn); + + ssize_t res; + + do { + res = write(conn->fd, conn->w.buf, conn->w.size); + } while (res < 0 && errno == EINTR); + + // Write can't be performed without blocking; we come back later + if (res < 0 && errno == EAGAIN) { + return; + } + + if (res < 0) { + conn->state = lnm_loop_state_end; + + return; + } + + // Move remaining data to front of buffer. Doing this here gives the data + // writer function more space to work with + memmove(conn->w.buf, &conn->w.buf[res], conn->w.size - res); + conn->w.size -= res; + } while (conn->state == lnm_loop_state_res); +} + +void lnm_loop_conn_io(lnm_loop *l, lnm_loop_conn *conn) { + switch (conn->state) { + case lnm_loop_state_req: + lnm_loop_conn_io_req(l, conn); + break; + case lnm_loop_state_res: + lnm_loop_conn_io_res(l, conn); + break; + default:; + } +} diff --git a/src/picohttpparser.c b/src/picohttpparser.c new file mode 100644 index 0000000..5e5783a --- /dev/null +++ b/src/picohttpparser.c @@ -0,0 +1,665 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include +#include +#ifdef __SSE4_2__ +#ifdef _MSC_VER +#include +#else +#include +#endif +#endif +#include "picohttpparser.h" + +#if __GNUC__ >= 3 +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#else +#define likely(x) (x) +#define unlikely(x) (x) +#endif + +#ifdef _MSC_VER +#define ALIGNED(n) _declspec(align(n)) +#else +#define ALIGNED(n) __attribute__((aligned(n))) +#endif + +#define IS_PRINTABLE_ASCII(c) ((unsigned char)(c)-040u < 0137u) + +#define CHECK_EOF() \ + if (buf == buf_end) { \ + *ret = -2; \ + return NULL; \ + } + +#define EXPECT_CHAR_NO_CHECK(ch) \ + if (*buf++ != ch) { \ + *ret = -1; \ + return NULL; \ + } + +#define EXPECT_CHAR(ch) \ + CHECK_EOF(); \ + EXPECT_CHAR_NO_CHECK(ch); + +#define ADVANCE_TOKEN(tok, toklen) \ + do { \ + const char *tok_start = buf; \ + static const char ALIGNED(16) ranges2[16] = "\000\040\177\177"; \ + int found2; \ + buf = findchar_fast(buf, buf_end, ranges2, 4, &found2); \ + if (!found2) { \ + CHECK_EOF(); \ + } \ + while (1) { \ + if (*buf == ' ') { \ + break; \ + } else if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { \ + if ((unsigned char)*buf < '\040' || *buf == '\177') { \ + *ret = -1; \ + return NULL; \ + } \ + } \ + ++buf; \ + CHECK_EOF(); \ + } \ + tok = tok_start; \ + toklen = buf - tok_start; \ + } while (0) + +static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\1\0\1\1\1\1\1\0\0\1\1\0\1\1\0\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0" + "\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\1\1" + "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\1\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + +static const char *findchar_fast(const char *buf, const char *buf_end, const char *ranges, size_t ranges_size, int *found) +{ + *found = 0; +#if __SSE4_2__ + if (likely(buf_end - buf >= 16)) { + __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); + + size_t left = (buf_end - buf) & ~15; + do { + __m128i b16 = _mm_loadu_si128((const __m128i *)buf); + int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); + if (unlikely(r != 16)) { + buf += r; + *found = 1; + break; + } + buf += 16; + left -= 16; + } while (likely(left != 0)); + } +#else + /* suppress unused parameter warning */ + (void)buf_end; + (void)ranges; + (void)ranges_size; +#endif + return buf; +} + +static const char *get_token_to_eol(const char *buf, const char *buf_end, const char **token, size_t *token_len, int *ret) +{ + const char *token_start = buf; + +#ifdef __SSE4_2__ + static const char ALIGNED(16) ranges1[16] = "\0\010" /* allow HT */ + "\012\037" /* allow SP and up to but not including DEL */ + "\177\177"; /* allow chars w. MSB set */ + int found; + buf = findchar_fast(buf, buf_end, ranges1, 6, &found); + if (found) + goto FOUND_CTL; +#else + /* find non-printable char within the next 8 bytes, this is the hottest code; manually inlined */ + while (likely(buf_end - buf >= 8)) { +#define DOIT() \ + do { \ + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) \ + goto NonPrintable; \ + ++buf; \ + } while (0) + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); +#undef DOIT + continue; + NonPrintable: + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + ++buf; + } +#endif + for (;; ++buf) { + CHECK_EOF(); + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + } + } +FOUND_CTL: + if (likely(*buf == '\015')) { + ++buf; + EXPECT_CHAR('\012'); + *token_len = buf - 2 - token_start; + } else if (*buf == '\012') { + *token_len = buf - token_start; + ++buf; + } else { + *ret = -1; + return NULL; + } + *token = token_start; + + return buf; +} + +static const char *is_complete(const char *buf, const char *buf_end, size_t last_len, int *ret) +{ + int ret_cnt = 0; + buf = last_len < 3 ? buf : buf + last_len - 3; + + while (1) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + CHECK_EOF(); + EXPECT_CHAR('\012'); + ++ret_cnt; + } else if (*buf == '\012') { + ++buf; + ++ret_cnt; + } else { + ++buf; + ret_cnt = 0; + } + if (ret_cnt == 2) { + return buf; + } + } + + *ret = -2; + return NULL; +} + +#define PARSE_INT(valp_, mul_) \ + if (*buf < '0' || '9' < *buf) { \ + buf++; \ + *ret = -1; \ + return NULL; \ + } \ + *(valp_) = (mul_) * (*buf++ - '0'); + +#define PARSE_INT_3(valp_) \ + do { \ + int res_ = 0; \ + PARSE_INT(&res_, 100) \ + *valp_ = res_; \ + PARSE_INT(&res_, 10) \ + *valp_ += res_; \ + PARSE_INT(&res_, 1) \ + *valp_ += res_; \ + } while (0) + +/* returned pointer is always within [buf, buf_end), or null */ +static const char *parse_token(const char *buf, const char *buf_end, const char **token, size_t *token_len, char next_char, + int *ret) +{ + /* We use pcmpestri to detect non-token characters. This instruction can take no more than eight character ranges (8*2*8=128 + * bits that is the size of a SSE register). Due to this restriction, characters `|` and `~` are handled in the slow loop. */ + static const char ALIGNED(16) ranges[] = "\x00 " /* control chars and up to SP */ + "\"\"" /* 0x22 */ + "()" /* 0x28,0x29 */ + ",," /* 0x2c */ + "//" /* 0x2f */ + ":@" /* 0x3a-0x40 */ + "[]" /* 0x5b-0x5d */ + "{\xff"; /* 0x7b-0xff */ + const char *buf_start = buf; + int found; + buf = findchar_fast(buf, buf_end, ranges, sizeof(ranges) - 1, &found); + if (!found) { + CHECK_EOF(); + } + while (1) { + if (*buf == next_char) { + break; + } else if (!token_char_map[(unsigned char)*buf]) { + *ret = -1; + return NULL; + } + ++buf; + CHECK_EOF(); + } + *token = buf_start; + *token_len = buf - buf_start; + return buf; +} + +/* returned pointer is always within [buf, buf_end), or null */ +static const char *parse_http_version(const char *buf, const char *buf_end, int *minor_version, int *ret) +{ + /* we want at least [HTTP/1.] to try to parse */ + if (buf_end - buf < 9) { + *ret = -2; + return NULL; + } + EXPECT_CHAR_NO_CHECK('H'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('P'); + EXPECT_CHAR_NO_CHECK('/'); + EXPECT_CHAR_NO_CHECK('1'); + EXPECT_CHAR_NO_CHECK('.'); + PARSE_INT(minor_version, 1); + return buf; +} + +static const char *parse_headers(const char *buf, const char *buf_end, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) +{ + for (;; ++*num_headers) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + break; + } else if (*buf == '\012') { + ++buf; + break; + } + if (*num_headers == max_headers) { + *ret = -1; + return NULL; + } + if (!(*num_headers != 0 && (*buf == ' ' || *buf == '\t'))) { + /* parsing name, but do not discard SP before colon, see + * http://www.mozilla.org/security/announce/2006/mfsa2006-33.html */ + if ((buf = parse_token(buf, buf_end, &headers[*num_headers].name, &headers[*num_headers].name_len, ':', ret)) == NULL) { + return NULL; + } + if (headers[*num_headers].name_len == 0) { + *ret = -1; + return NULL; + } + ++buf; + for (;; ++buf) { + CHECK_EOF(); + if (!(*buf == ' ' || *buf == '\t')) { + break; + } + } + } else { + headers[*num_headers].name = NULL; + headers[*num_headers].name_len = 0; + } + const char *value; + size_t value_len; + if ((buf = get_token_to_eol(buf, buf_end, &value, &value_len, ret)) == NULL) { + return NULL; + } + /* remove trailing SPs and HTABs */ + const char *value_end = value + value_len; + for (; value_end != value; --value_end) { + const char c = *(value_end - 1); + if (!(c == ' ' || c == '\t')) { + break; + } + } + headers[*num_headers].value = value; + headers[*num_headers].value_len = value_end - value; + } + return buf; +} + +static const char *parse_request(const char *buf, const char *buf_end, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) +{ + /* skip first empty line (some clients add CRLF after POST content) */ + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } + + /* parse request line */ + if ((buf = parse_token(buf, buf_end, method, method_len, ' ', ret)) == NULL) { + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + ADVANCE_TOKEN(*path, *path_len); + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + if (*method_len == 0 || *path_len == 0) { + *ret = -1; + return NULL; + } + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } else { + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); +} + +int phr_parse_request(const char *buf_start, size_t len, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf_start + len; + size_t max_headers = *num_headers; + int r; + + *method = NULL; + *method_len = 0; + *path = NULL; + *path_len = 0; + *minor_version = -1; + *num_headers = 0; + + /* if last_len != 0, check if the request is complete (a fast countermeasure + againt slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_request(buf, buf_end, method, method_len, path, path_len, minor_version, headers, num_headers, max_headers, + &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +static const char *parse_response(const char *buf, const char *buf_end, int *minor_version, int *status, const char **msg, + size_t *msg_len, struct phr_header *headers, size_t *num_headers, size_t max_headers, int *ret) +{ + /* parse "HTTP/1.x" */ + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + /* skip space */ + if (*buf != ' ') { + *ret = -1; + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + /* parse status code, we want at least [:digit:][:digit:][:digit:] to try to parse */ + if (buf_end - buf < 4) { + *ret = -2; + return NULL; + } + PARSE_INT_3(status); + + /* get message including preceding space */ + if ((buf = get_token_to_eol(buf, buf_end, msg, msg_len, ret)) == NULL) { + return NULL; + } + if (*msg_len == 0) { + /* ok */ + } else if (**msg == ' ') { + /* Remove preceding space. Successful return from `get_token_to_eol` guarantees that we would hit something other than SP + * before running past the end of the given buffer. */ + do { + ++*msg; + --*msg_len; + } while (**msg == ' '); + } else { + /* garbage found after status code */ + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); +} + +int phr_parse_response(const char *buf_start, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *minor_version = -1; + *status = 0; + *msg = NULL; + *msg_len = 0; + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_response(buf, buf_end, minor_version, status, msg, msg_len, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +int phr_parse_headers(const char *buf_start, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len) +{ + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_headers(buf, buf_end, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); +} + +enum { + CHUNKED_IN_CHUNK_SIZE, + CHUNKED_IN_CHUNK_EXT, + CHUNKED_IN_CHUNK_DATA, + CHUNKED_IN_CHUNK_CRLF, + CHUNKED_IN_TRAILERS_LINE_HEAD, + CHUNKED_IN_TRAILERS_LINE_MIDDLE +}; + +static int decode_hex(int ch) +{ + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } else if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 0xa; + } else if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 0xa; + } else { + return -1; + } +} + +ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *_bufsz) +{ + size_t dst = 0, src = 0, bufsz = *_bufsz; + ssize_t ret = -2; /* incomplete */ + + while (1) { + switch (decoder->_state) { + case CHUNKED_IN_CHUNK_SIZE: + for (;; ++src) { + int v; + if (src == bufsz) + goto Exit; + if ((v = decode_hex(buf[src])) == -1) { + if (decoder->_hex_count == 0) { + ret = -1; + goto Exit; + } + break; + } + if (decoder->_hex_count == sizeof(size_t) * 2) { + ret = -1; + goto Exit; + } + decoder->bytes_left_in_chunk = decoder->bytes_left_in_chunk * 16 + v; + ++decoder->_hex_count; + } + decoder->_hex_count = 0; + decoder->_state = CHUNKED_IN_CHUNK_EXT; + /* fallthru */ + case CHUNKED_IN_CHUNK_EXT: + /* RFC 7230 A.2 "Line folding in chunk extensions is disallowed" */ + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + if (decoder->bytes_left_in_chunk == 0) { + if (decoder->consume_trailer) { + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + } else { + goto Complete; + } + } + decoder->_state = CHUNKED_IN_CHUNK_DATA; + /* fallthru */ + case CHUNKED_IN_CHUNK_DATA: { + size_t avail = bufsz - src; + if (avail < decoder->bytes_left_in_chunk) { + if (dst != src) + memmove(buf + dst, buf + src, avail); + src += avail; + dst += avail; + decoder->bytes_left_in_chunk -= avail; + goto Exit; + } + if (dst != src) + memmove(buf + dst, buf + src, decoder->bytes_left_in_chunk); + src += decoder->bytes_left_in_chunk; + dst += decoder->bytes_left_in_chunk; + decoder->bytes_left_in_chunk = 0; + decoder->_state = CHUNKED_IN_CHUNK_CRLF; + } + /* fallthru */ + case CHUNKED_IN_CHUNK_CRLF: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src] != '\012') { + ret = -1; + goto Exit; + } + ++src; + decoder->_state = CHUNKED_IN_CHUNK_SIZE; + break; + case CHUNKED_IN_TRAILERS_LINE_HEAD: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src++] == '\012') + goto Complete; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_MIDDLE; + /* fallthru */ + case CHUNKED_IN_TRAILERS_LINE_MIDDLE: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + default: + assert(!"decoder is corrupt"); + } + } + +Complete: + ret = bufsz - src; +Exit: + if (dst != src) + memmove(buf + dst, buf + src, bufsz - src); + *_bufsz = dst; + return ret; +} + +int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder) +{ + return decoder->_state == CHUNKED_IN_CHUNK_DATA; +} + +#undef CHECK_EOF +#undef EXPECT_CHAR +#undef ADVANCE_TOKEN