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 );