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 590 591 /** 592 Contains all settings for configuring a basic HTTP server. 593 594 The defaults are sufficient for most normal uses. 595 */ 596 final class HTTPServerSettings { 597 /** The port on which the HTTP server is listening. 598 599 The default value is 80. If you are running a TLS enabled server you may want to set this 600 to 443 instead. 601 602 Using a value of `0` instructs the server to use any available port on 603 the given `bindAddresses` the actual addresses and ports can then be 604 queried with `TCPListener.bindAddresses`. 605 */ 606 ushort port = 80; 607 608 /** The interfaces on which the HTTP server is listening. 609 610 By default, the server will listen on all IPv4 and IPv6 interfaces. 611 */ 612 string[] bindAddresses = ["::", "0.0.0.0"]; 613 614 /** Determines the server host name. 615 616 If multiple servers are listening on the same port, the host name will determine which one 617 gets a request. 618 */ 619 string hostName; 620 621 /** Provides a way to reject incoming connections as early as possible. 622 623 Allows to ban and unban network addresses and reduce the impact of DOS 624 attacks. 625 626 If the callback returns `true` for a specific `NetworkAddress`, 627 then all incoming requests from that address will be rejected. 628 */ 629 RejectConnectionPredicate rejectConnectionPredicate; 630 631 /** Configures optional features of the HTTP server 632 633 Disabling unneeded features can improve performance or reduce the server 634 load in case of invalid or unwanted requests (DoS). By default, 635 HTTPServerOption.defaults is used. 636 */ 637 HTTPServerOption options = HTTPServerOption.defaults; 638 639 /** Time of a request after which the connection is closed with an error; not supported yet 640 641 The default limit of 0 means that the request time is not limited. 642 */ 643 Duration maxRequestTime = 0.seconds; 644 645 /** Maximum time between two request on a keep-alive connection 646 647 The default value is 10 seconds. 648 */ 649 Duration keepAliveTimeout = 10.seconds; 650 651 /// Maximum number of transferred bytes per request after which the connection is closed with 652 /// an error 653 ulong maxRequestSize = 2097152; 654 655 656 /// Maximum number of transferred bytes for the request header. This includes the request line 657 /// the url and all headers. 658 ulong maxRequestHeaderSize = 8192; 659 660 /// Sets a custom handler for displaying error pages for HTTP errors 661 @property HTTPServerErrorPageHandler errorPageHandler() @safe { return errorPageHandler_; } 662 /// ditto 663 @property void errorPageHandler(HTTPServerErrorPageHandler del) @safe { errorPageHandler_ = del; } 664 /// Scheduled for deprecation - use a `@safe` callback instead. 665 @property void errorPageHandler(void delegate(HTTPServerRequest, HTTPServerResponse, HTTPServerErrorInfo) @system del) 666 @system { 667 this.errorPageHandler = (req, res, err) @trusted { del(req, res, err); }; 668 } 669 670 private HTTPServerErrorPageHandler errorPageHandler_ = null; 671 672 /// If set, a HTTPS server will be started instead of plain HTTP. 673 TLSContext tlsContext; 674 675 /// Session management is enabled if a session store instance is provided 676 SessionStore sessionStore; 677 string sessionIdCookie = "vibe.session_id"; 678 679 /// Session options to use when initializing a new session. 680 SessionOption sessionOptions = SessionOption.httpOnly; 681 682 /// 683 import vibe.core.core : vibeVersionString; 684 string serverString = "vibe.d/" ~ vibeVersionString; 685 686 /** Specifies the format used for the access log. 687 688 The log format is given using the Apache server syntax. By default NCSA combined is used. 689 690 --- 691 "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" 692 --- 693 */ 694 string accessLogFormat = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""; 695 696 /// Spefifies the name of a file to which access log messages are appended. 697 string accessLogFile = ""; 698 699 /// If set, access log entries will be output to the console. 700 bool accessLogToConsole = false; 701 702 /** Specifies a custom access logger instance. 703 */ 704 HTTPLogger accessLogger; 705 706 /// Returns a duplicate of the settings object. 707 @property HTTPServerSettings dup() 708 @safe { 709 auto ret = new HTTPServerSettings; 710 foreach (mem; __traits(allMembers, HTTPServerSettings)) { 711 static if (mem == "sslContext") {} 712 else static if (mem == "bindAddresses") ret.bindAddresses = bindAddresses.dup; 713 else static if (__traits(compiles, __traits(getMember, ret, mem) = __traits(getMember, this, mem))) 714 __traits(getMember, ret, mem) = __traits(getMember, this, mem); 715 } 716 return ret; 717 } 718 719 /// Disable support for VibeDist and instead start listening immediately. 720 bool disableDistHost = false; 721 722 /** Responds to "Accept-Encoding" by using compression if possible. 723 724 Compression can also be manually enabled by setting the 725 "Content-Encoding" header of the HTTP response appropriately before 726 sending the response body. 727 728 This setting is disabled by default. Also note that there are still some 729 known issues with the GZIP compression code. 730 */ 731 bool useCompressionIfPossible = false; 732 733 734 /** Interval between WebSocket ping frames. 735 736 The default value is 60 seconds; set to Duration.zero to disable pings. 737 */ 738 Duration webSocketPingInterval = 60.seconds; 739 740 /** Constructs a new settings object with default values. 741 */ 742 this() @safe {} 743 744 /** Constructs a new settings object with a custom bind interface and/or port. 745 746 The syntax of `bind_string` is `[<IP address>][:<port>]`, where either of 747 the two parts can be left off. IPv6 addresses must be enclosed in square 748 brackets, as they would within a URL. 749 750 Throws: 751 An exception is thrown if `bind_string` is malformed. 752 */ 753 this(string bind_string) 754 @safe { 755 this(); 756 757 if (bind_string.startsWith('[')) { 758 auto idx = bind_string.indexOf(']'); 759 enforce(idx > 0, "Missing closing bracket for IPv6 address."); 760 bindAddresses = [bind_string[1 .. idx]]; 761 bind_string = bind_string[idx+1 .. $]; 762 763 enforce(bind_string.length == 0 || bind_string.startsWith(':'), 764 "Only a colon may follow the IPv6 address."); 765 } 766 767 auto idx = bind_string.indexOf(':'); 768 if (idx < 0) { 769 if (bind_string.length > 0) bindAddresses = [bind_string]; 770 } else { 771 if (idx > 0) bindAddresses = [bind_string[0 .. idx]]; 772 port = bind_string[idx+1 .. $].to!ushort; 773 } 774 } 775 776 /// 777 unittest { 778 auto s = new HTTPServerSettings(":8080"); 779 assert(s.bindAddresses == ["::", "0.0.0.0"]); // default bind addresses 780 assert(s.port == 8080); 781 782 s = new HTTPServerSettings("123.123.123.123"); 783 assert(s.bindAddresses == ["123.123.123.123"]); 784 assert(s.port == 80); 785 786 s = new HTTPServerSettings("[::1]:443"); 787 assert(s.bindAddresses == ["::1"]); 788 assert(s.port == 443); 789 } 790 } 791 792 793 /// Callback type used to determine whether to reject incoming connections 794 alias RejectConnectionPredicate = bool delegate (in NetworkAddress) @safe nothrow; 795 796 797 /** 798 Options altering how sessions are created. 799 800 Multiple values can be or'ed together. 801 802 See_Also: HTTPServerResponse.startSession 803 */ 804 enum SessionOption { 805 /// No options. 806 none = 0, 807 808 /** Instructs the browser to disallow accessing the session ID from JavaScript. 809 810 See_Also: Cookie.httpOnly 811 */ 812 httpOnly = 1<<0, 813 814 /** Instructs the browser to disallow sending the session ID over 815 unencrypted connections. 816 817 By default, the type of the connection on which the session is started 818 will be used to determine if secure or noSecure is used. 819 820 See_Also: noSecure, Cookie.secure 821 */ 822 secure = 1<<1, 823 824 /** Instructs the browser to allow sending the session ID over unencrypted 825 connections. 826 827 By default, the type of the connection on which the session is started 828 will be used to determine if secure or noSecure is used. 829 830 See_Also: secure, Cookie.secure 831 */ 832 noSecure = 1<<2, 833 834 /** 835 Instructs the browser to allow sending this cookie along with cross-site requests. 836 837 By default, the protection is `strict`. This flag allows to set it to `lax`. 838 The strict value will prevent the cookie from being sent by the browser 839 to the target site in all cross-site browsing context, 840 even when following a regular link. 841 */ 842 noSameSiteStrict = 1<<3, 843 } 844 845 846 /** 847 Represents a HTTP request as received by the server side. 848 */ 849 final class HTTPServerRequest : HTTPRequest { 850 import std.variant : Variant; 851 import vibe.utils.dictionarylist : DictionaryList; 852 853 private { 854 SysTime m_timeCreated; 855 HTTPServerSettings m_settings; 856 ushort m_port; 857 string m_peer; 858 859 // lazily parsed request components 860 Nullable!string m_path; 861 Nullable!CookieValueMap m_cookies; 862 Nullable!FormFields m_query; 863 Nullable!Json m_json; 864 Nullable!FormFields m_form; 865 FilePartFormFields m_files; 866 } 867 868 /// The IP address of the client 869 NetworkAddress clientAddress; 870 871 /// Determines if the request should be logged to the access log file. 872 bool noLog; 873 874 /// Determines if the request was issued over an TLS encrypted channel. 875 bool tls; 876 877 /** Information about the TLS certificate provided by the client. 878 879 Remarks: This field is only set if `tls` is true, and the peer 880 presented a client certificate. 881 */ 882 TLSCertificateInformation clientCertificate; 883 884 /** The path part of the requested URI. 885 */ 886 InetPath requestPath; 887 888 /** The user name part of the URL, if present. 889 */ 890 string username; 891 892 /** The _password part of the URL, if present. 893 */ 894 string password; 895 896 /** The _query string part of the URL. 897 */ 898 string queryString; 899 900 /** A map of general parameters for the request. 901 902 This map is supposed to be used by middleware functionality to store 903 information for later stages. For example vibe.http.router.URLRouter uses this map 904 to store the value of any named placeholders. 905 */ 906 DictionaryList!(string, true, 8) params; 907 908 /** A map of context items for the request. 909 910 This is especially useful for passing application specific data down 911 the chain of processors along with the request itself. 912 913 For example, a generic route may be defined to check user login status, 914 if the user is logged in, add a reference to user specific data to the 915 context. 916 917 This is implemented with `std.variant.Variant` to allow any type of data. 918 */ 919 DictionaryList!(Variant, true, 2) context; 920 921 /** Supplies the request body as a stream. 922 923 Note that when certain server options are set (such as 924 HTTPServerOption.parseJsonBody) and a matching request was sent, 925 the returned stream will be empty. If needed, remove those 926 options and do your own processing of the body when launching 927 the server. HTTPServerOption has a list of all options that affect 928 the request body. 929 */ 930 InputStream bodyReader; 931 932 /** The current Session object. 933 934 This field is set if HTTPServerResponse.startSession() has been called 935 on a previous response and if the client has sent back the matching 936 cookie. 937 938 Remarks: Requires the HTTPServerOption.parseCookies option. 939 */ 940 Session session; 941 942 943 this(SysTime time, ushort port) 944 @safe scope { 945 m_timeCreated = time.toUTC(); 946 m_port = port; 947 } 948 949 /// The IP address of the client in string form 950 @property string peer() 951 @safe nothrow scope { 952 if (!m_peer) { 953 version (Have_vibe_core) {} else scope (failure) assert(false); 954 // store the IP address (IPv4 addresses forwarded over IPv6 are stored in IPv4 format) 955 auto peer_address_string = this.clientAddress.toString(); 956 if (peer_address_string.startsWith("::ffff:") && peer_address_string[7 .. $].indexOf(':') < 0) 957 m_peer = peer_address_string[7 .. $]; 958 else m_peer = peer_address_string; 959 } 960 return m_peer; 961 } 962 963 /** The _path part of the URL. 964 965 Note that this function contains the decoded version of the 966 requested path, which can yield incorrect results if the path 967 contains URL encoded path separators. Use `requestPath` instead to 968 get an encoding-aware representation. 969 */ 970 deprecated("Use .requestPath instead") 971 string path() 972 @safe scope { 973 if (m_path.isNull) 974 m_path = urlDecode(requestPath.toString); 975 return m_path.get; 976 } 977 978 /** Contains the list of cookies that are stored on the client. 979 980 Note that the a single cookie name may occur multiple times if multiple 981 cookies have that name but different paths or domains that all match 982 the request URI. By default, the first cookie will be returned, which is 983 the or one of the cookies with the closest path match. 984 */ 985 @property ref CookieValueMap cookies() 986 @safe return { 987 if (m_cookies.isNull) { 988 m_cookies = CookieValueMap.init; 989 if (auto pv = "cookie" in headers) 990 parseCookies(*pv, m_cookies.get); 991 } 992 return m_cookies.get; 993 } 994 995 /** Contains all _form fields supplied using the _query string. 996 997 The fields are stored in the same order as they are received. 998 */ 999 @property ref FormFields query() 1000 @safe return { 1001 if (m_query.isNull) { 1002 m_query = FormFields.init; 1003 parseURLEncodedForm(queryString, m_query.get); 1004 } 1005 1006 return m_query.get; 1007 } 1008 1009 /** Contains the parsed Json for a JSON request. 1010 1011 A JSON request must have the Content-Type "application/json" or "application/vnd.api+json". 1012 */ 1013 @property ref Json json() 1014 @safe return { 1015 if (m_json.isNull) { 1016 auto splitter = contentType.splitter(';'); 1017 auto ctype = splitter.empty ? "" : splitter.front; 1018 1019 if (icmp2(ctype, "application/json") == 0 || icmp2(ctype, "application/vnd.api+json") == 0) { 1020 auto bodyStr = bodyReader.readAllUTF8(); 1021 if (!bodyStr.empty) m_json = parseJson(bodyStr); 1022 else m_json = Json.undefined; 1023 } else { 1024 m_json = Json.undefined; 1025 } 1026 } 1027 return m_json.get; 1028 } 1029 1030 /// Get the json body when there is no content-type header 1031 unittest { 1032 assert(createTestHTTPServerRequest(URL("http://localhost/")).json.type == Json.Type.undefined); 1033 } 1034 1035 /** Contains the parsed parameters of a HTML POST _form request. 1036 1037 The fields are stored in the same order as they are received. 1038 1039 Remarks: 1040 A form request must either have the Content-Type 1041 "application/x-www-form-urlencoded" or "multipart/form-data". 1042 */ 1043 @property ref FormFields form() 1044 @safe return { 1045 if (m_form.isNull) 1046 parseFormAndFiles(); 1047 1048 return m_form.get; 1049 } 1050 1051 /** Contains information about any uploaded file for a HTML _form request. 1052 */ 1053 @property ref FilePartFormFields files() 1054 @safe return { 1055 // m_form and m_files are parsed in one step 1056 if (m_form.isNull) { 1057 parseFormAndFiles(); 1058 assert(!m_form.isNull); 1059 } 1060 1061 return m_files; 1062 } 1063 1064 /** Time when this request started processing. 1065 */ 1066 @property SysTime timeCreated() const @safe scope { return m_timeCreated; } 1067 1068 1069 /** The full URL that corresponds to this request. 1070 1071 The host URL includes the protocol, host and optionally the user 1072 and password that was used for this request. This field is useful to 1073 construct self referencing URLs. 1074 1075 Note that the port is currently not set, so that this only works if 1076 the standard port is used. 1077 */ 1078 @property URL fullURL() 1079 const @safe scope { 1080 URL url; 1081 1082 auto xfh = this.headers.get("X-Forwarded-Host"); 1083 auto xfp = this.headers.get("X-Forwarded-Port"); 1084 auto xfpr = this.headers.get("X-Forwarded-Proto"); 1085 1086 // Set URL host segment. 1087 if (xfh.length) { 1088 url.host = xfh; 1089 } else if (!this.host.empty) { 1090 url.host = this.host; 1091 } else if (!m_settings.hostName.empty) { 1092 url.host = m_settings.hostName; 1093 } else { 1094 url.host = m_settings.bindAddresses[0]; 1095 } 1096 1097 // Set URL schema segment. 1098 if (xfpr.length) { 1099 url.schema = xfpr; 1100 } else if (this.tls) { 1101 url.schema = "https"; 1102 } else { 1103 url.schema = "http"; 1104 } 1105 1106 // Set URL port segment. 1107 if (xfp.length) { 1108 try { 1109 url.port = xfp.to!ushort; 1110 } catch (ConvException) { 1111 // TODO : Consider responding with a 400/etc. error from here. 1112 logWarn("X-Forwarded-Port header was not valid port (%s)", xfp); 1113 } 1114 } else if (!xfh) { 1115 if (url.schema == "https") { 1116 if (m_port != 443U) url.port = m_port; 1117 } else { 1118 if (m_port != 80U) url.port = m_port; 1119 } 1120 } 1121 1122 if (url.host.startsWith('[')) { // handle IPv6 address 1123 auto idx = url.host.indexOf(']'); 1124 if (idx >= 0 && idx+1 < url.host.length && url.host[idx+1] == ':') 1125 url.host = url.host[1 .. idx]; 1126 } else { // handle normal host names or IPv4 address 1127 auto idx = url.host.indexOf(':'); 1128 if (idx >= 0) url.host = url.host[0 .. idx]; 1129 } 1130 1131 url.username = this.username; 1132 url.password = this.password; 1133 url.localURI = this.requestURI; 1134 1135 return url; 1136 } 1137 1138 /** The relative path to the root folder. 1139 1140 Using this function instead of absolute URLs for embedded links can be 1141 useful to avoid dead link when the site is piped through a 1142 reverse-proxy. 1143 1144 The returned string always ends with a slash. 1145 */ 1146 @property string rootDir() 1147 const @safe scope { 1148 import std.algorithm.searching : count; 1149 import std.range : empty; 1150 auto depth = requestPath.bySegment.count!(s => !s.name.empty); 1151 if (depth > 0 && !requestPath.endsWithSlash) depth--; 1152 return depth == 0 ? "./" : replicate("../", depth); 1153 } 1154 1155 unittest { 1156 assert(createTestHTTPServerRequest(URL("http://localhost/")).rootDir == "./"); 1157 assert(createTestHTTPServerRequest(URL("http://localhost/foo")).rootDir == "./"); 1158 assert(createTestHTTPServerRequest(URL("http://localhost/foo/")).rootDir == "../"); 1159 assert(createTestHTTPServerRequest(URL("http://localhost/foo/bar")).rootDir == "../"); 1160 assert(createTestHTTPServerRequest(URL("http://localhost")).rootDir == "./"); 1161 } 1162 1163 /** The settings of the server serving this request. 1164 */ 1165 package @property const(HTTPServerSettings) serverSettings() 1166 const @safe scope { 1167 return m_settings; 1168 } 1169 1170 private void parseFormAndFiles() 1171 @safe scope { 1172 m_form = FormFields.init; 1173 parseFormData(m_form.get, m_files, headers.get("Content-Type", ""), bodyReader, MaxHTTPHeaderLineLength); 1174 } 1175 } 1176 1177 1178 /** 1179 Represents a HTTP response as sent from the server side. 1180 */ 1181 final class HTTPServerResponse : HTTPResponse { 1182 private { 1183 InterfaceProxy!Stream m_conn; 1184 InterfaceProxy!ConnectionStream m_rawConnection; 1185 InterfaceProxy!OutputStream m_bodyWriter; 1186 IAllocator m_requestAlloc; 1187 FreeListRef!ChunkedOutputStream m_chunkedBodyWriter; 1188 FreeListRef!CountingOutputStream m_countingWriter; 1189 FreeListRef!ZlibOutputStream m_zlibOutputStream; 1190 HTTPServerSettings m_settings; 1191 Session m_session; 1192 bool m_headerWritten = false; 1193 bool m_isHeadResponse = false; 1194 bool m_tls; 1195 bool m_requiresConnectionClose; 1196 SysTime m_timeFinalized; 1197 } 1198 1199 static if (!is(Stream == InterfaceProxy!Stream)) { 1200 this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 1201 @safe scope { 1202 this(InterfaceProxy!Stream(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc); 1203 } 1204 } 1205 1206 this(InterfaceProxy!Stream conn, InterfaceProxy!ConnectionStream raw_connection, HTTPServerSettings settings, IAllocator req_alloc) 1207 @safe scope { 1208 m_conn = conn; 1209 m_rawConnection = raw_connection; 1210 m_countingWriter = createCountingOutputStreamFL(conn); 1211 m_settings = settings; 1212 m_requestAlloc = req_alloc; 1213 } 1214 1215 /** Sends a redirect request to the client. 1216 1217 Params: 1218 url = The URL to redirect to 1219 status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found) 1220 */ 1221 void redirect(string url, int status = HTTPStatus.found) 1222 @safe scope { 1223 // Disallow any characters that may influence the header parsing 1224 enforce(!url.representation.canFind!(ch => ch < 0x20), 1225 "Control character in redirection URL."); 1226 1227 statusCode = status; 1228 headers["Location"] = url; 1229 writeBody("redirecting..."); 1230 } 1231 /// ditto 1232 void redirect(URL url, int status = HTTPStatus.found) 1233 @safe scope { 1234 redirect(url.toString(), status); 1235 } 1236 1237 /// 1238 @safe unittest { 1239 import vibe.http.router; 1240 1241 void request_handler(HTTPServerRequest req, HTTPServerResponse res) 1242 { 1243 res.redirect("http://example.org/some_other_url"); 1244 } 1245 1246 void test() 1247 { 1248 auto router = new URLRouter; 1249 router.get("/old_url", &request_handler); 1250 1251 listenHTTP(new HTTPServerSettings, router); 1252 } 1253 } 1254 1255 scope: 1256 1257 /** Returns the time at which the request was finalized. 1258 1259 Note that this field will only be set after `finalize` has been called. 1260 */ 1261 @property SysTime timeFinalized() const @safe { return m_timeFinalized; } 1262 1263 /** Determines if the HTTP header has already been written. 1264 */ 1265 @property bool headerWritten() const @safe { return m_headerWritten; } 1266 1267 /** Determines if the response does not need a body. 1268 */ 1269 bool isHeadResponse() const @safe { return m_isHeadResponse; } 1270 1271 /** Determines if the response is sent over an encrypted connection. 1272 */ 1273 bool tls() const @safe { return m_tls; } 1274 1275 /** Writes the entire response body at once. 1276 1277 Params: 1278 data = The data to write as the body contents 1279 status = Optional response status code to set 1280 content_type = Optional content type to apply to the response. 1281 If no content type is given and no "Content-Type" header is 1282 set in the response, this will default to 1283 `"application/octet-stream"`. 1284 1285 See_Also: `HTTPStatusCode` 1286 */ 1287 void writeBody(in ubyte[] data, string content_type = null) 1288 @safe { 1289 if (content_type.length) headers["Content-Type"] = content_type; 1290 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 1291 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", data.length); 1292 bodyWriter.write(data); 1293 } 1294 /// ditto 1295 void writeBody(in ubyte[] data, int status, string content_type = null) 1296 @safe { 1297 statusCode = status; 1298 writeBody(data, content_type); 1299 } 1300 /// ditto 1301 void writeBody(scope InputStream data, string content_type = null) 1302 @safe { 1303 if (content_type.length) headers["Content-Type"] = content_type; 1304 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 1305 data.pipe(bodyWriter); 1306 } 1307 1308 /** Writes the entire response body as a single string. 1309 1310 Params: 1311 data = The string to write as the body contents 1312 status = Optional response status code to set 1313 content_type = Optional content type to apply to the response. 1314 If no content type is given and no "Content-Type" header is 1315 set in the response, this will default to 1316 `"text/plain; charset=UTF-8"`. 1317 1318 See_Also: `HTTPStatusCode` 1319 */ 1320 /// ditto 1321 void writeBody(string data, string content_type = null) 1322 @safe { 1323 if (!content_type.length && "Content-Type" !in headers) 1324 content_type = "text/plain; charset=UTF-8"; 1325 writeBody(cast(const(ubyte)[])data, content_type); 1326 } 1327 /// ditto 1328 void writeBody(string data, int status, string content_type = null) 1329 @safe { 1330 statusCode = status; 1331 writeBody(data, content_type); 1332 } 1333 1334 /** Writes the whole response body at once, without doing any further encoding. 1335 1336 The caller has to make sure that the appropriate headers are set correctly 1337 (i.e. Content-Type and Content-Encoding). 1338 1339 Note that the version taking a RandomAccessStream may perform additional 1340 optimizations such as sending a file directly from the disk to the 1341 network card using a DMA transfer. 1342 1343 */ 1344 void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe 1345 if (isRandomAccessStream!RandomAccessStream) 1346 { 1347 assert(!m_headerWritten, "A body was already written!"); 1348 writeHeader(); 1349 if (m_isHeadResponse) return; 1350 1351 auto bytes = stream.size - stream.tell(); 1352 stream.pipe(m_conn); 1353 m_countingWriter.increment(bytes); 1354 } 1355 /// ditto 1356 void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = 0) @safe 1357 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1358 { 1359 assert(!m_headerWritten, "A body was already written!"); 1360 writeHeader(); 1361 if (m_isHeadResponse) return; 1362 1363 if (num_bytes > 0) { 1364 stream.pipe(m_conn, num_bytes); 1365 m_countingWriter.increment(num_bytes); 1366 } else stream.pipe(m_countingWriter, num_bytes); 1367 } 1368 /// ditto 1369 void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe 1370 if (isRandomAccessStream!RandomAccessStream) 1371 { 1372 statusCode = status; 1373 writeRawBody(stream); 1374 } 1375 /// ditto 1376 void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe 1377 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1378 { 1379 statusCode = status; 1380 writeRawBody(stream, num_bytes); 1381 } 1382 1383 1384 /// Writes a JSON message with the specified status 1385 void writeJsonBody(T)(T data, int status, bool allow_chunked = false) 1386 { 1387 statusCode = status; 1388 writeJsonBody(data, allow_chunked); 1389 } 1390 /// ditto 1391 void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false) 1392 { 1393 statusCode = status; 1394 writeJsonBody(data, content_type, allow_chunked); 1395 } 1396 1397 /// ditto 1398 void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false) 1399 { 1400 headers["Content-Type"] = content_type; 1401 writeJsonBody(data, allow_chunked); 1402 } 1403 /// ditto 1404 void writeJsonBody(T)(T data, bool allow_chunked = false) 1405 { 1406 doWriteJsonBody!(T, false)(data, allow_chunked); 1407 } 1408 /// ditto 1409 void writePrettyJsonBody(T)(T data, bool allow_chunked = false) 1410 { 1411 doWriteJsonBody!(T, true)(data, allow_chunked); 1412 } 1413 1414 private void doWriteJsonBody(T, bool PRETTY)(T data, bool allow_chunked = false) 1415 { 1416 import std.traits; 1417 import vibe.stream.wrapper; 1418 1419 static if (!is(T == Json) && is(typeof(data.data())) && isArray!(typeof(data.data()))) { 1420 static assert(!is(T == Appender!(typeof(data.data()))), "Passed an Appender!T to writeJsonBody - this is most probably not doing what's indended."); 1421 } 1422 1423 if ("Content-Type" !in headers) 1424 headers["Content-Type"] = "application/json; charset=UTF-8"; 1425 1426 1427 // set an explicit content-length field if chunked encoding is not allowed 1428 if (!allow_chunked) { 1429 import vibe.internal.rangeutil; 1430 long length = 0; 1431 auto counter = RangeCounter(() @trusted { return &length; } ()); 1432 static if (PRETTY) serializeToPrettyJson(counter, data); 1433 else serializeToJson(counter, data); 1434 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length); 1435 } 1436 1437 auto rng = streamOutputRange!1024(bodyWriter); 1438 static if (PRETTY) serializeToPrettyJson(() @trusted { return &rng; } (), data); 1439 else serializeToJson(() @trusted { return &rng; } (), data); 1440 } 1441 1442 /** 1443 * Writes the response with no body. 1444 * 1445 * This method should be used in situations where no body is 1446 * requested, such as a HEAD request. For an empty body, just use writeBody, 1447 * as this method causes problems with some keep-alive connections. 1448 */ 1449 void writeVoidBody() 1450 @safe { 1451 if (!m_isHeadResponse) { 1452 assert("Content-Length" !in headers); 1453 assert("Transfer-Encoding" !in headers); 1454 } 1455 assert(!headerWritten); 1456 writeHeader(); 1457 m_conn.flush(); 1458 } 1459 1460 /** A stream for writing the body of the HTTP response. 1461 1462 Note that after 'bodyWriter' has been accessed for the first time, it 1463 is not allowed to change any header or the status code of the response. 1464 */ 1465 @property InterfaceProxy!OutputStream bodyWriter() 1466 @safe scope { 1467 assert(!!m_conn); 1468 if (m_bodyWriter) { 1469 // for test responses, the body writer is pre-set, without headers 1470 // being written, so we may need to do that here 1471 if (!m_headerWritten) writeHeader(); 1472 1473 return m_bodyWriter; 1474 } 1475 1476 assert(!m_headerWritten, "A void body was already written!"); 1477 assert(this.statusCode >= 200, "1xx responses can't have body"); 1478 1479 if (m_isHeadResponse) { 1480 // for HEAD requests, we define a NullOutputWriter for convenience 1481 // - no body will be written. However, the request handler should call writeVoidBody() 1482 // and skip writing of the body in this case. 1483 if ("Content-Length" !in headers) 1484 headers["Transfer-Encoding"] = "chunked"; 1485 writeHeader(); 1486 m_bodyWriter = nullSink; 1487 return m_bodyWriter; 1488 } 1489 1490 if ("Content-Encoding" in headers && "Content-Length" in headers) { 1491 // we do not known how large the compressed body will be in advance 1492 // so remove the content-length and use chunked transfer 1493 headers.remove("Content-Length"); 1494 } 1495 1496 if (auto pcl = "Content-Length" in headers) { 1497 writeHeader(); 1498 m_countingWriter.writeLimit = (*pcl).to!ulong; 1499 m_bodyWriter = m_countingWriter; 1500 } else if (httpVersion <= HTTPVersion.HTTP_1_0) { 1501 if ("Connection" in headers) 1502 headers.remove("Connection"); // default to "close" 1503 writeHeader(); 1504 m_bodyWriter = m_conn; 1505 } else { 1506 headers["Transfer-Encoding"] = "chunked"; 1507 writeHeader(); 1508 m_chunkedBodyWriter = createChunkedOutputStreamFL(m_countingWriter); 1509 m_bodyWriter = m_chunkedBodyWriter; 1510 } 1511 1512 if (auto pce = "Content-Encoding" in headers) { 1513 if (icmp2(*pce, "gzip") == 0) { 1514 m_zlibOutputStream = createGzipOutputStreamFL(m_bodyWriter); 1515 m_bodyWriter = m_zlibOutputStream; 1516 } else if (icmp2(*pce, "deflate") == 0) { 1517 m_zlibOutputStream = createDeflateOutputStreamFL(m_bodyWriter); 1518 m_bodyWriter = m_zlibOutputStream; 1519 } else { 1520 logWarn("Unsupported Content-Encoding set in response: '"~*pce~"'"); 1521 } 1522 } 1523 1524 return m_bodyWriter; 1525 } 1526 1527 1528 1529 /** Special method sending a SWITCHING_PROTOCOLS response to the client. 1530 1531 Notice: For the overload that returns a `ConnectionStream`, it must be 1532 ensured that the returned instance doesn't outlive the request 1533 handler callback. 1534 1535 Params: 1536 protocol = The protocol set in the "Upgrade" header of the response. 1537 Use an empty string to skip setting this field. 1538 */ 1539 ConnectionStream switchProtocol(string protocol) 1540 @safe { 1541 statusCode = HTTPStatus.switchingProtocols; 1542 if (protocol.length) headers["Upgrade"] = protocol; 1543 writeVoidBody(); 1544 m_requiresConnectionClose = true; 1545 m_headerWritten = true; 1546 return createConnectionProxyStream(m_conn, m_rawConnection); 1547 } 1548 /// ditto 1549 void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del) 1550 @safe { 1551 statusCode = HTTPStatus.switchingProtocols; 1552 if (protocol.length) headers["Upgrade"] = protocol; 1553 writeVoidBody(); 1554 m_requiresConnectionClose = true; 1555 m_headerWritten = true; 1556 () @trusted { 1557 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 1558 del(conn); 1559 } (); 1560 finalize(); 1561 } 1562 1563 /** Special method for handling CONNECT proxy tunnel 1564 1565 Notice: For the overload that returns a `ConnectionStream`, it must be 1566 ensured that the returned instance doesn't outlive the request 1567 handler callback. 1568 */ 1569 ConnectionStream connectProxy() 1570 @safe { 1571 return createConnectionProxyStream(m_conn, m_rawConnection); 1572 } 1573 /// ditto 1574 void connectProxy(scope void delegate(scope ConnectionStream) @safe del) 1575 @safe { 1576 () @trusted { 1577 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 1578 del(conn); 1579 } (); 1580 finalize(); 1581 } 1582 1583 /** Sets the specified cookie value. 1584 1585 Params: 1586 name = Name of the cookie 1587 value = New cookie value - pass null to clear the cookie 1588 path = Path (as seen by the client) of the directory tree in which the cookie is visible 1589 encoding = Optional encoding (url, raw), default to URL encoding 1590 */ 1591 Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url) 1592 @safe { 1593 auto cookie = new Cookie(); 1594 cookie.path = path; 1595 cookie.setValue(value, encoding); 1596 if (value is null) { 1597 cookie.maxAge = 0; 1598 cookie.expires = "Thu, 01 Jan 1970 00:00:00 GMT"; 1599 } 1600 cookies[name] = cookie; 1601 return cookie; 1602 } 1603 1604 /** 1605 Initiates a new session. 1606 1607 The session is stored in the SessionStore that was specified when 1608 creating the server. Depending on this, the session can be persistent 1609 or temporary and specific to this server instance. 1610 */ 1611 Session startSession(string path = "/") 1612 @safe { 1613 return startSession(path, m_settings.sessionOptions); 1614 } 1615 1616 /// ditto 1617 Session startSession(string path, SessionOption options) 1618 @safe { 1619 assert(m_settings.sessionStore, "no session store set"); 1620 assert(!m_session, "Try to start a session, but already started one."); 1621 1622 bool secure; 1623 if (options & SessionOption.secure) secure = true; 1624 else if (options & SessionOption.noSecure) secure = false; 1625 else secure = this.tls; 1626 1627 m_session = m_settings.sessionStore.create(); 1628 m_session.set("$sessionCookiePath", path); 1629 m_session.set("$sessionCookieSecure", secure); 1630 auto cookie = setCookie(m_settings.sessionIdCookie, m_session.id, path); 1631 cookie.secure = secure; 1632 cookie.httpOnly = (options & SessionOption.httpOnly) != 0; 1633 cookie.sameSite = (options & SessionOption.noSameSiteStrict) ? 1634 Cookie.SameSite.lax : Cookie.SameSite.strict; 1635 return m_session; 1636 } 1637 1638 /** 1639 Terminates the current session (if any). 1640 */ 1641 void terminateSession() 1642 @safe { 1643 if (!m_session) return; 1644 auto cookie = setCookie(m_settings.sessionIdCookie, null, m_session.get!string("$sessionCookiePath")); 1645 cookie.secure = m_session.get!bool("$sessionCookieSecure"); 1646 m_session.destroy(); 1647 m_session = Session.init; 1648 } 1649 1650 @property ulong bytesWritten() @safe const { return m_countingWriter.bytesWritten; } 1651 1652 /** 1653 Waits until either the connection closes, data arrives, or until the 1654 given timeout is reached. 1655 1656 Returns: 1657 $(D true) if the connection was closed and $(D false) if either the 1658 timeout was reached, or if data has arrived for consumption. 1659 1660 See_Also: `connected` 1661 */ 1662 bool waitForConnectionClose(Duration timeout = Duration.max) 1663 @safe { 1664 if (!m_rawConnection || !m_rawConnection.connected) return true; 1665 m_rawConnection.waitForData(timeout); 1666 return !m_rawConnection.connected; 1667 } 1668 1669 /** 1670 Determines if the underlying connection is still alive. 1671 1672 Returns $(D true) if the remote peer is still connected and $(D false) 1673 if the remote peer closed the connection. 1674 1675 See_Also: `waitForConnectionClose` 1676 */ 1677 @property bool connected() 1678 @safe const { 1679 if (!m_rawConnection) return false; 1680 return m_rawConnection.connected; 1681 } 1682 1683 /** 1684 Finalizes the response. This is usually called automatically by the server. 1685 1686 This method can be called manually after writing the response to force 1687 all network traffic associated with the current request to be finalized. 1688 After the call returns, the `timeFinalized` property will be set. 1689 */ 1690 void finalize() 1691 @safe { 1692 if (m_zlibOutputStream) { 1693 m_zlibOutputStream.finalize(); 1694 m_zlibOutputStream.destroy(); 1695 } 1696 if (m_chunkedBodyWriter) { 1697 m_chunkedBodyWriter.finalize(); 1698 m_chunkedBodyWriter.destroy(); 1699 } 1700 1701 // ignore exceptions caused by an already closed connection - the client 1702 // may have closed the connection already and this doesn't usually indicate 1703 // a problem. 1704 if (m_rawConnection && m_rawConnection.connected) { 1705 try if (m_conn) m_conn.flush(); 1706 catch (Exception e) logDebug("Failed to flush connection after finishing HTTP response: %s", e.msg); 1707 if (!isHeadResponse && bytesWritten < headers.get("Content-Length", "0").to!ulong) { 1708 logDebug("HTTP response only written partially before finalization. Terminating connection."); 1709 m_requiresConnectionClose = true; 1710 } 1711 1712 m_rawConnection = InterfaceProxy!ConnectionStream.init; 1713 } 1714 1715 if (m_conn) { 1716 m_conn = InterfaceProxy!Stream.init; 1717 m_timeFinalized = Clock.currTime(UTC()); 1718 } 1719 } 1720 1721 private void writeHeader() 1722 @safe { 1723 import vibe.stream.wrapper; 1724 1725 assert(!m_headerWritten, "Try to write header after body has already begun."); 1726 assert(this.httpVersion != HTTPVersion.HTTP_1_0 || this.statusCode >= 200, "Informational status codes aren't supported by HTTP/1.0."); 1727 1728 // Don't set m_headerWritten for 1xx status codes 1729 if (this.statusCode >= 200) m_headerWritten = true; 1730 auto dst = streamOutputRange!1024(m_conn); 1731 1732 void writeLine(T...)(string fmt, T args) 1733 @safe { 1734 formattedWrite(() @trusted { return &dst; } (), fmt, args); 1735 dst.put("\r\n"); 1736 logTrace(fmt, args); 1737 } 1738 1739 logTrace("---------------------"); 1740 logTrace("HTTP server response:"); 1741 logTrace("---------------------"); 1742 1743 // write the status line 1744 writeLine("%s %d %s", 1745 getHTTPVersionString(this.httpVersion), 1746 this.statusCode, 1747 this.statusPhrase.length ? this.statusPhrase : httpStatusText(this.statusCode)); 1748 1749 // write all normal headers 1750 foreach (k, v; this.headers.byKeyValue) { 1751 dst.put(k); 1752 dst.put(": "); 1753 dst.put(v); 1754 dst.put("\r\n"); 1755 logTrace("%s: %s", k, v); 1756 } 1757 1758 logTrace("---------------------"); 1759 1760 // write cookies 1761 foreach (n, cookie; this.cookies.byKeyValue) { 1762 dst.put("Set-Cookie: "); 1763 cookie.writeString(() @trusted { return &dst; } (), n); 1764 dst.put("\r\n"); 1765 } 1766 1767 // finalize response header 1768 dst.put("\r\n"); 1769 } 1770 } 1771 1772 /** 1773 Represents the request listener for a specific `listenHTTP` call. 1774 1775 This struct can be used to stop listening for HTTP requests at runtime. 1776 */ 1777 struct HTTPListener { 1778 private { 1779 size_t[] m_virtualHostIDs; 1780 } 1781 1782 private this(size_t[] ids) @safe { m_virtualHostIDs = ids; } 1783 1784 @property NetworkAddress[] bindAddresses() 1785 @safe { 1786 NetworkAddress[] ret; 1787 foreach (l; s_listeners) 1788 if (l.m_virtualHosts.canFind!(v => m_virtualHostIDs.canFind(v.id))) { 1789 NetworkAddress a; 1790 a = resolveHost(l.bindAddress); 1791 a.port = l.bindPort; 1792 ret ~= a; 1793 } 1794 return ret; 1795 } 1796 1797 /** Stops handling HTTP requests and closes the TCP listening port if 1798 possible. 1799 */ 1800 void stopListening() 1801 @safe { 1802 import std.algorithm : countUntil; 1803 1804 foreach (vhid; m_virtualHostIDs) { 1805 foreach (lidx, l; s_listeners) { 1806 if (l.removeVirtualHost(vhid)) { 1807 if (!l.hasVirtualHosts) { 1808 l.m_listener.stopListening(); 1809 logInfo("Stopped to listen for HTTP%s requests on %s:%s", l.tlsContext ? "S": "", l.bindAddress, l.bindPort); 1810 s_listeners = s_listeners[0 .. lidx] ~ s_listeners[lidx+1 .. $]; 1811 } 1812 } 1813 break; 1814 } 1815 } 1816 } 1817 } 1818 1819 1820 /** Represents a single HTTP server port. 1821 1822 This class defines the incoming interface, port, and TLS configuration of 1823 the public server port. The public server port may differ from the local 1824 one if a reverse proxy of some kind is facing the public internet and 1825 forwards to this HTTP server. 1826 1827 Multiple virtual hosts can be configured to be served from the same port. 1828 Their TLS settings must be compatible and each virtual host must have a 1829 unique name. 1830 */ 1831 final class HTTPServerContext { 1832 private struct VirtualHost { 1833 HTTPServerRequestDelegate requestHandler; 1834 HTTPServerSettings settings; 1835 HTTPLogger[] loggers; 1836 size_t id; 1837 } 1838 1839 private { 1840 TCPListener m_listener; 1841 VirtualHost[] m_virtualHosts; 1842 string m_bindAddress; 1843 ushort m_bindPort; 1844 TLSContext m_tlsContext; 1845 static size_t s_vhostIDCounter = 1; 1846 } 1847 1848 @safe: 1849 1850 this(string bind_address, ushort bind_port) 1851 { 1852 m_bindAddress = bind_address; 1853 m_bindPort = bind_port; 1854 } 1855 1856 /** Returns the TLS context associated with the listener. 1857 1858 For non-HTTPS listeners, `null` will be returned. Otherwise, if only a 1859 single virtual host has been added, the TLS context of that host's 1860 settings is returned. For multiple virtual hosts, an SNI context is 1861 returned, which forwards to the individual contexts based on the 1862 requested host name. 1863 */ 1864 @property TLSContext tlsContext() { return m_tlsContext; } 1865 1866 /// The local network interface IP address associated with this listener 1867 @property string bindAddress() const { return m_bindAddress; } 1868 1869 /// The local port associated with this listener 1870 @property ushort bindPort() const { return m_bindPort; } 1871 1872 /// Determines if any virtual hosts have been addded 1873 @property bool hasVirtualHosts() const { return m_virtualHosts.length > 0; } 1874 1875 /** Adds a single virtual host. 1876 1877 Note that the port and bind address defined in `settings` must match the 1878 ones for this listener. The `settings.host` field must be unique for 1879 all virtual hosts. 1880 1881 Returns: Returns a unique ID for the new virtual host 1882 */ 1883 size_t addVirtualHost(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) 1884 { 1885 assert(settings.port == 0 || settings.port == m_bindPort, "Virtual host settings do not match bind port."); 1886 assert(settings.bindAddresses.canFind(m_bindAddress), "Virtual host settings do not match bind address."); 1887 1888 VirtualHost vhost; 1889 vhost.id = s_vhostIDCounter++; 1890 vhost.settings = settings; 1891 vhost.requestHandler = request_handler; 1892 1893 if (settings.accessLogger) vhost.loggers ~= settings.accessLogger; 1894 if (settings.accessLogToConsole) 1895 vhost.loggers ~= new HTTPConsoleLogger(settings, settings.accessLogFormat); 1896 if (settings.accessLogFile.length) 1897 vhost.loggers ~= new HTTPFileLogger(settings, settings.accessLogFormat, settings.accessLogFile); 1898 1899 if (!m_virtualHosts.length) m_tlsContext = settings.tlsContext; 1900 1901 enforce((m_tlsContext !is null) == (settings.tlsContext !is null), 1902 "Cannot mix HTTP and HTTPS virtual hosts within the same listener."); 1903 1904 if (m_tlsContext) addSNIHost(settings); 1905 1906 m_virtualHosts ~= vhost; 1907 1908 if (settings.hostName.length) { 1909 auto proto = settings.tlsContext ? "https" : "http"; 1910 auto port = settings.tlsContext && settings.port == 443 || !settings.tlsContext && settings.port == 80 ? "" : ":" ~ settings.port.to!string; 1911 logInfo("Added virtual host %s://%s:%s/ (%s)", proto, settings.hostName, m_bindPort, m_bindAddress); 1912 } 1913 1914 return vhost.id; 1915 } 1916 1917 /// Removes a previously added virtual host using its ID. 1918 bool removeVirtualHost(size_t id) 1919 { 1920 import std.algorithm.searching : countUntil; 1921 1922 auto idx = m_virtualHosts.countUntil!(c => c.id == id); 1923 if (idx < 0) return false; 1924 1925 auto ctx = m_virtualHosts[idx]; 1926 m_virtualHosts = m_virtualHosts[0 .. idx] ~ m_virtualHosts[idx+1 .. $]; 1927 return true; 1928 } 1929 1930 private void addSNIHost(HTTPServerSettings settings) 1931 { 1932 if (settings.tlsContext !is m_tlsContext && m_tlsContext.kind != TLSContextKind.serverSNI) { 1933 logDebug("Create SNI TLS context for %s, port %s", bindAddress, bindPort); 1934 m_tlsContext = createTLSContext(TLSContextKind.serverSNI); 1935 m_tlsContext.sniCallback = &onSNI; 1936 } 1937 1938 foreach (ctx; m_virtualHosts) { 1939 /*enforce(ctx.settings.hostName != settings.hostName, 1940 "A server with the host name '"~settings.hostName~"' is already " 1941 "listening on "~addr~":"~to!string(settings.port)~".");*/ 1942 } 1943 } 1944 1945 private TLSContext onSNI(string servername) 1946 { 1947 foreach (vhost; m_virtualHosts) 1948 if (vhost.settings.hostName.icmp(servername) == 0) { 1949 logDebug("Found context for SNI host '%s'.", servername); 1950 return vhost.settings.tlsContext; 1951 } 1952 logDebug("No context found for SNI host '%s'.", servername); 1953 return null; 1954 } 1955 } 1956 1957 /**************************************************************************************************/ 1958 /* Private types */ 1959 /**************************************************************************************************/ 1960 1961 private enum MaxHTTPHeaderLineLength = 4096; 1962 1963 private final class LimitedHTTPInputStream : LimitedInputStream { 1964 @safe: 1965 1966 this(InterfaceProxy!InputStream stream, ulong byte_limit, bool silent_limit = false) { 1967 super(stream, byte_limit, silent_limit, true); 1968 } 1969 override void onSizeLimitReached() { 1970 throw new HTTPStatusException(HTTPStatus.requestEntityTooLarge); 1971 } 1972 } 1973 1974 private final class TimeoutHTTPInputStream : InputStream { 1975 @safe: 1976 1977 private { 1978 long m_timeref; 1979 long m_timeleft; 1980 InterfaceProxy!InputStream m_in; 1981 } 1982 1983 this(InterfaceProxy!InputStream stream, Duration timeleft, SysTime reftime) 1984 { 1985 enforce(timeleft > 0.seconds, "Timeout required"); 1986 m_in = stream; 1987 m_timeleft = timeleft.total!"hnsecs"(); 1988 m_timeref = reftime.stdTime(); 1989 } 1990 1991 @property bool empty() { enforce(m_in, "InputStream missing"); return m_in.empty(); } 1992 @property ulong leastSize() { enforce(m_in, "InputStream missing"); return m_in.leastSize(); } 1993 @property bool dataAvailableForRead() { enforce(m_in, "InputStream missing"); return m_in.dataAvailableForRead; } 1994 const(ubyte)[] peek() { return m_in.peek(); } 1995 1996 size_t read(scope ubyte[] dst, IOMode mode) 1997 { 1998 enforce(m_in, "InputStream missing"); 1999 size_t nread = 0; 2000 checkTimeout(); 2001 // FIXME: this should use ConnectionStream.waitForData to enforce the timeout during the 2002 // read operation 2003 return m_in.read(dst, mode); 2004 } 2005 2006 alias read = InputStream.read; 2007 2008 private void checkTimeout() 2009 @safe { 2010 auto curr = Clock.currStdTime(); 2011 auto diff = curr - m_timeref; 2012 if (diff > m_timeleft) throw new HTTPStatusException(HTTPStatus.requestTimeout); 2013 m_timeleft -= diff; 2014 m_timeref = curr; 2015 } 2016 } 2017 2018 /**************************************************************************************************/ 2019 /* Private functions */ 2020 /**************************************************************************************************/ 2021 2022 private { 2023 import core.sync.mutex; 2024 2025 shared string s_distHost; 2026 shared ushort s_distPort = 11000; 2027 2028 HTTPServerContext[] s_listeners; 2029 } 2030 2031 /** 2032 [private] Starts a HTTP server listening on the specified port. 2033 2034 This is the same as listenHTTP() except that it does not use a VibeDist host for 2035 remote listening, even if specified on the command line. 2036 */ 2037 private HTTPListener listenHTTPPlain(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) 2038 @safe { 2039 import vibe.core.core : runWorkerTaskDist; 2040 import std.algorithm : canFind, find; 2041 2042 static TCPListener doListen(HTTPServerContext listen_info, bool reusePort, bool reuseAddress, bool is_tls) 2043 @safe { 2044 try { 2045 TCPListenOptions options = TCPListenOptions.defaults; 2046 if(reuseAddress) options |= TCPListenOptions.reuseAddress; else options &= ~TCPListenOptions.reuseAddress; 2047 if(reusePort) options |= TCPListenOptions.reusePort; else options &= ~TCPListenOptions.reusePort; 2048 auto ret = listenTCP(listen_info.bindPort, (TCPConnection conn) nothrow @safe { 2049 try handleHTTPConnection(conn, listen_info); 2050 catch (Exception e) { 2051 logError("HTTP connection handler has thrown: %s", e.msg); 2052 debug logDebug("Full error: %s", () @trusted { return e.toString().sanitize(); } ()); 2053 try conn.close(); 2054 catch (Exception e) logError("Failed to close connection: %s", e.msg); 2055 } 2056 }, listen_info.bindAddress, options); 2057 2058 // support port 0 meaning any available port 2059 if (listen_info.bindPort == 0) 2060 listen_info.m_bindPort = ret.bindAddress.port; 2061 2062 auto proto = is_tls ? "https" : "http"; 2063 auto urladdr = listen_info.bindAddress; 2064 if (urladdr.canFind(':')) urladdr = "["~urladdr~"]"; 2065 logInfo("Listening for requests on %s://%s:%s/", proto, urladdr, listen_info.bindPort); 2066 return ret; 2067 } catch( Exception e ) { 2068 logWarn("Failed to listen on %s:%s", listen_info.bindAddress, listen_info.bindPort); 2069 return TCPListener.init; 2070 } 2071 } 2072 2073 size_t[] vid; 2074 2075 // Check for every bind address/port, if a new listening socket needs to be created and 2076 // check for conflicting servers 2077 foreach (addr; settings.bindAddresses) { 2078 HTTPServerContext linfo; 2079 2080 auto l = s_listeners.find!(l => l.bindAddress == addr && l.bindPort == settings.port); 2081 if (!l.empty) linfo = l.front; 2082 else { 2083 auto li = new HTTPServerContext(addr, settings.port); 2084 if (auto tcp_lst = doListen(li, 2085 (settings.options & HTTPServerOption.reusePort) != 0, 2086 (settings.options & HTTPServerOption.reuseAddress) != 0, 2087 settings.tlsContext !is null)) // DMD BUG 2043 2088 { 2089 li.m_listener = tcp_lst; 2090 s_listeners ~= li; 2091 linfo = li; 2092 } 2093 } 2094 2095 if (linfo) vid ~= linfo.addVirtualHost(settings, request_handler); 2096 } 2097 2098 enforce(vid.length > 0, "Failed to listen for incoming HTTP connections on any of the supplied interfaces."); 2099 2100 return HTTPListener(vid); 2101 } 2102 2103 private alias TLSStreamType = ReturnType!(createTLSStreamFL!(InterfaceProxy!Stream)); 2104 2105 2106 private bool handleRequest(InterfaceProxy!Stream http_stream, TCPConnection tcp_connection, HTTPServerContext listen_info, ref HTTPServerSettings settings, ref bool keep_alive, scope IAllocator request_allocator) 2107 @safe { 2108 import std.algorithm.searching : canFind; 2109 2110 SysTime reqtime = Clock.currTime(UTC()); 2111 2112 // some instances that live only while the request is running 2113 FreeListRef!HTTPServerRequest req = FreeListRef!HTTPServerRequest(reqtime, listen_info.bindPort); 2114 FreeListRef!TimeoutHTTPInputStream timeout_http_input_stream; 2115 FreeListRef!LimitedHTTPInputStream limited_http_input_stream; 2116 FreeListRef!ChunkedInputStream chunked_input_stream; 2117 2118 // store the IP address 2119 req.clientAddress = tcp_connection.remoteAddress; 2120 2121 if (!listen_info.hasVirtualHosts) { 2122 logWarn("Didn't find a HTTP listening context for incoming connection. Dropping."); 2123 keep_alive = false; 2124 return false; 2125 } 2126 2127 // Default to the first virtual host for this listener 2128 HTTPServerContext.VirtualHost context = listen_info.m_virtualHosts[0]; 2129 HTTPServerRequestDelegate request_task = context.requestHandler; 2130 settings = context.settings; 2131 2132 // temporarily set to the default settings, the virtual host specific settings will be set further down 2133 req.m_settings = settings; 2134 2135 // Create the response object 2136 InterfaceProxy!ConnectionStream cproxy = tcp_connection; 2137 auto res = FreeListRef!HTTPServerResponse(http_stream, cproxy, settings, request_allocator/*.Scoped_payload*/); 2138 req.tls = res.m_tls = listen_info.tlsContext !is null; 2139 if (req.tls) { 2140 version (HaveNoTLS) assert(false); 2141 else { 2142 static if (is(InterfaceProxy!ConnectionStream == ConnectionStream)) 2143 req.clientCertificate = (cast(TLSStream)http_stream).peerCertificate; 2144 else 2145 req.clientCertificate = http_stream.extract!TLSStreamType.peerCertificate; 2146 } 2147 } 2148 2149 // Error page handler 2150 void errorOut(int code, string msg, string debug_msg, Throwable ex) 2151 @safe { 2152 assert(!res.headerWritten); 2153 2154 res.statusCode = code; 2155 if (settings && settings.errorPageHandler) { 2156 /*scope*/ auto err = new HTTPServerErrorInfo; 2157 err.code = code; 2158 err.message = msg; 2159 err.debugMessage = debug_msg; 2160 err.exception = ex; 2161 settings.errorPageHandler_(req, res, err); 2162 } else { 2163 if (debug_msg.length) 2164 res.writeBody(format("%s - %s\n\n%s\n\nInternal error information:\n%s", code, httpStatusText(code), msg, debug_msg)); 2165 else res.writeBody(format("%s - %s\n\n%s", code, httpStatusText(code), msg)); 2166 } 2167 assert(res.headerWritten); 2168 } 2169 2170 bool parsed = false; 2171 /*bool*/ keep_alive = false; 2172 2173 // parse the request 2174 try { 2175 logTrace("reading request.."); 2176 2177 // limit the total request time 2178 InterfaceProxy!InputStream reqReader = http_stream; 2179 if (settings.maxRequestTime > dur!"seconds"(0) && settings.maxRequestTime != Duration.max) { 2180 timeout_http_input_stream = FreeListRef!TimeoutHTTPInputStream(reqReader, settings.maxRequestTime, reqtime); 2181 reqReader = timeout_http_input_stream; 2182 } 2183 2184 // basic request parsing 2185 parseRequestHeader(req, reqReader, request_allocator, settings.maxRequestHeaderSize); 2186 logTrace("Got request header."); 2187 2188 // find the matching virtual host 2189 string reqhost; 2190 ushort reqport = 0; 2191 { 2192 string s = req.host; 2193 enforceHTTP(s.length > 0 || req.httpVersion <= HTTPVersion.HTTP_1_0, HTTPStatus.badRequest, "Missing Host header."); 2194 if (s.startsWith('[')) { // IPv6 address 2195 auto idx = s.indexOf(']'); 2196 enforce(idx > 0, "Missing closing ']' for IPv6 address."); 2197 reqhost = s[1 .. idx]; 2198 s = s[idx+1 .. $]; 2199 } else if (s.length) { // host name or IPv4 address 2200 auto idx = s.indexOf(':'); 2201 if (idx < 0) idx = s.length; 2202 enforceHTTP(idx > 0, HTTPStatus.badRequest, "Missing Host header."); 2203 reqhost = s[0 .. idx]; 2204 s = s[idx .. $]; 2205 } 2206 if (s.startsWith(':')) reqport = s[1 .. $].to!ushort; 2207 } 2208 2209 foreach (ctx; listen_info.m_virtualHosts) 2210 if (icmp2(ctx.settings.hostName, reqhost) == 0 && 2211 (!reqport || reqport == ctx.settings.port)) 2212 { 2213 context = ctx; 2214 settings = ctx.settings; 2215 request_task = ctx.requestHandler; 2216 break; 2217 } 2218 req.m_settings = settings; 2219 res.m_settings = settings; 2220 2221 // setup compressed output 2222 if (settings.useCompressionIfPossible) { 2223 if (auto pae = "Accept-Encoding" in req.headers) { 2224 if (canFind(*pae, "gzip")) { 2225 res.headers["Content-Encoding"] = "gzip"; 2226 } else if (canFind(*pae, "deflate")) { 2227 res.headers["Content-Encoding"] = "deflate"; 2228 } 2229 } 2230 } 2231 2232 // limit request size 2233 if (auto pcl = "Content-Length" in req.headers) { 2234 string v = *pcl; 2235 auto contentLength = parse!ulong(v); // DMDBUG: to! thinks there is a H in the string 2236 enforceBadRequest(v.length == 0, "Invalid content-length"); 2237 enforceBadRequest(settings.maxRequestSize <= 0 || contentLength <= settings.maxRequestSize, "Request size too big"); 2238 limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, contentLength); 2239 } else if (auto pt = "Transfer-Encoding" in req.headers) { 2240 enforceBadRequest(icmp(*pt, "chunked") == 0); 2241 chunked_input_stream = createChunkedInputStreamFL(reqReader); 2242 InterfaceProxy!InputStream ciproxy = chunked_input_stream; 2243 limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(ciproxy, settings.maxRequestSize, true); 2244 } else { 2245 limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, 0); 2246 } 2247 req.bodyReader = limited_http_input_stream; 2248 2249 // handle Expect header 2250 if (auto pv = "Expect" in req.headers) { 2251 if (icmp2(*pv, "100-continue") == 0) { 2252 logTrace("sending 100 continue"); 2253 http_stream.write("HTTP/1.1 100 Continue\r\n\r\n"); 2254 } 2255 } 2256 2257 // eagerly parse the URL as its lightweight and defacto @nogc 2258 auto url = URL.parse(req.requestURI); 2259 req.queryString = url.queryString; 2260 req.username = url.username; 2261 req.password = url.password; 2262 req.requestPath = url.path; 2263 2264 // lookup the session 2265 if (settings.sessionStore) { 2266 // use the first cookie that contains a valid session ID in case 2267 // of multiple matching session cookies 2268 foreach (val; req.cookies.getAll(settings.sessionIdCookie)) { 2269 req.session = settings.sessionStore.open(val); 2270 res.m_session = req.session; 2271 if (req.session) break; 2272 } 2273 } 2274 2275 // write default headers 2276 if (req.method == HTTPMethod.HEAD) res.m_isHeadResponse = true; 2277 if (settings.serverString.length) 2278 res.headers["Server"] = settings.serverString; 2279 res.headers["Date"] = formatRFC822DateAlloc(reqtime); 2280 if (req.persistent) 2281 res.headers["Keep-Alive"] = formatAlloc( 2282 request_allocator, "timeout=%d", settings.keepAliveTimeout.total!"seconds"()); 2283 2284 // finished parsing the request 2285 parsed = true; 2286 logTrace("persist: %s", req.persistent); 2287 keep_alive = req.persistent; 2288 2289 if (context.settings.rejectConnectionPredicate !is null) 2290 { 2291 import std.socket : Address, parseAddress; 2292 2293 auto forward = req.headers.get("X-Forwarded-For", null); 2294 if (forward !is null) 2295 { 2296 try { 2297 auto ix = forward.indexOf(','); 2298 if (ix != -1) 2299 forward = forward[0 .. ix]; 2300 if (context.settings.rejectConnectionPredicate(NetworkAddress(parseAddress(forward)))) 2301 errorOut(HTTPStatus.forbidden, 2302 httpStatusText(HTTPStatus.forbidden), null, null); 2303 } catch (Exception e) 2304 logTrace("Malformed X-Forwarded-For header: %s", e.msg); 2305 } 2306 } 2307 2308 // handle the request 2309 logTrace("handle request (body %d)", req.bodyReader.leastSize); 2310 res.httpVersion = req.httpVersion; 2311 request_task(req, res); 2312 2313 // if no one has written anything, return 404 2314 if (!res.headerWritten) { 2315 string dbg_msg; 2316 logDiagnostic("No response written for %s", req.requestURI); 2317 if (settings.options & HTTPServerOption.errorStackTraces) 2318 dbg_msg = format("No routes match path '%s'", req.requestURI); 2319 errorOut(HTTPStatus.notFound, httpStatusText(HTTPStatus.notFound), dbg_msg, null); 2320 } 2321 } catch (HTTPStatusException err) { 2322 if (!res.headerWritten) errorOut(err.status, err.msg, err.debugMessage, err); 2323 else logDiagnostic("HTTPStatusException while writing the response: %s", err.msg); 2324 debug logDebug("Exception while handling request %s %s: %s", req.method, 2325 req.requestURI, () @trusted { return err.toString().sanitize; } ()); 2326 if (!parsed || res.headerWritten || justifiesConnectionClose(err.status)) 2327 keep_alive = false; 2328 } catch (UncaughtException e) { 2329 auto status = parsed ? HTTPStatus.internalServerError : HTTPStatus.badRequest; 2330 string dbg_msg; 2331 if (settings.options & HTTPServerOption.errorStackTraces) 2332 dbg_msg = () @trusted { return e.toString().sanitize; } (); 2333 if (!res.headerWritten && tcp_connection.connected) 2334 errorOut(status, httpStatusText(status), dbg_msg, e); 2335 else logDiagnostic("Error while writing the response: %s", e.msg); 2336 debug logDebug("Exception while handling request %s %s: %s", req.method, 2337 req.requestURI, () @trusted { return e.toString().sanitize(); } ()); 2338 if (!parsed || res.headerWritten || !cast(Exception)e) keep_alive = false; 2339 } 2340 2341 if (tcp_connection.connected && keep_alive) { 2342 if (req.bodyReader && !req.bodyReader.empty) { 2343 req.bodyReader.pipe(nullSink); 2344 logTrace("dropped body"); 2345 } 2346 } 2347 2348 // finalize (e.g. for chunked encoding) 2349 res.finalize(); 2350 2351 if (res.m_requiresConnectionClose) 2352 keep_alive = false; 2353 2354 // NOTE: req.m_files may or may not be parsed/filled with actual data, as 2355 // it is lazily initialized when calling the .files or .form 2356 // properties 2357 foreach (k, v ; req.m_files.byKeyValue) { 2358 if (existsFile(v.tempPath)) { 2359 removeFile(v.tempPath); 2360 logDebug("Deleted upload tempfile %s", v.tempPath.toString()); 2361 } 2362 } 2363 2364 if (!req.noLog) { 2365 // log the request to access log 2366 foreach (log; context.loggers) 2367 log.log(req, res); 2368 } 2369 2370 //logTrace("return %s (used pool memory: %s/%s)", keep_alive, request_allocator.allocatedSize, request_allocator.totalSize); 2371 logTrace("return %s", keep_alive); 2372 return keep_alive != false; 2373 } 2374 2375 2376 private void parseRequestHeader(InputStream)(HTTPServerRequest req, InputStream http_stream, IAllocator alloc, ulong max_header_size) 2377 if (isInputStream!InputStream) 2378 { 2379 auto stream = FreeListRef!LimitedHTTPInputStream(http_stream, max_header_size); 2380 2381 logTrace("HTTP server reading status line"); 2382 auto reqln = () @trusted { return cast(string)stream.readLine(MaxHTTPHeaderLineLength, "\r\n", alloc); }(); 2383 2384 logTrace("--------------------"); 2385 logTrace("HTTP server request:"); 2386 logTrace("--------------------"); 2387 logTrace("%s", reqln); 2388 2389 //Method 2390 auto pos = reqln.indexOf(' '); 2391 enforceBadRequest(pos >= 0, "invalid request method"); 2392 2393 req.method = httpMethodFromString(reqln[0 .. pos]); 2394 reqln = reqln[pos+1 .. $]; 2395 //Path 2396 pos = reqln.indexOf(' '); 2397 enforceBadRequest(pos >= 0, "invalid request path"); 2398 2399 req.requestURI = reqln[0 .. pos]; 2400 reqln = reqln[pos+1 .. $]; 2401 2402 req.httpVersion = parseHTTPVersion(reqln); 2403 2404 //headers 2405 parseRFC5322Header(stream, req.headers, MaxHTTPHeaderLineLength, alloc, false); 2406 2407 foreach (k, v; req.headers.byKeyValue) 2408 logTrace("%s: %s", k, v); 2409 logTrace("--------------------"); 2410 } 2411 2412 private void parseCookies(string str, ref CookieValueMap cookies) 2413 @safe { 2414 import std.encoding : sanitize; 2415 import std.array : split; 2416 import std.string : strip; 2417 import std.algorithm.iteration : map, filter, each; 2418 import vibe.http.common : Cookie; 2419 () @trusted { return str.sanitize; } () 2420 .split(";") 2421 .map!(kv => kv.strip.split("=")) 2422 .filter!(kv => kv.length == 2) //ignore illegal cookies 2423 .each!(kv => cookies.add(kv[0], kv[1], Cookie.Encoding.raw) ); 2424 } 2425 2426 unittest 2427 { 2428 auto cvm = CookieValueMap(); 2429 parseCookies("foo=bar;; baz=zinga; öö=üü ; møøse=was=sacked; onlyval1; =onlyval2; onlykey=", cvm); 2430 assert(cvm["foo"] == "bar"); 2431 assert(cvm["baz"] == "zinga"); 2432 assert(cvm["öö"] == "üü"); 2433 assert( "møøse" ! in cvm); //illegal cookie gets ignored 2434 assert( "onlyval1" ! in cvm); //illegal cookie gets ignored 2435 assert(cvm["onlykey"] == ""); 2436 assert(cvm[""] == "onlyval2"); 2437 assert(cvm.length() == 5); 2438 cvm = CookieValueMap(); 2439 parseCookies("", cvm); 2440 assert(cvm.length() == 0); 2441 cvm = CookieValueMap(); 2442 parseCookies(";;=", cvm); 2443 assert(cvm.length() == 1); 2444 assert(cvm[""] == ""); 2445 } 2446 2447 shared static this() 2448 { 2449 version (VibeNoDefaultArgs) {} 2450 else { 2451 string disthost = s_distHost; 2452 ushort distport = s_distPort; 2453 import vibe.core.args : readOption; 2454 readOption("disthost|d", () @trusted { return &disthost; } (), "Sets the name of a vibedist server to use for load balancing."); 2455 readOption("distport", () @trusted { return &distport; } (), "Sets the port used for load balancing."); 2456 setVibeDistHost(disthost, distport); 2457 } 2458 } 2459 2460 private struct CacheTime 2461 { 2462 string cachedDate; 2463 SysTime nextUpdate; 2464 2465 this(SysTime nextUpdate) @safe @nogc pure nothrow 2466 { 2467 this.nextUpdate = nextUpdate; 2468 } 2469 2470 void update(SysTime time) @safe 2471 { 2472 this.nextUpdate = time + 1.seconds; 2473 this.nextUpdate.fracSecs = nsecs(0); 2474 } 2475 } 2476 2477 private string formatRFC822DateAlloc(SysTime time) 2478 @safe { 2479 static LAST = CacheTime(SysTime.min()); 2480 2481 if (time > LAST.nextUpdate) { 2482 auto app = new FixedAppender!(string, 29); 2483 writeRFC822DateTimeString(app, time); 2484 LAST.update(time); 2485 LAST.cachedDate = () @trusted { return app.data; } (); 2486 return () @trusted { return app.data; } (); 2487 } else 2488 return LAST.cachedDate; 2489 } 2490 2491 version (VibeDebugCatchAll) private alias UncaughtException = Throwable; 2492 else private alias UncaughtException = Exception;