Source file multipart_form_data.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
module StringMap = Map.Make(String)
let string_eq ~a ~a_start ~b ~len =
let r = ref true in
for i = 0 to len - 1 do
let a_i = a_start + i in
let b_i = i in
if a.[a_i] <> b.[b_i] then
r := false
done;
!r
let ends_with ~suffix ~suffix_length s =
let s_length = String.length s in
(s_length >= suffix_length) &&
(string_eq ~a:s ~a_start:(s_length - suffix_length) ~b:suffix ~len:suffix_length)
let rec first_matching p = function
| [] -> None
| x::xs ->
begin
match p x with
| Some y -> Some y
| None -> first_matching p xs
end
let option_map f = function
| None -> None
| Some x -> Some (f x)
let find_common_idx a b =
let rec go i =
if i <= 0 then
None
else
begin
if ends_with ~suffix:b ~suffix_length:i a then
Some (String.length a - i)
else
go (i - 1)
end
in
go (String.length b)
let word = function
| "" -> []
| w -> [Some w]
let split_on_string ~pattern s =
let pattern_length = String.length pattern in
let rec go start acc =
match Stringext.find_from ~start s ~pattern with
| Some match_start ->
let before = String.sub s start (match_start - start) in
let new_acc = None::(word before)@acc in
let new_start = match_start + pattern_length in
go new_start new_acc
| None ->
(word (Stringext.string_after s start))@acc
in
List.rev (go 0 [])
let split_and_process_string ~boundary s =
let f = function
| None -> `Delim
| Some w -> `Word w
in
List.map f @@ split_on_string ~pattern:boundary s
let split s boundary =
let r = ref None in
let push v =
match !r with
| None -> r := Some v
| Some _ -> assert false
in
let pop () =
let res = !r in
r := None;
res
in
let go c0 =
let c =
match pop () with
| Some x -> x ^ c0
| None -> c0
in
let string_to_process = match find_common_idx c boundary with
| None -> c
| Some idx ->
begin
let prefix = String.sub c 0 idx in
let suffix = String.sub c idx (String.length c - idx) in
push suffix;
prefix
end
in
Lwt.return @@ split_and_process_string ~boundary string_to_process
in
let initial = Lwt_stream.map_list_s go s in
let final =
Lwt_stream.flatten @@
Lwt_stream.from_direct @@ fun () ->
option_map (split_and_process_string ~boundary) @@ pop ()
in
Lwt_stream.append initial final
let until_next_delim s =
Lwt_stream.from @@ fun () ->
let%lwt res = Lwt_stream.get s in
match res with
| None
| Some `Delim -> Lwt.return_none
| Some (`Word w) -> Lwt.return_some w
let join s =
Lwt_stream.filter_map (function
| `Delim -> Some (until_next_delim @@ Lwt_stream.clone s)
| `Word _ -> None
) s
let align stream boundary =
join @@ split stream boundary
let content_type =
Stringext.chop_prefix ~prefix:"multipart/form-data; boundary=" content_type
let unquote s =
Scanf.sscanf s "%S" @@ (fun x -> x);;
let parse_name s =
option_map unquote @@ Stringext.chop_prefix ~prefix:"form-data; name=" s
let s =
match Stringext.cut ~on:": " s with
| Some (key, value) -> (key, value)
| None -> invalid_arg "parse_header"
let non_empty st =
let%lwt r = Lwt_stream.to_list @@ Lwt_stream.clone st in
Lwt.return (String.concat "" r <> "")
let : string Lwt_stream.t Lwt_stream.t -> header list Lwt.t
= fun lines ->
let%lwt = Lwt_stream.get_while_s non_empty lines in
Lwt_list.map_s (fun ->
let%lwt parts = Lwt_stream.to_list header_line_stream in
Lwt.return @@ parse_header @@ String.concat "" parts
) header_lines
type stream_part =
{ headers : header list
; body : string Lwt_stream.t
}
let parse_part chunk_stream =
let lines = align chunk_stream "\r\n" in
match%lwt get_headers lines with
| [] -> Lwt.return_none
| ->
let body = Lwt_stream.concat @@ Lwt_stream.clone lines in
Lwt.return_some { headers ; body }
let parse_stream ~stream ~content_type =
match extract_boundary content_type with
| None -> Lwt.fail_with "Cannot parse content-type"
| Some boundary ->
begin
let actual_boundary = ("--" ^ boundary) in
Lwt.return @@ Lwt_stream.filter_map_s parse_part @@ align stream actual_boundary
end
let s_part_body {body; _} = body
let s_part_name {; _} =
match
parse_name @@ List.assoc "Content-Disposition" headers
with
| Some x -> x
| None -> invalid_arg "s_part_name"
let parse_filename s =
let parts = split_on_string s ~pattern:"; " in
let f = function
| None -> None
| Some part ->
begin
match Stringext.cut part ~on:"=" with
| Some ("filename", quoted_string) -> Some (unquote quoted_string)
| _ -> None
end
in
first_matching f parts
let s_part_filename {; _} =
parse_filename @@ List.assoc "Content-Disposition" headers
type file = stream_part
let file_stream = s_part_body
let file_name = s_part_name
let file_content_type {; _} =
List.assoc "Content-Type" headers
let as_part part =
match s_part_filename part with
| Some _filename ->
Lwt.return (`File part)
| None ->
let%lwt chunks = Lwt_stream.to_list part.body in
let body = String.concat "" chunks in
Lwt.return (`String body)
let get_parts s =
let go part m =
let name = s_part_name part in
let%lwt parsed_part = as_part part in
Lwt.return @@ StringMap.add name parsed_part m
in
Lwt_stream.fold_s go s StringMap.empty
let concat a b =
match (a, b) with
| (_, "") -> a
| ("", _) -> b
| _ -> a ^ b
module Reader = struct
type t =
{ mutable buffer : string
; source : string Lwt_stream.t
}
let make stream =
{ buffer = ""
; source = stream
}
let unread r s =
r.buffer <- concat s r.buffer
let empty r =
if r.buffer = "" then
Lwt_stream.is_empty r.source
else
Lwt.return false
let read_next r =
let%lwt next_chunk = Lwt_stream.next r.source in
r.buffer <- concat r.buffer next_chunk;
Lwt.return_unit
let read_chunk r =
try%lwt
let%lwt () =
if r.buffer = "" then
read_next r
else
Lwt.return_unit
in
let res = r.buffer in
r.buffer <- "";
Lwt.return (Some res)
with Lwt_stream.Empty ->
Lwt.return None
let buffer_contains r s =
match Stringext.cut r.buffer ~on:s with
| Some _ -> true
| None -> false
let rec read_until r cond =
if cond () then
Lwt.return_unit
else
begin
let%lwt () = read_next r in
read_until r cond
end
let read_line r =
let delim = "\r\n" in
let%lwt () = read_until r (fun () -> buffer_contains r delim) in
match Stringext.cut r.buffer ~on:delim with
| None -> assert false
| Some (line, next) ->
begin
r.buffer <- next;
Lwt.return (line ^ delim)
end
end
let reader =
let rec go =
let%lwt line = Reader.read_line reader in
if line = "\r\n" then
Lwt.return headers
else
let = parse_header line in
go (header::headers)
in
go []
let rec compute_case reader boundary =
match%lwt Reader.read_chunk reader with
| None -> Lwt.return `Empty
| Some line ->
begin
match Stringext.cut line ~on:(boundary ^ "\r\n") with
| Some (pre, post) -> Lwt.return @@ `Boundary (pre, post)
| None ->
begin
match Stringext.cut line ~on:(boundary ^ "--\r\n") with
| Some (pre, post) -> Lwt.return @@ `Boundary (pre, post)
| None ->
begin
match find_common_idx line boundary with
| Some 0 ->
begin
Reader.unread reader line;
let%lwt () = Reader.read_next reader in
compute_case reader boundary
end
| Some amb_idx ->
let unambiguous = String.sub line 0 amb_idx in
let ambiguous = String.sub line amb_idx (String.length line - amb_idx) in
Lwt.return @@ `May_end_with_boundary (unambiguous, ambiguous)
| None -> Lwt.return @@ `App_data line
end
end
end
let iter_part reader boundary callback =
let fin = ref false in
let last () =
fin := true;
Lwt.return_unit
in
let handle ~send ~unread ~finish =
let%lwt () = callback send in
Reader.unread reader unread;
if finish then
last ()
else
Lwt.return_unit
in
while%lwt not !fin do
let%lwt res = compute_case reader boundary in
match res with
| `Empty -> last ()
| `Boundary (pre, post) -> handle ~send:pre ~unread:post ~finish:true
| `May_end_with_boundary (unambiguous, ambiguous) -> handle ~send:unambiguous ~unread:ambiguous ~finish:false
| `App_data line -> callback line
done
let read_file_part reader boundary callback =
iter_part reader boundary callback
let strip_crlf s =
if ends_with ~suffix:"\r\n" ~suffix_length:2 s then
String.sub s 0 (String.length s - 2)
else
s
let read_string_part reader boundary =
let value = Buffer.create 0 in
let append_to_value line = Lwt.return (Buffer.add_string value line) in
let%lwt () = iter_part reader boundary append_to_value in
Lwt.return @@ strip_crlf (Buffer.contents value)
let read_part reader boundary callback fields =
let%lwt = read_headers reader in
let content_disposition = List.assoc "Content-Disposition" headers in
let name =
match parse_name content_disposition with
| Some x -> x
| None -> invalid_arg "handle_multipart"
in
match parse_filename content_disposition with
| Some filename -> read_file_part reader boundary (callback ~name ~filename)
| None ->
let%lwt value = read_string_part reader boundary in
fields := (name, value)::!fields;
Lwt.return_unit
let handle_multipart reader boundary callback =
let fields = (ref [] : (string * string) list ref) in
let%lwt () =
let%lwt _dummyline = Reader.read_line reader in
let fin = ref false in
while%lwt not !fin do
if%lwt Reader.empty reader then
Lwt.return (fin := true)
else
read_part reader boundary callback fields
done
in
Lwt.return (!fields)
let parse ~stream ~content_type ~callback =
let reader = Reader.make stream in
let boundary =
match extract_boundary content_type with
| Some s -> "--" ^ s
| None -> invalid_arg "iter_multipart"
in
handle_multipart reader boundary callback