1 /* 2 * Copyright (c) 2017-2018 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: Copyright (c) 2017-2018 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/sel-net/sel/net/http.d, sel/net/http.d) 28 */ 29 module sel.net.http; 30 31 import std.array : Appender; 32 import std.conv : to, ConvException; 33 import std.socket : Socket; 34 import std.string : join, split, toUpper, toLower, strip; 35 import std.traits : EnumMembers; 36 37 import sel.net.stream : Stream, TcpStream; 38 39 /** 40 * Stream container for HTTP connection that reads/writes strings 41 * instead of array of bytes. 42 */ 43 class HttpStream { 44 45 private Stream stream; 46 47 public this(Stream stream) { 48 this.stream = stream; 49 } 50 51 public this(Socket socket) { 52 this(new TcpStream(socket)); 53 } 54 55 public ptrdiff_t send(string payload) { 56 return this.stream.send(cast(ubyte[])payload); 57 } 58 59 public string receive() { 60 return cast(string)this.stream.receive(); 61 } 62 63 } 64 65 /** 66 * Indicates the status of an HTTP response. 67 */ 68 struct Status { 69 70 /** 71 * HTTP response status code. 72 */ 73 uint code; 74 75 /** 76 * Additional short description of the status code. 77 */ 78 string message; 79 80 bool opEquals(uint code) { 81 return this.code == code; 82 } 83 84 bool opEquals(Status status) { 85 return this.opEquals(status.code); 86 } 87 88 /** 89 * Concatenates the status code and the message into 90 * a string. 91 * Example: 92 * --- 93 * assert(Status(200, "OK").toString() == "200 OK"); 94 * --- 95 */ 96 string toString() { 97 return this.code.to!string ~ " " ~ this.message; 98 } 99 100 /** 101 * Creates a status from a known list of codes/messages. 102 * Example: 103 * --- 104 * assert(Status.get(200).message == "OK"); 105 * --- 106 */ 107 public static Status get(uint code) { 108 foreach(statusCode ; [EnumMembers!StatusCodes]) { 109 if(code == statusCode.code) return statusCode; 110 } 111 return Status(code, "Unknown Status Code"); 112 } 113 114 } 115 116 /** 117 * HTTP status codes and their human-readable names. 118 */ 119 enum StatusCodes : Status { 120 121 // informational 122 continue_ = Status(100, "Continue"), 123 switchingProtocols = Status(101, "Switching Protocols"), 124 125 // success 126 ok = Status(200, "OK"), 127 created = Status(201, "Created"), 128 accepted = Status(202, "Accepted"), 129 nonAuthoritativeContent = Status(203, "Non-Authoritative Information"), 130 noContent = Status(204, "No Content"), 131 resetContent = Status(205, "Reset Content"), 132 partialContent = Status(206, "Partial Content"), 133 134 // redirection 135 multipleChoices = Status(300, "Multiple Choices"), 136 movedPermanently = Status(301, "Moved Permanently"), 137 found = Status(302, "Found"), 138 seeOther = Status(303, "See Other"), 139 notModified = Status(304, "Not Modified"), 140 useProxy = Status(305, "Use Proxy"), 141 switchProxy = Status(306, "Switch Proxy"), 142 temporaryRedirect = Status(307, "Temporary Redirect"), 143 permanentRedirect = Status(308, "Permanent Redirect"), 144 145 // client errors 146 badRequest = Status(400, "Bad Request"), 147 unauthorized = Status(401, "Unauthorized"), 148 paymentRequired = Status(402, "Payment Required"), 149 forbidden = Status(403, "Forbidden"), 150 notFound = Status(404, "Not Found"), 151 methodNotAllowed = Status(405, "Method Not Allowed"), 152 notAcceptable = Status(406, "Not Acceptable"), 153 proxyAuthenticationRequired = Status(407, "Proxy Authentication Required"), 154 requestTimeout = Status(408, "Request Timeout"), 155 conflict = Status(409, "Conflict"), 156 gone = Status(410, "Gone"), 157 lengthRequired = Status(411, "Length Required"), 158 preconditionFailed = Status(412, "Precondition Failed"), 159 payloadTooLarge = Status(413, "Payload Too Large"), 160 uriTooLong = Status(414, "URI Too Long"), 161 unsupportedMediaType = Status(415, "UnsupportedMediaType"), 162 rangeNotSatisfiable = Status(416, "Range Not Satisfiable"), 163 expectationFailed = Status(417, "Expectation Failed"), 164 165 // server errors 166 internalServerError = Status(500, "Internal Server Error"), 167 notImplemented = Status(501, "Not Implemented"), 168 badGateway = Status(502, "Bad Gateway"), 169 serviceUnavailable = Status(503, "Service Unavailable"), 170 gatewayTimeout = Status(504, "Gateway Timeout"), 171 httpVersionNotSupported = Status(505, "HTTP Version Not Supported"), 172 173 } 174 175 private enum defaultHeaders = ["Server": "sel-net/1.0"]; //TODO load version from .dub/version.json 176 177 /** 178 * Container for a HTTP request. 179 * Example: 180 * --- 181 * Request("GET", "/"); 182 * Request(Request.POST, "/subscribe.php"); 183 * --- 184 */ 185 struct Request { 186 187 enum GET = "GET"; 188 enum POST = "POST"; 189 190 /** 191 * Method used in the request (i.e. GET). Must be uppercase. 192 */ 193 string method; 194 195 /** 196 * Path of the request. It should start with a slash. 197 */ 198 string path; 199 200 /** 201 * HTTP headers of the request. 202 */ 203 string[string] headers; 204 205 /** 206 * Optional raw form data, for POST requests. 207 */ 208 string data; 209 210 /** 211 * If the request was parsed, indicates whether it was in a 212 * valid HTTP format. 213 */ 214 bool valid; 215 216 public this(string method, string path, string[string] headers=defaultHeaders) { 217 this.method = method; 218 this.path = path; 219 this.headers = headers; 220 } 221 222 /** 223 * Creates a get request. 224 * Example: 225 * --- 226 * auto get = Request.get("/index.html", ["Host": "127.0.0.1"]); 227 * assert(get.toString() == "GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n"); 228 * --- 229 */ 230 public static Request get(string path, string[string] headers=defaultHeaders) { 231 return Request(GET, path, headers); 232 } 233 234 /** 235 * Creates a post request. 236 * Example: 237 * --- 238 * auto post = Request.post("/sub.php", ["Connection": "Keep-Alive"], "name=Mark&surname=White"); 239 * assert(post.toString() == "POST /sub.php HTTP/1.1\r\nConnection: Keep-Alive\r\n\r\nname=Mark&surname=White"); 240 * --- 241 */ 242 public static Request post(string path, string[string] headers=defaultHeaders, string data="") { 243 Request request = Request(POST, path, headers); 244 request.data = data; 245 return request; 246 } 247 248 /// ditto 249 public static Request post(string path, string data, string[string] headers=defaultHeaders) { 250 return post(path, headers, data); 251 } 252 253 /** 254 * Encodes the request into a string. 255 * Example: 256 * --- 257 * auto request = Request(Request.GET, "index.html", ["Connection": "Keep-Alive"]); 258 * assert(request.toString() == "GET /index.html HTTP/1.1\r\nConnection: Keep-Alive\r\n"); 259 * --- 260 */ 261 public string toString() { 262 if(this.data.length) this.headers["Content-Length"] = to!string(this.data.length); 263 return encodeHTTP(this.method.toUpper() ~ " " ~ this.path ~ " HTTP/1.1", this.headers, this.data); 264 } 265 266 /** 267 * Parses a string and returns a Request. 268 * If the request is successfully parsed Request.valid will be true. 269 * Please note that every key in the header is converted to lowercase for 270 * an easier search in the associative array. 271 * Example: 272 * --- 273 * auto request = Request.parse("GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Keep-Alive\r\n"); 274 * assert(request.valid); 275 * assert(request.method == Request.GET); 276 * assert(request.headers["Host"] == "127.0.0.1"); 277 * assert(request.headers["Connection"] == "Keep-Alive"); 278 * --- 279 */ 280 public static Request parse(string str) { 281 Request request; 282 string status; 283 if(decodeHTTP(str, status, request.headers, request.data)) { 284 string[] spl = status.split(" "); 285 if(spl.length == 3) { 286 request.valid = true; 287 request.method = spl[0]; 288 request.path = spl[1]; 289 } 290 } 291 return request; 292 } 293 294 } 295 296 /** 297 * Container for an HTTP response. 298 * Example: 299 * --- 300 * Response(200, ["Connection": "Close"], "<b>Hi there</b>"); 301 * Response(404, [], "Cannot find the specified path"); 302 * Response(204); 303 * --- 304 */ 305 struct Response { 306 307 /** 308 * Status of the response. 309 */ 310 Status status; 311 312 /** 313 * HTTP headers of the request. 314 */ 315 string[string] headers; 316 317 /** 318 * Content of the request. Its type should be specified in 319 * the `content-type` field in the headers. 320 */ 321 string content; 322 323 /** 324 * If the response was parsed, indicates whether it was in a 325 * valid HTTP format. 326 */ 327 bool valid; 328 329 public this(Status status, string[string] headers=defaultHeaders, string content="") { 330 this.status = status; 331 this.headers = headers; 332 this.content = content; 333 } 334 335 public this(uint statusCode, string[string] headers=defaultHeaders, string content="") { 336 this(Status.get(statusCode), headers, content); 337 } 338 339 public this(Status status, string content) { 340 this(status, defaultHeaders, content); 341 } 342 343 public this(uint statusCode, string content) { 344 this(statusCode, defaultHeaders, content); 345 } 346 347 /** 348 * Creates a response for an HTTP error an automatically generates 349 * an HTML page to display it. 350 * Example: 351 * --- 352 * Response.error(404); 353 * Response.error(StatusCodes.methodNotAllowed, ["Allow": "GET"]); 354 * --- 355 */ 356 public static Response error(Status status, string[string] headers=defaultHeaders) { 357 immutable message = status.toString(); 358 headers["Content-Type"] = "text/html"; 359 return Response(status, headers, "<!DOCTYPE html><html><head><title>" ~ message ~ "</title></head><body><center><h1>" ~ message ~ "</h1></center><hr><center>" ~ headers.get("Server", "sel-net") ~ "</center></body></html>"); 360 } 361 362 /// ditto 363 public static Response error(uint statusCode, string[string] headers=defaultHeaders) { 364 return error(Status.get(statusCode), headers); 365 } 366 367 /** 368 * Creates a 3xx redirect response and adds the `Location` field to 369 * the header. 370 * If not specified status code `301 Moved Permanently` will be used. 371 * Example: 372 * --- 373 * Response.redirect("/index.html"); 374 * Response.redirect(302, "/view.php"); 375 * Response.redirect(StatusCodes.seeOther, "/icon.png", ["Server": "sel-net"]); 376 * --- 377 */ 378 public static Response redirect(Status status, string location, string[string] headers=defaultHeaders) { 379 headers["Location"] = location; 380 return Response(status, headers); 381 } 382 383 /// ditto 384 public static Response redirect(uint statusCode, string location, string[string] headers=defaultHeaders) { 385 return redirect(Status.get(statusCode), location, headers); 386 } 387 388 /// ditto 389 public static Response redirect(string location, string[string] headers=defaultHeaders) { 390 return redirect(StatusCodes.movedPermanently, location, headers); 391 } 392 393 /** 394 * Encodes the response into a string. 395 * The `Content-Length` header field is created automatically 396 * based on the length of the content field. 397 * Example: 398 * --- 399 * auto response = Response(200, [], "Hi"); 400 * assert(response.toString() == "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nHi"); 401 * --- 402 */ 403 public string toString() { 404 this.headers["Content-Length"] = to!string(this.content.length); 405 return encodeHTTP("HTTP/1.1 " ~ this.status.toString(), this.headers, this.content); 406 } 407 408 /** 409 * Parses a string and returns a Response. 410 * If the response is successfully parsed Response.valid will be true. 411 * Please note that every key in the header is converted to lowercase for 412 * an easier search in the associative array. 413 * Example: 414 * --- 415 * auto response = Response.parse("HTTP/1.1 200 OK\r\nContent-Type: plain/text\r\nContent-Length: 4\r\n\r\ntest"); 416 * assert(response.valid); 417 * assert(response.status == 200); 418 * assert(response.headers["content-type"] == "text/plain"); 419 * assert(response.headers["content-length"] == "4"); 420 * assert(response.content == "test"); 421 * --- 422 */ 423 public static Response parse(string str) { 424 Response response; 425 string status; 426 if(decodeHTTP(str, status, response.headers, response.content)) { 427 string[] head = status.split(" "); 428 if(head.length >= 3) { 429 try { 430 response.status = Status(to!uint(head[1]), join(head[2..$], " ")); 431 response.valid = true; 432 } catch(ConvException) {} 433 } 434 } 435 return response; 436 } 437 438 } 439 440 private enum CR_LF = "\r\n"; 441 442 private string encodeHTTP(string status, string[string] headers, string content) { 443 Appender!string ret; 444 ret.put(status); 445 ret.put(CR_LF); 446 foreach(key, value; headers) { 447 ret.put(key); 448 ret.put(": "); 449 ret.put(value); 450 ret.put(CR_LF); 451 } 452 ret.put(CR_LF); // empty line 453 ret.put(content); 454 return ret.data; 455 } 456 457 private bool decodeHTTP(string str, ref string status, ref string[string] headers, ref string content) { 458 string[] spl = str.split(CR_LF); 459 if(spl.length > 1) { 460 status = spl[0]; 461 size_t index; 462 while(++index < spl.length && spl[index].length) { // read until empty line 463 auto s = spl[index].split(":"); 464 if(s.length >= 2) { 465 headers[s[0].strip.toLower()] = s[1..$].join(":").strip; 466 } else { 467 return false; // invalid header 468 } 469 } 470 content = join(spl[index+1..$], "\r\n"); 471 return true; 472 } else { 473 return false; 474 } 475 }