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