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 }