Slon/Http/Server: Reimplement multipart/form-data to JSON parser

This commit is contained in:
Alec Murphy 2025-03-03 21:00:55 -05:00
parent 68e5cf5eb9
commit e1c6ca1b2b
2 changed files with 260 additions and 100 deletions

View file

@ -392,94 +392,242 @@ JsonObject* @slon_http_json_object_from_form_urlencoded_string(SlonHttpSession*
return obj;
}
JsonObject* @slon_http_json_object_from_multipart_form_data(SlonHttpSession* session, U8* multipart_form_data)
U0 @slon_http_json_object_add_nested_value(SlonHttpSession* session, JsonObject* obj, U8* name, U8* value, I64 type)
{
JsonObject* obj = Json.CreateObject();
U8* multipart_form_data_copy = @slon_strnew(session, multipart_form_data);
U8* boundary = StrFind("boundary=", session->header("content-type")) + 9;
// Strip begin double-quotes and ending CRLF, double-quotes
while (boundary[0] == '"')
boundary++;
while (boundary[StrLen(boundary) - 1] == '\"' || boundary[StrLen(boundary) - 1] == ' ' || boundary[StrLen(boundary) - 1] == '\r' || boundary[StrLen(boundary) - 1] == '\n')
boundary[StrLen(boundary) - 1] = NULL;
I64 state = SLON_MULTIPART_PARSER_CONSUME_BOUNDARY;
I64 lines_count = 0;
U8** lines = String.Split(multipart_form_data_copy, '\n', &lines_count);
U8* line;
U8* name;
U8* value = @slon_calloc(session, 262144);
U8* sub_key = NULL;
I64 i = 0;
while (i < lines_count) {
line = lines[i];
// Strip any ending CRLF
while (line[StrLen(line) - 1] == '\r' || line[StrLen(line) - 1] == '\n') {
line[StrLen(line) - 1] = NULL;
}
switch (state) {
case SLON_MULTIPART_PARSER_CONSUME_BOUNDARY:
if (StrFind(boundary, line)) {
state = SLON_MULTIPART_PARSER_CONSUME_CONTENT_DISPOSITION;
}
break;
case SLON_MULTIPART_PARSER_CONSUME_CONTENT_DISPOSITION:
if (StrFind("ontent-", line) && StrFind("isposition:", line) && StrFind("name=", line)) {
name = StrFind("name=", line) + 5;
// Strip begin/end double-quotes
while (name[0] == '"')
name++;
while (name[StrLen(name) - 1] == '\"')
name[StrLen(name) - 1] = NULL;
StrCpy(value, "");
state = SLON_MULTIPART_PARSER_CONSUME_CONTENT;
}
break;
case SLON_MULTIPART_PARSER_CONSUME_CONTENT:
if (StrFind(boundary, line)) {
if (String.EndsWith("[]", name)) {
// We have an array
StrFind("[]", name)[0] = NULL;
if (!obj->@(name)) {
obj->set(name, Json.CreateArray(), JSON_ARRAY);
}
obj->a(name)->append(Json.CreateItem(value, JSON_STRING));
} else if (StrFind("[", name) > 0) {
// We have an object
sub_key = StrFind("[", name) + 1;
while (sub_key[StrLen(sub_key) - 1] == ']') {
sub_key[StrLen(sub_key) - 1] = NULL;
}
StrFind("[", name)[0] = NULL;
if (!obj->@(name)) {
obj->set(name, Json.CreateObject(), JSON_OBJECT);
}
obj->o(name)->set(sub_key, value, JSON_STRING);
} else {
// We have a boring old parameter
obj->set(name, value, JSON_STRING);
}
if (!String.EndsWith("--", line)) {
state = SLON_MULTIPART_PARSER_CONSUME_CONTENT_DISPOSITION;
} else {
state = SLON_MULTIPART_PARSER_DONE;
}
} else {
String.Append(value, line);
}
break;
default:
break;
}
++i;
if (!session || !obj || !name || !value) {
return;
}
@slon_free(session, value);
@slon_free(session, multipart_form_data_copy);
// Handle simple 1-dimensional array case first: name[]=value
if (StrOcc(name, '[') == 1 && String.EndsWith("[]", name)) {
StrFind("[]", name)[0] = NULL;
if (!obj->a(name)) {
obj->set(name, Json.CreateArray(), JSON_ARRAY);
}
obj->a(name)->append(Json.CreateItem(value, type));
return;
}
Bool value_is_array_member = FALSE;
if (String.EndsWith("[]", name)) {
value_is_array_member = TRUE;
StrFind("[]", name)[0] = NULL;
}
I64 i = 0;
I64 depth = StrOcc(name, '[');
I64 keys_length = 0;
U8** keys = String.Split(name, '[', &keys_length);
for (i = 0; i < depth; i++) {
String.Trim(keys[i], ']', TRIM_RIGHT);
if (!obj->o(keys[i])) {
// Create the empty objects as we traverse
obj->set(keys[i], Json.CreateObject(), JSON_OBJECT);
}
obj = obj->o(keys[i]);
}
String.Trim(keys[i], ']', TRIM_RIGHT);
if (value_is_array_member) {
if (!obj->a(keys[i])) {
// Create the empty array if it does not exist
obj->set(keys[i], Json.CreateArray(), JSON_ARRAY);
}
// Append keys[i-1]: { keys[i]: [ ..., value ] }
obj->a(keys[i])->append(Json.CreateItem(value, type));
} else {
// Set keys[i-1]: { keys[i]: value }
obj->set(keys[i], value, type);
}
}
I64 @slon_http_free_and_null(U64 ptr)
{
if (ptr) {
Free(ptr);
}
return NULL;
}
JsonObject* @slon_http_json_object_from_multipart_form_data(SlonHttpSession* session, U8* data)
{
if (!session || !data || !session->header("content-type")) {
return SLON_EMPTY_JSON_OBJECT;
}
U8 scratch_buffer[256];
SlonMultipartParser* mp = @slon_calloc(session, sizeof(SlonMultipartParser));
mp->consumed = FifoU8New(2048, adam_task);
JsonObject* obj = Json.CreateObject();
U8* boundary = StrFind("boundary=", session->header("content-type")) + 9;
String.Trim(boundary);
String.Trim(boundary, '"');
mp->data = data;
mp->state = SLON_MULTIPART_CONSUME_BOUNDARY;
mp->length = Str2I64(session->header("content-length"));
U8* content_disposition_header = "content-disposition: form-data; ";
U8* content_type_header = "content-type: ";
SlonMultipartFile* file = NULL;
U8* name = NULL;
U8* tmp = NULL;
U8* value = NULL;
I64 token;
while (mp->pos < mp->length) {
token = mp->data[mp->pos];
switch (mp->state) {
case SLON_MULTIPART_CONSUME_DATA:
StrPrint(scratch_buffer, "\r\n--%s", boundary);
if (!MemCmp(mp->data + mp->pos, scratch_buffer, StrLen(scratch_buffer))) {
if (file) {
file->size = mp->data + mp->pos - file->buffer;
if (StrFind("[", name)) {
@slon_http_json_object_add_nested_value(session, obj, name, file, JSON_NUMBER);
} else {
obj->set(name, file, JSON_NUMBER);
}
} else {
mp->data[mp->pos] = NULL;
if (StrFind("[", name)) {
@slon_http_json_object_add_nested_value(session, obj, name, value, JSON_STRING);
} else {
obj->set(name, value, JSON_STRING);
}
}
file = NULL;
name = @slon_http_free_and_null(name);
tmp = @slon_http_free_and_null(tmp);
++mp->pos;
mp->state = SLON_MULTIPART_CONSUME_BOUNDARY;
} else {
++mp->pos;
}
break;
case SLON_MULTIPART_CONSUME_BOUNDARY:
StrPrint(scratch_buffer, "--%s", boundary);
if (!MemCmp(mp->data + mp->pos, scratch_buffer, StrLen(scratch_buffer))) {
mp->pos += StrLen(scratch_buffer) + 2;
mp->state = SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_HEADER;
} else {
++mp->pos;
}
break;
case SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_HEADER:
MemSet(scratch_buffer, NULL, StrLen(content_disposition_header) + 4);
MemCpy(scratch_buffer, mp->data + mp->pos, StrLen(content_disposition_header));
if (!StrICmp(scratch_buffer, content_disposition_header)) {
mp->pos += StrLen(scratch_buffer);
mp->state = SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME_FIELD;
} else {
++mp->pos;
}
break;
case SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME_FIELD:
if (!MemCmp(mp->data + mp->pos, "name=", 5)) {
mp->pos += 5;
FifoU8Flush(mp->consumed);
mp->state = SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME;
} else {
++mp->pos;
}
break;
case SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME:
switch (token) {
case ';':
case '\r':
name = @json_string_from_fifo(mp->consumed);
String.Trim(name);
String.Trim(name, '"');
mp->state = SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_TEXT_OR_FILE;
break;
default:
FifoU8Ins(mp->consumed, token);
break;
}
++mp->pos;
break;
case SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_TEXT_OR_FILE:
switch (token) {
case '\n':
tmp = @json_string_from_fifo(mp->consumed);
if (StrFind("filename=", tmp)) {
file = @slon_calloc(session, sizeof(SlonMultipartFile));
mp->state = SLON_MULTIPART_CONSUME_CONTENT_TYPE_HEADER;
} else {
mp->state = SLON_MULTIPART_SKIP_REMAINING_HEADERS;
}
break;
default:
FifoU8Ins(mp->consumed, token);
break;
}
++mp->pos;
break;
case SLON_MULTIPART_CONSUME_CONTENT_TYPE_HEADER:
MemSet(scratch_buffer, NULL, StrLen(content_type_header) + 4);
MemCpy(scratch_buffer, mp->data + mp->pos, StrLen(content_type_header));
if (!StrICmp(scratch_buffer, content_type_header)) {
mp->pos += StrLen(scratch_buffer);
mp->state = SLON_MULTIPART_CONSUME_CONTENT_TYPE;
} else {
++mp->pos;
}
break;
case SLON_MULTIPART_CONSUME_CONTENT_TYPE:
switch (token) {
case ';':
case '\r':
file->content_type = @json_string_from_fifo(mp->consumed);
String.Trim(file->content_type);
String.Trim(file->content_type, '"');
mp->state = SLON_MULTIPART_SKIP_REMAINING_HEADERS;
break;
default:
FifoU8Ins(mp->consumed, token);
break;
}
++mp->pos;
break;
case SLON_MULTIPART_SKIP_REMAINING_HEADERS:
switch (token) {
case '\r':
case '\n':
break;
default:
if (file) {
file->buffer = mp->data + mp->pos;
} else {
value = mp->data + mp->pos;
}
mp->state = SLON_MULTIPART_CONSUME_DATA;
break;
}
++mp->pos;
break;
}
}
slon_http_json_object_from_multipart_form_data_done:
FifoU8Del(mp->consumed);
@slon_free(session, mp);
name = @slon_http_free_and_null(name);
tmp = @slon_http_free_and_null(tmp);
return obj;
}
@ -501,11 +649,6 @@ U0 @slon_http_parse_request_as_form_urlencoded(SlonHttpSession* session)
U0 @slon_http_parse_request_as_multipart_form_data(SlonHttpSession* session)
{
if (StrFind("; filename=", session->request->data)) {
// Skip parsing - this is a media upload
session->request->json = Json.Parse("{}");
return;
}
session->request->json = @slon_http_json_object_from_multipart_form_data(session, session->request->data);
}

View file

@ -6,21 +6,24 @@
#define SLON_HTTP_VERB_POST 5
#define SLON_HTTP_VERB_PUT 6
#define SLON_MULTIPART_PARSER_CONSUME_BOUNDARY 0
#define SLON_MULTIPART_PARSER_CONSUME_CONTENT_DISPOSITION 1
#define SLON_MULTIPART_PARSER_CONSUME_CONTENT 2
#define SLON_MULTIPART_PARSER_DONE 3
#define SLON_MULTIPART_CONSUME_BOUNDARY 0
#define SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_HEADER 10
#define SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME_FIELD 11
#define SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_NAME 12
#define SLON_MULTIPART_CONSUME_CONTENT_DISPOSITION_TEXT_OR_FILE 13
#define SLON_MULTIPART_CONSUME_CONTENT_TYPE_HEADER 20
#define SLON_MULTIPART_CONSUME_CONTENT_TYPE 21
#define SLON_MULTIPART_CONSUME_DATA 30
#define SLON_MULTIPART_SKIP_REMAINING_HEADERS 100
#define SLON_SCRATCH_BUFFER_AND_REQUEST_JSON \
U8 scratch_buffer[256]; \
JsonObject* request_json = @slon_http_request_json(session);
#define SLON_DEBUG_PRINT_REQUEST_JSON \
JsonObject* request_json = @slon_http_request_json(session); \
U8* request_json_str = Json.Stringify(request_json); \
AdamLog("request_json: %s\n", request_json_str); \
Free(request_json_str);
JsonObject* SLON_HTTP_STATUS_CODES = Json.ParseFile("M:/Slon/Settings/status_codes.json");
JsonArray* SLON_TLDS = Json.ParseFile("M:/Slon/Settings/tlds.json");
@ -30,6 +33,20 @@ for (tld_cnt = 0; tld_cnt < SLON_TLDS->length; tld_cnt++) {
tld_array[tld_cnt] = SLON_TLDS->@(tld_cnt);
}
class SlonMultipartParser {
U8* data;
CFifoU8* consumed;
I64 pos;
I64 length;
I64 state;
};
class SlonMultipartFile {
U8* buffer;
I64 size;
U8* content_type;
};
class SlonHttpBuffer {
U8* data;
I64 size;