-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweb.carp
More file actions
3761 lines (3489 loc) · 167 KB
/
Copy pathweb.carp
File metadata and controls
3761 lines (3489 loc) · 167 KB
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
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(doc Web "is a minimal web framework for Carp with WebSocket and Server-Sent
Events support.
## Installation
```
(load \"git@github.com:carpentry-org/web@0.9.3\")
```
## Usage
```
(load \"git@github.com:carpentry-org/web@0.9.3\")
(defn hello [req params]
(Response.text @\"Hello, world!\"))
(defn echo [event params ws]
(match-ref event
(WSEvent.Connect) (WebSocket.send ws \"connected\")
(WSEvent.Message msg) (WebSocket.send ws &(fmt \"echo: %s\" msg))
(WSEvent.Binary data) (WebSocket.send-binary ws data)
(WSEvent.Close) ()))
(defn clock [event params s]
(match-ref event
(SSEEvent.Connect) (SSEStream.send-event s \"ready\" \"\")
(SSEEvent.Tick) (SSEStream.send s &(Int.str (System.time)))
(SSEEvent.Close) ()))
(defserver \"0.0.0.0\" 8080
(GET \"/hello\" hello)
(WS \"/ws/echo\" echo)
(SSE \"/events\" clock))
```")
(load "git@github.com:carpentry-org/http@0.4.2")
(load "git@github.com:carpentry-org/socket@0.2.3")
(load "git@github.com:carpentry-org/json@0.6.0")
(load "git@github.com:carpentry-org/file@0.3.0")
(load "git@github.com:carpentry-org/log@0.2.0")
(load "git@github.com:carpentry-org/utf8.carp@0.2.0")
; Byte-indexed prefix/suffix tests; the core ones measure in bytes but slice
; in characters.
(hidden web-starts-with?)
(defn web-starts-with? [s sub]
(let [n (String.length sub)]
(and (>= (String.length s) n) (= sub &(String.byte-slice s 0 n)))))
(hidden web-ends-with?)
(defn web-ends-with? [s sub]
(let [n (String.length s)
m (String.length sub)]
(and (>= n m) (= sub &(String.byte-slice s (- n m) n)))))
(defmodule SHA1
(hidden of-int)
(defn of-int [x] (Uint32.from-long (Long.from-int x)))
(hidden shl)
(defn shl [x k] (Uint32.bit-shift-left x (of-int k)))
(hidden shr)
(defn shr [x k] (Uint32.bit-shift-right x (of-int k)))
(hidden u32)
(defn u32 [hi lo]
(Uint32.bit-or
(Uint32.bit-shift-left (Uint32.from-long hi) (of-int 16))
(Uint32.from-long lo)))
(hidden rotl32)
(defn rotl32 [x n] (Uint32.bit-or (shl x n) (shr x (- 32 n))))
(hidden word32)
(defn word32 [b0 b1 b2 b3]
(Uint32.bit-or
(Uint32.bit-or (shl (of-int b0) 24) (shl (of-int b1) 16))
(Uint32.bit-or (shl (of-int b2) 8) (of-int b3))))
(doc digest "computes the SHA-1 digest of a byte array, returning 20 bytes.")
(defn digest [data]
(let-do [msg-len (Array.length data)
bit-len (Uint64.*
(Uint64.from-long (Long.from-int msg-len))
(Uint64.from-long 8l))
pad-len (let [r (Int.mod (+ msg-len 9) 64)]
(+ (+ msg-len 9) (if (= r 0) 0 (- 64 r))))
buf (Array.replicate pad-len &(Byte.from-int 0))]
(for [i 0 msg-len] (Array.aset! &buf i @(Array.unsafe-nth data i)))
(Array.aset! &buf msg-len (Byte.from-int 128))
(for [i 0 8]
(let [sh (Uint64.from-long (Long.from-int (* (- 7 i) 8)))
by (Uint64.bit-and (Uint64.bit-shift-right bit-len sh)
(Uint64.from-long 255l))]
(Array.aset! &buf
(+ (- pad-len 8) i)
(Byte.from-int (Long.to-int (Uint64.to-long by))))))
(let-do [h0 (u32 26437l 8961l)
h1 (u32 61389l 43913l)
h2 (u32 39098l 56574l)
h3 (u32 4146l 21622l)
h4 (u32 50130l 57840l)
kc0 (u32 23170l 31129l)
kc1 (u32 28377l 60321l)
kc2 (u32 36635l 48348l)
kc3 (u32 51810l 49622l)]
(for [blk 0 (/ pad-len 64)]
(let-do [off (* blk 64)
w (Array.replicate 80 &(Uint32.zero))]
(for [j 0 16]
(let [bi (+ off (* j 4))]
(Array.aset! &w
j
(word32
(Byte.to-int @(Array.unsafe-nth &buf bi))
(Byte.to-int @(Array.unsafe-nth &buf (+ bi 1)))
(Byte.to-int @(Array.unsafe-nth &buf (+ bi 2)))
(Byte.to-int @(Array.unsafe-nth &buf (+ bi 3)))))))
(for [j 16 80]
(Array.aset! &w
j
(rotl32
(Uint32.bit-xor
(Uint32.bit-xor @(Array.unsafe-nth &w (- j 3))
@(Array.unsafe-nth &w (- j 8)))
(Uint32.bit-xor @(Array.unsafe-nth &w (- j 14))
@(Array.unsafe-nth &w (- j 16))))
1)))
(let-do [a h0
b h1
c h2
d h3
e h4]
(for [j 0 80]
(let-do [f (cond
(< j 20)
(Uint32.bit-or (Uint32.bit-and b c)
(Uint32.bit-and (Uint32.bit-not b) d))
(< j 40) (Uint32.bit-xor b (Uint32.bit-xor c d))
(< j 60)
(Uint32.bit-or
(Uint32.bit-or (Uint32.bit-and b c)
(Uint32.bit-and b d))
(Uint32.bit-and c d))
(Uint32.bit-xor b (Uint32.bit-xor c d)))
k (cond (< j 20) kc0 (< j 40) kc1 (< j 60) kc2 kc3)
temp (Uint32.+
(Uint32.+ (Uint32.+ (Uint32.+ (rotl32 a 5) f) e) k)
@(Array.unsafe-nth &w j))]
(set! e d)
(set! d c)
(set! c (rotl32 b 30))
(set! b a)
(set! a temp)))
(set! h0 (Uint32.+ h0 a))
(set! h1 (Uint32.+ h1 b))
(set! h2 (Uint32.+ h2 c))
(set! h3 (Uint32.+ h3 d))
(set! h4 (Uint32.+ h4 e)))))
(let-do [result (Array.replicate 20 &(Byte.from-int 0))]
(for [i 0 5]
(let [h (cond (= i 0) h0 (= i 1) h1 (= i 2) h2 (= i 3) h3 h4)
base (* i 4)]
(for [j 0 4]
(Array.aset! &result
(+ base j)
(Byte.from-int
(Long.to-int
(Uint32.to-long
(Uint32.bit-and (shr h (* (- 3 j) 8))
(of-int 255)))))))))
result))))
(doc hex-digest "computes the SHA-1 digest of a byte array and returns it
as a 40-character lowercase hex string.")
(defn hex-digest [data]
(let-do [d (digest data)
hex @"0123456789abcdef"
out (Array.replicate 40 &\0)]
(for [i 0 20]
(let-do [b (Byte.to-int @(Array.unsafe-nth &d i))]
(Array.aset! &out (* i 2) (String.char-at &hex (bit-shift-right b 4)))
(Array.aset! &out (+ (* i 2) 1) (String.char-at &hex (bit-and b 15)))))
(String.from-chars &out))))
(defmodule Base64
(hidden b64-alphabet)
(def b64-alphabet
@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(doc encode "base64-encodes a byte array.")
(defn encode [data]
(let-do [len (Array.length data)
out (the (Array Char) [])
i 0]
(while (<= (+ i 3) len)
(let-do [b0 (Byte.to-int @(Array.unsafe-nth data i))
b1 (Byte.to-int @(Array.unsafe-nth data (+ i 1)))
b2 (Byte.to-int @(Array.unsafe-nth data (+ i 2)))]
(Array.push-back! &out
(String.char-at &b64-alphabet (bit-shift-right b0 2)))
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-or (bit-shift-left (bit-and b0
3)
4)
(bit-shift-right b1 4))))
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-or (bit-shift-left (bit-and b1
15)
2)
(bit-shift-right b2 6))))
(Array.push-back! &out (String.char-at &b64-alphabet (bit-and b2 63)))
(set! i (+ i 3))))
(let [rem (- len i)]
(cond
(= rem 1)
(let-do [b0 (Byte.to-int @(Array.unsafe-nth data i))]
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-shift-right b0 2)))
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-shift-left (bit-and b0 3) 4)))
(Array.push-back! &out \=)
(Array.push-back! &out \=))
(= rem 2)
(let-do [b0 (Byte.to-int @(Array.unsafe-nth data i))
b1 (Byte.to-int @(Array.unsafe-nth data (+ i 1)))]
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-shift-right b0 2)))
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-or (bit-shift-left (bit-and b0
3)
4)
(bit-shift-right b1 4))))
(Array.push-back! &out
(String.char-at &b64-alphabet
(bit-shift-left (bit-and b1 15)
2)))
(Array.push-back! &out \=))
()))
(String.from-chars &out))))
(defmodule HMAC
(hidden hex-encode)
(private hex-encode)
(defn hex-encode [bytes]
(let-do [hex @"0123456789abcdef"
len (Array.length bytes)
out (Array.replicate (* len 2) &\0)]
(for [i 0 len]
(let-do [b (Byte.to-int @(Array.unsafe-nth bytes i))]
(Array.aset! &out (* i 2) (String.char-at &hex (bit-shift-right b 4)))
(Array.aset! &out (+ (* i 2) 1) (String.char-at &hex (bit-and b 15)))))
(String.from-chars &out)))
(hidden block-size)
(private block-size)
(def block-size 64)
(doc sha1 "computes the HMAC-SHA1 digest of `msg` using `key`.
Returns 20 raw bytes.")
(defn sha1 [key msg]
(let-do [norm (if (> (Array.length key) block-size) (SHA1.digest key) @key)
padded (Array.replicate block-size &(Byte.from-int 0))]
(for [i 0 (Array.length &norm)]
(Array.aset! &padded i @(Array.unsafe-nth &norm i)))
(let-do [i-pad (Array.replicate block-size &(Byte.from-int 0))
o-pad (Array.replicate block-size &(Byte.from-int 0))]
(for [i 0 block-size]
(let-do [kb (Byte.to-int @(Array.unsafe-nth &padded i))]
(Array.aset! &i-pad i (Byte.from-int (bit-xor kb 54)))
(Array.aset! &o-pad i (Byte.from-int (bit-xor kb 92)))))
(let [inner-data (Array.concat &[i-pad @msg])
inner-hash (SHA1.digest &inner-data)
outer-data (Array.concat &[o-pad inner-hash])]
(SHA1.digest &outer-data)))))
(doc sha1-hex "computes the HMAC-SHA1 of `msg` using `key`, returning a
40-character lowercase hex string.")
(defn sha1-hex [key msg] (hex-encode &(sha1 key msg))))
(defmodule Map
(doc update-value! "calls `f` with a reference to the value at key `k`.
Does nothing if the key is not found. The reference allows mutation through
FFI functions (e.g. reading into a stream's buffer).")
(defn update-value! [m k f]
(let [idx (Int.positive-mod (hash k) @(n-buckets m))]
(let [bucket (Array.unsafe-nth (buckets m) idx)
i (Bucket.find bucket k)]
(when (>= i 0)
(~f (Pair.b (Array.unsafe-nth (Bucket.entries bucket) i)))))))
(doc value-ref! "calls `f` with a reference to the value at key `k` and
returns whatever `f` returns. If the key is missing, returns `default`.")
(defn value-ref! [m k f default]
(let [idx (Int.positive-mod (hash k) @(n-buckets m))]
(let [bucket (Array.unsafe-nth (buckets m) idx)
i (Bucket.find bucket k)]
(if (>= i 0)
(~f (Pair.b (Array.unsafe-nth (Bucket.entries bucket) i)))
default)))))
(relative-include "src/fstat_mtime.h")
(private web-fstat-mtime)
(hidden web-fstat-mtime)
(register web-fstat-mtime (Fn [Int] Long) "web_fstat_mtime")
(relative-include "src/is_directory.h")
(private web-is-directory?)
(hidden web-is-directory?)
(register web-is-directory? (Fn [&String] Bool) "web_is_directory")
(hidden web-pad2)
(defn web-pad2 [n] (if (< n 10) (fmt "0%d" n) (Int.str n)))
(hidden web-day-name)
(defn web-day-name [w]
(String.slice "SunMonTueWedThuFriSat" (* w 3) (* (+ w 1) 3)))
(hidden web-month-name)
(defn web-month-name [m]
(String.slice "JanFebMarAprMayJunJulAugSepOctNovDec" (* (- m 1) 3) (* m 3)))
; unix seconds → an RFC 9110 IMF-fixdate string, e.g. "Sun, 06 Nov 1994 08:49:37 GMT".
(hidden web-http-date)
(defn web-http-date [secs]
(let [days (Long./ secs 86400l)
tod (Long.mod secs 86400l)
z (Long.+ days 719468l)
era (Long./ z 146097l)
doe (Long.- z (Long.* era 146097l))
yoe (Long./
(Long.-
(Long.+ (Long.- doe (Long./ doe 1460l)) (Long./ doe 36524l))
(Long./ doe 146096l))
365l)
y0 (Long.+ yoe (Long.* era 400l))
doy (Long.- doe
(Long.+ (Long.* 365l yoe)
(Long.- (Long./ yoe 4l) (Long./ yoe 100l))))
mp (Long./ (Long.+ (Long.* 5l doy) 2l) 153l)
d (Long.to-int
(Long.+ (Long.- doy (Long./ (Long.+ (Long.* 153l mp) 2l) 5l)) 1l))
m (Long.to-int (if (Long.< mp 10l) (Long.+ mp 3l) (Long.- mp 9l)))
y (Long.to-int (if (< m 3) (Long.+ y0 1l) y0))
hh (Long.to-int (Long./ tod 3600l))
mm (Long.to-int (Long./ (Long.mod tod 3600l) 60l))
ss (Long.to-int (Long.mod tod 60l))
wd (Long.to-int (Long.mod (Long.+ days 4l) 7l))]
(fmt "%s, %s %s %s %s:%s:%s GMT"
&(web-day-name wd)
&(web-pad2 d)
&(web-month-name m)
&(Int.str y)
&(web-pad2 hh)
&(web-pad2 mm)
&(web-pad2 ss))))
(hidden web-date-uint)
(defn web-date-uint [bytes start count]
(let-do [v 0
ok true]
(for [i start (+ start count)]
(let [b @(Array.unsafe-nth bytes i)]
(if (and (>= b 48b) (<= b 57b))
(set! v (+ (* v 10) (- (Byte.to-int b) 48)))
(set! ok false))))
(if ok v -1)))
(hidden web-date-month)
(defn web-date-month [bytes off]
(let-do [result -1
names (String.to-bytes "JanFebMarAprMayJunJulAugSepOctNovDec")]
(for [m 0 12]
(when (and
(= @(Array.unsafe-nth bytes off) @(Array.unsafe-nth &names (* m 3)))
(and
(= @(Array.unsafe-nth bytes (+ off 1))
@(Array.unsafe-nth &names (+ (* m 3) 1)))
(= @(Array.unsafe-nth bytes (+ off 2))
@(Array.unsafe-nth &names (+ (* m 3) 2)))))
(set! result (+ m 1))))
result))
(hidden web-epoch-of)
(defn web-epoch-of [y m d hh mi se]
(let [yl (Long.from-int (if (< m 3) (- y 1) y))
era (Long./ yl 400l)
yoe (Long.- yl (Long.* era 400l))
mp (Long.from-int (if (> m 2) (- m 3) (+ m 9)))
doy (Long.+ (Long./ (Long.+ (Long.* 153l mp) 2l) 5l)
(Long.from-int (- d 1)))
doe (Long.+
(Long.+ (Long.* yoe 365l) (Long./ yoe 4l))
(Long.- doy (Long./ yoe 100l)))
days (Long.- (Long.+ (Long.* era 146097l) doe) 719468l)]
(Long.+ (Long.* days 86400l)
(Long.+ (Long.* (Long.from-int hh) 3600l)
(Long.+ (Long.* (Long.from-int mi) 60l) (Long.from-int se))))))
; an RFC 9110 IMF-fixdate string → unix seconds, or Nothing if malformed.
(hidden web-parse-http-date)
(defn web-parse-http-date [s]
(let [bytes (String.to-bytes s)]
(if (< (Array.length &bytes) 29)
(Maybe.Nothing)
(let [dd (web-date-uint &bytes 5 2)
mon (web-date-month &bytes 8)
yyyy (web-date-uint &bytes 12 4)
hh (web-date-uint &bytes 17 2)
mi (web-date-uint &bytes 20 2)
se (web-date-uint &bytes 23 2)]
(if (or (< dd 1)
(or (> dd 31)
(or (< mon 1)
(or (< yyyy 0)
(or (< hh 0)
(or (> hh 23)
(or (< mi 0)
(or (> mi 59) (or (< se 0) (> se 60))))))))))
(Maybe.Nothing)
(Maybe.Just (web-epoch-of yyyy mon dd hh mi se)))))))
(defmodule Response
(doc text "creates a 200 OK response with a plain text body.")
(defn text [body]
(let [k @"Content-Type"
v [@"text/plain; charset=utf-8"]]
(init 200 @"OK" @"HTTP/1.1" [] (Map.put {} &k &v) body)))
(doc html "creates a 200 OK response with an HTML body.")
(defn html [body]
(let [k @"Content-Type"
v [@"text/html; charset=utf-8"]]
(init 200 @"OK" @"HTTP/1.1" [] (Map.put {} &k &v) body)))
(doc json "creates a 200 OK response with a JSON body.")
(defn json [j]
(let [k @"Content-Type"
v [@"application/json"]
body (match (JSON.str j) (Result.Success s) s (Result.Error _) @"{}")]
(init 200 @"OK" @"HTTP/1.1" [] (Map.put {} &k &v) body)))
(doc not-found "creates a 404 Not Found response.")
(defn not-found [] (init 404 @"Not Found" @"HTTP/1.1" [] {} @"Not Found"))
(doc bad-request "creates a 400 Bad Request response.")
(defn bad-request []
(init 400 @"Bad Request" @"HTTP/1.1" [] {} @"Bad Request"))
(doc redirect "creates a 302 redirect response to `url`.")
(defn redirect [url]
(let [k @"Location"
v [url]]
(init 302 @"Found" @"HTTP/1.1" [] (Map.put {} &k &v) @"")))
(doc with-header "adds a header to a response.")
(defn with-header [r k v]
(set-headers r
(Map.update-with-default @(headers &r)
&k
&(fn [a] (Array.push-back a @&v))
[])))
(doc with-status "sets the status code and message of a response.")
(defn with-status [r code msg] (set-message (set-code r code) msg))
(doc content-type-for "infers an HTTP `Content-Type` for a file path
based on its extension, matched case-insensitively, so `photo.JPG` and
`photo.jpg` are the same. Unknown extensions return
`application/octet-stream`.")
(defn content-type-for [path]
(let [lowered (String.ascii-to-lower path)]
(cond
(web-ends-with? &lowered ".html") @"text/html; charset=utf-8"
(web-ends-with? &lowered ".js") @"application/javascript; charset=utf-8"
(web-ends-with? &lowered ".mjs") @"application/javascript; charset=utf-8"
(web-ends-with? &lowered ".css") @"text/css; charset=utf-8"
(web-ends-with? &lowered ".svg") @"image/svg+xml"
(web-ends-with? &lowered ".json") @"application/json"
(web-ends-with? &lowered ".png") @"image/png"
(web-ends-with? &lowered ".jpg") @"image/jpeg"
(web-ends-with? &lowered ".jpeg") @"image/jpeg"
(web-ends-with? &lowered ".gif") @"image/gif"
(web-ends-with? &lowered ".ico") @"image/x-icon"
(web-ends-with? &lowered ".webp") @"image/webp"
(web-ends-with? &lowered ".woff") @"font/woff"
(web-ends-with? &lowered ".woff2") @"font/woff2"
(web-ends-with? &lowered ".txt") @"text/plain; charset=utf-8"
(web-ends-with? &lowered ".xml") @"application/xml"
(web-ends-with? &lowered ".pdf") @"application/pdf"
@"application/octet-stream")))
(doc file "creates a 200 OK response by reading `path` from disk. Returns
a 404 Not Found response if the file is missing or unreadable. The
`Content-Type` header is inferred from the file extension via
[`content-type-for`](#content-type-for). An `ETag` header is computed
from the SHA-1 hash of the file contents.")
(defn file [path]
(match (File.open-with path "r")
(Result.Error _) (not-found)
(Result.Success f)
(match (File.read-all &f)
(Result.Error _) (do (File.close f) (not-found))
(Result.Success contents)
(let-do [ct-k @"Content-Type"
ct-v [(content-type-for path)]
etag (fmt "\"%s\""
&(SHA1.hex-digest &(String.to-bytes &contents)))
et-k @"ETag"
et-v [etag]
hdrs (Map.put (Map.put {} &ct-k &ct-v) &et-k &et-v)]
(File.close f)
(init 200 @"OK" @"HTTP/1.1" [] hdrs contents)))))
(doc chunked "creates a response with `Transfer-Encoding: chunked`.
The body is built from an array of chunk strings, each encoded with the
chunked framing (`<hex-length>\\r\\n<data>\\r\\n`) and terminated by a
final `0\\r\\n\\r\\n`.
```
(Response.chunked 200 @\"text/plain\"
&[@\"first chunk\" @\"second chunk\"])
```
The chunks are pre-encoded into a single buffer. For truly lazy streaming
where chunks are generated on demand, a future version will provide a
callback-based API.")
(defn chunked [code content-type chunks]
(let-do [sb (StringBuf.create)]
(for [i 0 (Array.length chunks)]
(let-do [chunk (Array.unsafe-nth chunks i)
len (String.length chunk)]
(StringBuf.append-str &sb &(fmt "%x\r\n" len))
(StringBuf.append-str &sb chunk)
(StringBuf.append-str &sb "\r\n")))
(StringBuf.append-str &sb "0\r\n\r\n")
(let-do [body (StringBuf.to-string &sb)]
(StringBuf.delete sb)
(->
(init code (Status.reason code) @"HTTP/1.1" [] {} body)
(with-header @"Transfer-Encoding" @"chunked")
(with-header @"Content-Type" content-type)))))
(doc sendfile "creates a 200 OK response that signals the server to
transfer the file at `path` directly to the socket via `sendfile(2)`,
avoiding the user-space copy. Falls back to [`file`](#file) if the
server does not detect the `X-Sendfile` header.
The `Content-Type` is inferred from the extension. The server sets
`Content-Length` from the file size. An `ETag` header is computed from
the file's modification time and size, avoiding a full read of the
file contents.
The server advertises `Accept-Ranges: bytes` and honours a single
`Range` request against the transfer, answering `206 Partial Content`
with a `Content-Range` header (or `416` when unsatisfiable).")
(defn sendfile [path]
(let [fd (IO.Raw.open path IO.Raw.O-RDONLY)]
(if (< fd 0)
(-> (init 200 @"OK" @"HTTP/1.1" [] {} @"")
(with-header @"Content-Type" (content-type-for path))
(with-header @"X-Sendfile" @path))
(let-do [sz (IO.Raw.fstat-size fd)
mt (web-fstat-mtime fd)]
(ignore (IO.Raw.close-fd fd))
(if (or (< sz 0l) (< mt 0l))
(-> (init 200 @"OK" @"HTTP/1.1" [] {} @"")
(with-header @"Content-Type" (content-type-for path))
(with-header @"X-Sendfile" @path))
(let [etag (fmt "\"%x-%x\"" mt sz)]
(-> (init 200 @"OK" @"HTTP/1.1" [] {} @"")
(with-header @"Content-Type" (content-type-for path))
(with-header @"X-Sendfile" @path)
(with-header @"ETag" etag)
(with-header @"Last-Modified" (web-http-date mt))))))))))
(doc Form "provides helpers for parsing `application/x-www-form-urlencoded`
and `multipart/form-data` request bodies.")
(defmodule Form
(doc parse "parses a URL-encoded form body into a `(Map String String)`.
```
(let [data (Form.decode (Request.body req))]
(Map.get &data \"username\"))
```
Keys and values are URL-decoded. Duplicate keys keep the last value.")
(hidden decode-form-value)
(defn decode-form-value [s]
(let-do [cs (String.to-bytes s)
len (Array.length &cs)]
(for [i 0 len]
(when (= @(Array.unsafe-nth &cs i) (Char.to-byte \+))
(Array.aset! &cs i (Char.to-byte \space))))
(URI.unescape &(String.from-bytes &cs))))
(defn decode [body]
(let-do [result (the (Map String String) {})
pairs &(String.split-by body &[\&])]
(for [i 0 (Array.length pairs)]
(let [pair (Array.unsafe-nth pairs i)]
(when (> (String.length pair) 0)
(let [eq (String.index-of pair \=)]
(if (< eq 0)
(Map.put! &result &(decode-form-value pair) &@"")
(let [k (decode-form-value &(String.byte-slice pair 0 eq))
v (decode-form-value
&(String.byte-slice pair (+ eq 1) (String.length pair)))]
(Map.put! &result &k &v)))))))
result))
(doc parse-request "parses the form body from a request. Returns an empty
map if the content type is not `application/x-www-form-urlencoded`.")
(defn decode-request [req]
(if (match (Request.header req "Content-Type")
(Maybe.Just ct)
(web-starts-with? &ct "application/x-www-form-urlencoded")
(Maybe.Nothing) false)
(decode (Request.body req))
(the (Map String String) {})))
(doc decode-multipart "decodes a `multipart/form-data` body given the
boundary string, via http’s `Multipart.parse`. Returns
`(Result (Array FormPart) String)`; fails when the opening boundary
delimiter is absent.
```
(match (Form.decode-multipart body \"boundary123\")
(Result.Success parts) (Array.nth &parts 0)
(Result.Error _) (Maybe.Nothing))
```
Each part’s `name`, optional `filename`, and optional `content-type` are
extracted from its MIME headers. The `body` field is the raw content between
the part headers and the next boundary delimiter.")
(defn decode-multipart [body boundary] (Multipart.parse body boundary))
(doc multipart? "checks whether a request has a `multipart/form-data`
content type.")
(defn multipart? [req]
(match (Request.header req "Content-Type")
(Maybe.Just ct) (web-starts-with? &ct "multipart/form-data")
(Maybe.Nothing) false))
(doc decode-multipart-request "decodes the multipart form body of a
request, taking the boundary from its `Content-Type` header, via http’s
`Request.multipart-data`. Returns `(Result (Array FormPart) String)`; fails
when the request is not `multipart/form-data` or carries no boundary.
```
(defn upload [req params]
(match (Form.decode-multipart-request req)
(Result.Success parts)
(Response.text (fmt \"got %d parts\" (Array.length &parts)))
(Result.Error _) (Response.bad-request)))
```")
(defn decode-multipart-request [req] (Request.multipart-data req)))
(doc WSEvent "represents an event on a WebSocket connection.
Handlers receive one of these for each lifecycle event:
- `Connect`: the connection was upgraded from HTTP
- `(Message msg)`: a text message arrived
- `(Binary data)`: a binary message arrived
- `Close`: the peer requested a close")
(deftype WSEvent
Connect
(Message [String])
(Binary [(Array Byte)])
Close)
(doc WebSocket "represents an active WebSocket connection.
Handlers receive a reference to this and call [`WebSocket.send`](#send) or
[`WebSocket.send-binary`](#send-binary) to queue outgoing text or binary
frames. The event loop drains the outbox after the handler returns.
The `protocol` field holds the negotiated subprotocol (RFC 6455 §4.2.2),
or `Nothing` if no subprotocol was negotiated.")
(deftype WebSocket [fd Int
protocol (Maybe String)
outbox (Array (Array Byte))])
(hidden WSFrame)
(deftype WSFrame
[fin Bool
opcode Int
rsv Int
masked Bool
payload (Array Byte)
consumed Int])
(defmodule WebSocket
(hidden encode-ws-frame)
(defn encode-ws-frame [opcode payload]
(let-do [len (Array.length payload)
frame (the (Array Byte) [])]
(Array.push-back! &frame (Byte.from-int (+ 128 opcode)))
; FIN + opcode
(cond
(< len 126) (Array.push-back! &frame (Byte.from-int len))
(< len 65536)
(do
(Array.push-back! &frame (Byte.from-int 126))
(Array.push-back! &frame (Byte.from-int (/ len 256)))
(Array.push-back! &frame (Byte.from-int (Int.mod len 256))))
(do
(Array.push-back! &frame (Byte.from-int 127))
(for [k 0 4] (Array.push-back! &frame (Byte.from-int 0)))
(Array.push-back! &frame
(Byte.from-int (bit-and (bit-shift-right len 24) 255)))
(Array.push-back! &frame
(Byte.from-int (bit-and (bit-shift-right len 16) 255)))
(Array.push-back! &frame
(Byte.from-int (bit-and (bit-shift-right len 8) 255)))
(Array.push-back! &frame (Byte.from-int (bit-and len 255)))))
(for [k 0 len] (Array.push-back! &frame @(Array.unsafe-nth payload k)))
frame))
(doc encode-text "encodes a string as a WebSocket text frame (no mask).")
(defn encode-text [msg]
(let [bytes (String.to-bytes msg)] (encode-ws-frame 1 &bytes)))
(doc encode-binary
"encodes a byte array as a WebSocket binary frame (no mask).")
(defn encode-binary [data] (encode-ws-frame 2 data))
(hidden encode-pong)
(defn encode-pong [payload] (encode-ws-frame 10 payload))
(doc encode-ping
"encodes a WebSocket ping frame with the given payload (may be empty).")
(defn encode-ping [payload] (encode-ws-frame 9 payload))
(hidden encode-close)
(defn encode-close []
(the (Array Byte) [(Byte.from-int 136) (Byte.from-int 0)]))
; 0x88 FIN + close, len 0
(hidden encode-close-with-code)
(defn encode-close-with-code [code]
(the (Array Byte)
[(Byte.from-int 136)
; 0x88 FIN + close
(Byte.from-int 2)
; payload length 2
(Byte.from-int (bit-and (bit-shift-right code 8) 255))
(Byte.from-int (bit-and code 255))]))
(hidden frame-len-exceeds?)
; Whether the frame at `offset` declares a payload longer than `cap`, decided
; without ever overflowing Int (a 64-bit length in [2^31, 2^32) would wrap to a
; negative Int). Returns false while the length field isn't fully buffered yet,
; so the caller keeps waiting for more bytes.
(defn frame-len-exceeds? [buf offset cap]
(let [avail (- (Array.length buf) offset)]
(if (< avail 2)
false
(let [plen0 (bit-and
(Byte.to-int @(Array.unsafe-nth buf (+ offset 1)))
127)]
(cond
(< plen0 126)
false
(= plen0 126)
(if (< avail 4)
false
(>
(+
(* (Byte.to-int @(Array.unsafe-nth buf (+ offset 2))) 256)
(Byte.to-int @(Array.unsafe-nth buf (+ offset 3))))
cap))
; 64-bit length in bytes offset+2..offset+9, big-endian
(< avail 10)
false
; any high-32 bit set => >= 2^32, far past any cap
(>
(bit-or
(bit-or
(Byte.to-int @(Array.unsafe-nth buf (+ offset 2)))
(Byte.to-int @(Array.unsafe-nth buf (+ offset 3))))
(bit-or
(Byte.to-int @(Array.unsafe-nth buf (+ offset 4)))
(Byte.to-int @(Array.unsafe-nth buf (+ offset 5)))))
0)
true
; low word's top byte >= 128 => >= 2^31, which would overflow Int
(>= (Byte.to-int @(Array.unsafe-nth buf (+ offset 6))) 128)
true
; remaining value is < 2^31, so computing and comparing it is safe
(>
(+
(* (Byte.to-int @(Array.unsafe-nth buf (+ offset 6))) 16777216)
(+
(* (Byte.to-int @(Array.unsafe-nth buf (+ offset 7))) 65536)
(+
(* (Byte.to-int @(Array.unsafe-nth buf (+ offset 8))) 256)
(Byte.to-int @(Array.unsafe-nth buf (+ offset 9))))))
cap))))))
(hidden decode-frame)
(defn decode-frame [buf offset]
(let [len (Array.length buf)
avail (- len offset)]
(if (< avail 2)
(the (Maybe WSFrame) (Maybe.Nothing))
(let-do [b0 (Byte.to-int @(Array.unsafe-nth buf offset))
b1 (Byte.to-int @(Array.unsafe-nth buf (+ offset 1)))
fin (> (bit-and b0 128) 0)
rsv (bit-and (bit-shift-right b0 4) 7)
opcode (bit-and b0 15)
masked (> (bit-and b1 128) 0)
plen0 (bit-and b1 127)
hdr-size 2
plen plen0
ok true]
(cond
(= plen0 126)
(if (< avail 4)
(set! ok false)
(do
(set! hdr-size 4)
(set! plen
(+
(*
(Byte.to-int @(Array.unsafe-nth buf (+ offset 2)))
256)
(Byte.to-int @(Array.unsafe-nth buf (+ offset 3)))))))
(= plen0 127)
(if (< avail 10)
(set! ok false)
(do
(set! hdr-size 10)
(if (or
(>
(bit-or
(bit-or
(Byte.to-int @(Array.unsafe-nth buf (+ offset 2)))
(Byte.to-int @(Array.unsafe-nth buf (+ offset 3))))
(bit-or
(Byte.to-int @(Array.unsafe-nth buf (+ offset 4)))
(Byte.to-int @(Array.unsafe-nth buf (+ offset 5)))))
0)
; a low-word top byte >= 128 makes the length >= 2^31, which
; would overflow Int to a negative payload length
(>= (Byte.to-int @(Array.unsafe-nth buf (+ offset 6))) 128))
(set! ok false)
(set! plen
(+
(+
(+
(*
(Byte.to-int @(Array.unsafe-nth buf
(+ offset 6)))
16777216)
(*
(Byte.to-int @(Array.unsafe-nth buf
(+ offset 7)))
65536))
(*
(Byte.to-int @(Array.unsafe-nth buf (+ offset 8)))
256))
(Byte.to-int @(Array.unsafe-nth buf (+ offset 9))))))))
())
(if (not ok)
(Maybe.Nothing)
(let [mask-size (if masked 4 0)
total (+ (+ hdr-size mask-size) plen)]
(if (< avail total)
(Maybe.Nothing)
(let-do [mask-off (+ offset hdr-size)
data-off (+ mask-off mask-size)
payload (Array.replicate plen &(Byte.from-int 0))]
(for [k 0 plen]
(let [b @(Array.unsafe-nth buf (+ data-off k))
unmasked (if masked
(Byte.from-int
(bit-xor (Byte.to-int b)
(Byte.to-int
@(Array.unsafe-nth buf
(+ mask-off
(Int.mod k 4))))))
b)]
(Array.aset! &payload k unmasked)))
(Maybe.Just (WSFrame.init fin opcode rsv masked payload total))))))))))
(hidden control-frame-protocol-error?)
; RFC 6455 constraints a control frame (opcode >= 8) must satisfy, checked
; before it is acted on. Violating any one fails the connection with 1002:
; §5.5 control frames MUST NOT be fragmented (FIN must be set),
; §5.5 their payload MUST be <= 125 bytes,
; §5.2 only close (8), ping (9) and pong (10) are defined — the reserved
; control opcodes 0xB-0xF must be rejected, not silently ignored.
(defn control-frame-protocol-error? [opcode fin plen]
(or (not fin) (> plen 125) (and (/= opcode 8) (/= opcode 9) (/= opcode 10))))
(hidden close-response-code)
; RFC 6455 §5.5.1/§7.4: the status code to echo for a received close frame
; (1002 malformed, 1007 non-UTF-8 reason), or Nothing when it had no payload.
(defn close-response-code [payload]
(let [len (Array.length payload)]
(cond
(= len 0) (Maybe.Nothing)
(= len 1) (Maybe.Just 1002)
(let [code (+
(* (Byte.to-int @(Array.unsafe-nth payload 0)) 256)
(Byte.to-int @(Array.unsafe-nth payload 1)))]
(cond
(or (< code 1000)
(and (>= code 1004) (<= code 1006))
(and (>= code 1012) (<= code 2999))
(> code 4999))
(Maybe.Just 1002)
(not (UTF8.valid? &(Array.suffix payload 2))) (Maybe.Just 1007)
(Maybe.Just code))))))
(hidden close-response-frame)
(defn close-response-frame [payload]
(match (close-response-code payload)
(Maybe.Nothing) (encode-close)
(Maybe.Just code) (encode-close-with-code code)))
(doc send "queues a text message for sending on the WebSocket connection.")
(defn send [ws msg] (Array.push-back! (WebSocket.outbox ws) (encode-text msg)))
(doc send-binary
"queues a binary message for sending on the WebSocket connection.")
(defn send-binary [ws data]
(Array.push-back! (WebSocket.outbox ws) (encode-binary data)))
(private ws-write-fd-)
(hidden ws-write-fd-)
(relative-include "src/ws_write.h")
(register ws-write-fd- (Fn [Int &(Array Byte)] Int) "ws_write_fd")
(doc send-now "sends a text message immediately, bypassing the outbox.
Writes the WebSocket frame directly to the socket, waiting for the socket to
drain if the kernel send buffer is full. Use this inside handlers that block
for a long time (e.g. LLM streaming) so that each message reaches the client
without waiting for the handler to return.
Returns `true` when the whole frame was written. Returns `false` when the
connection is dead or the client stopped draining; the caller should stop
sending on this connection.")
(defn send-now [ws msg]
(let [frame (encode-text msg)]
(= (ws-write-fd- @(WebSocket.fd ws) &frame) (Array.length &frame))))
(doc send-binary-now
"sends a binary message immediately, bypassing the outbox.
Like [`send-now`](#send-now) but for binary data.")
(defn send-binary-now [ws data]
(let [frame (encode-binary data)]
(= (ws-write-fd- @(WebSocket.fd ws) &frame) (Array.length &frame)))))
(hidden ws-magic-guid)
(def ws-magic-guid @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
(hidden ws-accept-key)
(defn ws-accept-key [client-key]
(let [combined (String.append client-key &ws-magic-guid)
hash (SHA1.digest &(String.to-bytes &combined))]
(Base64.encode &hash)))
(doc Handler "is the type of route handler functions.
A handler takes a reference to the parsed `Request` and a reference to a
`(Map String String)` of path parameters captured by `:name` segments in the
route pattern. It returns a `Response`.")
(deftype Route
[method String
pattern String
handler (Fn [&Request &(Map String String)] Response)])
(doc match-route "matches a route `pattern` against a request `path`.
Pattern segments starting with `:` are named captures. A `*` as the last
segment matches the rest of the path and captures it as the `*` parameter.
The bare pattern `*` matches any path. Returns `(Maybe (Map String String))`
containing the captured parameters, or `Nothing` if the path does not match.
Examples: `/api/*` matches `/api/foo/bar` with `* = foo/bar`.
`/users/:id/*` matches `/users/42/posts/1` with `id = 42`, `* = posts/1`.")
(defn match-route [pattern path]
(if (= pattern "*")
(Maybe.Just (the (Map String String) {}))
(let [pat-parts &(String.split-by pattern &[\/])
path-parts &(String.split-by path &[\/])
n-pat (Array.length pat-parts)
n-path (Array.length path-parts)