vibe.http.server 94/643(14%) line coverage

      
10
20
30
40
50
60
70
80
90
100
110
120
130
140
150
160
170
180
190
200
210
220
230
240
250
260
270
280
290
300
310
320
330
340
350
360
370
380
390
400
410
420
430
440
450
460
470
480
490
500
510
520
530
540
550
560
570
580
590
600
610
620
630
640
650
660
670
680
690
700
710
720
730
740
750
760
770
780
790
800
810
820
830
840
850
860
870
880
890
900
910
920
930
940
950
960
970
980
990
1000
1010
1020
1030
1040
1050
1060
1070
1080
1090
1100
1110
1120
1130
1140
1150
1160
1170
1180
1190
1200
1210
1220
1230
1240
1250
1260
1270
1280
1290
1300
1310
1320
1330
1340
1350
1360
1370
1380
1390
1400
1410
1420
1430
1440
1450
1460
1470
1480
1490
1500
1510
1520
1530
1540
1550
1560
1570
1580
1590
1600
1610
1620
1630
1640
1650
1660
1670
1680
1690
1700
1710
1720
1730
1740
1750
1760
1770
1780
1790
1800
1810
1820
1830
1840
1850
1860
1870
1880
1890
1900
1910
1920
1930
1940
1950
1960
1970
1980
1990
2000
2010
2020
2030
2040
2050
2060
2070
2080
2090
2100
2110
2120
2130
2140
2150
2160
2170
2180
2190
2200
2210
2220
2230
2240
2250
2260
2270
2280
2290
2300
2310
2320
2330
2340
2350
2360
2370
2380
2390
2400
2410
2420
2430
2440
2450
2460
2470
2480
2490
2500
2510
2520
2530
2540
2550
2560
2570
2580
2590
2600
2610
2620
2630
2640
2650
2660
2670
2680
2690
2700
2710
2720
2730
2740
2750
2760
2770
2780
2790
2800
2810
2820
2830
2840
2850
2860
2870
2880
2890
2900
2910
2920
2930
2940
2950
2960
2970
2980
2990
3000
3010
3020
3030
3040
3050
3060
3070
3080
3090
3100
3111
3121
3130
3140
3150
3160
3170
3180
3190
3200
3210
3220
3230
3240
3250
3260
3270
3280
3290
3300
3310
3320
3330
3340
3350
3360
3370
3380
3390
3400
3410
3420
3430
3440
3450
3460
3470
3480
3490
3500
3510
3520
3530
3540
3550
3560
3570
3580
3590
3600
3610
3620
3630
3640
3650
3660
3670
3680
3690
3700
3710
3720
3730
3740
3750
3760
3770
3780
3790
3800
3810
3820
3830
3840
3850
3860
3870
3880
3890
3900
3910
3920
3930
3940
3950
3960
3970
3980
3990
4000
4010
4020
4030
4040
4051
4061
4071
4081
4090
4100
4110
4120
4130
4140
4150
4160
4170
4180
4190
4200
4210
4220
4230
4240
4250
4260
4270
4280
4290
4300
4310
4320
4330
4340
4350
4360
4370
4380
4390
4400
4410
4420
44326
44426
4450
4460
4470
4480
44926
45052
45126
45226
45326
45426
45526
45626
45726
45826
45926
46026
4610
4620
4630
4640
4650
4660
4670
4680
4690
47026
47126
4720
4730
4740
47526
47626
47752
47826
4790
4800
4810
4820
4830
4840
4850
4860
4870
4880
4890
4900
4910
4920
4930
4940
4950
4960
4970
4980
4990
5000
5010
5020
5030
5040
5050
5060
5070
5080
5090
5100
5110
5120
5130
5140
5150
5160
5170
5180
5190
5200
5210
5220
5230
5240
5250
5260
5270
5280
5290
5300
5310
5320
5330
5340
5350
5360
5370
5380
5390
5400
5410
5420
5430
5440
5450
5460
5470
5480
5490
5500
5510
5520
5530
5540
5550
5560
5570
5580
5590
5600
5610
5620
5630
5640
5650
5660
5670
5680
5690
5700
5710
5720
5730
5740
5750
5760
5770
5780
5790
5800
5810
5820
5830
5840
5850
5860
5870
5880
5890
5900
5910
5920
5930
5940
5950
5960
5970
5980
5990
6000
6010
6020
6030
6040
6050
6060
6070
6080
6090
6100
6110
6120
6130
6140
6150
6160
6170
6180
6190
6200
6210
6220
6230
6240
6250
6260
6270
6280
6290
6300
6310
6320
6330
6340
6350
6360
6370
6380
6390
6400
6410
6420
6430
6440
6450
6460
6470
6480
6490
6500
6510
6520
6530
6540
6550
6560
6570
6580
6590
6600
6610
6620
6630
6640
6650
6660
6670
6680
6690
6700
6710
6720
6730
6740
6750
6760
6770
6780
6790
6800
6810
6820
6830
6840
6850
6860
6870
6880
6890
6900
6910
6920
6930
6940
6950
6960
6970
6980
6990
7000
7010
7020
7030
7040
7050
7060
7070
7080
7090
7100
7110
7120
7130
7140
7150
7160
7170
7180
7190
7200
7210
7220
7230
7240
7250
7260
7270
7280
7290
7300
7310
7320
7330
7340
7350
7360
7370
7380
7390
7400
7410
7420
7430
7440
7450
7460
7470
7480
7490
7500
7510
7520
7530
7540
7550
7560
7570
7580
7590
7600
7610
7620
7630
7640
7650
7660
7670
7680
7690
7700
7710
7720
7730
7740
7750
7760
7770
7780
7790
7800
7810
7820
7830
7840
7850
7860
7870
7880
7890
7900
7910
7920
7930
7940
7950
7960
7970
7980
7990
8000
8010
8020
8030
8040
8050
8060
8070
8080
8090
8100
8110
8120
8130
8140
8150
8160
8170
8180
8190
8200
8210
8220
8230
8240
8250
8260
8270
8280
8290
8300
8310
8320
8330
8340
8350
8360
8370
8380
8390
8400
8410
8420
8430
8440
8450
8460
8470
8480
8490
8500
8510
8520
8530
8540
8550
8560
8570
8580
8590
8600
8610
8620
8630
8640
8650
8660
8670
8680
8690
8700
8710
8720
8730
8740
8750
8760
8770
8780
8790
8800
8810
8820
8830
8840
8850
8860
8870
8880
8890
89043
89126
8920
89343
8940
8950
8960
8970
8980
8990
9000
9010
9020
9030
9040
9050
9060
9070
9080
9090
9100
9110
9120
9130
9140
9150
9160
9170
9180
9190
9200
9210
9220
9230
9240
9250
9260
9270
9280
9290
9300
9310
9320
9330
9340
9350
9360
9370
9380
9390
9400
9410
9420
9430
9440
9450
9460
9470
9480
9490
9500
9510
9520
9530
9540
9550
9560
9570
9580
9590
9600
9610
9620
9630
9640
9650
9660
9670
9680
9690
9700
9710
9720
9730
9740
9750
9760
9770
9780
9790
9800
9810
9820
9830
9842
9852
9860
9870
9880
9891
9900
9910
9922
9930
9940
9950
9960
9970
9980
9990
10000
10010
10020
10030
10040
10050
10062
10071
10080
10092
10100
10110
10120
10130
10140
10151
10161
10171
10180
10190
10200
10210
10220
10230
10240
10250
10260
10270
10280
10290
10300
10310
10320
10330
10340
10350
10360
10370
10380
10390
10400
10410
10420
10430
10440
10450
10460
10470
10480
10490
10500
10510
10520
10530
105426
10550
105626
105726
10580
10590
10600
10610
10620
10630
10640
10650
10660
10670
10680
10690
10700
10710
10720
10730
10740
10750
10760
10770
10780
10790
10800
10810
10820
10830
10840
10850
10860
10870
10880
10890
10900
10910
10920
10930
10940
10950
10960
10970
10980
10990
11000
11010
11020
11030
11040
11050
11060
11070
11080
11090
11100
11110
11120
11130
11140
11150
11160
11170
11180
11190
11200
11210
11220
11230
11240
11250
11260
11270
11280
11290
11300
11310
11320
11330
11340
11350
11360
11370
11380
11390
11400
11410
11420
11430
11440
11450
11460
11470
11480
11490
11500
11510
11520
11530
11540
11550
11560
11570
11580
11590
11600
11610
11620
11630
11640
11650
11660
11670
11680
11690
11700
11710
11720
11730
11740
11750
11760
11770
11780
11790
11800
118126
11820
118326
11840
11850
11860
118726
11880
118926
119026
119126
119226
119326
11940
11950
11960
11970
11980
11990
12000
12010
12020
12030
120417
12050
12060
12070
12080
12090
12100
12110
12120
12130
12140
12150
12160
12170
12180
12190
12200
12210
12220
12230
12240
12250
12260
12270
122824
12290
123012
123112
12320
12330
12340
12350
12360
12370
12380
12390
12400
12410
12420
12430
12440
12450
12460
12470
12480
12490
12500
12510
12520
12530
12540
12550
12560
12570
12580
12590
12600
12610
126224
126312
126412
12650
12660
12670
12680
12690
12700
12710
12720
12730
12740
12750
12760
12770
12780
12790
12800
12810
12820
12830
12840
12850
12860
12870
12880
12890
12900
12910
12920
12930
12940
12950
12960
12970
12980
12990
13000
13010
13020
13030
13040
13050
13060
13070
13080
13090
13100
13110
13120
13130
13140
13150
13160
13170
13180
13190
13200
13210
13220
13230
13240
13250
13260
13270
13280
13290
13300
13310
13320
13330
13340
13350
13360
13370
13380
13390
13400
13410
13420
13430
13440
13450
13460
13470
13480
13490
13500
13510
13520
13530
13540
13550
13560
13570
13580
13590
13600
13610
13620
13630
13640
13650
13660
13670
13680
13690
13700
13710
13720
13730
13740
13750
13760
13770
13780
13790
13800
13810
13820
13830
13840
13850
13860
13870
13880
13890
13900
13910
13920
13930
13940
13950
13960
13970
13980
13990
14000
14010
14020
14030
14040
14050
140613
140713
14080
140913
14100
141113
14120
14130
14140
14150
14160
14170
14180
14190
14200
14210
142213
14230
14240
14250
14260
14270
142813
142913
143013
143113
14320
14330
14340
14350
14360
14370
14380
14390
14400
14410
14420
14430
144413
14450
14460
14470
14480
14490
14500
14510
14520
14530
14540
14550
145613
14570
14580
14590
14600
14610
14620
14630
14640
14650
14660
14670
14680
14690
14700
14710
14720
14730
14740
14750
14760
14770
14780
14790
14800
14810
14820
14830
14840
14850
14860
14870
14880
14890
14900
14910
14920
14930
14940
14950
14960
14970
14980
14990
15000
15010
15020
15030
15040
15050
15060
15070
15080
15090
15100
15110
15120
15130
15140
15150
15160
15170
15180
15190
15200
15210
15220
15230
15240
15250
15260
15270
15280
15290
15300
15310
15320
15330
15340
15350
15360
15370
15380
15390
15400
15410
15420
15430
15440
15450
15460
15470
15480
15490
15500
15510
15520
15530
15540
15550
15560
15570
15580
15590
15600
15610
15620
15630
15640
15650
15660
15670
15680
15690
15700
15710
15720
15730
15740
15750
15760
15770
15780
15790
15800
15810
15820
15830
15840
15850
15860
15870
15880
15890
15900
15910
15920
15930
15940
15950
15960
15970
15980
15990
16000
16010
16020
16030
16040
16050
16060
16070
16080
16090
16100
16110
16120
16130
16140
16150
16160
16170
16180
16190
16200
16210
16220
16230
16240
16250
16260
16270
16280
16290
16300
16310
16320
16330
16340
16350
16360
16370
16380
16390
16400
16410
16420
16430
16440
16450
16460
16470
16480
16490
16500
16510
16520
16530
16540
16550
16560
16570
16580
16590
16600
16610
16620
16630
16640
16650
16660
16670
16680
16690
16700
16710
16720
16730
16740
16750
16760
16770
16780
16790
16800
16810
16820
16830
16840
168526
168613
168726
16880
168913
16900
169126
169213
169352
16940
16950
169613
169713
169813
16990
17000
170113
17020
17030
170413
17050
17060
1707181
170831
170931
171031
171131
171293
17130
17140
171513
17160
17170
171813
17190
17200
17210
17220
17230
17240
172513
17260
17270
17280
17290
17300
17310
17320
17330
17340
17350
17360
17370
17380
17390
17400
17410
17420
17430
17440
17450
17460
17470
17480
17490
17500
17510
17520
17530
17540
17550
17560
17570
17580
17590
17600
17610
17620
17630
17640
17650
17660
17670
17680
17690
17700
17710
17720
17730
17740
17750
17760
17770
17780
17790
17800
17810
17820
17830
17840
17850
17860
17870
17880
17890
17900
17910
17920
17930
17940
17950
17960
17970
17980
17990
18000
18010
18020
18030
18040
18050
18060
18070
18080
18090
18100
18110
18120
18130
18140
18150
18160
18170
18180
18190
18200
18210
18220
18230
18240
18250
18260
18270
18280
18290
18300
18310
18320
18330
18340
18350
18360
18370
18380
18390
18400
18410
18420
18430
18440
18450
18460
18470
18480
18490
18500
18510
18520
18530
18540
18550
18560
18570
18580
18590
18600
18610
18620
18630
18640
18650
18660
18670
18680
18690
18700
18710
18720
18730
18740
18750
18760
18770
18780
18790
18800
18810
18820
18830
18840
18850
18860
18870
18880
18890
18900
18910
18920
18930
18940
18950
18960
18970
18980
18990
19000
19010
19020
19030
19040
19050
19060
19070
19080
19090
19100
19110
19120
19130
19140
19150
19160
19170
19180
19190
19200
19210
19220
19230
19240
19250
19260
19270
19280
19290
19300
19310
19320
19330
19340
19350
19360
19370
19380
19390
19400
19410
19420
19430
19440
19450
19460
19470
19480
19490
19500
19510
19520
19530
19540
19550
19560
19570
19580
19590
19600
19610
19620
19630
19640
19650
19660
19670
19680
19690
19700
19710
19720
19730
19740
19750
19760
19770
19780
19790
19800
19810
19820
19830
19840
19850
19860
19870
19880
19890
19900
19910
19920
19930
19940
19950
19960
19970
19980
19990
20000
20010
20020
20030
20040
20050
20060
20070
20080
20090
20100
20110
20120
20130
20140
20150
20160
20170
20180
20190
20200
20210
20220
20230
20240
20250
20260
20270
20280
20290
20300
20310
20320
20330
20340
20350
20360
20370
20380
20390
20400
20410
20420
20430
20440
20450
20460
20470
20480
20490
20500
20510
20520
20530
20540
20550
20560
20570
20580
20590
20600
20610
20620
20630
20640
20650
20660
20670
20680
20690
20700
20710
20720
20730
20740
20750
20760
20770
20780
20790
20800
20810
20820
20830
20840
20850
20860
20870
20880
20890
20900
20910
20920
20930
20940
20950
20960
20970
20980
20990
21000
21010
21020
21030
21040
21050
21060
21070
21080
21090
21100
21110
21120
21130
21140
21150
21160
21170
21180
21190
21200
21210
21220
21230
21240
21250
21260
21270
21280
21290
21300
21310
21320
21330
21340
21350
21360
21370
21380
21390
21400
21410
21420
21430
21440
21450
21460
21470
21480
21490
21500
21510
21520
21530
21540
21550
21560
21570
21580
21590
21600
21610
21620
21630
21640
21650
21660
21670
21680
21690
21700
21710
21720
21730
21740
21750
21760
21770
21780
21790
21800
21810
21820
21830
21840
21850
21860
21870
21880
21890
21900
21910
21920
21930
21940
21950
21960
21970
21980
21990
22000
22010
22020
22030
22040
22050
22060
22070
22080
22090
22100
22110
22120
22130
22140
22150
22160
22170
22180
22190
22200
22210
22220
22230
22240
22250
22260
22270
22280
22290
22300
22310
22320
22330
22340
22350
22360
22370
22380
22390
22400
22410
22420
22430
22440
22450
22460
22470
22480
22490
22500
22510
22520
22530
22540
22550
22560
22570
22580
22590
22600
22610
22620
22630
22640
22650
22660
22670
22680
22690
22700
22710
22720
22730
22740
22750
22760
22770
22780
22790
22800
22810
22820
22830
22840
22850
22860
22870
22880
22890
22900
22910
22920
22930
22940
22950
22960
22970
22980
22990
23000
23010
23020
23030
23040
23050
23060
23070
23080
23090
23100
23110
23120
23130
23140
23150
23160
23170
23180
23190
23200
23210
23220
23230
23240
23250
23260
23270
23280
23290
23300
23310
23320
23330
23340
23350
23360
23370
23380
23390
23400
23410
23420
23430
23440
23450
23460
23470
23480
23490
23500
23510
23520
23530
23540
23550
23560
23570
23580
23590
23600
23610
23620
23630
23640
23650
23660
23670
23680
23690
23700
23710
23720
23730
23740
23750
23761
23771
23780
23792
23802
23811
23820
23830
23840
23850
23860
23870
23880
23890
23900
23910
23920
23930
/** A HTTP 1.1/1.0 server implementation. Copyright: © 2012-2017 RejectedSoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig, Jan Krüger, Ilya Shipunov */ module vibe.http.server; public import vibe.core.net; public import vibe.http.common; public import vibe.http.session; import vibe.core.file; import vibe.core.log; import vibe.data.json; import vibe.http.dist; import vibe.http.log; import vibe.inet.message; import vibe.inet.url; import vibe.inet.webform; import vibe.internal.interfaceproxy : InterfaceProxy; import vibe.stream.counting; import vibe.stream.operations; import vibe.stream.tls; import vibe.stream.wrapper : ConnectionProxyStream, createConnectionProxyStream, createConnectionProxyStreamFL; import vibe.stream.zlib; import vibe.textfilter.urlencode; import vibe.utils.array; import vibe.internal.allocator; import vibe.internal.freelistref; import vibe.utils.string; import core.atomic; import core.vararg; import std.algorithm : canFind; import std.array; import std.conv; import std.datetime; import std.encoding : sanitize; import std.exception; import std.format; import std.functional : toDelegate; import std.string; import std.traits : ReturnType; import std.typecons; import std.uri; version (VibeNoSSL) version = HaveNoTLS; else version (Have_botan) {} else version (Have_openssl) {} else version = HaveNoTLS; /**************************************************************************************************/ /* Public functions */ /**************************************************************************************************/ /** Starts a HTTP server listening on the specified port. request_handler will be called for each HTTP request that is made. The res parameter of the callback then has to be filled with the response data. request_handler can be either HTTPServerRequestDelegate/HTTPServerRequestFunction or a class/struct with a member function 'handleRequest' that has the same signature. Note that if the application has been started with the --disthost command line switch, listenHTTP() will automatically listen on the specified VibeDist host instead of locally. This allows for a seamless switch from single-host to multi-host scenarios without changing the code. If you need to listen locally, use listenHTTPPlain() instead. Params: settings = Customizes the HTTP servers functionality (host string or HTTPServerSettings object) request_handler = This callback is invoked for each incoming request and is responsible for generating the response. Returns: A handle is returned that can be used to stop listening for further HTTP requests with the supplied settings. Another call to `listenHTTP` can be used afterwards to start listening again. */ HTTPListener listenHTTP(Settings)(Settings _settings, HTTPServerRequestDelegate request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { // auto-construct HTTPServerSettings static if (is(Settings == string)) auto settings = new HTTPServerSettings(_settings); else alias settings = _settings; enforce(settings.bindAddresses.length, "Must provide at least one bind address for a HTTP server."); // if a VibeDist host was specified on the command line, register there instead of listening // directly. if (s_distHost.length && !settings.disableDistHost) { return listenHTTPDist(settings, request_handler, s_distHost, s_distPort); } else { return listenHTTPPlain(settings, request_handler); } } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestFunction request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, () @trusted { return toDelegate(request_handler); } ()); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestHandler request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, &request_handler.handleRequest); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestDelegateS request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, cast(HTTPServerRequestDelegate)request_handler); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestFunctionS request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, () @trusted { return toDelegate(request_handler); } ()); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestHandlerS request_handler) @safe if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, &request_handler.handleRequest); } /// Scheduled for deprecation - use a `@safe` callback instead. HTTPListener listenHTTP(Settings)(Settings settings, void delegate(HTTPServerRequest, HTTPServerResponse) @system request_handler) @system if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, (req, res) @trusted => request_handler(req, res)); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, void function(HTTPServerRequest, HTTPServerResponse) @system request_handler) @system if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, (req, res) @trusted => request_handler(req, res)); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, void delegate(scope HTTPServerRequest, scope HTTPServerResponse) @system request_handler) @system if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, (scope req, scope res) @trusted => request_handler(req, res)); } /// ditto HTTPListener listenHTTP(Settings)(Settings settings, void function(scope HTTPServerRequest, scope HTTPServerResponse) @system request_handler) @system if (is(Settings == string) || is(Settings == HTTPServerSettings)) { return listenHTTP(settings, (scope req, scope res) @trusted => request_handler(req, res)); } unittest { void test() { static void testSafeFunction(HTTPServerRequest req, HTTPServerResponse res) @safe {} listenHTTP("0.0.0.0:8080", &testSafeFunction); listenHTTP(":8080", new class HTTPServerRequestHandler { void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe {} }); listenHTTP(":8080", (req, res) {}); static void testSafeFunctionS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} listenHTTP(":8080", &testSafeFunctionS); void testSafeDelegateS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} listenHTTP(":8080", &testSafeDelegateS); listenHTTP(":8080", new class HTTPServerRequestHandler { void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} }); listenHTTP(":8080", (scope req, scope res) {}); } } /** Treats an existing connection as an HTTP connection and processes incoming requests. After all requests have been processed, the connection will be closed and the function returns to the caller. Params: connections = The stream to treat as an incoming HTTP client connection. context = Information about the incoming listener and available virtual hosts */ void handleHTTPConnection(TCPConnection connection, HTTPServerContext context) @safe { InterfaceProxy!Stream http_stream; http_stream = connection; scope (exit) connection.close(); // Set NODELAY to true, to avoid delays caused by sending the response // header and body in separate chunks. Note that to avoid other performance // issues (caused by tiny packets), this requires using an output buffer in // the event driver, which is the case at least for the default libevent // based driver. connection.tcpNoDelay = true; version(HaveNoTLS) {} else { TLSStreamType tls_stream; } if (!connection.waitForData(10.seconds())) { logDebug("Client didn't send the initial request in a timely manner. Closing connection."); return; } // If this is a HTTPS server, initiate TLS if (context.tlsContext) { version (HaveNoTLS) assert(false, "No TLS support compiled in."); else { logDebug("Accept TLS connection: %s", context.tlsContext.kind); // TODO: reverse DNS lookup for peer_name of the incoming connection for TLS client certificate verification purposes tls_stream = createTLSStreamFL(http_stream, context.tlsContext, TLSStreamState.accepting, null, connection.remoteAddress); http_stream = tls_stream; } } while (!connection.empty) { HTTPServerSettings settings; bool keep_alive; () @trusted { import vibe.internal.utilallocator: RegionListAllocator; version (VibeManualMemoryManagement) scope request_allocator = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance); else scope request_allocator = new RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance); handleRequest(http_stream, connection, context, settings, keep_alive, request_allocator); } (); if (!keep_alive) { logTrace("No keep-alive - disconnecting client."); break; } logTrace("Waiting for next request..."); // wait for another possible request on a keep-alive connection if (!connection.waitForData(settings.keepAliveTimeout)) { if (!connection.connected) logTrace("Client disconnected."); else logDebug("Keep-alive connection timed out!"); break; } } logTrace("Done handling connection."); } /** Provides a HTTP request handler that responds with a static Diet template. */ @property HTTPServerRequestDelegateS staticTemplate(string template_file)() { return (scope HTTPServerRequest req, scope HTTPServerResponse res){ res.render!(template_file, req); }; } /** Provides a HTTP request handler that responds with a static redirection to the specified URL. Params: url = The URL to redirect to status = Redirection status to use $(LPAREN)by default this is $(D HTTPStatus.found)$(RPAREN). Returns: Returns a $(D HTTPServerRequestDelegate) that performs the redirect */ HTTPServerRequestDelegate staticRedirect(string url, HTTPStatus status = HTTPStatus.found) @safe { return (HTTPServerRequest req, HTTPServerResponse res){ res.redirect(url, status); }; } /// ditto HTTPServerRequestDelegate staticRedirect(URL url, HTTPStatus status = HTTPStatus.found) @safe { return (HTTPServerRequest req, HTTPServerResponse res){ res.redirect(url, status); }; } /// unittest { import vibe.http.router; void test() { auto router = new URLRouter; router.get("/old_url", staticRedirect("http://example.org/new_url", HTTPStatus.movedPermanently)); listenHTTP(new HTTPServerSettings, router); } } /** Sets a VibeDist host to register with. */ void setVibeDistHost(string host, ushort port) @safe { s_distHost = host; s_distPort = port; } /** Renders the given Diet template and makes all ALIASES available to the template. You can call this function as a pseudo-member of `HTTPServerResponse` using D's uniform function call syntax. See_also: `diet.html.compileHTMLDietFile` Examples: --- string title = "Hello, World!"; int pageNumber = 1; res.render!("mytemplate.dt", title, pageNumber); --- */ @property void render(string template_file, ALIASES...)(HTTPServerResponse res) { res.contentType = "text/html; charset=UTF-8"; version (VibeUseOldDiet) pragma(msg, "VibeUseOldDiet is not supported anymore. Please undefine in the package recipe."); import vibe.stream.wrapper : streamOutputRange; import diet.html : compileHTMLDietFile; auto output = streamOutputRange!1024(res.bodyWriter); compileHTMLDietFile!(template_file, ALIASES, DefaultDietFilters)(output); } version (Have_diet_ng) { import diet.traits; /** Provides the default `css`, `javascript`, `markdown` and `htmlescape` filters */ @dietTraits struct DefaultDietFilters { import diet.html : HTMLOutputStyle; import std.string : splitLines; version (VibeOutputCompactHTML) enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.compact; else enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; static string filterCss(I)(I text, size_t indent = 0) { auto lines = splitLines(text); string indent_string = "\n"; while (indent-- > 0) indent_string ~= '\t'; string ret = indent_string~"<style type=\"text/css\"><!--"; indent_string = indent_string ~ '\t'; foreach (ln; lines) ret ~= indent_string ~ ln; indent_string = indent_string[0 .. $-1]; ret ~= indent_string ~ "--></style>"; return ret; } static string filterJavascript(I)(I text, size_t indent = 0) { auto lines = splitLines(text); string indent_string = "\n"; while (indent-- > 0) indent_string ~= '\t'; string ret = indent_string~"<script type=\"application/javascript\">"; ret ~= indent_string~'\t' ~ "//<![CDATA["; foreach (ln; lines) ret ~= indent_string ~ '\t' ~ ln; ret ~= indent_string ~ '\t' ~ "//]]>" ~ indent_string ~ "</script>"; return ret; } static string filterMarkdown(I)(I text) { import vibe.textfilter.markdown : markdown = filterMarkdown; // TODO: indent return markdown(text); } static string filterHtmlescape(I)(I text) { import vibe.textfilter.html : htmlEscape; // TODO: indent return htmlEscape(text); } static this() { filters["css"] = (input, scope output) { output(filterCss(input)); }; filters["javascript"] = (input, scope output) { output(filterJavascript(input)); }; filters["markdown"] = (input, scope output) { output(filterMarkdown(() @trusted { return cast(string)input; } ())); }; filters["htmlescape"] = (input, scope output) { output(filterHtmlescape(input)); }; } static SafeFilterCallback[string] filters; } unittest { static string compile(string diet)() { import std.array : appender; import std.string : strip; import diet.html : compileHTMLDietString; auto dst = appender!string; dst.compileHTMLDietString!(diet, DefaultDietFilters); return strip(cast(string)(dst.data)); } assert(compile!":css .test" == "<style type=\"text/css\"><!--\n\t.test\n--></style>"); assert(compile!":javascript test();" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); assert(compile!":markdown **test**" == "<p><strong>test</strong>\n</p>"); assert(compile!":htmlescape <test>" == "<test>"); assert(compile!":css !{\".test\"}" == "<style type=\"text/css\"><!--\n\t.test\n--></style>"); assert(compile!":javascript !{\"test();\"}" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); assert(compile!":markdown !{\"**test**\"}" == "<p><strong>test</strong>\n</p>"); assert(compile!":htmlescape !{\"<test>\"}" == "<test>"); assert(compile!":javascript\n\ttest();" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); } } /** Creates a HTTPServerRequest suitable for writing unit tests. */ HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method = HTTPMethod.GET, InputStream data = null) @safe { InetHeaderMap headers; return createTestHTTPServerRequest(url, method, headers, data); } /// ditto HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method, InetHeaderMap headers, InputStream data = null) @safe { auto tls = url.schema == "https"; auto ret = new HTTPServerRequest(Clock.currTime(UTC()), url.port ? url.port : tls ? 443 : 80); ret.requestPath = url.path; ret.queryString = url.queryString; ret.username = url.username; ret.password = url.password; ret.requestURI = url.localURI; ret.method = method; ret.tls = tls; ret.headers = headers; ret.bodyReader = data; return ret; } /** Creates a HTTPServerResponse suitable for writing unit tests. */ HTTPServerResponse createTestHTTPServerResponse(OutputStream data_sink = null, SessionStore session_store = null) @safe { import vibe.stream.wrapper; HTTPServerSettings settings; if (session_store) { settings = new HTTPServerSettings; settings.sessionStore = session_store; } if (!data_sink) data_sink = new NullOutputStream; auto stream = createProxyStream(Stream.init, data_sink); auto ret = new HTTPServerResponse(stream, null, settings, () @trusted { return vibeThreadAllocator(); } ()); return ret; } /**************************************************************************************************/ /* Public types */ /**************************************************************************************************/ /// Delegate based request handler alias HTTPServerRequestDelegate = void delegate(HTTPServerRequest req, HTTPServerResponse res) @safe; /// Static function based request handler alias HTTPServerRequestFunction = void function(HTTPServerRequest req, HTTPServerResponse res) @safe; /// Interface for class based request handlers interface HTTPServerRequestHandler { /// Handles incoming HTTP requests void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe ; } /// Delegate based request handler with scoped parameters alias HTTPServerRequestDelegateS = void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; /// Static function based request handler with scoped parameters alias HTTPServerRequestFunctionS = void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; /// Interface for class based request handlers with scoped parameters interface HTTPServerRequestHandlerS { /// Handles incoming HTTP requests void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; } unittest { static assert(is(HTTPServerRequestDelegateS : HTTPServerRequestDelegate)); static assert(is(HTTPServerRequestFunctionS : HTTPServerRequestFunction)); } /// Aggregates all information about an HTTP error status. final class HTTPServerErrorInfo { /// The HTTP status code int code; /// The error message string message; /// Extended error message with debug information such as a stack trace string debugMessage; /// The error exception, if any Throwable exception; } /// Delegate type used for user defined error page generator callbacks. alias HTTPServerErrorPageHandler = void delegate(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) @safe; private enum HTTPServerOptionImpl { none = 0, errorStackTraces = 1<<7, reusePort = 1<<8, distribute = 1<<9 // deprecated } // TODO: Should be turned back into an enum once the deprecated symbols can be removed /** Specifies optional features of the HTTP server. Disabling unneeded features can speed up the server or reduce its memory usage. Note that the options `parseFormBody`, `parseJsonBody` and `parseMultiPartBody` will also drain the `HTTPServerRequest.bodyReader` stream whenever a request body with form or JSON data is encountered. */ struct HTTPServerOption { static enum none = HTTPServerOptionImpl.none; deprecated("This is done lazily. It will be removed in 0.9.") static enum parseURL = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum parseQueryString = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum parseFormBody = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum parseJsonBody = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum parseMultiPartBody = none; /** Deprecated: Distributes request processing among worker threads Note that this functionality assumes that the request handler is implemented in a thread-safe way. However, the D type system is bypassed, so that no static verification takes place. For this reason, it is recommended to instead use `vibe.core.core.runWorkerTaskDist` and call `listenHTTP` from each task/thread individually. If the `reusePort` option is set, then all threads will be able to listen on the same port, with the operating system distributing the incoming connections. If possible, instead of threads, the use of separate processes is more robust and often faster. The `reusePort` option works the same way in this scenario. */ deprecated("Use runWorkerTaskDist or start threads separately. It will be removed in 0.9.") static enum distribute = HTTPServerOptionImpl.distribute; /** Enables stack traces (`HTTPServerErrorInfo.debugMessage`). Note that generating the stack traces are generally a costly operation that should usually be avoided in production environments. It can also reveal internal information about the application, such as function addresses, which can help an attacker to abuse possible security holes. */ static enum errorStackTraces = HTTPServerOptionImpl.errorStackTraces; /// Enable port reuse in `listenTCP()` static enum reusePort = HTTPServerOptionImpl.reusePort; /** The default set of options. Includes all parsing options, as well as the `errorStackTraces` option if the code is compiled in debug mode. */ static enum defaults = () { debug return HTTPServerOptionImpl.errorStackTraces; else return HTTPServerOptionImpl.none; } ().HTTPServerOption; deprecated("None has been renamed to none.") static enum None = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseURL = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseQueryString = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseFormBody = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseJsonBody = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseMultiPartBody = none; deprecated("This is done lazily. It will be removed in 0.9.") static enum ParseCookies = none; HTTPServerOptionImpl x; alias x this; } /** Contains all settings for configuring a basic HTTP server. The defaults are sufficient for most normal uses. */ final class HTTPServerSettings { /** The port on which the HTTP server is listening. The default value is 80. If you are running a TLS enabled server you may want to set this to 443 instead. Using a value of `0` instructs the server to use any available port on the given `bindAddresses` the actual addresses and ports can then be queried with `TCPListener.bindAddresses`. */ ushort port = 80; /** The interfaces on which the HTTP server is listening. By default, the server will listen on all IPv4 and IPv6 interfaces. */ string[] bindAddresses = ["::", "0.0.0.0"]; /** Determines the server host name. If multiple servers are listening on the same port, the host name will determine which one gets a request. */ string hostName; /** Configures optional features of the HTTP server Disabling unneeded features can improve performance or reduce the server load in case of invalid or unwanted requests (DoS). By default, HTTPServerOption.defaults is used. */ HTTPServerOptionImpl options = HTTPServerOption.defaults; /** Time of a request after which the connection is closed with an error; not supported yet The default limit of 0 means that the request time is not limited. */ Duration maxRequestTime = 0.seconds; /** Maximum time between two request on a keep-alive connection The default value is 10 seconds. */ Duration keepAliveTimeout = 10.seconds; /// Maximum number of transferred bytes per request after which the connection is closed with /// an error ulong maxRequestSize = 2097152; /// Maximum number of transferred bytes for the request header. This includes the request line /// the url and all headers. ulong maxRequestHeaderSize = 8192; /// Sets a custom handler for displaying error pages for HTTP errors @property HTTPServerErrorPageHandler errorPageHandler() @safe { return errorPageHandler_; } /// ditto @property void errorPageHandler(HTTPServerErrorPageHandler del) @safe { errorPageHandler_ = del; } /// Scheduled for deprecation - use a `@safe` callback instead. @property void errorPageHandler(void delegate(HTTPServerRequest, HTTPServerResponse, HTTPServerErrorInfo) @system del) @system { this.errorPageHandler = (req, res, err) @trusted { del(req, res, err); }; } private HTTPServerErrorPageHandler errorPageHandler_ = null; /// If set, a HTTPS server will be started instead of plain HTTP. TLSContext tlsContext; /// Session management is enabled if a session store instance is provided SessionStore sessionStore; string sessionIdCookie = "vibe.session_id"; /// import vibe.core.core : vibeVersionString; string serverString = "vibe.d/" ~ vibeVersionString; /** Specifies the format used for the access log. The log format is given using the Apache server syntax. By default NCSA combined is used. --- "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" --- */ string accessLogFormat = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""; /// Spefifies the name of a file to which access log messages are appended. string accessLogFile = ""; /// If set, access log entries will be output to the console. bool accessLogToConsole = false; /** Specifies a custom access logger instance. */ HTTPLogger accessLogger; /// Returns a duplicate of the settings object. @property HTTPServerSettings dup() @safe { auto ret = new HTTPServerSettings; foreach (mem; __traits(allMembers, HTTPServerSettings)) { static if (mem == "sslContext") {} else static if (mem == "bindAddresses") ret.bindAddresses = bindAddresses.dup; else static if (__traits(compiles, __traits(getMember, ret, mem) = __traits(getMember, this, mem))) __traits(getMember, ret, mem) = __traits(getMember, this, mem); } return ret; } /// Disable support for VibeDist and instead start listening immediately. bool disableDistHost = false; /** Responds to "Accept-Encoding" by using compression if possible. Compression can also be manually enabled by setting the "Content-Encoding" header of the HTTP response appropriately before sending the response body. This setting is disabled by default. Also note that there are still some known issues with the GZIP compression code. */ bool useCompressionIfPossible = false; /** Interval between WebSocket ping frames. The default value is 60 seconds; set to Duration.zero to disable pings. */ Duration webSocketPingInterval = 60.seconds; /** Constructs a new settings object with default values. */ this() @safe {} /** Constructs a new settings object with a custom bind interface and/or port. The syntax of `bind_string` is `[<IP address>][:<port>]`, where either of the two parts can be left off. IPv6 addresses must be enclosed in square brackets, as they would within a URL. Throws: An exception is thrown if `bind_string` is malformed. */ this(string bind_string) @safe { this(); if (bind_string.startsWith('[')) { auto idx = bind_string.indexOf(']'); enforce(idx > 0, "Missing closing bracket for IPv6 address."); bindAddresses = [bind_string[1 .. idx]]; bind_string = bind_string[idx+1 .. $]; enforce(bind_string.length == 0 || bind_string.startsWith(':'), "Only a colon may follow the IPv6 address."); } auto idx = bind_string.indexOf(':'); if (idx < 0) { if (bind_string.length > 0) bindAddresses = [bind_string]; } else { if (idx > 0) bindAddresses = [bind_string[0 .. idx]]; port = bind_string[idx+1 .. $].to!ushort; } } /// unittest { auto s = new HTTPServerSettings(":8080"); assert(s.bindAddresses == ["::", "0.0.0.0"]); // default bind addresses assert(s.port == 8080); s = new HTTPServerSettings("123.123.123.123"); assert(s.bindAddresses == ["123.123.123.123"]); assert(s.port == 80); s = new HTTPServerSettings("[::1]:443"); assert(s.bindAddresses == ["::1"]); assert(s.port == 443); } } /** Options altering how sessions are created. Multiple values can be or'ed together. See_Also: HTTPServerResponse.startSession */ enum SessionOption { /// No options. none = 0, /** Instructs the browser to disallow accessing the session ID from JavaScript. See_Also: Cookie.httpOnly */ httpOnly = 1<<0, /** Instructs the browser to disallow sending the session ID over unencrypted connections. By default, the type of the connection on which the session is started will be used to determine if secure or noSecure is used. See_Also: noSecure, Cookie.secure */ secure = 1<<1, /** Instructs the browser to allow sending the session ID over unencrypted connections. By default, the type of the connection on which the session is started will be used to determine if secure or noSecure is used. See_Also: secure, Cookie.secure */ noSecure = 1<<2 } /** Represents a HTTP request as received by the server side. */ final class HTTPServerRequest : HTTPRequest { private { SysTime m_timeCreated; HTTPServerSettings m_settings; ushort m_port; string m_peer; } public { /// The IP address of the client @property string peer() @safe nothrow { if (!m_peer) { version (Have_vibe_core) {} else scope (failure) assert(false); // store the IP address (IPv4 addresses forwarded over IPv6 are stored in IPv4 format) auto peer_address_string = this.clientAddress.toString(); if (peer_address_string.startsWith("::ffff:") && peer_address_string[7 .. $].indexOf(':') < 0) m_peer = peer_address_string[7 .. $]; else m_peer = peer_address_string; } return m_peer; } /// ditto NetworkAddress clientAddress; /// Determines if the request should be logged to the access log file. bool noLog; /// Determines if the request was issued over an TLS encrypted channel. bool tls; /** Information about the TLS certificate provided by the client. Remarks: This field is only set if `tls` is true, and the peer presented a client certificate. */ TLSCertificateInformation clientCertificate; /** Deprecated: The _path part of the URL. Note that this function contains the decoded version of the requested path, which can yield incorrect results if the path contains URL encoded path separators. Use `requestPath` instead to get an encoding-aware representation. */ string path() @safe { if (_path.isNull) { _path = urlDecode(requestPath.toString); } return _path.get; } private Nullable!string _path; /** The path part of the requested URI. */ InetPath requestPath; /** The user name part of the URL, if present. */ string username; /** The _password part of the URL, if present. */ string password; /** The _query string part of the URL. */ string queryString; /** Contains the list of _cookies that are stored on the client. Note that the a single cookie name may occur multiple times if multiple cookies have that name but different paths or domains that all match the request URI. By default, the first cookie will be returned, which is the or one of the cookies with the closest path match. */ @property ref CookieValueMap cookies() @safe { if (_cookies.isNull) { _cookies = CookieValueMap.init; if (auto pv = "cookie" in headers) parseCookies(*pv, _cookies); } return _cookies.get; } private Nullable!CookieValueMap _cookies; /** Contains all _form fields supplied using the _query string. The fields are stored in the same order as they are received. */ @property ref FormFields query() @safe { if (_query.isNull) { _query = FormFields.init; parseURLEncodedForm(queryString, _query); } return _query.get; } Nullable!FormFields _query; import vibe.utils.dictionarylist; /** A map of general parameters for the request. This map is supposed to be used by middleware functionality to store information for later stages. For example vibe.http.router.URLRouter uses this map to store the value of any named placeholders. */ DictionaryList!(string, true, 8) params; import std.variant : Variant; /** A map of context items for the request. This is especially useful for passing application specific data down the chain of processors along with the request itself. For example, a generic route may be defined to check user login status, if the user is logged in, add a reference to user specific data to the context. This is implemented with `std.variant.Variant` to allow any type of data. */ DictionaryList!(Variant, true, 2) context; /** Supplies the request body as a stream. Note that when certain server options are set (such as HTTPServerOption.parseJsonBody) and a matching request was sent, the returned stream will be empty. If needed, remove those options and do your own processing of the body when launching the server. HTTPServerOption has a list of all options that affect the request body. */ InputStream bodyReader; /** Contains the parsed Json for a JSON request. A JSON request must have the Content-Type "application/json" or "application/vnd.api+json". */ @property ref Json json() @safe { if (_json.isNull) { if (icmp2(contentType, "application/json") == 0 || icmp2(contentType, "application/vnd.api+json") == 0 ) { auto bodyStr = bodyReader.readAllUTF8(); if (!bodyStr.empty) _json = parseJson(bodyStr); } else { _json = Json.undefined; } } return _json.get; } private Nullable!Json _json; /** Contains the parsed parameters of a HTML POST _form request. The fields are stored in the same order as they are received. Remarks: A form request must either have the Content-Type "application/x-www-form-urlencoded" or "multipart/form-data". */ @property ref FormFields form() @safe { if (_form.isNull) parseFormAndFiles(); return _form.get; } private Nullable!FormFields _form; private void parseFormAndFiles() @safe { _form = FormFields.init; assert(!!bodyReader); parseFormData(_form, _files, headers.get("Content-Type", ""), bodyReader, MaxHTTPHeaderLineLength); } /** Contains information about any uploaded file for a HTML _form request. */ @property ref FilePartFormFields files() @safe { // _form and _files are parsed in one step if (_form.isNull) { parseFormAndFiles(); assert(!_form.isNull); } return _files; } private FilePartFormFields _files; /** The current Session object. This field is set if HTTPServerResponse.startSession() has been called on a previous response and if the client has sent back the matching cookie. Remarks: Requires the HTTPServerOption.parseCookies option. */ Session session; } package { /** The settings of the server serving this request. */ @property const(HTTPServerSettings) serverSettings() const @safe { return m_settings; } } this(SysTime time, ushort port) @safe { m_timeCreated = time.toUTC(); m_port = port; } /** Time when this request started processing. */ @property SysTime timeCreated() const @safe { return m_timeCreated; } /** The full URL that corresponds to this request. The host URL includes the protocol, host and optionally the user and password that was used for this request. This field is useful to construct self referencing URLs. Note that the port is currently not set, so that this only works if the standard port is used. */ @property URL fullURL() const @safe { URL url; auto xfh = this.headers.get("X-Forwarded-Host"); auto xfp = this.headers.get("X-Forwarded-Port"); auto xfpr = this.headers.get("X-Forwarded-Proto"); // Set URL host segment. if (xfh.length) { url.host = xfh; } else if (!this.host.empty) { url.host = this.host; } else if (!m_settings.hostName.empty) { url.host = m_settings.hostName; } else { url.host = m_settings.bindAddresses[0]; } // Set URL schema segment. if (xfpr.length) { url.schema = xfpr; } else if (this.tls) { url.schema = "https"; } else { url.schema = "http"; } // Set URL port segment. if (xfp.length) { try { url.port = xfp.to!ushort; } catch (ConvException) { // TODO : Consider responding with a 400/etc. error from here. logWarn("X-Forwarded-Port header was not valid port (%s)", xfp); } } else if (!xfh) { if (url.schema == "https") { if (m_port != 443U) url.port = m_port; } else { if (m_port != 80U) url.port = m_port; } } if (url.host.startsWith('[')) { // handle IPv6 address auto idx = url.host.indexOf(']'); if (idx >= 0 && idx+1 < url.host.length && url.host[idx+1] == ':') url.host = url.host[1 .. idx]; } else { // handle normal host names or IPv4 address auto idx = url.host.indexOf(':'); if (idx >= 0) url.host = url.host[0 .. idx]; } url.username = this.username; url.password = this.password; url.localURI = this.requestURI; return url; } /** The relative path to the root folder. Using this function instead of absolute URLs for embedded links can be useful to avoid dead link when the site is piped through a reverse-proxy. The returned string always ends with a slash. */ @property string rootDir() const @safe { import std.algorithm.searching : count; auto depth = requestPath.bySegment.count!(s => s.name.length > 0); if (depth > 0 && !requestPath.endsWithSlash) depth--; return depth == 0 ? "./" : replicate("../", depth); } unittest { assert(createTestHTTPServerRequest(URL("http://localhost/")).rootDir == "./"); assert(createTestHTTPServerRequest(URL("http://localhost/foo")).rootDir == "./"); assert(createTestHTTPServerRequest(URL("http://localhost/foo/")).rootDir == "../"); assert(createTestHTTPServerRequest(URL("http://localhost/foo/bar")).rootDir == "../"); assert(createTestHTTPServerRequest(URL("http://localhost")).rootDir == "./"); } } /** Represents a HTTP response as sent from the server side. */ final class HTTPServerResponse : HTTPResponse { private { InterfaceProxy!Stream m_conn; InterfaceProxy!ConnectionStream m_rawConnection; InterfaceProxy!OutputStream m_bodyWriter; IAllocator m_requestAlloc; FreeListRef!ChunkedOutputStream m_chunkedBodyWriter; FreeListRef!CountingOutputStream m_countingWriter; FreeListRef!ZlibOutputStream m_zlibOutputStream; HTTPServerSettings m_settings; Session m_session; bool m_headerWritten = false; bool m_isHeadResponse = false; bool m_tls; SysTime m_timeFinalized; } static if (!is(Stream == InterfaceProxy!Stream)) { this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) @safe { this(InterfaceProxy!Stream(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc); } } this(InterfaceProxy!Stream conn, InterfaceProxy!ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) @safe { m_conn = conn; m_rawConnection = raw_connection; m_countingWriter = createCountingOutputStreamFL(conn); m_settings = settings; m_requestAlloc = req_alloc; } /** Returns the time at which the request was finalized. Note that this field will only be set after `finalize` has been called. */ @property SysTime timeFinalized() const @safe { return m_timeFinalized; } /** Determines if the HTTP header has already been written. */ @property bool headerWritten() const @safe { return m_headerWritten; } /** Determines if the response does not need a body. */ bool isHeadResponse() const @safe { return m_isHeadResponse; } /** Determines if the response is sent over an encrypted connection. */ bool tls() const @safe { return m_tls; } /** Writes the entire response body at once. Params: data = The data to write as the body contents status = Optional response status code to set content_tyoe = Optional content type to apply to the response. If no content type is given and no "Content-Type" header is set in the response, this will default to `"application/octet-stream"`. See_Also: `HTTPStatusCode` */ void writeBody(in ubyte[] data, string content_type = null) @safe { if (content_type.length) headers["Content-Type"] = content_type; else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", data.length); bodyWriter.write(data); } /// ditto void writeBody(in ubyte[] data, int status, string content_type = null) @safe { statusCode = status; writeBody(data, content_type); } /// ditto void writeBody(scope InputStream data, string content_type = null) @safe { if (content_type.length) headers["Content-Type"] = content_type; else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; data.pipe(bodyWriter); } /** Writes the entire response body as a single string. Params: data = The string to write as the body contents status = Optional response status code to set content_type = Optional content type to apply to the response. If no content type is given and no "Content-Type" header is set in the response, this will default to `"text/plain; charset=UTF-8"`. See_Also: `HTTPStatusCode` */ /// ditto void writeBody(string data, string content_type = null) @safe { if (!content_type.length && "Content-Type" !in headers) content_type = "text/plain; charset=UTF-8"; writeBody(cast(const(ubyte)[])data, content_type); } /// ditto void writeBody(string data, int status, string content_type = null) @safe { statusCode = status; writeBody(data, content_type); } /** Writes the whole response body at once, without doing any further encoding. The caller has to make sure that the appropriate headers are set correctly (i.e. Content-Type and Content-Encoding). Note that the version taking a RandomAccessStream may perform additional optimizations such as sending a file directly from the disk to the network card using a DMA transfer. */ void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe if (isRandomAccessStream!RandomAccessStream) { assert(!m_headerWritten, "A body was already written!"); writeHeader(); if (m_isHeadResponse) return; auto bytes = stream.size - stream.tell(); stream.pipe(m_conn); m_countingWriter.increment(bytes); } /// ditto void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = 0) @safe if (isInputStream!InputStream && !isRandomAccessStream!InputStream) { assert(!m_headerWritten, "A body was already written!"); writeHeader(); if (m_isHeadResponse) return; if (num_bytes > 0) { stream.pipe(m_conn, num_bytes); m_countingWriter.increment(num_bytes); } else stream.pipe(m_countingWriter, num_bytes); } /// ditto void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe if (isRandomAccessStream!RandomAccessStream) { statusCode = status; writeRawBody(stream); } /// ditto void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe if (isInputStream!InputStream && !isRandomAccessStream!InputStream) { statusCode = status; writeRawBody(stream, num_bytes); } /// Writes a JSON message with the specified status void writeJsonBody(T)(T data, int status, bool allow_chunked = false) { statusCode = status; writeJsonBody(data, allow_chunked); } /// ditto void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false) { statusCode = status; writeJsonBody(data, content_type, allow_chunked); } /// ditto void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false) { headers["Content-Type"] = content_type; writeJsonBody(data, allow_chunked); } /// ditto void writeJsonBody(T)(T data, bool allow_chunked = false) { doWriteJsonBody!(T, false)(data, allow_chunked); } /// ditto void writePrettyJsonBody(T)(T data, bool allow_chunked = false) { doWriteJsonBody!(T, true)(data, allow_chunked); } private void doWriteJsonBody(T, bool PRETTY)(T data, bool allow_chunked = false) { import std.traits; import vibe.stream.wrapper; static if (!is(T == Json) && is(typeof(data.data())) && isArray!(typeof(data.data()))) { static assert(!is(T == Appender!(typeof(data.data()))), "Passed an Appender!T to writeJsonBody - this is most probably not doing what's indended."); } if ("Content-Type" !in headers) headers["Content-Type"] = "application/json; charset=UTF-8"; // set an explicit content-length field if chunked encoding is not allowed if (!allow_chunked) { import vibe.internal.rangeutil; long length = 0; auto counter = RangeCounter(() @trusted { return &length; } ()); static if (PRETTY) serializeToPrettyJson(counter, data); else serializeToJson(counter, data); headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length); } auto rng = streamOutputRange!1024(bodyWriter); static if (PRETTY) serializeToPrettyJson(() @trusted { return &rng; } (), data); else serializeToJson(() @trusted { return &rng; } (), data); } /** * Writes the response with no body. * * This method should be used in situations where no body is * requested, such as a HEAD request. For an empty body, just use writeBody, * as this method causes problems with some keep-alive connections. */ void writeVoidBody() @safe { if (!m_isHeadResponse) { assert("Content-Length" !in headers); assert("Transfer-Encoding" !in headers); } assert(!headerWritten); writeHeader(); m_conn.flush(); } /** A stream for writing the body of the HTTP response. Note that after 'bodyWriter' has been accessed for the first time, it is not allowed to change any header or the status code of the response. */ @property InterfaceProxy!OutputStream bodyWriter() @safe { assert(!!m_conn); if (m_bodyWriter) return m_bodyWriter; assert(!m_headerWritten, "A void body was already written!"); if (m_isHeadResponse) { // for HEAD requests, we define a NullOutputWriter for convenience // - no body will be written. However, the request handler should call writeVoidBody() // and skip writing of the body in this case. if ("Content-Length" !in headers) headers["Transfer-Encoding"] = "chunked"; writeHeader(); m_bodyWriter = nullSink; return m_bodyWriter; } if ("Content-Encoding" in headers && "Content-Length" in headers) { // we do not known how large the compressed body will be in advance // so remove the content-length and use chunked transfer headers.remove("Content-Length"); } if (auto pcl = "Content-Length" in headers) { writeHeader(); m_countingWriter.writeLimit = (*pcl).to!ulong; m_bodyWriter = m_countingWriter; } else if (httpVersion <= HTTPVersion.HTTP_1_0) { if ("Connection" in headers) headers.remove("Connection"); // default to "close" writeHeader(); m_bodyWriter = m_conn; } else { headers["Transfer-Encoding"] = "chunked"; writeHeader(); m_chunkedBodyWriter = createChunkedOutputStreamFL(m_countingWriter); m_bodyWriter = m_chunkedBodyWriter; } if (auto pce = "Content-Encoding" in headers) { if (icmp2(*pce, "gzip") == 0) { m_zlibOutputStream = createGzipOutputStreamFL(m_bodyWriter); m_bodyWriter = m_zlibOutputStream; } else if (icmp2(*pce, "deflate") == 0) { m_zlibOutputStream = createDeflateOutputStreamFL(m_bodyWriter); m_bodyWriter = m_zlibOutputStream; } else { logWarn("Unsupported Content-Encoding set in response: '"~*pce~"'"); } } return m_bodyWriter; } /** Sends a redirect request to the client. Params: url = The URL to redirect to status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found) */ void redirect(string url, int status = HTTPStatus.Found) @safe { // Disallow any characters that may influence the header parsing enforce(!url.representation.canFind!(ch => ch < 0x20), "Control character in redirection URL."); statusCode = status; headers["Location"] = url; writeBody("redirecting..."); } /// ditto void redirect(URL url, int status = HTTPStatus.Found) @safe { redirect(url.toString(), status); } /// @safe unittest { import vibe.http.router; void request_handler(HTTPServerRequest req, HTTPServerResponse res) { res.redirect("http://example.org/some_other_url"); } void test() { auto router = new URLRouter; router.get("/old_url", &request_handler); listenHTTP(new HTTPServerSettings, router); } } /** Special method sending a SWITCHING_PROTOCOLS response to the client. Notice: For the overload that returns a `ConnectionStream`, it must be ensured that the returned instance doesn't outlive the request handler callback. Params: protocol = The protocol set in the "Upgrade" header of the response. Use an empty string to skip setting this field. */ ConnectionStream switchProtocol(string protocol) @safe { statusCode = HTTPStatus.SwitchingProtocols; if (protocol.length) headers["Upgrade"] = protocol; writeVoidBody(); return createConnectionProxyStream(m_conn, m_rawConnection); } /// ditto void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del) @safe { statusCode = HTTPStatus.SwitchingProtocols; if (protocol.length) headers["Upgrade"] = protocol; writeVoidBody(); () @trusted { auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); del(conn); } (); finalize(); if (m_rawConnection && m_rawConnection.connected) m_rawConnection.close(); // connection not reusable after a protocol upgrade } /** Special method for handling CONNECT proxy tunnel Notice: For the overload that returns a `ConnectionStream`, it must be ensured that the returned instance doesn't outlive the request handler callback. */ ConnectionStream connectProxy() @safe { return createConnectionProxyStream(m_conn, m_rawConnection); } /// ditto void connectProxy(scope void delegate(scope ConnectionStream) @safe del) @safe { () @trusted { auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); del(conn); } (); finalize(); m_rawConnection.close(); // connection not reusable after a protocol upgrade } /** Sets the specified cookie value. Params: name = Name of the cookie value = New cookie value - pass null to clear the cookie path = Path (as seen by the client) of the directory tree in which the cookie is visible */ Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url) @safe { auto cookie = new Cookie(); cookie.path = path; cookie.setValue(value, encoding); if (value is null) { cookie.maxAge = 0; cookie.expires = "Thu, 01 Jan 1970 00:00:00 GMT"; } cookies[name] = cookie; return cookie; } /** Initiates a new session. The session is stored in the SessionStore that was specified when creating the server. Depending on this, the session can be persistent or temporary and specific to this server instance. */ Session startSession(string path = "/", SessionOption options = SessionOption.httpOnly) @safe { assert(m_settings.sessionStore, "no session store set"); assert(!m_session, "Try to start a session, but already started one."); bool secure; if (options & SessionOption.secure) secure = true; else if (options & SessionOption.noSecure) secure = false; else secure = this.tls; m_session = m_settings.sessionStore.create(); m_session.set("$sessionCookiePath", path); m_session.set("$sessionCookieSecure", secure); auto cookie = setCookie(m_settings.sessionIdCookie, m_session.id, path); cookie.secure = secure; cookie.httpOnly = (options & SessionOption.httpOnly) != 0; return m_session; } /** Terminates the current session (if any). */ void terminateSession() @safe { if (!m_session) return; auto cookie = setCookie(m_settings.sessionIdCookie, null, m_session.get!string("$sessionCookiePath")); cookie.secure = m_session.get!bool("$sessionCookieSecure"); m_session.destroy(); m_session = Session.init; } @property ulong bytesWritten() @safe const { return m_countingWriter.bytesWritten; } /** Waits until either the connection closes, data arrives, or until the given timeout is reached. Returns: $(D true) if the connection was closed and $(D false) if either the timeout was reached, or if data has arrived for consumption. See_Also: `connected` */ bool waitForConnectionClose(Duration timeout = Duration.max) @safe { if (!m_rawConnection || !m_rawConnection.connected) return true; m_rawConnection.waitForData(timeout); return !m_rawConnection.connected; } /** Determines if the underlying connection is still alive. Returns $(D true) if the remote peer is still connected and $(D false) if the remote peer closed the connection. See_Also: `waitForConnectionClose` */ @property bool connected() @safe const { if (!m_rawConnection) return false; return m_rawConnection.connected; } /** Finalizes the response. This is usually called automatically by the server. This method can be called manually after writing the response to force all network traffic associated with the current request to be finalized. After the call returns, the `timeFinalized` property will be set. */ void finalize() @safe { if (m_zlibOutputStream) { m_zlibOutputStream.finalize(); m_zlibOutputStream.destroy(); } if (m_chunkedBodyWriter) { m_chunkedBodyWriter.finalize(); m_chunkedBodyWriter.destroy(); } // ignore exceptions caused by an already closed connection - the client // may have closed the connection already and this doesn't usually indicate // a problem. if (m_rawConnection && m_rawConnection.connected) { try if (m_conn) m_conn.flush(); catch (Exception e) logDebug("Failed to flush connection after finishing HTTP response: %s", e.msg); if (!isHeadResponse && bytesWritten < headers.get("Content-Length", "0").to!long) { logDebug("HTTP response only written partially before finalization. Terminating connection."); m_rawConnection.close(); } m_rawConnection = InterfaceProxy!ConnectionStream.init; } if (m_conn) { m_conn = InterfaceProxy!Stream.init; m_timeFinalized = Clock.currTime(UTC()); } } private void writeHeader() @safe { import vibe.stream.wrapper; assert(!m_bodyWriter && !m_headerWritten, "Try to write header after body has already begun."); m_headerWritten = true; auto dst = streamOutputRange!1024(m_conn); void writeLine(T...)(string fmt, T args) @safe { formattedWrite(() @trusted { return &dst; } (), fmt, args); dst.put("\r\n"); logTrace(fmt, args); } logTrace("---------------------"); logTrace("HTTP server response:"); logTrace("---------------------"); // write the status line writeLine("%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase.length ? this.statusPhrase : httpStatusText(this.statusCode)); // write all normal headers foreach (k, v; this.headers) { dst.put(k); dst.put(": "); dst.put(v); dst.put("\r\n"); logTrace("%s: %s", k, v); } logTrace("---------------------"); // write cookies foreach (n, cookie; this.cookies) { dst.put("Set-Cookie: "); cookie.writeString(() @trusted { return &dst; } (), n); dst.put("\r\n"); } // finalize response header dst.put("\r\n"); } } /** Represents the request listener for a specific `listenHTTP` call. This struct can be used to stop listening for HTTP requests at runtime. */ struct HTTPListener { private { size_t[] m_virtualHostIDs; } private this(size_t[] ids) @safe { m_virtualHostIDs = ids; } @property NetworkAddress[] bindAddresses() { NetworkAddress[] ret; foreach (l; s_listeners) if (l.m_virtualHosts.canFind!(v => m_virtualHostIDs.canFind(v.id))) { NetworkAddress a; a = resolveHost(l.bindAddress); a.port = l.bindPort; ret ~= a; } return ret; } /** Stops handling HTTP requests and closes the TCP listening port if possible. */ void stopListening() @safe { import std.algorithm : countUntil; foreach (vhid; m_virtualHostIDs) { foreach (lidx, l; s_listeners) { if (l.removeVirtualHost(vhid)) { if (!l.hasVirtualHosts) { l.m_listener.stopListening(); logInfo("Stopped to listen for HTTP%s requests on %s:%s", l.tlsContext ? "S": "", l.bindAddress, l.bindPort); s_listeners = s_listeners[0 .. lidx] ~ s_listeners[lidx+1 .. $]; } } break; } } } } /** Represents a single HTTP server port. This class defines the incoming interface, port, and TLS configuration of the public server port. The public server port may differ from the local one if a reverse proxy of some kind is facing the public internet and forwards to this HTTP server. Multiple virtual hosts can be configured to be served from the same port. Their TLS settings must be compatible and each virtual host must have a unique name. */ final class HTTPServerContext { private struct VirtualHost { HTTPServerRequestDelegate requestHandler; HTTPServerSettings settings; HTTPLogger[] loggers; size_t id; } private { TCPListener m_listener; VirtualHost[] m_virtualHosts; string m_bindAddress; ushort m_bindPort; TLSContext m_tlsContext; static size_t s_vhostIDCounter = 1; } @safe: this(string bind_address, ushort bind_port) { m_bindAddress = bind_address; m_bindPort = bind_port; } /** Returns the TLS context associated with the listener. For non-HTTPS listeners, `null` will be returned. Otherwise, if only a single virtual host has been added, the TLS context of that host's settings is returned. For multiple virtual hosts, an SNI context is returned, which forwards to the individual contexts based on the requested host name. */ @property TLSContext tlsContext() { return m_tlsContext; } /// The local network interface IP address associated with this listener @property string bindAddress() const { return m_bindAddress; } /// The local port associated with this listener @property ushort bindPort() const { return m_bindPort; } /// Determines if any virtual hosts have been addded @property bool hasVirtualHosts() const { return m_virtualHosts.length > 0; } /** Adds a single virtual host. Note that the port and bind address defined in `settings` must match the ones for this listener. The `settings.host` field must be unique for all virtual hosts. Returns: Returns a unique ID for the new virtual host */ size_t addVirtualHost(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) { assert(settings.port == 0 || settings.port == m_bindPort, "Virtual host settings do not match bind port."); assert(settings.bindAddresses.canFind(m_bindAddress), "Virtual host settings do not match bind address."); VirtualHost vhost; vhost.id = s_vhostIDCounter++; vhost.settings = settings; vhost.requestHandler = request_handler; if (settings.accessLogger) vhost.loggers ~= settings.accessLogger; if (settings.accessLogToConsole) vhost.loggers ~= new HTTPConsoleLogger(settings, settings.accessLogFormat); if (settings.accessLogFile.length) vhost.loggers ~= new HTTPFileLogger(settings, settings.accessLogFormat, settings.accessLogFile); if (!m_virtualHosts.length) m_tlsContext = settings.tlsContext; enforce((m_tlsContext !is null) == (settings.tlsContext !is null), "Cannot mix HTTP and HTTPS virtual hosts within the same listener."); if (m_tlsContext) addSNIHost(settings); m_virtualHosts ~= vhost; if (settings.hostName.length) { auto proto = settings.tlsContext ? "https" : "http"; auto port = settings.tlsContext && settings.port == 443 || !settings.tlsContext && settings.port == 80 ? "" : ":" ~ settings.port.to!string; logInfo("Added virtual host %s://%s:%s/ (%s)", proto, settings.hostName, m_bindPort, m_bindAddress); } return vhost.id; } /// Removes a previously added virtual host using its ID. bool removeVirtualHost(size_t id) { import std.algorithm.searching : countUntil; auto idx = m_virtualHosts.countUntil!(c => c.id == id); if (idx < 0) return false; auto ctx = m_virtualHosts[idx]; m_virtualHosts = m_virtualHosts[0 .. idx] ~ m_virtualHosts[idx+1 .. $]; return true; } private void addSNIHost(HTTPServerSettings settings) { if (settings.tlsContext !is m_tlsContext && m_tlsContext.kind != TLSContextKind.serverSNI) { logDebug("Create SNI TLS context for %s, port %s", bindAddress, bindPort); m_tlsContext = createTLSContext(TLSContextKind.serverSNI); m_tlsContext.sniCallback = &onSNI; } foreach (ctx; m_virtualHosts) { /*enforce(ctx.settings.hostName != settings.hostName, "A server with the host name '"~settings.hostName~"' is already " "listening on "~addr~":"~to!string(settings.port)~".");*/ } } private TLSContext onSNI(string servername) { foreach (vhost; m_virtualHosts) if (vhost.settings.hostName.icmp(servername) == 0) { logDebug("Found context for SNI host '%s'.", servername); return vhost.settings.tlsContext; } logDebug("No context found for SNI host '%s'.", servername); return null; } } /**************************************************************************************************/ /* Private types */ /**************************************************************************************************/ private enum MaxHTTPHeaderLineLength = 4096; private final class LimitedHTTPInputStream : LimitedInputStream { @safe: this(InterfaceProxy!InputStream stream, ulong byte_limit, bool silent_limit = false) { super(stream, byte_limit, silent_limit, true); } override void onSizeLimitReached() { throw new HTTPStatusException(HTTPStatus.requestEntityTooLarge); } } private final class TimeoutHTTPInputStream : InputStream { @safe: private { long m_timeref; long m_timeleft; InterfaceProxy!InputStream m_in; } this(InterfaceProxy!InputStream stream, Duration timeleft, SysTime reftime) { enforce(timeleft > 0.seconds, "Timeout required"); m_in = stream; m_timeleft = timeleft.total!"hnsecs"(); m_timeref = reftime.stdTime(); } @property bool empty() { enforce(m_in, "InputStream missing"); return m_in.empty(); } @property ulong leastSize() { enforce(m_in, "InputStream missing"); return m_in.leastSize(); } @property bool dataAvailableForRead() { enforce(m_in, "InputStream missing"); return m_in.dataAvailableForRead; } const(ubyte)[] peek() { return m_in.peek(); } size_t read(scope ubyte[] dst, IOMode mode) { enforce(m_in, "InputStream missing"); size_t nread = 0; checkTimeout(); // FIXME: this should use ConnectionStream.waitForData to enforce the timeout during the // read operation return m_in.read(dst, mode); } alias read = InputStream.read; private void checkTimeout() @safe { auto curr = Clock.currStdTime(); auto diff = curr - m_timeref; if (diff > m_timeleft) throw new HTTPStatusException(HTTPStatus.RequestTimeout); m_timeleft -= diff; m_timeref = curr; } } /**************************************************************************************************/ /* Private functions */ /**************************************************************************************************/ private { import core.sync.mutex; shared string s_distHost; shared ushort s_distPort = 11000; HTTPServerContext[] s_listeners; } /** [private] Starts a HTTP server listening on the specified port. This is the same as listenHTTP() except that it does not use a VibeDist host for remote listening, even if specified on the command line. */ private HTTPListener listenHTTPPlain(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) @safe { import vibe.core.core : runWorkerTaskDist; import std.algorithm : canFind, find; static TCPListener doListen(HTTPServerContext listen_info, bool dist, bool reusePort) @safe { try { TCPListenOptions options = TCPListenOptions.defaults; if(reusePort) options |= TCPListenOptions.reusePort; else options &= ~TCPListenOptions.reusePort; auto ret = listenTCP(listen_info.bindPort, (TCPConnection conn) nothrow @safe { try handleHTTPConnection(conn, listen_info); catch (Exception e) { logError("HTTP connection handler has thrown: %s", e.msg); debug logDebug("Full error: %s", () @trusted { return e.toString().sanitize(); } ()); try conn.close(); catch (Exception e) logError("Failed to close connection: %s", e.msg); } }, listen_info.bindAddress, options); // support port 0 meaning any available port if (listen_info.bindPort == 0) listen_info.m_bindPort = ret.bindAddress.port; auto proto = listen_info.tlsContext ? "https" : "http"; auto urladdr = listen_info.bindAddress; if (urladdr.canFind(':')) urladdr = "["~urladdr~"]"; logInfo("Listening for requests on %s://%s:%s/", proto, urladdr, listen_info.bindPort); return ret; } catch( Exception e ) { logWarn("Failed to listen on %s:%s", listen_info.bindAddress, listen_info.bindPort); return TCPListener.init; } } size_t[] vid; // Check for every bind address/port, if a new listening socket needs to be created and // check for conflicting servers foreach (addr; settings.bindAddresses) { HTTPServerContext linfo; auto l = s_listeners.find!(l => l.bindAddress == addr && l.bindPort == settings.port); if (!l.empty) linfo = l.front; else { auto li = new HTTPServerContext(addr, settings.port); if (auto tcp_lst = doListen(li, (settings.options & HTTPServerOptionImpl.distribute) != 0, (settings.options & HTTPServerOption.reusePort) != 0)) // DMD BUG 2043 { li.m_listener = tcp_lst; s_listeners ~= li; linfo = li; } } if (linfo) vid ~= linfo.addVirtualHost(settings, request_handler); } enforce(vid.length > 0, "Failed to listen for incoming HTTP connections on any of the supplied interfaces."); return HTTPListener(vid); } private alias TLSStreamType = ReturnType!(createTLSStreamFL!(InterfaceProxy!Stream)); private bool handleRequest(InterfaceProxy!Stream http_stream, TCPConnection tcp_connection, HTTPServerContext listen_info, ref HTTPServerSettings settings, ref bool keep_alive, scope IAllocator request_allocator) @safe { import std.algorithm.searching : canFind; SysTime reqtime = Clock.currTime(UTC()); // some instances that live only while the request is running FreeListRef!HTTPServerRequest req = FreeListRef!HTTPServerRequest(reqtime, listen_info.bindPort); FreeListRef!TimeoutHTTPInputStream timeout_http_input_stream; FreeListRef!LimitedHTTPInputStream limited_http_input_stream; FreeListRef!ChunkedInputStream chunked_input_stream; // store the IP address req.clientAddress = tcp_connection.remoteAddress; if (!listen_info.hasVirtualHosts) { logWarn("Didn't find a HTTP listening context for incoming connection. Dropping."); keep_alive = false; return false; } // Default to the first virtual host for this listener HTTPServerContext.VirtualHost context = listen_info.m_virtualHosts[0]; HTTPServerRequestDelegate request_task = context.requestHandler; settings = context.settings; // temporarily set to the default settings, the virtual host specific settings will be set further down req.m_settings = settings; // Create the response object InterfaceProxy!ConnectionStream cproxy = tcp_connection; auto res = FreeListRef!HTTPServerResponse(http_stream, cproxy, settings, request_allocator/*.Scoped_payload*/); req.tls = res.m_tls = listen_info.tlsContext !is null; if (req.tls) { version (HaveNoTLS) assert(false); else { static if (is(InterfaceProxy!ConnectionStream == ConnectionStream)) req.clientCertificate = (cast(TLSStream)http_stream).peerCertificate; else req.clientCertificate = http_stream.extract!TLSStreamType.peerCertificate; } } // Error page handler void errorOut(int code, string msg, string debug_msg, Throwable ex) @safe { assert(!res.headerWritten); // stack traces sometimes contain random bytes - make sure they are replaced debug_msg = sanitizeUTF8(cast(const(ubyte)[])debug_msg); res.statusCode = code; if (settings && settings.errorPageHandler) { /*scope*/ auto err = new HTTPServerErrorInfo; err.code = code; err.message = msg; err.debugMessage = debug_msg; err.exception = ex; settings.errorPageHandler_(req, res, err); } else { if (debug_msg.length) res.writeBody(format("%s - %s\n\n%s\n\nInternal error information:\n%s", code, httpStatusText(code), msg, debug_msg)); else res.writeBody(format("%s - %s\n\n%s", code, httpStatusText(code), msg)); } assert(res.headerWritten); } bool parsed = false; /*bool*/ keep_alive = false; // parse the request try { logTrace("reading request.."); // limit the total request time InterfaceProxy!InputStream reqReader = http_stream; if (settings.maxRequestTime > dur!"seconds"(0) && settings.maxRequestTime != Duration.max) { timeout_http_input_stream = FreeListRef!TimeoutHTTPInputStream(reqReader, settings.maxRequestTime, reqtime); reqReader = timeout_http_input_stream; } // basic request parsing parseRequestHeader(req, reqReader, request_allocator, settings.maxRequestHeaderSize); logTrace("Got request header."); // find the matching virtual host string reqhost; ushort reqport = 0; { string s = req.host; enforceHTTP(s.length > 0 || req.httpVersion <= HTTPVersion.HTTP_1_0, HTTPStatus.badRequest, "Missing Host header."); if (s.startsWith('[')) { // IPv6 address auto idx = s.indexOf(']'); enforce(idx > 0, "Missing closing ']' for IPv6 address."); reqhost = s[1 .. idx]; s = s[idx+1 .. $]; } else if (s.length) { // host name or IPv4 address auto idx = s.indexOf(':'); if (idx < 0) idx = s.length; enforceHTTP(idx > 0, HTTPStatus.badRequest, "Missing Host header."); reqhost = s[0 .. idx]; s = s[idx .. $]; } if (s.startsWith(':')) reqport = s[1 .. $].to!ushort; } foreach (ctx; listen_info.m_virtualHosts) if (icmp2(ctx.settings.hostName, reqhost) == 0 && (!reqport || reqport == ctx.settings.port)) { context = ctx; settings = ctx.settings; request_task = ctx.requestHandler; break; } req.m_settings = settings; res.m_settings = settings; // setup compressed output if (settings.useCompressionIfPossible) { if (auto pae = "Accept-Encoding" in req.headers) { if (canFind(*pae, "gzip")) { res.headers["Content-Encoding"] = "gzip"; } else if (canFind(*pae, "deflate")) { res.headers["Content-Encoding"] = "deflate"; } } } // limit request size if (auto pcl = "Content-Length" in req.headers) { string v = *pcl; auto contentLength = parse!ulong(v); // DMDBUG: to! thinks there is a H in the string enforceBadRequest(v.length == 0, "Invalid content-length"); enforceBadRequest(settings.maxRequestSize <= 0 || contentLength <= settings.maxRequestSize, "Request size too big"); limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, contentLength); } else if (auto pt = "Transfer-Encoding" in req.headers) { enforceBadRequest(icmp(*pt, "chunked") == 0); chunked_input_stream = createChunkedInputStreamFL(reqReader); InterfaceProxy!InputStream ciproxy = chunked_input_stream; limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(ciproxy, settings.maxRequestSize, true); } else { limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, 0); } req.bodyReader = limited_http_input_stream; // handle Expect header if (auto pv = "Expect" in req.headers) { if (icmp2(*pv, "100-continue") == 0) { logTrace("sending 100 continue"); http_stream.write("HTTP/1.1 100 Continue\r\n\r\n"); } } // eagerly parse the URL as its lightweight and defacto @nogc auto url = URL.parse(req.requestURI); req.queryString = url.queryString; req.username = url.username; req.password = url.password; req.requestPath = url.path; // lookup the session if (settings.sessionStore) { // use the first cookie that contains a valid session ID in case // of multiple matching session cookies foreach (val; req.cookies.getAll(settings.sessionIdCookie)) { req.session = settings.sessionStore.open(val); res.m_session = req.session; if (req.session) break; } } // write default headers if (req.method == HTTPMethod.HEAD) res.m_isHeadResponse = true; if (settings.serverString.length) res.headers["Server"] = settings.serverString; res.headers["Date"] = formatRFC822DateAlloc(request_allocator, reqtime); if (req.persistent) res.headers["Keep-Alive"] = formatAlloc(request_allocator, "timeout=%d", settings.keepAliveTimeout.total!"seconds"()); // finished parsing the request parsed = true; logTrace("persist: %s", req.persistent); keep_alive = req.persistent; // handle the request logTrace("handle request (body %d)", req.bodyReader.leastSize); res.httpVersion = req.httpVersion; request_task(req, res); // if no one has written anything, return 404 if (!res.headerWritten) { string dbg_msg; logDiagnostic("No response written for %s", req.requestURI); if (settings.options & HTTPServerOption.errorStackTraces) dbg_msg = format("No routes match path '%s'", req.requestURI); errorOut(HTTPStatus.notFound, httpStatusText(HTTPStatus.notFound), dbg_msg, null); } } catch (HTTPStatusException err) { if (!res.headerWritten) errorOut(err.status, err.msg, err.debugMessage, err); else logDiagnostic("HTTPSterrorOutatusException while writing the response: %s", err.msg); debug logDebug("Exception while handling request %s %s: %s", req.method, req.requestURI, () @trusted { return err.toString().sanitize; } ()); if (!parsed || res.headerWritten || justifiesConnectionClose(err.status)) keep_alive = false; } catch (UncaughtException e) { auto status = parsed ? HTTPStatus.internalServerError : HTTPStatus.badRequest; string dbg_msg; if (settings.options & HTTPServerOption.errorStackTraces) dbg_msg = () @trusted { return e.toString().sanitize; } (); if (!res.headerWritten && tcp_connection.connected) errorOut(status, httpStatusText(status), dbg_msg, e); else logDiagnostic("Error while writing the response: %s", e.msg); debug logDebug("Exception while handling request %s %s: %s", req.method, req.requestURI, () @trusted { return e.toString().sanitize(); } ()); if (!parsed || res.headerWritten || !cast(Exception)e) keep_alive = false; } if (tcp_connection.connected) { if (req.bodyReader && !req.bodyReader.empty) { req.bodyReader.pipe(nullSink); logTrace("dropped body"); } } // finalize (e.g. for chunked encoding) res.finalize(); foreach (k, v ; req._files) { if (existsFile(v.tempPath)) { removeFile(v.tempPath); logDebug("Deleted upload tempfile %s", v.tempPath.toString()); } } if (!req.noLog) { // log the request to access log foreach (log; context.loggers) log.log(req, res); } //logTrace("return %s (used pool memory: %s/%s)", keep_alive, request_allocator.allocatedSize, request_allocator.totalSize); logTrace("return %s", keep_alive); return keep_alive != false; } private void parseRequestHeader(InputStream)(HTTPServerRequest req, InputStream http_stream, IAllocator alloc, ulong max_header_size) if (isInputStream!InputStream) { auto stream = FreeListRef!LimitedHTTPInputStream(http_stream, max_header_size); logTrace("HTTP server reading status line"); auto reqln = () @trusted { return cast(string)stream.readLine(MaxHTTPHeaderLineLength, "\r\n", alloc); }(); logTrace("--------------------"); logTrace("HTTP server request:"); logTrace("--------------------"); logTrace("%s", reqln); //Method auto pos = reqln.indexOf(' '); enforceBadRequest(pos >= 0, "invalid request method"); req.method = httpMethodFromString(reqln[0 .. pos]); reqln = reqln[pos+1 .. $]; //Path pos = reqln.indexOf(' '); enforceBadRequest(pos >= 0, "invalid request path"); req.requestURI = reqln[0 .. pos]; reqln = reqln[pos+1 .. $]; req.httpVersion = parseHTTPVersion(reqln); //headers parseRFC5322Header(stream, req.headers, MaxHTTPHeaderLineLength, alloc, false); foreach (k, v; req.headers) logTrace("%s: %s", k, v); logTrace("--------------------"); } private void parseCookies(string str, ref CookieValueMap cookies) @safe { import std.encoding : sanitize; import std.array : split; import std.string : strip; import std.algorithm.iteration : map, filter, each; import vibe.http.common : Cookie; () @trusted { return str.sanitize; } () .split(";") .map!(kv => kv.strip.split("=")) .filter!(kv => kv.length == 2) //ignore illegal cookies .each!(kv => cookies.add(kv[0], kv[1], Cookie.Encoding.raw) ); } unittest { auto cvm = CookieValueMap(); parseCookies("foo=bar;; baz=zinga; öö=üü ; møøse=was=sacked; onlyval1; =onlyval2; onlykey=", cvm); assert(cvm["foo"] == "bar"); assert(cvm["baz"] == "zinga"); assert(cvm["öö"] == "üü"); assert( "møøse" ! in cvm); //illegal cookie gets ignored assert( "onlyval1" ! in cvm); //illegal cookie gets ignored assert(cvm["onlykey"] == ""); assert(cvm[""] == "onlyval2"); assert(cvm.length() == 5); cvm = CookieValueMap(); parseCookies("", cvm); assert(cvm.length() == 0); cvm = CookieValueMap(); parseCookies(";;=", cvm); assert(cvm.length() == 1); assert(cvm[""] == ""); } shared static this() { version (VibeNoDefaultArgs) {} else { string disthost = s_distHost; ushort distport = s_distPort; import vibe.core.args : readOption; readOption("disthost|d", () @trusted { return &disthost; } (), "Sets the name of a vibedist server to use for load balancing."); readOption("distport", () @trusted { return &distport; } (), "Sets the port used for load balancing."); setVibeDistHost(disthost, distport); } } private string formatRFC822DateAlloc(IAllocator alloc, SysTime time) @safe { auto app = AllocAppender!string(alloc); writeRFC822DateTimeString(app, time); return () @trusted { return app.data; } (); } version (VibeDebugCatchAll) private alias UncaughtException = Throwable; else private alias UncaughtException = Exception;