ocaml http/1, http/2 and websocket client and server library
1(** Multipart form data parsing.
2
3 {[
4 (* Non-streaming *)
5 match Multipart.parse req with
6 | Ok parts -> Multipart.find_part "name" parts
7 | Error e -> Response.bad_request (Multipart.error_to_string e)
8
9 (* Streaming large files *)
10 match Multipart.create_parser req with
11 | Ok parser ->
12 Multipart.iter_parts (fun part ->
13 Stream.Async.iter write_chunk part.body
14 ) parser
15 | Error e -> ...
16 ]} *)
17
18module Hmf = Http_multipart_formdata
19
20type part = {
21 name : string;
22 filename : string option;
23 content_type : string;
24 data : string;
25}
26
27type error =
28 | Missing_content_type
29 | Not_multipart
30 | Missing_boundary
31 | Invalid_boundary of string
32 | Parse_error of string
33
34let error_to_string = function
35 | Missing_content_type -> "Missing Content-Type header"
36 | Not_multipart -> "Content-Type is not multipart/form-data"
37 | Missing_boundary -> "Missing boundary parameter in Content-Type"
38 | Invalid_boundary msg -> "Invalid boundary: " ^ msg
39 | Parse_error msg -> "Parse error: " ^ msg
40
41let is_multipart (req : Request.t) =
42 match Request.content_type req with
43 | None -> false
44 | Some ct ->
45 let ct_lower = String.lowercase_ascii ct in
46 let prefix = "multipart/form-data" in
47 String.length ct_lower >= String.length prefix
48 && String.sub ct_lower 0 (String.length prefix) = prefix
49
50let boundary (req : Request.t) : (Hmf.boundary, error) result =
51 match Request.content_type req with
52 | None -> Error Missing_content_type
53 | Some ct -> (
54 if not (is_multipart req) then Error Not_multipart
55 else
56 match Hmf.boundary ct with
57 | Ok b -> Ok b
58 | Error msg ->
59 if String.length msg > 0 && msg.[0] = '\'' then
60 Error Missing_boundary
61 else Error (Invalid_boundary msg))
62
63let parse (req : Request.t) : (part list, error) result =
64 match boundary req with
65 | Error e -> Error e
66 | Ok b -> (
67 let body = Request.body req in
68 match Hmf.parts b body with
69 | Error msg -> Error (Parse_error msg)
70 | Ok parts ->
71 Ok
72 (List.map
73 (fun (_name, (header, body)) ->
74 {
75 name = Hmf.name header;
76 filename = Hmf.filename header;
77 content_type = Hmf.content_type header;
78 data = body;
79 })
80 parts))
81
82let find_part name parts = List.find_opt (fun p -> p.name = name) parts
83
84let find_file name parts =
85 List.find_opt (fun p -> p.name = name && Option.is_some p.filename) parts
86
87let to_assoc parts : (string * part) list =
88 List.map (fun p -> (p.name, p)) parts
89
90type stream_part = {
91 name : string;
92 filename : string option;
93 content_type : string;
94 body : Cstruct.t Stream.Async.t;
95}
96
97type parser = {
98 reader : Hmf.reader;
99 body_stream : unit -> Cstruct.t option;
100 mutable current_state : Hmf.read;
101 mutable finished : bool;
102}
103
104let create_parser (req : Request.t) : (parser, error) result =
105 match boundary req with
106 | Error e -> Error e
107 | Ok b ->
108 let body_stream =
109 Request.body_reader req |> fun r -> r.H1_server.read_stream
110 in
111 let reader = Hmf.reader b `Incremental in
112 let current_state = Hmf.read reader in
113 Ok { reader; body_stream; current_state; finished = false }
114
115let rec advance_state parser =
116 match parser.current_state with
117 | `Awaiting_input continue -> (
118 match parser.body_stream () with
119 | None ->
120 parser.current_state <- continue `Eof;
121 advance_state parser
122 | Some chunk ->
123 parser.current_state <- continue (`Cstruct chunk);
124 advance_state parser)
125 | other -> other
126
127let make_body_stream parser : Cstruct.t Stream.Async.t =
128 let finished = ref false in
129 fun () ->
130 if !finished then None
131 else
132 let rec read_body () =
133 match advance_state parser with
134 | `Body chunk ->
135 parser.current_state <- Hmf.read parser.reader;
136 Some chunk
137 | `Body_end ->
138 finished := true;
139 parser.current_state <- Hmf.read parser.reader;
140 None
141 | `End ->
142 finished := true;
143 parser.finished <- true;
144 None
145 | `Error _ ->
146 finished := true;
147 parser.finished <- true;
148 None
149 | `Header _ ->
150 finished := true;
151 None
152 | `Awaiting_input _ -> read_body ()
153 in
154 read_body ()
155
156let next_part parser : stream_part option =
157 if parser.finished then None
158 else
159 let rec find_header () =
160 match advance_state parser with
161 | `Header header ->
162 parser.current_state <- Hmf.read parser.reader;
163 let body = make_body_stream parser in
164 Some
165 {
166 name = Hmf.name header;
167 filename = Hmf.filename header;
168 content_type = Hmf.content_type header;
169 body;
170 }
171 | `End ->
172 parser.finished <- true;
173 None
174 | `Error _ ->
175 parser.finished <- true;
176 None
177 | `Body _ | `Body_end ->
178 parser.current_state <- Hmf.read parser.reader;
179 find_header ()
180 | `Awaiting_input _ -> find_header ()
181 in
182 find_header ()
183
184let iter_parts f parser : (unit, error) result =
185 let rec loop () =
186 match next_part parser with
187 | None ->
188 if parser.finished then Ok () else Error (Parse_error "Unexpected end")
189 | Some part ->
190 f part;
191 Stream.Async.drain part.body;
192 loop ()
193 in
194 loop ()
195
196let fold_parts f init parser =
197 let rec loop acc =
198 match next_part parser with
199 | None ->
200 if parser.finished then Ok acc else Error (Parse_error "Unexpected end")
201 | Some part ->
202 let acc' = f part acc in
203 Stream.Async.drain part.body;
204 loop acc'
205 in
206 loop init
207
208let collect_body (part : stream_part) : string =
209 Stream.Async.cstructs_to_string part.body
210
211let parse_assoc req =
212 match parse req with Error e -> Error e | Ok parts -> Ok (to_assoc parts)