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