1 module vibe.http.server; 2 3 public import vibe.core.net; 4 import vibe.core.stream; 5 import vibe.http.internal.http1; 6 import vibe.http.internal.http2.settings; 7 import vibe.http.internal.http2.exchange; 8 9 public import vibe.http.log; 10 public import vibe.http.common; 11 public import vibe.http.session; 12 import vibe.inet.message; 13 import vibe.core.file; 14 import vibe.core.log; 15 import vibe.inet.url; 16 import vibe.inet.webform; 17 import vibe.data.json; 18 import vibe.internal.allocator; 19 import vibe.internal.freelistref; 20 import vibe.internal.interfaceproxy : InterfaceProxy; 21 import vibe.stream.wrapper : ConnectionProxyStream, createConnectionProxyStream, createConnectionProxyStreamFL; 22 import vibe.stream.tls; 23 import vibe.utils.array; 24 import vibe.utils..string; 25 import vibe.stream.counting; 26 import vibe.stream.operations; 27 import vibe.stream.zlib; 28 import vibe.textfilter.urlencode : urlEncode, urlDecode; 29 30 import std.datetime; 31 import std.typecons; 32 import std.conv; 33 import std.array; 34 import std.algorithm; 35 import std.format; 36 import std.parallelism; 37 import std.exception; 38 import std..string; 39 import std.traits; 40 import std.encoding : sanitize; 41 42 version (VibeNoSSL) version = HaveNoTLS; 43 else version (Have_botan) {} 44 else version (Have_openssl) {} 45 else version = HaveNoTLS; 46 47 /**************************************************************************************************/ 48 /* Public functions */ 49 /**************************************************************************************************/ 50 51 /** 52 Starts a HTTP server listening on the specified port. 53 54 request_handler will be called for each HTTP request that is made. The 55 res parameter of the callback then has to be filled with the response 56 data. 57 58 request_handler can be either HTTPServerRequestDelegate/HTTPServerRequestFunction 59 or a class/struct with a member function 'handleRequest' that has the same 60 signature. 61 62 Note that if the application has been started with the --disthost command line 63 switch, listenHTTP() will automatically listen on the specified VibeDist host 64 instead of locally. This allows for a seamless switch from single-host to 65 multi-host scenarios without changing the code. If you need to listen locally, 66 use listenHTTPPlain() instead. 67 68 Params: 69 settings = Customizes the HTTP servers functionality (host string or HTTPServerSettings object) 70 request_handler = This callback is invoked for each incoming request and is responsible 71 for generating the response. 72 73 Returns: 74 A handle is returned that can be used to stop listening for further HTTP 75 requests with the supplied settings. Another call to `listenHTTP` can be 76 used afterwards to start listening again. 77 */ 78 import vibe.http.router; 79 HTTPListener listenHTTP(alias Handler)(HTTPServerSettings settings) 80 if (is(typeof(Handler) == URLRouter) || is(typeof(Handler) : HTTPServerRequestHandler)) 81 { 82 //if (!settings) 83 //settings = HTTPServerSettings; 84 return listenHTTPPlain(settings, (req, res) @trusted => Handler.handleRequest(req, res)); 85 } 86 87 import std.traits; 88 import std.typetuple; 89 HTTPListener listenHTTP(alias Handler)(HTTPServerSettings settings) 90 if ((isCallable!Handler) 91 && is(ReturnType!Handler == void) 92 && is(ParameterTypeTuple!Handler == TypeTuple!(HTTPServerRequest, HTTPServerResponse))) 93 { 94 //if (!settings) 95 //settings = HTTPServerSettings; 96 return listenHTTPPlain(settings, (req, res) @trusted => Handler(req, res)); 97 } 98 99 HTTPListener listenHTTP(H)(HTTPServerSettings settings, H handler) 100 { 101 return listenHTTP!handler(settings); 102 } 103 104 HTTPListener listenHTTP(H)(string bind_string, H handler) 105 { 106 auto settings = new HTTPServerSettings(bind_string); 107 return listenHTTP!handler(settings); 108 } 109 110 /* Testing listenHTTP 111 */ 112 unittest 113 { 114 void test() 115 { 116 static void testSafeFunction(HTTPServerRequest req, HTTPServerResponse res) @safe {} 117 listenHTTP("0.0.0.0:8080", &testSafeFunction); 118 listenHTTP(":8080", new class HTTPServerRequestHandler { 119 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe {} 120 }); 121 //listenHTTP(":8080", (req, res) {}); // fails on parameter type tuple 122 123 static void testSafeFunctionS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} 124 listenHTTP(":8080", &testSafeFunctionS); 125 void testSafeDelegateS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} 126 listenHTTP(":8080", &testSafeDelegateS); 127 listenHTTP(":8080", new class HTTPServerRequestHandler { 128 void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {} 129 }); 130 //listenHTTP(":8080", (scope req, scope res) {}); // fails on parameter type tuple 131 } 132 } 133 134 unittest { 135 import vibe.http.router; 136 137 void test() 138 { 139 auto router = new URLRouter; 140 router.get("/old_url", staticRedirect("http://example.org/new_url", HTTPStatus.movedPermanently)); 141 HTTPServerSettings settings; 142 listenHTTP!router(settings); 143 } 144 } 145 146 unittest { 147 // testing a callable as request handler 148 void handleRequest (HTTPServerRequest req, HTTPServerResponse res) 149 @safe { 150 if (req.path == "/") 151 res.writeBody("Hello, World! Delegate"); 152 } 153 154 auto settings = new HTTPServerSettings(); 155 settings.port = 8060; 156 settings.bindAddresses = ["localhost"]; 157 158 listenHTTP!handleRequest(settings); 159 } 160 161 162 /** 163 Provides a HTTP request handler that responds with a static Diet template. 164 */ 165 @property HTTPServerRequestDelegateS staticTemplate(string template_file)() 166 { 167 return (scope HTTPServerRequest req, scope HTTPServerResponse res){ 168 res.render!(template_file, req); 169 }; 170 } 171 172 173 /** 174 Provides a HTTP request handler that responds with a static redirection to the specified URL. 175 176 Params: 177 url = The URL to redirect to 178 status = Redirection status to use $(LPAREN)by default this is $(D HTTPStatus.found)$(RPAREN). 179 180 Returns: 181 Returns a $(D HTTPServerRequestDelegate) that performs the redirect 182 */ 183 HTTPServerRequestDelegate staticRedirect(string url, HTTPStatus status = HTTPStatus.found) 184 @safe { 185 return (HTTPServerRequest req, HTTPServerResponse res){ 186 res.redirect(url, status); 187 }; 188 } 189 /// ditto 190 HTTPServerRequestDelegate staticRedirect(URL url, HTTPStatus status = HTTPStatus.found) 191 @safe { 192 return (HTTPServerRequest req, HTTPServerResponse res){ 193 res.redirect(url, status); 194 }; 195 } 196 197 /// 198 199 /** 200 Sets a VibeDist host to register with. 201 */ 202 void setVibeDistHost(string host, ushort port) 203 @safe { 204 s_distHost = host; 205 s_distPort = port; 206 } 207 208 209 /** 210 Renders the given Diet template and makes all ALIASES available to the template. 211 212 You can call this function as a pseudo-member of `HTTPServerResponse` using 213 D's uniform function call syntax. 214 215 See_also: `diet.html.compileHTMLDietFile` 216 217 Examples: 218 --- 219 string title = "Hello, World!"; 220 int pageNumber = 1; 221 res.render!("mytemplate.dt", title, pageNumber); 222 --- 223 */ 224 @property void render(string template_file, ALIASES...)(HTTPServerResponse res) 225 { 226 res.contentType = "text/html; charset=UTF-8"; 227 version (VibeUseOldDiet) 228 pragma(msg, "VibeUseOldDiet is not supported anymore. Please undefine in the package recipe."); 229 import vibe.stream.wrapper : streamOutputRange; 230 import diet.html : compileHTMLDietFile; 231 auto output = streamOutputRange!1024(res.bodyWriter); 232 compileHTMLDietFile!(template_file, ALIASES, DefaultDietFilters)(output); 233 } 234 235 version (Have_diet_ng) 236 { 237 import diet.traits; 238 239 /** 240 Provides the default `css`, `javascript`, `markdown` and `htmlescape` filters 241 */ 242 @dietTraits 243 struct DefaultDietFilters { 244 import diet.html : HTMLOutputStyle; 245 import std..string : splitLines; 246 247 version (VibeOutputCompactHTML) enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.compact; 248 else enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; 249 250 static string filterCss(I)(I text, size_t indent = 0) 251 { 252 auto lines = splitLines(text); 253 254 string indent_string = "\n"; 255 while (indent-- > 0) indent_string ~= '\t'; 256 257 string ret = indent_string~"<style type=\"text/css\"><!--"; 258 indent_string = indent_string ~ '\t'; 259 foreach (ln; lines) ret ~= indent_string ~ ln; 260 indent_string = indent_string[0 .. $-1]; 261 ret ~= indent_string ~ "--></style>"; 262 263 return ret; 264 } 265 266 267 static string filterJavascript(I)(I text, size_t indent = 0) 268 { 269 auto lines = splitLines(text); 270 271 string indent_string = "\n"; 272 while (indent-- > 0) indent_string ~= '\t'; 273 274 string ret = indent_string~"<script type=\"application/javascript\">"; 275 ret ~= indent_string~'\t' ~ "//<![CDATA["; 276 foreach (ln; lines) ret ~= indent_string ~ '\t' ~ ln; 277 ret ~= indent_string ~ '\t' ~ "//]]>" ~ indent_string ~ "</script>"; 278 279 return ret; 280 } 281 282 static string filterMarkdown(I)(I text) 283 { 284 import vibe.textfilter.markdown : markdown = filterMarkdown; 285 // TODO: indent 286 return markdown(text); 287 } 288 289 static string filterHtmlescape(I)(I text) 290 { 291 import vibe.textfilter.html : htmlEscape; 292 // TODO: indent 293 return htmlEscape(text); 294 } 295 296 static this() 297 { 298 filters["css"] = (input, scope output) { output(filterCss(input)); }; 299 filters["javascript"] = (input, scope output) { output(filterJavascript(input)); }; 300 filters["markdown"] = (input, scope output) { output(filterMarkdown(() @trusted { return cast(string)input; } ())); }; 301 filters["htmlescape"] = (input, scope output) { output(filterHtmlescape(input)); }; 302 } 303 304 static SafeFilterCallback[string] filters; 305 } 306 307 308 unittest { 309 static string compile(string diet)() { 310 import std.array : appender; 311 import std..string : strip; 312 import diet.html : compileHTMLDietString; 313 auto dst = appender!string; 314 dst.compileHTMLDietString!(diet, DefaultDietFilters); 315 return strip(cast(string)(dst.data)); 316 } 317 318 assert(compile!":css .test" == "<style type=\"text/css\"><!--\n\t.test\n--></style>"); 319 assert(compile!":javascript test();" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); 320 assert(compile!":markdown **test**" == "<p><strong>test</strong>\n</p>"); 321 assert(compile!":htmlescape <test>" == "<test>"); 322 assert(compile!":css !{\".test\"}" == "<style type=\"text/css\"><!--\n\t.test\n--></style>"); 323 assert(compile!":javascript !{\"test();\"}" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); 324 assert(compile!":markdown !{\"**test**\"}" == "<p><strong>test</strong>\n</p>"); 325 assert(compile!":htmlescape !{\"<test>\"}" == "<test>"); 326 assert(compile!":javascript\n\ttest();" == "<script type=\"application/javascript\">\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); 327 } 328 } 329 330 331 /** 332 Creates a HTTPServerRequest suitable for writing unit tests. 333 */ 334 HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method = HTTPMethod.GET, InputStream data = null) 335 @safe { 336 InetHeaderMap headers; 337 return createTestHTTPServerRequest(url, method, headers, data); 338 } 339 /// ditto 340 HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method, InetHeaderMap headers, InputStream data = null) 341 @safe { 342 auto tls = url.schema == "https"; 343 auto ret = HTTPServerRequest(Clock.currTime(UTC()), url.port ? url.port : tls ? 443 : 80); 344 ret.requestPath = url.path; 345 ret.queryString = url.queryString; 346 ret.username = url.username; 347 ret.password = url.password; 348 ret.requestURI = url.localURI; 349 ret.method = method; 350 ret.tls = tls; 351 //ret.headers = headers; // TODO compiler error 352 ret.bodyReader = data; 353 return ret; 354 } 355 356 /** 357 Creates a HTTPServerResponse suitable for writing unit tests. 358 359 Params: 360 data_sink = Optional output stream that captures the data that gets 361 written to the response 362 session_store = Optional session store to use when sessions are involved 363 data_mode = If set to `TestHTTPResponseMode.bodyOnly`, only the body 364 contents get written to `data_sink`. Otherwise the raw response 365 including the HTTP header is written. 366 */ 367 HTTPServerResponse createTestHTTPServerResponse(OutputStream data_sink = null, 368 SessionStore session_store = null, 369 TestHTTPResponseMode data_mode = TestHTTPResponseMode.plain) 370 @safe { 371 import vibe.stream.wrapper; 372 373 auto settings = new HTTPServerSettings; 374 if (session_store) { 375 //settings = HTTPServerSettings; 376 settings.sessionStore = session_store; 377 } 378 379 InterfaceProxy!Stream outstr; 380 if (data_sink && data_mode == TestHTTPResponseMode.plain) 381 outstr = createProxyStream(Stream.init, data_sink); 382 else outstr = createProxyStream(Stream.init, nullSink); 383 384 auto ret = HTTPServerResponse(outstr, InterfaceProxy!ConnectionStream.init, 385 settings, () @trusted { return vibeThreadAllocator(); } ()); 386 if (data_sink && data_mode == TestHTTPResponseMode.bodyOnly) ret.m_data.m_bodyWriter = data_sink; 387 return ret; 388 } 389 390 391 /**************************************************************************************************/ 392 /* Public types */ 393 /**************************************************************************************************/ 394 395 /// Interface for class based request handlers 396 interface HTTPServerRequestHandler { 397 /// Handles incoming HTTP requests 398 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe ; 399 } 400 401 402 alias HTTPContext = HTTPServerContext; 403 404 /// Delegate based request handler 405 alias HTTPServerRequestDelegate = void delegate(HTTPServerRequest req, HTTPServerResponse res) @safe; 406 alias HTTPServerRequestDelegateS = void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; 407 /// Static function based request handler 408 alias HTTPServerRequestFunction = void function(HTTPServerRequest req, HTTPServerResponse res) @safe; 409 410 411 /// Aggregates all information about an HTTP error status. 412 struct HTTPServerErrorInfo { 413 /// The HTTP status code 414 int code; 415 /// The error message 416 string message; 417 /// Extended error message with debug information such as a stack trace 418 string debugMessage; 419 /// The error exception, if any 420 Throwable exception; 421 } 422 423 424 /// Delegate type used for user defined error page generator callbacks. 425 alias HTTPServerErrorPageHandler = void delegate(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) @safe; 426 427 428 enum TestHTTPResponseMode { 429 plain, 430 bodyOnly 431 } 432 433 434 private enum HTTPServerOptionImpl { 435 none = 0, 436 errorStackTraces = 1<<7, 437 reusePort = 1<<8, 438 distribute = 1<<9 // deprecated 439 } 440 441 // TODO: Should be turned back into an enum once the deprecated symbols can be removed 442 /** 443 Specifies optional features of the HTTP server. 444 445 Disabling unneeded features can speed up the server or reduce its memory usage. 446 447 Note that the options `parseFormBody`, `parseJsonBody` and `parseMultiPartBody` 448 will also drain the `HTTPServerRequest.bodyReader` stream whenever a request 449 body with form or JSON data is encountered. 450 */ 451 struct HTTPServerOption { 452 static enum none = HTTPServerOptionImpl.none; 453 deprecated("This is done lazily. It will be removed in 0.9.") 454 static enum parseURL = none; 455 deprecated("This is done lazily. It will be removed in 0.9.") 456 static enum parseQueryString = none; 457 deprecated("This is done lazily. It will be removed in 0.9.") 458 static enum parseFormBody = none; 459 deprecated("This is done lazily. It will be removed in 0.9.") 460 static enum parseJsonBody = none; 461 deprecated("This is done lazily. It will be removed in 0.9.") 462 static enum parseMultiPartBody = none; 463 /* Deprecated: Distributes request processing among worker threads 464 465 Note that this functionality assumes that the request handler 466 is implemented in a thread-safe way. However, the D type system 467 is bypassed, so that no static verification takes place. 468 469 For this reason, it is recommended to instead use 470 `vibe.core.core.runWorkerTaskDist` and call `listenHTTP` 471 from each task/thread individually. If the `reusePort` option 472 is set, then all threads will be able to listen on the same port, 473 with the operating system distributing the incoming connections. 474 475 If possible, instead of threads, the use of separate processes 476 is more robust and often faster. The `reusePort` option works 477 the same way in this scenario. 478 */ 479 deprecated("Use runWorkerTaskDist or start threads separately. It will be removed in 0.9.") 480 static enum distribute = HTTPServerOptionImpl.distribute; 481 /* Enables stack traces (`HTTPServerErrorInfo.debugMessage`). 482 483 Note that generating the stack traces are generally a costly 484 operation that should usually be avoided in production 485 environments. It can also reveal internal information about 486 the application, such as function addresses, which can 487 help an attacker to abuse possible security holes. 488 */ 489 static enum errorStackTraces = HTTPServerOptionImpl.errorStackTraces; 490 /// Enable port reuse in `listenTCP()` 491 static enum reusePort = HTTPServerOptionImpl.reusePort; 492 493 /* The default set of options. 494 495 Includes all parsing options, as well as the `errorStackTraces` 496 option if the code is compiled in debug mode. 497 */ 498 static enum defaults = () { debug return HTTPServerOptionImpl.errorStackTraces; else return HTTPServerOptionImpl.none; } ().HTTPServerOption; 499 500 deprecated("None has been renamed to none.") 501 static enum None = none; 502 deprecated("This is done lazily. It will be removed in 0.9.") 503 static enum ParseURL = none; 504 deprecated("This is done lazily. It will be removed in 0.9.") 505 static enum ParseQueryString = none; 506 deprecated("This is done lazily. It will be removed in 0.9.") 507 static enum ParseFormBody = none; 508 deprecated("This is done lazily. It will be removed in 0.9.") 509 static enum ParseJsonBody = none; 510 deprecated("This is done lazily. It will be removed in 0.9.") 511 static enum ParseMultiPartBody = none; 512 deprecated("This is done lazily. It will be removed in 0.9.") 513 static enum ParseCookies = none; 514 515 HTTPServerOptionImpl x; 516 alias x this; 517 } 518 519 520 /** 521 Contains all settings for configuring a basic HTTP server. 522 523 The defaults are sufficient for most normal uses. 524 */ 525 final class HTTPServerSettings { 526 /** The port on which the HTTP server is listening. 527 528 The default value is 80. If you are running a TLS enabled server you may want to set this 529 to 443 instead. 530 531 Using a value of `0` instructs the server to use any available port on 532 the given `bindAddresses` the actual addresses and ports can then be 533 queried with `TCPListener.bindAddresses`. 534 */ 535 ushort port = 80; 536 537 /** The interfaces on which the HTTP server is listening. 538 539 By default, the server will listen on all IPv4 and IPv6 interfaces. 540 */ 541 string[] bindAddresses = ["::", "0.0.0.0"]; 542 543 /** Determines the server host name. 544 545 If multiple servers are listening on the same port, the host name will determine which one 546 gets a request. 547 */ 548 string hostName; 549 550 /** Provides a way to reject incoming connections as early as possible. 551 552 Allows to ban and unban network addresses and reduce the impact of DOS 553 attacks. 554 555 If the callback returns `true` for a specific `NetworkAddress`, 556 then all incoming requests from that address will be rejected. 557 */ 558 RejectConnectionPredicate rejectConnectionPredicate; 559 560 /** Configures optional features of the HTTP server 561 562 Disabling unneeded features can improve performance or reduce the server 563 load in case of invalid or unwanted requests (DoS). By default, 564 HTTPServerOption.defaults is used. 565 */ 566 HTTPServerOptionImpl options = HTTPServerOption.defaults; 567 568 /** Time of a request after which the connection is closed with an error; not supported yet 569 570 The default limit of 0 means that the request time is not limited. 571 */ 572 Duration maxRequestTime = 0.seconds; 573 574 /** Maximum time between two request on a keep-alive connection 575 576 The default value is 10 seconds. 577 */ 578 Duration keepAliveTimeout = 10.seconds; 579 580 /// Maximum number of transferred bytes per request after which the connection is closed with 581 /// an error 582 ulong maxRequestSize = 2097152; 583 584 585 /// Maximum number of transferred bytes for the request header. This includes the request line 586 /// the url and all headers. 587 ulong maxRequestHeaderSize = 8192; 588 589 /// Sets a custom handler for displaying error pages for HTTP errors 590 @property HTTPServerErrorPageHandler errorPageHandler() @safe { return errorPageHandler_; } 591 /// ditto 592 @property void errorPageHandler(HTTPServerErrorPageHandler del) @safe { errorPageHandler_ = del; } 593 /// Scheduled for deprecation - use a `@safe` callback instead. 594 @property void errorPageHandler(void delegate(HTTPServerRequest, HTTPServerResponse, HTTPServerErrorInfo) @system del) 595 @system { 596 this.errorPageHandler = (req, res, err) @trusted { del(req, res, err); }; 597 } 598 599 void handleErrorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo err) 600 @safe { 601 errorPageHandler_(req, res, err); 602 } 603 604 private HTTPServerErrorPageHandler errorPageHandler_ = null; 605 606 /// If set, a HTTPS server will be started instead of plain HTTP. 607 TLSContext tlsContext; 608 609 /// Session management is enabled if a session store instance is provided 610 SessionStore sessionStore; 611 string sessionIdCookie = "vibe.session_id"; 612 613 /// 614 import vibe.core.core : vibeVersionString; 615 string serverString = "vibe.d/" ~ vibeVersionString; 616 617 /** Specifies the format used for the access log. 618 619 The log format is given using the Apache server syntax. By default NCSA combined is used. 620 621 --- 622 "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" 623 --- 624 */ 625 string accessLogFormat = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""; 626 627 /// Spefifies the name of a file to which access log messages are appended. 628 string accessLogFile = ""; 629 630 /// If set, access log entries will be output to the console. 631 bool accessLogToConsole = false; 632 633 /** Specifies a custom access logger instance. 634 */ 635 HTTPLogger accessLogger; 636 637 /// Returns a duplicate of the settings object. 638 @property HTTPServerSettings dup() 639 @safe { 640 //auto ret = HTTPServerSettings; 641 auto ret = new HTTPServerSettings; 642 foreach (mem; __traits(allMembers, HTTPServerSettings)) { 643 static if (mem == "sslContext") {} 644 else static if (mem == "bindAddresses") ret.bindAddresses = bindAddresses.dup; 645 else static if (__traits(compiles, __traits(getMember, ret, mem) = __traits(getMember, this, mem))) 646 __traits(getMember, ret, mem) = __traits(getMember, this, mem); 647 } 648 return ret; 649 } 650 651 /// Disable support for VibeDist and instead start listening immediately. 652 bool disableDistHost = false; 653 654 /** Responds to "Accept-Encoding" by using compression if possible. 655 656 Compression can also be manually enabled by setting the 657 "Content-Encoding" header of the HTTP response appropriately before 658 sending the response body. 659 660 This setting is disabled by default. Also note that there are still some 661 known issues with the GZIP compression code. 662 */ 663 bool useCompressionIfPossible = false; 664 665 666 /** Interval between WebSocket ping frames. 667 668 The default value is 60 seconds; set to Duration.zero to disable pings. 669 */ 670 Duration webSocketPingInterval = 60.seconds; 671 672 /** Constructs a new settings object with default values. 673 */ 674 this() @safe {} 675 676 /** Constructs a new settings object with a custom bind interface and/or port. 677 678 The syntax of `bind_string` is `[<IP address>][:<port>]`, where either of 679 the two parts can be left off. IPv6 addresses must be enclosed in square 680 brackets, as they would within a URL. 681 682 Throws: 683 An exception is thrown if `bind_string` is malformed. 684 */ 685 this(string bind_string) 686 @safe { 687 //this(); 688 689 if (bind_string.startsWith('[')) { 690 auto idx = bind_string.indexOf(']'); 691 enforce(idx > 0, "Missing closing bracket for IPv6 address."); 692 bindAddresses = [bind_string[1 .. idx]]; 693 bind_string = bind_string[idx+1 .. $]; 694 695 enforce(bind_string.length == 0 || bind_string.startsWith(':'), 696 "Only a colon may follow the IPv6 address."); 697 } 698 699 auto idx = bind_string.indexOf(':'); 700 if (idx < 0) { 701 if (bind_string.length > 0) bindAddresses = [bind_string]; 702 } else { 703 if (idx > 0) bindAddresses = [bind_string[0 .. idx]]; 704 port = bind_string[idx+1 .. $].to!ushort; 705 } 706 } 707 708 /// 709 unittest { 710 auto s = new HTTPServerSettings(":8080"); 711 assert(s.bindAddresses == ["::", "0.0.0.0"]); // default bind addresses 712 assert(s.port == 8080); 713 714 s = new HTTPServerSettings("123.123.123.123"); 715 assert(s.bindAddresses == ["123.123.123.123"]); 716 assert(s.port == 80); 717 718 s = new HTTPServerSettings("[::1]:443"); 719 assert(s.bindAddresses == ["::1"]); 720 assert(s.port == 443); 721 } 722 } 723 724 725 /// Callback type used to determine whether to reject incoming connections 726 alias RejectConnectionPredicate = bool delegate (in NetworkAddress) @safe nothrow; 727 728 729 /** Options altering how sessions are created. 730 731 Multiple values can be or'ed together. 732 733 See_Also: HTTPServerResponse.startSession 734 */ 735 enum SessionOption { 736 /// No options. 737 none = 0, 738 739 /* Instructs the browser to disallow accessing the session ID from JavaScript. 740 741 See_Also: Cookie.httpOnly 742 */ 743 httpOnly = 1<<0, 744 745 /* Instructs the browser to disallow sending the session ID over 746 unencrypted connections. 747 748 By default, the type of the connection on which the session is started 749 will be used to determine if secure or noSecure is used. 750 751 See_Also: noSecure, Cookie.secure 752 */ 753 secure = 1<<1, 754 755 /* Instructs the browser to allow sending the session ID over unencrypted 756 connections. 757 758 By default, the type of the connection on which the session is started 759 will be used to determine if secure or noSecure is used. 760 761 See_Also: secure, Cookie.secure 762 */ 763 noSecure = 1<<2 764 } 765 766 767 768 /** 769 Represents a HTTP request as received by the server side. 770 */ 771 struct HTTPServerRequest { 772 import vibe.utils.dictionarylist : DictionaryList; 773 774 private HTTPServerRequestData* m_data; 775 776 @safe: 777 778 this (HTTPServerRequestData* data) 779 { 780 m_data = data; 781 } 782 783 this (SysTime reqtime, ushort bindPort) 784 { 785 auto data = new HTTPServerRequestData(reqtime, bindPort); 786 () @trusted { m_data = data; } (); 787 } 788 789 auto opCast(T)() const if (is(T == bool)) { return m_data !is null; } 790 791 package @property scope const(HTTPServerSettings) serverSettings() 792 const { 793 return m_data.serverSettings; 794 } 795 796 @property scope data() pure nothrow { return m_data; } 797 798 @property scope string requestURI() const pure nothrow { return m_data.requestURI; } 799 // ditto 800 @property void requestURI(string uri) pure nothrow { m_data.requestURI = uri; } 801 802 @property scope string peer() { return m_data.peer; } 803 804 @property scope CookieValueMap cookies() { return m_data.cookies; } 805 806 @property scope FormFields query() { return m_data.query; } 807 808 @property scope Json json() { return m_data.json; } 809 810 @property scope FormFields form() { return m_data.form; } 811 812 @property scope FilePartFormFields files() { return m_data.files; } 813 814 @property scope SysTime timeCreated() const { return m_data.timeCreated; } 815 816 @property scope URL fullURL() const { return m_data.fullURL; } 817 818 @property scope string rootDir() const pure nothrow { return m_data.rootDir; } 819 820 @property scope string username() const pure nothrow { return m_data.username; } 821 // ditto 822 @property void username(string name) nothrow { m_data.username = name; } 823 824 @property scope string path() pure { return m_data.path; } 825 // ditto 826 @property void path(scope string p) { m_data.path = path; } 827 828 @property ref InetHeaderMap headers() pure nothrow { return m_data.headers; } 829 830 @property scope bool persistent() const pure nothrow { return m_data.persistent; } 831 832 @property scope string queryString() const pure nothrow { return m_data.queryString; } 833 // ditto 834 @property void queryString(string qstr) nothrow { m_data.queryString = qstr; } 835 836 /** A map of general parameters for the request. 837 838 This map is supposed to be used by middleware functionality to store 839 information for later stages. For example vibe.http.router.URLRouter uses this map 840 to store the value of any named placeholders. 841 */ 842 @property scope ref DictionaryList!(string, true, 8) params() pure nothrow { return m_data.params; } 843 844 @property scope string requestURL() const pure nothrow { return m_data.requestURL; } 845 846 @property scope HTTPVersion httpVersion() const pure nothrow { return m_data.httpVersion; } 847 // ditto 848 @property void httpVersion(HTTPVersion hver) nothrow { m_data.httpVersion = hver; } 849 850 @property scope string host() const pure nothrow { return m_data.host; } 851 // ditto 852 @property void host(string v) { m_data.headers["Host"] = v; } 853 854 @property scope string contentType() const pure nothrow { return m_data.contentType; } 855 856 // ditto 857 @property void contentType(string ct) { m_data.headers["Content-Type"] = ct; } 858 859 @property scope string contentTypeParameters() const pure nothrow { return m_data.contentTypeParameters; } 860 861 @property scope NetworkAddress clientAddress() const pure nothrow { return m_data.clientAddress; } 862 // ditto 863 @property void clientAddress(NetworkAddress naddr) nothrow { m_data.clientAddress = naddr; } 864 865 @property scope bool tls() const pure nothrow { return m_data.tls; } 866 // ditto 867 @property void tls(bool val) nothrow { m_data.tls = val; } 868 869 @property scope HTTPMethod method() const pure nothrow { return m_data.method; } 870 // ditto 871 @property void method(HTTPMethod m) nothrow { m_data.method = m; } 872 873 package @property scope HTTPServerSettings m_settings() pure nothrow { return m_data.m_settings; } 874 // ditto 875 package @property void m_settings(HTTPServerSettings settings) nothrow { m_data.m_settings = settings; } 876 877 @property scope InputStream bodyReader() nothrow { return m_data.bodyReader; } 878 // ditto 879 @property void bodyReader(InputStream inStr) nothrow { m_data.bodyReader = inStr; } 880 881 @property scope string password() const pure nothrow { return m_data.password; } 882 // ditto 883 @property void password(string pwd) nothrow { m_data.password = pwd; } 884 885 @property scope Session session() pure nothrow { return m_data.session; } 886 // ditto 887 @property void session(Session session) { m_data.session = session; } 888 889 @property scope InetPath requestPath() const pure nothrow { return m_data.requestPath; } 890 // ditto 891 @property void requestPath(InetPath reqpath) nothrow { m_data.requestPath = reqpath; } 892 893 @property scope FilePartFormFields _files() pure nothrow { return m_data._files; } 894 895 @property scope bool noLog() const pure nothrow { return m_data.noLog; } 896 897 @property scope TLSCertificateInformation clientCertificate() 898 pure nothrow { 899 return m_data.clientCertificate; 900 } 901 902 @property void clientCertificate(TLSCertificateInformation cert) 903 pure nothrow { 904 m_data.clientCertificate = cert; 905 } 906 } 907 908 /** 909 Represents a HTTP response as sent from the server side. 910 */ 911 struct HTTPServerResponse { 912 @safe: 913 914 private HTTPServerResponseData* m_data; 915 916 this (HTTPServerResponseData* data) 917 { 918 m_data = data; 919 } 920 921 static if (!is(Stream == InterfaceProxy!Stream)) { 922 this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 923 @safe { 924 this(InterfaceProxy!Stream(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc); 925 } 926 } 927 928 this(InterfaceProxy!Stream conn, InterfaceProxy!ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 929 { 930 HTTPServerResponseData* data = new HTTPServerResponseData(conn, raw_connection, settings, req_alloc); 931 this(data); 932 } 933 934 auto opCast(T)() const @safe nothrow if (is(T == bool)) { return m_data !is null; } 935 936 @property scope data() @safe { return m_data; } 937 938 @property scope HTTPVersion httpVersion() { return m_data.httpVersion; } 939 940 @property void httpVersion(HTTPVersion h) { m_data.httpVersion = h; } 941 942 @property scope int statusCode() { return m_data.statusCode; } 943 944 @property void statusCode(scope HTTPStatus code) @safe { 945 m_data.statusCode = code; 946 } 947 948 @property void statusCode(scope int code) @safe { 949 m_data.statusCode = code; 950 } 951 952 @property scope string statusPhrase() { return m_data.statusPhrase; } 953 954 @property scope InetHeaderMap headers() { return m_data.headers; } 955 956 @property void headers(scope InetHeaderMap hmap) @safe { 957 m_data.headers = hmap; 958 } 959 960 @property scope Cookie[string] cookies() { return m_data.cookies; } 961 962 @property string toString() { return m_data.toString(); } 963 964 @property scope string contentType() { return m_data.contentType(); } 965 @property void contentType(string ct) { return m_data.contentType(ct); } 966 967 @property scope SysTime timeFinalized() const { return m_data.timeFinalized; } 968 969 @property scope bool headerWritten() const { return m_data.headerWritten; } 970 971 @property scope bool isHeadResponse() const { return m_data.isHeadResponse(); } 972 973 @property scope bool tls() const { return m_data.tls(); } 974 975 @property void tls(bool v) { m_data.m_tls = v; } 976 977 @property void m_settings(HTTPServerSettings s) { m_data.m_settings = s; } 978 979 @property void m_session(Session s) { m_data.m_session = s; } 980 981 @property void m_isHeadResponse(bool b) { m_data.m_isHeadResponse = b; } 982 983 void setStatusCode(int v) { m_data.statusCode = v; } 984 985 void writeBody(in ubyte[] data, string contentType = null) 986 { 987 m_data.writeBody(data, contentType); 988 } 989 void writeBody(scope InputStream data, string content_type = null) 990 { 991 m_data.writeBody(data, content_type); 992 } 993 void writeBody(string data, string content_type = null) 994 { 995 m_data.writeBody(data, content_type); 996 } 997 void writeBody(string data, int status, string content_type = null) 998 { 999 m_data.writeBody(data, status, content_type); 1000 } 1001 1002 void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe 1003 if (isRandomAccessStream!RandomAccessStream) 1004 { 1005 m_data.writeRawBody(stream); 1006 } 1007 /// ditto 1008 void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = 0) @safe 1009 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1010 { 1011 m_data.writeRawBody(stream, num_bytes); 1012 } 1013 /// ditto 1014 void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe 1015 if (isRandomAccessStream!RandomAccessStream) 1016 { 1017 m_data.writeRawBody(stream, status); 1018 } 1019 /// ditto 1020 void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe 1021 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1022 { 1023 m_data.writeRawBody(stream, status, num_bytes); 1024 } 1025 1026 /// Writes a JSON message with the specified status 1027 void writeJsonBody(T)(T data, int status, bool allow_chunked = false) 1028 { 1029 m_data.writeJsonBody(data, status, allow_chunked); 1030 } 1031 /// ditto 1032 void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false) 1033 { 1034 m_data.writeJsonBody(data, status, content_type, allow_chunked); 1035 } 1036 1037 /// ditto 1038 void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false) 1039 { 1040 m_data.writeJsonBody(data, content_type, allow_chunked); 1041 } 1042 /// ditto 1043 void writeJsonBody(T)(T data, bool allow_chunked = false) 1044 { 1045 m_data.writeJsonBody(data, allow_chunked); 1046 } 1047 /// ditto 1048 void writePrettyJsonBody(T)(T data, bool allow_chunked = false) 1049 { 1050 m_data.writePrettyJsonBody(data, allow_chunked); 1051 } 1052 1053 @property void writeVoidBody() 1054 { 1055 m_data.writeVoidBody(); 1056 } 1057 /// ditto 1058 @property void writeVoidBody(Stream)(Stream stream) 1059 { 1060 m_data.writeVoidBody(stream); 1061 } 1062 1063 @property InterfaceProxy!OutputStream bodyWriter() 1064 { 1065 return m_data.bodyWriter; 1066 } 1067 1068 package @property void bodyWriterH2(T)(ref T writer, const bool writeH = false) 1069 { 1070 m_data.bodyWriterH2(writer, writeH); 1071 } 1072 1073 /** Sends a redirect request to the client. 1074 1075 Params: 1076 url = The URL to redirect to 1077 status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found) 1078 */ 1079 void redirect(T)(T url, int status = HTTPStatus.Found) 1080 if(is(typeof(url) == string) || is(typeof(url) == URL)) 1081 { 1082 m_data.redirect(url, status); 1083 } 1084 1085 /** Special method sending a SWITCHING_PROTOCOLS response to the client. 1086 1087 Notice: For the overload that returns a `ConnectionStream`, it must be 1088 ensured that the returned instance doesn't outlive the request 1089 handler callback. 1090 1091 Params: 1092 protocol = The protocol set in the "Upgrade" header of the response. 1093 Use an empty string to skip setting this field. 1094 */ 1095 scope ConnectionStream switchProtocol(string protocol) 1096 { 1097 return m_data.switchProtocol(protocol); 1098 } 1099 /// ditto 1100 void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del) 1101 { 1102 m_data.switchProtocol(protocol, del); 1103 } 1104 /// ditto 1105 package void switchToHTTP2(HANDLER)(HANDLER handler, HTTP2ServerContext context) 1106 @safe { 1107 m_data.switchToHTTP2(handler, context); 1108 } 1109 1110 // Send a BadRequest and close connection (failed switch to HTTP/2) 1111 package void sendBadRequest() { 1112 m_data.sendBadRequest(); 1113 } 1114 1115 /** Special method for handling CONNECT proxy tunnel 1116 1117 Notice: For the overload that returns a `ConnectionStream`, it must be 1118 ensured that the returned instance doesn't outlive the request 1119 handler callback. 1120 */ 1121 scope ConnectionStream connectProxy() 1122 { 1123 return m_data.connectProxy(); 1124 } 1125 /// ditto 1126 void connectProxy(scope void delegate(scope ConnectionStream) @safe del) 1127 { 1128 m_data.connectProxy(del); 1129 } 1130 1131 /** Sets the specified cookie value. 1132 1133 Params: 1134 name = Name of the cookie 1135 value = New cookie value - pass null to clear the cookie 1136 path = Path (as seen by the client) of the directory tree in which the cookie is visible 1137 */ 1138 scope Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url) 1139 { 1140 return m_data.setCookie(name, value, path, encoding); 1141 } 1142 1143 /** 1144 Initiates a new session. 1145 1146 The session is stored in the SessionStore that was specified when 1147 creating the server. Depending on this, the session can be persistent 1148 or temporary and specific to this server instance. 1149 */ 1150 scope Session startSession(string path = "/", SessionOption options = SessionOption.httpOnly) 1151 { 1152 return m_data.startSession(path, options); 1153 } 1154 1155 /** 1156 Terminates the current session (if any). 1157 */ 1158 void terminateSession() 1159 { 1160 m_data.terminateSession(); 1161 } 1162 1163 @property scope ulong bytesWritten() const { return m_data.bytesWritten; } 1164 1165 /** 1166 Waits until either the connection closes, data arrives, or until the 1167 given timeout is reached. 1168 1169 Returns: 1170 $(D true) if the connection was closed and $(D false) if either the 1171 timeout was reached, or if data has arrived for consumption. 1172 1173 See_Also: `connected` 1174 */ 1175 bool waitForConnectionClose(Duration timeout = Duration.max) 1176 { 1177 return m_data.waitForConnectionClose(timeout); 1178 } 1179 1180 /** 1181 Determines if the underlying connection is still alive. 1182 1183 Returns $(D true) if the remote peer is still connected and $(D false) 1184 if the remote peer closed the connection. 1185 1186 See_Also: `waitForConnectionClose` 1187 */ 1188 @property bool connected() const { return m_data.connected; } 1189 1190 /** 1191 Finalizes the response. This is usually called automatically by the server. 1192 1193 This method can be called manually after writing the response to force 1194 all network traffic associated with the current request to be finalized. 1195 After the call returns, the `timeFinalized` property will be set. 1196 */ 1197 void finalize() { m_data.finalize(); } 1198 } 1199 1200 1201 /** 1202 Represents the request listener for a specific `listenHTTP` call. 1203 1204 This struct can be used to stop listening for HTTP requests at runtime. 1205 */ 1206 struct HTTPListener { 1207 private { 1208 size_t[] m_virtualHostIDs; 1209 } 1210 1211 private this(size_t[] ids) @safe { m_virtualHostIDs = ids; } 1212 1213 @property NetworkAddress[] bindAddresses() 1214 { 1215 NetworkAddress[] ret; 1216 foreach (l; s_contexts) 1217 if (l.m_virtualHosts.canFind!(v => m_virtualHostIDs.canFind(v.id))) { 1218 NetworkAddress a; 1219 a = resolveHost(l.bindAddress); 1220 a.port = l.bindPort; 1221 ret ~= a; 1222 } 1223 return ret; 1224 } 1225 1226 /** Stops handling HTTP requests and closes the TCP listening port if 1227 possible. 1228 */ 1229 void stopListening() 1230 @safe { 1231 import std.algorithm : countUntil; 1232 1233 foreach (vhid; m_virtualHostIDs) { 1234 foreach (lidx, l; s_contexts) { 1235 if (l.removeVirtualHost(vhid)) { 1236 if (!l.hasVirtualHosts) { 1237 l.stopListening(); 1238 logInfo("Stopped to listen for HTTP%s requests on %s:%s", l.tlsContext ? "S": "", l.bindAddress, l.bindPort); 1239 logInfo("Stopped to listen for HTTP%s requests on %s:%s", "", l.bindAddress, l.bindPort); 1240 s_contexts = s_contexts[0 .. lidx] ~ s_contexts[lidx+1 .. $]; 1241 } 1242 } 1243 break; 1244 } 1245 } 1246 } 1247 } 1248 1249 /** Represents a single HTTP server port. 1250 1251 This class defines the incoming interface, port, and TLS configuration of 1252 the public server port. The public server port may differ from the local 1253 one if a reverse proxy of some kind is facing the public internet and 1254 forwards to this HTTP server. 1255 1256 Multiple virtual hosts can be configured to be served from the same port. 1257 Their TLS settings must be compatible and each virtual host must have a 1258 */ 1259 final class HTTPServerContext { 1260 struct VirtualHost { 1261 HTTPServerRequestDelegate requestHandler; 1262 HTTPServerSettings settings; 1263 HTTPLogger[] loggers; 1264 size_t id; 1265 } 1266 1267 private { 1268 TCPListener m_listener; 1269 VirtualHost[] m_virtualHosts; 1270 string m_bindAddress; 1271 ushort m_bindPort; 1272 TLSContext m_tlsContext; 1273 static size_t s_vhostIDCounter = 1; 1274 } 1275 1276 @safe: 1277 1278 this(string bind_address, ushort bind_port) 1279 { 1280 m_bindAddress = bind_address; 1281 m_bindPort = bind_port; 1282 } 1283 1284 /** Returns the TLS context associated with the listener. 1285 1286 For non-HTTPS listeners, `null` will be returned. Otherwise, if only a 1287 single virtual host has been added, the TLS context of that host's 1288 settings is returned. For multiple virtual hosts, an SNI context is 1289 returned, which forwards to the individual contexts based on the 1290 requested host name. 1291 */ 1292 @property TLSContext tlsContext() { return m_tlsContext; } 1293 1294 /// The local network interface IP address associated with this listener 1295 @property string bindAddress() const { return m_bindAddress; } 1296 1297 /// The local port associated with this listener 1298 @property ushort bindPort() const { return m_bindPort; } 1299 1300 /// Determines if any virtual hosts have been addded 1301 @property bool hasVirtualHosts() const { return m_virtualHosts.length > 0; } 1302 1303 /// Make m_virtualhosts visible 1304 @property scope VirtualHost[] virtualHosts() { return m_virtualHosts; } 1305 1306 /** Adds a single virtual host. 1307 1308 Note that the port and bind address defined in `settings` must match the 1309 ones for this listener. The `settings.host` field must be unique for 1310 all virtual hosts. 1311 1312 Returns: Returns a unique ID for the new virtual host 1313 */ 1314 size_t addVirtualHost(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) 1315 { 1316 assert(settings.port == 0 || settings.port == m_bindPort, "Virtual host settings do not match bind port."); 1317 assert(settings.bindAddresses.canFind(m_bindAddress), "Virtual host settings do not match bind address."); 1318 1319 VirtualHost vhost; 1320 vhost.id = s_vhostIDCounter++; 1321 vhost.settings = settings; 1322 vhost.requestHandler = request_handler; 1323 1324 if (settings.accessLogger) vhost.loggers ~= settings.accessLogger; 1325 if (settings.accessLogToConsole) 1326 vhost.loggers ~= new HTTPConsoleLogger(settings, settings.accessLogFormat); 1327 if (settings.accessLogFile.length) 1328 vhost.loggers ~= new HTTPFileLogger(settings, settings.accessLogFormat, settings.accessLogFile); 1329 1330 if (!m_virtualHosts.length) m_tlsContext = settings.tlsContext; 1331 1332 enforce((m_tlsContext !is null) == (settings.tlsContext !is null), 1333 "Cannot mix HTTP and HTTPS virtual hosts within the same listener."); 1334 1335 if (m_tlsContext) addSNIHost(settings); 1336 1337 m_virtualHosts ~= vhost; 1338 1339 if (settings.hostName.length) { 1340 auto proto = settings.tlsContext ? "https" : "http"; 1341 auto port = settings.tlsContext && settings.port == 443 || !settings.tlsContext && settings.port == 80 ? "" : ":" ~ settings.port.to!string; 1342 logInfo("Added virtual host %s://%s:%s/ (%s)", proto, settings.hostName, m_bindPort, m_bindAddress); 1343 } 1344 1345 return vhost.id; 1346 } 1347 1348 /// Removes a previously added virtual host using its ID. 1349 bool removeVirtualHost(size_t id) 1350 { 1351 import std.algorithm.searching : countUntil; 1352 1353 auto idx = m_virtualHosts.countUntil!(c => c.id == id); 1354 if (idx < 0) return false; 1355 1356 auto ctx = m_virtualHosts[idx]; 1357 m_virtualHosts = m_virtualHosts[0 .. idx] ~ m_virtualHosts[idx+1 .. $]; 1358 return true; 1359 } 1360 1361 void stopListening() 1362 { 1363 m_listener.stopListening(); 1364 } 1365 1366 private void addSNIHost(HTTPServerSettings settings) 1367 { 1368 if (settings.tlsContext !is m_tlsContext && m_tlsContext.kind != TLSContextKind.serverSNI) { 1369 logDebug("Create SNI TLS context for %s, port %s", bindAddress, bindPort); 1370 m_tlsContext = createTLSContext(TLSContextKind.serverSNI); 1371 m_tlsContext.sniCallback = &onSNI; 1372 } 1373 1374 } 1375 1376 private TLSContext onSNI(string servername) 1377 { 1378 foreach (vhost; m_virtualHosts) 1379 if (vhost.settings.hostName.icmp(servername) == 0) { 1380 logDebug("Found context for SNI host '%s'.", servername); 1381 return vhost.settings.tlsContext; 1382 } 1383 logDebug("No context found for SNI host '%s'.", servername); 1384 return null; 1385 } 1386 } 1387 1388 1389 /**************************************************************************************************/ 1390 /* Private types */ 1391 /**************************************************************************************************/ 1392 1393 private enum MaxHTTPHeaderLineLength = 4096; 1394 1395 /**************************************************************************************************/ 1396 /* Private functions */ 1397 /**************************************************************************************************/ 1398 1399 private { 1400 import core.sync.mutex; 1401 1402 shared string s_distHost; 1403 shared ushort s_distPort = 11000; 1404 1405 HTTPServerContext[] s_listeners; 1406 } 1407 1408 1409 private { 1410 HTTPContext[] s_contexts; 1411 } 1412 1413 //private HTTPContext getDefaultHTTPContext(in ref NetworkAddress addr) 1414 1415 //assert(false, "TODO"); 1416 //} 1417 1418 1419 /** 1420 [private] Starts a HTTP server listening on the specified port. 1421 1422 This is the same as listenHTTP() except that it does not use a VibeDist host for 1423 remote listening, even if specified on the command line. 1424 */ 1425 1426 private HTTPListener listenHTTPPlain(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) 1427 @safe 1428 { 1429 import vibe.core.core : runWorkerTaskDist; 1430 import std.algorithm : canFind, find; 1431 1432 static TCPListener doListen(HTTPServerContext listen_info, bool dist, bool reusePort) 1433 @safe { 1434 try { 1435 TCPListenOptions options = TCPListenOptions.defaults; 1436 if(reusePort) options |= TCPListenOptions.reusePort; else options &= ~TCPListenOptions.reusePort; 1437 auto ret = listenTCP(listen_info.bindPort, (TCPConnection conn) nothrow @safe { 1438 // check wether the client's address is banned 1439 foreach (ref virtual_host; listen_info.m_virtualHosts) 1440 if ((virtual_host.settings.rejectConnectionPredicate !is null) && 1441 virtual_host.settings.rejectConnectionPredicate(conn.remoteAddress)) 1442 return; 1443 1444 //logInfo("ListenHTTP"); 1445 try { handleHTTP1Connection(conn, listen_info); 1446 } catch (Exception e) { 1447 logError("HTTP connection handler has thrown: %s", e.msg); 1448 debug logDebug("Full error: %s", () @trusted { return e.toString().sanitize(); } ()); 1449 try conn.close(); 1450 catch (Exception e) logError("Failed to close connection: %s", e.msg); 1451 } 1452 }, listen_info.bindAddress, options); 1453 1454 // support port 0 meaning any available port 1455 if (listen_info.bindPort == 0) 1456 listen_info.m_bindPort = ret.bindAddress.port; 1457 1458 auto proto = listen_info.tlsContext ? "https" : "http"; 1459 auto urladdr = listen_info.bindAddress; 1460 if (urladdr.canFind(':')) urladdr = "["~urladdr~"]"; 1461 logInfo("Listening for requests on %s://%s:%s/", proto, urladdr, listen_info.bindPort); 1462 return ret; 1463 } catch( Exception e ) { 1464 logWarn("Failed to listen on %s:%s", listen_info.bindAddress, listen_info.bindPort); 1465 return TCPListener.init; 1466 } 1467 } 1468 1469 size_t[] vid; 1470 1471 // Check for every bind address/port, if a new listening socket needs to be created and 1472 // check for conflicting servers 1473 foreach (addr; settings.bindAddresses) { 1474 HTTPServerContext linfo; 1475 1476 auto l = s_contexts.find!(l => l.bindAddress == addr && l.bindPort == settings.port); 1477 if (!l.empty) linfo = l.front; 1478 else { 1479 auto li = new HTTPServerContext(addr, settings.port); 1480 if (auto tcp_lst = doListen(li, (settings.options & HTTPServerOptionImpl.distribute) != 0, (settings.options & HTTPServerOption.reusePort) != 0)) // DMD BUG 2043 1481 { 1482 li.m_listener = tcp_lst; 1483 s_contexts ~= li; 1484 linfo = li; 1485 } 1486 } 1487 1488 if (linfo) vid ~= linfo.addVirtualHost(settings, request_handler); 1489 } 1490 1491 enforce(vid.length > 0, "Failed to listen for incoming HTTP connections on any of the supplied interfaces."); 1492 1493 return HTTPListener(vid); 1494 } 1495 1496 unittest{ 1497 // testing a class that implements HTTPServerRequestHandler 1498 1499 class MyReqHandler : HTTPServerRequestHandler 1500 { 1501 override void handleRequest(HTTPServerRequest req, HTTPServerResponse res) 1502 @safe { 1503 if (req.path == "/") 1504 res.writeBody("Hello, World! Interface"); 1505 } 1506 } 1507 1508 auto settings = new HTTPServerSettings(); 1509 settings.port = 8050; 1510 settings.bindAddresses = ["localhost"]; 1511 1512 MyReqHandler mrh = new MyReqHandler; 1513 1514 listenHTTP!mrh(settings); 1515 } 1516 1517 unittest { 1518 // testing HTTPS connections 1519 void handleRequest (HTTPServerRequest req, HTTPServerResponse res) 1520 @safe { 1521 if (req.path == "/") 1522 res.writeBody("Hello, World! Delegate"); 1523 } 1524 1525 auto settings = new HTTPServerSettings(); 1526 settings.port = 8070; 1527 settings.bindAddresses = ["localhost"]; 1528 settings.tlsContext = createTLSContext(TLSContextKind.server); 1529 settings.tlsContext.useCertificateChainFile("tests/server.crt"); 1530 settings.tlsContext.usePrivateKeyFile("tests/server.key"); 1531 1532 listenHTTP!handleRequest(settings); 1533 } 1534 1535 //// NOTE: just a possible idea for the low level api 1536 //struct HTTPRequestHandler { 1537 //void read(alias HeaderCallback, alias BodyCallback)() 1538 //{ 1539 //connection.readHeaders!HeaderCallback(); 1540 //connection.readBody!BodyCallback(); 1541 //} 1542 1543 //void write(alias HeaderCallback, alias BodyCallback)() 1544 //{ 1545 //connection.writeHeader!HeaderCallback(); 1546 //connection.writeBody!BodyCallback(); 1547 //} 1548 //} 1549 1550 1551 struct HTTPServerRequestData { 1552 import vibe.utils.dictionarylist; 1553 1554 @disable this(this); 1555 1556 @safe: 1557 1558 private { 1559 SysTime m_timeCreated; 1560 HTTPServerSettings m_settings; 1561 ushort m_port; 1562 string m_peer; 1563 } 1564 1565 protected { 1566 1567 InterfaceProxy!Stream m_conn; 1568 1569 /// The HTTP protocol version used for the request 1570 1571 HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; 1572 1573 /// The HTTP _method of the request 1574 HTTPMethod method = HTTPMethod.GET; 1575 1576 /** The request URI 1577 1578 Note that the request URI usually does not include the global 1579 'http://server' part, but only the local path and a query string. 1580 A possible exception is a proxy server, which will get full URLs. 1581 */ 1582 string requestURI = "/"; 1583 1584 /// Compatibility alias - scheduled for deprecation 1585 alias requestURL = requestURI; 1586 1587 /// All request _headers 1588 InetHeaderMap headers; 1589 1590 /// The IP address of the client 1591 @property string peer() 1592 @safe nothrow { 1593 if (!m_peer) { 1594 version (Have_vibe_core) {} else scope (failure) assert(false); 1595 // store the IP address (IPv4 addresses forwarded over IPv6 are stored in IPv4 format) 1596 auto peer_address_string = this.clientAddress.toAddressString(); 1597 if (peer_address_string.startsWith("::ffff:") && peer_address_string[7 .. $].indexOf(':') < 0) 1598 m_peer = peer_address_string[7 .. $]; 1599 else m_peer = peer_address_string; 1600 } 1601 return m_peer; 1602 } 1603 1604 /// ditto 1605 NetworkAddress clientAddress; 1606 1607 /// Determines if the request should be logged to the access log file. 1608 bool noLog; 1609 1610 /// Determines if the request was issued over an TLS encrypted channel. 1611 bool tls; 1612 1613 /* Information about the TLS certificate provided by the client. 1614 1615 Remarks: This field is only set if `tls` is true, and the peer 1616 presented a client certificate. 1617 */ 1618 TLSCertificateInformation clientCertificate; 1619 1620 /* Deprecated: The _path part of the URL. 1621 1622 Note that this function contains the decoded version of the 1623 requested path, which can yield incorrect results if the path 1624 contains URL encoded path separators. Use `requestPath` instead to 1625 get an encoding-aware representation. 1626 */ 1627 string path() @safe pure { 1628 if (_path.isNull) { 1629 _path = urlDecode(requestPath.toString); 1630 } 1631 return _path.get; 1632 } 1633 1634 void path(string st) @safe { 1635 assert(_path.isNull, "Unable to set request path"); 1636 _path = st; 1637 } 1638 1639 private Nullable!string _path; 1640 1641 //* The path part of the requested URI. 1642 InetPath requestPath; 1643 1644 //* The user name part of the URL, if present. 1645 string username; 1646 1647 //* The _password part of the URL, if present. 1648 string password; 1649 1650 //* The _query string part of the URL. 1651 string queryString; 1652 1653 /** A map of general parameters for the request. 1654 1655 This map is supposed to be used by middleware functionality to store 1656 information for later stages. For example vibe.http.router.URLRouter uses this map 1657 to store the value of any named placeholders. 1658 */ 1659 DictionaryList!(string, true, 8) params; 1660 1661 /* Contains the list of _cookies that are stored on the client. 1662 1663 Note that the a single cookie name may occur multiple times if multiple 1664 cookies have that name but different paths or domains that all match 1665 the request URI. By default, the first cookie will be returned, which is 1666 the or one of the cookies with the closest path match. 1667 */ 1668 @property ref CookieValueMap cookies() @safe { 1669 if (_cookies.isNull) { 1670 _cookies = CookieValueMap.init; 1671 if (auto pv = "cookie" in headers) 1672 parseCookies(*pv, _cookies.get); 1673 } 1674 return _cookies.get; 1675 } 1676 private Nullable!CookieValueMap _cookies; 1677 1678 /* Contains all _form fields supplied using the _query string. 1679 1680 The fields are stored in the same order as they are received. 1681 */ 1682 @property ref FormFields query() @safe { 1683 if (_query.isNull) { 1684 _query = FormFields.init; 1685 parseURLEncodedForm(queryString, _query.get); 1686 } 1687 1688 return _query.get; 1689 } 1690 Nullable!FormFields _query; 1691 1692 import vibe.utils.dictionarylist; 1693 /* A map of general parameters for the request. 1694 1695 This map is supposed to be used by middleware functionality to store 1696 information for later stages. For example vibe.http.router.URLRouter uses this map 1697 to store the value of any named placeholders. 1698 */ 1699 1700 import std.variant : Variant; 1701 /* A map of context items for the request. 1702 1703 This is especially useful for passing application specific data down 1704 the chain of processors along with the request itself. 1705 1706 For example, a generic route may be defined to check user login status, 1707 if the user is logged in, add a reference to user specific data to the 1708 context. 1709 1710 This is implemented with `std.variant.Variant` to allow any type of data. 1711 */ 1712 DictionaryList!(Variant, true, 2) context; 1713 1714 /* Supplies the request body as a stream. 1715 1716 Note that when certain server options are set (such as 1717 HTTPServerOption.parseJsonBody) and a matching request was sent, 1718 the returned stream will be empty. If needed, remove those 1719 options and do your own processing of the body when launching 1720 the server. HTTPServerOption has a list of all options that affect 1721 the request body. 1722 */ 1723 InputStream bodyReader; 1724 1725 /* Contains the parsed Json for a JSON request. 1726 1727 A JSON request must have the Content-Type "application/json" or "application/vnd.api+json". 1728 */ 1729 @property ref Json json() @safe { 1730 if (_json.isNull) { 1731 if (icmp2(contentType, "application/json") == 0 || icmp2(contentType, "application/vnd.api+json") == 0 ) { 1732 auto bodyStr = bodyReader.readAllUTF8(); 1733 if (!bodyStr.empty) _json = parseJson(bodyStr); 1734 } else { 1735 _json = Json.undefined; 1736 } 1737 } 1738 return _json.get; 1739 } 1740 1741 private Nullable!Json _json; 1742 1743 /* Contains the parsed parameters of a HTML POST _form request. 1744 1745 The fields are stored in the same order as they are received. 1746 1747 Remarks: 1748 A form request must either have the Content-Type 1749 "application/x-www-form-urlencoded" or "multipart/form-data". 1750 */ 1751 @property ref FormFields form() @safe { 1752 if (_form.isNull) 1753 parseFormAndFiles(); 1754 1755 return _form.get; 1756 } 1757 1758 private Nullable!FormFields _form; 1759 1760 private void parseFormAndFiles() @safe { 1761 _form = FormFields.init; 1762 parseFormData(_form.get, _files, headers.get("Content-Type", ""), bodyReader, MaxHTTPHeaderLineLength); 1763 } 1764 1765 //* Contains information about any uploaded file for a HTML _form request. 1766 @property ref FilePartFormFields files() @safe return { 1767 // _form and _files are parsed in one step 1768 if (_form.isNull) { 1769 parseFormAndFiles(); 1770 assert(!_form.isNull); 1771 } 1772 1773 return _files; 1774 } 1775 1776 private FilePartFormFields _files; 1777 1778 /* The current Session object. 1779 1780 This field is set if HTTPServerResponse.startSession() has been called 1781 on a previous response and if the client has sent back the matching 1782 cookie. 1783 1784 Remarks: Requires the HTTPServerOption.parseCookies option. 1785 */ 1786 Session session; 1787 1788 public string toString() 1789 { 1790 return httpMethodString(method) ~ " " ~ requestURL ~ " " ~ getHTTPVersionString(httpVersion); 1791 } 1792 1793 /** Shortcut to the 'Host' header (always present for HTTP 1.1) 1794 */ 1795 @property string host() const pure nothrow { auto ph = "Host" in headers; return ph ? *ph : null; } 1796 /// ditto 1797 @property void host(string v) { headers["Host"] = v; } 1798 1799 /** Returns the mime type part of the 'Content-Type' header. 1800 1801 This function gets the pure mime type (e.g. "text/plain") 1802 without any supplimentary parameters such as "charset=...". 1803 Use contentTypeParameters to get any parameter string or 1804 headers["Content-Type"] to get the raw value. 1805 */ 1806 @property string contentType() 1807 const pure nothrow { 1808 auto pv = "Content-Type" in headers; 1809 if( !pv ) return null; 1810 auto idx = std..string.indexOf(*pv, ';'); 1811 return idx >= 0 ? (*pv)[0 .. idx] : *pv; 1812 } 1813 /// ditto 1814 @property void contentType(string ct) { headers["Content-Type"] = ct; } 1815 1816 /** Returns any supplementary parameters of the 'Content-Type' header. 1817 1818 This is a semicolon separated ist of key/value pairs. Usually, if set, 1819 this contains the character set used for text based content types. 1820 */ 1821 @property string contentTypeParameters() 1822 const pure nothrow { 1823 auto pv = "Content-Type" in headers; 1824 if( !pv ) return null; 1825 auto idx = std..string.indexOf(*pv, ';'); 1826 return idx >= 0 ? (*pv)[idx+1 .. $] : null; 1827 } 1828 1829 /** Determines if the connection persists across requests. 1830 */ 1831 @property bool persistent() const pure nothrow 1832 { 1833 auto ph = "connection" in headers; 1834 switch(httpVersion) { 1835 case HTTPVersion.HTTP_1_0: 1836 if (ph && icmp(*ph, "keep-alive") == 0) return true; 1837 return false; 1838 case HTTPVersion.HTTP_1_1: 1839 if (ph && icmp(*ph, "keep-alive") != 0) return false; 1840 return true; 1841 default: 1842 return false; 1843 } 1844 } 1845 } 1846 1847 package { 1848 //* The settings of the server serving this request. 1849 @property const(HTTPServerSettings) serverSettings() const nothrow @safe 1850 { 1851 return m_settings; 1852 } 1853 } 1854 1855 this(SysTime time, ushort port) 1856 @safe nothrow { 1857 m_timeCreated = time.toUTC(); 1858 m_port = port; 1859 } 1860 1861 //* Time when this request started processing. 1862 @property SysTime timeCreated() const @safe nothrow { return m_timeCreated; } 1863 1864 1865 /* The full URL that corresponds to this request. 1866 1867 The host URL includes the protocol, host and optionally the user 1868 and password that was used for this request. This field is useful to 1869 construct self referencing URLs. 1870 1871 Note that the port is currently not set, so that this only works if 1872 the standard port is used. 1873 */ 1874 @property URL fullURL() 1875 const @safe { 1876 URL url; 1877 1878 auto xfh = this.headers.get("X-Forwarded-Host"); 1879 auto xfp = this.headers.get("X-Forwarded-Port"); 1880 auto xfpr = this.headers.get("X-Forwarded-Proto"); 1881 1882 // Set URL host segment. 1883 if (xfh.length) { 1884 url.host = xfh; 1885 } else if (!this.host.empty) { 1886 url.host = this.host; 1887 } else if (!m_settings.hostName.empty) { 1888 url.host = m_settings.hostName; 1889 } else { 1890 url.host = m_settings.bindAddresses[0]; 1891 } 1892 1893 // Set URL schema segment. 1894 if (xfpr.length) { 1895 url.schema = xfpr; 1896 } else if (this.tls) { 1897 url.schema = "https"; 1898 } else { 1899 url.schema = "http"; 1900 } 1901 1902 // Set URL port segment. 1903 if (xfp.length) { 1904 try { 1905 url.port = xfp.to!ushort; 1906 } catch (ConvException) { 1907 // TODO : Consider responding with a 400/etc. error from here. 1908 logWarn("X-Forwarded-Port header was not valid port (%s)", xfp); 1909 } 1910 } else if (!xfh) { 1911 if (url.schema == "https") { 1912 if (m_port != 443U) url.port = m_port; 1913 } else { 1914 if (m_port != 80U) url.port = m_port; 1915 } 1916 } 1917 1918 if (url.host.startsWith('[')) { // handle IPv6 address 1919 auto idx = url.host.indexOf(']'); 1920 if (idx >= 0 && idx+1 < url.host.length && url.host[idx+1] == ':') 1921 url.host = url.host[1 .. idx]; 1922 } else { // handle normal host names or IPv4 address 1923 auto idx = url.host.indexOf(':'); 1924 if (idx >= 0) url.host = url.host[0 .. idx]; 1925 } 1926 1927 url.username = this.username; 1928 url.password = this.password; 1929 url.localURI = this.requestURI; 1930 1931 return url; 1932 } 1933 1934 /* The relative path to the root folder. 1935 1936 Using this function instead of absolute URLs for embedded links can be 1937 useful to avoid dead link when the site is piped through a 1938 reverse-proxy. 1939 1940 The returned string always ends with a slash. 1941 */ 1942 @property string rootDir() 1943 const @safe pure nothrow { 1944 import std.range.primitives : walkLength; 1945 auto depth = requestPath.bySegment.walkLength; 1946 return depth == 0 ? "./" : replicate("../", depth); 1947 } 1948 } 1949 1950 1951 struct HTTPServerResponseData { 1952 @disable this(this); 1953 @safe: 1954 1955 private { 1956 InterfaceProxy!Stream m_conn; 1957 InterfaceProxy!ConnectionStream m_rawConnection; 1958 InterfaceProxy!OutputStream m_bodyWriter; 1959 IAllocator m_requestAlloc; 1960 FreeListRef!ChunkedOutputStream m_chunkedBodyWriter; 1961 FreeListRef!CountingOutputStream m_countingWriter; 1962 FreeListRef!ZlibOutputStream m_zlibOutputStream; 1963 HTTPServerSettings m_settings; 1964 Session m_session; 1965 bool m_headerWritten = false; 1966 bool m_isHeadResponse = false; 1967 bool m_tls; 1968 SysTime m_timeFinalized; 1969 } 1970 1971 protected { 1972 1973 /// The protocol version of the response - should not be changed 1974 HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; 1975 1976 /// The status code of the response, 200 by default 1977 int statusCode = HTTPStatus.OK; 1978 1979 /** The status phrase of the response 1980 1981 If no phrase is set, a default one corresponding to the status code will be used. 1982 */ 1983 string statusPhrase; 1984 1985 /// The response header fields 1986 InetHeaderMap headers; 1987 1988 /// All cookies that shall be set on the client for this request 1989 Cookie[string] cookies; 1990 /** Shortcut to the "Content-Type" header 1991 */ 1992 @property string contentType() const { auto pct = "Content-Type" in headers; return pct ? *pct : "application/octet-stream"; } 1993 /// ditto 1994 @property void contentType(string ct) { headers["Content-Type"] = ct; } 1995 1996 static if (!is(Stream == InterfaceProxy!Stream)) { 1997 this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 1998 @safe { 1999 this(InterfaceProxy!Stream(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc); 2000 } 2001 } 2002 2003 this(InterfaceProxy!Stream conn, InterfaceProxy!ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 2004 @safe { 2005 m_conn = conn; 2006 m_rawConnection = raw_connection; 2007 m_countingWriter = createCountingOutputStreamFL(conn); 2008 m_settings = settings; 2009 m_requestAlloc = req_alloc; 2010 } 2011 2012 /** Returns the time at which the request was finalized. 2013 2014 Note that this field will only be set after `finalize` has been called. 2015 */ 2016 @property SysTime timeFinalized() const @safe { return m_timeFinalized; } 2017 2018 /** Determines if the HTTP header has already been written. 2019 */ 2020 @property bool headerWritten() const @safe { return m_headerWritten; } 2021 2022 /** Determines if the response does not need a body. 2023 */ 2024 bool isHeadResponse() const @safe { return m_isHeadResponse; } 2025 2026 /** Determines if the response is sent over an encrypted connection. 2027 */ 2028 bool tls() const @safe { return m_tls; } 2029 2030 /** Writes the entire response body at once. 2031 2032 Params: 2033 data = The data to write as the body contents 2034 status = Optional response status code to set 2035 content_tyoe = Optional content type to apply to the response. 2036 If no content type is given and no "Content-Type" header is 2037 set in the response, this will default to 2038 `"application/octet-stream"`. 2039 2040 See_Also: `HTTPStatusCode` 2041 */ 2042 void writeBody(in ubyte[] data, string content_type = null) 2043 @safe { 2044 if (content_type.length) headers["Content-Type"] = content_type; 2045 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 2046 ulong length = data.length; 2047 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length); 2048 headers["Content-Length"] = format("%d", length); 2049 bodyWriter.write(data); 2050 } 2051 /// ditto 2052 void writeBody(in ubyte[] data, int status, string content_type = null) 2053 @safe { 2054 statusCode = status; 2055 writeBody(data, content_type); 2056 } 2057 /// ditto 2058 void writeBody(scope InputStream data, string content_type = null) 2059 @safe { 2060 if (content_type.length) headers["Content-Type"] = content_type; 2061 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 2062 data.pipe(bodyWriter); 2063 } 2064 2065 /** Writes the entire response body as a single string. 2066 2067 Params: 2068 data = The string to write as the body contents 2069 status = Optional response status code to set 2070 content_type = Optional content type to apply to the response. 2071 If no content type is given and no "Content-Type" header is 2072 set in the response, this will default to 2073 `"text/plain; charset=UTF-8"`. 2074 2075 See_Also: `HTTPStatusCode` 2076 */ 2077 /// ditto 2078 void writeBody(string data, string content_type = null) 2079 @safe { 2080 if (!content_type.length && "Content-Type" !in headers) 2081 content_type = "text/plain; charset=UTF-8"; 2082 writeBody(cast(const(ubyte)[])data, content_type); 2083 } 2084 /// ditto 2085 void writeBody(string data, int status, string content_type = null) 2086 @safe { 2087 statusCode = status; 2088 writeBody(data, content_type); 2089 } 2090 2091 /** Writes the whole response body at once, without doing any further encoding. 2092 2093 The caller has to make sure that the appropriate headers are set correctly 2094 (i.e. Content-Type and Content-Encoding). 2095 2096 Note that the version taking a RandomAccessStream may perform additional 2097 optimizations such as sending a file directly from the disk to the 2098 network card using a DMA transfer. 2099 2100 */ 2101 void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe 2102 if (isRandomAccessStream!RandomAccessStream) 2103 { 2104 assert(!m_headerWritten, "A body was already written!"); 2105 writeHeader(); 2106 if (m_isHeadResponse) return; 2107 2108 auto bytes = stream.size - stream.tell(); 2109 stream.pipe(m_conn); 2110 m_countingWriter.increment(bytes); 2111 } 2112 /// ditto 2113 void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = 0) @safe 2114 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 2115 { 2116 assert(!m_headerWritten, "A body was already written!"); 2117 writeHeader(); 2118 if (m_isHeadResponse) return; 2119 2120 if (num_bytes > 0) { 2121 stream.pipe(m_conn, num_bytes); 2122 m_countingWriter.increment(num_bytes); 2123 } else stream.pipe(m_countingWriter, num_bytes); 2124 } 2125 /// ditto 2126 void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe 2127 if (isRandomAccessStream!RandomAccessStream) 2128 { 2129 statusCode = status; 2130 writeRawBody(stream); 2131 } 2132 /// ditto 2133 void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe 2134 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 2135 { 2136 statusCode = status; 2137 writeRawBody(stream, num_bytes); 2138 } 2139 2140 2141 /// Writes a JSON message with the specified status 2142 void writeJsonBody(T)(T data, int status, bool allow_chunked = false) 2143 { 2144 statusCode = status; 2145 writeJsonBody(data, allow_chunked); 2146 } 2147 /// ditto 2148 void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false) 2149 { 2150 statusCode = status; 2151 writeJsonBody(data, content_type, allow_chunked); 2152 } 2153 2154 /// ditto 2155 void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false) 2156 { 2157 headers["Content-Type"] = content_type; 2158 writeJsonBody(data, allow_chunked); 2159 } 2160 /// ditto 2161 void writeJsonBody(T)(T data, bool allow_chunked = false) 2162 { 2163 doWriteJsonBody!(T, false)(data, allow_chunked); 2164 } 2165 /// ditto 2166 void writePrettyJsonBody(T)(T data, bool allow_chunked = false) 2167 { 2168 doWriteJsonBody!(T, true)(data, allow_chunked); 2169 } 2170 2171 private void doWriteJsonBody(T, bool PRETTY)(T data, bool allow_chunked = false) 2172 { 2173 import std.traits; 2174 import vibe.stream.wrapper; 2175 2176 static if (!is(T == Json) && is(typeof(data.data())) && isArray!(typeof(data.data()))) { 2177 static assert(!is(T == Appender!(typeof(data.data()))), "Passed an Appender!T to writeJsonBody - this is most probably not doing what's indended."); 2178 } 2179 2180 if ("Content-Type" !in headers) 2181 headers["Content-Type"] = "application/json; charset=UTF-8"; 2182 2183 2184 // set an explicit content-length field if chunked encoding is not allowed 2185 if (!allow_chunked) { 2186 import vibe.internal.rangeutil; 2187 long length = 0; 2188 auto counter = RangeCounter(() @trusted { return &length; } ()); 2189 static if (PRETTY) serializeToPrettyJson(counter, data); 2190 else serializeToJson(counter, data); 2191 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length); 2192 } 2193 2194 auto rng = streamOutputRange!1024(bodyWriter); 2195 static if (PRETTY) serializeToPrettyJson(() @trusted { return &rng; } (), data); 2196 else serializeToJson(() @trusted { return &rng; } (), data); 2197 } 2198 2199 /** 2200 * Writes the response with no body. 2201 * 2202 * This method should be used in situations where no body is 2203 * requested, such as a HEAD request. For an empty body, just use writeBody, 2204 * as this method causes problems with some keep-alive connections. 2205 */ 2206 void writeVoidBody() @safe 2207 { 2208 writeVoidBody(m_conn); 2209 } 2210 /// ditto 2211 void writeVoidBody(Stream)(Stream stream) @safe 2212 if(isOutputStream!Stream) 2213 { 2214 if (!m_isHeadResponse) { 2215 assert("Content-Length" !in headers); 2216 assert("Transfer-Encoding" !in headers); 2217 } 2218 assert(!headerWritten); 2219 writeHeader(stream); 2220 stream.flush(); 2221 } 2222 2223 /** A stream for writing the body of the HTTP response. 2224 2225 Note that after 'bodyWriter' has been accessed for the first time, it 2226 is not allowed to change any header or the status code of the response. 2227 */ 2228 @property InterfaceProxy!OutputStream bodyWriter() 2229 @safe { 2230 assert(!!m_conn); 2231 if (m_bodyWriter) return m_bodyWriter; 2232 2233 assert(!m_headerWritten, "A void body was already written!"); 2234 2235 if (m_isHeadResponse) { 2236 // for HEAD requests, we define a NullOutputWriter for convenience 2237 // - no body will be written. However, the request handler should call writeVoidBody() 2238 // and skip writing of the body in this case. 2239 if ("Content-Length" !in headers) 2240 headers["Transfer-Encoding"] = "chunked"; 2241 writeHeader(); 2242 m_bodyWriter = nullSink; 2243 return m_bodyWriter; 2244 } 2245 2246 if ("Content-Encoding" in headers && "Content-Length" in headers) { 2247 // we do not known how large the compressed body will be in advance 2248 // so remove the content-length and use chunked transfer 2249 headers.remove("Content-Length"); 2250 } 2251 2252 if (auto pcl = "Content-Length" in headers) { 2253 writeHeader(); 2254 m_countingWriter.writeLimit = (*pcl).to!ulong; 2255 m_bodyWriter = m_countingWriter; 2256 } else if (httpVersion <= HTTPVersion.HTTP_1_0) { 2257 if ("Connection" in headers) 2258 headers.remove("Connection"); // default to "close" 2259 writeHeader(); 2260 m_bodyWriter = m_conn; 2261 } else { 2262 headers["Transfer-Encoding"] = "chunked"; 2263 writeHeader(); 2264 m_chunkedBodyWriter = createChunkedOutputStreamFL(m_countingWriter); 2265 m_bodyWriter = m_chunkedBodyWriter; 2266 } 2267 2268 if (auto pce = "Content-Encoding" in headers) { 2269 if (icmp2(*pce, "gzip") == 0) { 2270 m_zlibOutputStream = createGzipOutputStreamFL(m_bodyWriter); 2271 m_bodyWriter = m_zlibOutputStream; 2272 } else if (icmp2(*pce, "deflate") == 0) { 2273 m_zlibOutputStream = createDeflateOutputStreamFL(m_bodyWriter); 2274 m_bodyWriter = m_zlibOutputStream; 2275 } else { 2276 logWarn("Unsupported Content-Encoding set in response: '"~*pce~"'"); 2277 } 2278 } 2279 2280 2281 2282 return m_bodyWriter; 2283 } 2284 2285 /** 2286 * Used to change the bodyWriter during a HTTP/2 connection 2287 */ 2288 import vibe.stream.memory; 2289 @property void bodyWriterH2(T)(ref T writer, const bool writeH = false) @safe 2290 if(isOutputStream!T) 2291 { 2292 assert(!m_bodyWriter, "Unable to set bodyWriter"); 2293 2294 // write the current set headers before initiating the bodyWriter 2295 if(writeH) writeHeader(writer); 2296 2297 static if(!is(T == InterfaceProxy!OutputStream)) { 2298 InterfaceProxy!OutputStream bwriter = writer; 2299 m_bodyWriter = bwriter; 2300 } else { 2301 m_bodyWriter = writer; 2302 } 2303 } 2304 2305 /** Sends a redirect request to the client. 2306 2307 Params: 2308 url = The URL to redirect to 2309 status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found) 2310 */ 2311 void redirect(string url, int status = HTTPStatus.Found) 2312 @safe { 2313 // Disallow any characters that may influence the header parsing 2314 enforce(!url.representation.canFind!(ch => ch < 0x20), 2315 "Control character in redirection URL."); 2316 2317 statusCode = status; 2318 headers["Location"] = url; 2319 writeBody("redirecting..."); 2320 } 2321 /// ditto 2322 void redirect(URL url, int status = HTTPStatus.Found) 2323 @safe { 2324 redirect(url.toString(), status); 2325 } 2326 2327 /// 2328 @safe unittest { 2329 import vibe.http.router; 2330 2331 void request_handler(HTTPServerRequest req, HTTPServerResponse res) 2332 { 2333 res.redirect("http://example.org/some_other_url"); 2334 } 2335 2336 void test() 2337 { 2338 auto router = new URLRouter; 2339 router.get("/old_url", &request_handler); 2340 HTTPServerSettings settings; 2341 listenHTTP!router(settings); 2342 } 2343 } 2344 2345 2346 /** Special method sending a SWITCHING_PROTOCOLS response to the client. 2347 2348 Notice: For the overload that returns a `ConnectionStream`, it must be 2349 ensured that the returned instance doesn't outlive the request 2350 handler callback. 2351 2352 Notice: The overload which accepts a connection_handler alias is used for 2353 HTTP/1 to HTTP/2 switching in cleartext HTTP 2354 2355 Params: 2356 protocol = The protocol set in the "Upgrade" header of the response. 2357 Use an empty string to skip setting this field. 2358 */ 2359 ConnectionStream switchProtocol(string protocol) 2360 @safe { 2361 statusCode = HTTPStatus.SwitchingProtocols; 2362 if (protocol.length) headers["Upgrade"] = protocol; 2363 writeVoidBody(); 2364 return createConnectionProxyStream(m_conn, m_rawConnection); 2365 } 2366 2367 /// ditto 2368 void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del) 2369 @safe { 2370 statusCode = HTTPStatus.SwitchingProtocols; 2371 if (protocol.length) headers["Upgrade"] = protocol; 2372 writeVoidBody(); 2373 () @trusted { 2374 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 2375 del(conn); 2376 } (); 2377 finalize(); 2378 if (m_rawConnection && m_rawConnection.connected) 2379 m_rawConnection.close(); // connection not reusable after a protocol upgrade 2380 } 2381 2382 package void switchToHTTP2(HANDLER)(HANDLER handler, HTTP2ServerContext context) 2383 @safe { 2384 //logInfo("sending SWITCHING_PROTOCOL response"); 2385 2386 statusCode = HTTPStatus.switchingProtocols; 2387 headers["Upgrade"] = "h2c"; 2388 2389 writeVoidBody(); 2390 2391 // TODO improve handler (handleHTTP2Connection) connection management 2392 auto tcp_conn = m_rawConnection.extract!TCPConnection; 2393 handler(tcp_conn, tcp_conn, context, false); 2394 2395 finalize(); 2396 // close the existing connection 2397 if (m_rawConnection && m_rawConnection.connected) 2398 m_rawConnection.close(); // connection not reusable after a protocol upgrade 2399 } 2400 2401 // send a badRequest error response and close the connection 2402 package void sendBadRequest() @safe 2403 { 2404 statusCode = HTTPStatus.badRequest; 2405 2406 writeVoidBody(); 2407 2408 finalize(); 2409 if (m_rawConnection && m_rawConnection.connected) 2410 m_rawConnection.close(); // connection not reusable after a protocol upgrade 2411 } 2412 2413 2414 /** Special method for handling CONNECT proxy tunnel 2415 2416 Notice: For the overload that returns a `ConnectionStream`, it must be 2417 ensured that the returned instance doesn't outlive the request 2418 handler callback. 2419 */ 2420 ConnectionStream connectProxy() 2421 @safe { 2422 return createConnectionProxyStream(m_conn, m_rawConnection); 2423 } 2424 /// ditto 2425 void connectProxy(scope void delegate(scope ConnectionStream) @safe del) 2426 @safe { 2427 () @trusted { 2428 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 2429 del(conn); 2430 } (); 2431 finalize(); 2432 m_rawConnection.close(); // connection not reusable after a protocol upgrade 2433 } 2434 2435 /** Sets the specified cookie value. 2436 2437 Params: 2438 name = Name of the cookie 2439 value = New cookie value - pass null to clear the cookie 2440 path = Path (as seen by the client) of the directory tree in which the cookie is visible 2441 */ 2442 Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url) 2443 @safe { 2444 auto cookie = new Cookie(); 2445 cookie.path = path; 2446 cookie.setValue(value, encoding); 2447 if (value is null) { 2448 cookie.maxAge = 0; 2449 cookie.expires = "Thu, 01 Jan 1970 00:00:00 GMT"; 2450 } 2451 cookies[name] = cookie; 2452 return cookie; 2453 } 2454 2455 /** 2456 Initiates a new session. 2457 2458 The session is stored in the SessionStore that was specified when 2459 creating the server. Depending on this, the session can be persistent 2460 or temporary and specific to this server instance. 2461 */ 2462 Session startSession(string path = "/", SessionOption options = SessionOption.httpOnly) 2463 @safe { 2464 assert(m_settings.sessionStore, "no session store set"); 2465 assert(!m_session, "Try to start a session, but already started one."); 2466 2467 bool secure; 2468 if (options & SessionOption.secure) secure = true; 2469 else if (options & SessionOption.noSecure) secure = false; 2470 else secure = this.tls; 2471 2472 m_session = m_settings.sessionStore.create(); 2473 m_session.set("$sessionCookiePath", path); 2474 m_session.set("$sessionCookieSecure", secure); 2475 auto cookie = setCookie(m_settings.sessionIdCookie, m_session.id, path); 2476 cookie.secure = secure; 2477 cookie.httpOnly = (options & SessionOption.httpOnly) != 0; 2478 return m_session; 2479 } 2480 2481 /** 2482 Terminates the current session (if any). 2483 */ 2484 void terminateSession() 2485 @safe { 2486 if (!m_session) return; 2487 auto cookie = setCookie(m_settings.sessionIdCookie, null, m_session.get!string("$sessionCookiePath")); 2488 cookie.secure = m_session.get!bool("$sessionCookieSecure"); 2489 m_session.destroy(); 2490 m_session = Session.init; 2491 } 2492 2493 @property ulong bytesWritten() @safe const { return m_countingWriter.bytesWritten; } 2494 2495 /** 2496 Waits until either the connection closes, data arrives, or until the 2497 given timeout is reached. 2498 2499 Returns: 2500 $(D true) if the connection was closed and $(D false) if either the 2501 timeout was reached, or if data has arrived for consumption. 2502 2503 See_Also: `connected` 2504 */ 2505 bool waitForConnectionClose(Duration timeout = Duration.max) 2506 @safe { 2507 if (!m_rawConnection || !m_rawConnection.connected) return true; 2508 m_rawConnection.waitForData(timeout); 2509 return !m_rawConnection.connected; 2510 } 2511 2512 /** 2513 Determines if the underlying connection is still alive. 2514 2515 Returns $(D true) if the remote peer is still connected and $(D false) 2516 if the remote peer closed the connection. 2517 2518 See_Also: `waitForConnectionClose` 2519 */ 2520 @property bool connected() 2521 @safe const { 2522 if (!m_rawConnection) return false; 2523 return m_rawConnection.connected; 2524 } 2525 2526 /** 2527 Finalizes the response. This is usually called automatically by the server. 2528 2529 This method can be called manually after writing the response to force 2530 all network traffic associated with the current request to be finalized. 2531 After the call returns, the `timeFinalized` property will be set. 2532 */ 2533 void finalize() 2534 @safe { 2535 if (m_zlibOutputStream) { 2536 m_zlibOutputStream.finalize(); 2537 m_zlibOutputStream.destroy(); 2538 } 2539 if (m_chunkedBodyWriter) { 2540 m_chunkedBodyWriter.finalize(); 2541 m_chunkedBodyWriter.destroy(); 2542 } 2543 2544 // ignore exceptions caused by an already closed connection - the client 2545 // may have closed the connection already and this doesn't usually indicate 2546 // a problem. 2547 if (m_rawConnection && m_rawConnection.connected) { 2548 try if (m_conn) m_conn.flush(); 2549 catch (Exception e) logDebug("Failed to flush connection after finishing HTTP response: %s", e.msg); 2550 if (!isHeadResponse && bytesWritten < headers.get("Content-Length", "0").to!long) { 2551 logDebug("HTTP response only written partially before finalization. Terminating connection."); 2552 m_rawConnection.close(); 2553 } 2554 m_rawConnection = InterfaceProxy!ConnectionStream.init; 2555 } 2556 2557 if (m_conn) { 2558 m_conn = InterfaceProxy!Stream.init; 2559 m_timeFinalized = Clock.currTime(UTC()); 2560 } 2561 } 2562 2563 } 2564 2565 private void writeHeader() 2566 @safe { 2567 writeHeader(m_conn); 2568 } 2569 2570 // accept a destination stream 2571 private void writeHeader(Stream)(Stream conn) @safe 2572 if(isOutputStream!Stream) 2573 { 2574 import vibe.stream.wrapper; 2575 2576 assert(!m_bodyWriter && !m_headerWritten, "Try to write header after body has already begun."); 2577 m_headerWritten = true; 2578 auto dst = streamOutputRange!1024(conn); 2579 2580 void writeLine(T...)(string fmt, T args) 2581 @safe { 2582 formattedWrite(() @trusted { return &dst; } (), fmt, args); 2583 dst.put("\r\n"); 2584 logTrace(fmt, args); 2585 } 2586 2587 logTrace("---------------------"); 2588 logTrace("HTTP server response:"); 2589 logTrace("---------------------"); 2590 2591 // write the status line 2592 writeLine("%s %d %s", 2593 getHTTPVersionString(this.httpVersion), 2594 this.statusCode, 2595 this.statusPhrase.length ? this.statusPhrase : httpStatusText(this.statusCode)); 2596 2597 // write all normal headers 2598 foreach (k, v; this.headers.byKeyValue) { 2599 dst.put(k); 2600 dst.put(": "); 2601 dst.put(v); 2602 dst.put("\r\n"); 2603 logTrace("%s: %s", k, v); 2604 } 2605 2606 logTrace("---------------------"); 2607 2608 // write cookies 2609 foreach (n, cookie; this.cookies) { 2610 dst.put("Set-Cookie: "); 2611 cookie.writeString(() @trusted { return &dst; } (), n); 2612 dst.put("\r\n"); 2613 } 2614 2615 // finalize response header 2616 dst.put("\r\n"); 2617 } 2618 2619 public string toString() 2620 { 2621 auto app = appender!string(); 2622 formattedWrite(app, "%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase); 2623 return app.data; 2624 } 2625 } 2626 2627 2628 private void parseCookies(string str, ref CookieValueMap cookies) 2629 @safe { 2630 import std.encoding : sanitize; 2631 import std.array : split; 2632 import std..string : strip; 2633 import std.algorithm.iteration : map, filter, each; 2634 import vibe.http.common : Cookie; 2635 () @trusted { return str.sanitize; } () 2636 .split(";") 2637 .map!(kv => kv.strip.split("=")) 2638 .filter!(kv => kv.length == 2) //ignore illegal cookies 2639 .each!(kv => cookies.add(kv[0], kv[1], Cookie.Encoding.raw) ); 2640 } 2641 2642 unittest 2643 { 2644 auto cvm = CookieValueMap(); 2645 parseCookies("foo=bar;; baz=zinga; öö=üü ; møøse=was=sacked; onlyval1; =onlyval2; onlykey=", cvm); 2646 assert(cvm["foo"] == "bar"); 2647 assert(cvm["baz"] == "zinga"); 2648 assert(cvm["öö"] == "üü"); 2649 assert( "møøse" ! in cvm); //illegal cookie gets ignored 2650 assert( "onlyval1" ! in cvm); //illegal cookie gets ignored 2651 assert(cvm["onlykey"] == ""); 2652 assert(cvm[""] == "onlyval2"); 2653 assert(cvm.length() == 5); 2654 cvm = CookieValueMap(); 2655 parseCookies("", cvm); 2656 assert(cvm.length() == 0); 2657 cvm = CookieValueMap(); 2658 parseCookies(";;=", cvm); 2659 assert(cvm.length() == 1); 2660 assert(cvm[""] == ""); 2661 } 2662 2663 void parseHTTP2RequestHeader(R)(ref R headers, HTTPServerRequest reqStruct) @safe 2664 { 2665 import std.algorithm.searching : find, startsWith; 2666 import std.algorithm.iteration : filter; 2667 auto req = reqStruct.data; 2668 2669 //Method 2670 req.method = cast(HTTPMethod)headers.find!((h,m) => h.name == m)(":method")[0].value; 2671 2672 //Host 2673 auto host = headers.find!((h,m) => h.name == m)(":authority"); 2674 if(!host.empty) req.host = cast(string)host[0].value; 2675 2676 //Path 2677 req.path = cast(string)headers.find!((h,m) => h.name == m)(":path")[0].value; 2678 2679 //URI 2680 req.requestURI = req.host; 2681 2682 //HTTP version 2683 req.httpVersion = HTTPVersion.HTTP_2; 2684 2685 2686 //headers 2687 foreach(h; headers.filter!(f => !f.name.startsWith(":"))) { 2688 req.headers[h.name] = cast(string)h.value; 2689 } 2690 } 2691 2692 uint parseRequestHeader(InputStream)(HTTPServerRequest reqStruct, InputStream http_stream, IAllocator alloc, ulong max_header_size) 2693 if (isInputStream!InputStream) 2694 { 2695 auto req = reqStruct.data; 2696 auto stream = FreeListRef!LimitedHTTPInputStream(http_stream, max_header_size); 2697 2698 logTrace("HTTP server reading status line"); 2699 auto reqln = () @trusted { return cast(string)stream.readLine(MaxHTTPHeaderLineLength, "\r\n", alloc); }(); 2700 2701 if(reqln == "PRI * HTTP/2.0") return cast(uint)reqln.length; 2702 2703 logTrace("--------------------"); 2704 logTrace("HTTP server request:"); 2705 logTrace("--------------------"); 2706 logTrace("%s", reqln); 2707 2708 //Method 2709 auto pos = reqln.indexOf(' '); 2710 enforceBadRequest(pos >= 0, "invalid request method"); 2711 2712 req.method = httpMethodFromString(reqln[0 .. pos]); 2713 reqln = reqln[pos+1 .. $]; 2714 //Path 2715 pos = reqln.indexOf(' '); 2716 enforceBadRequest(pos >= 0, "invalid request path"); 2717 2718 req.requestURI = reqln[0 .. pos]; 2719 reqln = reqln[pos+1 .. $]; 2720 2721 req.httpVersion = parseHTTPVersion(reqln); 2722 2723 //headers 2724 parseRFC5322Header(stream, req.headers, MaxHTTPHeaderLineLength, alloc, false); 2725 2726 foreach (k, v; req.headers.byKeyValue) 2727 logTrace("%s: %s", k, v); 2728 logTrace("--------------------"); 2729 return 0; 2730 } 2731 2732 string formatRFC822DateAlloc(IAllocator alloc, SysTime time) 2733 @safe { 2734 auto app = AllocAppender!string(alloc); 2735 writeRFC822DateTimeString(app, time); 2736 return () @trusted { return app.data; } (); 2737 } 2738 2739 version (VibeDebugCatchAll) alias UncaughtException = Throwable; 2740 else alias UncaughtException = Exception;