Issues with HTTP multipart/form-data file upload
Xavier Del Campo Romero <[email protected]> Mon, 26 Aug 2024 01:27:29 +0200
| Newsgroups | gmane.comp.web.dillo.devel |
|---|---|
| Message-ID | <[email protected]> |
Hello Dillo dev community, I am testing support among web browsers for slcl [1], a JS-less minimalist web storage solution. slcl relies on "multipart/form-data"-encoded HTTP requests to upload files to a server. Whereas Dillo helped me to uncover a few wrong assumptions on my code, I have realised a few issues on Dillo itself related to filue uploads that should be considered. Issue #1: While Dillo is fine uploading small files (up to a few MiB), things go wrong with larger files, so much that memory usage increases up to multiple GiB and can even lock the system up. This is because Dillo is designed to send requests always from memory, which means file contents must be dumped into memory first, and this might be unfeasible for large files. Suggestion: Instead, Dillo should ideally send file contents on-the-fly, so that memory usage is kept to a minimum regardless the file size. Issue #2: Even if Dillo generates a 70-byte, random boundary string (yet mostly filled with '-', similarly to Firefox [2]), it ensures it is not found anywhere inside the file contents. Again, this can be a serious bottleneck in the case of large files, as it requires to scan the whole file for a match. Suggestion: Define *all* of the 70 bytes in the boundary string as random, and assume they would never be found inside a file. The chance of accidental collision is so low that it is not worth the effort into checking them. Suggested patches: - 0001-dialog.cc-Generate-more-random-boundaries.patch) Issue #3: Dillo only supports uploading 1 file at a time. This is mostly because it relies (probably temporarily?) on the a_Dialog_save_file function [3]. However, this is not a limitation on the HTTP protocol, and other implementation such as Firefox or Chromium-based browsers support this. Suggested patches: - 0002-dialog-Add-a_Dialog_select_files.patch - 0003-WIP-multi-file-uploads.patch Conclusions: Dillo seems designed to always send requests from memory, so it is not straightforward to break this assumption in order to support large file uploads. 0003-WIP-multi-file-uploads.patch is an incomplete first step into fixing this, but it surely needs deeper design changes. I did not put more effort into these patches for the time being because, after seeing the potential complexity behind this task, I thought it was a better idea to ask the community for feedback and guidelines. Thank you very much for reading. Best regards, Xavier Del Campo Romero [1]: https://gitea.privatedns.org/xavi/slcl [2]: https://github.com/dillo-browser/dillo/blob/8a360e32ac3136494a494379a6dbbacef6f95da2/src/form.cc#L1299-L1301 [3]: https://github.com/dillo-browser/dillo/blob/8a360e32ac3136494a494379a6dbbacef6f95da2/src/dialog.cc#L256-L264 _______________________________________________ Dillo-dev mailing list -- dillo-dev-lx9mn2B4QYRWk0Htik3J/[email protected] To unsubscribe send an email to dillo-dev-leave-lx9mn2B4QYRWk0Htik3J/[email protected]
0001-dialog.cc-Generate-more-random-boundaries.patch
(text/x-patch, 2.6 KB)
From 432b8127904abed1dfb178d46cfcd39aaddbc259 Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Romero <[email protected]> Date: Mon, 26 Aug 2024 01:10:16 +0200 Subject: [PATCH 1/3] dialog.cc: Generate more random boundaries Even if major implementations tend to add several '-' characters to multipart/form-data boundaries, this is not enforced by RFC 2046. Making this boundary string more random would allow Dillo to assume the boundary string would never be found inside the file to upload. --- src/dialog.cc | 1 + src/form.cc | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/dialog.cc b/src/dialog.cc index ac007315..0137472b 100644 --- a/src/dialog.cc +++ b/src/dialog.cc @@ -14,6 +14,7 @@ */ #include <math.h> // for rint() +#include <string.h> #include <FL/fl_ask.H> #include <FL/Fl_Window.H> diff --git a/src/form.cc b/src/form.cc index 93bd4864..6e81edf6 100644 --- a/src/form.cc +++ b/src/form.cc @@ -1246,6 +1246,24 @@ Dstr *DilloHtmlForm::buildQueryData(DilloHtmlInput *active_submit) return DataStr; } +static void generate_boundary(Dstr *boundary) +{ + for (int i = 0; i < 70; i++) { + /* Extracted from RFC 2046, section 5.1.1. */ + static const char set[] = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "'()+_,-./:=? "; + char s[sizeof " "] = {0}; + + do { + *s = rand(); + } while (!strspn(s, set)); + + dStr_append(boundary, s); + } +} + /** * Generate a boundary string for use in separating the parts of a * multipart/form-data submission. @@ -1253,7 +1271,6 @@ Dstr *DilloHtmlForm::buildQueryData(DilloHtmlInput *active_submit) char *DilloHtmlForm::makeMultipartBoundary(iconv_t char_encoder, DilloHtmlInput *active_submit) { - const int max_tries = 10; Dlist *values = dList_new(5); Dstr *DataStr = dStr_new(""); Dstr *boundary = dStr_new(""); @@ -1294,15 +1311,8 @@ char *DilloHtmlForm::makeMultipartBoundary(iconv_t char_encoder, } } - /* generate a boundary that is not contained within the data */ - for (int i = 0; i < max_tries && !ret; i++) { - // Firefox-style boundary - dStr_sprintf(boundary, "---------------------------%d%d%d", - rand(), rand(), rand()); - dStr_truncate(boundary, 70); - if (dStr_memmem(DataStr, boundary) == NULL) - ret = boundary->str; - } + generate_boundary(boundary); + ret = boundary->str; dList_free(values); dStr_free(DataStr, 1); dStr_free(boundary, (ret == NULL)); -- 2.34.1
0002-dialog-Add-a_Dialog_select_files.patch
(text/x-patch, 2.9 KB)
From de375a3e0dbe2c4f2d341cf2804ef4e7ef9862c4 Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Romero <[email protected]> Date: Mon, 26 Aug 2024 01:16:43 +0200 Subject: [PATCH 2/3] dialog: Add a_Dialog_select_files As its name suggests, and as opposed to a_Dialog_select_file, this function allows to select multiple files from a directory. This commit is meant to allow multi-file uploads by future commits. --- src/dialog.cc | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/dialog.hh | 4 ++++ 2 files changed, 69 insertions(+) diff --git a/src/dialog.cc b/src/dialog.cc index 0137472b..6e4f6e6d 100644 --- a/src/dialog.cc +++ b/src/dialog.cc @@ -278,6 +278,71 @@ char *a_Dialog_open_file(const char *title, return (fc_name) ? a_Misc_escape_chars(fc_name, "% #") : NULL; } +/* Implementation based on upstream fltk: src/fl_file_dir.cxx */ +static void show_file_chooser(Fl_File_Chooser *chooser) +{ + Fl_Window *g; + + chooser->show(); + g = Fl::grab(); + + if (g) + Fl::grab(0); + + while (chooser->shown()) + Fl::wait(); + + if (g) + Fl::grab(g); +} + +Dlist *a_Dialog_select_files(const char *title, + const char *pattern, const char *fname) +{ + Dlist *list = NULL; + Fl_File_Chooser chooser(fname, pattern, Fl_File_Chooser::MULTI, title); + int n; + + show_file_chooser(&chooser); + n = chooser.count(); + + if (n <= 0) { + goto failure; + } + + list = dList_new(n); + + if (!list) { + goto failure; + } + + for (int i = 0; i < n; i++) { + const char *fc_name = chooser.value(i); + char *escaped; + + if (!fc_name) { + goto failure; + } else if (!(escaped = a_Misc_escape_chars(fc_name, "% #"))) { + goto failure; + } + + /* dList_apend has no way to tell whether the operation was successful.*/ + dList_append(list, escaped); + } + + return list; + +failure: + + if (list) { + for (int i = 0; list->len; i++) + free(list->list[i]); + } + + dList_free(list); + return NULL; +} + /** * Close text window. */ diff --git a/src/dialog.hh b/src/dialog.hh index 0a489590..4a360494 100644 --- a/src/dialog.hh +++ b/src/dialog.hh @@ -1,6 +1,8 @@ #ifndef __DIALOG_HH__ #define __DIALOG_HH__ +#include "dlib/dlib.h" + #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ @@ -18,6 +20,8 @@ const char *a_Dialog_save_file(const char *title, const char *pattern, const char *fname); const char *a_Dialog_select_file(const char *title, const char *pattern, const char *fname); +Dlist *a_Dialog_select_files(const char *title, + const char *pattern, const char *fname); char *a_Dialog_open_file(const char *title, const char *pattern, const char *fname); void a_Dialog_text_window(const char *title, const char *txt); -- 2.34.1
0003-WIP-multi-file-uploads.patch
(text/x-patch, 8.5 KB)
From 915e8c34973258955ab3170ca43fca00b4207e4e Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Romero <[email protected]> Date: Mon, 26 Aug 2024 01:18:42 +0200 Subject: [PATCH 3/3] WIP multi-file uploads TODO: send file data on-the-fly instead of dumping file contents into memory. --- src/dialog.cc | 15 ----------- src/dialog.hh | 2 -- src/form.cc | 75 +++++++++++++++++++++++++++++++++++++-------------- src/uicmd.cc | 4 +-- src/uicmd.hh | 3 ++- 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/dialog.cc b/src/dialog.cc index 6e4f6e6d..e818d028 100644 --- a/src/dialog.cc +++ b/src/dialog.cc @@ -249,21 +249,6 @@ const char *a_Dialog_save_file(const char *title, return fl_file_chooser(title, pattern, fname); } -/** - * Show the select file dialog. - * - * @return pointer to chosen filename, or NULL on Cancel. - */ -const char *a_Dialog_select_file(const char *title, - const char *pattern, const char *fname) -{ - /* - * FileChooser::type(MULTI) appears to allow multiple files to be selected, - * but just follow save_file's path for now. - */ - return a_Dialog_save_file(title, pattern, fname); -} - /** * Show the open file dialog. * diff --git a/src/dialog.hh b/src/dialog.hh index 4a360494..61a65090 100644 --- a/src/dialog.hh +++ b/src/dialog.hh @@ -18,8 +18,6 @@ const char *a_Dialog_input(const char *title, const char *msg); const char *a_Dialog_passwd(const char *title, const char *msg); const char *a_Dialog_save_file(const char *title, const char *pattern, const char *fname); -const char *a_Dialog_select_file(const char *title, - const char *pattern, const char *fname); Dlist *a_Dialog_select_files(const char *title, const char *pattern, const char *fname); char *a_Dialog_open_file(const char *title, diff --git a/src/form.cc b/src/form.cc index 6e81edf6..b1407ab7 100644 --- a/src/form.cc +++ b/src/form.cc @@ -13,6 +13,7 @@ #include "html_common.hh" #include <errno.h> +#include <stdio.h> #include <iconv.h> #include "lout/misc.hh" @@ -150,13 +151,12 @@ public: //BUG: for now everything is public entries, it is the initial value */ DilloHtmlSelect *select; bool init_val; /* only meaningful for buttons */ - Dstr *file_data; /* only meaningful for file inputs. - TODO: may become a list... */ + Dlist *file_list; /* only meaningful for file inputs. */ private: void connectTo(DilloHtmlReceiver *form_receiver); void activate(DilloHtmlForm *form, int num_entry_fields,EventButton *event); - void readFile(BrowserWindow *bw); + void getFileList(BrowserWindow *bw); public: DilloHtmlInput (DilloHtmlInputType type, Embed *embed, @@ -1175,6 +1175,7 @@ Dstr *DilloHtmlForm::buildQueryData(DilloHtmlInput *active_submit) if ((valcount = dList_length(values)) > 0) { if (input->type == DILLO_HTML_INPUT_FILE) { +#if 0 if (valcount > 1) MSG_WARN("multiple files per form control not supported\n"); Dstr *file = (Dstr *) dList_nth_data(values, 0); @@ -1195,6 +1196,7 @@ Dstr *DilloHtmlForm::buildQueryData(DilloHtmlInput *active_submit) dStr_free(dfilename, 1); } dStr_free(file, 1); +#endif } else if (input->type == DILLO_HTML_INPUT_INDEX) { /* no name */ Dstr *val = (Dstr *) dList_nth_data(values, 0); @@ -1290,6 +1292,7 @@ char *DilloHtmlForm::makeMultipartBoundary(iconv_t char_encoder, dStr_free(dstr, 1); } if (input->type == DILLO_HTML_INPUT_FILE) { +#if 0 LabelButtonResource *lbr = (LabelButtonResource*)input->embed->getResource(); const char *filename = lbr->getLabel(); @@ -1299,6 +1302,7 @@ char *DilloHtmlForm::makeMultipartBoundary(iconv_t char_encoder, dStr_append_l(DataStr, dstr->str, dstr->len); dStr_free(dstr, 1); } +#endif } int length = dList_length(values); for (int i = 0; i < length; i++) { @@ -1659,7 +1663,8 @@ void DilloHtmlReceiver::clicked (Resource *resource, */ DilloHtmlInput::DilloHtmlInput (DilloHtmlInputType type2, Embed *embed2, const char *name2, const char *init_str2, - bool init_val2) + bool init_val2) : + file_list(NULL) { type = type2; embed = embed2; @@ -1675,7 +1680,6 @@ DilloHtmlInput::DilloHtmlInput (DilloHtmlInputType type2, Embed *embed2, default: break; } - file_data = NULL; reset (); } @@ -1686,7 +1690,14 @@ DilloHtmlInput::~DilloHtmlInput () { dFree(name); dFree(init_str); - dStr_free(file_data, 1); + + if (file_list) { + for (int i = 0; i < file_list->len; i++) + free(file_list->list[i]); + } + + dList_free(file_list); + if (select) delete select; } @@ -1721,7 +1732,7 @@ void DilloHtmlInput::activate(DilloHtmlForm *form, int num_entry_fields, { switch (type) { case DILLO_HTML_INPUT_FILE: - readFile (form->html->bw); + getFileList(form->html->bw); break; case DILLO_HTML_INPUT_RESET: case DILLO_HTML_INPUT_BUTTON_RESET: @@ -1747,21 +1758,32 @@ void DilloHtmlInput::activate(DilloHtmlForm *form, int num_entry_fields, /** * Read a file into cache */ -void DilloHtmlInput::readFile (BrowserWindow *bw) -{ - const char *filename = a_UIcmd_select_file(); - if (filename) { - a_UIcmd_set_msg(bw, "Loading file..."); - dStr_free(file_data, 1); - file_data = a_Misc_file2dstr(filename); - if (file_data) { - a_UIcmd_set_msg(bw, "File loaded."); - LabelButtonResource *lbr = (LabelButtonResource*)embed->getResource(); - lbr->setLabel(filename); - } else { - a_UIcmd_set_msg(bw, "ERROR: can't load: %s", filename); +void DilloHtmlInput::getFileList (BrowserWindow *bw) +{ + file_list = a_UIcmd_select_files(); + + if (!file_list) + return; + + LabelButtonResource *lbr = (LabelButtonResource*)embed->getResource(); + char buf[sizeof "18446744073709551615 files selected"]; + const char *label; + + if (file_list->len > 1) { + int n = snprintf(buf, sizeof(buf), "%d files selected", + file_list->len); + + if (n < 0 || (size_t)n >= sizeof(buf)) { + printf("getFileList: snprintf(3) returned %d", n); + return; } + + label = buf; + } else { + label = (const char *)file_list->list[0]; } + + lbr->setLabel(label); } /** @@ -1810,6 +1832,16 @@ void DilloHtmlInput::appendValuesTo(Dlist *values, bool is_active_submit) break; case DILLO_HTML_INPUT_FILE: { + if (!file_list) { + fprintf(stderr, "Upload file list is empty\n"); + break; + } + + for (int i = 0; i < file_list->len; i++) { + dList_append(values, dStr_new((const char *)file_list->list[i])); + } + +#if 0 LabelButtonResource *lbr = (LabelButtonResource*)embed->getResource(); const char *filename = lbr->getLabel(); if (filename[0] && strcmp(filename, init_str)) { @@ -1821,6 +1853,9 @@ void DilloHtmlInput::appendValuesTo(Dlist *values, bool is_active_submit) MSG("FORM file input \"%s\" not loaded.\n", filename); } } +#else +#warning TODO +#endif } break; case DILLO_HTML_INPUT_IMAGE: diff --git a/src/uicmd.cc b/src/uicmd.cc index 187aeabc..c66e2c30 100644 --- a/src/uicmd.cc +++ b/src/uicmd.cc @@ -1070,9 +1070,9 @@ void a_UIcmd_save(void *vbw) /* * Select a file */ -const char *a_UIcmd_select_file() +Dlist *a_UIcmd_select_files() { - return a_Dialog_select_file("Dillo: Select a File", NULL, NULL); + return a_Dialog_select_files("Dillo: Select a File", NULL, NULL); } /* diff --git a/src/uicmd.hh b/src/uicmd.hh index 0a5c8fb5..deeec29e 100644 --- a/src/uicmd.hh +++ b/src/uicmd.hh @@ -14,6 +14,7 @@ #define __UICMD_HH__ #include "bw.h" +#include "dlib/dlib.h" #ifdef __cplusplus extern "C" { @@ -44,7 +45,7 @@ void a_UIcmd_stop(void *vbw); void a_UIcmd_tools(void *vbw, int x, int y); void a_UIcmd_save_link(BrowserWindow *bw, const DilloUrl *url); void a_UIcmd_open_file(void *vbw); -const char *a_UIcmd_select_file(void); +Dlist *a_UIcmd_select_files(void); void a_UIcmd_search_dialog(void *vbw); const char *a_UIcmd_get_passwd(const char *user); void a_UIcmd_book(void *vbw); -- 2.34.1
OpenPGP_0x84FF3612A9BF43F2.asc
(application/pgp-keys, 4.8 KB)
-----BEGIN PGP PUBLIC KEY BLOCK----- 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 =Om+c -----END PGP PUBLIC KEY BLOCK-----
OpenPGP_signature.asc
(application/pgp-signature, 840 B)
-----BEGIN PGP SIGNATURE----- wsF5BAABCAAjFiEEL4wEMJP9YSkk8Lb8hP82Eqm/Q/IFAmbLveEFAwAAAAAACgkQhP82Eqm/Q/J9 Hg//QSXrreH2HjsOBa6l+JhHdY15S0FECeEX0DbcLFZWc+Bpa/m0umXYoYuUKgIe4xuDFIWi4yyC XKljKa4c8JIFoUayW8Fzh8a2woM0VQvXhyiNgLLGud4ZnFaVrZdb/hjKh3TExFQEXKFFmGWIh4Sn s5eSValFcGhdc1/3Z1V4oOyH/M79rla1Bz/LkwSa637Rt3DCiKs1PdnHcroixbbUhPpGAww576Ue sXCDvV+My4RNaHM+yIhpbKCn5q4qrQF4EUcesBbPpGp2aF7oFEO4cIl69x3nJPm9saHfkoqFY5/l Wefxs+OrIdcuz4eytdBTjfgUnjar0WfizaZbmHOCmDT/ueWQZI0PPw1wLzJF6V5vJVk9DSI+79ir cvd1kI8FXm/dEt7HVLIsJY8iZmX2S+NGL9jjjRD7YUeMU+3fK4VeG9q/FiPibB20FJnZcIN0+Nl5 GEYrKG2S7JjaDOsXx4lomKGBn411ngDZJHPBc4J5NHDKT3GmyDysHfMIaWpGwcyyE+Oifv60PK1D tCqBuDkjjVrjewQ7FT++stEPTWkEWFHYGB73BaWnHbE/57lCgttMzJIgaui3Z+ZT/TDyP/nggIr+ 7z7GhkRkmmk4L8A79+taFI8q7ZXV0LzwLE8NXe8jsHW2CBwg6KzkJ0PLB3lBQ0yOv2XfM6XXu1tQ SZw= =9hOw -----END PGP SIGNATURE-----