Skip to content

Commit 709d110

Browse files
authored
Merge pull request #93 from c-cube/simon/multipart-form
library for multipart form data handling
2 parents b80c5f9 + 731dd7d commit 709d110

18 files changed

+688
-11
lines changed

examples/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
(name echo)
1313
(flags :standard -warn-error -a+8)
1414
(modules echo vfs)
15-
(libraries tiny_httpd logs tiny_httpd_camlzip))
15+
(libraries tiny_httpd logs tiny_httpd_camlzip tiny_httpd.multipart-form-data))
1616

1717
(executable
1818
(name writer)

examples/echo.ml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
open Tiny_httpd_core
22
module Log = Tiny_httpd.Log
3+
module MFD = Tiny_httpd_multipart_form_data
34

45
let now_ = Unix.gettimeofday
56

@@ -78,6 +79,58 @@ let setup_logging () =
7879
Logs.set_reporter @@ Logs.format_reporter ();
7980
Logs.set_level ~all:true (Some Logs.Debug)
8081

82+
let setup_upload server : unit =
83+
Server.add_route_handler_stream ~meth:`POST server
84+
Route.(exact "upload" @/ return)
85+
(fun req ->
86+
let (`boundary boundary) =
87+
match MFD.parse_content_type req.headers with
88+
| Some b -> b
89+
| None -> Response.fail_raise ~code:400 "no boundary found"
90+
in
91+
92+
let st = MFD.create ~boundary req.body in
93+
let tbl = Hashtbl.create 16 in
94+
let cur = ref "" in
95+
let cur_kind = ref "" in
96+
let buf = Buffer.create 16 in
97+
let rec loop () =
98+
match MFD.next st with
99+
| End_of_input ->
100+
if !cur <> "" then
101+
Hashtbl.add tbl !cur (!cur_kind, Buffer.contents buf)
102+
| Part headers ->
103+
if !cur <> "" then
104+
Hashtbl.add tbl !cur (!cur_kind, Buffer.contents buf);
105+
(match MFD.Content_disposition.parse headers with
106+
| Some { kind; name = Some name; filename = _ } ->
107+
cur := name;
108+
cur_kind := kind;
109+
Buffer.clear buf;
110+
loop ()
111+
| _ -> Response.fail_raise ~code:400 "content disposition missing")
112+
| Read sl ->
113+
Buffer.add_subbytes buf sl.bytes sl.off sl.len;
114+
loop ()
115+
in
116+
loop ();
117+
118+
let open Tiny_httpd_html in
119+
let data =
120+
Hashtbl.fold
121+
(fun name (kind, data) acc ->
122+
Printf.sprintf "%S (kind: %S): %S" name kind data :: acc)
123+
tbl []
124+
in
125+
let html =
126+
body []
127+
[
128+
pre []
129+
[ txt (Printf.sprintf "{\n%s\n}" @@ String.concat "\n" data) ];
130+
]
131+
in
132+
Response.make_string ~code:201 @@ Ok (to_string_top html))
133+
81134
let () =
82135
let port_ = ref 8080 in
83136
let j = ref 32 in
@@ -198,6 +251,8 @@ let () =
198251
~dir_behavior:Tiny_httpd.Dir.Index_or_lists ())
199252
~vfs:Vfs.vfs ~prefix:"vfs";
200253

254+
setup_upload server;
255+
201256
(* main page *)
202257
Server.add_route_handler server
203258
Route.(return)
@@ -267,6 +322,34 @@ let () =
267322
txt " (POST) to log out";
268323
];
269324
];
325+
li []
326+
[
327+
form
328+
[
329+
A.action "/upload";
330+
A.enctype "multipart/form-data";
331+
A.target "_self";
332+
A.method_ "POST";
333+
]
334+
[
335+
label [] [ txt "my beautiful form" ];
336+
input [ A.type_ "file"; A.name "file1" ];
337+
input [ A.type_ "file"; A.name "file2" ];
338+
input
339+
[
340+
A.type_ "text";
341+
A.name "a";
342+
A.placeholder "text A";
343+
];
344+
input
345+
[
346+
A.type_ "text";
347+
A.name "b";
348+
A.placeholder "text B";
349+
];
350+
input [ A.type_ "submit" ];
351+
];
352+
];
270353
];
271354
];
272355
]

