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.container.internal.appender : FixedAppender; 15 import vibe.container.internal.utilallocator; 16 import vibe.core.file; 17 import vibe.core.log; 18 import vibe.data.json; 19 import vibe.http.dist; 20 import vibe.http.log; 21 import vibe.inet.message; 22 import vibe.inet.url; 23 import vibe.inet.webform; 24 import vibe.internal.interfaceproxy : InterfaceProxy; 25 import vibe.stream.counting; 26 import vibe.stream.operations; 27 import vibe.stream.tls; 28 import vibe.stream.wrapper : ConnectionProxyStream, createConnectionProxyStream, createConnectionProxyStreamFL; 29 import vibe.stream.zlib; 30 import vibe.textfilter.urlencode; 31 import vibe.internal.freelistref; 32 import vibe.internal.string : formatAlloc, icmp2; 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 scope request_allocator = createRequestAllocator(); 247 scope (exit) freeRequestAllocator(request_allocator); 248 249 handleRequest(http_stream, connection, context, settings, keep_alive, request_allocator); 250 } (); 251 if (!keep_alive) { logTrace("No keep-alive - disconnecting client."); break; } 252 253 logTrace("Waiting for next request..."); 254 // wait for another possible request on a keep-alive connection 255 if (!connection.waitForData(settings.keepAliveTimeout)) { 256 if (!connection.connected) logTrace("Client disconnected."); 257 else logDebug("Keep-alive connection timed out!"); 258 break; 259 } 260 } 261 262 logTrace("Done handling connection."); 263 } 264 265 266 /** 267 Provides a HTTP request handler that responds with a static Diet template. 268 */ 269 @property HTTPServerRequestDelegateS staticTemplate(string template_file)() 270 { 271 return (scope HTTPServerRequest req, scope HTTPServerResponse res){ 272 res.render!(template_file, req); 273 }; 274 } 275 276 /** 277 Provides a HTTP request handler that responds with a static redirection to the specified URL. 278 279 Params: 280 url = The URL to redirect to 281 status = Redirection status to use $(LPAREN)by default this is $(D HTTPStatus.found)$(RPAREN). 282 283 Returns: 284 Returns a $(D HTTPServerRequestDelegate) that performs the redirect 285 */ 286 HTTPServerRequestDelegate staticRedirect(string url, HTTPStatus status = HTTPStatus.found) 287 @safe { 288 return (HTTPServerRequest req, HTTPServerResponse res){ 289 res.redirect(url, status); 290 }; 291 } 292 /// ditto 293 HTTPServerRequestDelegate staticRedirect(URL url, HTTPStatus status = HTTPStatus.found) 294 @safe { 295 return (HTTPServerRequest req, HTTPServerResponse res){ 296 res.redirect(url, status); 297 }; 298 } 299 300 /// 301 unittest { 302 import vibe.http.router; 303 304 void test() 305 { 306 auto router = new URLRouter; 307 router.get("/old_url", staticRedirect("http://example.org/new_url", HTTPStatus.movedPermanently)); 308 309 listenHTTP(new HTTPServerSettings, router); 310 } 311 } 312 313 314 /** 315 Sets a VibeDist host to register with. 316 */ 317 void setVibeDistHost(string host, ushort port) 318 @safe { 319 s_distHost = host; 320 s_distPort = port; 321 } 322 323 324 /** 325 Renders the given Diet template and makes all ALIASES available to the template. 326 327 You can call this function as a pseudo-member of `HTTPServerResponse` using 328 D's uniform function call syntax. 329 330 See_also: `diet.html.compileHTMLDietFile` 331 332 Examples: 333 --- 334 string title = "Hello, World!"; 335 int pageNumber = 1; 336 res.render!("mytemplate.dt", title, pageNumber); 337 --- 338 */ 339 @property void render(string template_file, ALIASES...)(HTTPServerResponse res) 340 { 341 res.contentType = "text/html; charset=UTF-8"; 342 version (VibeUseOldDiet) 343 pragma(msg, "VibeUseOldDiet is not supported anymore. Please undefine in the package recipe."); 344 import vibe.stream.wrapper : streamOutputRange; 345 import diet.html : compileHTMLDietFile; 346 auto output = streamOutputRange!1024(res.bodyWriter); 347 compileHTMLDietFile!(template_file, ALIASES, DefaultDietFilters)(output); 348 } 349 350 351 /** 352 Provides the default `css`, `javascript`, `markdown` and `htmlescape` filters 353 */ 354 @dietTraits 355 struct DefaultDietFilters { 356 import diet.html : HTMLOutputStyle; 357 import diet.traits : SafeFilterCallback; 358 import std.string : splitLines; 359 360 version (VibeOutputCompactHTML) enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.compact; 361 else enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; 362 363 static string filterCss(I)(I text, size_t indent = 0) 364 { 365 auto lines = splitLines(text); 366 367 string indent_string = "\n"; 368 while (indent-- > 0) indent_string ~= '\t'; 369 370 string ret = indent_string~"<style><!--"; 371 indent_string = indent_string ~ '\t'; 372 foreach (ln; lines) ret ~= indent_string ~ ln; 373 indent_string = indent_string[0 .. $-1]; 374 ret ~= indent_string ~ "--></style>"; 375 376 return ret; 377 } 378 379 380 static string filterJavascript(I)(I text, size_t indent = 0) 381 { 382 auto lines = splitLines(text); 383 384 string indent_string = "\n"; 385 while (indent-- > 0) indent_string ~= '\t'; 386 387 string ret = indent_string~"<script>"; 388 ret ~= indent_string~'\t' ~ "//<![CDATA["; 389 foreach (ln; lines) ret ~= indent_string ~ '\t' ~ ln; 390 ret ~= indent_string ~ '\t' ~ "//]]>" ~ indent_string ~ "</script>"; 391 392 return ret; 393 } 394 395 static string filterMarkdown(I)(I text) 396 { 397 import vibe.textfilter.markdown : markdown = filterMarkdown; 398 // TODO: indent 399 return markdown(text); 400 } 401 402 static string filterHtmlescape(I)(I text) 403 { 404 import vibe.textfilter.html : htmlEscape; 405 // TODO: indent 406 return htmlEscape(text); 407 } 408 409 static this() 410 { 411 filters["css"] = (in input, scope output) { output(filterCss(input)); }; 412 filters["javascript"] = (in input, scope output) { output(filterJavascript(input)); }; 413 filters["markdown"] = (in input, scope output) { output(filterMarkdown(() @trusted { return cast(string)input; } ())); }; 414 filters["htmlescape"] = (in input, scope output) { output(filterHtmlescape(input)); }; 415 } 416 417 static SafeFilterCallback[string] filters; 418 } 419 420 421 unittest { 422 static string compile(string diet)() { 423 import std.array : appender; 424 import std.string : strip; 425 import diet.html : compileHTMLDietString; 426 auto dst = appender!string; 427 dst.compileHTMLDietString!(diet, DefaultDietFilters); 428 return strip(cast(string)(dst.data)); 429 } 430 431 assert(compile!":css .test" == "<style><!--\n\t.test\n--></style>"); 432 assert(compile!":javascript test();" == "<script>\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); 433 assert(compile!":markdown **test**" == "<p><strong>test</strong>\n</p>"); 434 assert(compile!":htmlescape <test>" == "<test>"); 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!":javascript\n\ttest();" == "<script>\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>"); 440 } 441 442 443 /** 444 Creates a HTTPServerRequest suitable for writing unit tests. 445 */ 446 HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method = HTTPMethod.GET, InputStream data = null) 447 @safe { 448 InetHeaderMap headers; 449 return createTestHTTPServerRequest(url, method, headers, data); 450 } 451 /// ditto 452 HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method, InetHeaderMap headers, InputStream data = null) 453 @safe { 454 auto tls = url.schema == "https"; 455 auto ret = new HTTPServerRequest(Clock.currTime(UTC()), url.port ? url.port : tls ? 443 : 80); 456 ret.m_settings = new HTTPServerSettings; 457 ret.requestPath = url.path; 458 ret.queryString = url.queryString; 459 ret.username = url.username; 460 ret.password = url.password; 461 ret.requestURI = url.localURI; 462 ret.method = method; 463 ret.tls = tls; 464 ret.headers = headers; 465 ret.bodyReader = data; 466 return ret; 467 } 468 469 /** 470 Creates a HTTPServerResponse suitable for writing unit tests. 471 472 Params: 473 data_sink = Optional output stream that captures the data that gets 474 written to the response 475 session_store = Optional session store to use when sessions are involved 476 data_mode = If set to `TestHTTPResponseMode.bodyOnly`, only the body 477 contents get written to `data_sink`. Otherwise the raw response 478 including the HTTP header is written. 479 */ 480 HTTPServerResponse createTestHTTPServerResponse(OutputStream data_sink = null, 481 SessionStore session_store = null, 482 TestHTTPResponseMode data_mode = TestHTTPResponseMode.plain) 483 @safe { 484 import vibe.stream.wrapper; 485 486 HTTPServerSettings settings; 487 if (session_store) { 488 settings = new HTTPServerSettings; 489 settings.sessionStore = session_store; 490 } 491 492 InterfaceProxy!Stream outstr; 493 if (data_sink && data_mode == TestHTTPResponseMode.plain) 494 outstr = createProxyStream(Stream.init, data_sink); 495 else outstr = createProxyStream(Stream.init, nullSink); 496 497 auto ret = new HTTPServerResponse(outstr, InterfaceProxy!ConnectionStream.init, 498 settings, () @trusted { return vibeThreadAllocator(); } ()); 499 if (data_sink && data_mode == TestHTTPResponseMode.bodyOnly) ret.m_bodyWriter = data_sink; 500 return ret; 501 } 502 503 504 /**************************************************************************************************/ 505 /* Public types */ 506 /**************************************************************************************************/ 507 508 /// Delegate based request handler 509 alias HTTPServerRequestDelegate = void delegate(HTTPServerRequest req, HTTPServerResponse res) @safe; 510 /// Static function based request handler 511 alias HTTPServerRequestFunction = void function(HTTPServerRequest req, HTTPServerResponse res) @safe; 512 /// Interface for class based request handlers 513 interface HTTPServerRequestHandler { 514 /// Handles incoming HTTP requests 515 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe ; 516 } 517 518 /// Delegate based request handler with scoped parameters 519 alias HTTPServerRequestDelegateS = void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; 520 /// Static function based request handler with scoped parameters 521 alias HTTPServerRequestFunctionS = void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; 522 /// Interface for class based request handlers with scoped parameters 523 interface HTTPServerRequestHandlerS { 524 /// Handles incoming HTTP requests 525 void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe; 526 } 527 528 unittest { 529 static assert(is(HTTPServerRequestDelegateS : HTTPServerRequestDelegate)); 530 static assert(is(HTTPServerRequestFunctionS : HTTPServerRequestFunction)); 531 } 532 533 /// Aggregates all information about an HTTP error status. 534 final class HTTPServerErrorInfo { 535 /// The HTTP status code 536 int code; 537 /// The error message 538 string message; 539 /// Extended error message with debug information such as a stack trace 540 string debugMessage; 541 /// The error exception, if any 542 Throwable exception; 543 } 544 545 /// Delegate type used for user defined error page generator callbacks. 546 alias HTTPServerErrorPageHandler = void delegate(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) @safe; 547 548 549 enum TestHTTPResponseMode { 550 plain, 551 bodyOnly 552 } 553 554 555 /** 556 Specifies optional features of the HTTP server. 557 558 Disabling unneeded features can speed up the server or reduce its memory usage. 559 560 Note that the options `parseFormBody`, `parseJsonBody` and `parseMultiPartBody` 561 will also drain the `HTTPServerRequest.bodyReader` stream whenever a request 562 body with form or JSON data is encountered. 563 */ 564 enum HTTPServerOption { 565 none = 0, 566 /** Enables stack traces (`HTTPServerErrorInfo.debugMessage`). 567 568 Note that generating the stack traces are generally a costly 569 operation that should usually be avoided in production 570 environments. It can also reveal internal information about 571 the application, such as function addresses, which can 572 help an attacker to abuse possible security holes. 573 */ 574 errorStackTraces = 1<<7, 575 /// Enable port reuse in `listenTCP()` 576 reusePort = 1<<8, 577 /// Enable address reuse in `listenTCP()` 578 reuseAddress = 1<<10, 579 /** The default set of options. 580 581 Includes all parsing options, as well as the `errorStackTraces` 582 option if the code is compiled in debug mode. 583 */ 584 defaults = () { auto ret = reuseAddress; debug ret |= errorStackTraces; return ret; } (), 585 } 586 587 588 /** 589 Contains all settings for configuring a basic HTTP server. 590 591 The defaults are sufficient for most normal uses. 592 */ 593 final class HTTPServerSettings { 594 /** The port on which the HTTP server is listening. 595 596 The default value is 80. If you are running a TLS enabled server you may want to set this 597 to 443 instead. 598 599 Using a value of `0` instructs the server to use any available port on 600 the given `bindAddresses` the actual addresses and ports can then be 601 queried with `TCPListener.bindAddresses`. 602 */ 603 ushort port = 80; 604 605 /** The interfaces on which the HTTP server is listening. 606 607 By default, the server will listen on all IPv4 and IPv6 interfaces. 608 */ 609 string[] bindAddresses = ["::", "0.0.0.0"]; 610 611 /** Determines the server host name. 612 613 If multiple servers are listening on the same port, the host name will determine which one 614 gets a request. 615 */ 616 string hostName; 617 618 /** Provides a way to reject incoming connections as early as possible. 619 620 Allows to ban and unban network addresses and reduce the impact of DOS 621 attacks. 622 623 If the callback returns `true` for a specific `NetworkAddress`, 624 then all incoming requests from that address will be rejected. 625 */ 626 RejectConnectionPredicate rejectConnectionPredicate; 627 628 /** Configures optional features of the HTTP server 629 630 Disabling unneeded features can improve performance or reduce the server 631 load in case of invalid or unwanted requests (DoS). By default, 632 HTTPServerOption.defaults is used. 633 */ 634 HTTPServerOption options = HTTPServerOption.defaults; 635 636 /** Time of a request after which the connection is closed with an error; not supported yet 637 638 The default limit of 0 means that the request time is not limited. 639 */ 640 Duration maxRequestTime = 0.seconds; 641 642 /** Maximum time between two request on a keep-alive connection 643 644 The default value is 10 seconds. 645 */ 646 Duration keepAliveTimeout = 10.seconds; 647 648 /// Maximum number of transferred bytes per request after which the connection is closed with 649 /// an error 650 ulong maxRequestSize = 2097152; 651 652 653 /// Maximum number of transferred bytes for the request header. This includes the request line 654 /// the url and all headers. 655 ulong maxRequestHeaderSize = 8192; 656 657 /// Maximum number of bytes in a single line in the request header. 658 size_t maxRequestHeaderLineSize = 4096; 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.container.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, m_settings.maxRequestHeaderLineSize); 1174 } 1175 } 1176 1177 1178 /** 1179 Represents a HTTP response as sent from the server side. 1180 */ 1181 final class HTTPServerResponse : HTTPResponse { 1182 alias Allocator = typeof(vibeThreadAllocator()); 1183 1184 private { 1185 InterfaceProxy!Stream m_conn; 1186 InterfaceProxy!ConnectionStream m_rawConnection; 1187 InterfaceProxy!OutputStream m_bodyWriter; 1188 Allocator m_requestAlloc; 1189 FreeListRef!ChunkedOutputStream m_chunkedBodyWriter; 1190 FreeListRef!CountingOutputStream m_countingWriter; 1191 FreeListRef!ZlibOutputStream m_zlibOutputStream; 1192 HTTPServerSettings m_settings; 1193 Session m_session; 1194 bool m_headerWritten = false; 1195 bool m_isHeadResponse = false; 1196 bool m_tls; 1197 bool m_requiresConnectionClose; 1198 SysTime m_timeFinalized; 1199 } 1200 1201 static if (!is(Stream == InterfaceProxy!Stream)) { 1202 this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, Allocator req_alloc) 1203 @safe scope { 1204 this(InterfaceProxy!Stream(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc); 1205 } 1206 } 1207 1208 this(InterfaceProxy!Stream conn, InterfaceProxy!ConnectionStream raw_connection, HTTPServerSettings settings, Allocator req_alloc) 1209 @safe scope { 1210 m_conn = conn; 1211 m_rawConnection = raw_connection; 1212 m_countingWriter = createCountingOutputStreamFL(conn); 1213 m_settings = settings; 1214 m_requestAlloc = req_alloc; 1215 } 1216 1217 /** Sends a redirect request to the client. 1218 1219 Params: 1220 url = The URL to redirect to 1221 status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found) 1222 */ 1223 void redirect(string url, int status = HTTPStatus.found) 1224 @safe scope { 1225 // Disallow any characters that may influence the header parsing 1226 enforce(!url.representation.canFind!(ch => ch < 0x20), 1227 "Control character in redirection URL."); 1228 1229 statusCode = status; 1230 headers["Location"] = url; 1231 writeBody("redirecting..."); 1232 } 1233 /// ditto 1234 void redirect(URL url, int status = HTTPStatus.found) 1235 @safe scope { 1236 redirect(url.toString(), status); 1237 } 1238 1239 /// 1240 @safe unittest { 1241 import vibe.http.router; 1242 1243 void request_handler(HTTPServerRequest req, HTTPServerResponse res) 1244 { 1245 res.redirect("http://example.org/some_other_url"); 1246 } 1247 1248 void test() 1249 { 1250 auto router = new URLRouter; 1251 router.get("/old_url", &request_handler); 1252 1253 listenHTTP(new HTTPServerSettings, router); 1254 } 1255 } 1256 1257 scope: 1258 1259 /** Returns the time at which the request was finalized. 1260 1261 Note that this field will only be set after `finalize` has been called. 1262 */ 1263 @property SysTime timeFinalized() const @safe { return m_timeFinalized; } 1264 1265 /** Determines if the HTTP header has already been written. 1266 */ 1267 @property bool headerWritten() const @safe { return m_headerWritten; } 1268 1269 /** Determines if the response does not need a body. 1270 */ 1271 bool isHeadResponse() const @safe { return m_isHeadResponse; } 1272 1273 /** Determines if the response is sent over an encrypted connection. 1274 */ 1275 bool tls() const @safe { return m_tls; } 1276 1277 /** Writes the entire response body at once. 1278 1279 Params: 1280 data = The data to write as the body contents 1281 status = Optional response status code to set 1282 content_type = Optional content type to apply to the response. 1283 If no content type is given and no "Content-Type" header is 1284 set in the response, this will default to 1285 `"application/octet-stream"`. 1286 1287 See_Also: `HTTPStatusCode` 1288 */ 1289 void writeBody(in ubyte[] data, string content_type = null) 1290 @safe { 1291 if (content_type.length) headers["Content-Type"] = content_type; 1292 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 1293 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", data.length); 1294 bodyWriter.write(data); 1295 } 1296 /// ditto 1297 void writeBody(in ubyte[] data, int status, string content_type = null) 1298 @safe { 1299 statusCode = status; 1300 writeBody(data, content_type); 1301 } 1302 /// ditto 1303 void writeBody(scope InputStream data, string content_type = null) 1304 @safe { 1305 if (content_type.length) headers["Content-Type"] = content_type; 1306 else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream"; 1307 data.pipe(bodyWriter); 1308 } 1309 1310 /** Writes the entire response body as a single string. 1311 1312 Params: 1313 data = The string to write as the body contents 1314 status = Optional response status code to set 1315 content_type = Optional content type to apply to the response. 1316 If no content type is given and no "Content-Type" header is 1317 set in the response, this will default to 1318 `"text/plain; charset=UTF-8"`. 1319 1320 See_Also: `HTTPStatusCode` 1321 */ 1322 /// ditto 1323 void writeBody(string data, string content_type = null) 1324 @safe { 1325 if (!content_type.length && "Content-Type" !in headers) 1326 content_type = "text/plain; charset=UTF-8"; 1327 writeBody(cast(const(ubyte)[])data, content_type); 1328 } 1329 /// ditto 1330 void writeBody(string data, int status, string content_type = null) 1331 @safe { 1332 statusCode = status; 1333 writeBody(data, content_type); 1334 } 1335 1336 /** Writes the whole response body at once, without doing any further encoding. 1337 1338 The caller has to make sure that the appropriate headers are set correctly 1339 (i.e. Content-Type and Content-Encoding). 1340 1341 Note that the version taking a RandomAccessStream may perform additional 1342 optimizations such as sending a file directly from the disk to the 1343 network card using a DMA transfer. 1344 1345 */ 1346 void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe 1347 if (isRandomAccessStream!RandomAccessStream) 1348 { 1349 assert(!m_headerWritten, "A body was already written!"); 1350 writeHeader(); 1351 if (m_isHeadResponse) return; 1352 1353 auto bytes = stream.size - stream.tell(); 1354 stream.pipe(m_conn); 1355 m_countingWriter.increment(bytes); 1356 } 1357 /// ditto 1358 void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = 0) @safe 1359 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1360 { 1361 assert(!m_headerWritten, "A body was already written!"); 1362 writeHeader(); 1363 if (m_isHeadResponse) return; 1364 1365 if (num_bytes > 0) { 1366 stream.pipe(m_conn, num_bytes); 1367 m_countingWriter.increment(num_bytes); 1368 } else stream.pipe(m_countingWriter, num_bytes); 1369 } 1370 /// ditto 1371 void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe 1372 if (isRandomAccessStream!RandomAccessStream) 1373 { 1374 statusCode = status; 1375 writeRawBody(stream); 1376 } 1377 /// ditto 1378 void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe 1379 if (isInputStream!InputStream && !isRandomAccessStream!InputStream) 1380 { 1381 statusCode = status; 1382 writeRawBody(stream, num_bytes); 1383 } 1384 1385 1386 /// Writes a JSON message with the specified status 1387 void writeJsonBody(T)(T data, int status, bool allow_chunked = false) 1388 { 1389 statusCode = status; 1390 writeJsonBody(data, allow_chunked); 1391 } 1392 /// ditto 1393 void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false) 1394 { 1395 statusCode = status; 1396 writeJsonBody(data, content_type, allow_chunked); 1397 } 1398 1399 /// ditto 1400 void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false) 1401 { 1402 headers["Content-Type"] = content_type; 1403 writeJsonBody(data, allow_chunked); 1404 } 1405 /// ditto 1406 void writeJsonBody(T)(T data, bool allow_chunked = false) 1407 { 1408 doWriteJsonBody!(T, false)(data, allow_chunked); 1409 } 1410 /// ditto 1411 void writePrettyJsonBody(T)(T data, bool allow_chunked = false) 1412 { 1413 doWriteJsonBody!(T, true)(data, allow_chunked); 1414 } 1415 1416 private void doWriteJsonBody(T, bool PRETTY)(T data, bool allow_chunked = false) 1417 { 1418 import std.traits; 1419 import vibe.stream.wrapper; 1420 1421 static if (!is(T == Json) && is(typeof(data.data())) && isArray!(typeof(data.data()))) { 1422 static assert(!is(T == Appender!(typeof(data.data()))), "Passed an Appender!T to writeJsonBody - this is most probably not doing what's indended."); 1423 } 1424 1425 if ("Content-Type" !in headers) 1426 headers["Content-Type"] = "application/json; charset=UTF-8"; 1427 1428 1429 // set an explicit content-length field if chunked encoding is not allowed 1430 if (!allow_chunked) { 1431 import vibe.internal.rangeutil; 1432 long length = 0; 1433 auto counter = RangeCounter(() @trusted { return &length; } ()); 1434 static if (PRETTY) serializeToPrettyJson(counter, data); 1435 else serializeToJson(counter, data); 1436 headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length); 1437 } 1438 1439 auto rng = streamOutputRange!1024(bodyWriter); 1440 static if (PRETTY) serializeToPrettyJson(() @trusted { return &rng; } (), data); 1441 else serializeToJson(() @trusted { return &rng; } (), data); 1442 } 1443 1444 /** 1445 * Writes the response with no body. 1446 * 1447 * This method should be used in situations where no body is 1448 * requested, such as a HEAD request. For an empty body, just use writeBody, 1449 * as this method causes problems with some keep-alive connections. 1450 */ 1451 void writeVoidBody() 1452 @safe { 1453 if (!m_isHeadResponse) { 1454 assert("Content-Length" !in headers); 1455 assert("Transfer-Encoding" !in headers); 1456 } 1457 assert(!headerWritten); 1458 writeHeader(); 1459 m_conn.flush(); 1460 } 1461 1462 /** A stream for writing the body of the HTTP response. 1463 1464 Note that after 'bodyWriter' has been accessed for the first time, it 1465 is not allowed to change any header or the status code of the response. 1466 */ 1467 @property InterfaceProxy!OutputStream bodyWriter() 1468 @safe scope { 1469 assert(!!m_conn); 1470 if (m_bodyWriter) { 1471 // for test responses, the body writer is pre-set, without headers 1472 // being written, so we may need to do that here 1473 if (!m_headerWritten) writeHeader(); 1474 1475 return m_bodyWriter; 1476 } 1477 1478 assert(!m_headerWritten, "A void body was already written!"); 1479 assert(this.statusCode >= 200, "1xx responses can't have body"); 1480 1481 if (m_isHeadResponse) { 1482 // for HEAD requests, we define a NullOutputWriter for convenience 1483 // - no body will be written. However, the request handler should call writeVoidBody() 1484 // and skip writing of the body in this case. 1485 if ("Content-Length" !in headers) 1486 headers["Transfer-Encoding"] = "chunked"; 1487 writeHeader(); 1488 m_bodyWriter = nullSink; 1489 return m_bodyWriter; 1490 } 1491 1492 if ("Content-Encoding" in headers && "Content-Length" in headers) { 1493 // we do not known how large the compressed body will be in advance 1494 // so remove the content-length and use chunked transfer 1495 headers.remove("Content-Length"); 1496 } 1497 1498 if (auto pcl = "Content-Length" in headers) { 1499 writeHeader(); 1500 m_countingWriter.writeLimit = (*pcl).to!ulong; 1501 m_bodyWriter = m_countingWriter; 1502 } else if (httpVersion <= HTTPVersion.HTTP_1_0) { 1503 if ("Connection" in headers) 1504 headers.remove("Connection"); // default to "close" 1505 writeHeader(); 1506 m_bodyWriter = m_conn; 1507 } else { 1508 headers["Transfer-Encoding"] = "chunked"; 1509 writeHeader(); 1510 m_chunkedBodyWriter = createChunkedOutputStreamFL(m_countingWriter); 1511 m_bodyWriter = m_chunkedBodyWriter; 1512 } 1513 1514 if (auto pce = "Content-Encoding" in headers) { 1515 if (icmp2(*pce, "gzip") == 0) { 1516 m_zlibOutputStream = createGzipOutputStreamFL(m_bodyWriter); 1517 m_bodyWriter = m_zlibOutputStream; 1518 } else if (icmp2(*pce, "deflate") == 0) { 1519 m_zlibOutputStream = createDeflateOutputStreamFL(m_bodyWriter); 1520 m_bodyWriter = m_zlibOutputStream; 1521 } else { 1522 logWarn("Unsupported Content-Encoding set in response: '"~*pce~"'"); 1523 } 1524 } 1525 1526 return m_bodyWriter; 1527 } 1528 1529 1530 1531 /** Special method sending a SWITCHING_PROTOCOLS response to the client. 1532 1533 Notice: For the overload that returns a `ConnectionStream`, it must be 1534 ensured that the returned instance doesn't outlive the request 1535 handler callback. 1536 1537 Params: 1538 protocol = The protocol set in the "Upgrade" header of the response. 1539 Use an empty string to skip setting this field. 1540 */ 1541 ConnectionStream switchProtocol(string protocol) 1542 @safe { 1543 statusCode = HTTPStatus.switchingProtocols; 1544 if (protocol.length) headers["Upgrade"] = protocol; 1545 writeVoidBody(); 1546 m_requiresConnectionClose = true; 1547 m_headerWritten = true; 1548 return createConnectionProxyStream(m_conn, m_rawConnection); 1549 } 1550 /// ditto 1551 void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del) 1552 @safe { 1553 statusCode = HTTPStatus.switchingProtocols; 1554 if (protocol.length) headers["Upgrade"] = protocol; 1555 writeVoidBody(); 1556 m_requiresConnectionClose = true; 1557 m_headerWritten = true; 1558 () @trusted { 1559 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 1560 del(conn); 1561 } (); 1562 finalize(); 1563 } 1564 1565 /** Special method for handling CONNECT proxy tunnel 1566 1567 Notice: For the overload that returns a `ConnectionStream`, it must be 1568 ensured that the returned instance doesn't outlive the request 1569 handler callback. 1570 */ 1571 ConnectionStream connectProxy() 1572 @safe { 1573 return createConnectionProxyStream(m_conn, m_rawConnection); 1574 } 1575 /// ditto 1576 void connectProxy(scope void delegate(scope ConnectionStream) @safe del) 1577 @safe { 1578 () @trusted { 1579 auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection); 1580 del(conn); 1581 } (); 1582 finalize(); 1583 } 1584 1585 /** Sets the specified cookie value. 1586 1587 Params: 1588 name = Name of the cookie 1589 value = New cookie value - pass null to clear the cookie 1590 path = Path (as seen by the client) of the directory tree in which the cookie is visible 1591 encoding = Optional encoding (url, raw), default to URL encoding 1592 */ 1593 Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url) 1594 @safe { 1595 auto cookie = new Cookie(); 1596 cookie.path = path; 1597 cookie.setValue(value, encoding); 1598 if (value is null) { 1599 cookie.maxAge = 0; 1600 cookie.expires = "Thu, 01 Jan 1970 00:00:00 GMT"; 1601 } 1602 cookies[name] = cookie; 1603 return cookie; 1604 } 1605 1606 /** 1607 Initiates a new session. 1608 1609 The session is stored in the SessionStore that was specified when 1610 creating the server. Depending on this, the session can be persistent 1611 or temporary and specific to this server instance. 1612 */ 1613 Session startSession(string path = "/") 1614 @safe { 1615 return startSession(path, m_settings.sessionOptions); 1616 } 1617 1618 /// ditto 1619 Session startSession(string path, SessionOption options) 1620 @safe { 1621 assert(m_settings.sessionStore, "no session store set"); 1622 assert(!m_session, "Try to start a session, but already started one."); 1623 1624 bool secure; 1625 if (options & SessionOption.secure) secure = true; 1626 else if (options & SessionOption.noSecure) secure = false; 1627 else secure = this.tls; 1628 1629 m_session = m_settings.sessionStore.create(); 1630 m_session.set("$sessionCookiePath", path); 1631 m_session.set("$sessionCookieSecure", secure); 1632 auto cookie = setCookie(m_settings.sessionIdCookie, m_session.id, path); 1633 cookie.secure = secure; 1634 cookie.httpOnly = (options & SessionOption.httpOnly) != 0; 1635 cookie.sameSite = (options & SessionOption.noSameSiteStrict) ? 1636 Cookie.SameSite.lax : Cookie.SameSite.strict; 1637 return m_session; 1638 } 1639 1640 /** 1641 Terminates the current session (if any). 1642 */ 1643 void terminateSession() 1644 @safe { 1645 if (!m_session) return; 1646 auto cookie = setCookie(m_settings.sessionIdCookie, null, m_session.get!string("$sessionCookiePath")); 1647 cookie.secure = m_session.get!bool("$sessionCookieSecure"); 1648 m_session.destroy(); 1649 m_session = Session.init; 1650 } 1651 1652 @property ulong bytesWritten() @safe const { return m_countingWriter.bytesWritten; } 1653 1654 /** 1655 Waits until either the connection closes, data arrives, or until the 1656 given timeout is reached. 1657 1658 Returns: 1659 $(D true) if the connection was closed and $(D false) if either the 1660 timeout was reached, or if data has arrived for consumption. 1661 1662 See_Also: `connected` 1663 */ 1664 bool waitForConnectionClose(Duration timeout = Duration.max) 1665 @safe { 1666 if (!m_rawConnection || !m_rawConnection.connected) return true; 1667 m_rawConnection.waitForData(timeout); 1668 return !m_rawConnection.connected; 1669 } 1670 1671 /** 1672 Determines if the underlying connection is still alive. 1673 1674 Returns $(D true) if the remote peer is still connected and $(D false) 1675 if the remote peer closed the connection. 1676 1677 See_Also: `waitForConnectionClose` 1678 */ 1679 @property bool connected() 1680 @safe const { 1681 if (!m_rawConnection) return false; 1682 return m_rawConnection.connected; 1683 } 1684 1685 /** 1686 Finalizes the response. This is usually called automatically by the server. 1687 1688 This method can be called manually after writing the response to force 1689 all network traffic associated with the current request to be finalized. 1690 After the call returns, the `timeFinalized` property will be set. 1691 */ 1692 void finalize() 1693 @safe { 1694 if (m_zlibOutputStream) { 1695 m_zlibOutputStream.finalize(); 1696 m_zlibOutputStream.destroy(); 1697 } 1698 if (m_chunkedBodyWriter) { 1699 m_chunkedBodyWriter.finalize(); 1700 m_chunkedBodyWriter.destroy(); 1701 } 1702 1703 // ignore exceptions caused by an already closed connection - the client 1704 // may have closed the connection already and this doesn't usually indicate 1705 // a problem. 1706 if (m_rawConnection && m_rawConnection.connected) { 1707 try if (m_conn) m_conn.flush(); 1708 catch (Exception e) logDebug("Failed to flush connection after finishing HTTP response: %s", e.msg); 1709 if (!isHeadResponse && bytesWritten < headers.get("Content-Length", "0").to!ulong) { 1710 logDebug("HTTP response only written partially before finalization. Terminating connection."); 1711 m_requiresConnectionClose = true; 1712 } 1713 1714 m_rawConnection = InterfaceProxy!ConnectionStream.init; 1715 } 1716 1717 if (m_conn) { 1718 m_conn = InterfaceProxy!Stream.init; 1719 m_timeFinalized = Clock.currTime(UTC()); 1720 } 1721 } 1722 1723 private void writeHeader() 1724 @safe { 1725 import vibe.stream.wrapper; 1726 1727 assert(!m_headerWritten, "Try to write header after body has already begun."); 1728 assert(this.httpVersion != HTTPVersion.HTTP_1_0 || this.statusCode >= 200, "Informational status codes aren't supported by HTTP/1.0."); 1729 1730 // Don't set m_headerWritten for 1xx status codes 1731 if (this.statusCode >= 200) m_headerWritten = true; 1732 auto dst = streamOutputRange!1024(m_conn); 1733 1734 void writeLine(T...)(string fmt, T args) 1735 @safe { 1736 formattedWrite(() @trusted { return &dst; } (), fmt, args); 1737 dst.put("\r\n"); 1738 logTrace(fmt, args); 1739 } 1740 1741 logTrace("---------------------"); 1742 logTrace("HTTP server response:"); 1743 logTrace("---------------------"); 1744 1745 // write the status line 1746 writeLine("%s %d %s", 1747 getHTTPVersionString(this.httpVersion), 1748 this.statusCode, 1749 this.statusPhrase.length ? this.statusPhrase : httpStatusText(this.statusCode)); 1750 1751 // write all normal headers 1752 foreach (k, v; this.headers.byKeyValue) { 1753 dst.put(k); 1754 dst.put(": "); 1755 dst.put(v); 1756 dst.put("\r\n"); 1757 logTrace("%s: %s", k, v); 1758 } 1759 1760 logTrace("---------------------"); 1761 1762 // write cookies 1763 foreach (n, cookie; () @trusted { return this.cookies.byKeyValue; } ()) { 1764 dst.put("Set-Cookie: "); 1765 cookie.writeString(() @trusted { return &dst; } (), n); 1766 dst.put("\r\n"); 1767 } 1768 1769 // finalize response header 1770 dst.put("\r\n"); 1771 } 1772 } 1773 1774 /** 1775 Represents the request listener for a specific `listenHTTP` call. 1776 1777 This struct can be used to stop listening for HTTP requests at runtime. 1778 */ 1779 struct HTTPListener { 1780 private { 1781 size_t[] m_virtualHostIDs; 1782 } 1783 1784 private this(size_t[] ids) @safe { m_virtualHostIDs = ids; } 1785 1786 @property NetworkAddress[] bindAddresses() 1787 @safe { 1788 NetworkAddress[] ret; 1789 foreach (l; s_listeners) 1790 if (l.m_virtualHosts.canFind!(v => m_virtualHostIDs.canFind(v.id))) { 1791 NetworkAddress a; 1792 a = resolveHost(l.bindAddress); 1793 a.port = l.bindPort; 1794 ret ~= a; 1795 } 1796 return ret; 1797 } 1798 1799 /** Stops handling HTTP requests and closes the TCP listening port if 1800 possible. 1801 */ 1802 void stopListening() 1803 @safe { 1804 import std.algorithm : countUntil; 1805 1806 foreach (vhid; m_virtualHostIDs) { 1807 foreach (lidx, l; s_listeners) { 1808 if (l.removeVirtualHost(vhid)) { 1809 if (!l.hasVirtualHosts) { 1810 l.m_listener.stopListening(); 1811 logInfo("Stopped to listen for HTTP%s requests on %s:%s", l.tlsContext ? "S": "", l.bindAddress, l.bindPort); 1812 s_listeners = s_listeners[0 .. lidx] ~ s_listeners[lidx+1 .. $]; 1813 } 1814 } 1815 break; 1816 } 1817 } 1818 } 1819 } 1820 1821 1822 /** Represents a single HTTP server port. 1823 1824 This class defines the incoming interface, port, and TLS configuration of 1825 the public server port. The public server port may differ from the local 1826 one if a reverse proxy of some kind is facing the public internet and 1827 forwards to this HTTP server. 1828 1829 Multiple virtual hosts can be configured to be served from the same port. 1830 Their TLS settings must be compatible and each virtual host must have a 1831 unique name. 1832 */ 1833 final class HTTPServerContext { 1834 private struct VirtualHost { 1835 HTTPServerRequestDelegate requestHandler; 1836 HTTPServerSettings settings; 1837 HTTPLogger[] loggers; 1838 size_t id; 1839 } 1840 1841 private { 1842 TCPListener m_listener; 1843 VirtualHost[] m_virtualHosts; 1844 string m_bindAddress; 1845 ushort m_bindPort; 1846 TLSContext m_tlsContext; 1847 static size_t s_vhostIDCounter = 1; 1848 } 1849 1850 @safe: 1851 1852 this(string bind_address, ushort bind_port) 1853 { 1854 m_bindAddress = bind_address; 1855 m_bindPort = bind_port; 1856 } 1857 1858 /** Returns the TLS context associated with the listener. 1859 1860 For non-HTTPS listeners, `null` will be returned. Otherwise, if only a 1861 single virtual host has been added, the TLS context of that host's 1862 settings is returned. For multiple virtual hosts, an SNI context is 1863 returned, which forwards to the individual contexts based on the 1864 requested host name. 1865 */ 1866 @property TLSContext tlsContext() { return m_tlsContext; } 1867 1868 /// The local network interface IP address associated with this listener 1869 @property string bindAddress() const { return m_bindAddress; } 1870 1871 /// The local port associated with this listener 1872 @property ushort bindPort() const { return m_bindPort; } 1873 1874 /// Determines if any virtual hosts have been addded 1875 @property bool hasVirtualHosts() const { return m_virtualHosts.length > 0; } 1876 1877 /** Adds a single virtual host. 1878 1879 Note that the port and bind address defined in `settings` must match the 1880 ones for this listener. The `settings.host` field must be unique for 1881 all virtual hosts. 1882 1883 Returns: Returns a unique ID for the new virtual host 1884 */ 1885 size_t addVirtualHost(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler) 1886 { 1887 assert(settings.port == 0 || settings.port == m_bindPort, "Virtual host settings do not match bind port."); 1888 assert(settings.bindAddresses.canFind(m_bindAddress), "Virtual host settings do not match bind address."); 1889 1890 VirtualHost vhost; 1891 vhost.id = s_vhostIDCounter++; 1892 vhost.settings = settings; 1893 vhost.requestHandler = request_handler; 1894 1895 if (settings.accessLogger) vhost.loggers ~= settings.accessLogger; 1896 if (settings.accessLogToConsole) 1897 vhost.loggers ~= new HTTPConsoleLogger(settings, settings.accessLogFormat); 1898 if (settings.accessLogFile.length) 1899 vhost.loggers ~= new HTTPFileLogger(settings, settings.accessLogFormat, settings.accessLogFile); 1900 1901 if (!m_virtualHosts.length) m_tlsContext = settings.tlsContext; 1902 1903 enforce((m_tlsContext !is null) == (settings.tlsContext !is null), 1904 "Cannot mix HTTP and HTTPS virtual hosts within the same listener."); 1905 1906 if (m_tlsContext) addSNIHost(settings); 1907 1908 m_virtualHosts ~= vhost; 1909 1910 if (settings.hostName.length) { 1911 auto proto = settings.tlsContext ? "https" : "http"; 1912 auto port = settings.tlsContext && settings.port == 443 || !settings.tlsContext && settings.port == 80 ? "" : ":" ~ settings.port.to!string; 1913 logInfo("Added virtual host %s://%s:%s/ (%s)", proto, settings.hostName, m_bindPort, m_bindAddress); 1914 } 1915 1916 return vhost.id; 1917 } 1918 1919 /// Removes a previously added virtual host using its ID. 1920 bool removeVirtualHost(size_t id) 1921 { 1922 import std.algorithm.searching : countUntil; 1923 1924 auto idx = m_virtualHosts.countUntil!(c => c.id == id); 1925 if (idx < 0) return false; 1926 1927 auto ctx = m_virtualHosts[idx]; 1928 m_virtualHosts = m_virtualHosts[0 .. idx] ~ m_virtualHosts[idx+1 .. $]; 1929 return true; 1930 } 1931 1932 private void addSNIHost(HTTPServerSettings settings) 1933 { 1934 if (settings.tlsContext !is m_tlsContext && m_tlsContext.kind != TLSContextKind.serverSNI) { 1935 logDebug("Create SNI TLS context for %s, port %s", bindAddress, bindPort); 1936 m_tlsContext = createTLSContext(TLSContextKind.serverSNI); 1937 m_tlsContext.sniCallback = &onSNI; 1938 } 1939 1940 foreach (ctx; m_virtualHosts) { 1941 /*enforce(ctx.settings.hostName != settings.hostName, 1942 "A server with the host name '"~settings.hostName~"' is already " 1943 "listening on "~addr~":"~to!string(settings.port)~".");*/ 1944 } 1945 } 1946 1947 private TLSContext onSNI(string servername) 1948 { 1949 foreach (vhost; m_virtualHosts) 1950 if (vhost.settings.hostName.icmp(servername) == 0) { 1951 logDebug("Found context for SNI host '%s'.", servername); 1952 return vhost.settings.tlsContext; 1953 } 1954 logDebug("No context found for SNI host '%s'.", servername); 1955 return null; 1956 } 1957 } 1958 1959 /**************************************************************************************************/ 1960 /* Private types */ 1961 /**************************************************************************************************/ 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(Allocator)(InterfaceProxy!Stream http_stream, TCPConnection tcp_connection, HTTPServerContext listen_info, ref HTTPServerSettings settings, ref bool keep_alive, scope Allocator 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, settings.maxRequestHeaderLineSize); 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, Allocator)(HTTPServerRequest req, InputStream http_stream, Allocator alloc, ulong max_header_size, size_t max_header_line_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(max_header_line_size, "\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, max_header_line_size, 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, 32); 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;