538 lines
14 KiB
C
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;
|
|
}
|