vessel/web/http.c
Arija A. 4f0bf80e5f
refact: Remove mem.h
Signed-off-by: Arija A. <ari@ari.lt>
2025-06-21 23:43:31 +03:00

881 lines
26 KiB
C

#include "include/conf.h"
#include <vessel/def.h>
#include <vessel/clrs.h>
#include <vessel/switch.h>
#include "include/http.h"
#include <ctype.h>
#include <stdio.h>
/* TODO: Make a function to read an HTTP chunk. */
const char *vw_HTTP_code_to_message(const uint16_t code) {
switch (code) {
/* 1xx */
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing";
case 103:
return "Early Hints";
/* 2xx */
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status";
case 208:
return "Already Reported";
case 226:
return "IM Used";
/* 3xx */
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 306:
return "Switch Proxy";
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
/* 4xx */
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot";
case 419:
return "Page Expired";
case 420:
return "Enhance Your Calm";
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Content";
case 423:
return "Locked";
case 424:
return "Failed Dependency";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 444:
return "No Response";
case 451:
return "Unavailable For Legal Reasons";
/* 5xx */
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage";
case 508:
return "Loop Detected";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
/* 6xx */
/* HTTP/666: Satan's Favourite (custom Vessel server HTTP code)
*
* This code is used mainly as a way to indicate an expected error,
* blockage, or abortion. To you this may mean one thing - content
* may be incomplete and premature. To the user this may indicate
* that abuse was detected or they were blocked in some other way,
* or that some internal state got messed up and detected therefore
* an abortion of the request on the TCP level happened.
*
* "Satan's Favourite" refers to bad luck on the user's side, or if
* they were being evil - they are the Satan per se. The status
* number and message are mainly novelty, honestly.
*
* TL;DR Something (unsure what) got messed up, we intentionally
* messed it up you, or detected you trying to do so. Therefore,
* goodbye.
*/
case 666:
return "Satan's Favourite";
/* Else */
default:
return "Unknown Status";
}
}
const char *vw_HTTP_code_to_description(uint16_t code) {
switch (code) {
/* 1xx Informational */
case 100:
return "The server has received your request and you can continue sending the rest of "
"it";
case 101:
return "The server is switching to a different communication protocol as requested";
case 102:
return "The server is still processing your request; please wait";
case 103:
return "Here are some preliminary hints while the server prepares the full response";
/* 2xx Success */
case 200:
return "Your request was successful and the server returned the requested information";
case 201:
return "Your request was successful and a new resource was created";
case 202:
return "Your request has been accepted and is being processed";
case 203:
return "The information returned comes from a third party, not the original server";
case 204:
return "Your request was successful but there is no content to show";
case 205:
return "Your request was successful; please reset the view or form";
case 206:
return "Only part of the requested content is being sent back";
case 207:
return "The server is returning multiple pieces of information in one response";
case 208:
return "Some information has already been reported earlier, so it's not repeated";
case 226:
return "The server has fulfilled your request with some additional processing";
/* 3xx Redirection */
case 300:
return "There are multiple options for the resource you requested; please choose one";
case 301:
return "The page you requested has moved permanently to a new address";
case 302:
return "The page you requested has moved temporarily to a different address";
case 303:
return "Please look at a different page to find the resource you want";
case 304:
return "The page has not changed since your last visit; you can use your cached copy";
case 305:
return "You need to use a proxy server to access the requested page";
case 306:
return "Please use a different proxy server to access the requested resource";
case 307:
return "Please try your request again at a different address temporarily";
case 308:
return "Please use the new permanent address for future requests";
/* 4xx Client Errors */
case 400:
return "Your request was invalid or cannot be processed by the server";
case 401:
return "You need to log in to access this resource";
case 402:
return "Payment is required to access this resource";
case 403:
return "You don't have permission to access this page";
case 404:
return "The requested page or resource could not be found on the server";
case 405:
return "The method you used is not allowed for this page";
case 406:
return "The server cannot provide the content in a format you requested";
case 407:
return "You need to authenticate with the proxy server before proceeding";
case 408:
return "Your request took too long to complete; please try again";
case 409:
return "There is a conflict with the current state of the resource";
case 410:
return "The requested resource has been permanently removed";
case 411:
return "The server requires a content length to process your request";
case 412:
return "Some conditions in your request were not met";
case 413:
return "The data you sent is too large for the server to process";
case 414:
return "The URL you entered is too long for the server to handle";
case 415:
return "The server does not support the format of the data you sent";
case 416:
return "The range you requested is not available";
case 417:
return "The server cannot meet the requirements you specified";
case 418:
return "I'm a teapot - the server is overloaded";
case 419:
return "The page has expired; please refresh and try again";
case 420:
return "You are sending requests too quickly; please slow down";
case 421:
return "Your request was sent to the wrong server; please try again";
case 422:
return "Your request was well-formed but contains semantic errors";
case 423:
return "The resource you are trying to access is locked";
case 424:
return "Your request failed because a previous request it depended on failed";
case 425:
return "The server is not ready to process your request yet";
case 426:
return "You need to upgrade your protocol to continue";
case 428:
return "The server requires certain conditions to be met before processing your "
"request";
case 429:
return "You have sent too many requests in a short time; please wait and try again";
case 431:
return "Your request headers are too large for the server to process";
case 444:
return "The server closed the connection without sending a response";
case 451:
return "Access to this resource is denied for legal reasons";
/* 5xx Server Errors */
case 500:
return "The server encountered an error and could not complete your request";
case 501:
return "The server does not support the functionality required to fulfill your "
"request";
case 502:
return "The server received an invalid response from an upstream server";
case 503:
return "The server is currently unavailable, usually due to maintenance or overload";
case 504:
return "The server did not receive a timely response from another server";
case 505:
return "The server does not support the HTTP protocol version used in your request";
case 506:
return "The server has a configuration error preventing your request from being "
"fulfilled";
case 507:
return "The server is unable to store the necessary data to complete your request";
case 508:
return "The server detected an infinite loop while processing your request";
case 510:
return "Further extensions to your request are required for the server to fulfill it";
case 511:
return "You need to authenticate to gain network access";
/* 6xx Custom */
case 666:
return "Your request was blocked or aborted due to suspicious activity";
/* Default */
default:
return "An unknown error occurred; the server returned an unrecognized status code";
}
}
const char *vw_HTTP_code_to_colour(const uint16_t code) {
if (code >= 600) {
return VS_CLR_BOLD VS_CLR_UNDERLINE VS_CLR_RED;
}
if (code >= 500) {
return VS_CLR_BOLD VS_CLR_MAGENTA;
}
if (code >= 400) {
return VS_CLR_BOLD VS_CLR_RED;
}
if (code >= 300) {
return VS_CLR_BOLD VS_CLR_YELLOW;
}
if (code >= 200) {
return VS_CLR_BOLD VS_CLR_GREEN;
}
if (code >= 100) {
return VS_CLR_BOLD VS_CLR_CYAN;
}
return VS_CLR_BOLD;
}
ssize_t vw_HTTP_url_decode(char *str) {
if (!str) {
return -1;
}
char a = 0;
char b = 0;
char c = 0;
char *read_ptr = str;
char *write_ptr = str;
/* NOTE: Skips NULL bytes. */
while (*read_ptr) {
if ((*read_ptr == '%') && (read_ptr[1] && read_ptr[2]) &&
isxdigit((unsigned char)read_ptr[1]) && isxdigit((unsigned char)read_ptr[2])) {
a = read_ptr[1];
b = read_ptr[2];
a = (char)((a >= 'a') ? (a - 'a' + 10) : (a >= 'A' ? a - 'A' + 10 : a - '0'));
b = (char)((b >= 'a') ? (b - 'a' + 10) : (b >= 'A' ? b - 'A' + 10 : b - '0'));
c = (char)((16 * a) + b);
read_ptr += 3;
if (c) {
*write_ptr++ = c;
}
} else if (*read_ptr == '+') {
*write_ptr++ = ' ';
read_ptr++;
} else {
c = *read_ptr++;
if (c == '\0') {
continue;
}
*write_ptr++ = c;
}
}
*write_ptr = '\0';
return (ssize_t)(write_ptr - str);
}
char *vw_HTTP_html_encode(const char *str) {
if (!str) {
return NULL;
}
size_t len = 0;
const char *p = str;
while (*p) {
switch (*p) {
case '&':
len += 5;
break; /* &amp; */
case '<':
len += 4;
break; /* &lt; */
case '>':
len += 4;
break; /* &gt; */
case '"':
len += 6;
break; /* &quot; */
case '\'':
len += 6;
break; /* &#39; */
default:
len += 1;
break;
}
++p;
}
char *out = VS_MALLOC(len + 1);
if (!out) {
return NULL;
}
char *q = out;
while (*str) {
switch (*str) {
case '&':
memcpy(q, "&amp;", 5);
q += 5;
break;
case '<':
memcpy(q, "&lt;", 4);
q += 4;
break;
case '>':
memcpy(q, "&gt;", 4);
q += 4;
break;
case '"':
memcpy(q, "&quot;", 6);
q += 6;
break;
case '\'':
memcpy(q, "&#39;", 5);
q += 5;
break;
default:
*q++ = *str;
break;
}
++str;
}
*q = '\0';
return out;
}
ssize_t vw_HTTP_html_decode(char *str) {
if (!str) {
return -1;
}
char *src = str, *dst = str;
while (*src) {
if (*src == '&') {
if (!strncmp(src, "&amp;", 5)) {
*dst++ = '&';
src += 5;
} else if (!strncmp(src, "&lt;", 4)) {
*dst++ = '<';
src += 4;
} else if (!strncmp(src, "&gt;", 4)) {
*dst++ = '>';
src += 4;
} else if (!strncmp(src, "&quot;", 6)) {
*dst++ = '"';
src += 6;
} else if (!strncmp(src, "&#39;", 5)) {
*dst++ = '\'';
src += 5;
} else {
*dst++ = *src++;
}
} else {
*dst++ = *src++;
}
}
*dst = '\0';
return dst - str;
}
bool vw_HTTP_parse_query(const char *param_start, vs_HMap *out) {
if (!param_start || !out) {
return false;
}
while (*param_start) {
const char *equal_sign = strchr(param_start, '=');
const char *next_param = strchr(param_start, '&');
if (!next_param) {
next_param = param_start + strlen(param_start);
}
const size_t key_len = equal_sign && equal_sign < next_param
? (size_t)(equal_sign - param_start)
: (size_t)(next_param - param_start);
if (key_len == 0) {
/* Skip empty keys */
param_start = next_param + (*next_param ? 1 : 0);
continue;
}
char *key = VS_MALLOC(key_len + 1);
if (!key) {
return false;
}
strncpy(key, param_start, key_len);
key[key_len] = '\0';
if (equal_sign && equal_sign < next_param) {
const size_t value_len = (size_t)(next_param - (equal_sign + 1));
if (value_len == 0) {
goto insert_empty;
}
char *value = VS_MALLOC(value_len + 1);
if (!value) {
VS_FREE(key);
return false;
}
strncpy(value, equal_sign + 1, value_len);
value[value_len] = '\0';
if (vw_HTTP_url_decode(value) == -1) {
VS_FREE(key);
VS_FREE(value);
return false;
}
if (!vs_HMapID_is_valid(vs_HMap_insert(out, key, value))) {
VS_FREE(key);
VS_FREE(value);
return false;
}
} else {
insert_empty:
vs_HMap_insert(out, key, NULL);
}
VS_FREE(key);
param_start = next_param + (*next_param ? 1 : 0);
}
return true;
}
/* TODO: Make this validator more standard */
bool vw_HTTP_validate_request_headers(const void *request, size_t size) {
/* The most minimal request "could" look like `GET / HTTP/2\r\n\r\n` which
* is 16 bytes. */
if (!request || size < VW_HTTP_SMALLEST_REQUEST) {
return false;
}
const uint8_t *ptr = request;
const uint8_t *end = (const uint8_t *)request + size;
while (ptr < end) {
if (*ptr < 0x20 && *ptr != '\r' && *ptr != '\n') {
return false;
}
/* Check for CRLF sequence */
if (*ptr == '\r') {
/* \r should always be led by \n. If it is at the end - it cannot be
* terminated by an LF. */
/* The space is for multi-line headers. */
if (ptr + 1 >= end || (*(ptr + 1) != '\n' && *(ptr + 1) != ' ')) {
return false; /* Missing LF after CR */
}
++ptr; /* Move past LF */
} else if (*ptr == '\n') {
/* The space is for multi-line headers. */
if (ptr + 1 >= end || *(ptr + 1) != ' ') {
/* LF without preceding CR. This case should not be hit if we hit \r
* already. */
return false;
}
}
++ptr;
}
/* LGTM! The last 4 bytes should be verified by the user. */
return true;
}
size_t vw_HTTP_time(const time_t ts, vw_HTTPTime out) {
if (!out) {
return 0;
}
return strftime(out, VW_HTTP_TIME_SIZE, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&ts));
}
static char *vw_HTTP_parse_multipart_header_parse(
char *ptr, char *out, size_t out_size, const char terminator, const char fallback, bool *fell) {
bool in_quotes = false;
size_t idx = 0;
if (fell) {
*fell = false;
}
while (*ptr && idx < out_size) {
switch (*ptr) {
case '"':
in_quotes = !in_quotes;
++ptr;
break;
case '\\':
if (in_quotes) {
++ptr; /* Skip backslash */
if (*ptr) {
out[idx++] = *(ptr++); /* Save the escaped character */
} else {
return NULL; /* Backslash at the end. */
}
} else {
return NULL; /* Invalid escape outside quotes */
}
break;
case ' ':
if (in_quotes) {
out[idx++] = ' ';
}
++ptr;
break;
default:
if (*ptr == terminator) {
if (in_quotes) {
out[idx++] = *(ptr++);
} else {
*ptr = '\0';
ptr++;
goto end;
}
} else if (!in_quotes && fallback && *ptr == fallback) {
if (fell) {
*fell = true;
}
*ptr = '\0';
ptr++;
goto end;
} else {
out[idx++] = *(ptr++);
}
break;
}
}
end:
if (in_quotes) {
return NULL;
}
out[idx] = '\0';
return ptr;
}
bool vw_HTTP_parse_multipart_header(const char *value, vs_HMap *out) {
if (!value || !out) {
return false;
}
char *ptr = vs_dupstr(value);
char *og_ptr = ptr;
if (!ptr) {
return false;
}
char key[128];
char val[VW_HTTP_HEADER_VALUE_MAX_LENGTH - sizeof(key)];
while (*ptr) {
bool fell = false;
/*
* TODO: Make the parser less tolerant to fallthroughs?
*
* For instance
*
* Content-Disposition: form-data; name="file"; filename="test"
*
* should be valid, although,
*
* Content-Disposition: name="file"; form-data; filename="test"
* (fallthrough mid header)
*
* and
*
* Content-Disposition: form-data; meow; name="file";
* filename="test" (double fallthrough)
*
* should not.
*/
if (!(ptr = vw_HTTP_parse_multipart_header_parse(ptr, key, sizeof(key), '=', ';', &fell))) {
goto error;
}
vs_lowstr(key);
if (fell) {
if (!vs_HMapID_is_valid(vs_HMap_insert(out, key, NULL))) {
goto error;
}
} else {
if (!(ptr = vw_HTTP_parse_multipart_header_parse(
ptr, val, sizeof(val), ';', '\0', NULL))) {
goto error;
}
if (!vs_HMapID_is_valid(vs_HMap_insert(out, key, vs_dupstr(val)))) {
goto error;
}
}
}
VS_FREE(og_ptr);
return true;
error:
VS_FREE(og_ptr);
return false;
}
/* TODO: Implement better header parsing & proper multi-line headers */
bool vw_HTTP_parse_headers(const void *data, size_t buf_size, vs_HMap *out, size_t *idx) {
size_t new_idx = 0;
if (!data || buf_size == 0 || !out) {
return false;
}
if (!idx) {
idx = &new_idx;
}
char key[VW_HTTP_HEADER_KEY_MAX_LENGTH];
char value[VW_HTTP_HEADER_VALUE_MAX_LENGTH];
const char *buf = (const char *)data;
while (*idx < buf_size && buf[*idx]) {
size_t jdx = 0;
/* Read header key */
while (buf[*idx] != ':' && buf[*idx] != '\r' && jdx < VW_HTTP_HEADER_KEY_MAX_LENGTH - 1) {
key[jdx++] = buf[(*idx)++];
}
key[jdx] = '\0';
/* Check for colon, and then skip it */
if (buf[*idx] != ':') {
return false;
}
++(*idx);
/* Skip optional spaces after colon */
while (*idx < buf_size && buf[*idx] == ' ') {
++(*idx);
}
/* Read header value */
jdx = 0;
while (*idx < buf_size && buf[*idx] != '\r' && jdx < VW_HTTP_HEADER_VALUE_MAX_LENGTH - 1) {
value[jdx++] = buf[(*idx)++];
}
value[jdx] = '\0';
/* Check for CRLF after header value and skip it. Ignores last one as it
* does not have it (due to readb()). */
if (*idx < buf_size) {
if (strncmp(buf + *idx, VW_HTTP_CRLF, VW_HTTP_CRLF_LENGTH) != 0) {
return false;
}
(*idx) += VW_HTTP_CRLF_LENGTH;
}
/* Convert key to lowercase and insert into headers map if it doesn't
* exist */
vs_lowstr(key);
if (!vs_HMap_find(out, key)) {
/* Common headers which have case-insensitive values. */
switch (vs_switch_hash(key)) {
case VW_HTTP_ACCEPT_HASH:
case VW_HTTP_USER_AGENT_HASH:
case VW_HTTP_CACHE_CONTROL_HASH:
case VW_HTTP_ACCEPT_ENCODING_HASH:
case VW_HTTP_ACCEPT_LANGUAGE_HASH:
case VW_HTTP_ORIGIN_HASH:
case VW_HTTP_HOST_HASH:
case VW_HTTP_ACCESS_CONTROL_REQUEST_METHOD_HASH:
case VW_HTTP_ACCESS_CONTROL_REQUEST_HEADERS_HASH:
case VW_HTTP_RANGE_HASH:
case VW_HTTP_X_REQUESTED_WITH_HASH:
case VW_HTTP_CONNECTION_HASH:
case VW_HTTP_ACCEPT_RANGES_HASH:
case VW_HTTP_CONTENT_ENCODING_HASH:
case VW_HTTP_EXPECT_HASH:
case VW_HTTP_TE_HASH:
vs_lowstr(value);
}
if (!vs_HMapID_is_valid(vs_HMap_insert(out, key, vs_dupstr(value)))) {
return false;
}
}
}
return true;
}