-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnetutils.csym
More file actions
2192 lines (2110 loc) · 84.4 KB
/
Copy pathnetutils.csym
File metadata and controls
2192 lines (2110 loc) · 84.4 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
#$cSYM/1.0(netutils.ecs):-,-,-,-,-,2031,2031,2031,2031,2031,2031,2031,2032,2031,2031,2038,2038,2038,2038,2038,2038,2038,2038,2038,2039,2038,2038,2048,2048,2048,2048,2048,2048,2048,2048,2048,2049,2050,2052,2053,2054,2055,2056,2058,2059,2060,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2079,2080,2081,2082,2083,2084,2085,2086,2081,2081,2081,2081,2081,2087,2087,2088,2089,2087,2090,2091,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2048,2048,0,2,3,3,4,6,7,13,14,15,16,18,19,20,22,23,29,30,31,33,34,35,36,37,38,39,40,41,42,43,44,45,46,61,65,70,72,73,74,75,76,77,78,79,80,81,83,84,91,92,95,98,99,103,104,105,106,109,111,112,113,114,115,119,120,121,122,123,124,125,126,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,130,130,130,130,130,152,152,153,154,152,155,156,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,161,161,161,161,161,198,198,199,200,198,201,202,204,205,206,207,209,210,211,212,214,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,274,275,276,276,277,278,278,278,279,280,281,282,283,284,285,286,299,302,303,304,305,306,307,308,309,310,311,312,313,317,318,319,320,321,322,323,325,326,328,330,331,332,334,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,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,428,428,428,428,428,430,430,431,430,432,433,434,435,436,437,440,441,442,444,445,446,447,448,449,450,451,452,453,457,459,460,461,462,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,510,511,512,513,514,515,516,517,518,519,521,522,523,524,525,527,528,529,530,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,568,569,570,571,572,578,579,580,581,582,583,584,585,586,587,588,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,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,646,647,648,649,650,651,652,653,659,660,661,663,664,665,668,669,670,671,672,673,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,696,697,698,699,700,702,703,706,707,708,709,710,711,712,713,716,717,718,719,720,721,722,723,724,727,728,729,731,732,733,734,735,736,737,740,741,742,743,745,746,749,750,751,752,753,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,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,862,863,864,865,866,868,869,870,871,872,873,874,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,906,908,909,910,911,912,913,914,915,916,917,918,922,923,924,925,926,927,928,929,930,931,932,933,934,935,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,960,961,962,963,964,965,966,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1050,1051,1052,1053,1054,1055,1056,1057,1058,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1079,1080,1081,1082,1083,1084,1086,1087,1088,1089,1090,1091,1092,1093,1094,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1147,1148,1149,1150,1151,1152,1154,1155,1156,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1196,1197,1198,1199,1200,1201,1202,1203,1205,1206,1207,1208,1209,1210,1211,1211,1212,1213,1214,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1265,1266,1267,1268,1271,1272,1273,1274,1275,1276,1278,1279,1280,1282,1283,1284,1286,1287,1288,1289,1290,1291,1292,1294,1295,1296,1297,1298,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1317,1318,1319,1320,1321,1322,1323,1329,1330,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1345,1345,1345,1345,1345,1360,1360,1361,1360,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1338,1338,1338,1338,1338,1377,1377,1378,1379,1380,1377,1381,1382,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1410,1411,1412,1413,1414,1415,1416,1395,1395,1395,1395,1395,1417,1417,1418,1419,1417,1420,1421,1422,1423,1424,1425,1427,1428,1429,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1454,1454,1454,1454,1454,1465,1465,1466,1467,1465,1468,1469,1470,1471,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1554,1554,1554,1554,1554,1556,1556,1557,1556,1558,1559,1560,1561,1562,1563,1564,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1584,1585,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1602,1602,1602,1602,1602,1622,1622,1623,1624,1622,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1639,1640,1641,1642,1643,1644,1645,1646,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1686,1686,1686,1686,1686,1693,1693,1694,1695,1696,1693,1697,1701,1702,1703,1704,1705,1706,1707,1708,1709,1711,1712,1713,1714,1715,1716,1718,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1754,1755,1757,1758,1759,1760,1762,1763,1764,1765,1767,1768,1769,1771,1772,1773,1775,1776,1777,1779,1780,1781,1783,1784,1785,1786,1787,1788,1789,1790,1791,1791,1792,1793,1793,1795,1796,1797,1798,1799,1800,1801,1803,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1818,1818,1818,1818,1818,1820,1820,1821,1822,1820,1823,1824,1826,1830,1831,1832,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1847,1848,1849,1850,1851,1852,1853,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1871,1872,1873,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1916,1917,1918,1919,1920,1921,1922,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1947,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2029,2029,2030,2033,2034,2035,2036,2036,2036,2037,2040,2041,2042,2043,2043,2043,2044,2045,2046,2047,2047,2047,2047,2107,2108,2109,2110,2111,2111,2112,2113,2114,2115,2116,2117,2118,2118,2118,2119,2120,2121,2122,2123,2124,2125,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2157,2159,2160,2162,2163,2164,2165,2166,2167,2168,2170,2171,2172,2173,2174,2175,2176,2178,2179,2180,2181,2182,2185,2186,2187,2188,2189
package netutils
import codec.json.value as json_value
import network.*, regex
import codec.json
constant server_name = "CovScript-NetUtils"
constant server_version = "2.2"
# ============================================================================
# Configuration — override before use
# ============================================================================
constant http_client_read_chunk = 8192 # bytes per client recv() call
constant http_max_header_line_size = 8192 # single header line
constant http_max_header_size = 65536 # total header block
constant http_max_body_size = 67108864 # buffered body limit (64 MiB)
constant framing_hex_size = 16 # must match C++ NETWORK_FIXED_HEX_SIZE
constant default_http_port = 80
constant default_https_port = 443
constant default_http_thread_count = 4 # Asio I/O threads
constant default_http_worker_count = 32 # per-process HTTP handler fibers
# ============================================================================
# Utilities
# ============================================================================
var request_line_reg = regex.build_optimize("^([A-Z]+) ([^ ?]+)(?:\\?([^ ]*))? HTTP/([0-9.]+)$")
var request_header_reg = regex.build_optimize("^([^:]*): ?(.*)$")
var keep_alive_replace_reg = regex.build_optimize("Connection: keep-alive")
namespace state_codes
constant code_200 = "200 OK"
constant code_400 = "400 Bad Request"
constant code_403 = "403 Forbidden"
constant code_404 = "404 Not Found"
constant code_408 = "408 Request Timeout"
constant code_413 = "413 Payload Too Large"
constant code_429 = "429 Too Many Requests"
constant code_431 = "431 Request Header Fields Too Large"
constant code_500 = "500 Internal Server Error"
constant code_502 = "502 Bad Gateway"
constant code_503 = "503 Service Unavailable"
constant code_eof = "000 End of file"
end
var status_code_map = {
200: state_codes.code_200,
400: state_codes.code_400,
403: state_codes.code_403,
404: state_codes.code_404,
408: state_codes.code_408,
413: state_codes.code_413,
429: state_codes.code_429,
431: state_codes.code_431,
500: state_codes.code_500,
502: state_codes.code_502,
503: state_codes.code_503,
0: state_codes.code_eof
}.to_hash_map()
var wday_map = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
}
var mon_map = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
}
function time_padding(obj, width)
var time = to_string(obj)
var last = width - time.size
if last <= 0
return time
end
var str = new string
foreach it in range(last) do str.append("0")
return str + time
end
function compose_http_time()
var tm = runtime.utc_time()
return wday_map[tm.wday] + ", " +
time_padding(tm.mday, 2) + " " +
mon_map[tm.mon] + " " +
to_string(1900 + tm.year) + " " +
time_padding(tm.hour, 2) + ":" +
time_padding(tm.min, 2) + ":" +
time_padding(tm.sec, 2) + " GMT"
end
# Build a minimal HTTP response (always Connection: close, empty body).
function compose_response(code)
return "HTTP/1.1 " + code + "\r\n" +
"Connection: close\r\n" +
"Content-Length: 0\r\n\r\n"
end
# Send an HTTP error response and wait for the write to complete.
# Consolidates the repeated async.write + compose_response + wait pattern.
function send_error_response(sock, code)
var state = async.write(sock, compose_response(code))
state.wait()
end
# Set to an iostream.fstream to enable server-side logging.
var log_stream = null
function log(msg)
if log_stream != null
log_stream.println("[" + compose_http_time() + "]: " + msg)
end
end
# Send a framed message and wait for completion.
# Returns true on success, false on I/O error.
function send_content(sock, content)
var state = async.write(sock, to_fixed_hex(content.size) + content)
if !state.wait()
log("send_content failed: " + state.get_error())
return false
end
return true
end
# Read a framed message (no timeout). Returns payload or null.
function receive_content(sock)
try
var state = async.read(sock, framing_hex_size)
if !state.wait()
log("Read content header failed: " + state.get_error())
return null
end
var hex_str = state.get_result()
if hex_str.empty()
log("Read content header failed: empty hex string")
return null
end
var size = from_fixed_hex(hex_str)
if size <= 0 || size > http_max_body_size
log("Read content header failed: invalid frame size " + to_string(size))
return null
end
state = async.read(sock, size)
if !state.wait()
log("Read content body failed: " + state.get_error())
return null
end
return state.get_result()
catch e
log("receive_content exception: " + e.what())
return null
end
end
# Read a framed message with timeout. Returns {error_code, payload}.
function receive_content_s(sock, timeout)
var start_time = runtime.time()
try
var state = async.read(sock, framing_hex_size)
if !state.wait_for(timeout)
if state.has_done()
log("Read content header failed: " + state.get_error())
return {state_codes.code_500, null}
else
log("Read content header failed: Timeout")
return {state_codes.code_408, null}
end
end
var hex_str = state.get_result()
if hex_str.empty()
log("Read content header failed: empty hex string")
return {state_codes.code_500, null}
end
var size = from_fixed_hex(hex_str)
if size <= 0 || size > http_max_body_size
log("Read content header failed: invalid frame size " + to_string(size))
return {state_codes.code_500, null}
end
state = async.read(sock, size)
var remain = timeout - (runtime.time() - start_time)
if remain <= 0
log("Read content body failed: Timeout")
return {state_codes.code_408, null}
end
if !state.wait_for(remain)
if state.has_done()
log("Read content body failed: " + state.get_error())
return {state_codes.code_500, null}
else
log("Read content body failed: Timeout")
return {state_codes.code_408, null}
end
end
return {null, state.get_result()}
catch e
log("receive_content_s exception: " + e.what())
return {state_codes.code_500, null}
end
end
function read_file(path)
var ifs = iostream.fstream(path, iostream.openmode.bin_in)
var data = new string
while ifs.good()
# Read 256KB per iteration
data.append(runtime.await(ifs.read, 262144))
end
return move(data)
end
var path_extractor = regex.build_optimize("^([A-Za-z]:)")
# Normalize a filesystem path: resolve . and .., convert separators.
function path_normalize(path)
if path == null || path.empty()
return "."
end
var alt_sep = (system.path.separator == '/' ? '\\' : '/')
var is_unc = false
var is_abs = false
var drive_letter = ""
var norm_path = new string
foreach ch in path
if ch == alt_sep
norm_path.append('/')
else
norm_path.append(ch)
end
end
context.unlink_var("path")
path = norm_path
if system.path.separator == '\\'
if path.find("//", 0) == 0 # UNC Path
is_unc = true
path = path.substr(2, path.size - 2)
else
var m = path_extractor.match(path)
if !m.empty()
drive_letter = m.str(1)
path = path.substr(drive_letter.size, path.size - drive_letter.size)
is_abs = true
end
end
is_abs = is_abs or is_unc
else
if path.find("/", 0) == 0
is_abs = true
end
end
var components = path.split({'/'})
var stack = new array
foreach comp in components
if comp.empty() or comp == "."
continue
end
if comp == ".."
if stack.size > 0 and stack.back != ".."
stack.pop_back()
else
if !is_abs
stack.push_back("..")
end
end
else
stack.push_back(comp)
end
end
var result = new string
if is_unc
result = "//"
else if drive_letter != ""
result = drive_letter
else if is_abs
result = "/"
end
foreach comp in stack
if !result.empty() and result[-1] != '/'
result.append("/")
end
result.append(comp)
end
return result
end
var mime_map = {
".html": "text/html",
".htm": "text/html",
".txt": "text/plain",
".js": "application/javascript",
".css": "text/css",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".json": "application/json"
} as hash_map
# Map file extension to MIME type.
function get_mime(path)
var pos = path.rfind(".", path.size - 1)
if pos == -1
return "application/octet-stream"
end
var ext = path.substr(pos, path.size - pos).tolower()
if mime_map.exist(ext)
return mime_map[ext]
else
return "application/octet-stream"
end
end
# Parsed HTTP request. In single-process mode write_response is async.write;
# in multi-process mode deserialize() swaps it to send_content.
class http_session
var url = null
var args = null
var host = null
var method = null
var version = null
var connection = null
# POST data
var post_data = null
var content_length = null
# Original request headers (for proxy forwarding)
var request_headers = null
# for handler
var sock = null
var write_response = async.write
var response_state = null
# for multi-node setup
var response = null
# Master-side dispatch retry counter (not serialized).
var dispatch_attempts = 0
function send_response(code, data, type)
var content_len = to_string(data.size)
var resp = new string
resp.append("HTTP/" + version + " " + code + "\r\n")
resp.append("Date: " + compose_http_time() + "\r\n")
resp.append("Server: " + server_name + "/" + server_version + "\r\n")
resp.append("Connection: " + connection + "\r\n")
resp.append("Content-Length: " + content_len + "\r\n")
resp.append("Content-Type: " + type + "\r\n")
resp.append("\r\n")
resp.append(data)
response_state = write_response(sock, resp)
end
function serialize()
var obj = json_value.make_object()
obj.set_member("url", json_value.make_string(url))
obj.set_member("args", json_value.make_string(args))
obj.set_member("host", json_value.make_string(host))
obj.set_member("method", json_value.make_string(method))
obj.set_member("version", json_value.make_string(version))
obj.set_member("connection", json_value.make_string(connection))
if content_length != null && content_length > 0
obj.set_member("content_length", json_value.make_int(content_length))
end
if request_headers != null
var hdrs = json_value.make_object()
foreach it in request_headers
hdrs.set_member(it.first, json_value.make_string(it.second))
end
obj.set_member("request_headers", hdrs)
end
return json.to_string(obj)
end
function deserialize(data)
var obj = json.from_string(data)
url = obj.get_member("url").as_string()
args = obj.get_member("args").as_string()
host = obj.get_member("host").as_string()
method = obj.get_member("method").as_string()
version = obj.get_member("version").as_string()
connection = obj.get_member("connection").as_string()
if obj.get_member("content_length") != null
content_length = obj.get_member("content_length").as_int()
end
if obj.get_member("request_headers") != null
request_headers = new hash_map
var hdrs = obj.get_member("request_headers")
var keys = hdrs.get_member_names()
foreach key in keys
request_headers[key] = hdrs.get_member(key).as_string()
end
end
write_response = send_content
end
end
# Parse raw request header lines into an http_session. Returns null on rejection.
function create_http_session(request_header)
var session = new http_session
var request_line = request_header.pop_front()
var match = request_line_reg.match(request_line)
if !match.empty()
session.method = match.str(1)
session.url = match.str(2)
session.args = match.str(3)
session.version = match.str(4)
else
return null
end
if session.url.empty()
session.url = "/"
end
var has_content_length = false
session.request_headers = new hash_map
foreach line in request_header
var match = request_header_reg.match(line)
if !match.empty()
var header_name = match.str(1).tolower()
var header_value = match.str(2)
switch header_name
case "host"
session.host = header_value
end
case "connection"
session.connection = header_value.tolower()
end
case "content-length"
if has_content_length
return null
end
has_content_length = true
try
session.content_length = header_value as integer
catch e
return null
end
if session.content_length < 0 || session.content_length > http_max_body_size
return null
end
end
case "transfer-encoding"
# Chunked request bodies are not implemented. Ignoring this
# header would leave bytes to be parsed as a new request.
return null
end
default
# Store all other headers for proxy forwarding
session.request_headers[header_name] = header_value
end
end
end
end
if session.connection == null || session.connection.empty()
session.connection = (session.version <= "1.0" ? "close" : "keep-alive")
end
return move(session)
end
# Read and parse one HTTP request from sock. Handles timeouts, size limits,
# and body consumption. Sends error response on failure. Returns session or null.
function read_http_header(sock, state, keep_alive_timeout, max_body_size)
# Read HTTP headers
var header = new array
var error_code = null
var header_size = 0
loop
# Check the deadline before starting the read: breaking with a
# just-started async op pending would leave it dangling past shutdown.
var timeout = keep_alive_timeout - runtime.time()
if timeout <= 0
error_code = state_codes.code_408
break
end
async.read_until(sock, state, "\r\n")
if !state.wait_for(timeout)
if state.has_done()
if state.eof()
log("Read request header: End of file")
error_code = state_codes.code_eof
else
log("Read request header error: " + state.get_error())
error_code = state_codes.code_500
end
else
log("Read request header error: Keep-alive timeout.")
error_code = state_codes.code_408
end
break
end
var raw_line = state.get_result()
header_size += raw_line.size
if raw_line.size > http_max_header_line_size || header_size > http_max_header_size
log("Read request header error: Header limit exceeded.")
error_code = state_codes.code_431
break
end
var line = raw_line.trim()
if line.empty()
break
end
header.push_back(move(line))
end
if error_code == null && header.empty()
log("Read request header error: Empty request.")
error_code = state_codes.code_400
end
if error_code != null
if error_code != state_codes.code_eof
send_error_response(sock, error_code)
end
return null
end
# Construct HTTP session
var session = create_http_session(header)
if session == null
log("Parse request header error.")
send_error_response(sock, state_codes.code_400)
return null
end
log("Received: Method = " + session.method + ", URL = " + session.url + ", Host = " + session.host + ", Connection = " + session.connection)
if session.connection == "keep-alive"
sock.set_opt_keep_alive(true)
end
# Reject oversized request bodies before buffering
if session.content_length != null && session.content_length > max_body_size
log("Request body too large: " + to_string(session.content_length) + " > " + to_string(max_body_size))
send_error_response(sock, state_codes.code_413)
return null
end
# Consume framed request data for every method so keep-alive parsing stays aligned.
if session.content_length != null && session.content_length > 0
session.post_data = state.get_buffer(session.content_length)
var remaining = session.content_length - session.post_data.size
while remaining > 0
# Same as above: check the deadline before starting the read.
var timeout = keep_alive_timeout - runtime.time()
if timeout <= 0
error_code = state_codes.code_408
break
end
state = async.read(sock, remaining)
if !state.wait_for(timeout)
if state.has_done()
if state.eof()
log("Read POST body: End of file")
error_code = state_codes.code_eof
else
log("Read POST body error: " + state.get_error())
error_code = state_codes.code_500
end
else
log("Read POST body error: Keep-alive timeout.")
error_code = state_codes.code_408
end
break
end
session.post_data.append(state.get_result())
remaining = session.content_length - session.post_data.size
end
if error_code != null
if error_code != state_codes.code_eof
send_error_response(sock, error_code)
end
return null
end
end
return move(session)
end
# Route session to the matching handler. Falls back to wwwroot static serving.
# Returns true if a handler was invoked successfully, false on error.
function call_http_handler(session, server)
var error_code = null
if server->url_map.exist(session.url)
server->url_map[session.url](*server, session)
else
# Check for prefix match (used by bind_proxy).
# Sort candidate keys by length descending so the longest prefix wins.
# Enforce a path-component boundary to prevent /api from matching
# /api-v2: either the key ends with '/' already, or the character
# after the prefix must be '/' or end-of-string.
var candidates = new array
foreach it in server->url_map
var key = it.first
if session.url.find(key, 0) == 0
var after_size = session.url.size - key.size
if after_size == 0 || key[-1] == '/' || session.url.substr(key.size, 1) == "/"
candidates.push_back(key)
end
end
end
if !candidates.empty()
# Sort by length descending — longest prefix wins
var changed = true
while changed
changed = false
var i = 0
while i + 1 < candidates.size
if candidates[i].size < candidates[i + 1].size
var tmp = candidates[i]
candidates[i] = candidates[i + 1]
candidates[i + 1] = tmp
changed = true
end
i += 1
end
end
server->url_map[candidates[0]](*server, session)
else
if server->wwwroot_path != null
var base_path = server->wwwroot_path
var full_path = path_normalize(base_path + "/" + session.url)
log("Resolved path: " + full_path)
# Boundary check: accept only exact match or base_path followed by '/'
# to prevent sibling-directory escape (e.g. www_evil matching root www).
if full_path == base_path || full_path.find(base_path + "/", 0) == 0
if system.path.is_directory(full_path)
if full_path[-1] != '/'
full_path.append('/')
end
full_path.append("index.html")
log("Directory request, try to serve: " + full_path)
end
if system.path.is_file(full_path) && system.file.can_read(full_path)
log("Serving file: " + full_path)
session.send_response(state_codes.code_200, server->read_file(full_path), get_mime(full_path))
else
if system.file.exist(full_path)
error_code = state_codes.code_403
else
error_code = state_codes.code_404
end
end
else
error_code = state_codes.code_403
end
else
error_code = state_codes.code_403
end
end
end
if error_code != null
log("Resolving resource error: " + error_code)
if server->url_map.exist(error_code)
server->url_map[error_code](*server, session)
else
# Route through the session so multi-process mode sends a framed
# response to the master instead of raw HTTP bytes on the
# slave<->master socket (which would desynchronize the framing).
session.connection = "close"
session.send_response(error_code, "", "text/html")
end
return false
else
return true
end
end
# ============================================================================
# Workers (single-process: coroutine per connection)
# ============================================================================
struct worker_type
var co = null
var rank = 0
# -1 = error, 0 = ready, 1 = wait, 2 = busy
var state = 0
var server = null
end
# Single-process worker: accept, read, handle, write, keep-alive loop.
function simple_worker(self)
loop
if self->server->stopped
return
end
self->state = 1
# Accept new connection
var sock = new tcp.socket
var state = async.accept(sock, self->server->acceptor)
if !state.wait()
log("Accept error: " + state.get_error())
continue
end
self->state = 2
sock.set_opt_no_delay(true)
var last_request_time = runtime.time()
var request_count = 0
var read_state = new async.state
loop
if self->server->stopped
break
end
var session = read_http_header(sock, read_state, last_request_time + self->server->keep_alive_timeout, self->server->max_body_size)
if session == null
break
end
# Close cleanly once max_keep_alive requests are served: advertise
# Connection: close on the final response
var force_close = false
if ++request_count >= self->server->max_keep_alive
session.connection = "close"
force_close = true
end
# Call handler
session.sock = sock
var handler_ok = call_http_handler(session, self->server)
# Wait for the response write (including error responses sent on
# handler failure) before deciding the connection's fate.
if session.response_state != null && !session.response_state.wait()
log("Write response error: " + session.response_state.get_error())
break
end
if !handler_ok
break
end
last_request_time = runtime.time()
# Keep-Alive check — force_close guards against handlers that
# overwrite session.connection after the limit was reached
if !sock.is_open() || force_close || session.connection == "close"
break
end
end
if !sock.safe_shutdown()
log("safe_shutdown returned false — async jobs may still be pending")
end
end
end
# Per-connection state for the master process.
struct http_conn
var sock = null
var read_state = null
# -1 = close, 0 = established, 1 = busy
var state = 0
var keep_alive = true
var request_count = 0
var last_request_time = 0
var request_idx = 0
var request_queue = new array
end
# Per-slave state tracked by the master.
struct slave_node
var last_conn_time = null
var sock = null
var rank = null
# -1 = error, 0 = ready, 1 = busy
var state = 0
end
# Accept new slave connections and perform handshake.
function master_spawn_worker(self)
loop
if self->server->stopped
return
end
# Accept new connection
log("Accepting new worker...")
var sock = new tcp.socket
var state = async.accept(sock, self->server->master_acceptor)
if !state.wait()
log("Master accept error: " + state.get_error())
continue
end
sock.set_opt_no_delay(true)
sock.set_opt_keep_alive(true)
var node = gcnew slave_node
node->sock = sock
if !self->server->deprecated_rank.empty()
node->rank = self->server->deprecated_rank.pop_front()
else
node->rank = self->server->slave_list.size
end
log("Starting handshake with worker rank " + node->rank)
# Start handshake
var rank_str = to_string(node->rank)
if !send_content(sock, {"SERVER", server_version, rank_str}.join(" "))
log("Handshake error: send_content failed")
sock.safe_shutdown()
continue
end
var error_code = null, response = null
(error_code, response) = receive_content_s(sock, self->server->slave_spawn_timeout)
if error_code != null
log("Handshake timeout or error.")
sock.safe_shutdown()
continue
end
var header = response.split({' '})
if header.size != 3 || header[0] != "WORKER" || header[1] != server_version || header[2] != rank_str
log("Handshake error: Invalid response: " + header)
sock.safe_shutdown()
continue
end
node->last_conn_time = runtime.time()
self->server->slave_list.push_back(node)
log("Worker rank " + to_string(node->rank) + " connected, total workers = " + to_string(self->server->slave_list.size))
end
end
# Accept HTTP client connections and push onto conn_list.
function master_accept_worker(self)
loop
if self->server->stopped
return
end
if self->server->conn_list.size >= self->server->max_connections
fiber.yield()
continue
end
log("Accepting new HTTP request..")
var sock = new tcp.socket
var state = async.accept(sock, self->server->acceptor)
if !state.wait()
log("Accept error: " + state.get_error())
continue
end
sock.set_opt_no_delay(true)
var conn = gcnew http_conn
conn->sock = sock
conn->read_state = new async.state
conn->last_request_time = runtime.time()
self->server->conn_list.push_back(conn)
end
end
# Read HTTP requests from connections and enqueue for dispatch.
function master_request_worker(self)
link clist = self->server->conn_list
loop
if self->server->stopped
return
end
var conn = null
for it = clist.begin, it != clist.end, null
var c = it.data
if c->state == -1 || !c->sock.is_open()
it = clist.erase(it)
continue
end
if c->state == 0 && c->keep_alive
clist.erase(it)
clist.push_back(c)
conn = c
break
end
it.next()
end
if conn == null
fiber.yield()
continue
end
conn->state = 1
var sock = conn->sock
var session = read_http_header(sock, conn->read_state, conn->last_request_time + self->server->keep_alive_timeout, self->server->max_body_size)
if session == null
if !sock.safe_shutdown()
log("safe_shutdown returned false — async jobs may still be pending")
end
conn->state = -1
continue
end
# Check keep-alive — read_http_header already enforces the timeout;
# only the per-connection request counter needs checking here.
if ++conn->request_count >= self->server->max_keep_alive
log("Keep-alive exceeded max request count.")
session.connection = "close"
end
conn->last_request_time = runtime.time()
# Check connection type
if session.connection == "close"
conn->keep_alive = false
end
conn->request_queue.push_back(move(session))
conn->state = 0
end
end
# Write completed responses back to clients; closes connections
# that have exhausted keep-alive.
function master_response_worker(self)
link clist = self->server->conn_list
loop
if self->server->stopped
return
end
if clist.empty()
fiber.yield()
continue
end
var conn = null
foreach it in clist
if !it->request_queue.empty()
conn = it
break
end
end
if conn == null
fiber.yield()
continue
end
while !conn->request_queue.empty()
link session = conn->request_queue.front
if session != null && session.response != null
# When keep-alive is disabled the connection must close
# regardless of what the slave handler wrote. Force the
# header to prevent a mismatch between the response and
# the actual connection lifecycle.
if !conn->keep_alive
session.response = regex.replace(keep_alive_replace_reg,
session.response, "Connection: close")
end
var response_state = async.write(conn->sock, session.response)
if !response_state.wait()
log("Write response error: " + response_state.get_error())
conn->keep_alive = false
conn->request_queue = new array
break
end
conn->request_queue.pop_front()
--conn->request_idx
# Restart the keep-alive idle window only once the response is
# written, matching simple_worker: slave processing time must
# not eat into the client's idle allowance.
conn->last_request_time = runtime.time()
else
fiber.yield()
break
end
end
if conn->request_queue.empty() && !conn->keep_alive
if !conn->sock.safe_shutdown()
log("safe_shutdown returned false — async jobs may still be pending")
end
conn->state = -1
end
end
end
# Scan the slave list once: retire dead nodes (recycling their rank), rotate
# a ready node to the back of the list and return it, or null if none ready.
# The caller is responsible for marking the returned node busy (state = 1).
function master_pick_slave(self)
link slist = self->server->slave_list
for it = slist.begin, it != slist.end, null
var n = it.data
if n->state == -1
self->server->deprecated_rank.push_back(n->rank)
it = slist.erase(it)
continue
end
if n->state == 0
slist.erase(it)
slist.push_back(n)
return n
end
it.next()
end
return null
end
# Send queued requests to available slaves; heartbeat idle slaves.
function master_dispatch_worker(self)
loop
if self->server->stopped
return
end
var node = master_pick_slave(self)
if node == null
# Pure yield — runtime.delay would block the whole OS thread
# (all fibers), collapsing dispatch throughput whenever the
# slave pool is busy or empty.
fiber.yield()
continue
end
var conn = null
foreach it in self->server->conn_list
if it->request_idx < it->request_queue.size
conn = it
break
end
end
var timeout = node->last_conn_time + self->server->slave_keep_alive_timeout - runtime.time()
if timeout <= 0
timeout = self->server->slave_keep_alive_timeout
end
if conn == null
# Departure queue empty, send heartbeat
if runtime.time() - node->last_conn_time >= self->server->heartbeat_interval
node->state = 1
send_content(node->sock, "SLAVE_HEALTH_QUERY")
var (error_code, response) = receive_content_s(node->sock, timeout)
if error_code == null && response == "SLAVE_HEALTH_CONFIRM"
log("Heartbeat success for worker rank " + node->rank)
node->last_conn_time = runtime.time()
node->state = 0
else
log("Heartbeat failed for worker rank " + node->rank)
if !node->sock.safe_shutdown()
log("safe_shutdown returned false — async jobs may still be pending")
end