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