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 DataVec inBuffer; 60 sizediff_t expect; 61 size_t responseSize; 62 bool requestProcessing; // user code is asynchronously processing current request 63 bool firstRequest = true; 64 Duration timeout = HttpServer.defaultTimeout; 65 bool timeoutActive; 66 string banner; 67 68 this(IConnection c) 69 { 70 debug (HTTP) debugLog("New connection from %s", remoteAddressStr(null)); 71 72 timer = new TimeoutAdapter(c); 73 timer.setIdleTimeout(timeout); 74 c = timer; 75 76 this.conn = c; 77 conn.handleReadData = &onNewRequest; 78 conn.handleDisconnect = &onDisconnect; 79 80 timeoutActive = true; 81 } 82 83 debug (HTTP) 84 final void debugLog(Args...)(Args args) 85 { 86 stderr.writef("[%s %s] ", Clock.currTime(), cast(void*)this); 87 stderr.writefln(args); 88 } 89 90 final void onNewRequest(Data data) 91 { 92 try 93 { 94 inBuffer ~= data; 95 debug (HTTP) debugLog("Receiving start of request (%d new bytes, %d total)", data.length, inBuffer.bytes.length); 96 97 string reqLine; 98 Headers headers; 99 100 if (!parseHeaders(inBuffer, reqLine, headers)) 101 { 102 debug (HTTP) debugLog("Headers not yet received. Data in buffer:\n%s---", cast(string)inBuffer.joinToHeap()); 103 return; 104 } 105 106 debug (HTTP) 107 { 108 debugLog("Headers received:"); 109 debugLog("> %s", reqLine); 110 foreach (name, value; headers) 111 debugLog("> %s: %s", name, value); 112 } 113 114 currentRequest = new HttpRequest; 115 currentRequest.parseRequestLine(reqLine); 116 currentRequest.headers = headers; 117 118 auto connection = toLower(currentRequest.headers.get("Connection", null)); 119 switch (currentRequest.protocolVersion) 120 { 121 case "1.0": 122 persistent = connection == "keep-alive"; 123 break; 124 default: // 1.1+ 125 persistent = connection != "close"; 126 break; 127 } 128 debug (HTTP) debugLog("This %s connection %s persistent", currentRequest.protocolVersion, persistent ? "IS" : "is NOT"); 129 130 expect = 0; 131 if ("Content-Length" in currentRequest.headers) 132 expect = to!size_t(currentRequest.headers["Content-Length"]); 133 134 if (expect > 0) 135 { 136 if (expect > inBuffer.bytes.length) 137 conn.handleReadData = &onContinuation; 138 else 139 processRequest(inBuffer.shift(expect)); 140 } 141 else 142 processRequest(DataVec.init); 143 } 144 catch (CaughtException e) 145 { 146 debug (HTTP) debugLog("Exception onNewRequest: %s", e); 147 HttpResponse response; 148 debug 149 { 150 response = new HttpResponse(); 151 response.status = HttpStatusCode.InternalServerError; 152 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 153 response.headers["Content-Type"] = "text/plain"; 154 response.data = DataVec(Data(e.toString())); 155 } 156 sendResponse(response); 157 } 158 } 159 160 void onDisconnect(string reason, DisconnectType type) 161 { 162 debug (HTTP) debugLog("Disconnect: %s", reason); 163 connected = false; 164 } 165 166 final void onContinuation(Data data) 167 { 168 debug (HTTP) debugLog("Receiving continuation of request: \n%s---", cast(string)data.contents); 169 inBuffer ~= data; 170 171 if (!requestProcessing && inBuffer.bytes.length >= expect) 172 { 173 debug (HTTP) debugLog("%s/%s", inBuffer.bytes.length, expect); 174 processRequest(inBuffer.shift(expect)); 175 } 176 } 177 178 final void processRequest(DataVec data) 179 { 180 debug (HTTP) debugLog("processRequest (%d bytes)", data.bytes.length); 181 currentRequest.data = move(data); 182 timeoutActive = false; 183 timer.cancelIdleTimeout(); 184 if (handleRequest) 185 { 186 // Log unhandled exceptions, but don't mess up the stack trace 187 //scope(failure) logRequest(currentRequest, null); 188 189 // sendResponse may be called immediately, or later 190 requestProcessing = true; 191 handleRequest(currentRequest); 192 } 193 } 194 195 final void logRequest(HttpRequest request, HttpResponse response) 196 { 197 debug // avoid linewrap in terminal during development 198 enum DEBUG = true; 199 else 200 enum DEBUG = false; 201 202 if (log) log(([ 203 "", // align IP to tab 204 remoteAddressStr(request), 205 response ? text(cast(ushort)response.status) : "-", 206 request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-", 207 request ? request.method : "-", 208 request ? formatLocalAddress(request) ~ request.resource : "-", 209 response ? response.headers.get("Content-Type", "-") : "-", 210 ] ~ (DEBUG ? [] : [ 211 request ? request.headers.get("Referer", "-") : "-", 212 request ? request.headers.get("User-Agent", "-") : "-", 213 ])).join("\t")); 214 } 215 216 abstract string formatLocalAddress(HttpRequest r); 217 218 /// Idle connections are those which can be closed when the server 219 /// is shutting down. 220 final @property bool idle() 221 { 222 // Technically, with a persistent connection, we never know if 223 // there is a request on the wire on the way to us which we 224 // haven't received yet, so it's not possible to truly know 225 // when the connection is idle and can be safely closed. 226 // However, we do have the ability to do that for 227 // non-persistent connections - assume that a connection is 228 // never idle until we receive (and process) the first 229 // request. Therefore, in deployments where clients require 230 // that an outstanding request is always processed before the 231 // server is shut down, non-persistent connections can be used 232 // (i.e. no attempt to reuse `HttpClient`) to achieve this. 233 if (firstRequest) 234 return false; 235 236 if (requestProcessing) 237 return false; 238 239 foreach (datum; inBuffer) 240 if (datum.length) 241 return false; 242 243 return true; 244 } 245 246 public: 247 /// Send the given HTTP response. 248 final void sendResponse(HttpResponse response) 249 { 250 requestProcessing = false; 251 if (!response) 252 { 253 debug (HTTP) debugLog("sendResponse(null) - generating dummy response"); 254 response = new HttpResponse(); 255 response.status = HttpStatusCode.InternalServerError; 256 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 257 response.data = DataVec(Data("Internal Server Error")); 258 } 259 assert(response.status != 0); 260 261 if (currentRequest) 262 { 263 response.optimizeData(currentRequest.headers); 264 response.sliceData(currentRequest.headers); 265 } 266 267 if ("Content-Length" !in response.headers) 268 response.headers["Content-Length"] = text(response.data.bytes.length); 269 270 sendHeaders(response); 271 272 bool isHead = currentRequest ? currentRequest.method == "HEAD" : false; 273 if (response && response.data.length && !isHead) 274 sendData(response.data[]); 275 276 responseSize = response ? response.data.bytes.length : 0; 277 debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize); 278 279 closeResponse(); 280 281 logRequest(currentRequest, response); 282 } 283 284 /// Send these headers only. 285 /// Low-level alternative to `sendResponse`. 286 final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null) 287 { 288 assert(status, "Unset status code"); 289 290 if (!statusMessage) 291 statusMessage = HttpResponse.getStatusMessage(status); 292 293 StringBuilder respMessage; 294 auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0"; 295 respMessage.put("HTTP/", protocolVersion, " "); 296 297 if (banner && "X-Powered-By" !in headers) 298 headers["X-Powered-By"] = banner; 299 300 if ("Date" !in headers) 301 headers["Date"] = httpTime(Clock.currTime()); 302 303 if ("Connection" !in headers) 304 { 305 if (persistent && protocolVersion=="1.0") 306 headers["Connection"] = "Keep-Alive"; 307 else 308 if (!persistent && protocolVersion=="1.1") 309 headers["Connection"] = "close"; 310 } 311 312 respMessage.put("%d %s\r\n".format(status, statusMessage)); 313 foreach (string header, string value; headers) 314 respMessage.put(header, ": ", value, "\r\n"); 315 316 debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> ")); 317 318 respMessage.put("\r\n"); 319 conn.send(Data(respMessage.get())); 320 } 321 322 /// ditto 323 final void sendHeaders(HttpResponse response) 324 { 325 sendHeaders(response.headers, response.status, response.statusMessage); 326 } 327 328 /// Send this data only. 329 /// Headers should have already been sent. 330 /// Low-level alternative to `sendResponse`. 331 final void sendData(scope Data[] data) 332 { 333 conn.send(data); 334 } 335 336 /// Accept more requests on the same connection? 337 protected bool acceptMore() { return true; } 338 339 /// Finalize writing the response. 340 /// Headers and data should have already been sent. 341 /// Low-level alternative to `sendResponse`. 342 final void closeResponse() 343 { 344 firstRequest = false; 345 if (persistent && acceptMore) 346 { 347 // reset for next request 348 debug (HTTP) debugLog(" Waiting for next request."); 349 conn.handleReadData = &onNewRequest; 350 if (!timeoutActive) 351 { 352 // Give the client time to download large requests. 353 // Assume a minimal speed of 1kb/s. 354 timer.setIdleTimeout(timeout + (responseSize / 1024).seconds); 355 timeoutActive = true; 356 } 357 if (inBuffer.bytes.length) // a second request has been pipelined 358 { 359 debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length); 360 onNewRequest(Data()); 361 } 362 } 363 else 364 { 365 string reason = persistent ? "Server has been shut down" : "Non-persistent connection"; 366 debug (HTTP) debugLog(" Closing connection (%s).", reason); 367 conn.disconnect(reason); 368 } 369 } 370 371 /// Retrieve the remote address of the peer, as a string. 372 abstract @property string remoteAddressStr(HttpRequest r); 373 } 374 375 /// Basic unencrypted HTTP 1.0/1.1 server. 376 class HttpServer 377 { 378 enum defaultTimeout = 30.seconds; /// The default timeout used for incoming connections. 379 380 // public: 381 this(Duration timeout = defaultTimeout) 382 { 383 assert(timeout > Duration.zero); 384 this.timeout = timeout; 385 386 conn = new TcpServer(); 387 conn.handleClose = &onClose; 388 conn.handleAccept = &onAccept; 389 } /// 390 391 /// Listen on the given TCP address and port. 392 /// If port is 0, listen on a random available port. 393 /// Returns the port that the server is actually listening on. 394 ushort listen(ushort port, string addr = null) 395 { 396 port = conn.listen(port, addr); 397 if (log) 398 foreach (address; conn.localAddresses) 399 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 400 return port; 401 } 402 403 /// Listen on the given addresses. 404 void listen(AddressInfo[] addresses) 405 { 406 conn.listen(addresses); 407 if (log) 408 foreach (address; conn.localAddresses) 409 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 410 } 411 412 /// Get listen addresses. 413 @property Address[] localAddresses() { return conn.localAddresses; } 414 415 /// Stop listening, and close idle client connections. 416 void close() 417 { 418 debug(HTTP) stderr.writeln("Shutting down"); 419 if (log) log("Shutting down."); 420 conn.close(); 421 422 debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength); 423 424 // Close idle connections 425 foreach (connection; connections.iterator.array) 426 if (connection.idle && connection.conn.state == ConnectionState.connected) 427 connection.conn.disconnect("HTTP server shutting down"); 428 } 429 430 /// Optional HTTP request log. 431 Logger log; 432 433 /// Single-ended doubly-linked list of active connections 434 SEDListContainer!HttpServerConnection connections; 435 436 /// Callback for when the socket was closed. 437 void delegate() handleClose; 438 /// Callback for an incoming request. 439 void delegate(HttpRequest request, HttpServerConnection conn) handleRequest; 440 441 /// What to send in the `"X-Powered-By"` header. 442 string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)"; 443 444 /// If set, the name of the header which will be used to obtain 445 /// the actual IP of the connecting peer. Useful when this 446 /// `HttpServer` is behind a reverse proxy. 447 string remoteIPHeader; 448 449 protected: 450 TcpServer conn; 451 Duration timeout; 452 453 void onClose() 454 { 455 if (handleClose) 456 handleClose(); 457 } 458 459 IConnection createConnection(TcpConnection tcp) 460 { 461 return tcp; 462 } 463 464 @property string protocol() { return "http"; } 465 466 void onAccept(TcpConnection incoming) 467 { 468 try 469 new HttpServerConnection(this, incoming, createConnection(incoming), protocol); 470 catch (Exception e) 471 { 472 if (log) 473 log("Error accepting connection: " ~ e.msg); 474 if (incoming.state == ConnectionState.connected) 475 incoming.disconnect(); 476 } 477 } 478 } 479 480 /** 481 HTTPS server. Set SSL parameters on ctx after instantiation. 482 483 Example: 484 --- 485 auto s = new HttpsServer(); 486 s.ctx.enableDH(4096); 487 s.ctx.enableECDH(); 488 s.ctx.setCertificate("server.crt"); 489 s.ctx.setPrivateKey("server.key"); 490 --- 491 */ 492 class HttpsServer : HttpServer 493 { 494 SSLContext ctx; /// The SSL context. 495 496 this() 497 { 498 ctx = ssl.createContext(SSLContext.Kind.server); 499 } /// 500 501 protected: 502 override @property string protocol() { return "https"; } 503 504 override IConnection createConnection(TcpConnection tcp) 505 { 506 return ssl.createAdapter(ctx, tcp); 507 } 508 } 509 510 /// Standard TCP-based HTTP server connection. 511 final class HttpServerConnection : BaseHttpServerConnection 512 { 513 TcpConnection tcp; /// The TCP transport. 514 HttpServer server; /// `HttpServer` owning this connection. 515 /// Cached local and remote addresses. 516 Address localAddress, remoteAddress; 517 518 mixin DListLink; 519 520 /// Retrieves the remote peer address, honoring `remoteIPHeader` if set. 521 override @property string remoteAddressStr(HttpRequest r) 522 { 523 if (server.remoteIPHeader) 524 { 525 if (r) 526 if (auto p = server.remoteIPHeader in r.headers) 527 return (*p).split(",")[$ - 1]; 528 529 return "[local:" ~ remoteAddress.toAddrString() ~ "]"; 530 } 531 532 return remoteAddress.toAddrString(); 533 } 534 535 protected: 536 string protocol; 537 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 }