Source file inmost.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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
include Method
include Status
type buffer = Body.bigstring
type multipart_state = {
mutable state_init : bool;
mutable name : string option;
mutable filename : string option;
mutable stream : (< > * Multipart_form.Header.t * string Lwt_stream.t) Lwt_stream.t;
}
let initial_multipart_state () = {
state_init = true;
name = None;
filename = None;
stream = Lwt_stream.of_list [];
}
module Cipher = Dream__cipher.Cipher
module Scope_variable_metadata =
struct
type 'a t = string option * ('a -> string) option
end
module Scope = Hmap.Make (Scope_variable_metadata)
type app = {
globals : Scope.t ref;
mutable debug : bool;
mutable https : bool;
mutable secrets : string list;
}
let debug app =
app.debug
let set_debug value app =
app.debug <- value
let secret app =
List.hd app.secrets
let set_secrets secrets app =
app.secrets <- secrets
let new_app () = {
globals = ref Scope.empty;
debug = false;
https = false;
secrets = [];
}
type 'a message = {
specific : 'a;
headers : (string * string) list;
body : Body.body_cell;
locals : Scope.t;
first : 'a message;
last : 'a message ref;
}
type incoming = {
app : app;
client : string;
method_ : method_;
target : string;
prefix : string list;
path : string list;
query : (string * string) list;
request_version : int * int;
upload : multipart_state;
}
type websocket = {
send : [ `Text | `Binary ] -> string -> unit Lwt.t;
receive : unit -> string option Lwt.t;
close : int option -> unit Lwt.t;
}
type outgoing = {
status : status;
websocket : (websocket -> unit Lwt.t) option;
}
type request = incoming message
type response = outgoing message
type 'a promise = 'a Lwt.t
type handler = request -> response Lwt.t
type middleware = handler -> handler
let first message =
message.first
let last message =
!(message.last)
let update message =
message.last := message;
message
let client request =
request.specific.client
let https request =
request.specific.app.https
let method_ request =
request.specific.method_
let target request =
request.specific.target
let internal_prefix request =
request.specific.prefix
let prefix request =
Formats.make_path (List.rev request.specific.prefix)
let path request =
request.specific.path
let version request =
request.specific.request_version
let with_client client request =
update {request with specific = {request.specific with client}}
let with_method_ method_ request =
update {request with specific = {request.specific with method_}}
let with_prefix prefix request =
update {request with specific = {request.specific with prefix}}
let with_path path request =
update {request with specific = {request.specific with path}}
let with_version version request =
update {request with
specific = {request.specific with request_version = version}}
let status response =
response.specific.status
let all_queries request =
request.specific.query
let query name request =
List.assoc_opt name request.specific.query
let queries name request =
request.specific.query
|> List.fold_left (fun accumulator (name', value) ->
if name' = name then
value::accumulator
else
accumulator)
[]
|> List.rev
let message =
message.headers
let message =
update {message with headers}
let name message =
let name = String.lowercase_ascii name in
message.headers
|> List.fold_left (fun matched (name', value) ->
if String.lowercase_ascii name' = name then
value::matched
else
matched)
[]
|> List.rev
let name message =
let name = String.lowercase_ascii name in
message.headers
|> List.find (fun (name', _) -> String.lowercase_ascii name' = name)
|> snd
let name message =
try Some (header_basic name message)
with Not_found -> None
let name message =
try ignore (header_basic name message); true
with Not_found -> false
let name value message =
update {message with headers = message.headers @ [(name, value)]}
let name message =
let name = String.lowercase_ascii name in
update {message with headers =
message.headers
|> List.filter (fun (name', _) -> String.lowercase_ascii name' <> name)}
let name value message =
message
|> drop_header name
|> add_header name value
let all_cookies request =
request
|> headers "Cookie"
|> List.map Formats.from_cookie
|> List.flatten
let body message =
Body.body message.body
let read message =
Body.read message.body
let next ~buffer ?string ?flush ~close ~exn message =
Body.next ~bigstring:buffer ?string ?flush ~close ~exn message.body
let with_body body message =
let body =
if String.length body = 0 then
`Empty
else
`String body
in
update {message with body = ref body}
let with_stream message =
update {message with body = ref (`Stream (ref `Idle))}
let write message chunk =
Body.write chunk message.body
let write_buffer ?(offset = 0) ?length message chunk =
let length =
match length with
| Some length -> length
| None -> Lwt_bytes.length chunk - offset
in
Body.write_bigstring chunk offset length message.body
let flush message =
Body.flush message.body
let close_stream message =
Body.close_stream message.body
let has_body message =
Body.has_body message.body
let is_websocket response =
response.specific.websocket
let fold_scope f initial scope =
Scope.fold (fun (B (key, value)) accumulator ->
match Scope.Key.info key with
| Some name, Some show_value -> f name (show_value value) accumulator
| _ -> accumulator)
scope
initial
type 'a local = 'a Scope.key
let new_local ?name ?show_value () =
Scope.Key.create (name, show_value)
let local key message =
Scope.find key message.locals
let with_local key value message =
update {message with locals = Scope.add key value message.locals}
let fold_locals f initial message =
fold_scope f initial message.locals
type 'a global = {
key : 'a Scope.key;
initializer_ : unit -> 'a;
}
let new_global ?name ?show_value initializer_ = {
key = Scope.Key.create (name, show_value);
initializer_;
}
let global {key; initializer_} request =
match Scope.find key !(request.specific.app.globals) with
| Some value -> value
| None ->
let value = initializer_ () in
request.specific.app.globals :=
Scope.add key value !(request.specific.app.globals);
value
let fold_globals f initial request =
fold_scope f initial !(request.specific.app.globals)
let app request =
request.specific.app
let request_from_http
~app
~client
~method_
~target
~version
~ =
let path, query = Formats.split_target target in
let rec request = {
specific = {
app;
client;
method_;
target;
prefix = [];
path = Formats.from_path path;
query = Formats.from_form_urlencoded query;
request_version = version;
upload = initial_multipart_state ();
};
headers;
body = ref (`Stream (ref `Idle));
locals = Scope.empty;
first = request;
last = ref request;
} in
request
let request
?(client = "127.0.0.1:12345")
?(method_ = `GET)
?(target = "/")
?(version = 1, 1)
?( = [])
body =
let path, query = Formats.split_target target in
let body =
if String.length body = 0 then
`Empty
else
`String body
in
let rec request = {
specific = {
app = new_app ();
client;
method_;
target;
prefix = [];
path = Formats.from_path path;
query = Formats.from_form_urlencoded query;
request_version = version;
upload = initial_multipart_state ();
};
headers;
body = ref body;
locals = Scope.empty;
first = request;
last = ref request;
} in
request
let response
?status
?code
?( = [])
body =
let status =
match status, code with
| None, None -> `OK
| Some status, _ -> status
| None, Some code -> int_to_status code
in
let body =
if String.length body = 0 then
`Empty
else
`String body
in
let rec response = {
specific = {
status;
websocket = None;
};
headers;
body = ref body;
locals = Scope.empty;
first = response;
last = ref response;
} in
response
let respond ?status ?code ? body =
response ?status ?code ?headers body
|> Lwt.return
let html ?status ?code ? body =
response ?status ?code ?headers body
|> with_header "Content-Type" Formats.text_html
|> Lwt.return
let json ?status ?code ? body =
response ?status ?code ?headers body
|> with_header "Content-Type" Formats.application_json
|> Lwt.return
let redirect ?status ?code ? _request location =
let status =
match status, code with
| None, None -> Some (`See_Other)
| _ -> status
in
response ?status ?code ?headers location
|> with_header "Location" location
|> Lwt.return
let stream ?status ?code ? f =
let response =
response ?status ?code ?headers ""
|> with_stream
in
Lwt.async (fun () -> f response);
Lwt.return response
let empty ? status =
respond ?headers ~status ""
let not_found _ =
respond ~status:`Not_Found ""
let websocket ? handler =
let response = response ?headers ~status:`Switching_Protocols "" in
let response =
{response with specific =
{response.specific with websocket = Some handler}}
in
Lwt.return response
let send ?(kind = `Text) websocket message =
websocket.send kind message
let receive websocket =
websocket.receive ()
let close_websocket ?code websocket =
websocket.close code
let no_middleware handler request =
handler request
let rec pipeline middlewares handler =
match middlewares with
| [] -> handler
| middleware::more -> middleware (pipeline more handler)
let =
List.stable_sort (fun (name, _) (name', _) -> compare name name') headers
let encryption_secret request =
List.hd request.specific.app.secrets
let decryption_secrets request =
request.specific.app.secrets
let encrypt ?associated_data request plaintext =
Cipher.encrypt
(module Cipher.AEAD_AES_256_GCM)
?associated_data
(encryption_secret request)
plaintext
let decrypt ?associated_data request ciphertext =
Cipher.decrypt
(module Cipher.AEAD_AES_256_GCM)
?associated_data
(decryption_secrets request)
ciphertext
let infer_cookie_prefix prefix domain path secure =
match prefix, domain, path, secure with
| Some (Some `Host), _, _, _ -> "__Host-"
| Some (Some `Secure), _, _, _ -> "__Secure-"
| Some None, _, _, _ -> ""
| None, None, Some "/", true -> "__Host-"
| None, _, _, true -> "__Secure-"
| None, _, _, _ -> ""
let cookie
?prefix:cookie_prefix
?decrypt:(decrypt_cookie = true)
?domain
?path
?secure
name
request =
let path =
match path with
| Some path -> path
| None -> Some (prefix request)
in
let secure =
match secure with
| Some secure -> secure
| None -> https request
in
let cookie_prefix = infer_cookie_prefix cookie_prefix domain path secure in
let name = cookie_prefix ^ name in
let test = fun (name', _) -> name = name' in
match all_cookies request |> List.find_opt test with
| None -> None
| Some (_, value) ->
if not decrypt_cookie then
Some value
else
match Formats.from_base64url value with
| None ->
None
| Some value ->
decrypt request value ~associated_data:("dream.cookie-" ^ name)
let set_cookie
?prefix:cookie_prefix
?encrypt:(encrypt_cookie = true)
?expires
?max_age
?domain
?path
?secure
?(http_only = true)
?(same_site = Some `Strict)
name
value
request
response =
let path =
match path with
| Some path -> path
| None -> Some (prefix request)
in
let secure =
match secure with
| Some secure -> secure
| None -> https request
in
let cookie_prefix = infer_cookie_prefix cookie_prefix domain path secure in
let name = cookie_prefix ^ name in
let value =
if encrypt_cookie then
encrypt request value ~associated_data:("dream.cookie-" ^ name)
|> Formats.to_base64url
else
value
in
let set_cookie =
Formats.to_set_cookie
?expires ?max_age ?domain ?path ~secure ~http_only ?same_site name value
in
add_header "Set-Cookie" set_cookie response