1 /** 
2  * Contains the core HTTP server components.
3  */
4 module handy_httpd.server;
5 
6 import std.stdio;
7 import std.socket;
8 import std.regex;
9 import std.container.dlist : DList;
10 import core.sync.semaphore : Semaphore;
11 import core.atomic : atomicLoad;
12 import core.thread.threadgroup : ThreadGroup;
13 
14 import handy_httpd.request;
15 import handy_httpd.response;
16 import handy_httpd.handler;
17 import handy_httpd.parse_utils : parseRequest, Msg;
18 
19 import httparsed : MsgParser, initParser;
20 
21 /** 
22  * A simple HTTP server that accepts requests on a given port and address, and
23  * lets a configured HttpRequestHandler produce a response, to send back to the
24  * client.
25  */
26 class HttpServer {
27     private Address address;
28     private size_t receiveBufferSize;
29     private int connectionQueueSize;
30     private size_t workerPoolSize;
31     private bool verbose;
32     private HttpRequestHandler handler;
33     private shared bool ready = false;
34     private Socket serverSocket = null;
35     private Semaphore requestSemaphore;
36     private DList!Socket requestQueue;
37 
38     this(
39         HttpRequestHandler handler = noOpHandler(),
40         string hostname = "127.0.0.1",
41         ushort port = 8080,
42         size_t receiveBufferSize = 8192,
43         int connectionQueueSize = 100,
44         bool verbose = false,
45         size_t workerPoolSize = 25
46     ) {
47         this.address = parseAddress(hostname, port);
48         this.receiveBufferSize = receiveBufferSize;
49         this.connectionQueueSize = connectionQueueSize;
50         this.workerPoolSize = workerPoolSize;
51         this.verbose = verbose;
52         this.handler = handler;
53     }
54 
55     /**
56      * Will be called before the socket is bound to the address. One can set
57      * special socket options in here by overriding it. 
58      * 
59      * Note: one application would be to add SocketOption.REUSEADDR, in 
60      * order to prevent long TIME_WAIT states preventing quick restarts 
61      * of the server after termination on some systems. Learn more about it
62      * here: https://stackoverflow.com/a/14388707.
63      */
64     protected void configurePreBind(Socket socket) {}
65 
66     /** 
67      * Starts the server on the calling thread, so that it will begin accepting
68      * HTTP requests. Once the server is able to accept requests, `isReady()`
69      * will return true, and will remain true until the server is stopped by
70      * calling `stop()`.
71      */
72     public void start() {
73         serverSocket = new TcpSocket();
74         configurePreBind(serverSocket);
75         serverSocket.bind(this.address);
76         if (this.verbose) writefln!"Bound to address %s"(this.address);
77         serverSocket.listen(this.connectionQueueSize);
78         this.ready = true;
79 
80         // Initialize worker threads.
81         this.requestSemaphore = new Semaphore();
82         ThreadGroup tg = new ThreadGroup();
83         for (int i = 0; i < this.workerPoolSize; i++) {
84             tg.create(&workerThreadFunction);
85         }
86 
87         if (this.verbose) writeln("Now accepting connections.");
88         while (serverSocket.isAlive()) {
89             Socket clientSocket = serverSocket.accept();
90             this.requestQueue.insertBack(clientSocket);
91             this.requestSemaphore.notify();
92         }
93         this.ready = false;
94         
95         // Shutdown worker threads. We call notify() one last time to stop them waiting.
96         for (int i = 0; i < this.workerPoolSize; i++) {
97             this.requestSemaphore.notify();
98         }
99         tg.joinAll();
100     }
101 
102     /** 
103      * Shuts down the server by closing the server socket, if possible. This
104      * will block until all pending requests have been fulfilled.
105      */
106     public void stop() {
107         if (verbose) writeln("Stopping the server.");
108         if (serverSocket !is null) {
109             serverSocket.close();
110         }
111     }
112 
113     /** 
114      * Tells whether the server is ready to receive requests.
115      * Returns: Whether the server is ready to receive requests.
116      */
117     public bool isReady() {
118         return ready;
119     }
120 
121     /** 
122      * Sets the server's verbosity, which determines whether detailed log
123      * messages are printed during runtime.
124      * Params:
125      *   verbose = Whether to enable verbose output.
126      * Returns: The server instance, for method chaining.
127      */
128     public HttpServer setVerbose(bool verbose) {
129         this.verbose = verbose;
130         return this;
131     }
132 
133     /** 
134      * Tells whether the server will give verbose output.
135      * Returns: Whether the server is set to give verbose output.
136      */
137     public bool isVerbose() {
138         return this.verbose;
139     }
140 
141     /** 
142      * Worker function that runs for all worker threads that process incoming
143      * requests. Workers will wait for the requestSemaphore to be notified so
144      * that they can process a request. The worker will stay alive as long as
145      * this server is set as ready.
146      */
147     private void workerThreadFunction() {
148         MsgParser!Msg requestParser = initParser!Msg();
149         ubyte[] receiveBuffer = new ubyte[this.receiveBufferSize];
150         while (atomicLoad(this.ready)) {
151             this.requestSemaphore.wait();
152             if (!this.requestQueue.empty) {
153                 Socket clientSocket = this.requestQueue.removeAny();
154                 auto received = clientSocket.receive(receiveBuffer);
155                 if (received == 0 || received == Socket.ERROR) {
156                     continue; // Skip if we didn't receive valid data.
157                 }
158                 string data = cast(string) receiveBuffer[0..received];
159                 requestParser.msg.reset();
160                 auto request = parseRequest(requestParser, data);
161                 request.server = this;
162                 request.clientSocket = clientSocket;
163                 if (verbose) writefln!"<- %s %s"(request.method, request.url);
164                 try {
165                     HttpResponse response;
166                     response.status = 200;
167                     response.statusText = "OK";
168                     response.clientSocket = clientSocket;
169                     this.handler.handle(request, response);
170                 } catch (Exception e) {
171                     writefln!"An error occurred while handling a request: %s"(e.msg);
172                 }
173                 clientSocket.close();
174             }
175         }
176     }
177 }