vessel/web/form.c
Arija A. 8aa50503f7
refact: Switch to char* fully
Signed-off-by: Arija A. <ari@ari.lt>
2025-06-22 18:52:23 +03:00

538 lines
14 KiB
C

#include "include/form.h"
#include <vessel/switch.h>
#include <vessel/hashing.h>
#include <stdio.h>
static vw_FormField *vw_FormField_new(void) {
vw_FormField *field = VS_MALLOC(sizeof(*field));
if (!field) {
return NULL;
}
if (!vs_HMap_init(&field->headers)) {
VS_FREE(field);
return NULL;
}
field->type = vw_FormFieldType_plain;
field->value = NULL;
return field;
}
static void vw_FormField_destroy(vw_FormField *field) {
if (!field) {
return;
}
vs_HMap_destroy_free(&field->headers);
if (field->type == vw_FormFieldType_file) {
vw_FormFile_destroy(field->value);
}
if (field->value) {
VS_FREE(field->value);
}
VS_FREE(field);
}
static inline bool vw_read_url_encoded_form(vw_HTTPRequest *req, vs_HMap *hmap) {
char value[VW_FORM_VALUE_MAX + 1];
const size_t read_body = vw_HTTPBody_read(&req->body, value, VW_FORM_VALUE_MAX);
if (read_body == 0) { /* Technically an empty form is still a form, just, uh, empty */
return true;
}
/* else */
if (read_body == VS_STREAM_ERROR) {
return false;
}
vs_HMap out = { 0 };
if (!vs_HMap_init(&out)) {
return false;
}
value[read_body] = '\0';
if (!vw_HTTP_parse_query(value, &out)) {
vs_HMap_destroy_free(&out);
return false;
}
for (size_t idx = 0; idx < out.size; ++idx) {
vw_FormField *field = vw_FormField_new();
if (!field) {
return false;
}
const vs_HMapEntry *entry = out.occupied_ents[idx];
field->value = vs_dupnstr(entry->value, entry->key_length);
field->type = vw_FormFieldType_plain;
if (!vs_HMapID_is_valid(vs_HMap_insert(hmap, entry->key, field))) {
vw_FormField_destroy(field);
vs_HMap_destroy_free(&out);
return false;
}
}
return vs_HMap_destroy_free(&out);
}
static bool vw_multipart_skip_data(vw_HTTPRequest *req, uint8_t *http_buf, const char *boundary) {
vs_Stream_readbf(req->stream, http_buf, VW_HTTP_BUFFER_MAX_SIZE, NULL, false, "--%s", boundary);
vs_Stream_read(req->stream, http_buf, 2); /* Skip either -- or \r\n */
if (*http_buf == '-') {
vs_Stream_read(req->stream, http_buf, 2); /* Skip \r\n of the last sequence */
return true;
}
return false;
}
#define VW_MULTIPART_SKIP_DATA_LOOP(f, disposition, ...) \
vw_FormField_destroy((f)); \
vs_HMap_clear_free(&(disposition)); \
if (vw_multipart_skip_data(__VA_ARGS__)) { \
break; \
} \
continue
static inline bool
vw_read_mutipart_form(vw_HTTPRequest *req, vs_HMap *hmap, vs_HMapEntry *content_type) {
vw_FormField *field = NULL;
size_t size = 0;
vs_HMap content_type_multipart = { 0 };
uint8_t end[2] = { 0 };
uint8_t http_buf[VW_HTTP_BUFFER_MAX_SIZE];
uint8_t value[VW_FORM_VALUE_MAX];
if (!vs_HMap_init(&content_type_multipart)) {
return false;
}
if (!vw_HTTP_parse_multipart_header(content_type->value, &content_type_multipart)) {
vs_HMap_destroy_free(&content_type_multipart);
return false;
}
const vs_HMapEntry *boundary_entry = vs_HMap_find(&content_type_multipart, "boundary");
if (!boundary_entry) {
vs_HMap_destroy_free(&content_type_multipart);
return false;
}
char *boundary = vs_dupstr(boundary_entry->value);
if (!boundary) {
vs_HMap_destroy_free(&content_type_multipart);
return false;
}
vs_HMap_destroy_free(&content_type_multipart);
const size_t boundary_size = strlen(boundary);
if (boundary_size < 2) {
goto error_wbound;
}
bool found = false;
/* Init the disposition vs_HMap */
vs_HMap disposition = { 0 };
if (!vs_HMap_init(&disposition)) {
goto error_wbound;
}
/* Read the delimiter */
if (vw_HTTPBody_readbf(&req->body,
http_buf,
VW_HTTP_BUFFER_MAX_SIZE,
&found,
true,
"--%s" VW_HTTP_CRLF,
boundary) != 0 ||
!found) {
goto error_wdis;
}
while (*end != '-') {
vs_HMap_clear_free(&disposition);
/*
* 40 is the bare minimum header length required for this form type.
*
* >>> len("content-disposition:form-data;name=a\r\n\r\n")
* 40
*/
size = vw_HTTPBody_readb(&req->body,
http_buf,
VW_HTTP_BUFFER_MAX_SIZE,
VW_HTTP_2_CRLF,
VW_HTTP_2_CRLF_LENGTH,
&found,
true);
if (vs_Stream_ignore_err(size) < 40 || !found) {
goto error_wdis;
}
/* Headers */
field = vw_FormField_new();
if (!field) {
goto error_wdis;
}
if (!vw_HTTP_parse_headers(http_buf, size, &field->headers, NULL)) {
goto error_wfield;
}
/* Disposition */
vs_HMapEntry *disposition_b = vs_HMap_find(&field->headers, "content-disposition");
if (!disposition_b) {
/* We can just skip the ones with no disposition header. */
VW_MULTIPART_SKIP_DATA_LOOP(field, disposition, req, http_buf, boundary);
}
if (!vw_HTTP_parse_multipart_header(disposition_b->value, &disposition)) {
/* Bad header */
VW_MULTIPART_SKIP_DATA_LOOP(field, disposition, req, http_buf, boundary);
}
vs_HMapEntry *form_data_disposition_b = vs_HMap_find_val(&disposition, NULL, NULL);
if (!form_data_disposition_b || strcmp(form_data_disposition_b->key, "form-data") != 0) {
/* No `form-data` */
VW_MULTIPART_SKIP_DATA_LOOP(field, disposition, req, http_buf, boundary);
}
vs_HMapEntry *name = vs_HMap_find(&disposition, "name");
if (!name || !name->value) {
/* No key */
VW_MULTIPART_SKIP_DATA_LOOP(field, disposition, req, http_buf, boundary);
}
vs_HMapEntry *filename =
vs_HMap_find(&disposition, "filename*"); /* `filename*` is prioritised
over `filename` (standard
practice) */
if (!filename) {
filename = vs_HMap_find(&disposition, "filename");
}
/* Parsing */
bool do_insert = true;
if (filename) {
do_insert = *(uint8_t *)filename->value != '\0';
vw_FormFile *file = NULL;
if (do_insert) {
file = VS_MALLOC(sizeof(*file));
if (!file) {
goto error_wfield;
}
if (!vw_FormFile_init(file, filename->value)) {
VS_FREE(file);
goto error_wfield;
}
field->type = vw_FormFieldType_file;
field->value = file;
if (!vw_FormFile_open(file)) {
vw_FormFile_destroy(file);
VS_FREE(file);
goto error_wfield;
}
}
for (size_t chunk = 0; chunk < VW_FORM_FILE_VALUE_CHUNKS; ++chunk) {
size = vw_HTTPBody_readbf(
&req->body, value, VW_FORM_VALUE_MAX, &found, false, "--%s", boundary);
if (vs_Stream_ignore_err(size) <= 0) {
break;
}
if (found) {
if (vw_HTTPBody_read(&req->body, end, 2) != 2) {
if (do_insert) {
vw_FormFile_destroy(file);
VS_FREE(file);
}
goto error_wfield;
}
if (size >= VW_HTTP_CRLF_LENGTH) {
size -= VW_HTTP_CRLF_LENGTH;
}
}
if (do_insert && vs_File_write(&file->file, value, size) <= 0) {
break;
}
if (found || *end == '-') {
break;
}
}
if (do_insert) {
vw_FormFile_close(file);
}
if (!found) {
if (do_insert) {
vw_FormFile_destroy(file);
VS_FREE(file);
}
goto error_wfield;
}
} else {
do_insert = true;
size = vw_HTTPBody_readbf(
&req->body, value, VW_FORM_VALUE_MAX, &found, true, "--%s", boundary);
if (size == VS_STREAM_ERROR || !found) {
goto error_wfield;
}
if (vw_HTTPBody_read(&req->body, end, 2) != 2) {
goto error_wfield;
}
if (size == 0) {
value[0] = '\0';
}
field->type = vw_FormFieldType_plain;
/* Skips CRLF after content */
field->value = vs_dupnstr((const char *)value, (size_t)(size - VW_HTTP_CRLF_LENGTH));
}
if (do_insert) {
if (!vs_HMapID_is_valid(vs_HMap_insert(hmap, name->value, field))) {
goto error_wfield;
}
} else {
vw_FormField_destroy(field);
}
}
VS_FREE(boundary);
vs_HMap_destroy_free(&disposition);
return true;
error_wfield:
vw_FormField_destroy(field);
error_wdis:
vs_HMap_destroy_free(&disposition);
error_wbound:
VS_FREE(boundary);
return false;
}
static inline bool vw_read_plain_form(vw_HTTPRequest *req, vs_HMap *hmap) {
char value[VW_FORM_VALUE_MAX + 1];
const size_t body_read = vw_HTTPBody_read(&req->body, value, VW_FORM_VALUE_MAX);
if (body_read == VS_STREAM_ERROR) {
return false;
}
vw_FormField *field = vw_FormField_new();
if (!field) {
return false;
}
value[body_read] = '\0';
field->value = vs_dupnstr(value, body_read);
if (!field->value) {
vw_FormField_destroy(field);
return false;
}
field->type = vw_FormFieldType_plain;
/* Add a content-length header. */
char rd_a[32];
if (snprintf(rd_a, 32, "%zu", body_read) < 0) {
vw_FormField_destroy(field);
return false;
}
vs_HMap_insert(&field->headers, "content-length", vs_dupstr(rd_a));
return vs_HMapID_is_valid(vs_HMap_insert(hmap, VW_FORM_PLAIN_KEY, field));
}
bool vw_Form_read(vw_HTTPRequest *req, vs_HMap *hmap) {
if (!req || !hmap) {
return false;
}
vs_HMapEntry *entry = vs_HMap_find(&req->headers, "content-type");
if (!entry || !entry->value) {
return false;
}
if (vs_startstr(entry->value, "multipart/form-data", false)) {
return vw_read_mutipart_form(req, hmap, entry);
}
switch (vs_switch_hash(entry->value)) {
case VW_FORM_URL_ENCODED_HASH:
return vw_read_url_encoded_form(req, hmap);
case VW_FORM_PLAIN_HASH:
return vw_read_plain_form(req, hmap);
default:
return false;
}
return true;
}
bool vw_Form_print(vs_HMap *hmap) {
if (!hmap) {
return false;
}
printf(" * Form: %p\n", (void *)hmap);
for (size_t idx = 0; idx < hmap->size; ++idx) {
const char *key = hmap->occupied_ents[idx]->key;
const vw_FormField *field = (const vw_FormField *)hmap->occupied_ents[idx]->value;
switch (field->type) {
case vw_FormFieldType_file: {
const vw_FormFile *file = (const vw_FormFile *)field->value;
printf(" %s => <... file %s ...>\n", key, file->filename);
} break;
case vw_FormFieldType_plain:
printf(" %s => %s\n", key, (const char *)field->value);
break;
default:
printf(" %s => <unknown: %p>\n", key, field->value);
break;
}
}
puts(" * End Form");
return true;
}
bool vw_Form_destroy(vs_HMap *hmap) {
if (!hmap) {
return false;
}
for (size_t idx = 0; idx < hmap->size; ++idx) {
vw_FormField_destroy(hmap->occupied_ents[idx]->value);
}
return vs_HMap_destroy(hmap);
}
/* TODO: Cache files/avoid FS operations? */
bool vw_FormFile_init(vw_FormFile *file, const char *filename) {
if (!file || !filename) {
return false;
}
if (!vs_File_init(&file->file, NULL)) {
return false;
}
file->filename = vs_dupstr(filename);
return true;
}
bool vw_FormFile_open(vw_FormFile *file) {
if (!file) {
return false;
}
if (!vs_File_isopen(&file->file) && !vs_File_create_tmp(&file->file)) {
return false;
}
return true;
}
bool vw_FormFile_close(vw_FormFile *file) {
if (!file) {
return false;
}
vs_File_close(&file->file);
return true;
}
size_t vw_FormFile_read(vw_FormFile *file, void *buf, size_t count) {
if (!file || !buf) {
return VS_STREAM_ERROR;
}
if (!vw_FormFile_open(file)) {
return VS_STREAM_ERROR;
}
return vs_File_read(&file->file, buf, count);
}
bool vw_FormFile_destroy(vw_FormFile *file) {
if (!file) {
return false;
}
vs_File_destroy(&file->file);
VS_FREE(file->filename);
return true;
}