1 /*******************************************************************************
2 
3     Test-suite for the task based HTTP server.
4 
5     This test uses a TCP socket connection to `localhost:8080`.
6 
7     FLAKY: the unittests in this module are a bit flaky, as they rely on making
8     various system calls (`socket`, `connect`, `read/write`, epoll API
9     functions, etc) which could, under certain environmental conditions, fail.
10 
11     Copyright:      Copyright (c) 2017 dunnhumby Germany GmbH. All rights reserved
12 
13     License:
14         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
15         Alternatively, this file may be distributed under the terms of the Tango
16         3-Clause BSD License (see LICENSE_BSD.txt for details).
17 
18 *******************************************************************************/
19 
20 module integrationtest.httpserver.main;
21 
22 import ocean.meta.types.Qualifiers;
23 
24 import ocean.net.http.TaskHttpConnectionHandler;
25 import ocean.net.http.HttpConst : HttpResponseCode;
26 import ocean.net.http.consts.HttpMethod;
27 import ocean.net.http.HttpException;
28 import ocean.net.server.SelectListener;
29 import ocean.task.Task;
30 import ocean.task.Scheduler;
31 import ocean.io.select.EpollSelectDispatcher;
32 import ocean.sys.socket.IPSocket;
33 import ocean.sys.ErrnoException;
34 import core.stdc.errno;
35 import core.stdc.stdlib;
36 
37 /// The payload of the HTTP response.
38 static immutable response_payload = "Hello World!";
39 
40 /// The server address, initialised in `main` and used by both the server and
41 /// the client.
42 IPSocket!().InetAddress srv_address;
43 
44 /// Task-based HTTP connection handler supporting only HTTP GET with
45 /// `response_payload` as the response payload.
46 class TestHttpHandler: TaskHttpConnectionHandler
47 {
48     import ocean.io.Stdout;
49 
50     public this ( scope FinalizeDg finalizer )
51     {
52         super(finalizer, HttpMethod.Get);
53     }
54 
55     override protected HttpResponseCode handleRequest ( out cstring response_msg_body )
56     {
57         response_msg_body = response_payload;
58         return HttpResponseCode.OK;
59     }
60 
61     /// Print errors to make debugging easier. If the test succeeds then none of
62     /// the following methods is called.
63     override protected void notifyIOException ( ErrnoException e, bool is_error )
64     {
65         printEx(e);
66     }
67 
68     override protected bool handleHttpServerException ( HttpServerException e )
69     {
70         printEx(e);
71         return super.handleHttpServerException(e);
72     }
73 
74     override protected bool handleHttpException ( HttpException e )
75     {
76         printEx(e);
77         return super.handleHttpException(e);
78     }
79 
80     static void printEx ( Exception e )
81     {
82         Stderr.formatln("{} @{}:{}", e.message(), e.file, e.line);
83     }
84 }
85 
86 /// HTTP client task. It sends one HTTP GET request and receives and parses the
87 /// response, expecting `response_payload` as the response payload.
88 class ClientTask: Task
89 {
90     import ocean.io.select.protocol.task.TaskSelectTransceiver;
91     import ocean.io.select.protocol.generic.ErrnoIOException: SocketError;
92     import ocean.core.Test: test;
93 
94     TaskSelectTransceiver tst;
95     IPSocket!() socket;
96     SocketError e;
97     Exception e_unhandled = null;
98 
99     this ( )
100     {
101         this.socket = new IPSocket!();
102         this.e = new SocketError(socket);
103         this.tst = new TaskSelectTransceiver(socket, e, e);
104     }
105 
106     override void run ( )
107     {
108         try
109         {
110             this.e.enforce(this.socket.tcpSocket(true) >= 0, "", "socket");
111             connect(this.tst,
112                 (IPSocket!() socket) {return !socket.connect(srv_address.addr);}
113             );
114             this.tst.write("GET / HTTP/1.1\r\nHost: example.net\r\n\r\n");
115             scope parser = new ResponseParser(response_payload.length);
116             this.tst.readConsume(&parser.consume);
117             test!("==")(parser.payload, response_payload);
118         }
119         catch (TaskKillException e)
120             throw e;
121         catch (Exception e)
122             this.e_unhandled = e;
123     }
124 }
125 
126 /*******************************************************************************
127 
128     Runs the server. The server is a simple echo server. It serves just
129     one request and then it exits.
130 
131     Params:
132         socket_path = the unix socket path.
133 
134     Returns:
135         `EXIT_SUCCESS`
136 
137     Throws:
138         `Exception` on error.
139 
140 *******************************************************************************/
141 
142 version (unittest) {} else
143 int main ( )
144 {
145     initScheduler(SchedulerConfiguration.init);
146 
147     auto client = new ClientTask;
148     auto srv_socket = new IPSocket!();
149     alias SelectListener!(TestHttpHandler) Listener;
150     auto listener = new Listener(srv_address("127.0.0.1", 8080), srv_socket);
151 
152     client.terminationHook = {theScheduler.epoll.unregister(listener);};
153 
154     with (theScheduler)
155     {
156         epoll.register(listener);
157         schedule(client);
158         eventLoop();
159     }
160 
161     if (client.e_unhandled)
162         throw client.e_unhandled;
163 
164     return EXIT_SUCCESS;
165 }
166 
167 /// Stores and parses the response data which `readConsume` outputs.
168 static class ResponseParser
169 {
170     /// Everything passed to `consume` is appended here.
171     char[] response;
172     /// The token that denotes the end of the HTTP header and the beginning of
173     /// the payload.
174     static immutable end_of_header_token = "\r\n\r\n";
175     /// true if `end_of_header_token` has been fond in `response`.
176     bool have_payload;
177     /// The index in `response` after `end_of_header_token`.
178     size_t payload_start;
179     /// The expected payload length so that `consume` knows when to finish.
180     /// The preferred way is to use the "Content-Length" HTTP response header
181     /// line, but for simplicity we don't parse the full HTTP header here.
182     size_t payload_length;
183 
184     this ( size_t payload_length )
185     {
186         this.payload_length = payload_length;
187     }
188 
189     /// Returns the payload or `null` if `end_of_header_token` hasn't been
190     /// found yet.
191     char[] payload ( )
192     {
193         return this.have_payload
194             ? this.response[this.payload_start .. $]
195             : null;
196     }
197 
198     /// `readConsume` callback, appends `data` to `this.response`, then looks
199     /// for `end_of_header_token`. Returns `data.length` if finished or
200     /// a greater value if the full payload isn't there yet.
201     size_t consume ( void[] data )
202     {
203         this.response ~= cast(char[])data;
204         if (!this.have_payload)
205         {
206             if (auto end_of_header = cast(char*)memmem(
207                 this.response.ptr, this.response.length,
208                 end_of_header_token.ptr, end_of_header_token.length
209             ))
210             {
211                 this.have_payload = true;
212                 this.payload_start = end_of_header - this.response.ptr
213                     + end_of_header_token.length;
214                 assert(this.payload_start <= this.response.length);
215                 assert(
216                     this.response[
217                         this.payload_start - end_of_header_token.length
218                         .. this.payload_start
219                     ] == end_of_header_token
220                 );
221             }
222         }
223 
224         return data.length + (this.payload.length < this.payload_length);
225     }
226 
227     unittest
228     {
229         {
230             scope parser = new typeof(this)(3);
231             assert(parser.consume("abcde".dup) == 6);
232             assert(parser.consume("fgh\r\n\r\ni".dup) == 9);
233             assert(parser.payload == "i".dup);
234             assert(parser.consume("jk".dup) == 2);
235             assert(parser.payload == "ijk".dup);
236         }
237         {
238             scope parser = new typeof(this)(3);
239             assert(parser.consume("abcd\r".dup) == 6);
240             assert(parser.consume("\n\r\nef".dup) == 6);
241             assert(parser.consume("g".dup) == 1);
242             assert(parser.payload == "efg".dup);
243         }
244         {
245             scope parser = new typeof(this)(3);
246             assert(parser.consume("abc\r\n\r\n".dup) == 8);
247             assert(parser.consume("efg".dup) == 3);
248             assert(parser.payload == "efg".dup);
249         }
250     }
251 }
252 
253 /// glibc function. Looks for b_ptr[0 .. b_len] in a_ptr[0 .. a_len] and returns
254 /// - a pointer to the first occurrence if found or
255 /// - null if not found or
256 /// - a_ptr if b_len == 0.
257 extern (C) inout(void)* memmem (
258     inout(void)* a_ptr, size_t a_len, const(void)* b_ptr, size_t b_len
259 );