1 module lighttp.util; 2 3 import std.array : Appender; 4 import std.conv : to, ConvException; 5 import std.datetime : DateTime; 6 import std.json : JSONValue; 7 import std.regex : ctRegex; 8 import std.string : toUpper, toLower, split, join, strip, indexOf; 9 import std.traits : EnumMembers; 10 import std.uri : encode, decodeComponent; 11 12 import libasync : NetworkAddress; 13 14 import url : URL; 15 16 /** 17 * Indicates the status of an HTTP response. 18 */ 19 struct Status { 20 21 /** 22 * HTTP response status code. 23 */ 24 uint code = 200; 25 26 /** 27 * Additional short description of the status code. 28 */ 29 string message; 30 31 bool opEquals(uint code) { 32 return this.code == code; 33 } 34 35 bool opEquals(Status status) { 36 return this.opEquals(status.code); 37 } 38 39 /** 40 * Concatenates the status code and the message into 41 * a string. 42 * Example: 43 * --- 44 * assert(Status(200, "OK").toString() == "200 OK"); 45 * --- 46 */ 47 string toString() { 48 return this.code.to!string ~ " " ~ this.message; 49 } 50 51 /** 52 * Creates a status from a known list of codes/messages. 53 * Example: 54 * --- 55 * assert(Status.get(200).message == "OK"); 56 * --- 57 */ 58 public static Status get(uint code) { 59 foreach(statusCode ; [EnumMembers!StatusCodes]) { 60 if(code == statusCode.code) return statusCode; 61 } 62 return Status(code, "Unknown Status Code"); 63 } 64 65 } 66 67 /** 68 * HTTP status codes and their human-readable names. 69 */ 70 enum StatusCodes : Status { 71 72 // informational 73 continue_ = Status(100, "Continue"), 74 switchingProtocols = Status(101, "Switching Protocols"), 75 76 // successful 77 ok = Status(200, "OK"), 78 created = Status(201, "Created"), 79 accepted = Status(202, "Accepted"), 80 nonAuthoritativeContent = Status(203, "Non-Authoritative Information"), 81 noContent = Status(204, "No Content"), 82 resetContent = Status(205, "Reset Content"), 83 partialContent = Status(206, "Partial Content"), 84 85 // redirection 86 multipleChoices = Status(300, "Multiple Choices"), 87 movedPermanently = Status(301, "Moved Permanently"), 88 found = Status(302, "Found"), 89 seeOther = Status(303, "See Other"), 90 notModified = Status(304, "Not Modified"), 91 useProxy = Status(305, "Use Proxy"), 92 switchProxy = Status(306, "Switch Proxy"), 93 temporaryRedirect = Status(307, "Temporary Redirect"), 94 permanentRedirect = Status(308, "Permanent Redirect"), 95 96 // client error 97 badRequest = Status(400, "Bad Request"), 98 unauthorized = Status(401, "Unauthorized"), 99 paymentRequired = Status(402, "Payment Required"), 100 forbidden = Status(403, "Forbidden"), 101 notFound = Status(404, "Not Found"), 102 methodNotAllowed = Status(405, "Method Not Allowed"), 103 notAcceptable = Status(406, "Not Acceptable"), 104 proxyAuthenticationRequired = Status(407, "Proxy Authentication Required"), 105 requestTimeout = Status(408, "Request Timeout"), 106 conflict = Status(409, "Conflict"), 107 gone = Status(410, "Gone"), 108 lengthRequired = Status(411, "Length Required"), 109 preconditionFailed = Status(412, "Precondition Failed"), 110 payloadTooLarge = Status(413, "Payload Too Large"), 111 uriTooLong = Status(414, "URI Too Long"), 112 unsupportedMediaType = Status(415, "Unsupported Media Type"), 113 rangeNotSatisfiable = Status(416, "Range Not Satisfiable"), 114 expectationFailed = Status(417, "Expectation Failed"), 115 116 // server error 117 internalServerError = Status(500, "Internal Server Error"), 118 notImplemented = Status(501, "Not Implemented"), 119 badGateway = Status(502, "Bad Gateway"), 120 serviceUnavailable = Status(503, "Service Unavailable"), 121 gatewayTimeout = Status(504, "Gateway Timeout"), 122 httpVersionNotSupported = Status(505, "HTTP Version Not Supported"), 123 124 } 125 126 /** 127 * Frequently used mime types. 128 */ 129 enum MimeTypes : string { 130 131 // text 132 html = "text/html", 133 javascript = "text/javascript", 134 css = "text/css", 135 text = "text/plain", 136 137 // images 138 png = "image/png", 139 jpeg = "image/jpeg", 140 gif = "image/gif", 141 ico = "image/x-icon", 142 svg = "image/svg+xml", 143 144 // other 145 json = "application/json", 146 zip = "application/zip", 147 bin = "application/octet-stream", 148 149 } 150 151 struct Cookie { 152 153 string name; 154 string value; 155 DateTime expires; 156 size_t maxAge; 157 string domain; 158 string path; 159 bool secure; 160 bool httpOnly; 161 162 string toString() { 163 Appender!string ret; 164 ret.put(name); 165 ret.put("="); 166 ret.put(value); 167 if(expires != DateTime.init) { 168 ret.put(";Expires="); 169 ret.put(expires.toString()); //TODO format correctly 170 } 171 if(maxAge != size_t.init) { 172 ret.put(";Max-Age="); 173 ret.put(maxAge.to!string); 174 } 175 if(domain.length) { 176 ret.put(";Domain="); 177 ret.put(domain); 178 } 179 if(path.length) { 180 ret.put(";Path="); 181 ret.put(path); 182 } 183 if(secure) ret.put(";Secure"); 184 if(httpOnly) ret.put(";HttpOnly"); 185 return ret.data; 186 } 187 188 } 189 190 private enum User { 191 192 client, 193 server 194 195 } 196 197 private enum Type { 198 199 request, 200 response 201 202 } 203 204 /** 205 * Base class for requests and responses. 206 */ 207 abstract class Http { 208 209 /** 210 * Headers utility. 211 */ 212 static struct Headers { 213 214 static struct Header { 215 216 string key; 217 string value; 218 219 } 220 221 private Header[] _headers; 222 size_t[string] _headersIndexes; 223 224 /** 225 * Gets the pointer to a header. 226 * The key is case insensitive. 227 * Example: 228 * --- 229 * headers["Connection"] = "keep-alive"; 230 * assert(("connection" in headers) is ("Connection" in headers)); 231 * --- 232 */ 233 string* opBinaryRight(string op : "in")(string key) pure @safe { 234 if(auto ptr = key.toLower in _headersIndexes) return &_headers[*ptr].value; 235 else return null; 236 } 237 238 /** 239 * Gets the value of a header. 240 * The key is case insensitive. 241 * Example: 242 * --- 243 * headers["Connection"] = "keep-alive"; 244 * assert(header["connection"] == "keep-alive"); 245 * --- 246 */ 247 string opIndex(string key) pure @safe { 248 return _headers[_headersIndexes[key.toLower]].value; 249 } 250 251 /** 252 * Gets the value of a header and returns `defaultValue` if 253 * it does not exist. 254 * Example: 255 * --- 256 * assert(headers.get("Connection", "close") == "close"); 257 * headers["Connection"] = "keep-alive"; 258 * assert(headers.get("Connection", "close") != "close"); 259 * --- 260 */ 261 string get(string key, lazy string defaultValue) pure @safe { 262 if(auto ptr = key.toLower in _headersIndexes) return _headers[*ptr].value; 263 else return defaultValue; 264 } 265 266 /** 267 * Assigns, and overrides if it already exists, a header value. 268 * The key is case-insensitive. 269 * Example: 270 * --- 271 * headers["Connection"] = "keep-alive"; 272 * headers["connection"] = "close"; 273 * assert(headers["Connection"] == "close"); 274 * -- 275 */ 276 string opIndexAssign(string value, string key) pure @safe { 277 if(auto ptr = key.toLower in _headersIndexes) return _headers[*ptr].value = value; 278 else { 279 _headersIndexes[key.toLower] = _headers.length; 280 _headers ~= Header(key, value); 281 return value; 282 } 283 } 284 285 /** 286 * Adds a new key-value pair to the headers without overriding 287 * the existing ones. 288 * Example: 289 * --- 290 * headers.add("Set-Cookie", "a=b"); 291 * headers.add("Set-Cookie", "b=c"); 292 * --- 293 */ 294 void add(string key, string value) pure @safe { 295 if(!(key.toLower in _headersIndexes)) _headersIndexes[key.toLower] = _headers.length; 296 _headers ~= Header(key, value); 297 } 298 299 @property Header[] headers() { 300 return _headers; 301 } 302 303 } 304 305 protected string _method; 306 307 /** 308 * Gets and sets the status of the request/response. 309 * The value can be one of the enum `StatusCodes` or a custom 310 * status using the `Status` struct. 311 */ 312 public Status status; 313 314 /** 315 * Gets the header manager of the request/response. 316 * The available methods are the same of the associative array's 317 * except that keys are case-insensitive. 318 * Example: 319 * --- 320 * headers["Connection"] = "keep-alive"; 321 * assert(headers["connection"] == "keep-alive"); 322 * --- 323 */ 324 public Headers headers; 325 326 private bool _cookiesInit = false; 327 private string[string] _cookies; 328 329 protected string _body; 330 331 /** 332 * Gets the method used for the request. 333 */ 334 public @property string method() pure nothrow @safe @nogc { 335 return _method; 336 } 337 338 /** 339 * Gets the cookies sent in the `Cookie` header in the request. 340 * This property is lazily initialized. 341 */ 342 public @property string[string] cookies() { 343 if(!_cookiesInit) { 344 if(auto cookies = "cookie" in headers) { 345 foreach(cookie ; split(*cookies, ";")) { 346 cookie = cookie.strip; 347 immutable eq = cookie.indexOf("="); 348 if(eq > 0) { 349 _cookies[cookie[0..eq]] = cookie[eq+1..$]; 350 } 351 } 352 } 353 _cookiesInit = true; 354 } 355 return _cookies; 356 } 357 358 /** 359 * Adds a cookie to the response's header. 360 */ 361 public void add(Cookie cookie) { 362 this.headers.add("Set-Cookie", cookie.toString()); 363 } 364 365 /** 366 * Gets the body of the request/response. 367 */ 368 public @property string body_() pure nothrow @safe @nogc { 369 return _body; 370 } 371 372 /** 373 * Sets the body of the request/response. 374 */ 375 public @property string body_(T)(T data) { 376 static if(is(T : string)) { 377 return _body = cast(string)data; 378 } else static if(is(T == JSONValue)) { 379 this.contentType = MimeTypes.json; 380 return _body = data.toString(); 381 } else static if(is(T == JSONValue[string]) || is(T == JSONValue[])) { 382 return body_ = JSONValue(data); 383 } else { 384 return _body = data.to!string; 385 } 386 } 387 388 /// ditto 389 static if(__VERSION__ >= 2078) alias body = body_; 390 391 /** 392 * Sets the response's content-type header. 393 * Example: 394 * --- 395 * response.contentType = MimeTypes.html; 396 * --- 397 */ 398 public @property string contentType(string contentType) pure @safe { 399 return this.headers["Content-Type"] = contentType; 400 } 401 402 public abstract bool parse(string); 403 404 } 405 406 /** 407 * Class for request and response. 408 */ 409 template HttpImpl(User user, Type type) { 410 411 class HttpImpl : Http { 412 413 static if(user == User.client && type == Type.response || user == User.server && type == Type.request) public NetworkAddress address; 414 415 static if(type == Type.request) private URL _url; 416 417 static if(user == User.server && type == Type.response) { 418 419 public void delegate() send; 420 public bool ready = true; 421 422 } 423 424 static if(user == user.client && type == Type.request) public this(string method, string path, string body_="") { 425 _method = method; 426 _url.path = path; 427 _body = body_; 428 } 429 430 /** 431 * Gets the url of the request. `url.path` can be used to 432 * retrive the path and `url.queryParams` to retrive the query 433 * parameters. 434 */ 435 static if(type == Type.request) public @property URL url() pure nothrow @safe @nogc { 436 return _url; 437 } 438 439 static if(user == User.server && type == Type.response) { 440 441 /** 442 * Creates a 3xx redirect response and adds the `Location` field to 443 * the header. 444 * If not specified status code `301 Moved Permanently` will be used. 445 * Example: 446 * --- 447 * http.redirect("/index.html"); 448 * http.redirect(302, "/view.php"); 449 * http.redirect(StatusCodes.seeOther, "/icon.png"); 450 * --- 451 */ 452 public void redirect(Status status, string location) { 453 this.status = status; 454 this.headers["Location"] = location; 455 this.headers["Connection"] = "keep-alive"; 456 } 457 458 /// ditto 459 public void redirect(uint statusCode, string location) { 460 this.redirect(Status.get(statusCode), location); 461 } 462 463 /// ditto 464 public void redirect(string location) { 465 this.redirect(StatusCodes.movedPermanently, location); 466 } 467 468 } 469 470 public override string toString() { 471 this.headers["Content-Length"] = to!string(this.body_.length); 472 static if(type == Type.request) { 473 return encodeHTTP(this.method.toUpper() ~ " " ~ encode(this.url.path ~ this.url.queryParams.toString()) ~ " HTTP/1.1", this.headers.headers, this.body_); 474 } else { 475 return encodeHTTP("HTTP/1.1 " ~ this.status.toString(), this.headers.headers, this.body_); 476 } 477 } 478 479 public override bool parse(string data) { 480 string status; 481 static if(type == Type.request) { 482 if(decodeHTTP(data, status, this.headers, this._body)) { 483 string[] spl = status.split(" "); 484 if(spl.length == 3) { 485 _method = spl[0]; 486 immutable q = spl[1].indexOf("?"); 487 if(q >= 0) { 488 _url.path = decodeComponent(spl[1][0..q]); 489 foreach(param ; spl[1][q+1..$].split("&")) { 490 immutable eq = param.indexOf("="); 491 if(eq >= 0) { 492 _url.queryParams.add(decodeComponent(param[0..eq]), decodeComponent(param[eq+1..$])); 493 } else { 494 _url.queryParams.add(decodeComponent(param), ""); 495 } 496 } 497 } else { 498 _url.path = decodeComponent(spl[1]); 499 } 500 return true; 501 } 502 } 503 } else { 504 if(decodeHTTP(data, status, this.headers, this._body)) { 505 string[] head = status.split(" "); 506 if(head.length >= 3) { 507 try { 508 this.status = Status(to!uint(head[1]), join(head[2..$], " ")); 509 return true; 510 } catch(ConvException) {} 511 } 512 } 513 } 514 return false; 515 } 516 517 } 518 519 } 520 521 alias ClientRequest = HttpImpl!(User.client, Type.request); 522 523 alias ClientResponse = HttpImpl!(User.client, Type.response); 524 525 alias ServerRequest = HttpImpl!(User.server, Type.request); 526 527 alias ServerResponse = HttpImpl!(User.server, Type.response); 528 529 private enum crlf = "\r\n"; 530 531 private string encodeHTTP(string status, Http.Headers.Header[] headers, string content) { 532 Appender!string ret; 533 ret.put(status); 534 ret.put(crlf); 535 foreach(header ; headers) { 536 ret.put(header.key); 537 ret.put(": "); 538 ret.put(header.value); 539 ret.put(crlf); 540 } 541 ret.put(crlf); // empty line 542 ret.put(content); 543 return ret.data; 544 } 545 546 private bool decodeHTTP(string str, ref string status, ref Http.Headers headers, ref string content) { 547 string[] spl = str.split(crlf); 548 if(spl.length > 1) { 549 status = spl[0]; 550 size_t index; 551 while(++index < spl.length && spl[index].length) { // read until empty line 552 auto s = spl[index].split(":"); 553 if(s.length >= 2) { 554 headers.add(s[0].strip, s[1..$].join(":").strip); 555 } else { 556 return false; // invalid header 557 } 558 } 559 content = (index + 1 < spl.length) ? join(spl[index + 1..$], crlf) : string.init; 560 return true; 561 } else { 562 return false; 563 } 564 }