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