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