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