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.remoteHosts(remoteAddress.toAddrString())[0], 320 response ? text(cast(ushort)response.status) : "-", 321 format("%9.2f ms", request.age.total!"usecs" / 1000f), 322 request.method, 323 formatAddress(protocol, localAddress, request.host, request.port) ~ request.resource, 324 response ? response.headers.get("Content-Type", "-") : "-", 325 ] ~ (DEBUG ? [] : [ 326 request.headers.get("Referer", "-"), 327 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 respMessage.put("HTTP/", currentRequest.protocolVersion, " "); 351 352 if ("X-Powered-By" !in headers) 353 headers["X-Powered-By"] = "ae.net.http.server (+https://github.com/CyberShadow/ae)"; 354 355 headers["Date"] = httpTime(Clock.currTime()); 356 if (persistent && currentRequest.protocolVersion=="1.0") 357 headers["Connection"] = "Keep-Alive"; 358 else 359 if (!persistent && currentRequest.protocolVersion=="1.1") 360 headers["Connection"] = "close"; 361 else 362 headers.remove("Connection"); 363 364 respMessage.put("%d %s\r\n".format(status, statusMessage)); 365 foreach (string header, string value; headers) 366 respMessage.put(header, ": ", value, "\r\n"); 367 368 debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> ")); 369 370 respMessage.put("\r\n"); 371 conn.send(Data(respMessage.get())); 372 } 373 374 void sendHeaders(HttpResponse response) 375 { 376 sendHeaders(response.headers, response.status, response.statusMessage); 377 } 378 379 void sendResponse(HttpResponse response) 380 { 381 requestProcessing = false; 382 if (!response) 383 { 384 debug (HTTP) debugLog("sendResponse(null) - generating dummy response"); 385 response = new HttpResponse(); 386 response.status = HttpStatusCode.InternalServerError; 387 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 388 response.data = [Data("Internal Server Error")]; 389 } 390 391 response.optimizeData(currentRequest.headers); 392 response.sliceData(currentRequest.headers); 393 394 if ("Content-Length" !in response.headers) 395 response.headers["Content-Length"] = text(response.data.bytes.length); 396 397 sendHeaders(response); 398 399 if (response && response.data.length && currentRequest.method != "HEAD") 400 sendData(response.data); 401 402 debug (HTTP) debugLog("Sent response (%d bytes data)", 403 response ? response.data.bytes.length : 0); 404 405 closeResponse(); 406 407 logRequest(currentRequest, response); 408 } 409 410 void sendData(Data[] data) 411 { 412 conn.send(data); 413 } 414 415 void closeResponse() 416 { 417 if (persistent && server.conn.isListening) 418 { 419 // reset for next request 420 debug (HTTP) debugLog(" Waiting for next request."); 421 conn.handleReadData = &onNewRequest; 422 if (!timeoutActive) 423 { 424 timer.resumeIdleTimeout(); 425 timeoutActive = true; 426 } 427 if (inBuffer.bytes.length) // a second request has been pipelined 428 { 429 debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length); 430 onNewRequest(Data()); 431 } 432 } 433 else 434 { 435 string reason = persistent ? "Server has been shut down" : "Non-persistent connection"; 436 debug (HTTP) debugLog(" Closing connection (%s).", reason); 437 conn.disconnect(reason); 438 } 439 } 440 } 441 442 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0) 443 { 444 string addr = address.toAddrString(); 445 string port = 446 address.addressFamily == AddressFamily.UNIX ? null : 447 logPort ? text(logPort) : 448 address.toPortString(); 449 return protocol ~ "://" ~ 450 (vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~ 451 (port is null || port == "80" ? "" : ":" ~ port); 452 } 453 454 unittest 455 { 456 import ae.net.http.client; 457 import ae.net.http.responseex; 458 459 int[] replies; 460 int closeAfter; 461 462 // Sum "a" from GET and "b" from POST 463 auto s = new HttpServer; 464 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 465 auto get = request.urlParameters; 466 auto post = request.decodePostData(); 467 auto response = new HttpResponseEx; 468 auto result = to!int(get["a"]) + to!int(post["b"]); 469 replies ~= result; 470 conn.sendResponse(response.serveJson(result)); 471 if (--closeAfter == 0) 472 s.close(); 473 }; 474 475 // Test server, client, parameter encoding 476 replies = null; 477 closeAfter = 1; 478 auto port = s.listen(0, "127.0.0.1"); 479 httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null); 480 socketManager.loop(); 481 482 // Test pipelining, protocol errors 483 replies = null; 484 closeAfter = 2; 485 port = s.listen(0, "127.0.0.1"); 486 TcpConnection c = new TcpConnection; 487 c.handleConnect = { 488 c.send(Data( 489 "GET /?a=123456 HTTP/1.1 490 Content-length: 8 491 Content-type: application/x-www-form-urlencoded 492 493 b=654321" ~ 494 "GET /derp HTTP/1.1 495 Content-length: potato 496 497 " ~ 498 "GET /?a=1234567 HTTP/1.1 499 Content-length: 9 500 Content-type: application/x-www-form-urlencoded 501 502 b=7654321")); 503 c.disconnect(); 504 }; 505 c.connect("127.0.0.1", port); 506 507 socketManager.loop(); 508 509 assert(replies == [777777, 8888888]); 510 511 /+ 512 void testFile(string fn) 513 { 514 std.file.write(fn, "42"); 515 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 516 auto response = new HttpResponseEx; 517 conn.sendResponse(response.serveFile(request.resource[1..$], "")); 518 if (--closeAfter == 0) 519 s.close(); 520 }; 521 port = s.listen(0, "127.0.0.1"); 522 closeAfter = 1; 523 httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null); 524 socketManager.loop(); 525 std.file.remove(fn); 526 } 527 528 testFile("http-test.bin"); 529 testFile("http-test.txt"); 530 +/ 531 }