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