1 module handy_httpd.server; 2 3 import std.stdio; 4 import std.socket; 5 import std.regex; 6 import std.uri; 7 import std.conv; 8 import std.parallelism; 9 import std.string; 10 import std.typecons; 11 import std.array; 12 import std.algorithm; 13 14 import httparsed; 15 16 import handy_httpd.request; 17 import handy_httpd.response; 18 import handy_httpd.handler; 19 20 struct Header { 21 const(char)[] name; 22 const(char)[] value; 23 } 24 25 struct Msg { 26 @safe pure nothrow @nogc: 27 void onMethod(const(char)[] method) { this.method = method; } 28 29 void onUri(const(char)[] uri) { this.uri = uri; } 30 31 int onVersion(const(char)[] ver) { 32 minorVer = parseHttpVersion(ver); 33 return minorVer >= 0 ? 0 : minorVer; 34 } 35 36 void onHeader(const(char)[] name, const(char)[] value) { 37 this.m_headers[m_headersLength].name = name; 38 this.m_headers[m_headersLength++].value = value; 39 } 40 41 void onStatus(int status) { this.status = status; } 42 43 void onStatusMsg(const(char)[] statusMsg) { this.statusMsg = statusMsg; } 44 45 public const(char)[] method; 46 public const(char)[] uri; 47 public int minorVer; 48 public int status; 49 public const(char)[] statusMsg; 50 51 private Header[64] m_headers; 52 private size_t m_headersLength; 53 54 Header[] headers() return { return m_headers[0..m_headersLength]; } 55 } 56 57 /** 58 * A simple HTTP server that accepts requests on a given port and address, and 59 * lets a configured HttpRequestHandler produce a response, to send back to the 60 * client. 61 */ 62 class HttpServer { 63 private Address address; 64 size_t receiveBufferSize; 65 private MsgParser!Msg requestParser; 66 private bool verbose; 67 private HttpRequestHandler handler; 68 private TaskPool workerPool; 69 private bool ready = false; 70 private Socket serverSocket = null; 71 72 this( 73 HttpRequestHandler handler = noOpHandler(), 74 string hostname = "127.0.0.1", 75 ushort port = 8080, 76 size_t receiveBufferSize = 8192, 77 bool verbose = false, 78 size_t workerPoolSize = 25 79 ) { 80 this.address = parseAddress(hostname, port); 81 this.receiveBufferSize = receiveBufferSize; 82 this.verbose = verbose; 83 this.handler = handler; 84 this.requestParser = initParser!Msg(); 85 86 this.workerPool = new TaskPool(workerPoolSize); 87 this.workerPool.isDaemon = true; 88 } 89 90 /** 91 * Starts the server on the calling thread, so that it will begin accepting 92 * HTTP requests. 93 */ 94 public void start() { 95 serverSocket = new TcpSocket(); 96 serverSocket.bind(address); 97 if (verbose) writefln("Bound to address %s", address); 98 serverSocket.listen(100); 99 if (verbose) writeln("Now accepting connections."); 100 ready = true; 101 while (serverSocket.isAlive()) { 102 auto clientSocket = serverSocket.accept(); 103 auto t = task!handleRequest(clientSocket, handler, receiveBufferSize, verbose); 104 workerPool.put(t); 105 } 106 ready = false; 107 } 108 109 /** 110 * Shuts down the server by closing the server socket, if possible. Note 111 * that this is not a blocking call, and the server will shutdown soon. 112 */ 113 public void stop() { 114 if (serverSocket !is null) { 115 serverSocket.close(); 116 } 117 } 118 119 public bool isReady() { 120 return ready; 121 } 122 123 /** 124 * Sets the server's verbosity. 125 * Params: 126 * verbose = Whether to enable verbose output. 127 * Returns: The server instance, for method chaining. 128 */ 129 public HttpServer setVerbose(bool verbose) { 130 this.verbose = verbose; 131 return this; 132 } 133 } 134 135 private void handleRequest(Socket clientSocket, HttpRequestHandler handler, size_t bufferSize, bool verbose) { 136 ubyte[] receiveBuffer = new ubyte[bufferSize]; 137 auto received = clientSocket.receive(receiveBuffer); 138 string data = cast(string) receiveBuffer[0..received]; 139 auto request = parseRequest(data); 140 if (verbose) writefln!"<- %s %s"(request.method, request.url); 141 try { 142 auto response = handler.handle(request); 143 clientSocket.send(response.toBytes()); 144 if (verbose) writefln!"\t-> %d %s"(response.status, response.statusText); 145 } catch (Exception e) { 146 writefln!"An error occurred while handling a request: %s"(e.msg); 147 } 148 clientSocket.close(); 149 } 150 151 private HttpRequest parseRequest(string s) { 152 MsgParser!Msg requestParser = initParser!Msg(); 153 // requestParser.msg.m_headersLength = 0; // Reset the parser headers. 154 int result = requestParser.parseRequest(s); 155 if (result != s.length) { 156 throw new Exception("Error! parse result doesn't match length. " ~ result.to!string); 157 } 158 string[string] headers; 159 foreach (h; requestParser.headers) { 160 headers[h.name] = cast(string) h.value; 161 } 162 string rawUrl = decode(cast(string) requestParser.uri); 163 auto urlAndParams = parseUrlAndParams(rawUrl); 164 165 return HttpRequest( 166 cast(string) requestParser.method, 167 urlAndParams[0], 168 requestParser.minorVer, 169 headers, 170 urlAndParams[1] 171 ); 172 } 173 174 /** 175 * Parses a path and set of query parameters from a raw URL string. 176 * Params: 177 * rawUrl = The raw url containing both path and query params. 178 * Returns: A tuple containing the path and parsed query params. 179 */ 180 private Tuple!(string, string[string]) parseUrlAndParams(string rawUrl) { 181 Tuple!(string, string[string]) result; 182 auto p = rawUrl.indexOf('?'); 183 if (p == -1) { 184 result[0] = rawUrl; 185 result[1] = null; 186 } else { 187 result[0] = rawUrl[0..p]; 188 result[1] = parseQueryString(rawUrl[p..$]); 189 } 190 return result; 191 } 192 193 /** 194 * Parses a set of query parameters from a query string. 195 * Params: 196 * queryString = The raw query string to parse, including the preceding '?' character. 197 * Returns: An associative array containing parsed params. 198 */ 199 private string[string] parseQueryString(string queryString) { 200 string[string] params; 201 if (queryString.length > 1) { 202 string[] paramSections = queryString[1..$].split("&").filter!(s => s.length > 0).array; 203 foreach (paramSection; paramSections) { 204 string paramName; 205 string paramValue; 206 auto p = paramSection.indexOf('='); 207 if (p == -1 || p + 1 == paramSection.length) { 208 paramName = paramSection; 209 paramValue = "true"; 210 } else { 211 paramName = paramSection[0..p]; 212 paramValue = paramSection[p+1..$]; 213 } 214 params[paramName] = paramValue; 215 } 216 } 217 writeln(params); 218 return params; 219 }