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