src/core/headers.ml

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ let for_all pred s =
4646
true
4747
with Exit -> false
4848
49+
let parse_line_ (line : string) : _ result =
50+
try
51+
let i =
52+
try String.index line ':'
53+
with Not_found -> failwith "invalid header, missing ':'"
54+
in
55+
let k = String.sub line 0 i in
56+
if not (for_all is_tchar k) then
57+
failwith (Printf.sprintf "Invalid header key: %S" k);
58+
let v =
59+
String.sub line (i + 1) (String.length line - i - 1) |> String.trim
60+
in
61+
Ok (k, v)
62+
with Failure msg -> Error msg
63+
4964
let parse_ ~(buf : Buf.t) (bs : IO.Input.t) : t =
5065
let rec loop acc =
5166
match IO.Input.read_line_using_opt ~buf bs with
@@ -56,16 +71,10 @@ let parse_ ~(buf : Buf.t) (bs : IO.Input.t) : t =
5671
bad_reqf 400 "bad header line, not ended in CRLF"
5772
| Some line ->
5873
let k, v =
59-
try
60-
let i = String.index line ':' in
61-
let k = String.sub line 0 i in
62-
if not (for_all is_tchar k) then
63-
invalid_arg (Printf.sprintf "Invalid header key: %S" k);
64-
let v =
65-
String.sub line (i + 1) (String.length line - i - 1) |> String.trim
66-
in
67-
k, v
68-
with _ -> bad_reqf 400 "invalid header line: %S" line
74+
match parse_line_ line with
75+
| Ok r -> r
76+
| Error msg ->
77+
bad_reqf 400 "invalid header line: %s\nline is: %S" msg line
6978
in
7079
loop ((String.lowercase_ascii k, v) :: acc)
7180
in

src/core/headers.mli

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ val pp : Format.formatter -> t -> unit
3333
(** Pretty print the headers. *)
3434

3535
val parse_ : buf:Buf.t -> IO.Input.t -> t
36+
(**/*)
37+
38+
val parse_line_ : string -> (string * string, string) result
39+
(**/*)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
open Utils_
2+
3+
type t = { kind: string; name: string option; filename: string option }
4+
5+
(** Simple display *)
6+
let to_string (self : t) =
7+
let stropt = function
8+
| None -> "None"
9+
| Some s -> spf "%S" s
10+
in
11+
spf "{kind=%S; name=%s; filename=%s}" self.kind (stropt self.name)
12+
(stropt self.filename)
13+
14+
let parse (hs : Tiny_httpd.Headers.t) : t option =
15+
match Tiny_httpd.Headers.get "content-disposition" hs with
16+
| None -> None
17+
| Some s ->
18+
(match String.split_on_char ';' s with
19+
| [] ->
20+
failwith (Printf.sprintf "multipart: invalid content-disposition %S" s)
21+
| kind :: tl ->
22+
let name = ref None in
23+
let filename = ref None in
24+
List.iter
25+
(fun s ->
26+
match Utils_.split1_on ~c:'=' @@ String.trim s with
27+
| Some ("name", v) -> name := Some (Utils_.remove_quotes v)
28+
| Some ("filename", v) -> filename := Some (Utils_.remove_quotes v)
29+
| _ -> ())
30+
tl;
31+
Some { kind; name = !name; filename = !filename })

src/multipart_form/dune

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(library
2+
(name tiny_httpd_multipart_form_data)
3+
(public_name tiny_httpd.multipart-form-data)
4+
(synopsis "Port of multipart-form-data for tiny_httpd")
5+
(libraries iostream tiny_httpd))

0 commit comments

Comments
 (0)