1 /** 2 * A simple HTTP server. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Stéphan Kochen <stephan@kochen.nl> 12 * Vladimir Panteleev <ae@cy.md> 13 * Simon Arlott 14 */ 15 16 module ae.net.http.server; 17 18 import std.conv; 19 import std.datetime; 20 import std.exception; 21 import std.range; 22 import std.socket; 23 import std.string; 24 import std.uri; 25 26 import ae.net.asockets; 27 import ae.net.ietf.headerparse; 28 import ae.net.ietf.headers; 29 import ae.net.ssl; 30 import ae.sys.data; 31 import ae.sys.log; 32 import ae.utils.container.listnode; 33 import ae.utils.exception; 34 import ae.utils.text; 35 import ae.utils.textout; 36 37 public import ae.net.http.common; 38 39 debug(HTTP) import std.stdio : stderr; 40 41 /// The base class for an incoming connection to a HTTP server, 42 /// unassuming of transport. 43 class BaseHttpServerConnection 44 { 45 public: 46 TimeoutAdapter timer; /// Time-out adapter. 47 IConnection conn; /// Connection used for this HTTP connection. 48 49 HttpRequest currentRequest; /// The current in-flight request. 50 bool persistent; /// Whether we will keep the connection open after the request is handled. 51 52 bool connected = true; /// Are we connected now? 53 Logger log; /// Optional HTTP log. 54 55 void delegate(HttpRequest request) handleRequest; /// Callback to handle a fully received request. 56 57 protected: 58 Data[] inBuffer; 59 sizediff_t expect; 60 size_t responseSize; 61 bool requestProcessing; // user code is asynchronously processing current request 62 bool firstRequest = true; 63 Duration timeout = HttpServer.defaultTimeout; 64 bool timeoutActive; 65 string banner; 66 67 this(IConnection c) 68 { 69 debug (HTTP) debugLog("New connection from %s", remoteAddressStr(null)); 70 71 timer = new TimeoutAdapter(c); 72 timer.setIdleTimeout(timeout); 73 c = timer; 74 75 this.conn = c; 76 conn.handleReadData = &onNewRequest; 77 conn.handleDisconnect = &onDisconnect; 78 79 timeoutActive = true; 80 } 81 82 debug (HTTP) 83 final void debugLog(Args...)(Args args) 84 { 85 stderr.writef("[%s %s] ", Clock.currTime(), cast(void*)this); 86 stderr.writefln(args); 87 } 88 89 final void onNewRequest(Data data) 90 { 91 try 92 { 93 inBuffer ~= data; 94 debug (HTTP) debugLog("Receiving start of request (%d new bytes, %d total)", data.length, inBuffer.bytes.length); 95 96 string reqLine; 97 Headers headers; 98 99 if (!parseHeaders(inBuffer, reqLine, headers)) 100 { 101 debug (HTTP) debugLog("Headers not yet received. Data in buffer:\n%s---", cast(string)inBuffer.joinToHeap()); 102 return; 103 } 104 105 debug (HTTP) 106 { 107 debugLog("Headers received:"); 108 debugLog("> %s", reqLine); 109 foreach (name, value; headers) 110 debugLog("> %s: %s", name, value); 111 } 112 113 currentRequest = new HttpRequest; 114 currentRequest.parseRequestLine(reqLine); 115 currentRequest.headers = headers; 116 117 auto connection = toLower(currentRequest.headers.get("Connection", null)); 118 switch (currentRequest.protocolVersion) 119 { 120 case "1.0": 121 persistent = connection == "keep-alive"; 122 break; 123 default: // 1.1+ 124 persistent = connection != "close"; 125 break; 126 } 127 debug (HTTP) debugLog("This %s connection %s persistent", currentRequest.protocolVersion, persistent ? "IS" : "is NOT"); 128 129 expect = 0; 130 if ("Content-Length" in currentRequest.headers) 131 expect = to!size_t(currentRequest.headers["Content-Length"]); 132 133 if (expect > 0) 134 { 135 if (expect > inBuffer.bytes.length) 136 conn.handleReadData = &onContinuation; 137 else 138 processRequest(inBuffer.popFront(expect)); 139 } 140 else 141 processRequest(null); 142 } 143 catch (CaughtException e) 144 { 145 debug (HTTP) debugLog("Exception onNewRequest: %s", e); 146 HttpResponse response; 147 debug 148 { 149 response = new HttpResponse(); 150 response.status = HttpStatusCode.InternalServerError; 151 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 152 response.headers["Content-Type"] = "text/plain"; 153 response.data = [Data(e.toString())]; 154 } 155 sendResponse(response); 156 } 157 } 158 159 void onDisconnect(string reason, DisconnectType type) 160 { 161 debug (HTTP) debugLog("Disconnect: %s", reason); 162 connected = false; 163 } 164 165 final void onContinuation(Data data) 166 { 167 debug (HTTP) debugLog("Receiving continuation of request: \n%s---", cast(string)data.contents); 168 inBuffer ~= data; 169 170 if (!requestProcessing && inBuffer.bytes.length >= expect) 171 { 172 debug (HTTP) debugLog("%s/%s", inBuffer.bytes.length, expect); 173 processRequest(inBuffer.popFront(expect)); 174 } 175 } 176 177 final void processRequest(Data[] data) 178 { 179 debug (HTTP) debugLog("processRequest (%d bytes)", data.bytes.length); 180 currentRequest.data = data; 181 timeoutActive = false; 182 timer.cancelIdleTimeout(); 183 if (handleRequest) 184 { 185 // Log unhandled exceptions, but don't mess up the stack trace 186 //scope(failure) logRequest(currentRequest, null); 187 188 // sendResponse may be called immediately, or later 189 requestProcessing = true; 190 handleRequest(currentRequest); 191 } 192 } 193 194 final void logRequest(HttpRequest request, HttpResponse response) 195 { 196 debug // avoid linewrap in terminal during development 197 enum DEBUG = true; 198 else 199 enum DEBUG = false; 200 201 if (log) log(([ 202 "", // align IP to tab 203 remoteAddressStr(request), 204 response ? text(cast(ushort)response.status) : "-", 205 request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-", 206 request ? request.method : "-", 207 request ? formatLocalAddress(request) ~ request.resource : "-", 208 response ? response.headers.get("Content-Type", "-") : "-", 209 ] ~ (DEBUG ? [] : [ 210 request ? request.headers.get("Referer", "-") : "-", 211 request ? request.headers.get("User-Agent", "-") : "-", 212 ])).join("\t")); 213 } 214 215 abstract string formatLocalAddress(HttpRequest r); 216 217 /// Idle connections are those which can be closed when the server 218 /// is shutting down. 219 final @property bool idle() 220 { 221 // Technically, with a persistent connection, we never know if 222 // there is a request on the wire on the way to us which we 223 // haven't received yet, so it's not possible to truly know 224 // when the connection is idle and can be safely closed. 225 // However, we do have the ability to do that for 226 // non-persistent connections - assume that a connection is 227 // never idle until we receive (and process) the first 228 // request. Therefore, in deployments where clients require 229 // that an outstanding request is always processed before the 230 // server is shut down, non-persistent connections can be used 231 // (i.e. no attempt to reuse `HttpClient`) to achieve this. 232 if (firstRequest) 233 return false; 234 235 if (requestProcessing) 236 return false; 237 238 foreach (datum; inBuffer) 239 if (datum.length) 240 return false; 241 242 return true; 243 } 244 245 public: 246 /// Send the given HTTP response. 247 final void sendResponse(HttpResponse response) 248 { 249 requestProcessing = false; 250 if (!response) 251 { 252 debug (HTTP) debugLog("sendResponse(null) - generating dummy response"); 253 response = new HttpResponse(); 254 response.status = HttpStatusCode.InternalServerError; 255 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 256 response.data = [Data("Internal Server Error")]; 257 } 258 assert(response.status != 0); 259 260 if (currentRequest) 261 { 262 response.optimizeData(currentRequest.headers); 263 response.sliceData(currentRequest.headers); 264 } 265 266 if ("Content-Length" !in response.headers) 267 response.headers["Content-Length"] = text(response.data.bytes.length); 268 269 sendHeaders(response); 270 271 bool isHead = currentRequest ? currentRequest.method == "HEAD" : false; 272 if (response && response.data.length && !isHead) 273 sendData(response.data); 274 275 responseSize = response ? response.data.bytes.length : 0; 276 debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize); 277 278 closeResponse(); 279 280 logRequest(currentRequest, response); 281 } 282 283 /// Send these headers only. 284 /// Low-level alternative to `sendResponse`. 285 final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null) 286 { 287 assert(status, "Unset status code"); 288 289 if (!statusMessage) 290 statusMessage = HttpResponse.getStatusMessage(status); 291 292 StringBuilder respMessage; 293 auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0"; 294 respMessage.put("HTTP/", protocolVersion, " "); 295 296 if (banner && "X-Powered-By" !in headers) 297 headers["X-Powered-By"] = banner; 298 299 if ("Date" !in headers) 300 headers["Date"] = httpTime(Clock.currTime()); 301 302 if ("Connection" !in headers) 303 { 304 if (persistent && protocolVersion=="1.0") 305 headers["Connection"] = "Keep-Alive"; 306 else 307 if (!persistent && protocolVersion=="1.1") 308 headers["Connection"] = "close"; 309 } 310 311 respMessage.put("%d %s\r\n".format(status, statusMessage)); 312 foreach (string header, string value; headers) 313 respMessage.put(header, ": ", value, "\r\n"); 314 315 debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> ")); 316 317 respMessage.put("\r\n"); 318 conn.send(Data(respMessage.get())); 319 } 320 321 /// ditto 322 final void sendHeaders(HttpResponse response) 323 { 324 sendHeaders(response.headers, response.status, response.statusMessage); 325 } 326 327 /// Send this data only. 328 /// Headers should have already been sent. 329 /// Low-level alternative to `sendResponse`. 330 final void sendData(Data[] data) 331 { 332 conn.send(data); 333 } 334 335 /// Accept more requests on the same connection? 336 protected bool acceptMore() { return true; } 337 338 /// Finalize writing the response. 339 /// Headers and data should have already been sent. 340 /// Low-level alternative to `sendResponse`. 341 final void closeResponse() 342 { 343 firstRequest = false; 344 if (persistent && acceptMore) 345 { 346 // reset for next request 347 debug (HTTP) debugLog(" Waiting for next request."); 348 conn.handleReadData = &onNewRequest; 349 if (!timeoutActive) 350 { 351 // Give the client time to download large requests. 352 // Assume a minimal speed of 1kb/s. 353 timer.setIdleTimeout(timeout + (responseSize / 1024).seconds); 354 timeoutActive = true; 355 } 356 if (inBuffer.bytes.length) // a second request has been pipelined 357 { 358 debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length); 359 onNewRequest(Data()); 360 } 361 } 362 else 363 { 364 string reason = persistent ? "Server has been shut down" : "Non-persistent connection"; 365 debug (HTTP) debugLog(" Closing connection (%s).", reason); 366 conn.disconnect(reason); 367 } 368 } 369 370 /// Retrieve the remote address of the peer, as a string. 371 abstract @property string remoteAddressStr(HttpRequest r); 372 } 373 374 /// Basic unencrypted HTTP 1.0/1.1 server. 375 class HttpServer 376 { 377 enum defaultTimeout = 30.seconds; /// The default timeout used for incoming connections. 378 379 // public: 380 this(Duration timeout = defaultTimeout) 381 { 382 assert(timeout > Duration.zero); 383 this.timeout = timeout; 384 385 conn = new TcpServer(); 386 conn.handleClose = &onClose; 387 conn.handleAccept = &onAccept; 388 } /// 389 390 /// Listen on the given TCP address and port. 391 /// If port is 0, listen on a random available port. 392 /// Returns the port that the server is actually listening on. 393 ushort listen(ushort port, string addr = null) 394 { 395 port = conn.listen(port, addr); 396 if (log) 397 foreach (address; conn.localAddresses) 398 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 399 return port; 400 } 401 402 /// Listen on the given addresses. 403 void listen(AddressInfo[] addresses) 404 { 405 conn.listen(addresses); 406 if (log) 407 foreach (address; conn.localAddresses) 408 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 409 } 410 411 /// Get listen addresses. 412 @property Address[] localAddresses() { return conn.localAddresses; } 413 414 /// Stop listening, and close idle client connections. 415 void close() 416 { 417 debug(HTTP) stderr.writeln("Shutting down"); 418 if (log) log("Shutting down."); 419 conn.close(); 420 421 debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength); 422 423 // Close idle connections 424 foreach (connection; connections.iterator.array) 425 if (connection.idle && connection.conn.state == ConnectionState.connected) 426 connection.conn.disconnect("HTTP server shutting down"); 427 } 428 429 /// Optional HTTP request log. 430 Logger log; 431 432 /// Single-ended doubly-linked list of active connections 433 SEDListContainer!HttpServerConnection connections; 434 435 /// Callback for when the socket was closed. 436 void delegate() handleClose; 437 /// Callback for an incoming request. 438 void delegate(HttpRequest request, HttpServerConnection conn) handleRequest; 439 440 /// What to send in the `"X-Powered-By"` header. 441 string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)"; 442 443 /// If set, the name of the header which will be used to obtain 444 /// the actual IP of the connecting peer. Useful when this 445 /// `HttpServer` is behind a reverse proxy. 446 string remoteIPHeader; 447 448 protected: 449 TcpServer conn; 450 Duration timeout; 451 452 void onClose() 453 { 454 if (handleClose) 455 handleClose(); 456 } 457 458 IConnection createConnection(TcpConnection tcp) 459 { 460 return tcp; 461 } 462 463 @property string protocol() { return "http"; } 464 465 void onAccept(TcpConnection incoming) 466 { 467 try 468 new HttpServerConnection(this, incoming, createConnection(incoming), protocol); 469 catch (Exception e) 470 { 471 if (log) 472 log("Error accepting connection: " ~ e.msg); 473 if (incoming.state == ConnectionState.connected) 474 incoming.disconnect(); 475 } 476 } 477 } 478 479 /** 480 HTTPS server. Set SSL parameters on ctx after instantiation. 481 482 Example: 483 --- 484 auto s = new HttpsServer(); 485 s.ctx.enableDH(4096); 486 s.ctx.enableECDH(); 487 s.ctx.setCertificate("server.crt"); 488 s.ctx.setPrivateKey("server.key"); 489 --- 490 */ 491 class HttpsServer : HttpServer 492 { 493 SSLContext ctx; /// The SSL context. 494 495 this() 496 { 497 ctx = ssl.createContext(SSLContext.Kind.server); 498 } /// 499 500 protected: 501 override @property string protocol() { return "https"; } 502 503 override IConnection createConnection(TcpConnection tcp) 504 { 505 return ssl.createAdapter(ctx, tcp); 506 } 507 } 508 509 /// Standard TCP-based HTTP server connection. 510 final class HttpServerConnection : BaseHttpServerConnection 511 { 512 TcpConnection tcp; /// The TCP transport. 513 HttpServer server; /// `HttpServer` owning this connection. 514 /// Cached local and remote addresses. 515 Address localAddress, remoteAddress; 516 517 mixin DListLink; 518 519 /// Retrieves the remote peer address, honoring `remoteIPHeader` if set. 520 override @property string remoteAddressStr(HttpRequest r) 521 { 522 if (server.remoteIPHeader) 523 { 524 if (r) 525 if (auto p = server.remoteIPHeader in r.headers) 526 return (*p).split(",")[$ - 1]; 527 528 return "[local:" ~ remoteAddress.toAddrString() ~ "]"; 529 } 530 531 return remoteAddress.toAddrString(); 532 } 533 534 protected: 535 string protocol; 536 537 this(HttpServer server, TcpConnection tcp, IConnection c, string protocol = "http") 538 { 539 this.server = server; 540 this.tcp = tcp; 541 this.log = server.log; 542 this.protocol = protocol; 543 this.banner = server.banner; 544 this.timeout = server.timeout; 545 this.handleRequest = (HttpRequest r) => server.handleRequest(r, this); 546 this.localAddress = tcp.localAddress; 547 this.remoteAddress = tcp.remoteAddress; 548 549 super(c); 550 551 server.connections.pushFront(this); 552 } 553 554 override void onDisconnect(string reason, DisconnectType type) 555 { 556 super.onDisconnect(reason, type); 557 server.connections.remove(this); 558 } 559 560 override bool acceptMore() { return server.conn.isListening; } 561 override string formatLocalAddress(HttpRequest r) { return formatAddress(protocol, localAddress, r.host, r.port); } 562 } 563 564 /// `BaseHttpServerConnection` implementation with files, allowing to 565 /// e.g. read a request from standard input and write the response to 566 /// standard output. 567 version (Posix) 568 final class FileHttpServerConnection : BaseHttpServerConnection 569 { 570 this(File input = stdin, File output = stdout, string protocol = "stdin") 571 { 572 this.protocol = protocol; 573 574 auto c = new Duplex( 575 new FileConnection(input.fileno), 576 new FileConnection(output.fileno), 577 ); 578 579 super(c); 580 } /// 581 582 override @property string remoteAddressStr(HttpRequest r) { return "-"; } /// Stub. 583 584 protected: 585 import std.stdio : File, stdin, stdout; 586 587 string protocol; 588 589 override string formatLocalAddress(HttpRequest r) { return protocol ~ "://"; } 590 } 591 592 /// Formats a remote address for logging. 593 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0) 594 { 595 string addr = address.toAddrString(); 596 string port = 597 address.addressFamily == AddressFamily.UNIX ? null : 598 logPort ? text(logPort) : 599 address.toPortString(); 600 return protocol ~ "://" ~ 601 (vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~ 602 (port is null || port == "80" ? "" : ":" ~ port); 603 } 604 605 version (unittest) import ae.net.http.client; 606 version (unittest) import ae.net.http.responseex; 607 unittest 608 { 609 int[] replies; 610 int closeAfter; 611 612 // Sum "a" from GET and "b" from POST 613 auto s = new HttpServer; 614 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 615 auto get = request.urlParameters; 616 auto post = request.decodePostData(); 617 auto response = new HttpResponseEx; 618 auto result = to!int(get["a"]) + to!int(post["b"]); 619 replies ~= result; 620 conn.sendResponse(response.serveJson(result)); 621 if (--closeAfter == 0) 622 s.close(); 623 }; 624 625 // Test server, client, parameter encoding 626 replies = null; 627 closeAfter = 1; 628 auto port = s.listen(0, "127.0.0.1"); 629 httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null); 630 socketManager.loop(); 631 632 // Test pipelining, protocol errors 633 replies = null; 634 closeAfter = 2; 635 port = s.listen(0, "127.0.0.1"); 636 TcpConnection c = new TcpConnection; 637 c.handleConnect = { 638 c.send(Data( 639 "GET /?a=123456 HTTP/1.1 640 Content-length: 8 641 Content-type: application/x-www-form-urlencoded 642 643 b=654321" ~ 644 "GET /derp HTTP/1.1 645 Content-length: potato 646 647 " ~ 648 "GET /?a=1234567 HTTP/1.1 649 Content-length: 9 650 Content-type: application/x-www-form-urlencoded 651 652 b=7654321")); 653 c.disconnect(); 654 }; 655 c.connect("127.0.0.1", port); 656 657 socketManager.loop(); 658 659 assert(replies == [777777, 8888888]); 660 661 // Test bad headers 662 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 663 auto response = new HttpResponseEx; 664 conn.sendResponse(response.serveText("OK")); 665 if (--closeAfter == 0) 666 s.close(); 667 }; 668 closeAfter = 1; 669 670 port = s.listen(0, "127.0.0.1"); 671 c = new TcpConnection; 672 c.handleConnect = { 673 c.send(Data("\n\n\n\n\n")); 674 c.disconnect(); 675 676 // Now send a valid request to end the loop 677 c = new TcpConnection; 678 c.handleConnect = { 679 c.send(Data("GET / HTTP/1.0\n\n")); 680 c.disconnect(); 681 }; 682 c.connect("127.0.0.1", port); 683 }; 684 c.connect("127.0.0.1", port); 685 686 socketManager.loop(); 687 688 /+ 689 void testFile(string fn) 690 { 691 std.file.write(fn, "42"); 692 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 693 auto response = new HttpResponseEx; 694 conn.sendResponse(response.serveFile(request.resource[1..$], "")); 695 if (--closeAfter == 0) 696 s.close(); 697 }; 698 port = s.listen(0, "127.0.0.1"); 699 closeAfter = 1; 700 httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null); 701 socketManager.loop(); 702 std.file.remove(fn); 703 } 704 705 testFile("http-test.bin"); 706 testFile("http-test.txt"); 707 +/ 708 }