1 /* 2 Copyright (c) 2023-2024 Andrea Fontana 3 4 Permission is hereby granted, free of charge, to any person 5 obtaining a copy of this software and associated documentation 6 files (the "Software"), to deal in the Software without 7 restriction, including without limitation the rights to use, 8 copy, modify, merge, publish, distribute, sublicense, and/or sell 9 copies of the Software, and to permit persons to whom the 10 Software is furnished to do so, subject to the following 11 conditions: 12 13 The above copyright notice and this permission notice shall be 14 included in all copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 OTHER DEALINGS IN THE SOFTWARE. 24 */ 25 26 /// Everything you need to communicate. 27 module serverino.interfaces; 28 29 import std.conv : to; 30 import std.string : format, representation, indexOf, lastIndexOf, toLower, toStringz, strip; 31 import std.range : empty, assumeSorted; 32 import std.algorithm : map, canFind, splitter, startsWith; 33 import core.thread : Thread; 34 import std.datetime : SysTime, Clock, dur, Duration, DateTime; 35 import std.experimental.logger : log, warning, fatal, critical; 36 import std.socket : Address, Socket, SocketShutdown, socket_t, SocketOptionLevel, SocketOption, Linger, AddressFamily; 37 38 import serverino.databuffer; 39 import serverino.common; 40 import core.stdc.ctype; 41 42 /++ A cookie. Use `Cookie("key", "value")` to create a cookie. You can chain methods. 43 + --- 44 + auto cookie = Cookie("name", "value").path("/").domain("example.com").secure().maxAge(1.days); 45 + output.setCookie(cookie); 46 + --- 47 +/ 48 struct Cookie 49 { 50 /// Cookie SameSite flag 51 enum SameSite 52 { 53 NotSet = "NotSet", /// SameSite flag will not be set. 54 Strict = "Strict", /// Strict value. Cookie will be sent only if the request is from the same site. 55 Lax = "Lax", /// Lax value. Cookie will be sent only if the request is from the same site, except for links from external sites. 56 None = "None" /// None value. Cookie will be sent always. Secure flag will be set. 57 } 58 59 @disable this(); 60 61 /// Build a cookie with name and value 62 @safe @nogc nothrow this(string name, string value) { _name = name; _value = value; _valid = true; } 63 64 /// Set cookie path 65 @safe @nogc nothrow @property ref Cookie path(string path) scope return { _path = path; return this; } 66 67 /// Set cookie domain 68 @safe @nogc nothrow @property ref Cookie domain(string domain) scope return { _domain = domain; return this; } 69 70 /// Set cookie secure flag. This cookie will be sent only thru https. 71 @safe @nogc nothrow @property ref Cookie secure(bool secure = true) scope return { _secure = secure; return this; } 72 73 /// Set cookie httpOnly flag. This cookie will not be accessible from javascript. 74 @safe @nogc nothrow @property ref Cookie httpOnly(bool httpOnly = true) scope return { _httpOnly = httpOnly; return this; } 75 76 /// Set cookie expire time. It overrides maxAge. 77 @safe @nogc nothrow @property ref Cookie expire(SysTime expire) scope return { _maxAge = Duration.zero; _expire = expire; return this; } 78 79 /// Set cookie max age. It overrides expire. 80 @safe @nogc nothrow @property ref Cookie maxAge(Duration maxAge) scope return { _expire = SysTime.init; _maxAge = maxAge; return this; } 81 82 /// Set cookie SameSite flag 83 @safe @nogc nothrow @property ref Cookie sameSite(SameSite sameSite) scope return { _sameSite = sameSite; return this; } 84 85 /// Invalidate cookie. It will be deleted from browser on output.setCookie() request. 86 @safe @nogc nothrow @property ref Cookie invalidate() scope return 87 { 88 _value = string.init; 89 _expire = SysTime.init; 90 _maxAge = Duration.min; 91 return this; 92 } 93 94 private: 95 96 string _name; 97 string _value; 98 string _path; 99 string _domain; 100 bool _secure = false; 101 bool _httpOnly = false; 102 SysTime _expire = SysTime.init; 103 Duration _maxAge = Duration.zero; 104 SameSite _sameSite = SameSite.NotSet; 105 106 bool _valid = false; 107 } 108 109 /// HTTP version used in request 110 enum HttpVersion 111 { 112 HTTP10 = "HTTP/1.0", 113 HTTP11 = "HTTP/1.1" 114 } 115 116 /++ A request from user. Do not store ref to this struct anywhere. 117 + --- 118 + void handler(Request request, Output output) 119 + { 120 + info("You asked for ", request.uri, " with method ", request.method, " and params ", request.get.data); 121 + } 122 + --- 123 +/ 124 struct Request 125 { 126 /// Print request data 127 @safe string dump()(bool html = true) const 128 { 129 import std.string : replace; 130 string d = toString(); 131 132 if (html) d = `<img src="data:image/png;base64,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" alt="" style="display:flex;margin-right:auto; text-align: center;margin-bottom:10px;"><pre style="width:auto;border-radius:4px;padding:10px;background:#eff1ecff;overflow-x:auto">` ~ d.replace("&", "&").replace("<", "<").replace(">", ">") ~ "</pre>"; 133 134 return d; 135 } 136 137 /// ditto 138 @safe string toString()() const 139 { 140 141 string output; 142 output ~= format("Serverino %s.%s.%s\n\n", SERVERINO_MAJOR, SERVERINO_MINOR, SERVERINO_REVISION); 143 output ~= format("Worker: #%s\n", worker.to!string); 144 output ~= format("Build ID: %s\n", buildId); 145 output ~= "\n"; 146 output ~= "Request:\n"; 147 output ~= format(" • Method: %s\n", method.to!string); 148 output ~= format(" • Uri: %s\n", uri); 149 150 if (!_internal._user.empty) 151 output ~= format(" • Authorization: user => `%s` password => `%s`\n",_internal._user,_internal._password.map!(x=>'*')); 152 153 if (!get.data.empty) 154 { 155 output ~= "\nQuery Params:\n"; 156 foreach(k,v; get.data) 157 { 158 output ~= format(" • %s => %s\n", k, v); 159 } 160 } 161 162 if (!body.data.empty) 163 output ~= "\nContent-type: " ~ body.contentType ~ " (size: %s bytes)\n".format(body.data.length); 164 165 if (!post.data.empty) 166 { 167 output ~= "\nPost Params:\n"; 168 foreach(k,v; post.data) 169 { 170 output ~= format(" • %s => %s\n", k, v); 171 } 172 } 173 174 if (!form.data.empty) 175 { 176 output ~= "\nForm Data:\n"; 177 foreach(k,v; form.data) 178 { 179 import std.file : getSize; 180 181 if (v.isFile) output ~= format(" • `%s` (content-type: %s, size: %s bytes, path: %s)\n", k, v.contentType, getSize(v.path), v.path); 182 else output ~= format(" • `%s` (content-type: %s, size: %s bytes)\n", k, v.contentType, v.data.length); 183 } 184 } 185 186 if (!cookie.data.empty) 187 { 188 output ~= "\nCookies:\n"; 189 foreach(k,v; cookie.data) 190 { 191 output ~= format(" • %s => %s\n", k, v); 192 } 193 } 194 195 output ~= "\nHeaders:\n"; 196 foreach(k,v; header.data) 197 { 198 output ~= format(" • %s => %s\n", k, v); 199 } 200 201 return output; 202 } 203 204 /// HTTP methods 205 public enum Method 206 { 207 Get, /// GET 208 Post, /// POST 209 Head, /// HEAD 210 Put, /// PUT 211 Delete, /// DELETE 212 Connect, /// CONNECT 213 Options, /// OPTIONS 214 Patch, /// PATCH 215 Trace, /// TRACE 216 Unknown = -1 /// Unknown method 217 } 218 219 /++ Params from query string 220 + --- 221 + request.get.has("name"); // true for http://localhost:8000/page?name=hello 222 + request.get.read("name", "blah") // returns "Karen" for http://localhost:8000/page?name=Karen 223 + request.get.read("name", "blah") // returns "blah" for http://localhost:8000/page?test=123 224 + --- 225 +/ 226 @safe @nogc @property nothrow public auto get() const { return SafeAccess!string(_internal._get); } 227 228 /++ Params from post if content-type is "application/x-www-form-urlencoded" 229 + --- 230 + request.post.has("name"); 231 + request.post.read("name", "Anonymous") // returns "Anonymous" if name was not posted 232 + --- 233 +/ 234 @safe @nogc @property nothrow public auto post() const { return SafeAccess!string(_internal._post); } 235 236 /++ 237 + The fields from a form. Only if content-type is "multipart/form-data". 238 + --- 239 + FormData fd = request.form.read("form_id"); 240 + if (fd.isFile) 241 + { 242 + // We have a file attached 243 + info("File name: ", fd.filename); 244 + info("File path: ", fd.path); 245 + } 246 + else 247 + { 248 + // We have data inlined 249 + into("Content-Type: ", fd.contentType, " Size: ", fd.data.length, " bytes") 250 + info("Data: ", fd.data); 251 + } 252 --- 253 +/ 254 @safe @nogc @property nothrow public auto form() const { return SafeAccess!FormData(_internal._form); } 255 256 /++ Raw posted data 257 --- 258 import std.experimental.logger; 259 info("Content-Type: ", request.body.contentType, " Size: ", request.body.data.length, " bytes"); 260 --- 261 +/ 262 @safe @nogc @property nothrow public auto body() const { import std.typecons: tuple; return tuple!("data", "contentType")(_internal._data,_internal._postDataContentType); } 263 264 /++ 265 Http headers, always lowercase 266 --- 267 request.header.read("user-agent"); 268 --- 269 +/ 270 @safe @nogc @property nothrow public auto header() const { return SafeAccess!(string)(_internal._header); } 271 272 /// Cookies received from user 273 @safe @nogc @property nothrow public auto cookie() const { return SafeAccess!string(_internal._cookie); } 274 275 /// The uri requested by user 276 @safe @nogc @property nothrow public const(string) uri() const { return _internal._uri; } 277 278 /// Which worker is processing this request? 279 @safe @nogc @property nothrow public auto worker() const { return _internal._worker; } 280 281 /// The host that received the request 282 @safe @nogc @property nothrow public auto host() const { return _internal._host; } 283 284 /// Use at your own risk! Raw data from user. 285 @safe @nogc @property nothrow public auto requestLine() const { return _internal._rawRequestLine; } 286 287 /// Basic http authentication user. Safe only if sent thru https! 288 @safe @nogc @property nothrow public auto user() const { return _internal._user; } 289 290 /// Basic http authentication password. Safe only if sent thru https! 291 @safe @nogc @property nothrow public auto password() const { return _internal._password; } 292 293 /// The sequence of endpoints called so far 294 @safe @nogc @property nothrow public auto route() const { return _internal._route; } 295 296 static package string simpleNotSecureCompileTimeHash(string seed = "") 297 { 298 // Definetely not a secure hash function 299 // Created just to give a unique ID to each build. 300 301 char[16] h = "SimpleNotSecure!"; 302 char[32] h2; 303 304 auto s = (seed ~ "_" ~ __TIMESTAMP__).representation; 305 static immutable hex = "0123456789abcdef".representation; 306 307 ulong sc = 104_059; 308 309 foreach_reverse(idx, c; s) 310 { 311 sc += 1+(cast(ushort)c); 312 sc *= 79_193; 313 h[15-idx%16] ^= cast(char)(sc%255); 314 } 315 316 foreach(idx, c; h) 317 { 318 sc += 1+(cast(ushort)c); 319 sc *= 96_911; 320 h2[idx*2] = hex[(sc%256)/16]; 321 h2[idx*2+1]= hex[(sc%256)%16]; 322 } 323 324 return h2.dup; 325 } 326 327 /// Every time you compile the app this value will change 328 enum buildId = simpleNotSecureCompileTimeHash(); 329 330 /// HTTP method 331 @safe @property @nogc nothrow public Method method() const 332 { 333 switch(_internal._method) 334 { 335 case "GET": return Method.Get; /// GET 336 case "POST": return Method.Post; /// POST 337 case "HEAD": return Method.Head; /// HEAD 338 case "PUT": return Method.Put; /// PUT 339 case "DELETE": return Method.Delete; /// DELETE 340 case "CONNECT": return Method.Connect; /// CONNECT 341 case "OPTIONS": return Method.Options; /// OPTIONS 342 case "PATCH": return Method.Patch; /// PATCH 343 case "TRACE": return Method.Trace; /// TRACE 344 default: return Method.Unknown; /// Unknown 345 } 346 } 347 348 349 package enum ParsingStatus 350 { 351 OK = 0, /// 352 MaxUploadSizeExceeded, /// 353 InvalidBody, /// 354 InvalidRequest /// 355 } 356 357 /++ Simple structure to safely access data from an associative array. 358 + --- 359 + // request.cookie returns a SafeAccess!string 360 + // get a cookie named "user", default to "anonymous" 361 + auto user = request.cookie.read("user", "anonymous"); 362 + 363 + // Access the underlying AA 364 + auto data = request.cookie.data; 365 + foreach(k,v; data) info(k, " => ", v); 366 + --- 367 +/ 368 struct SafeAccess(T) 369 { 370 public: 371 372 /++ 373 Read a value. Return defaultValue if k does not exist. 374 --------- 375 request.cookie.read("user", "anonymous"); 376 --------- 377 +/ 378 @safe @nogc nothrow auto read(string key, T defaultValue = T.init) const 379 { 380 auto v = key in _data; 381 382 if (v == null) return defaultValue; 383 return *v; 384 } 385 386 /// Check if value exists 387 @safe @nogc nothrow bool has(string key) const 388 { 389 return (key in _data) != null; 390 } 391 392 /// Return the underlying AA 393 @safe @nogc nothrow @property auto data() const { return _data; } 394 395 auto toString() const { return _data.to!string; } 396 397 private: 398 399 @safe @nogc nothrow private this(const ref T[string] data) { _data = data; } 400 const T[string] _data; 401 } 402 403 /++ Data sent through multipart/form-data. 404 +/ 405 public struct FormData 406 { 407 string name; /// Form field name 408 409 string contentType; /// Content type 410 char[] data; /// Data, if inlined (empty if `isFile() == true`) 411 412 string filename; /// We have a file attached. Its name. 413 string path; /// If we have a file attached, here it is saved. 414 415 /// Is it a file or is data inlined? 416 @safe @nogc nothrow @property bool isFile() const { return !filename.empty; } 417 } 418 419 package struct RequestImpl 420 { 421 void process() 422 { 423 import std.algorithm : splitter; 424 import std.regex : match, ctRegex; 425 import std.uri : decodeComponent; 426 import std.string : translate, split, strip; 427 import std.process : thisProcessID; 428 429 static string myPID; 430 if (myPID.length == 0) myPID = thisProcessID().to!string; 431 432 foreach(ref h; _rawHeaders.splitter("\r\n")) 433 { 434 auto first = h.indexOf(":"); 435 if (first < 0) continue; 436 _header[h[0..first]] = h[first+1..$]; 437 } 438 439 _worker = myPID; 440 441 if ("host" in _header) _host = _header["host"]; 442 443 // Read get params 444 if (!_rawQueryString.empty) 445 foreach(m; match(_rawQueryString, ctRegex!("([^=&]*)(?:=([^&]*))?&?", "g"))) 446 _get[m.captures[1].decodeComponent] = translate(m.captures[2],['+':' ']).decodeComponent; 447 448 // Read post params 449 try 450 { 451 import std.algorithm : filter, endsWith, countUntil; 452 import std.range : drop, takeOne; 453 import std.array : array; 454 455 if (_method == "POST") 456 { 457 auto contentType = "application/octet-stream"; 458 459 if ("content-type" in _header && !_header["content-type"].empty) 460 contentType = _header["content-type"]; 461 462 auto cSplitted = contentType.splitter(";"); 463 464 _postDataContentType = cSplitted.front.toLower.strip; 465 cSplitted.popFront(); 466 467 if (_postDataContentType == "application/x-www-form-urlencoded") 468 { 469 import std.stdio; 470 471 // Ok that's easy... 472 foreach(m; match(_data, ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g"))) 473 _post[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent; 474 } 475 else if (_postDataContentType == "multipart/form-data") 476 { 477 // The hard way 478 string boundary; 479 480 // Usually they declare the boundary 481 if (!cSplitted.empty) 482 { 483 boundary = cSplitted.front.strip; 484 485 if (boundary.startsWith("boundary=")) boundary = boundary[9..$].strip().strip(`"`); 486 else boundary = string.init; 487 } 488 489 // Sometimes they write it on the first line. 490 if (boundary.empty) 491 { 492 auto lines = _data.splitter("\r\n").filter!(x => !x.empty).takeOne; 493 494 if (!lines.empty) 495 { 496 auto firstLine = lines.front; 497 498 if (firstLine.length < 512 && firstLine.startsWith("--")) 499 boundary = firstLine[2..$].to!string; 500 } 501 } 502 503 if (!boundary.empty) 504 { 505 bool lastBoundary = false; 506 foreach(chunk; _data.splitter("--" ~ boundary)) 507 { 508 // All chunks must end with \r\n 509 if (!chunk.endsWith("\r\n")) 510 continue; 511 512 // The last one is --boundary-- 513 chunk = chunk[0..$-2]; 514 if (chunk.length == 2 && chunk == "--") 515 { 516 lastBoundary = true; 517 break; 518 } 519 520 // All chunks must start with \r\n 521 if (!chunk.startsWith("\r\n")) 522 continue; 523 524 chunk = chunk[2..$]; 525 526 bool done = false; 527 string content_disposition; 528 string content_type = "text/plain"; 529 530 // "Headers" 531 while(true) 532 { 533 534 auto nextLine = chunk.countUntil("\n"); 535 auto line = chunk[0..nextLine]; 536 537 // All lines must end with \r\n 538 if (!line.endsWith("\r")) 539 break; 540 541 line = line[0..$-1]; 542 543 if (line == "\r") 544 { 545 done = true; 546 break; 547 } 548 else if (line.toLower.startsWith("content-disposition:")) 549 content_disposition = line.to!string; 550 else if (line.toLower.startsWith("content-type:")) 551 content_type = line.to!string["content-type:".length..$].strip; 552 else break; 553 554 if (nextLine + 1 >= chunk.length) 555 break; 556 557 chunk = chunk[nextLine+1 .. $]; 558 } 559 560 // All chunks must start with \r\n 561 if (!chunk.startsWith("\r\n")) continue; 562 chunk = chunk[2..$]; 563 564 if (content_disposition.empty) continue; 565 566 // content-disposition fields 567 auto form_data_raw = content_disposition.splitter(";").drop(1).map!(x=>x.split("=")).array; 568 string[string] form_data; 569 570 foreach(f; form_data_raw) 571 { 572 if (f.length > 1) 573 { 574 auto k = f[0].strip; 575 auto v = f[1].strip; 576 577 if (v.length < 2) continue; 578 form_data[k] = v[1..$-1]; 579 } 580 } 581 582 if ("name" !in form_data) continue; 583 584 FormData fd; 585 fd.name = form_data["name"]; 586 fd.contentType = content_type; 587 588 if ("filename" !in form_data) fd.data = chunk.to!(char[]); 589 else 590 { 591 fd.filename = form_data["filename"]; 592 import core.atomic : atomicFetchAdd; 593 import std.path : extension, buildPath; 594 import std.file : tempDir; 595 596 597 string now = Clock.currTime.toUnixTime.to!string; 598 string uploadId = "%05d".format(atomicFetchAdd(_uploadId, 1)); 599 string path = tempDir.buildPath("upload_%s_%s_%s%s".format(now, myPID, uploadId, extension(fd.filename))); 600 601 fd.path = path; 602 603 import std.file : write; 604 write(path, chunk); 605 } 606 607 _form[fd.name] = fd; 608 609 } 610 611 if (!lastBoundary) 612 { 613 debug warning("Can't parse multipart/form-data content"); 614 _parsingStatus = ParsingStatus.InvalidBody; 615 616 // Something went wrong with parsing, we ignore data. 617 clearFiles(); 618 _post = typeof(_post).init; 619 _form = typeof(_form).init; 620 } 621 622 } 623 } 624 } 625 } 626 catch (Exception e) { _parsingStatus = ParsingStatus.InvalidBody; } 627 628 // Read cookies 629 if ("cookie" in _header) 630 foreach(m; match(_header["cookie"], ctRegex!("([^=]+)=([^;]+)?;? ?", "g"))) 631 _cookie[m.captures[1].decodeComponent] = m.captures[2].decodeComponent; 632 633 if ("authorization" in _header) 634 { 635 import std.base64 : Base64, Base64Exception; 636 import std.string : indexOf; 637 auto auth = _header["authorization"]; 638 639 if (auth.length > 6 && auth[0..6].toLower == "basic ") 640 { 641 try 642 { 643 auth = (cast(char[])Base64.decode(auth[6..$])).to!string; 644 auto delim = auth.indexOf(":"); 645 646 if (delim < 0) _user = auth; 647 else 648 { 649 _user = auth[0..delim]; 650 651 if (delim < auth.length-1) 652 _password = auth[delim+1..$]; 653 } 654 655 } 656 catch(Base64Exception e) 657 { 658 _user = string.init; 659 _password = string.init; 660 debug warning("Authorization header ignored. Error decoding base64."); 661 } 662 } 663 } 664 665 666 } 667 668 void clearFiles() { 669 import std.file : remove, exists; 670 foreach(f; _form) 671 try {if (exists(f.path)) remove(f.path); } catch(Exception e) { } 672 } 673 674 ~this() { clearFiles(); } 675 676 677 char[] _data; 678 string[string] _get; 679 string[string] _post; 680 string[string] _header; 681 string[string] _cookie; 682 683 string _uri; 684 string _method; 685 string _host; 686 string _postDataContentType; 687 string _worker; 688 string _user; 689 string _password; 690 size_t _uploadId; 691 692 string _rawQueryString; 693 string _rawHeaders; 694 string _rawRequestLine; 695 696 string[] _route; 697 698 HttpVersion _httpVersion; 699 700 FormData[string] _form; 701 ParsingStatus _parsingStatus = ParsingStatus.OK; 702 703 size_t _requestId; 704 705 void clear() 706 { 707 clearFiles(); 708 709 _form = null; 710 _data = null; 711 _get = null; 712 _post = null; 713 _header = null; 714 _cookie = null; 715 _uri = string.init; 716 717 _method = string.init; 718 _host = string.init; 719 _user = string.init; 720 _password = string.init; 721 _httpVersion = HttpVersion.HTTP10; 722 723 _rawQueryString = string.init; 724 _rawHeaders = string.init; 725 _rawRequestLine = string.init; 726 727 _postDataContentType = string.init; 728 729 _parsingStatus = ParsingStatus.OK; 730 731 _route.length = 0; 732 _route.reserve(10); 733 } 734 } 735 736 package RequestImpl* _internal; 737 } 738 739 /++ A response to user. Default content-type is "text/html". 740 + --- 741 + // Set status code to 404 742 + output.status = 404 743 + 744 + // Send a response. Same as: output.write("Sorry, page not found."); 745 + output ~= "Sorry, page not found."; 746 + --- 747 +/ 748 struct Output 749 { 750 751 public: 752 753 /// Override timeout for this request 754 @safe void setTimeout(Duration max) { _internal._timeout = max; } 755 756 /++ 757 + Add a http header. 758 + You can't set `content-length`, `status` or `transfer-encoding` headers. They are managed by serverino internally. 759 + --- 760 + // Set content-type to json, default is text/html 761 + output.addHeader("content-type", "application/json"); 762 + output.addHeader("expire", 3.days); 763 + --- 764 +/ 765 @safe void addHeader(in string key, in string value) 766 { 767 string k = key.toLower; 768 769 debug if (["content-length", "status", "transfer-encoding"].canFind(k)) 770 { 771 warning("You can't set `", key, "` header. It's managed by serverino internally."); 772 if (k == "status") warning("Use `output.status = XXX` instead."); 773 return; 774 } 775 776 _internal._dirty = true; 777 _internal._headers ~= KeyValue(k, value); 778 } 779 780 /// Ditto 781 @safe void addHeader(in string key, in Duration dur) { addHeader(key, Clock.currTime + dur); } 782 783 /// Ditto 784 @safe void addHeader(in string key, in SysTime time) { addHeader(key, toHTTPDate(time)); } 785 786 /// You can reply with a file. Automagical mime-type detection. 787 bool serveFile(const string path, bool guessMime = true) 788 { 789 _internal._dirty = true; 790 791 import std.file : exists, getSize, isFile; 792 import std.path : extension, baseName; 793 import std.stdio : File; 794 795 if (!exists(path) || !isFile(path)) 796 { 797 warning("Trying to serve `", baseName(path) ,"`, but it doesn't exists."); 798 return false; 799 } 800 801 size_t fs = path.getSize().to!size_t; 802 _internal._headers ~= KeyValue("content-length", fs.to!string); 803 804 if (!_internal._headers.canFind!(x=>x.key == "content-type")) 805 { 806 string header = "application/octet-stream"; 807 808 if (guessMime) 809 { 810 static immutable mimes = 811 [ 812 ".html" : "text/html", ".htm" : "text/html", ".shtml" : "text/html", ".css" : "text/css", ".xml" : "text/xml", 813 ".gif" : "image/gif", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript", 814 ".atom" : "application/atom+xml", ".rss" : "application/rss+xml", ".mml" : "text/mathml", ".txt" : "text/plain", 815 ".jad" : "text/vnd.sun.j2me.app-descriptor", ".wml" : "text/vnd.wap.wml", ".htc" : "text/x-component", 816 ".png" : "image/png", ".tif" : "image/tiff", ".tiff" : "image/tiff", ".wbmp" : "image/vnd.wap.wbmp", 817 ".ico" : "image/x-icon", ".jng" : "image/x-jng", ".bmp" : "image/x-ms-bmp", ".svg" : "image/svg+xml", 818 ".svgz" : "image/svg+xml", ".webp" : "image/webp", ".woff" : "application/font-woff", 819 ".jar" : "application/java-archive", ".war" : "application/java-archive", ".ear" : "application/java-archive", 820 ".json" : "application/json", ".hqx" : "application/mac-binhex40", ".doc" : "application/msword", 821 ".pdf" : "application/pdf", ".ps" : "application/postscript", ".eps" : "application/postscript", 822 ".ai" : "application/postscript", ".rtf" : "application/rtf", ".m3u8" : "application/vnd.apple.mpegurl", 823 ".xls" : "application/vnd.ms-excel", ".eot" : "application/vnd.ms-fontobject", 824 ".ppt" : "application/vnd.ms-powerpoint", ".wmlc" : "application/vnd.wap.wmlc", 825 ".kml" : "application/vnd.google-earth.kml+xml", ".kmz" : "application/vnd.google-earth.kmz", 826 ".7z" : "application/x-7z-compressed", ".cco" : "application/x-cocoa", 827 ".jardiff" : "application/x-java-archive-diff", ".jnlp" : "application/x-java-jnlp-file", 828 ".run" : "application/x-makeself", ".pl" : "application/x-perl", ".pm" : "application/x-perl", 829 ".prc" : "application/x-pilot", ".pdb" : "application/x-pilot", ".rar" : "application/x-rar-compressed", 830 ".rpm" : "application/x-redhat-package-manager", ".sea" : "application/x-sea", 831 ".swf" : "application/x-shockwave-flash", ".sit" : "application/x-stuffit", ".tcl" : "application/x-tcl", 832 ".tk" : "application/x-tcl", ".der" : "application/x-x509-ca-cert", ".pem" : "application/x-x509-ca-cert", 833 ".crt" : "application/x-x509-ca-cert", ".xpi" : "application/x-xpinstall", ".xhtml" : "application/xhtml+xml", 834 ".xspf" : "application/xspf+xml", ".zip" : "application/zip", ".bin" : "application/octet-stream", 835 ".exe" : "application/octet-stream", ".dll" : "application/octet-stream", ".deb" : "application/octet-stream", 836 ".dmg" : "application/octet-stream", ".iso" : "application/octet-stream", ".img" : "application/octet-stream", 837 ".msi" : "application/octet-stream", ".msp" : "application/octet-stream", ".msm" : "application/octet-stream", 838 ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 839 ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 840 ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".mid" : "audio/midi", 841 ".midi" : "audio/midi", ".kar" : "audio/midi", ".mp3" : "audio/mpeg", ".ogg" : "audio/ogg", ".m4a" : "audio/x-m4a", 842 ".ra" : "audio/x-realaudio", ".3gpp" : "video/3gpp", ".3gp" : "video/3gpp", ".ts" : "video/mp2t", ".mp4" : "video/mp4", 843 ".mpeg" : "video/mpeg", ".mpg" : "video/mpeg", ".mov" : "video/quicktime", ".webm" : "video/webm", ".flv" : "video/x-flv", 844 ".m4v" : "video/x-m4v", ".mng" : "video/x-mng", ".asx" : "video/x-ms-asf", ".asf" : "video/x-ms-asf", 845 ".wmv" : "video/x-ms-wmv", ".avi" : "video/x-msvideo" 846 ]; 847 848 if (path.extension in mimes) 849 header = mimes[path.extension]; 850 } 851 852 addHeader("content-type", header); 853 } 854 855 ubyte[] buffer; 856 buffer.length = fs; 857 File toSend = File(path, "r"); 858 859 auto bytesRead = toSend.rawRead(buffer); 860 861 if (bytesRead.length != fs) 862 { 863 sendData("HTTP/1.0 500 Internal server error\r\n"); 864 return false; 865 } 866 867 //sendHeaders(); 868 sendData(bytesRead); 869 return true; 870 } 871 872 873 /++ Add or edit a cookie. 874 + To delete a cookie, use cookie.invalidate() and then setCookie(cookie) 875 +/ 876 @safe void setCookie(Cookie c) 877 { 878 _internal._dirty = true; 879 880 if (!c._valid) 881 throw new Exception("Invalid cookie. Please use Cookie(name, value) to create a valid cookie."); 882 883 _internal._cookies ~= c; 884 } 885 886 887 /// Read status. 888 @safe @nogc @property nothrow ushort status() { return _internal._status; } 889 890 /// Set response status. 200 by default. 891 @safe @property void status(ushort status) 892 { 893 _internal._dirty = true; 894 _internal._status = status; 895 } 896 897 /** 898 * Syntax sugar. Easier way to write output. 899 * Example: 900 * -------------------- 901 * output ~= "Hello world"; 902 * -------------------- 903 */ 904 void opOpAssign(string op, T)(T data) if (op == "~") { write(data.to!string); } 905 906 /** 907 * Mute/unmute output. If false, serverino will not send any data to user. 908 * -------------------- 909 * output = false; // Mute the output. 910 * output ~= "Hello world"; // Serverino will not send this to user. 911 * -------------------- 912 */ 913 void opAssign(in bool v) { 914 _internal._sendBody = v; 915 } 916 917 /// Write data to output. You can write as many times as you want. 918 @safe void write(string data = string.init) { write(data.representation); } 919 920 /// Ditto 921 @safe void write(in void[] data) 922 { 923 _internal._dirty = true; 924 sendData(data); 925 } 926 927 struct KeyValue 928 { 929 @safe this (in string key, in string value) { this.key = key; this.value = value; } 930 string key; 931 string value; 932 } 933 934 package: 935 936 @safe void sendData(const string data) { sendData(data.representation); } 937 @safe void sendData(bool force = false)(const void[] data) 938 { 939 _internal._dirty = true; 940 _internal._sendBuffer.append(cast(const char[])data); 941 } 942 943 @safe static string toHTTPDate(SysTime t) { 944 string[] mm = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; 945 string[] dd = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; 946 947 SysTime gmt = t.toUTC(); 948 949 return format("%s, %s %s %s %s:%s:%s GMT", 950 dd[gmt.dayOfWeek], gmt.day, mm[gmt.month], gmt.year, 951 gmt.hour, gmt.minute, gmt.second 952 ); 953 } 954 955 struct OutputImpl 956 { 957 Cookie[] _cookies; 958 KeyValue[] _headers; 959 bool _keepAlive; 960 string _httpVersion; 961 ushort _status; 962 Duration _timeout; 963 bool _dirty; 964 size_t _requestId; 965 DataBuffer!char _sendBuffer; 966 DataBuffer!char _headersBuffer; 967 string _buffer; 968 Socket _channel; 969 bool _flushed; 970 bool _sendBody; 971 972 973 @safe void buildHeaders() 974 { 975 import std.uri : encodeComponent; 976 import std.array : appender; 977 978 _headersBuffer.reserve(1024, true); 979 _headersBuffer.clear(); 980 981 immutable string[short] StatusCode = 982 [ 983 200: "OK", 201 : "Created", 202 : "Accepted", 203 : "Non-Authoritative Information", 204 : "No Content", 205 : "Reset Content", 206 : "Partial Content", 984 985 300 : "Multiple Choices", 301 : "Moved Permanently", 302 : "Found", 303 : "See Other", 304 : "Not Modified", 305 : "Use Proxy", 307 : "Temporary Redirect", 986 987 400 : "Bad Request", 401 : "Unauthorized", 402 : "Payment Required", 403 : "Forbidden", 404 : "Not Found", 405 : "Method Not Allowed", 988 406 : "Not Acceptable", 407 : "Proxy Authentication Required", 408 : "Request Timeout", 409 : "Conflict", 410 : "Gone", 989 411 : "Lenght Required", 412 : "Precondition Failed", 413 : "Request Entity Too Large", 414 : "Request-URI Too Long", 415 : "Unsupported Media Type", 990 416 : "Requested Range Not Satisfable", 417 : "Expectation Failed", 422 : "Unprocessable Content", 991 992 500 : "Internal Server Error", 501 : "Not Implemented", 502 : "Bad Gateway", 503 : "Service Unavailable", 504 : "Gateway Timeout", 505 : "HTTP Version Not Supported" 993 ]; 994 995 string statusDescription; 996 997 immutable item = _status in StatusCode; 998 if (item != null) statusDescription = *item; 999 else statusDescription = "Unknown"; 1000 1001 bool has_content_type = false; 1002 _headersBuffer.append(format("%s %s %s\r\n", _httpVersion, _status, statusDescription)); 1003 1004 if (!_keepAlive) _headersBuffer.append("connection: close\r\n"); 1005 else _headersBuffer.append("connection: keep-alive\r\n"); 1006 1007 // send user-defined headers 1008 foreach(const ref header;_headers) 1009 { 1010 if (!_sendBody && (header.key == "content-length" || header.key == "transfer-encoding")) 1011 continue; 1012 1013 _headersBuffer.append(format("%s: %s\r\n", header.key, header.value)); 1014 if (header.key == "content-type") has_content_type = true; 1015 } 1016 1017 if (!_sendBody) 1018 _headersBuffer.append(format("content-length: 0\r\n")); 1019 else 1020 { 1021 _headersBuffer.append("content-length: "); 1022 _headersBuffer.append(_sendBuffer.length.to!string); 1023 _headersBuffer.append("\r\n"); 1024 } 1025 1026 // Default content-type is text/html if not defined by user 1027 if (!has_content_type && _sendBody) 1028 _headersBuffer.append(format("content-type: text/html;charset=utf-8\r\n")); 1029 1030 // If required, I add headers to write cookies 1031 foreach(Cookie c;_cookies) 1032 { 1033 _headersBuffer.append(format("set-cookie: %s=%s", encodeComponent(c._name), encodeComponent(c._value))); 1034 1035 if (c._maxAge != Duration.zero) 1036 { 1037 if (c._maxAge.isNegative) _headersBuffer.append("; Max-Age=-1"); 1038 else _headersBuffer.append(format("; Max-Age=%s", c._maxAge.total!"seconds")); 1039 } 1040 else if (c._expire != SysTime.init) 1041 { 1042 _headersBuffer.append(format("; Expires=%s", Output.toHTTPDate(c._expire))); 1043 } 1044 1045 if (!c._path.length == 0) _headersBuffer.append(format("; path=%s", c._path.encodeComponent())); 1046 if (!c._domain.length == 0) _headersBuffer.append(format("; domain=%s", c._domain.encodeComponent())); 1047 1048 if (c._sameSite != Cookie.SameSite.NotSet) 1049 { 1050 if (c._sameSite == Cookie.SameSite.None) c._secure = true; 1051 _headersBuffer.append(format("; SameSite=%s", c._sameSite.to!string)); 1052 } 1053 1054 if (c._secure) _headersBuffer.append(format("; Secure")); 1055 if (c._httpOnly) _headersBuffer.append(format("; HttpOnly")); 1056 1057 _headersBuffer.append("\r\n"); 1058 } 1059 1060 _headersBuffer.append("\r\n"); 1061 } 1062 1063 void clear() 1064 { 1065 // HACK 1066 _timeout = 0.dur!"seconds"; 1067 _httpVersion = HttpVersion.HTTP10; 1068 _dirty = false; 1069 _status = 200; 1070 _cookies = null; 1071 _headers = null; 1072 _keepAlive = false; 1073 _flushed = false; 1074 _headersBuffer.clear(); 1075 _sendBuffer.clear(); 1076 _sendBody = true; 1077 } 1078 } 1079 1080 OutputImpl* _internal; 1081 } 1082