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