diff --git a/.dockerignore b/.dockerignore index 26ee61d..9bda86a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,15 +2,14 @@ !src/ !include/ -!Makefile -!config.mk !lsm/src/ !lsm/include/ !lsm/Makefile !lsm/config.mk -!lnm/src/ -!lnm/include/ -!lnm/Makefile -!lnm/config.mk +!thirdparty/include +!thirdparty/src + +!Makefile +!config.mk diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2fe4b2f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lnm"] - path = lnm - url = https://git.rustybever.be/Chewing_Bever/lnm diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 0ee849e..b6063bb 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -1,7 +1,6 @@ matrix: PLATFORM: - 'linux/amd64' - - 'linux/arm64' platform: ${PLATFORM} @@ -35,13 +34,13 @@ pipeline: commands: - apk add --no-cache minio-client - mcli alias set rb 'https://s3.rustybever.be' "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY" - - mcli cp build/lander "rb/lander/commits/$CI_COMMIT_SHA/lander-$(echo '${PLATFORM}' | sed 's:/:-:g')" - - mcli cp landerctl/build/landerctl "rb/lander/commits/$CI_COMMIT_SHA/landerctl-$(echo '${PLATFORM}' | sed 's:/:-:g')" + - mcli cp build/lander landerctl/build/landerctl "rb/lander/commits/$CI_COMMIT_SHA/" secrets: - minio_access_key - minio_secret_key when: - branch: dev + branch: + exclude: [ release/* ] event: push publish-rel: diff --git a/CHANGELOG.md b/CHANGELOG.md index aca5284..82b5c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,31 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/lander/src/branch/dev) -## [0.2.1](https://git.rustybever.be/Chewing_Bever/lander/src/tag/0.2.1) - -### Added - -* LNM - Lander Network Module - * Rewrite of the event loop & HTTP loop - * Fully independent library, maintained in its own repository - * Numerous improvements - * Streaming of headers - * Allow custom & an arbitrary number of response headers - * Better API for adding routes - * State machine HTTP loop - * Automatically support HEAD requests for all GET requests - * Event loop uses `epoll` instead of `poll` - * Configurable multithreading using `epoll` - * Trie-based router (no more RegEx) -* Landerctl - * `-c` flag to use custom config file (useful for testing) - -## Removed - -* Content-Disposition header for files -* Secure routes with a specified key (e.g. `/sl/:key`), as these were identical - to `/s/:key` routes - ## [0.2.0](https://git.rustybever.be/Chewing_Bever/lander/src/tag/0.2.0) ### Added diff --git a/Makefile b/Makefile index 9cd57fe..f2c448c 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,14 @@ BIN := $(BUILD_DIR)/$(BIN_FILENAME) SRCS != find '$(SRC_DIR)' -iname '*.c' SRCS_TEST != find '$(TEST_DIR)' -iname '*.c' +SRCS_THIRDPARTY != find '$(THIRDPARTY_DIR)/src' -iname '*.c' SRCS_H != find include -iname '*.h' SRCS_H_INTERNAL != find $(SRC_DIR) -iname '*.h' -OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) +OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) $(SRCS_THIRDPARTY:%=$(BUILD_DIR)/%.o) OBJS_TEST := $(SRCS_TEST:%=$(BUILD_DIR)/%.o) -DEPS := $(SRCS:%=$(BUILD_DIR)/%.d) $(SRCS_TEST:%=$(BUILD_DIR)/%.d) +DEPS := $(SRCS:%=$(BUILD_DIR)/%.d) $(SRCS_THIRDPARTY:%=$(BUILD_DIR)/%.d) $(SRCS_TEST:%=$(BUILD_DIR)/%.d) BINS_TEST := $(OBJS_TEST:%.c.o=%) TARGETS_TEST := $(BINS_TEST:%=test-%) @@ -38,17 +39,17 @@ objs: $(OBJS) liblsm: $(MAKE) -C lsm -.PHONY: liblnm -liblnm: - $(MAKE) -C lnm - -$(BIN): liblsm liblnm $(OBJS) +$(BIN): liblsm $(OBJS) $(CC) -o $@ $(OBJS) $(_LDFLAGS) $(BUILD_DIR)/$(SRC_DIR)/%.c.o: $(SRC_DIR)/%.c mkdir -p $(dir $@) $(CC) $(_CFLAGS) -c $< -o $@ +$(BUILD_DIR)/$(THIRDPARTY_DIR)/%.c.o: $(THIRDPARTY_DIR)/%.c + mkdir -p $(dir $@) + $(CC) $(_CFLAGS) -c $< -o $@ + .PHONY: bin-docker bin-docker: docker build -t lander . @@ -67,7 +68,7 @@ run: $(BIN) valgrind: $(BIN) LANDER_API_KEY=test \ LANDER_DATA_DIR=data \ - valgrind --track-origins=yes '$(BUILD_DIR)/$(BIN_FILENAME)' + valgrind '$(BUILD_DIR)/$(BIN_FILENAME)' .PHONY: test test: $(TARGETS_TEST) @@ -134,7 +135,6 @@ check: clean: rm -rf $(BUILD_DIR) $(MAKE) -C lsm clean - $(MAKE) -C lnm clean $(MAKE) -C landerctl clean .PHONY: bear diff --git a/config.mk b/config.mk index d5230d7..da3e0b8 100644 --- a/config.mk +++ b/config.mk @@ -1,4 +1,4 @@ -VERSION := 0.2.1 +VERSION := 0.2.0 BIN_FILENAME = lander @@ -7,9 +7,9 @@ SRC_DIR = src TEST_DIR = test THIRDPARTY_DIR = thirdparty -INC_DIRS = include $(THIRDPARTY_DIR)/include lsm/include lnm/include -LIBS = lsm lnm -LIB_DIRS = ./lsm/build ./lnm/build +INC_DIRS = include $(THIRDPARTY_DIR)/include lsm/include +LIBS = m lsm +LIB_DIRS = ./lsm/build # -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 diff --git a/include/event_loop.h b/include/event_loop.h new file mode 100644 index 0000000..24dd05a --- /dev/null +++ b/include/event_loop.h @@ -0,0 +1,137 @@ +#ifndef LANDER_EVENT_LOOP +#define LANDER_EVENT_LOOP + +#include +#include +#include + +// Size of the read and write buffers for each connection, in bytes +#define EVENT_LOOP_BUFFER_SIZE 2048 + +/** + * State of a connection + */ +typedef enum { + event_loop_conn_state_req = 0, + event_loop_conn_state_res = 1, + event_loop_conn_state_end = 2, +} event_loop_conn_state; + +/** + * Represents an active connection managed by the event loop + */ +typedef struct event_loop_conn { + int fd; + event_loop_conn_state state; + // Read buffer + size_t rbuf_size; + size_t rbuf_read; + uint8_t rbuf[EVENT_LOOP_BUFFER_SIZE]; + // Write buffer + size_t wbuf_size; + size_t wbuf_sent; + uint8_t wbuf[EVENT_LOOP_BUFFER_SIZE]; + + // If true, the server will close the connection after the final write buffer + // has been written + bool close_after_write; + // Context for a request + void *ctx; +} event_loop_conn; + +/* + * Main struct object representing the event loop + */ +typedef struct event_loop { + event_loop_conn **connections; + size_t connection_count; + // Global context passed to every connection + void *gctx; + /** + * Function to initialize a connection context. + * + * @param gctx global context of the event loop + * @return pointer to the allocated object. + */ + void *(*ctx_init)(void *gctx); + /** + * Function to free a connection context object. + * + * @param ctx context to free + */ + void (*ctx_free)(void *ctx); + /** + * Function to process incoming data while in the req state. + * + * @param conn connection to process + * @return whether the function can be called again immediately in the same + * event loop cycle. This allows quicly processing multiple requests without + * waiting for I/O. + */ + bool (*handle_data)(event_loop_conn *conn); + /** + * Function to process outgoing data while in the res state. + * + * @param conn connection to proces + */ + void (*write_data)(event_loop_conn *conn); +} event_loop; + +/* + * Initialize a new connection struct + * + * @param el the event loop + * @return pointer to the newly allocated connection struct + */ +event_loop_conn *event_loop_conn_init(event_loop *el); + +/* + * Free a connection struct + * + * @param el the event loop + * @param conn connection struct to free + */ +void event_loop_conn_free(event_loop *el, event_loop_conn *conn); + +/* + * Handle I/O for a connection, be it reading input or writing output. + * + * @param el the event loop + * @param conn the connection to process + */ +void event_loop_conn_io(event_loop *el, event_loop_conn *conn); + +/* + * Initialize a new event loop struct + * + * @return pointer to the newly allocated event loop struct + */ +event_loop *event_loop_init(); + +/* + * Place a new connection into the event loop's internal array. + * + * @param el the event loop + * @param conn connection to insert + * @return 0 on success, -1 if the internal realloc failed. + */ +int event_loop_put(event_loop *el, event_loop_conn *conn); + +/** + * Accept a new connection for the given file descriptor. + * + * @param el the event loop + * @param fd file descriptor for the connection + * @return 0 if successful, negative value otherwise + */ +int event_loop_accept(event_loop *el, int fd); + +/* + * Run the event loop. This function never returns. + * + * @param el the event loop + * @param port on what port to listen + */ +void event_loop_run(event_loop *el, int port); + +#endif diff --git a/include/http/req.h b/include/http/req.h new file mode 100644 index 0000000..ab8922b --- /dev/null +++ b/include/http/req.h @@ -0,0 +1,42 @@ +#ifndef LANDER_HTTP_REQ +#define LANDER_HTTP_REQ + +#include +#include +#include +#include + +#include "http/types.h" +#include "picohttpparser.h" + +#define HTTP_MAX_ALLOWED_HEADERS 32 +#define HTTP_MAX_REGEX_GROUPS 4 + +/** + * Struct representing the specific type of request + */ +typedef struct http_request { + size_t len; + int minor_version; + http_method method; + const char *path; + size_t path_len; + const char *query; + size_t query_len; + http_body body; + regmatch_t regex_groups[HTTP_MAX_REGEX_GROUPS]; + struct phr_header headers[HTTP_MAX_ALLOWED_HEADERS]; + size_t num_headers; +} http_request; + +/** + * Result of the HTTP parse function + */ +typedef enum http_parse_error { + http_parse_error_ok = 0, + http_parse_error_incomplete = 1, + http_parse_error_invalid = 2, + http_parse_error_unknown_method = 3 +} http_parse_error; + +#endif diff --git a/include/http/res.h b/include/http/res.h new file mode 100644 index 0000000..6fda002 --- /dev/null +++ b/include/http/res.h @@ -0,0 +1,69 @@ +#ifndef LANDER_HTTP_RES +#define LANDER_HTTP_RES + +#include +#include + +#include "http/types.h" + +/** + * Struct describing a header for the response. + */ +typedef struct http_response_header { + http_header type; + const char *value; + bool owned; +} http_response_header; + +/** + * Struct representing an HTTP response. + */ +typedef struct http_response { + http_status status; + const char *head; + size_t head_len; + size_t head_written; + http_body body; + http_response_header headers[4]; + size_t header_count; +} http_response; + +/** + * Set the request body to the given buffer. + * + * @param res response to modify + * @param body pointer to the buf containing the body + * @param body_len length of the body + * @owned whether the body should be freed after processing the request + */ +void http_res_set_body_buf(http_response *res, const char *body, + size_t body_len, bool owned); + +/** + * Set the request body to the given filename. + * + * @param res response to modify + * @param filename path to the file to return + */ +void http_res_set_body_file(http_response *res, const char *filename); + +/** + * Add a header to the response. + * + * @param res response to modify + * @param type type of the header + * @param value value of the header + * @param owned whether the value should be freed after processing the request + */ +void http_res_add_header(http_response *res, http_header type, + const char *value, bool owned); + +/** + * Add a Content-Type header corresponding to the mime type. + * + * @param res response to modiy + * @param mime_type mime type of the response + */ +void http_res_set_mime_type(http_response *res, http_mime_type mime_type); + +#endif diff --git a/include/http/types.h b/include/http/types.h new file mode 100644 index 0000000..cccf0a0 --- /dev/null +++ b/include/http/types.h @@ -0,0 +1,173 @@ +#ifndef LANDER_HTTP_TYPES +#define LANDER_HTTP_TYPES + +#include +#include +#include + +// Array mapping the http_request_method enum to strings +extern const char *http_method_names[]; +extern const size_t http_method_names_len; + +typedef enum http_method { + http_get = 0, + http_post = 1, + http_put = 2, + http_patch = 3, + http_delete = 4 +} http_method; + +// Array mapping the http_response_type enum to strings +extern const char *http_status_names[][32]; + +typedef enum http_status { + // 1xx + http_continue = 100, + http_switching_protocols = 101, + http_processing = 102, + http_early_hints = 103, + // 2xx + http_ok = 200, + http_created = 201, + http_accepted = 202, + http_non_authoritative_information = 203, + http_no_content = 204, + http_reset_content = 205, + http_partial_content = 206, + http_multi_status = 207, + http_already_reported = 208, + // 3xx + http_multiple_choices = 300, + http_moved_permanently = 301, + http_found = 302, + http_see_other = 303, + http_not_modified = 304, + http_temporary_redirect = 307, + http_permanent_redirect = 308, + // 4xx + http_bad_request = 400, + http_unauthorized = 401, + http_payment_required = 402, + http_forbidden = 403, + http_not_found = 404, + http_method_not_allowed = 405, + http_not_acceptable = 406, + http_proxy_authentication_required = 407, + http_request_timeout = 408, + http_conflict = 409, + http_gone = 410, + http_length_required = 411, + http_precondition_failed = 412, + http_content_too_large = 413, + http_uri_too_long = 414, + http_unsupported_media_type = 415, + http_range_not_satisfiable = 416, + http_expection_failed = 417, + http_im_a_teapot = 418, + http_misdirected_request = 421, + http_unprocessable_content = 422, + http_locked = 423, + http_failed_dependency = 424, + http_too_early = 425, + http_upgrade_required = 426, + http_precondition_required = 428, + http_too_many_requests = 429, + http_request_header_fields_too_large = 431, + // 5xx + http_internal_server_error = 500, + http_method_not_implemented = 501, + http_bad_gateway = 502, + http_service_unavailable = 503, + http_gateway_timeout = 504, + http_http_version_not_supported = 505, + http_variant_also_negotiates = 506, + http_insufficient_storage = 507, + http_loop_detected = 508, + http_not_extended = 510, + http_network_authentication_required = 511 +} http_status; + +// Array mapping the http_mime_type enum to strings +extern const char *http_mime_type_names[][2]; + +typedef enum http_mime_type { + http_mime_aac = 0, + http_mime_bz, + http_mime_bz2, + http_mime_css, + http_mime_csv, + http_mime_gz, + http_mime_gif, + http_mime_htm, + http_mime_html, + http_mime_jar, + http_mime_jpeg, + http_mime_jpg, + http_mime_js, + http_mime_json, + http_mime_mp3, + http_mime_mp4, + http_mime_png, + http_mime_pdf, + http_mime_rar, + http_mime_sh, + http_mime_svg, + http_mime_tar, + http_mime_txt, + http_mime_wav, + http_mime_7z +} http_mime_type; + +// Array mapping the http_header enum to strings +extern const char *http_header_names[]; + +typedef enum http_header { + http_header_connection = 0, + http_header_location, + http_header_content_type, + http_header_content_disposition +} http_header; + +typedef enum http_body_type { + http_body_buf = 0, + http_body_file = 1 +} http_body_type; + +typedef struct http_body { + http_body_type type; + char *buf; + bool buf_owned; + FILE *file; + char *fname; + bool fname_owned; + // In the context of a request, expected_len is the content of the request's + // Content-Length header, and len is how many bytes have already been + // received. + // In the context of a response, expected_len is the actual length of the + // body, and len is how many have been written. + size_t expected_len; + size_t len; +} http_body; + +/** + * Initialize a new body struct. + * + * @return pointer to the newly allocated object. + */ +http_body *http_body_init(); + +/** + * Reset a body, allowing it to be reused as if newly allocated. + * + * @param body body to reset + */ +void http_body_reset(http_body *body); + +/** + * Free a body. Internally, this calls http_body_reset. + * + * @param body body to free + */ +void http_body_free(http_body *body); + +#endif diff --git a/include/http_loop.h b/include/http_loop.h new file mode 100644 index 0000000..131bd6e --- /dev/null +++ b/include/http_loop.h @@ -0,0 +1,241 @@ +#ifndef LANDER_HTTP_LOOP +#define LANDER_HTTP_LOOP + +#include + +#include "event_loop.h" +#include "http/req.h" +#include "http/res.h" +#include "http/types.h" + +// Max amount of steps a route can use +#define HTTP_LOOP_MAX_STEPS 17 + +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) + +/** + * Type of a route + */ +typedef enum http_route_type { + http_route_literal = 0, + http_route_regex = 1, +} http_route_type; + +/** + * Function describing a step in a route's processing. + * + * @param conn connection to process + * @return whether processing can proceed to the next step without performing + * I/O first. For a request step, `false` means more data needs to be read + * before the step can finish its processing. For response steps, `false` means + * there's new data in the write buffer that needs to be written. + */ +typedef bool (*http_step)(event_loop_conn *conn); + +extern const http_step http_default_res_steps[HTTP_LOOP_MAX_STEPS]; + +/** + * Struct describing a route a request can take. + */ +typedef struct http_route { + http_route_type type; + http_method method; + char *path; + // Compiled regex for a regex route. This value gets set at runtime when + // starting the http loop + regex_t *regex; + const http_step steps[HTTP_LOOP_MAX_STEPS]; + const http_step steps_res[HTTP_LOOP_MAX_STEPS]; +} http_route; + +/** + * Global context passed to every connection using the same pointer + */ +typedef struct http_loop_gctx { + http_route *routes; + size_t route_count; + void *(*custom_ctx_init)(); + void (*custom_ctx_reset)(void *); + void (*custom_ctx_free)(void *); + const char *api_key; + // Custom global context + void *c; +} http_loop_gctx; + +/** + * Initialize a new global context + * + * @return pointer to the newly allocated object. + */ +http_loop_gctx *http_loop_gctx_init(); + +/** + * Invidivual context initialized for every connection + */ +typedef struct http_loop_ctx { + http_request req; + http_response res; + http_route *route; + size_t current_step; + http_loop_gctx *g; + void *c; +} http_loop_ctx; + +/** + * Initialize a context struct + * + * @param g global context + * @return pointer to the newly allocated context + */ +http_loop_ctx *http_loop_ctx_init(http_loop_gctx *g); + +/** + * Resets an already allocated context so that it can be reused for a new + * request. + * + * @param ctx context to reset + */ +void http_loop_ctx_reset(http_loop_ctx *ctx); + +/** + * Free a context struct. Internally this first calls http_loop_ctx_reset. + * + * @param ctx context to free + */ +void http_loop_ctx_free(http_loop_ctx *ctx); + +/** + * Represents an HTTP loop + */ +typedef struct event_loop http_loop; + +/** + * Process incoming data as an HTTP request. This is the "handle_data" function + * for the event loop. + * + * @param conn connection to process + * @return whether another request can be processed immediately. + */ +bool http_loop_handle_request(event_loop_conn *conn); + +/** + * Try to parse the incoming data as an HTTP request. + * + * @param conn connection to process + * @return result of the parse + */ +http_parse_error http_loop_parse_request(event_loop_conn *conn); + +/** + * Try to match the parsed request with one of the defined routes, aka route the + * request. + * + * @param conn connection to process + */ +void http_loop_route_request(event_loop_conn *conn); + +/** + * Advance the processing of the routed request's processing by cycling through + * the request's various steps. + * + * @param conn connection to process + */ +void http_loop_process_request(event_loop_conn *conn); + +/** + * Handles the response processing. This is the `write_data` function for the + * event loop. + * + * @param conn connection to process + */ +void http_loop_handle_response(event_loop_conn *conn); + +/** + * Request step that consumes the request body and stores it in a buffer. + * + * @param conn connection to process + * @return true if the body has been fully received, false otherwise + */ +bool http_loop_step_body_to_buf(event_loop_conn *conn); + +/** + * Request step that consumes the request body and stores it in a file. + * + * @param conn connection to process + * @return true if the body has been fully received, false otherwise + */ +bool http_loop_step_body_to_file(event_loop_conn *conn); + +/** + * Try to parse the Content-Length header. + * + * @param conn connection to process + */ +bool http_loop_step_parse_content_length(event_loop_conn *conn); + +/** + * Authenticate the request using the X-Api-Key header. + * + * @param conn connection to check + * @return always true + */ +bool http_loop_step_auth(event_loop_conn *conn); + +/** + * A step that simply sets the connection's state to res. + * + * @param conn connection to process + * @return always true + */ +bool http_loop_step_switch_res(event_loop_conn *conn); + +/** + * Write the HTTP header back to the connection. If `res->head` is not set, a + * header will be generated for you. + * + * @param conn connection to process + */ +bool http_loop_step_write_header(event_loop_conn *conn); + +/** + * Write the HTTP body back to the connection. + * + * @param conn connection to process + */ +bool http_loop_step_write_body(event_loop_conn *conn); + +/** + * Initialize a new http loop. + * + * @param routes array of routes that should be served + * @param route_count how many elements are in `routes` + * @param custom_gctx the application's custom global context; can be NULL + * @param custom_ctx_init function to initialize a new custom context + * @param custom_ctx_reset function to reset a custom context + * @param custom_ctx_free function to free a custom context; will always be run + * after a reset + * @return pointer to the newly allocated object + */ +http_loop *http_loop_init(http_route *routes, size_t route_count, + void *custom_gctx, void *(*custom_ctx_init)(), + void(custom_ctx_reset)(void *), + void(custom_ctx_free)(void *)); + +/** + * Set the API key the authentication steps should use. + * + * @param hl HTTP loop to set key in + * @param api_key API key to use + */ +void http_loop_set_api_key(http_loop *hl, const char *api_key); + +/** + * Run the HTTP loop. This function never returns. + * + * @param el the event loop + * @param port on what port to listen + */ +void http_loop_run(http_loop *hl, int port); + +#endif diff --git a/include/lander.h b/include/lander.h index 7b418bc..a30c32d 100644 --- a/include/lander.h +++ b/include/lander.h @@ -1,10 +1,10 @@ #ifndef LANDER #define LANDER -#include "lnm/common.h" -#include "lnm/http/loop.h" +#include "http_loop.h" #include "lsm/store.h" +extern http_route lander_routes[6]; extern const char lander_key_charset[]; typedef struct lander_gctx { @@ -31,44 +31,44 @@ typedef enum lander_entry_type : uint8_t { void *lander_gctx_init(); -lnm_err lander_ctx_init(void **c_ctx, void *gctx); +void *lander_ctx_init(); void lander_ctx_reset(lander_ctx *ctx); void lander_ctx_free(lander_ctx *ctx); -lnm_http_step_err lander_get_index(lnm_http_conn *conn); +bool lander_get_index(event_loop_conn *conn); -lnm_http_step_err lander_get_entry(lnm_http_conn *conn); +bool lander_get_entry(event_loop_conn *conn); -lnm_http_step_err lander_post_redirect(lnm_http_conn *conn); +bool lander_post_redirect(event_loop_conn *conn); -lnm_http_step_err lander_post_redirect_secure(lnm_http_conn *conn); +bool lander_post_paste(event_loop_conn *conn); -lnm_http_step_err lander_post_paste(lnm_http_conn *conn); +bool lander_post_paste(event_loop_conn *conn); -lnm_http_step_err lander_post_paste_secure(lnm_http_conn *conn); +bool lander_post_redirect(event_loop_conn *conn); -lnm_http_step_err lander_stream_body_to_entry(lnm_http_conn *conn); +bool lander_stream_body_to_entry(event_loop_conn *conn); -lnm_http_step_err lander_post_redirect_body_to_attr(lnm_http_conn *conn); +bool lander_stream_body_to_client(event_loop_conn *conn); -lnm_http_step_err lander_remove_entry(lnm_http_conn *conn); +bool lander_post_redirect_body_to_attr(event_loop_conn *conn); -lnm_http_step_err lander_post_file(lnm_http_conn *conn); +bool lander_remove_entry(event_loop_conn *conn); -lnm_http_step_err lander_post_file_secure(lnm_http_conn *conn); +bool lander_post_file(event_loop_conn *conn); /** * Store the requested header as an attribute, if it's present. */ -void lander_header_to_attr(lnm_http_loop_ctx *ctx, const char *header, +void lander_header_to_attr(http_loop_ctx *ctx, const char *header, lander_attr_type attr_type); /** * Store the attribute's value as the provided header, if present. */ -void lander_attr_to_header(lnm_http_loop_ctx *ctx, lander_attr_type attr_type, - lnm_http_header header_type); +void lander_attr_to_header(http_loop_ctx *ctx, lander_attr_type attr_type, + http_header header_type); #endif diff --git a/landerctl/landerrc b/landerctl/landerrc deleted file mode 100644 index faa8b21..0000000 --- a/landerctl/landerrc +++ /dev/null @@ -1,3 +0,0 @@ -api_key = test -server_url = http://localhost:18080 -ca_certs_bundle = /etc/ssl/certs/ca-certificates.crt diff --git a/landerctl/src/main.c b/landerctl/src/main.c index fd53944..2e0b375 100644 --- a/landerctl/src/main.c +++ b/landerctl/src/main.c @@ -10,56 +10,25 @@ #include "landerctl.h" const char *cfg_file_name = ".landerrc"; -const char *usage = "%s [-SPFsv] [-c CONFIG_FILE] arg [key]\n"; +const char *usage = "%s [-SPFsv] arg [key]\n"; int main(int argc, char **argv) { landerctl_ctx ctx = {0}; + char *err_msg = NULL; + + landerctl_cfg_err parse_res; const char *home_dir = getenv("HOME"); - const char *cfg_path; if (home_dir == NULL) { - cfg_path = cfg_file_name; + parse_res = landerctl_cfg_parse(&ctx.cfg, cfg_file_name); } else { - // This is a blatant memleak if a custom config file is set, but it really - // doesn't matter for a short-lived CLI tool - char *buf = malloc(strlen(home_dir) + strlen(cfg_file_name) + 2); - sprintf(buf, "%s/%s", home_dir, cfg_file_name); - cfg_path = buf; + char cfg_path[strlen(home_dir) + strlen(cfg_file_name) + 2]; + sprintf(cfg_path, "%s/%s", home_dir, cfg_file_name); + + parse_res = landerctl_cfg_parse(&ctx.cfg, cfg_path); } - opterr = 0; - int c; - - while ((c = getopt(argc, argv, "SPFsvc:")) != -1) { - switch (c) { - case 'S': - ctx.mode = landerctl_mode_short; - break; - case 'P': - ctx.mode = landerctl_mode_paste; - break; - case 'F': - ctx.mode = landerctl_mode_file; - break; - case 's': - ctx.secure = true; - break; - case 'v': - ctx.verbose = true; - break; - case 'c': - cfg_path = optarg; - break; - case '?': - printf(usage, argv[0]); - exit(2); - } - } - - char *err_msg = NULL; - landerctl_cfg_err parse_res = landerctl_cfg_parse(&ctx.cfg, cfg_path); - switch (parse_res) { case landerctl_cfg_err_ok: break; @@ -79,6 +48,33 @@ int main(int argc, char **argv) { exit(1); } + opterr = 0; + + int c; + + while ((c = getopt(argc, argv, "SPFsv")) != -1) { + switch (c) { + case 'S': + ctx.mode = landerctl_mode_short; + break; + case 'P': + ctx.mode = landerctl_mode_paste; + break; + case 'F': + ctx.mode = landerctl_mode_file; + break; + case 's': + ctx.secure = true; + break; + case 'v': + ctx.verbose = true; + break; + case '?': + printf(usage, argv[0]); + exit(2); + } + } + if (ctx.mode == landerctl_mode_none) { printf("No mode specified.\n\n"); printf(usage, argv[0]); diff --git a/lnm b/lnm deleted file mode 160000 index 195eb9e..0000000 --- a/lnm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 195eb9eb4832f80ee8ab2372192196018362b252 diff --git a/src/event_loop/event_loop.c b/src/event_loop/event_loop.c new file mode 100644 index 0000000..a01ca37 --- /dev/null +++ b/src/event_loop/event_loop.c @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "event_loop.h" +#include "log.h" + +static void event_loop_fd_set_nb(int fd) { + int flags = fcntl(fd, F_GETFL); + + flags |= O_NONBLOCK; + + fcntl(fd, F_SETFL, flags); +} + +event_loop *event_loop_init() { + event_loop *el = calloc(sizeof(event_loop), 1); + + // No idea if this is a good starter value + el->connections = calloc(sizeof(event_loop_conn *), 16); + el->connection_count = 16; + + return el; +} + +int event_loop_put(event_loop *el, event_loop_conn *conn) { + if ((size_t)conn->fd >= el->connection_count) { + event_loop_conn **resized = + realloc(el->connections, sizeof(event_loop_conn *) * (conn->fd + 1)); + + if (resized == NULL) { + return -1; + } + + el->connections = resized; + el->connection_count = conn->fd + 1; + } + + el->connections[conn->fd] = conn; + + return 0; +} + +int event_loop_accept(event_loop *el, int fd) { + struct sockaddr_in client_addr; + socklen_t socklen = sizeof(client_addr); + int connfd = accept(fd, (struct sockaddr *)&client_addr, &socklen); + + if (connfd < 0) { + return -1; + } + + // set the new connection fd to nonblocking mode + event_loop_fd_set_nb(connfd); + + // creating the struct Conn + event_loop_conn *conn = event_loop_conn_init(el); + + // Close the connectoin if we fail to allocate a connection struct + if (conn == NULL) { + close(connfd); + + return -3; + } + + conn->fd = connfd; + conn->state = event_loop_conn_state_req; + + int res = event_loop_put(el, conn); + + if (res != 0) { + close(connfd); + + return -4; + } + + debug("Connection established on fd %i", connfd); + + return 0; +} + +void event_loop_run(event_loop *el, int port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + + if (fd < 0) { + critical(1, "Failed to open listening socket, errno: %i", errno); + } + + int val = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + + // bind + struct sockaddr_in addr = {.sin_family = AF_INET, + .sin_port = ntohs(port), + .sin_addr.s_addr = ntohl(0)}; + + int res = bind(fd, (const struct sockaddr *)&addr, sizeof(addr)); + + if (res) { + critical(1, "Failed to bind listening socket, errno: %i", errno); + } + + debug("Listening socket bound to fd %i", fd); + + res = listen(fd, SOMAXCONN); + + if (res) { + critical(1, "Failed to start listening on listening socket, errno: %i", + errno); + } + + // The listening socket is always poll'ed in non-blocking mode as well + event_loop_fd_set_nb(fd); + + // TODO don't hardcode the number 32 + struct pollfd *poll_args = calloc(sizeof(struct pollfd), 32); + + // for convenience, the listening fd is put in the first position + struct pollfd pfd = {fd, POLLIN, 0}; + poll_args[0] = pfd; + + event_loop_conn *conn; + int events; + + info("Starting event loop on port %i", port); + + while (1) { + size_t poll_args_count = 1; + + // connection fds + for (size_t i = 0; i < el->connection_count; i++) { + conn = el->connections[i]; + + if (conn == NULL) { + continue; + } + + events = (conn->state == event_loop_conn_state_req) ? POLLIN : POLLOUT; + events |= POLLERR; + + pfd.fd = conn->fd; + pfd.events = events; + + poll_args[poll_args_count] = pfd; + poll_args_count++; + + // We do at most 32 connections at a time for now + if (poll_args_count == 32) + break; + } + + // poll for active fds + // the timeout argument doesn't matter here + int rv = poll(poll_args, (nfds_t)poll_args_count, -1); + + if (rv < 0) { + critical(1, "Poll failed, errno: %i", errno); + } + + // process active connections + for (size_t i = 1; i < poll_args_count; i++) { + if (poll_args[i].revents) { + conn = el->connections[poll_args[i].fd]; + + event_loop_conn_io(el, conn); + + if (conn->state == event_loop_conn_state_end) { + // client closed normally, or something bad happened. + // destroy this connection + el->connections[conn->fd] = NULL; + + close(conn->fd); + debug("Connection closed on fd %i", conn->fd); + event_loop_conn_free(el, conn); + } + } + } + + // try to accept a new connection if the listening fd is active + if (poll_args[0].revents) { + event_loop_accept(el, fd); + } + } +} diff --git a/src/event_loop/event_loop_conn.c b/src/event_loop/event_loop_conn.c new file mode 100644 index 0000000..4791d8f --- /dev/null +++ b/src/event_loop/event_loop_conn.c @@ -0,0 +1,13 @@ +#include "event_loop.h" + +event_loop_conn *event_loop_conn_init(event_loop *el) { + event_loop_conn *conn = calloc(sizeof(event_loop_conn), 1); + conn->ctx = el->ctx_init(el->gctx); + + return conn; +} + +void event_loop_conn_free(event_loop *el, event_loop_conn *conn) { + el->ctx_free(conn->ctx); + free(conn); +} diff --git a/src/event_loop/event_loop_io.c b/src/event_loop/event_loop_io.c new file mode 100644 index 0000000..674c6ec --- /dev/null +++ b/src/event_loop/event_loop_io.c @@ -0,0 +1,98 @@ +#include +#include +#include + +#include "event_loop.h" + +void event_loop_conn_io_res(event_loop *el, event_loop_conn *conn) { + do { + ssize_t res = 0; + size_t remain = conn->wbuf_size - conn->wbuf_sent; + + do { + res = write(conn->fd, &conn->wbuf[conn->wbuf_sent], remain); + } while (res < 0 && errno == EINTR); + + // EAGAIN doesn't mean there was an error, but rather that there's no more + // data right now, but there might be more later, aka "try again later" + if (res < 0 && errno == EAGAIN) { + return; + } + + // If it's not EGAIN, there was an error writing so we simply end the + // request + if (res < 0) { + conn->state = event_loop_conn_state_end; + return; + } + + conn->wbuf_sent += (size_t)res; + + // After writing the entire buffer, we run the write_data command to receive + // new data, or exit the loop + if (conn->wbuf_sent == conn->wbuf_size) { + conn->wbuf_sent = 0; + conn->wbuf_size = 0; + + el->write_data(conn); + } + } while (conn->state == event_loop_conn_state_res); +} + +/** + * Read new data into the read buffer. This command performs at most one + * successful read syscall. + * + * Returns whether the function should be retried immediately or not. + */ +void event_loop_conn_io_req(event_loop *el, event_loop_conn *conn) { + do { + // Move remaining data to start of buffer + memmove(conn->rbuf, &conn->rbuf[conn->rbuf_read], + conn->rbuf_size - conn->rbuf_read); + conn->rbuf_size -= conn->rbuf_read; + conn->rbuf_read = 0; + + ssize_t res; + size_t cap = EVENT_LOOP_BUFFER_SIZE - conn->rbuf_size; + + // Try to read at most cap bytes from the file descriptor + do { + res = read(conn->fd, &conn->rbuf[conn->rbuf_size], cap); + } while (res < 0 && errno == EINTR); + + // EGAIN means we try again later + if (res < 0 && errno == EAGAIN) { + return; + } + + // Any other negative error message means the read errored out. If res is 0, + // we've reached the end of the input which (usually) means the remote peer + // has closed the connection. Either way, we close the connection. + if (res <= 0) { + conn->state = event_loop_conn_state_end; + + return; + } + + conn->rbuf_size += (size_t)res; + + // This loop allows processing multiple requests from a single read buffer + while (el->handle_data(conn)) + ; + } + // We can keep reading as long as we're in request mode + while (conn->state == event_loop_conn_state_req); +} + +void event_loop_conn_io(event_loop *el, event_loop_conn *conn) { + switch (conn->state) { + case event_loop_conn_state_req: + event_loop_conn_io_req(el, conn); + break; + case event_loop_conn_state_res: + event_loop_conn_io_res(el, conn); + break; + case event_loop_conn_state_end:; + } +} diff --git a/src/http/http_consts.c b/src/http/http_consts.c new file mode 100644 index 0000000..8aa6f4b --- /dev/null +++ b/src/http/http_consts.c @@ -0,0 +1,131 @@ +#include + +#include "http/types.h" + +// Very important that this is in the same order as http_request_method +const char *http_method_names[] = {"GET", "POST", "PUT", "PATCH", "DELETE"}; +const size_t http_method_names_len = + sizeof(http_method_names) / sizeof(http_method_names[0]); + +// clang-format off + +const char *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 *http_header_names[] = { + "Connection", + "Location", + "Content-Type", + "Content-Disposition" +}; + +const char *http_mime_type_names[][2] = { + { "aac", "audio/aac" }, + { "bz", "application/x-bzip" }, + { "bz2", "application/x-bzip2" }, + { "css", "text/css" }, + { "csv", "text/csv" }, + { "gz", "application/gzip" }, + { "gif", "image/gif" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "jar", "application/java-archive" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "js", "text/javascript" }, + { "json", "application/json" }, + { "mp3", "audio/mpeg" }, + { "mp4", "video/mp4" }, + { "png", "image/png" }, + { "pdf", "application/pdf" }, + { "rar", "application/vnd.rar" }, + { "sh", "application/x-sh" }, + { "svg", "image/svg+xml" }, + { "tar", "application/x-tar" }, + { "txt", "text/plain" }, + { "wav", "audio/wav" }, + { "7z", "application/x-7z-compressed" }, +}; + +// clang-format on diff --git a/src/http/res.c b/src/http/res.c new file mode 100644 index 0000000..c3e8806 --- /dev/null +++ b/src/http/res.c @@ -0,0 +1,37 @@ +#include + +#include "http/res.h" + +void http_res_set_body_buf(http_response *res, const char *body, + size_t body_len, bool owned) { + res->body.type = http_body_buf; + res->body.buf = (char *)body; + res->body.expected_len = body_len; + res->body.buf_owned = owned; +} + +void http_res_set_body_file(http_response *res, const char *filename) { + struct stat st; + stat(filename, &st); + + // TODO error handling + FILE *f = fopen(filename, "r"); + + res->body.type = http_body_file; + res->body.file = f; + res->body.expected_len = st.st_size; +} + +void http_res_add_header(http_response *res, http_header type, + const char *value, bool owned) { + res->headers[res->header_count].type = type; + res->headers[res->header_count].value = value; + res->headers[res->header_count].owned = owned; + + res->header_count++; +} + +void http_res_set_mime_type(http_response *res, http_mime_type mime_type) { + http_res_add_header(res, http_header_content_type, + http_mime_type_names[mime_type][1], false); +} diff --git a/src/http/types.c b/src/http/types.c new file mode 100644 index 0000000..d7f2647 --- /dev/null +++ b/src/http/types.c @@ -0,0 +1,33 @@ +#include + +#include "http/types.h" + +http_body *http_body_init() { return calloc(sizeof(http_body), 1); } + +void http_body_reset(http_body *body) { + if (body->type == http_body_file) { + fclose(body->file); + } + + if (body->buf_owned) { + free(body->buf); + } + + if (body->fname_owned) { + free(body->fname); + } + + body->type = 0; + body->buf = NULL; + body->buf_owned = false; + body->file = NULL; + body->fname = NULL; + body->fname_owned = false; + body->expected_len = 0; + body->len = 0; +} + +void http_body_free(http_body *body) { + http_body_reset(body); + free(body); +} diff --git a/src/http_loop/http_loop.c b/src/http_loop/http_loop.c new file mode 100644 index 0000000..cb4289e --- /dev/null +++ b/src/http_loop/http_loop.c @@ -0,0 +1,101 @@ +#include + +#include "http/types.h" +#include "http_loop.h" +#include "log.h" + +const http_step http_default_res_steps[HTTP_LOOP_MAX_STEPS] = { + http_loop_step_write_header, http_loop_step_write_body, NULL}; + +bool http_loop_handle_request(event_loop_conn *conn) { + // Prevents the request handler function from looping indefinitely without + // ever consuming new data + if (conn->rbuf_size - conn->rbuf_read == 0) { + return false; + } + + http_loop_ctx *ctx = conn->ctx; + + // If route is defined, we're currently processing a request + if (ctx->route == NULL) { + http_parse_error res = http_loop_parse_request(conn); + + if (res == http_parse_error_invalid || + (res == http_parse_error_incomplete && + conn->rbuf_size == EVENT_LOOP_BUFFER_SIZE)) { + conn->state = event_loop_conn_state_end; + + return false; + } + + conn->rbuf_read += ctx->req.len; + + // It's fun to respond with extremely specific error messages + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501 + if (res == http_parse_error_unknown_method) { + ctx->res.status = http_method_not_implemented; + conn->state = event_loop_conn_state_res; + } else { + http_loop_route_request(conn); + } + } + + if (conn->state == event_loop_conn_state_req) { + http_loop_process_request(conn); + } + + // TODO in highly concurrent situations, it might actually be better to always + // return false here, as this allows cycling better through all connections + return conn->state == event_loop_conn_state_req; +} + +event_loop *http_loop_init(http_route *routes, size_t route_count, + void *custom_gctx, void *(*custom_ctx_init)(), + void(custom_ctx_reset)(), void(custom_ctx_free)()) { + event_loop *el = event_loop_init(); + + el->ctx_init = (void *(*)(void *))http_loop_ctx_init; + el->ctx_free = (void (*)(void *))http_loop_ctx_free; + el->handle_data = http_loop_handle_request; + el->write_data = http_loop_handle_response; + + http_loop_gctx *gctx = http_loop_gctx_init(); + gctx->c = custom_gctx; + gctx->routes = routes; + gctx->route_count = route_count; + gctx->custom_ctx_init = custom_ctx_init; + gctx->custom_ctx_reset = custom_ctx_reset; + gctx->custom_ctx_free = custom_ctx_free; + el->gctx = gctx; + + return el; +} + +void http_loop_set_api_key(http_loop *hl, const char *api_key) { + http_loop_gctx *gctx = hl->gctx; + gctx->api_key = api_key; +} + +void http_loop_run(event_loop *el, int port) { + debug("Compiling RegEx routes"); + + http_loop_gctx *gctx = el->gctx; + + for (size_t i = 0; i < gctx->route_count; i++) { + http_route *route = &gctx->routes[i]; + + if (route->type == http_route_regex) { + regex_t *r = calloc(sizeof(regex_t), 1); + + if (regcomp(r, route->path, REG_EXTENDED) != 0) { + critical(1, "RegEx expression '%s' failed to compile", route->path); + } + + route->regex = r; + } + } + + debug("RegEx routes compiled successfully"); + + event_loop_run(el, port); +} diff --git a/src/http_loop/http_loop_ctx.c b/src/http_loop/http_loop_ctx.c new file mode 100644 index 0000000..8d0db1e --- /dev/null +++ b/src/http_loop/http_loop_ctx.c @@ -0,0 +1,52 @@ +#include + +#include "http/types.h" +#include "http_loop.h" + +http_loop_gctx *http_loop_gctx_init() { + http_loop_gctx *gctx = calloc(sizeof(http_loop_gctx), 1); + + return gctx; +} + +http_loop_ctx *http_loop_ctx_init(http_loop_gctx *g) { + http_loop_ctx *ctx = calloc(sizeof(http_loop_ctx), 1); + ctx->g = g; + ctx->c = g->custom_ctx_init(); + + return ctx; +} + +void http_loop_ctx_free(http_loop_ctx *ctx) { + http_loop_ctx_reset(ctx); + ctx->g->custom_ctx_free(ctx->c); + + free(ctx); +} + +void http_loop_ctx_reset(http_loop_ctx *ctx) { + ctx->route = NULL; + ctx->current_step = 0; + + if (ctx->res.head != NULL) { + free((void *)ctx->res.head); + ctx->res.head = NULL; + } + + http_body_reset(&ctx->req.body); + http_body_reset(&ctx->res.body); + + for (size_t i = 0; i < ctx->res.header_count; i++) { + if (ctx->res.headers[i].owned) { + free((void *)ctx->res.headers[i].value); + } + } + + ctx->res.header_count = 0; + + ctx->res.status = 0; + ctx->res.head_len = 0; + ctx->res.head_written = 0; + + ctx->g->custom_ctx_reset(ctx->c); +} diff --git a/src/http_loop/http_loop_req.c b/src/http_loop/http_loop_req.c new file mode 100644 index 0000000..a8cd841 --- /dev/null +++ b/src/http_loop/http_loop_req.c @@ -0,0 +1,149 @@ +#include + +#include "http_loop.h" +#include "log.h" + +http_parse_error http_loop_parse_request(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + // First we try to parse the incoming HTTP request + size_t num_headers = HTTP_MAX_ALLOWED_HEADERS; + http_request *req = &ctx->req; + + const char *method; + size_t method_len; + char *path; + size_t path_len; + + int res = + phr_parse_request((const char *)&conn->rbuf[conn->rbuf_read], + conn->rbuf_size - conn->rbuf_read, &method, &method_len, + (const char **)&path, &path_len, &req->minor_version, + req->headers, &num_headers, 0); + + if (res == -1) { + return http_parse_error_invalid; + } else if (res == -2) { + return http_parse_error_incomplete; + } + + req->num_headers = num_headers; + req->len = res; + + // Try to parse the method type + bool match = false; + size_t i = 0; + + for (i = 0; i < http_method_names_len; i++) { + if (strncmp(method, http_method_names[i], method_len) == 0) { + req->method = i; + match = true; + } + } + + if (!match) { + return http_parse_error_unknown_method; + } + + // Split path into path & query + i = 0; + bool no_query = true; + + while (no_query && i < path_len) { + if (path[i] == '?') { + // Ensure we don't store an invalid pointer if the request simply ends + // with '?' + if (i + 1 < req->path_len) { + req->query = &path[i + 1]; + req->query_len = path_len - (i + 1); + } + + path_len = i; + + no_query = false; + } + + i++; + } + + // The path needs to be NULL-terminated in order for regex routes to be + // matched properly. We know we can overwrite this char because it's either + // '?' if there's a query, or '\n' because we know the buf contains a valid + // HTTP rqeuest + path[path_len] = '\0'; + + req->path = path; + req->path_len = path_len; + + // Ensure we clear the old request's query + if (no_query) { + req->query = NULL; + req->query_len = 0; + } + + return http_parse_error_ok; +} + +void http_loop_route_request(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + http_loop_gctx *gctx = ctx->g; + + info("%s %.*s", http_method_names[ctx->req.method], ctx->req.path_len, + ctx->req.path); + + http_route *route; + bool path_matched = false; + + for (size_t i = 0; i < gctx->route_count; i++) { + route = &gctx->routes[i]; + + switch (route->type) { + case http_route_literal: + if (strncmp(route->path, ctx->req.path, ctx->req.path_len) == 0) { + path_matched = true; + + if (ctx->req.method == route->method) { + ctx->route = route; + return; + } + } + break; + case http_route_regex: + if (regexec(route->regex, ctx->req.path, HTTP_MAX_REGEX_GROUPS, + ctx->req.regex_groups, 0) == 0) { + path_matched = true; + + if (ctx->req.method == route->method) { + ctx->route = route; + return; + } + } + break; + } + } + + // Fallthrough case + ctx->res.status = path_matched ? http_method_not_allowed : http_not_found; + conn->state = event_loop_conn_state_res; +} + +void http_loop_process_request(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + // We keep processing step functions as long as they don't need to wait for + // I/O + while ((conn->state == event_loop_conn_state_req) && + (ctx->route->steps[ctx->current_step] != NULL) && + ctx->route->steps[ctx->current_step](conn)) { + ctx->current_step++; + } + + // Request processing can stop early by switching the connection state + // Either way, we reset the step counter as it will be used by the response + // steps + if ((conn->state != event_loop_conn_state_req) || + (ctx->route->steps[ctx->current_step] == NULL)) { + ctx->current_step = 0; + conn->state = event_loop_conn_state_res; + } +} diff --git a/src/http_loop/http_loop_res.c b/src/http_loop/http_loop_res.c new file mode 100644 index 0000000..b29550f --- /dev/null +++ b/src/http_loop/http_loop_res.c @@ -0,0 +1,127 @@ +#include "http_loop.h" +#include "log.h" + +// cppcheck-suppress syntaxError +static const char *http_response_format = "HTTP/1.1 %i %s\n" + "Server: lander/" LANDER_VERSION "\n" + "Content-Length: %lu\n"; + +/* + * This function precalculates the size of the total buffer required using + * snprintf. When this function is called with a buf size of 0, it never tries + * to write any data, but it does return the amount of bytes that would be + * written. + */ +void http_loop_init_header(http_response *res) { + if (res->status == 0) { + res->status = http_ok; + } + + const char *response_type_name = + http_status_names[res->status / 100 - 1][res->status % 100]; + + // First we calculate the size of the start of the header + int buf_size = snprintf(NULL, 0, http_response_format, res->status, + response_type_name, res->body.expected_len); + + // We add each header's required size + for (size_t i = 0; i < res->header_count; i++) { + buf_size += + snprintf(NULL, 0, "%s: %s\n", http_header_names[res->headers[i].type], + res->headers[i].value); + } + + // The + 1 is required to store the final null byte, but we will replace it + // with the required final newline + char *buf = malloc(buf_size + 1); + buf_size = sprintf(buf, http_response_format, res->status, response_type_name, + res->body.expected_len); + + for (size_t i = 0; i < res->header_count; i++) { + buf_size += + sprintf(&buf[buf_size], "%s: %s\n", + http_header_names[res->headers[i].type], res->headers[i].value); + } + + buf[buf_size] = '\n'; + + res->head = buf; + res->head_len = buf_size + 1; +} + +bool http_loop_step_write_header(event_loop_conn *conn) { + http_response *res = &((http_loop_ctx *)conn->ctx)->res; + + // Create head response + if (res->head == NULL) { + http_loop_init_header(res); + } + + // Step has finished its work + if (res->head_written == res->head_len) { + return true; + } + + size_t bytes_to_write = MIN(res->head_len - res->head_written, + EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); + memcpy(&conn->wbuf[conn->wbuf_size], &res->head[res->head_written], + bytes_to_write); + + conn->wbuf_size += bytes_to_write; + res->head_written += bytes_to_write; + + return false; +} + +bool http_loop_step_write_body(event_loop_conn *conn) { + http_response *res = &((http_loop_ctx *)conn->ctx)->res; + + if (res->body.expected_len == res->body.len) { + return true; + } + + size_t bytes_to_write = MIN(res->body.expected_len - res->body.len, + EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); + + size_t bytes_written; + + switch (res->body.type) { + case http_body_buf: + memcpy(&conn->wbuf[conn->wbuf_size], &(res->body.buf)[res->body.len], + bytes_to_write); + conn->wbuf_size += bytes_to_write; + res->body.len += bytes_to_write; + break; + case http_body_file: + bytes_written = fread(&conn->wbuf[conn->wbuf_size], sizeof(uint8_t), + bytes_to_write, res->body.file); + conn->wbuf_size += bytes_written; + res->body.len += bytes_written; + break; + } + + return false; +} + +void http_loop_handle_response(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + // Non-routed requests also need to be processed + const http_step *steps = + ctx->route != NULL ? ctx->route->steps_res : http_default_res_steps; + + while ((conn->state == event_loop_conn_state_res) && + (steps[ctx->current_step] != NULL) && steps[ctx->current_step](conn)) { + ctx->current_step++; + } + + // Response processing can stop early be switching the connection state + // After response processing has finished its work, we reset the context to + // prepare for a new request + if ((conn->state != event_loop_conn_state_res) || + (steps[ctx->current_step] == NULL)) { + http_loop_ctx_reset(ctx); + + conn->state = event_loop_conn_state_req; + } +} diff --git a/src/http_loop/http_loop_steps.c b/src/http_loop/http_loop_steps.c new file mode 100644 index 0000000..99c5cce --- /dev/null +++ b/src/http_loop/http_loop_steps.c @@ -0,0 +1,179 @@ +#include +#include + +#include "http_loop.h" +#include "lander.h" + +// Just a naive pow implementation; might improve later +static uint64_t ipow(uint64_t base, uint64_t power) { + uint64_t res = 1; + + while (power > 0) { + res *= base; + power--; + } + + return res; +} + +/* + * Converts a string to a number, returning true if the string contained a valid + * positive number. + */ +static bool string_to_num(size_t *res, const char *s, size_t len) { + *res = 0; + + for (size_t i = 0; i < len; i++) { + int val = s[i] - '0'; + + if (val < 0 || val > 9) { + return false; + } + + *res += (uint64_t)val * ipow(10, (len - 1) - i); + } + + return true; +} + +bool http_loop_step_parse_content_length(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + for (size_t i = 0; i < ctx->req.num_headers; i++) { + const struct phr_header *header = &ctx->req.headers[i]; + + if (strncmp(header->name, "Content-Length", header->name_len) == 0) { + // If the content length header is present but contains an invalid + // number, we return a bad request error + if (!string_to_num(&ctx->req.body.expected_len, header->value, + header->value_len)) { + ctx->res.status = http_bad_request; + conn->state = event_loop_conn_state_res; + + return true; + } + // The content length was actually 0, so we can instantly return here + else if (ctx->req.body.expected_len == 0) { + return true; + } + } + } + + // A zero here means there's no content length header + if (ctx->req.body.expected_len == 0) { + ctx->res.status = http_length_required; + conn->state = event_loop_conn_state_res; + } + + return true; +} + +/* + * Try to find and parse the Content-Length header. This function returns true + * if it was successful. If false is returned, the underlying step should + * immediately exit. + */ +bool try_parse_content_length(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + for (size_t i = 0; i < ctx->req.num_headers; i++) { + const struct phr_header *header = &ctx->req.headers[i]; + + if (strncmp(header->name, "Content-Length", header->name_len) == 0) { + // If the content length header is present but contains an invalid + // number, we return a bad request error + if (!string_to_num(&ctx->req.body.expected_len, header->value, + header->value_len)) { + ctx->res.status = http_bad_request; + conn->state = event_loop_conn_state_res; + + return false; + } + // The content length was actually 0, so we can instantly return here + else if (ctx->req.body.expected_len == 0) { + return false; + } + } + } + + // A zero here means there's no content length header + if (ctx->req.body.expected_len == 0) { + ctx->res.status = http_length_required; + conn->state = event_loop_conn_state_res; + + return false; + } + + return true; +} + +bool http_loop_step_body_to_buf(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + if (ctx->req.body.expected_len == 0) { + if (!try_parse_content_length(conn)) { + return true; + } + + ctx->req.body.type = http_body_buf; + ctx->req.body.buf = malloc(ctx->req.body.expected_len * sizeof(char)); + ctx->req.body.len = 0; + } + + size_t bytes_to_copy = MIN(conn->rbuf_size - conn->rbuf_read, + ctx->req.body.expected_len - ctx->req.body.len); + memcpy(&ctx->req.body.buf[ctx->req.body.len], &conn->rbuf[conn->rbuf_read], + bytes_to_copy); + ctx->req.body.len += bytes_to_copy; + conn->rbuf_read += bytes_to_copy; + + return ctx->req.body.len == ctx->req.body.expected_len; +} + +bool http_loop_step_body_to_file(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + if (ctx->req.body.expected_len == 0) { + if (!try_parse_content_length(conn)) { + return true; + } + + ctx->req.body.type = http_body_file; + ctx->req.body.file = fopen(ctx->req.body.fname, "wb"); + ctx->req.body.len = 0; + } + + size_t bytes_to_write = MIN(conn->rbuf_size - conn->rbuf_read, + ctx->req.body.expected_len - ctx->req.body.len); + size_t bytes_written = fwrite(&conn->rbuf[conn->rbuf_read], sizeof(uint8_t), + bytes_to_write, ctx->req.body.file); + ctx->req.body.len += bytes_written; + conn->rbuf_read += bytes_written; + + return ctx->req.body.len == ctx->req.body.expected_len; +} + +bool http_loop_step_auth(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + for (size_t i = 0; i < ctx->req.num_headers; i++) { + const struct phr_header *header = &ctx->req.headers[i]; + + if ((strncmp("X-Api-Key", header->name, header->name_len) == 0) && + (strncmp(header->value, ctx->g->api_key, header->value_len) == 0) && + (strlen(ctx->g->api_key) == header->value_len)) { + return true; + } + } + + ctx->res.status = http_unauthorized; + conn->state = event_loop_conn_state_res; + + return true; +} + +bool http_loop_step_switch_res(event_loop_conn *conn) { + conn->state = event_loop_conn_state_res; + + return true; +} diff --git a/src/lander/lander.c b/src/lander/lander.c index ff44b5c..5d1c1fe 100644 --- a/src/lander/lander.c +++ b/src/lander/lander.c @@ -1,27 +1,66 @@ #include #include -#include "lnm/common.h" -#include "lsm/store.h" - +#include "http/types.h" +#include "http_loop.h" #include "lander.h" +#include "lsm/store.h" const char lander_key_charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +http_route lander_routes[] = { + {.type = http_route_literal, + .method = http_get, + .path = "/", + .steps = {lander_get_index, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}}, + { + .type = http_route_regex, + .method = http_get, + .path = "^/([^/]+)$", + .steps = {lander_get_entry, NULL}, + .steps_res = {http_loop_step_write_header, lander_stream_body_to_client, + NULL}, + }, + { + .type = http_route_regex, + .method = http_delete, + .path = "^/([^/]+)$", + .steps = {http_loop_step_auth, lander_remove_entry, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}, + }, + { + .type = http_route_regex, + .method = http_post, + .path = "^/s(l?)/([^/]*)$", + .steps = {http_loop_step_auth, lander_post_redirect, + http_loop_step_body_to_buf, lander_post_redirect_body_to_attr, + NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}, + }, + {.type = http_route_regex, + .method = http_post, + .path = "^/p(l?)/([^/]*)$", + .steps = {http_loop_step_auth, http_loop_step_parse_content_length, + lander_post_paste, lander_stream_body_to_entry, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}}, + {.type = http_route_regex, + .method = http_post, + .path = "^/f(l?)/([^/]*)$", + .steps = {http_loop_step_auth, http_loop_step_parse_content_length, + lander_post_file, lander_stream_body_to_entry, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}}, +}; + void *lander_gctx_init() { return calloc(1, sizeof(lander_gctx)); } -lnm_err lander_ctx_init(void **c_ctx, void *gctx) { - lander_ctx *ctx = calloc(1, sizeof(lander_ctx)); - - if (ctx == NULL) { - return lnm_err_failed_alloc; - } - - *c_ctx = ctx; - - return lnm_err_ok; -} +void *lander_ctx_init() { return calloc(1, sizeof(lander_ctx)); } void lander_ctx_reset(lander_ctx *ctx) { if (ctx->entry != NULL) { @@ -33,30 +72,36 @@ void lander_ctx_reset(lander_ctx *ctx) { void lander_ctx_free(lander_ctx *ctx) { free(ctx); } -void lander_header_to_attr(lnm_http_loop_ctx *ctx, const char *header_name, +void lander_header_to_attr(http_loop_ctx *ctx, const char *header_name, lander_attr_type attr_type) { lander_ctx *c_ctx = ctx->c; - const char *header_value; - size_t header_value_len; + for (size_t i = 0; i < ctx->req.num_headers; i++) { + const struct phr_header *header = &ctx->req.headers[i]; - if (lnm_http_req_header_get_s(&header_value, &header_value_len, &ctx->req, - header_name) == lnm_err_ok) { - lsm_str *value; - lsm_str_init_copy_n(&value, (char *)header_value, header_value_len); + if (strncmp(header->name, header_name, header->name_len) == 0) { + if (header->value_len > 0) { + lsm_str *value; + lsm_str_init_copy_n(&value, (char *)header->value, header->value_len); - lsm_entry_attr_insert(c_ctx->entry, attr_type, value); + lsm_entry_attr_insert(c_ctx->entry, attr_type, value); + } + + return; + } } } -void lander_attr_to_header(lnm_http_loop_ctx *ctx, lander_attr_type attr_type, - lnm_http_header header_type) { +void lander_attr_to_header(http_loop_ctx *ctx, lander_attr_type attr_type, + http_header header_type) { lander_ctx *c_ctx = ctx->c; lsm_str *value; if (lsm_entry_attr_get(&value, c_ctx->entry, attr_type) == lsm_error_ok) { - lnm_http_res_add_header_len(&ctx->res, header_type, - (char *)lsm_str_ptr(value), lsm_str_len(value), - false); + char *buf = malloc(lsm_str_len(value) + 1); + memcpy(buf, lsm_str_ptr(value), lsm_str_len(value)); + buf[lsm_str_len(value)] = '\0'; + + http_res_add_header(&ctx->res, header_type, buf, true); } } diff --git a/src/lander/lander_delete.c b/src/lander/lander_delete.c index f13326b..e91b6c9 100644 --- a/src/lander/lander_delete.c +++ b/src/lander/lander_delete.c @@ -1,31 +1,29 @@ -#include "lnm/http/req.h" -#include "lnm/loop.h" - #include "lander.h" -lnm_http_step_err lander_remove_entry(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_remove_entry(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - lnm_http_loop_gctx *gctx = ctx->g; + http_loop_gctx *gctx = ctx->g; lander_gctx *c_gctx = gctx->c; - const char *key_s; - size_t key_len = lnm_http_req_route_segment(&key_s, &ctx->req, "key"); + const char *key_s = &ctx->req.path[ctx->req.regex_groups[1].rm_so]; + int key_len = ctx->req.regex_groups[1].rm_eo - ctx->req.regex_groups[1].rm_so; lsm_str *key; lsm_str_init_copy_n(&key, (char *)key_s, key_len); switch (lsm_store_open_write(&c_ctx->entry, c_gctx->store, key)) { case lsm_error_ok: - lsm_entry_remove(c_ctx->entry); break; case lsm_error_not_found: - ctx->res.status = lnm_http_status_not_found; - break; + ctx->res.status = http_not_found; + return true; default: - ctx->res.status = lnm_http_status_internal_server_error; - break; + ctx->res.status = http_internal_server_error; + return true; } - return lnm_http_step_err_done; + lsm_entry_remove(c_ctx->entry); + + return true; } diff --git a/src/lander/lander_get.c b/src/lander/lander_get.c index 7d4a186..102c631 100644 --- a/src/lander/lander_get.c +++ b/src/lander/lander_get.c @@ -1,14 +1,11 @@ #include -#include - -#include "lnm/http/consts.h" -#include "lnm/http/loop.h" -#include "lnm/http/req.h" -#include "lnm/log.h" -#include "lnm/loop.h" -#include "lsm/store.h" +#include "event_loop.h" +#include "http/res.h" +#include "http/types.h" #include "lander.h" +#include "log.h" +#include "lsm/store.h" static const char index_page[] = "\n" @@ -20,19 +17,18 @@ static const char index_page[] = " \n" "\n"; -lnm_http_step_err lander_get_index(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_get_index(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; - lnm_http_res_body_set_buf(&ctx->res, (char *)index_page, - sizeof(index_page) - 1, false); - lnm_http_res_add_header(&ctx->res, lnm_http_header_content_type, "text/html", - false); + http_res_set_body_buf(&ctx->res, index_page, sizeof(index_page) - 1, false); + http_res_set_mime_type(&ctx->res, http_mime_html); - return lnm_http_step_err_done; + conn->state = event_loop_conn_state_res; + return true; } -lnm_http_step_err lander_get_redirect(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +void lander_get_redirect(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; // For redirects, the URL is stored as an in-memory attribute @@ -41,107 +37,124 @@ lnm_http_step_err lander_get_redirect(lnm_http_conn *conn) { // This shouldn't be able to happen if (lsm_entry_attr_get(&url_attr_val, c_ctx->entry, lander_attr_type_url) != lsm_error_ok) { - lnm_lerror("lander", "%s", - "Entry of type redirect detected without URL attribute"); + error("Entry of type redirect detected without URL attribute"); - ctx->res.status = lnm_http_status_internal_server_error; + ctx->res.status = http_internal_server_error; lsm_entry_close(c_ctx->entry); c_ctx->entry = NULL; - return lnm_http_step_err_res; + return; } - lnm_http_res_add_header_len(&ctx->res, lnm_http_header_location, - (char *)lsm_str_ptr(url_attr_val), - lsm_str_len(url_attr_val), false); + char *buf = malloc(lsm_str_len(url_attr_val) + 1); + memcpy(buf, lsm_str_ptr(url_attr_val), lsm_str_len(url_attr_val)); - ctx->res.status = lnm_http_status_moved_permanently; + buf[lsm_str_len(url_attr_val)] = '\0'; - return lnm_http_step_err_done; + ctx->res.status = http_moved_permanently; + http_res_add_header(&ctx->res, http_header_location, buf, true); + + // We no longer need the entry at this point, so we can unlock it early + // This will also signal to the response code not to read any data from + // the entry + lsm_entry_close(c_ctx->entry); + c_ctx->entry = NULL; } -lnm_err lander_entry_data_streamer(uint64_t *written, char *buf, - lnm_http_conn *conn, uint64_t offset, - uint64_t len) { - // TODO respect offset variable - - lnm_http_loop_ctx *ctx = conn->ctx; +void lander_get_paste(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - lsm_entry_data_read(written, buf, c_ctx->entry, len); - - return lnm_err_ok; + ctx->res.body.expected_len = lsm_entry_data_len(c_ctx->entry); + http_res_set_mime_type(&ctx->res, http_mime_txt); } -lnm_http_step_err lander_get_paste(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +void lander_get_file(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - lnm_http_res_body_set_fn(&ctx->res, lander_entry_data_streamer, - lsm_entry_data_len(c_ctx->entry)); - lnm_http_res_add_header(&ctx->res, lnm_http_header_content_type, "text/plain", - false); - - return lnm_http_step_err_done; -} - -lnm_http_step_err lander_get_file(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; - lander_ctx *c_ctx = ctx->c; - - lnm_http_res_body_set_fn(&ctx->res, lander_entry_data_streamer, - lsm_entry_data_len(c_ctx->entry)); + ctx->res.body.expected_len = lsm_entry_data_len(c_ctx->entry); lander_attr_to_header(ctx, lander_attr_type_content_type, - lnm_http_header_content_type); + http_header_content_type); - return lnm_http_step_err_done; + lsm_str *value; + char *buf; + + if (lsm_entry_attr_get(&value, c_ctx->entry, lander_attr_type_file_name) == + lsm_error_ok) { + buf = malloc(24 + lsm_str_len(value)); + int len = lsm_str_len(value); + sprintf(buf, "attachment; filename=\"%*s\"", len, lsm_str_ptr(value)); + } else { + buf = malloc(11); + strcpy(buf, "attachment"); + } + + http_res_add_header(&ctx->res, http_header_content_disposition, buf, true); } -lnm_http_step_err lander_get_entry(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_get_entry(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - lnm_http_loop_gctx *gctx = ctx->g; + http_loop_gctx *gctx = ctx->g; lander_gctx *c_gctx = gctx->c; - const char *key_s; - size_t key_len = lnm_http_req_route_segment(&key_s, &ctx->req, "key"); + const char *key_s = &ctx->req.path[ctx->req.regex_groups[1].rm_so]; + int key_len = ctx->req.regex_groups[1].rm_eo - ctx->req.regex_groups[1].rm_so; lsm_str *key; lsm_str_init_copy_n(&key, (char *)key_s, key_len); - lsm_error lsm_res = lsm_store_open_read(&c_ctx->entry, c_gctx->store, key); - lsm_str_free(key); - switch (lsm_res) { + switch (lsm_store_open_read(&c_ctx->entry, c_gctx->store, key)) { case lsm_error_ok: break; case lsm_error_not_found: - ctx->res.status = lnm_http_status_not_found; - return lnm_http_step_err_res; + ctx->res.status = http_not_found; + conn->state = event_loop_conn_state_res; + return true; default: - ctx->res.status = lnm_http_status_internal_server_error; - return lnm_http_step_err_res; + ctx->res.status = http_internal_server_error; + conn->state = event_loop_conn_state_res; + return true; } lander_entry_type t; lsm_entry_attr_get_uint8_t((uint8_t *)&t, c_ctx->entry, lander_attr_type_entry_type); - lnm_http_step_err res; - switch (t) { case lander_entry_type_redirect: - res = lander_get_redirect(conn); + lander_get_redirect(conn); break; case lander_entry_type_paste: - res = lander_get_paste(conn); + lander_get_paste(conn); break; case lander_entry_type_file: - res = lander_get_file(conn); + lander_get_file(conn); break; - default: - ctx->res.status = lnm_http_status_internal_server_error; - res = lnm_http_step_err_res; } - return res; + return true; +} + +bool lander_stream_body_to_client(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + lander_ctx *c_ctx = ctx->c; + + if ((c_ctx->entry == NULL) || + (ctx->res.body.expected_len == ctx->res.body.len)) { + return true; + } + + uint64_t to_write = MIN(EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size, + ctx->res.body.expected_len - ctx->res.body.len); + + uint64_t read = 0; + lsm_entry_data_read(&read, (char *)&conn->wbuf[conn->wbuf_size], c_ctx->entry, + to_write); + + ctx->res.body.len += read; + conn->wbuf_size += read; + + return false; } diff --git a/src/lander/lander_post.c b/src/lander/lander_post.c index 7befe03..9711d03 100644 --- a/src/lander/lander_post.c +++ b/src/lander/lander_post.c @@ -1,10 +1,8 @@ -#include - -#include "lnm/http/req.h" -#include "lnm/loop.h" -#include "lsm/store.h" - +#include "http/res.h" +#include "http/types.h" #include "lander.h" +#include "log.h" +#include "lsm/store.h" static void randomize_key(char *key, int len) { size_t charset_len = strlen(lander_key_charset); @@ -21,36 +19,39 @@ static void randomize_key(char *key, int len) { * * @return true on success, false otherwise */ -bool lander_insert_entry(lnm_http_loop_ctx *ctx, bool secure) { - lnm_http_loop_gctx *gctx = ctx->g; +bool lander_insert_entry(http_loop_ctx *ctx) { + http_loop_gctx *gctx = ctx->g; lander_gctx *c_gctx = gctx->c; lander_ctx *c_ctx = ctx->c; - const char *key_s; - size_t key_len = lnm_http_req_route_segment(&key_s, &ctx->req, "key"); - lsm_str *key; + int key_len; - if (key_len == 0) { + if (ctx->req.regex_groups[2].rm_eo == ctx->req.regex_groups[2].rm_so) { // Generate a random key to insert + bool secure = + (ctx->req.regex_groups[1].rm_eo - ctx->req.regex_groups[1].rm_so) == 1; key_len = secure ? 16 : 4; - key_s = malloc((key_len + 1) * sizeof(char)); + char *key_s = malloc((key_len + 1) * sizeof(char)); - randomize_key((char *)key_s, key_len); - lsm_str_init(&key, (char *)key_s); + randomize_key(key_s, key_len); + lsm_str_init(&key, key_s); } else { + const char *key_s = &ctx->req.path[ctx->req.regex_groups[2].rm_so]; + key_len = ctx->req.regex_groups[2].rm_eo - ctx->req.regex_groups[2].rm_so; + lsm_str_init_copy_n(&key, key_s, key_len); } // TODO free key on error switch (lsm_store_insert(&c_ctx->entry, c_gctx->store, key)) { case lsm_error_already_present: - ctx->res.status = lnm_http_status_conflict; + ctx->res.status = http_conflict; return false; case lsm_error_ok: break; default: - ctx->res.status = lnm_http_status_internal_server_error; + ctx->res.status = http_internal_server_error; return false; } @@ -60,75 +61,61 @@ bool lander_insert_entry(lnm_http_loop_ctx *ctx, bool secure) { buf[0] = '/'; buf[key_len + 1] = '\0'; - lnm_http_res_add_header(&ctx->res, lnm_http_header_location, buf, true); - ctx->res.status = lnm_http_status_created; + http_res_add_header(&ctx->res, http_header_location, buf, true); + ctx->res.status = http_created; return true; } -static lnm_http_step_err __lander_post_redirect(lnm_http_conn *conn, - bool secure) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_post_redirect(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - if (!lander_insert_entry(ctx, secure)) { - return lnm_http_step_err_res; + if (!lander_insert_entry(ctx)) { + conn->state = event_loop_conn_state_res; + return true; } lsm_entry_attr_insert_uint8_t(c_ctx->entry, lander_attr_type_entry_type, lander_entry_type_redirect); - return lnm_http_step_err_done; + return true; } -lnm_http_step_err lander_post_redirect(lnm_http_conn *conn) { - return __lander_post_redirect(conn, false); -} - -lnm_http_step_err lander_post_redirect_secure(lnm_http_conn *conn) { - return __lander_post_redirect(conn, true); -} - -lnm_http_step_err lander_post_redirect_body_to_attr(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_post_redirect_body_to_attr(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; lsm_str *attr_value; lsm_str_init_copy_n(&attr_value, ctx->req.body.buf, ctx->req.body.len); lsm_entry_attr_insert(c_ctx->entry, lander_attr_type_url, attr_value); - return lnm_http_step_err_done; + return true; } -static lnm_http_step_err __lander_post_paste(lnm_http_conn *conn, bool secure) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_post_paste(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - if (!lander_insert_entry(ctx, secure)) { - return lnm_http_step_err_res; + if (!lander_insert_entry(ctx)) { + conn->state = event_loop_conn_state_res; + return true; } lsm_entry_attr_insert_uint8_t(c_ctx->entry, lander_attr_type_entry_type, lander_entry_type_paste); lander_header_to_attr(ctx, "X-Lander-Filename", lander_attr_type_file_name); - return lnm_http_step_err_done; + return true; } -lnm_http_step_err lander_post_paste(lnm_http_conn *conn) { - return __lander_post_paste(conn, false); -} - -lnm_http_step_err lander_post_paste_secure(lnm_http_conn *conn) { - return __lander_post_paste(conn, true); -} - -static lnm_http_step_err __lander_post_file(lnm_http_conn *conn, bool secure) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_post_file(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; - if (!lander_insert_entry(ctx, secure)) { - return lnm_http_step_err_res; + if (!lander_insert_entry(ctx)) { + conn->state = event_loop_conn_state_res; + return true; } lsm_entry_attr_insert_uint8_t(c_ctx->entry, lander_attr_type_entry_type, @@ -137,13 +124,5 @@ static lnm_http_step_err __lander_post_file(lnm_http_conn *conn, bool secure) { lander_attr_type_content_type); lander_header_to_attr(ctx, "X-Lander-Filename", lander_attr_type_file_name); - return lnm_http_step_err_done; -} - -lnm_http_step_err lander_post_file(lnm_http_conn *conn) { - return __lander_post_file(conn, false); -} - -lnm_http_step_err lander_post_file_secure(lnm_http_conn *conn) { - return __lander_post_file(conn, true); + return true; } diff --git a/src/lander/lander_steps.c b/src/lander/lander_steps.c index 00c86ba..7804df5 100644 --- a/src/lander/lander_steps.c +++ b/src/lander/lander_steps.c @@ -1,27 +1,22 @@ #include -#include "lnm/http/loop.h" -#include "lnm/loop.h" - #include "lander.h" -lnm_http_step_err lander_stream_body_to_entry(lnm_http_conn *conn) { - lnm_http_loop_ctx *ctx = conn->ctx; +bool lander_stream_body_to_entry(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; lander_ctx *c_ctx = ctx->c; uint64_t to_append = - LNM_MIN(conn->r.size - conn->r.read, - ctx->req.body.expected_len - lsm_entry_data_len(c_ctx->entry)); + MIN(conn->rbuf_size - conn->rbuf_read, + ctx->req.body.expected_len - lsm_entry_data_len(c_ctx->entry)); lsm_str *data; - lsm_str_init_copy_n(&data, (char *)&conn->r.buf[conn->r.read], to_append); + lsm_str_init_copy_n(&data, (char *)&conn->rbuf[conn->rbuf_read], to_append); lsm_entry_data_append(c_ctx->entry, data); - conn->r.read += to_append; + conn->rbuf_read += to_append; lsm_str_free(data); - return lsm_entry_data_len(c_ctx->entry) == ctx->req.body.expected_len - ? lnm_http_step_err_done - : lnm_http_step_err_io_needed; + return lsm_entry_data_len(c_ctx->entry) == ctx->req.body.expected_len; } diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..1ca0bc7 --- /dev/null +++ b/src/log.c @@ -0,0 +1,31 @@ +#include + +#include "log.h" + +const char *log_level_names[] = {"DEBUG", "INFO ", "WARN ", "ERROR", + "CRITICAL"}; + +log_level _log_level = log_level_debug; + +void _lander_log(log_level level, FILE *f, const char *fmt, ...) { + if (level < _log_level) { + return; + } + + // Log to stdout by default + f = (f == NULL) ? stdout : f; + + char date_str[32]; + + time_t now = time(NULL); + strftime(date_str, sizeof(date_str) - 1, "%Y-%m-%d %H:%M:%S", + localtime(&now)); + fprintf(f, "[%s][%s] ", date_str, log_level_names[level]); + + va_list ap; + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + + fprintf(f, "\n"); +} diff --git a/src/main.c b/src/main.c index f0f8e65..2f52fc1 100644 --- a/src/main.c +++ b/src/main.c @@ -2,93 +2,13 @@ #include #include -#include "lnm/http/loop.h" -#include "lnm/log.h" - #include "lander.h" - -const char *lander_server = "lander/" LANDER_VERSION; - -lnm_http_loop *loop_init(lander_gctx *gctx, const char *api_key) { - lnm_http_loop *hl; - lnm_http_loop_init(&hl, gctx, lander_ctx_init, - (lnm_http_ctx_reset_fn)lander_ctx_reset, - (lnm_http_ctx_free_fn)lander_ctx_free); - lnm_http_loop_set_api_key(hl, api_key); - lnm_http_loop_set_server(hl, lander_server); - - lnm_http_router *router; - lnm_http_router_init(&router); - - lnm_http_route *route; - lnm_http_router_add(&route, router, lnm_http_method_get, "/"); - lnm_http_route_step_append(route, lander_get_index, false); - - lnm_http_router_add(&route, router, lnm_http_method_get, "/:key"); - lnm_http_route_step_append(route, lander_get_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_delete, "/:key"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_remove_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/s/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_redirect, false); - lnm_http_route_step_append(route, lnm_http_loop_step_body_to_buf, false); - lnm_http_route_step_append(route, lander_post_redirect_body_to_attr, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/sl/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_redirect_secure, false); - lnm_http_route_step_append(route, lnm_http_loop_step_body_to_buf, false); - lnm_http_route_step_append(route, lander_post_redirect_body_to_attr, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/s/:key"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_redirect, false); - lnm_http_route_step_append(route, lnm_http_loop_step_body_to_buf, false); - lnm_http_route_step_append(route, lander_post_redirect_body_to_attr, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/p/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_paste, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/pl/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_paste_secure, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/p/:key"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_paste, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/f/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_file, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/fl/"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_file_secure, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_router_add(&route, router, lnm_http_method_post, "/f/:key"); - lnm_http_route_step_append(route, lnm_http_loop_step_auth, false); - lnm_http_route_step_append(route, lander_post_file, false); - lnm_http_route_step_append(route, lander_stream_body_to_entry, false); - - lnm_http_loop_router_set(hl, router); - - return hl; -} +#include "log.h" #define ENV(var, env_var) \ const char *var = getenv(env_var); \ if (var == NULL) { \ - lnm_lcritical("main", "Missing environment variable %s", env_var); \ - exit(1); \ + critical(1, "Missing environment variable %s", env_var); \ } #define ENV_OPT(var, env_var, default) \ @@ -98,11 +18,9 @@ lnm_http_loop *loop_init(lander_gctx *gctx, const char *api_key) { } int main() { + setvbuf(stdout, NULL, _IONBF, 0); srand(time(NULL)); - lnm_log_init_global(); - lnm_log_register_stdout(lnm_log_level_info); - ENV(api_key, "LANDER_API_KEY"); ENV_OPT(port_str, "LANDER_PORT", "18080"); ENV_OPT(data_dir_s, "LANDER_DATA_DIR", "."); @@ -110,8 +28,7 @@ int main() { int port = atoi(port_str); if (port <= 0 || port >= 1 << 16) { - lnm_lcritical("main", "Invalid TCP port %s", port_str); - exit(1); + critical(1, "Invalid TCP port %s", port_str); } lander_gctx *c_gctx = lander_gctx_init(); @@ -120,14 +37,19 @@ int main() { lsm_str *data_dir; lsm_str_init_copy(&data_dir, (char *)data_dir_s); - lnm_linfo("main", "Initializing store from path '%s'", data_dir_s); + info("Initializing store from path '%s'", data_dir_s); if (lsm_store_load(&c_gctx->store, data_dir) != lsm_error_ok) { - lnm_lcritical("main", "%s", "Failed to load existing store."); + critical(2, "Failed to load existing store."); } - lnm_linfo("main", "Store loaded containing %lu entries", - lsm_store_size(c_gctx->store)); - lnm_http_loop *hl = loop_init(c_gctx, api_key); - lnm_http_loop_run(hl, port, 1, 0); + info("Store loaded containing %lu entries", lsm_store_size(c_gctx->store)); + + http_loop *hl = http_loop_init( + lander_routes, sizeof(lander_routes) / sizeof(lander_routes[0]), c_gctx, + lander_ctx_init, (void (*)(void *))lander_ctx_reset, + (void (*)(void *))lander_ctx_free); + http_loop_set_api_key(hl, api_key); + + http_loop_run(hl, port); } diff --git a/thirdparty/include/picohttpparser.h b/thirdparty/include/picohttpparser.h new file mode 100644 index 0000000..07537cf --- /dev/null +++ b/thirdparty/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/thirdparty/src/picohttpparser.c b/thirdparty/src/picohttpparser.c new file mode 100644 index 0000000..5e5783a --- /dev/null +++ b/thirdparty/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