1 /** 2 * Concepts shared between HTTP clients and servers. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Stéphan Kochen <stephan@kochen.nl> 12 * Vladimir Panteleev <ae@cy.md> 13 * Simon Arlott 14 */ 15 16 module ae.net.http.common; 17 18 import core.time; 19 20 import std.algorithm; 21 import std.array; 22 import std.base64; 23 import std.string; 24 import std.conv; 25 import std.ascii; 26 import std.exception; 27 import std.datetime; 28 import std.typecons : tuple; 29 30 import ae.net.ietf.headers; 31 import ae.sys.data; 32 import ae.sys.dataset; 33 import ae.utils.array : amap, afilter, auniq, asort; 34 import ae.utils.text; 35 import ae.utils.time; 36 import zlib = ae.utils.zlib; 37 import gzip = ae.utils.gzip; 38 39 /// Base HTTP message class 40 private abstract class HttpMessage 41 { 42 public: 43 string protocol = "http"; 44 string protocolVersion = "1.0"; 45 Headers headers; 46 DataVec data; 47 SysTime creationTime; 48 49 this() 50 { 51 creationTime = Clock.currTime(); 52 } 53 54 @property Duration age() 55 { 56 return Clock.currTime() - creationTime; 57 } 58 } 59 60 /// HTTP request class 61 class HttpRequest : HttpMessage 62 { 63 public: 64 /// HTTP method, e.g., "GET". 65 string method = "GET"; 66 67 /// If this request is going through a HTTP proxy server, this 68 /// should be set to its address. 69 string proxy; 70 71 this() 72 { 73 } /// 74 75 this(string url) 76 { 77 this.resource = url; 78 } /// 79 80 /// Resource part of URL (everything after the hostname) 81 @property string resource() 82 { 83 return _resource; 84 } 85 86 /// Set the resource part of the URL, or the entire URL. 87 /// Setting the resource to a full URL will fill in the Host header, as well. 88 @property void resource(string value) 89 { 90 _resource = value; 91 92 // applies to both Client/Server as some clients put a full URL in the GET line instead of using a "Host" header 93 string protocol; 94 if (_resource.asciiStartsWith("http://")) 95 protocol = "http"; 96 else 97 if (_resource.asciiStartsWith("https://")) 98 protocol = "https"; 99 100 if (protocol) 101 { 102 this.protocol = protocol; 103 104 value = value[protocol.length+3..$]; 105 auto pathstart = value.indexOf('/'); 106 string _host; 107 if (pathstart == -1) 108 { 109 _host = value; 110 _resource = "/"; 111 } 112 else 113 { 114 _host = value[0..pathstart]; 115 _resource = value[pathstart..$]; 116 } 117 118 auto authEnd = _host.indexOf('@'); 119 if (authEnd != -1) 120 { 121 // Assume HTTP Basic auth 122 import ae.utils.array : bytes; 123 headers["Authorization"] = "Basic " ~ Base64.encode(_host[0 .. authEnd].bytes).assumeUnique; 124 _host = _host[authEnd + 1 .. $]; 125 } 126 127 auto portstart = _host.indexOf(':'); 128 if (portstart != -1) 129 { 130 port = to!ushort(_host[portstart+1..$]); 131 _host = _host[0..portstart]; 132 } 133 host = _host; 134 } 135 } 136 137 /// The hostname, without the port number 138 @property string host() 139 { 140 string _host = headers.get("Host", null); 141 auto colon = _host.lastIndexOf(":"); 142 return colon<0 ? _host : _host[0..colon]; 143 } 144 145 /// Sets the hostname (and the `"Host"` header). 146 /// Must not include a port number. 147 /// Does not change the previously-set port number. 148 @property void host(string _host) 149 { 150 auto _port = this.port; 151 headers["Host"] = _port==protocolDefaultPort ? _host : _host ~ ":" ~ text(_port); 152 } 153 154 /// Retrieves the default port number for the currently set `protocol`. 155 @property ushort protocolDefaultPort() 156 { 157 switch (protocol) 158 { 159 case "http": 160 return 80; 161 case "https": 162 return 443; 163 default: 164 throw new Exception("Unknown protocol: " ~ protocol); 165 } 166 } 167 168 /// Port number, from `"Host"` header. 169 /// Defaults to `protocolDefaultPort`. 170 @property ushort port() 171 { 172 if ("Host" in headers) 173 { 174 string _host = headers["Host"]; 175 auto colon = _host.lastIndexOf(":"); 176 return colon<0 ? protocolDefaultPort : to!ushort(_host[colon+1..$]); 177 } 178 else 179 return _port ? _port : protocolDefaultPort; 180 } 181 182 /// Sets the port number. 183 /// If it is equal to `protocolDefaultPort`, then it is not 184 /// included in the `"Host"` header. 185 @property void port(ushort _port) 186 { 187 if ("Host" in headers) 188 { 189 if (_port == protocolDefaultPort) 190 headers["Host"] = this.host; 191 else 192 headers["Host"] = this.host ~ ":" ~ text(_port); 193 } 194 else 195 this._port = _port; 196 } 197 198 /// Path part of request (until the `'?'`). 199 @property string path() 200 { 201 auto p = resource.indexOf('?'); 202 if (p >= 0) 203 return resource[0..p]; 204 else 205 return resource; 206 } 207 208 /// Query string part of request (atfer the `'?'`). 209 @property string queryString() 210 { 211 auto p = resource.indexOf('?'); 212 if (p >= 0) 213 return resource[p+1..$]; 214 else 215 return null; 216 } 217 218 /// ditto 219 @property void queryString(string value) 220 { 221 auto p = resource.indexOf('?'); 222 if (p >= 0) 223 resource = resource[0..p]; 224 if (value) 225 resource = resource ~ '?' ~ value; 226 } 227 228 /// The query string parameters. 229 @property UrlParameters urlParameters() 230 { 231 return decodeUrlParameters(queryString); 232 } 233 234 /// ditto 235 @property void urlParameters(UrlParameters parameters) 236 { 237 queryString = encodeUrlParameters(parameters); 238 } 239 240 /// URL without resource (protocol, host and port). 241 @property string root() 242 { 243 return protocol ~ "://" ~ host ~ (port==protocolDefaultPort ? null : ":" ~ to!string(port)); 244 } 245 246 /// Full URL. 247 @property string url() 248 { 249 return root ~ resource; 250 } 251 252 /// Full URL without query parameters or fragment. 253 @property string baseURL() 254 { 255 return root ~ resource.findSplit("?")[0]; 256 } 257 258 /// The hostname part of the proxy address, if any. 259 @property string proxyHost() 260 { 261 auto portstart = proxy.indexOf(':'); 262 if (portstart != -1) 263 return proxy[0..portstart]; 264 return proxy; 265 } 266 267 /// The port number of the proxy address if it specified, otherwise `80`. 268 @property ushort proxyPort() 269 { 270 auto portstart = proxy.indexOf(':'); 271 if (portstart != -1) 272 return to!ushort(proxy[portstart+1..$]); 273 return 80; 274 } 275 276 /// Parse the first line in a HTTP request ("METHOD /resource HTTP/1.x"). 277 void parseRequestLine(string reqLine) 278 { 279 enforce(reqLine.length > 10, "Request line too short"); 280 auto methodEnd = reqLine.indexOf(' '); 281 enforce(methodEnd > 0, "Malformed request line"); 282 method = reqLine[0 .. methodEnd]; 283 reqLine = reqLine[methodEnd + 1 .. reqLine.length]; 284 285 auto resourceEnd = reqLine.lastIndexOf(' '); 286 enforce(resourceEnd > 0, "Malformed request line"); 287 resource = reqLine[0 .. resourceEnd]; 288 289 string protocol = reqLine[resourceEnd+1..$]; 290 enforce(protocol.startsWith("HTTP/")); 291 protocolVersion = protocol[5..$]; 292 } 293 294 /// Decodes submitted form data, and returns an AA of values. 295 UrlParameters decodePostData() 296 { 297 auto contentType = headers.get("Content-Type", "").decodeTokenHeader; 298 299 switch (contentType.value) 300 { 301 case "application/x-www-form-urlencoded": 302 return decodeUrlParameters(cast(string)data.joinToHeap()); 303 case "multipart/form-data": 304 return decodeMultipart(data.joinData, contentType.properties.get("boundary", null)) 305 .map!(part => tuple(part.headers.get("Content-Disposition", null).decodeTokenHeader.properties.get("name", null), cast(string)part.data.toHeap())) 306 .UrlParameters; 307 case "": 308 throw new Exception("No Content-Type"); 309 default: 310 throw new Exception("Unknown Content-Type: " ~ contentType.value); 311 } 312 } 313 314 /// Get list of hosts as specified in headers (e.g. X-Forwarded-For). 315 /// First item in returned array is the node furthest away. 316 /// Duplicates are removed. 317 /// Specify socket remote address in remoteHost to add it to the list. 318 deprecated("Insecure, use HttpServer.remoteIPHeader") 319 string[] remoteHosts(string remoteHost = null) 320 { 321 return 322 (headers.get("X-Forwarded-For", null).split(",").amap!(std..string.strip)() ~ 323 headers.get("X-Forwarded-Host", null) ~ 324 remoteHost) 325 .afilter!`a && a != "unknown"`() 326 .auniq(); 327 } 328 329 deprecated unittest 330 { 331 auto req = new HttpRequest(); 332 assert(req.remoteHosts() == []); 333 assert(req.remoteHosts("3.3.3.3") == ["3.3.3.3"]); 334 335 req.headers["X-Forwarded-For"] = "1.1.1.1, 2.2.2.2"; 336 req.headers["X-Forwarded-Host"] = "2.2.2.2"; 337 assert(req.remoteHosts("3.3.3.3") == ["1.1.1.1", "2.2.2.2", "3.3.3.3"]); 338 } 339 340 /// Basic cookie parsing 341 string[string] getCookies() 342 { 343 string[string] cookies; 344 foreach (segment; headers.get("Cookie", null).split(";")) 345 { 346 segment = segment.strip(); 347 auto p = segment.indexOf('='); 348 if (p > 0) 349 cookies[segment[0..p]] = segment[p+1..$]; 350 } 351 return cookies; 352 } 353 354 private: 355 string _resource; 356 ushort _port = 0; // used only when no "Host" in headers; otherwise, taken from there 357 } 358 359 /// HTTP response status codes 360 enum HttpStatusCode : ushort 361 { 362 None = 0, /// 363 364 Continue = 100, /// 365 SwitchingProtocols = 101, /// 366 367 OK = 200, /// 368 Created = 201, /// 369 Accepted = 202, /// 370 NonAuthoritativeInformation = 203, /// 371 NoContent = 204, /// 372 ResetContent = 205, /// 373 PartialContent = 206, /// 374 375 MultipleChoices = 300, /// 376 MovedPermanently = 301, /// 377 Found = 302, /// 378 SeeOther = 303, /// 379 NotModified = 304, /// 380 UseProxy = 305, /// 381 //(Unused) = 306, /// 382 TemporaryRedirect = 307, /// 383 384 BadRequest = 400, /// 385 Unauthorized = 401, /// 386 PaymentRequired = 402, /// 387 Forbidden = 403, /// 388 NotFound = 404, /// 389 MethodNotAllowed = 405, /// 390 NotAcceptable = 406, /// 391 ProxyAuthenticationRequired = 407, /// 392 RequestTimeout = 408, /// 393 Conflict = 409, /// 394 Gone = 410, /// 395 LengthRequired = 411, /// 396 PreconditionFailed = 412, /// 397 RequestEntityTooLarge = 413, /// 398 RequestUriTooLong = 414, /// 399 UnsupportedMediaType = 415, /// 400 RequestedRangeNotSatisfiable = 416, /// 401 ExpectationFailed = 417, /// 402 403 InternalServerError = 500, /// 404 NotImplemented = 501, /// 405 BadGateway = 502, /// 406 ServiceUnavailable = 503, /// 407 GatewayTimeout = 504, /// 408 HttpVersionNotSupported = 505, /// 409 } 410 411 /// HTTP reply class 412 class HttpResponse : HttpMessage 413 { 414 public: 415 HttpStatusCode status; /// HTTP status code 416 string statusMessage; /// HTTP status message, if one was supplied 417 418 /// What Zlib compression level to use when compressing the reply. 419 /// Set to a negative value to disable compression. 420 int compressionLevel = 1; 421 422 /// Returns the message corresponding to the given `HttpStatusCode`, 423 /// or `null` if the code is unknown. 424 static string getStatusMessage(HttpStatusCode code) 425 { 426 switch(code) 427 { 428 case 100: return "Continue"; 429 case 101: return "Switching Protocols"; 430 431 case 200: return "OK"; 432 case 201: return "Created"; 433 case 202: return "Accepted"; 434 case 203: return "Non-Authoritative Information"; 435 case 204: return "No Content"; 436 case 205: return "Reset Content"; 437 case 206: return "Partial Content"; 438 case 300: return "Multiple Choices"; 439 case 301: return "Moved Permanently"; 440 case 302: return "Found"; 441 case 303: return "See Other"; 442 case 304: return "Not Modified"; 443 case 305: return "Use Proxy"; 444 case 306: return "(Unused)"; 445 case 307: return "Temporary Redirect"; 446 447 case 400: return "Bad Request"; 448 case 401: return "Unauthorized"; 449 case 402: return "Payment Required"; 450 case 403: return "Forbidden"; 451 case 404: return "Not Found"; 452 case 405: return "Method Not Allowed"; 453 case 406: return "Not Acceptable"; 454 case 407: return "Proxy Authentication Required"; 455 case 408: return "Request Timeout"; 456 case 409: return "Conflict"; 457 case 410: return "Gone"; 458 case 411: return "Length Required"; 459 case 412: return "Precondition Failed"; 460 case 413: return "Request Entity Too Large"; 461 case 414: return "Request-URI Too Long"; 462 case 415: return "Unsupported Media Type"; 463 case 416: return "Requested Range Not Satisfiable"; 464 case 417: return "Expectation Failed"; 465 466 case 500: return "Internal Server Error"; 467 case 501: return "Not Implemented"; 468 case 502: return "Bad Gateway"; 469 case 503: return "Service Unavailable"; 470 case 504: return "Gateway Timeout"; 471 case 505: return "HTTP Version Not Supported"; 472 default: return null; 473 } 474 } 475 476 /// Set the response status code and message 477 void setStatus(HttpStatusCode code) 478 { 479 status = code; 480 statusMessage = getStatusMessage(code); 481 } 482 483 /// Initializes this `HttpResponse` with the given `statusLine`. 484 final void parseStatusLine(string statusLine) 485 { 486 auto versionEnd = statusLine.indexOf(' '); 487 if (versionEnd == -1) 488 throw new Exception("Malformed status line"); 489 protocolVersion = statusLine[0..versionEnd]; 490 statusLine = statusLine[versionEnd+1..statusLine.length]; 491 492 auto statusEnd = statusLine.indexOf(' '); 493 string statusCode; 494 if (statusEnd >= 0) 495 { 496 statusCode = statusLine[0 .. statusEnd]; 497 statusMessage = statusLine[statusEnd+1..statusLine.length]; 498 } 499 else 500 { 501 statusCode = statusLine; 502 statusMessage = null; 503 } 504 status = cast(HttpStatusCode)to!ushort(statusCode); 505 } 506 507 /// If the data is compressed, return the decompressed data 508 // this is not a property on purpose - to avoid using it multiple times as it will unpack the data on every access 509 // TODO: there is no reason for above limitation 510 Data getContent() 511 { 512 if ("Content-Encoding" in headers && headers["Content-Encoding"]=="deflate") 513 return zlib.uncompress(data[]).joinData(); 514 else 515 if ("Content-Encoding" in headers && headers["Content-Encoding"]=="gzip") 516 return gzip.uncompress(data[]).joinData(); 517 else 518 return data.joinData(); 519 assert(0); 520 } 521 522 protected void compressWithDeflate() 523 { 524 assert(compressionLevel >= 0); 525 data = zlib.compress(data[], zlib.ZlibOptions(compressionLevel)); 526 } 527 528 protected void compressWithGzip() 529 { 530 assert(compressionLevel >= 0); 531 data = gzip.compress(data[], zlib.ZlibOptions(compressionLevel)); 532 } 533 534 /// Called by the server to compress content, if possible/appropriate 535 final package void optimizeData(ref const Headers requestHeaders) 536 { 537 if (compressionLevel < 0) 538 return; 539 auto acceptEncoding = requestHeaders.get("Accept-Encoding", null); 540 if (acceptEncoding && "Content-Encoding" !in headers) 541 { 542 auto contentType = headers.get("Content-Type", null); 543 if (contentType.startsWith("text/") 544 || contentType == "application/json" 545 || contentType == "image/vnd.microsoft.icon" 546 || contentType == "image/svg+xml") 547 { 548 auto supported = parseItemList(acceptEncoding) ~ ["*"]; 549 foreach (method; supported) 550 switch (method) 551 { 552 case "deflate": 553 headers["Content-Encoding"] = method; 554 headers.add("Vary", "Accept-Encoding"); 555 compressWithDeflate(); 556 return; 557 case "gzip": 558 headers["Content-Encoding"] = method; 559 headers.add("Vary", "Accept-Encoding"); 560 compressWithGzip(); 561 return; 562 case "*": 563 if("Content-Encoding" in headers) 564 headers.remove("Content-Encoding"); 565 return; 566 default: 567 break; 568 } 569 assert(0); 570 } 571 } 572 } 573 574 /// Called by the server to apply range request. 575 final package void sliceData(ref const Headers requestHeaders) 576 { 577 if (status == HttpStatusCode.OK) 578 { 579 if ("If-Modified-Since" in requestHeaders && 580 "Last-Modified" in headers && 581 headers["Last-Modified"].parseTime!(TimeFormats.HTTP) <= requestHeaders["If-Modified-Since"].parseTime!(TimeFormats.HTTP)) 582 { 583 setStatus(HttpStatusCode.NotModified); 584 data = null; 585 return; 586 } 587 588 headers["Accept-Ranges"] = "bytes"; 589 auto prange = "Range" in requestHeaders; 590 if (prange && (*prange).startsWith("bytes=")) 591 { 592 auto ranges = (*prange)[6..$].split(",")[0].split("-").map!(s => s.length ? s.to!size_t : size_t.max)().array(); 593 enforce(ranges.length == 2, "Bad range request"); 594 ranges[1]++; 595 auto datum = this.data.bytes; 596 auto datumLength = datum.length; 597 if (ranges[1] == size_t.min) // was not specified (size_t.max overflowed into 0) 598 ranges[1] = datumLength; 599 if (ranges[0] >= datumLength || ranges[0] >= ranges[1] || ranges[1] > datumLength) 600 { 601 //writeError(HttpStatusCode.RequestedRangeNotSatisfiable); 602 setStatus(HttpStatusCode.RequestedRangeNotSatisfiable); 603 data = DataVec(Data(statusMessage)); 604 return; 605 } 606 else 607 { 608 setStatus(HttpStatusCode.PartialContent); 609 this.data = datum[ranges[0]..ranges[1]]; 610 headers["Content-Range"] = "bytes %d-%d/%d".format(ranges[0], ranges[0] + this.data.bytes.length - 1, datumLength); 611 } 612 } 613 } 614 } 615 } 616 617 /// Sets headers to request clients to not cache a response. 618 void disableCache(ref Headers headers) 619 { 620 headers["Expires"] = "Mon, 26 Jul 1997 05:00:00 GMT"; // disable IE caching 621 //headers["Last-Modified"] = "" . gmdate( "D, d M Y H:i:s" ) . " GMT"; 622 headers["Cache-Control"] = "no-cache, must-revalidate"; 623 headers["Pragma"] = "no-cache"; 624 } 625 626 /// Sets headers to request clients to cache a response indefinitely. 627 void cacheForever(ref Headers headers) 628 { 629 headers["Expires"] = httpTime(Clock.currTime().add!"years"(1)); 630 headers["Cache-Control"] = "public, max-age=31536000"; 631 } 632 633 /// Formats a timestamp in the format used by HTTP (RFC 2822). 634 string httpTime(SysTime time) 635 { 636 time.timezone = UTC(); 637 return time.formatTime!(TimeFormats.HTTP)(); 638 } 639 640 import std.algorithm : sort; 641 642 /// Parses a list in the format of "a, b, c;q=0.5, d" and returns 643 /// an array of items sorted by "q" (["a", "b", "d", "c"]) 644 string[] parseItemList(string s) 645 { 646 static struct Item 647 { 648 float q = 1.0; 649 string str; 650 651 this(string s) 652 { 653 auto params = s.split(";"); 654 if (!params.length) return; 655 str = params[0]; 656 foreach (param; params[1..$]) 657 if (param.startsWith("q=")) 658 q = to!float(param[2..$]); 659 } 660 } 661 662 return s 663 .split(",") 664 .amap!(a => Item(strip(a)))() 665 .asort!`a.q > b.q`() 666 .amap!`a.str`(); 667 } 668 669 unittest 670 { 671 assert(parseItemList("a, b, c;q=0.5, d") == ["a", "b", "d", "c"]); 672 } 673 674 // TODO: optimize / move to HtmlWriter 675 deprecated("Use ae.utils.xml.entities") 676 string httpEscape(string str) 677 { 678 string result; 679 foreach(c;str) 680 switch(c) 681 { 682 case '<': 683 result ~= "<"; 684 break; 685 case '>': 686 result ~= ">"; 687 break; 688 case '&': 689 result ~= "&"; 690 break; 691 case '\xDF': // the beta-like symbol 692 result ~= "ß"; 693 break; 694 default: 695 result ~= [c]; 696 } 697 return result; 698 } 699 700 public import ae.net.ietf.url : UrlParameters, encodeUrlParameter, encodeUrlParameters, decodeUrlParameter, decodeUrlParameters; 701 702 /// Represents a part from a multipart/* message. 703 struct MultipartPart 704 { 705 /// The part's individual headers. 706 Headers headers; 707 708 /// The part's contents. 709 Data data; 710 } 711 712 /// Encode a multipart body with the given parts and boundary. 713 Data encodeMultipart(MultipartPart[] parts, string boundary) 714 { 715 Data data; 716 foreach (ref part; parts) 717 { 718 data ~= "--" ~ boundary ~ "\r\n"; 719 foreach (name, value; part.headers) 720 data ~= name ~ ": " ~ value ~ "\r\n"; 721 data ~= "\r\n"; 722 assert((cast(string)part.data.contents).indexOf(boundary) < 0); 723 data ~= part.data; 724 data ~= "\r\n"; 725 } 726 data ~= "--" ~ boundary ~ "--\r\n"; 727 return data; 728 } 729 730 /// Decode a multipart body using the given boundary. 731 MultipartPart[] decodeMultipart(Data data, string boundary) 732 { 733 auto s = cast(char[])data.contents; 734 auto term = "\r\n--" ~ boundary ~ "--\r\n"; 735 enforce(s.endsWith(term), "Bad multipart terminator"); 736 s = s[0..$-term.length]; 737 auto delim = "--" ~ boundary ~ "\r\n"; 738 enforce(s.skipOver(delim), "Bad multipart start"); 739 delim = "\r\n" ~ delim; 740 auto parts = s.split(delim); 741 MultipartPart[] result; 742 foreach (part; parts) 743 { 744 auto segs = part.findSplit("\r\n\r\n"); 745 enforce(segs[1], "Can't find headers in multipart part"); 746 MultipartPart p; 747 foreach (line; segs[0].split("\r\n")) 748 { 749 auto hparts = line.findSplit(":"); 750 p.headers[hparts[0].strip.idup] = hparts[2].strip.idup; 751 } 752 p.data = Data(segs[2]); 753 result ~= p; 754 } 755 return result; 756 } 757 758 unittest 759 { 760 auto parts = [ 761 MultipartPart(Headers(["Foo" : "bar"]), Data.init), 762 MultipartPart(Headers(["Baz" : "quux", "Frob" : "xyzzy"]), Data("Content goes here\xFF")), 763 ]; 764 auto boundary = "abcde"; 765 auto parts2 = parts.encodeMultipart(boundary).decodeMultipart(boundary); 766 assert(parts2.length == parts.length); 767 foreach (p; 0..parts.length) 768 { 769 assert(parts[p].headers == parts2[p].headers); 770 assert(parts[p].data.contents == parts2[p].data.contents); 771 } 772 } 773 774 private bool asciiStartsWith(string s, string prefix) 775 { 776 if (s.length < prefix.length) 777 return false; 778 import std.ascii; 779 foreach (i, c; prefix) 780 if (toLower(c) != toLower(s[i])) 781 return false; 782 return true; 783 }