1 /****************************************************************************** 2 3 HTTP connection handler base class for use with the SelectListener. The 4 connection and request handler methods are run in a task. 5 6 To build a HTTP server, create a `TaskHttpConnectionHandler` subclass which 7 implements `handleRequest()`, and use that subclass as connection handler in 8 the `SelectListener`. 9 10 Copyright: 11 Copyright (c) 2017 dunnhumby Germany GmbH. 12 All rights reserved. 13 14 License: 15 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 16 Alternatively, this file may be distributed under the terms of the Tango 17 3-Clause BSD License (see LICENSE_BSD.txt for details). 18 19 ******************************************************************************/ 20 21 module ocean.net.http.TaskHttpConnectionHandler; 22 23 import ocean.meta.types.Qualifiers; 24 25 import ocean.net.server.connection.TaskConnectionHandler; 26 27 /// ditto 28 29 abstract class TaskHttpConnectionHandler : TaskConnectionHandler 30 { 31 import ocean.net.http.HttpRequest; 32 import ocean.net.http.HttpResponse; 33 import ocean.net.http.HttpException; 34 35 import ocean.net.http.HttpConst: HttpResponseCode; 36 import ocean.net.http.consts.HttpMethod: HttpMethod; 37 import ocean.net.http.consts.HeaderFieldNames; 38 39 import ocean.io.select.protocol.generic.ErrnoIOException: IOError, IOWarning; 40 41 import ocean.sys.socket.AddressIPSocket; 42 import ocean.sys.ErrnoException; 43 import ocean.core.Enforce; 44 45 46 /************************************************************************** 47 48 HTTP request message parser 49 50 **************************************************************************/ 51 52 protected HttpRequest request; 53 54 /************************************************************************** 55 56 HTTP response message generator 57 58 **************************************************************************/ 59 60 protected HttpResponse response; 61 62 /************************************************************************** 63 64 Reused exception instance; may be thrown by a subclass as well. 65 66 **************************************************************************/ 67 68 protected HttpException http_exception; 69 70 /************************************************************************** 71 72 Maximum number of requests through the same connection when using 73 persistent connections; 0 disables using persistent connections. 74 75 **************************************************************************/ 76 77 protected uint keep_alive_maxnum = 0; 78 79 /************************************************************************** 80 81 Status code for the case when a required message header parameters are 82 missing. 83 84 **************************************************************************/ 85 86 protected auto default_exception_status_code = HttpResponseCode.InternalServerError; 87 88 /************************************************************************** 89 90 Supported HTTP methods, set in the constructor (only checked for element 91 existence; the actual value is irrelevant) 92 93 **************************************************************************/ 94 95 private bool[HttpMethod] supported_methods; 96 97 /************************************************************************** 98 99 Constructor 100 101 Uses the default request message parser/response generator settings. 102 That means, the request parser will be set up for request methods 103 without a message body, such as GET or HEAD (in contrast to POST or PUT 104 which have a message body). 105 106 Params: 107 finalizer = finalizer callback of the select listener 108 supported_methods = list of supported HTTP methods 109 110 **************************************************************************/ 111 112 protected this ( scope FinalizeDg finalizer, HttpMethod[] supported_methods ... ) 113 { 114 this(finalizer, new HttpRequest, new HttpResponse, supported_methods); 115 } 116 117 /************************************************************************** 118 119 Constructor 120 121 Params: 122 finalizer = finalizer callback of the select listener 123 request = request message parser 124 response = response message generator 125 supported_methods = list of supported HTTP methods 126 127 **************************************************************************/ 128 129 protected this ( scope FinalizeDg finalizer, 130 HttpRequest request, HttpResponse response, 131 HttpMethod[] supported_methods ... ) 132 { 133 134 super(new AddressIPSocket!(), finalizer); 135 136 this.request = request; 137 this.response = response; 138 this.http_exception = request.http_exception; 139 140 foreach (method; supported_methods) 141 { 142 this.supported_methods[method] = true; 143 } 144 145 this.supported_methods.rehash; 146 } 147 148 /*************************************************************************** 149 150 Connection handler method. 151 152 ***************************************************************************/ 153 154 final protected override void handle ( ) 155 { 156 bool keep_alive = false; 157 158 uint n = 0; 159 160 try 161 { 162 do try try 163 { 164 HttpResponseCode status; 165 166 cstring response_msg_body; 167 168 try 169 { 170 this.receiveRequest(); 171 172 keep_alive = n? n < this.keep_alive_maxnum : 173 this.keep_alive_maxnum && this.keep_alive; 174 175 n++; 176 177 status = this.handleRequest(response_msg_body); 178 } 179 catch (HttpParseException e) 180 { 181 this.handleHttpException(e); 182 /* 183 * On request parse error this connection cannot stay alive 184 * because when the request was not completely parsed, its 185 * end and therefore the beginning of the next request is 186 * unknown, so the server-client communication is broken. 187 */ 188 break; 189 } 190 catch (HttpException e) 191 { 192 keep_alive &= this.handleHttpException(e); 193 status = e.status; 194 } 195 catch (HttpServerException e) 196 { 197 keep_alive &= this.handleHttpServerException(e); 198 status = this.default_exception_status_code; 199 } 200 201 this.sendResponse(status, response_msg_body, keep_alive); 202 } 203 finally 204 { 205 this.onResponseSent(); 206 } 207 finally 208 { 209 this.request.reset(); 210 } 211 while (keep_alive); 212 } 213 catch (IOError e) 214 { 215 this.notifyIOException(e, true); 216 } 217 catch (IOWarning e) 218 { 219 this.notifyIOException(e, false); 220 } 221 } 222 223 /************************************************************************** 224 225 Resettable interface method; resets the request. 226 227 **************************************************************************/ 228 229 public void reset ( ) 230 { 231 this.request.reset(); 232 } 233 234 /************************************************************************** 235 236 Handles the request. This method is called while a task is running, 237 see `ocean.task.Task`. 238 239 Params: 240 response_msg_body = body of the response body 241 242 Returns: 243 HTTP status code 244 245 **************************************************************************/ 246 247 abstract protected HttpResponseCode handleRequest ( out cstring response_msg_body ); 248 249 /*************************************************************************** 250 251 Called after handleRequest() has returned and when the response message 252 buffer is no longer referenced or after handleRequest() has thrown an 253 exception. 254 A subclass may override this method to release resources. This is useful 255 especially when a large number of persistent connections is open where 256 each connection is only used sporadically. 257 258 ***************************************************************************/ 259 260 protected void onResponseSent ( ) { } 261 262 /************************************************************************** 263 264 Receives the HTTP request message. 265 266 Throws: 267 - HttpParseException on request message parse error, 268 - HttpException if the request contains parameter values that are 269 invalid, of range or not supported (unsupported HTTP version or 270 method, for example), 271 - HeaderParameterException if a required header parameter is missing 272 or has an invalid value (a misformatted number, for example), 273 - IOWarning when a socket read/write operation results in an 274 end-of-flow or hung-up condition, 275 - IOError when an error event is triggered for a socket. 276 277 **************************************************************************/ 278 279 private void receiveRequest ( ) 280 { 281 this.transceiver.readConsume((void[] data) 282 { 283 size_t consumed = this.request.parse(cast (char[]) data, this.request_msg_body_length); 284 285 return this.request.finished? consumed : data.length + 1; 286 }); 287 288 enforce(this.http_exception.set(HttpResponseCode.NotImplemented), 289 this.request.method in this.supported_methods); 290 } 291 292 /************************************************************************** 293 294 Sends the HTTP response message. 295 296 Params: 297 status = HTTP status 298 response_msg_body = response message body, if any 299 keep_alive = tell the client that this connection will 300 - true: stay persistent or 301 - false: be closed 302 after the response message has been sent. 303 304 Throws: 305 IOError on socket I/O error. 306 307 **************************************************************************/ 308 309 private void sendResponse ( HttpResponseCode status, cstring response_msg_body, bool keep_alive ) 310 { 311 with (this.response) 312 { 313 http_version = this.request.http_version; 314 315 set(HeaderFieldNames.General.Names.Connection, keep_alive? "keep-alive" : "close"); 316 317 this.transceiver.write(render(status, response_msg_body)); 318 this.transceiver.flush(); 319 } 320 } 321 322 /************************************************************************** 323 324 Tells the request message body length. 325 This method should be overridden when a request message body is 326 expected. It is invoked when the message header is completely parsed. 327 The default behaviour is expecting no request message body. 328 329 Returns: 330 the request message body length in bytes (0 indicates that no 331 request message body is expected) 332 333 Throws: 334 HttpException (use the http_exception member) with status set to 335 - status.RequestEntityTooLarge to reject a request whose message 336 body is too long or 337 - an appropriate status to abort request processing and 338 immediately send the response if the message body length 339 cannot be determined, e.g. because required request header 340 parameters are missing. 341 342 **************************************************************************/ 343 344 protected size_t request_msg_body_length ( ) 345 { 346 return 0; 347 } 348 349 /************************************************************************** 350 351 Handles HTTP server exception e which was thrown while parsing the 352 request message or from handleRequest() or request_msg_body_length() and 353 is not a HttpException. 354 A subclass may override this method to be notified when an exception is 355 thrown and decide whether the connection may stay persistent or should 356 be closed after the response has been sent. 357 The default behaviour is allowing the connection to stay persistent. 358 359 Params: 360 e = HTTP server exception e which was thrown while parsing the 361 request message or from handleRequest() or 362 request_msg_body_length() and is not a HttpException. 363 364 Returns: 365 true if the connection may stay persistent or false if it must be 366 closed after the response has been sent. 367 368 **************************************************************************/ 369 370 protected bool handleHttpServerException ( HttpServerException e ) 371 { 372 return true; 373 } 374 375 /************************************************************************** 376 377 Handles HTTP exception e which was thrown while parsing the request 378 message or from handleRequest() or request_msg_body_length(). 379 A subclass may override this method to be notified when an exception is 380 thrown and decide whether the connection may stay persistent or should 381 be closed after the response has been sent. 382 The default behaviour is allowing the connection being persistent unless 383 the status code indicated by the exception is 413: "Request Entity Too 384 Large". 385 386 Params: 387 e = HTTP server exception e which was thrown while parsing the 388 request message or from handleRequest() or 389 request_msg_body_length(). e.status reflects the response status 390 code and may be changed when overriding this method. 391 392 Returns: 393 true if the connection may stay persistent or false if it should be 394 closed after the response has been sent. 395 396 **************************************************************************/ 397 398 protected bool handleHttpException ( HttpException e ) 399 { 400 return e.status != e.status.RequestEntityTooLarge; 401 } 402 403 /************************************************************************** 404 405 Called when an IOWarning or IOError is caught. May be overridden by a 406 subclass to be notified. 407 408 An IOWarning is thrown when a socket read/write operation results in an 409 end-of-flow or hung-up condition, an IOError when an error event is 410 triggered for a socket. 411 412 Params: 413 e = caught IOWarning or IOError 414 is_error = true: e was an IOError, false: e was an IOWarning 415 416 **************************************************************************/ 417 418 protected void notifyIOException ( ErrnoException e, bool is_error ) { } 419 420 /************************************************************************** 421 422 Detects whether the connection should stay persistent or not. 423 424 Returns: 425 true if the connection should stay persistent or false if not 426 427 **************************************************************************/ 428 429 private bool keep_alive ( ) 430 { 431 switch (this.request.http_version) 432 { 433 case this.request.http_version.v1_1: 434 return !this.request.matches("connection", "close"); 435 436 case this.request.http_version.v1_0: 437 default: 438 return this.request.matches("connection", "keep-alive"); 439 } 440 } 441 }