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