1 // FIXME: if an exception is thrown, we shouldn't necessarily cache... 2 // FIXME: there's some annoying duplication of code in the various versioned mains 3 4 // add the Range header in there too. should return 206 5 6 // FIXME: cgi per-request arena allocator 7 8 // i need to add a bunch of type templates for validations... mayne @NotNull or NotNull! 9 10 // FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable 11 // but the later one can edit and simplify the api. You'd have to use the subclass tho! 12 13 /* 14 void foo(int f, @("test") string s) {} 15 16 void main() { 17 static if(is(typeof(foo) Params == __parameters)) 18 //pragma(msg, __traits(getAttributes, Params[0])); 19 pragma(msg, __traits(getAttributes, Params[1..2])); 20 else 21 pragma(msg, "fail"); 22 } 23 */ 24 25 // Note: spawn-fcgi can help with fastcgi on nginx 26 27 // FIXME: to do: add openssl optionally 28 // make sure embedded_httpd doesn't send two answers if one writes() then dies 29 30 // future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections 31 32 /* 33 Session manager process: it spawns a new process, passing a 34 command line argument, to just be a little key/value store 35 of some serializable struct. On Windows, it CreateProcess. 36 On Linux, it can just fork or maybe fork/exec. The session 37 key is in a cookie. 38 39 Server-side event process: spawns an async manager. You can 40 push stuff out to channel ids and the clients listen to it. 41 42 websocket process: spawns an async handler. They can talk to 43 each other or get info from a cgi request. 44 45 Tempting to put web.d 2.0 in here. It would: 46 * map urls and form generation to functions 47 * have data presentation magic 48 * do the skeleton stuff like 1.0 49 * auto-cache generated stuff in files (at least if pure?) 50 * introspect functions in json for consumers 51 52 53 https://linux.die.net/man/3/posix_spawn 54 */ 55 56 /++ 57 Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. 58 59 --- 60 import arsd.cgi; 61 62 // Instead of writing your own main(), you should write a function 63 // that takes a Cgi param, and use mixin GenericMain 64 // for maximum compatibility with different web servers. 65 void hello(Cgi cgi) { 66 cgi.setResponseContentType("text/plain"); 67 68 if("name" in cgi.get) 69 cgi.write("Hello, " ~ cgi.get["name"]); 70 else 71 cgi.write("Hello, world!"); 72 } 73 74 mixin GenericMain!hello; 75 --- 76 77 Test on console (works in any interface mode): 78 $(CONSOLE 79 $ ./cgi_hello GET / name=whatever 80 ) 81 82 If using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd): 83 $(CONSOLE 84 $ ./cgi_hello --port 8080 85 # now you can go to http://localhost:8080/?name=whatever 86 ) 87 88 Please note: the default port for http is 8085 and for cgi is 4000. I recommend you set your own by the command line argument in a startup script instead of relying on any hard coded defaults. It is possible though to hard code your own with [RequestServer]. 89 90 91 Compile_versions: 92 93 If you are using `dub`, use: 94 95 ```sdlang 96 subConfiguration "arsd-official:cgi" "VALUE_HERE" 97 ``` 98 99 or to dub.json: 100 101 ```json 102 "subConfigurations": {"arsd-official:cgi": "VALUE_HERE"} 103 ``` 104 105 to change versions. The possible options for `VALUE_HERE` are: 106 107 $(LIST 108 * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default. 109 * `cgi` for traditional cgi binaries. 110 * `fastcgi` for FastCGI builds. 111 * `scgi` for SCGI builds. 112 * `stdio_http` for speaking raw http over stdin and stdout. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information. 113 ) 114 115 With dmd, use: 116 117 $(TABLE_ROWS 118 119 * + Interfaces 120 + (mutually exclusive) 121 122 * - `-version=plain_cgi` 123 - The default building the module alone without dub - a traditional, plain CGI executable will be generated. 124 * - `-version=embedded_httpd` 125 - A HTTP server will be embedded in the generated executable. This is default when building with dub. 126 * - `-version=fastcgi` 127 - A FastCGI executable will be generated. 128 * - `-version=scgi` 129 - A SCGI (SimpleCGI) executable will be generated. 130 131 * - `-version=embedded_httpd_threads` 132 - The embedded HTTP server will use a single process with a thread pool. (use instead of plain `embedded_httpd` if you want this specific implementation) 133 * - `-version=embedded_httpd_processes` 134 - The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation) 135 * - `-version=embedded_httpd_processes_accept_after_fork` 136 - It will call accept() in each child process, after forking. This is currently the only option, though I am experimenting with other ideas. You probably should NOT specify this right now. 137 * - `-version=stdio_http` 138 - The embedded HTTP server will be spoken over stdin and stdout. 139 140 * + Tweaks 141 + (can be used together with others) 142 143 * - `-version=cgi_with_websocket` 144 - The CGI class has websocket server support. 145 146 * - `-version=with_openssl` 147 - not currently used 148 * - `-version=cgi_embedded_sessions` 149 - The session server will be embedded in the cgi.d server process 150 * - `-version=cgi_session_server_process` 151 - The session will be provided in a separate process, provided by cgi.d. 152 ) 153 154 Compile_and_run: 155 156 For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory. 157 158 For FastCGI: `dmd yourfile.d cgi.d -version=fastcgi` and run it. spawn-fcgi helps on nginx. You can put the file in the directory for Apache. On IIS, run it with a port on the command line (this causes it to call FCGX_OpenSocket, which can work on nginx too). 159 160 For SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line. 161 162 For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program. 163 164 You can also simulate a request by passing parameters on the command line, like: 165 166 $(CONSOLE 167 ./yourprogram GET / name=adr 168 ) 169 170 And it will print the result to stdout. 171 172 CGI_Setup_tips: 173 174 On Apache, you may do `SetHandler cgi-script` in your `.htaccess` file. 175 176 Integration_tips: 177 178 cgi.d works well with dom.d for generating html. You may also use web.d for other utilities and automatic api wrapping. 179 180 dom.d usage: 181 182 --- 183 import arsd.cgi; 184 import arsd.dom; 185 186 void hello_dom(Cgi cgi) { 187 auto document = new Document(); 188 189 static import std.file; 190 // parse the file in strict mode, requiring it to be well-formed UTF-8 XHTML 191 // (You'll appreciate this if you've ever had to deal with a missing </div> 192 // or something in a php or erb template before that would randomly mess up 193 // the output in your browser. Just check it and throw an exception early!) 194 // 195 // You could also hard-code a template or load one at compile time with an 196 // import expression, but you might appreciate making it a regular file 197 // because that means it can be more easily edited by the frontend team and 198 // they can see their changes without needing to recompile the program. 199 // 200 // Note on CTFE: if you do choose to load a static file at compile time, 201 // you *can* parse it in CTFE using enum, which will cause it to throw at 202 // compile time, which is kinda cool too. Be careful in modifying that document, 203 // though, as it will be a static instance. You might want to clone on on demand, 204 // or perhaps modify it lazily as you print it out. (Try element.tree, it returns 205 // a range of elements which you could send through std.algorithm functions. But 206 // since my selector implementation doesn't work on that level yet, you'll find that 207 // harder to use. Of course, you could make a static list of matching elements and 208 // then use a simple e is e2 predicate... :) ) 209 document.parseUtf8(std.file.read("your_template.html"), true, true); 210 211 // fill in data using DOM functions, so placing it is in the hands of HTML 212 // and it will be properly encoded as text too. 213 // 214 // Plain html templates can't run server side logic, but I think that's a 215 // good thing - it keeps them simple. You may choose to extend the html, 216 // but I think it is best to try to stick to standard elements and fill them 217 // in with requested data with IDs or class names. A further benefit of 218 // this is the designer can also highlight data based on sources in the CSS. 219 // 220 // However, all of dom.d is available, so you can format your data however 221 // you like. You can do partial templates with innerHTML too, or perhaps better, 222 // injecting cloned nodes from a partial document. 223 // 224 // There's a lot of possibilities. 225 document["#name"].innerText = cgi.request("name", "default name"); 226 227 // send the document to the browser. The second argument to `cgi.write` 228 // indicates that this is all the data at once, enabling a few small 229 // optimizations. 230 cgi.write(document.toString(), true); 231 } 232 --- 233 234 Concepts: 235 Input: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod], 236 and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId]) 237 238 Output: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse] 239 240 Cookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies] 241 242 Caching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache] 243 244 Redirections: [Cgi.setResponseLocation] 245 246 Other Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived] 247 248 Overriding behavior: [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState] 249 250 Installing: Apache, IIS, CGI, FastCGI, SCGI, embedded HTTPD (not recommended for production use) 251 252 Guide_for_PHP_users: 253 If you are coming from PHP, here's a quick guide to help you get started: 254 255 $(SIDE_BY_SIDE 256 $(COLUMN 257 ```php 258 <?php 259 $foo = $_POST["foo"]; 260 $bar = $_GET["bar"]; 261 $baz = $_COOKIE["baz"]; 262 263 $user_ip = $_SERVER["REMOTE_ADDR"]; 264 $host = $_SERVER["HTTP_HOST"]; 265 $path = $_SERVER["PATH_INFO"]; 266 267 setcookie("baz", "some value"); 268 269 echo "hello!"; 270 ?> 271 ``` 272 ) 273 $(COLUMN 274 --- 275 import arsd.cgi; 276 void app(Cgi cgi) { 277 string foo = cgi.post["foo"]; 278 string bar = cgi.get["bar"]; 279 string baz = cgi.cookies["baz"]; 280 281 string user_ip = cgi.remoteAddress; 282 string host = cgi.host; 283 string path = cgi.pathInfo; 284 285 cgi.setCookie("baz", "some value"); 286 287 cgi.write("hello!"); 288 } 289 290 mixin GenericMain!app 291 --- 292 ) 293 ) 294 295 $(H3 Array elements) 296 297 298 In PHP, you can give a form element a name like `"something[]"`, and then 299 `$_POST["something"]` gives an array. In D, you can use whatever name 300 you want, and access an array of values with the `cgi.getArray["name"]` and 301 `cgi.postArray["name"]` members. 302 303 $(H3 Databases) 304 305 PHP has a lot of stuff in its standard library. cgi.d doesn't include most 306 of these, but the rest of my arsd repository has much of it. For example, 307 to access a MySQL database, download `database.d` and `mysql.d` from my 308 github repo, and try this code (assuming, of course, your database is 309 set up): 310 311 --- 312 import arsd.cgi; 313 import arsd.mysql; 314 315 void app(Cgi cgi) { 316 auto database = new MySql("localhost", "username", "password", "database_name"); 317 foreach(row; mysql.query("SELECT count(id) FROM people")) 318 cgi.write(row[0] ~ " people in database"); 319 } 320 321 mixin GenericMain!app; 322 --- 323 324 Similar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases, 325 implementing the same basic interface. 326 327 See_Also: 328 329 You may also want to see [arsd.dom], [arsd.web], and [arsd.html] for more code for making 330 web applications. 331 332 For working with json, try [arsd.jsvar]. 333 334 [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in 335 accessing databases. 336 337 If you are looking to access a web application via HTTP, try [std.net.curl], [arsd.curl], or [arsd.http2]. 338 339 Copyright: 340 341 cgi.d copyright 2008-2022, Adam D. Ruppe. Provided under the Boost Software License. 342 343 Yes, this file is old, and yes, it is still actively maintained and used. 344 +/ 345 module arsd.cgi; 346 347 version(Demo) 348 unittest { 349 350 } 351 352 static import std.file; 353 354 // for a single thread, linear request thing, use: 355 // -version=embedded_httpd_threads -version=cgi_no_threads 356 357 version(Posix) { 358 version(CRuntime_Musl) { 359 360 } else version(minimal) { 361 362 } else { 363 version(GNU) { 364 // GDC doesn't support static foreach so I had to cheat on it :( 365 } else version(FreeBSD) { 366 // I never implemented the fancy stuff there either 367 } else { 368 version=with_breaking_cgi_features; 369 version=with_sendfd; 370 version=with_addon_servers; 371 } 372 } 373 } 374 375 version(Windows) { 376 version(minimal) { 377 378 } else { 379 // not too concerned about gdc here since the mingw version is fairly new as well 380 version=with_breaking_cgi_features; 381 } 382 } 383 384 void cloexec(int fd) { 385 version(Posix) { 386 import core.sys.posix.fcntl; 387 fcntl(fd, F_SETFD, FD_CLOEXEC); 388 } 389 } 390 391 void cloexec(Socket s) { 392 version(Posix) { 393 import core.sys.posix.fcntl; 394 fcntl(s.handle, F_SETFD, FD_CLOEXEC); 395 } 396 } 397 398 version(embedded_httpd_hybrid) { 399 version=embedded_httpd_threads; 400 version(cgi_no_fork) {} else version(Posix) 401 version=cgi_use_fork; 402 version=cgi_use_fiber; 403 } 404 405 version(cgi_use_fork) 406 enum cgi_use_fork_default = true; 407 else 408 enum cgi_use_fork_default = false; 409 410 // the servers must know about the connections to talk to them; the interfaces are vital 411 version(with_addon_servers) 412 version=with_addon_servers_connections; 413 414 version(embedded_httpd) { 415 version(linux) 416 version=embedded_httpd_processes; 417 else { 418 version=embedded_httpd_threads; 419 } 420 421 /* 422 version(with_openssl) { 423 pragma(lib, "crypto"); 424 pragma(lib, "ssl"); 425 } 426 */ 427 } 428 429 version(embedded_httpd_processes) 430 version=embedded_httpd_processes_accept_after_fork; // I am getting much better average performance on this, so just keeping it. But the other way MIGHT help keep the variation down so i wanna keep the code to play with later 431 432 version(embedded_httpd_threads) { 433 // unless the user overrides the default.. 434 version(cgi_session_server_process) 435 {} 436 else 437 version=cgi_embedded_sessions; 438 } 439 version(scgi) { 440 // unless the user overrides the default.. 441 version(cgi_session_server_process) 442 {} 443 else 444 version=cgi_embedded_sessions; 445 } 446 447 // fall back if the other is not defined so we can cleanly version it below 448 version(cgi_embedded_sessions) {} 449 else version=cgi_session_server_process; 450 451 452 version=cgi_with_websocket; 453 454 enum long defaultMaxContentLength = 5_000_000; 455 456 /* 457 458 To do a file download offer in the browser: 459 460 cgi.setResponseContentType("text/csv"); 461 cgi.header("Content-Disposition: attachment; filename=\"customers.csv\""); 462 */ 463 464 // FIXME: the location header is supposed to be an absolute url I guess. 465 466 // FIXME: would be cool to flush part of a dom document before complete 467 // somehow in here and dom.d. 468 469 470 // these are public so you can mixin GenericMain. 471 // FIXME: use a function level import instead! 472 public import std.string; 473 public import std.stdio; 474 public import std.conv; 475 import std.uri; 476 import std.uni; 477 import std.algorithm.comparison; 478 import std.algorithm.searching; 479 import std.exception; 480 import std.base64; 481 static import std.algorithm; 482 import std.datetime; 483 import std.range; 484 485 import std.process; 486 487 import std.zlib; 488 489 490 T[] consume(T)(T[] range, int count) { 491 if(count > range.length) 492 count = range.length; 493 return range[count..$]; 494 } 495 496 int locationOf(T)(T[] data, string item) { 497 const(ubyte[]) d = cast(const(ubyte[])) data; 498 const(ubyte[]) i = cast(const(ubyte[])) item; 499 500 // this is a vague sanity check to ensure we aren't getting insanely 501 // sized input that will infinite loop below. it should never happen; 502 // even huge file uploads ought to come in smaller individual pieces. 503 if(d.length > (int.max/2)) 504 throw new Exception("excessive block of input"); 505 506 for(int a = 0; a < d.length; a++) { 507 if(a + i.length > d.length) 508 return -1; 509 if(d[a..a+i.length] == i) 510 return a; 511 } 512 513 return -1; 514 } 515 516 /// If you are doing a custom cgi class, mixing this in can take care of 517 /// the required constructors for you 518 mixin template ForwardCgiConstructors() { 519 this(long maxContentLength = defaultMaxContentLength, 520 string[string] env = null, 521 const(ubyte)[] delegate() readdata = null, 522 void delegate(const(ubyte)[]) _rawDataOutput = null, 523 void delegate() _flush = null 524 ) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); } 525 526 this(string[] args) { super(args); } 527 528 this( 529 BufferedInputRange inputData, 530 string address, ushort _port, 531 int pathInfoStarts = 0, 532 bool _https = false, 533 void delegate(const(ubyte)[]) _rawDataOutput = null, 534 void delegate() _flush = null, 535 // this pointer tells if the connection is supposed to be closed after we handle this 536 bool* closeConnection = null) 537 { 538 super(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection); 539 } 540 541 this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); } 542 } 543 544 /// thrown when a connection is closed remotely while we waiting on data from it 545 class ConnectionClosedException : Exception { 546 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 547 super(message, file, line, next); 548 } 549 } 550 551 552 version(Windows) { 553 // FIXME: ugly hack to solve stdin exception problems on Windows: 554 // reading stdin results in StdioException (Bad file descriptor) 555 // this is probably due to http://d.puremagic.com/issues/show_bug.cgi?id=3425 556 private struct stdin { 557 struct ByChunk { // Replicates std.stdio.ByChunk 558 private: 559 ubyte[] chunk_; 560 public: 561 this(size_t size) 562 in { 563 assert(size, "size must be larger than 0"); 564 } 565 do { 566 chunk_ = new ubyte[](size); 567 popFront(); 568 } 569 570 @property bool empty() const { 571 return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job 572 } 573 @property nothrow ubyte[] front() { return chunk_; } 574 void popFront() { 575 enforce(!empty, "Cannot call popFront on empty range"); 576 chunk_ = stdin.rawRead(chunk_); 577 } 578 } 579 580 import core.sys.windows.windows; 581 static: 582 583 T[] rawRead(T)(T[] buf) { 584 uint bytesRead; 585 auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); 586 587 if (!result) { 588 auto err = GetLastError(); 589 if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input 590 return buf[0..0]; 591 // Some other error, throw it 592 593 char* buffer; 594 scope(exit) LocalFree(buffer); 595 596 // FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 597 // FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 598 FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null); 599 throw new Exception(to!string(buffer)); 600 } 601 enforce(!(bytesRead % T.sizeof), "I/O error"); 602 return buf[0..bytesRead / T.sizeof]; 603 } 604 605 auto byChunk(size_t sz) { return ByChunk(sz); } 606 607 void close() { 608 std.stdio.stdin.close; 609 } 610 } 611 } 612 613 /// The main interface with the web request 614 class Cgi { 615 public: 616 /// the methods a request can be 617 enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work 618 // these are defined in the standard, but idk if they are useful for anything 619 OPTIONS, TRACE, CONNECT, 620 // These seem new, I have only recently seen them 621 PATCH, MERGE, 622 // this is an extension for when the method is not specified and you want to assume 623 CommandLine } 624 625 626 /+ 627 /++ 628 Cgi provides a per-request memory pool 629 630 +/ 631 void[] allocateMemory(size_t nBytes) { 632 633 } 634 635 /// ditto 636 void[] reallocateMemory(void[] old, size_t nBytes) { 637 638 } 639 640 /// ditto 641 void freeMemory(void[] memory) { 642 643 } 644 +/ 645 646 647 /* 648 import core.runtime; 649 auto args = Runtime.args(); 650 651 we can call the app a few ways: 652 653 1) set up the environment variables and call the app (manually simulating CGI) 654 2) simulate a call automatically: 655 ./app method 'uri' 656 657 for example: 658 ./app get /path?arg arg2=something 659 660 Anything on the uri is treated as query string etc 661 662 on get method, further args are appended to the query string (encoded automatically) 663 on post method, further args are done as post 664 665 666 @name means import from file "name". if name == -, it uses stdin 667 (so info=@- means set info to the value of stdin) 668 669 670 Other arguments include: 671 --cookie name=value (these are all concated together) 672 --header 'X-Something: cool' 673 --referrer 'something' 674 --port 80 675 --remote-address some.ip.address.here 676 --https yes 677 --user-agent 'something' 678 --userpass 'user:pass' 679 --authorization 'Basic base64encoded_user:pass' 680 --accept 'content' // FIXME: better example 681 --last-event-id 'something' 682 --host 'something.com' 683 684 Non-simulation arguments: 685 --port xxx listening port for non-cgi things (valid for the cgi interfaces) 686 --listening-host the ip address the application should listen on, or if you want to use unix domain sockets, it is here you can set them: `--listening-host unix:filename` or, on Linux, `--listening-host abstract:name`. 687 688 */ 689 690 /** Initializes it with command line arguments (for easy testing) */ 691 this(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) { 692 rawDataOutput = _rawDataOutput; 693 // these are all set locally so the loop works 694 // without triggering errors in dmd 2.064 695 // we go ahead and set them at the end of it to the this version 696 int port; 697 string referrer; 698 string remoteAddress; 699 string userAgent; 700 string authorization; 701 string origin; 702 string accept; 703 string lastEventId; 704 bool https; 705 string host; 706 RequestMethod requestMethod; 707 string requestUri; 708 string pathInfo; 709 string queryString; 710 711 bool lookingForMethod; 712 bool lookingForUri; 713 string nextArgIs; 714 715 string _cookie; 716 string _queryString; 717 string[][string] _post; 718 string[string] _headers; 719 720 string[] breakUp(string s) { 721 string k, v; 722 auto idx = s.indexOf("="); 723 if(idx == -1) { 724 k = s; 725 } else { 726 k = s[0 .. idx]; 727 v = s[idx + 1 .. $]; 728 } 729 730 return [k, v]; 731 } 732 733 lookingForMethod = true; 734 735 scriptName = args[0]; 736 scriptFileName = args[0]; 737 738 environmentVariables = cast(const) environment.toAA; 739 740 foreach(arg; args[1 .. $]) { 741 if(arg.startsWith("--")) { 742 nextArgIs = arg[2 .. $]; 743 } else if(nextArgIs.length) { 744 if (nextArgIs == "cookie") { 745 auto info = breakUp(arg); 746 if(_cookie.length) 747 _cookie ~= "; "; 748 _cookie ~= std.uri.encodeComponent(info[0]) ~ "=" ~ std.uri.encodeComponent(info[1]); 749 } 750 else if (nextArgIs == "port") { 751 port = to!int(arg); 752 } 753 else if (nextArgIs == "referrer") { 754 referrer = arg; 755 } 756 else if (nextArgIs == "remote-address") { 757 remoteAddress = arg; 758 } 759 else if (nextArgIs == "user-agent") { 760 userAgent = arg; 761 } 762 else if (nextArgIs == "authorization") { 763 authorization = arg; 764 } 765 else if (nextArgIs == "userpass") { 766 authorization = "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup; 767 } 768 else if (nextArgIs == "origin") { 769 origin = arg; 770 } 771 else if (nextArgIs == "accept") { 772 accept = arg; 773 } 774 else if (nextArgIs == "last-event-id") { 775 lastEventId = arg; 776 } 777 else if (nextArgIs == "https") { 778 if(arg == "yes") 779 https = true; 780 } 781 else if (nextArgIs == "header") { 782 string thing, other; 783 auto idx = arg.indexOf(":"); 784 if(idx == -1) 785 throw new Exception("need a colon in a http header"); 786 thing = arg[0 .. idx]; 787 other = arg[idx + 1.. $]; 788 _headers[thing.strip.toLower()] = other.strip; 789 } 790 else if (nextArgIs == "host") { 791 host = arg; 792 } 793 // else 794 // skip, we don't know it but that's ok, it might be used elsewhere so no error 795 796 nextArgIs = null; 797 } else if(lookingForMethod) { 798 lookingForMethod = false; 799 lookingForUri = true; 800 801 if(arg.asLowerCase().equal("commandline")) 802 requestMethod = RequestMethod.CommandLine; 803 else 804 requestMethod = to!RequestMethod(arg.toUpper()); 805 } else if(lookingForUri) { 806 lookingForUri = false; 807 808 requestUri = arg; 809 810 auto idx = arg.indexOf("?"); 811 if(idx == -1) 812 pathInfo = arg; 813 else { 814 pathInfo = arg[0 .. idx]; 815 _queryString = arg[idx + 1 .. $]; 816 } 817 } else { 818 // it is an argument of some sort 819 if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { 820 auto parts = breakUp(arg); 821 _post[parts[0]] ~= parts[1]; 822 allPostNamesInOrder ~= parts[0]; 823 allPostValuesInOrder ~= parts[1]; 824 } else { 825 if(_queryString.length) 826 _queryString ~= "&"; 827 auto parts = breakUp(arg); 828 _queryString ~= std.uri.encodeComponent(parts[0]) ~ "=" ~ std.uri.encodeComponent(parts[1]); 829 } 830 } 831 } 832 833 acceptsGzip = false; 834 keepAliveRequested = false; 835 requestHeaders = cast(immutable) _headers; 836 837 cookie = _cookie; 838 cookiesArray = getCookieArray(); 839 cookies = keepLastOf(cookiesArray); 840 841 queryString = _queryString; 842 getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 843 get = keepLastOf(getArray); 844 845 postArray = cast(immutable) _post; 846 post = keepLastOf(_post); 847 848 // FIXME 849 filesArray = null; 850 files = null; 851 852 isCalledWithCommandLineArguments = true; 853 854 this.port = port; 855 this.referrer = referrer; 856 this.remoteAddress = remoteAddress; 857 this.userAgent = userAgent; 858 this.authorization = authorization; 859 this.origin = origin; 860 this.accept = accept; 861 this.lastEventId = lastEventId; 862 this.https = https; 863 this.host = host; 864 this.requestMethod = requestMethod; 865 this.requestUri = requestUri; 866 this.pathInfo = pathInfo; 867 this.queryString = queryString; 868 this.postBody = null; 869 } 870 871 private { 872 string[] allPostNamesInOrder; 873 string[] allPostValuesInOrder; 874 string[] allGetNamesInOrder; 875 string[] allGetValuesInOrder; 876 } 877 878 CgiConnectionHandle getOutputFileHandle() { 879 return _outputFileHandle; 880 } 881 882 CgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE; 883 884 /** Initializes it using a CGI or CGI-like interface */ 885 this(long maxContentLength = defaultMaxContentLength, 886 // use this to override the environment variable listing 887 in string[string] env = null, 888 // and this should return a chunk of data. return empty when done 889 const(ubyte)[] delegate() readdata = null, 890 // finally, use this to do custom output if needed 891 void delegate(const(ubyte)[]) _rawDataOutput = null, 892 // to flush teh custom output 893 void delegate() _flush = null 894 ) 895 { 896 897 // these are all set locally so the loop works 898 // without triggering errors in dmd 2.064 899 // we go ahead and set them at the end of it to the this version 900 int port; 901 string referrer; 902 string remoteAddress; 903 string userAgent; 904 string authorization; 905 string origin; 906 string accept; 907 string lastEventId; 908 bool https; 909 string host; 910 RequestMethod requestMethod; 911 string requestUri; 912 string pathInfo; 913 string queryString; 914 915 916 917 isCalledWithCommandLineArguments = false; 918 rawDataOutput = _rawDataOutput; 919 flushDelegate = _flush; 920 auto getenv = delegate string(string var) { 921 if(env is null) 922 return std.process.environment.get(var); 923 auto e = var in env; 924 if(e is null) 925 return null; 926 return *e; 927 }; 928 929 environmentVariables = env is null ? 930 cast(const) environment.toAA : 931 env; 932 933 // fetching all the request headers 934 string[string] requestHeadersHere; 935 foreach(k, v; env is null ? cast(const) environment.toAA() : env) { 936 if(k.startsWith("HTTP_")) { 937 requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v; 938 } 939 } 940 941 this.requestHeaders = assumeUnique(requestHeadersHere); 942 943 requestUri = getenv("REQUEST_URI"); 944 945 cookie = getenv("HTTP_COOKIE"); 946 cookiesArray = getCookieArray(); 947 cookies = keepLastOf(cookiesArray); 948 949 referrer = getenv("HTTP_REFERER"); 950 userAgent = getenv("HTTP_USER_AGENT"); 951 remoteAddress = getenv("REMOTE_ADDR"); 952 host = getenv("HTTP_HOST"); 953 pathInfo = getenv("PATH_INFO"); 954 955 queryString = getenv("QUERY_STRING"); 956 scriptName = getenv("SCRIPT_NAME"); 957 { 958 import core.runtime; 959 auto sfn = getenv("SCRIPT_FILENAME"); 960 scriptFileName = sfn.length ? sfn : Runtime.args[0]; 961 } 962 963 bool iis = false; 964 965 // Because IIS doesn't pass requestUri, we simulate it here if it's empty. 966 if(requestUri.length == 0) { 967 // IIS sometimes includes the script name as part of the path info - we don't want that 968 if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName)) 969 pathInfo = pathInfo[scriptName.length .. $]; 970 971 requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : ""); 972 973 iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339 974 975 // FIXME: this works for apache and iis... but what about others? 976 } 977 978 979 auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 980 getArray = assumeUnique(ugh); 981 get = keepLastOf(getArray); 982 983 984 // NOTE: on shitpache, you need to specifically forward this 985 authorization = getenv("HTTP_AUTHORIZATION"); 986 // this is a hack because Apache is a shitload of fuck and 987 // refuses to send the real header to us. Compatible 988 // programs should send both the standard and X- versions 989 990 // NOTE: if you have access to .htaccess or httpd.conf, you can make this 991 // unnecessary with mod_rewrite, so it is commented 992 993 //if(authorization.length == 0) // if the std is there, use it 994 // authorization = getenv("HTTP_X_AUTHORIZATION"); 995 996 // the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong 997 if(getenv("SERVER_PORT").length && getenv("REDIRECT_HTTPS") != "on") 998 port = to!int(getenv("SERVER_PORT")); 999 else 1000 port = 0; // this was probably called from the command line 1001 1002 auto ae = getenv("HTTP_ACCEPT_ENCODING"); 1003 if(ae.length && ae.indexOf("gzip") != -1) 1004 acceptsGzip = true; 1005 1006 accept = getenv("HTTP_ACCEPT"); 1007 lastEventId = getenv("HTTP_LAST_EVENT_ID"); 1008 1009 auto ka = getenv("HTTP_CONNECTION"); 1010 if(ka.length && ka.asLowerCase().canFind("keep-alive")) 1011 keepAliveRequested = true; 1012 1013 auto or = getenv("HTTP_ORIGIN"); 1014 origin = or; 1015 1016 auto rm = getenv("REQUEST_METHOD"); 1017 if(rm.length) 1018 requestMethod = to!RequestMethod(getenv("REQUEST_METHOD")); 1019 else 1020 requestMethod = RequestMethod.CommandLine; 1021 1022 // FIXME: hack on REDIRECT_HTTPS; this is there because the work app uses mod_rewrite which loses the https flag! So I set it with [E=HTTPS=%HTTPS] or whatever but then it gets translated to here so i want it to still work. This is arguably wrong but meh. 1023 https = (getenv("HTTPS") == "on" || getenv("REDIRECT_HTTPS") == "on"); 1024 1025 // FIXME: DOCUMENT_ROOT? 1026 1027 // FIXME: what about PUT? 1028 if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { 1029 version(preserveData) // a hack to make forwarding simpler 1030 immutable(ubyte)[] data; 1031 size_t amountReceived = 0; 1032 auto contentType = getenv("CONTENT_TYPE"); 1033 1034 // FIXME: is this ever not going to be set? I guess it depends 1035 // on if the server de-chunks and buffers... seems like it has potential 1036 // to be slow if they did that. The spec says it is always there though. 1037 // And it has worked reliably for me all year in the live environment, 1038 // but some servers might be different. 1039 auto cls = getenv("CONTENT_LENGTH"); 1040 auto contentLength = to!size_t(cls.length ? cls : "0"); 1041 1042 immutable originalContentLength = contentLength; 1043 if(contentLength) { 1044 if(maxContentLength > 0 && contentLength > maxContentLength) { 1045 setResponseStatus("413 Request entity too large"); 1046 write("You tried to upload a file that is too large."); 1047 close(); 1048 throw new Exception("POST too large"); 1049 } 1050 prepareForIncomingDataChunks(contentType, contentLength); 1051 1052 1053 int processChunk(in ubyte[] chunk) { 1054 if(chunk.length > contentLength) { 1055 handleIncomingDataChunk(chunk[0..contentLength]); 1056 amountReceived += contentLength; 1057 contentLength = 0; 1058 return 1; 1059 } else { 1060 handleIncomingDataChunk(chunk); 1061 contentLength -= chunk.length; 1062 amountReceived += chunk.length; 1063 } 1064 if(contentLength == 0) 1065 return 1; 1066 1067 onRequestBodyDataReceived(amountReceived, originalContentLength); 1068 return 0; 1069 } 1070 1071 1072 if(readdata is null) { 1073 foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096)) 1074 if(processChunk(chunk)) 1075 break; 1076 } else { 1077 // we have a custom data source.. 1078 auto chunk = readdata(); 1079 while(chunk.length) { 1080 if(processChunk(chunk)) 1081 break; 1082 chunk = readdata(); 1083 } 1084 } 1085 1086 onRequestBodyDataReceived(amountReceived, originalContentLength); 1087 postArray = assumeUnique(pps._post); 1088 filesArray = assumeUnique(pps._files); 1089 files = keepLastOf(filesArray); 1090 post = keepLastOf(postArray); 1091 this.postBody = pps.postBody; 1092 cleanUpPostDataState(); 1093 } 1094 1095 version(preserveData) 1096 originalPostData = data; 1097 } 1098 // fixme: remote_user script name 1099 1100 1101 this.port = port; 1102 this.referrer = referrer; 1103 this.remoteAddress = remoteAddress; 1104 this.userAgent = userAgent; 1105 this.authorization = authorization; 1106 this.origin = origin; 1107 this.accept = accept; 1108 this.lastEventId = lastEventId; 1109 this.https = https; 1110 this.host = host; 1111 this.requestMethod = requestMethod; 1112 this.requestUri = requestUri; 1113 this.pathInfo = pathInfo; 1114 this.queryString = queryString; 1115 } 1116 1117 /// Cleans up any temporary files. Do not use the object 1118 /// after calling this. 1119 /// 1120 /// NOTE: it is called automatically by GenericMain 1121 // FIXME: this should be called if the constructor fails too, if it has created some garbage... 1122 void dispose() { 1123 foreach(file; files) { 1124 if(!file.contentInMemory) 1125 if(std.file.exists(file.contentFilename)) 1126 std.file.remove(file.contentFilename); 1127 } 1128 } 1129 1130 private { 1131 struct PostParserState { 1132 string contentType; 1133 string boundary; 1134 string localBoundary; // the ones used at the end or something lol 1135 bool isMultipart; 1136 bool needsSavedBody; 1137 1138 ulong expectedLength; 1139 ulong contentConsumed; 1140 immutable(ubyte)[] buffer; 1141 1142 // multipart parsing state 1143 int whatDoWeWant; 1144 bool weHaveAPart; 1145 string[] thisOnesHeaders; 1146 immutable(ubyte)[] thisOnesData; 1147 1148 string postBody; 1149 1150 UploadedFile piece; 1151 bool isFile = false; 1152 1153 size_t memoryCommitted; 1154 1155 // do NOT keep mutable references to these anywhere! 1156 // I assume they are unique in the constructor once we're all done getting data. 1157 string[][string] _post; 1158 UploadedFile[][string] _files; 1159 } 1160 1161 PostParserState pps; 1162 } 1163 1164 /// This represents a file the user uploaded via a POST request. 1165 static struct UploadedFile { 1166 /// If you want to create one of these structs for yourself from some data, 1167 /// use this function. 1168 static UploadedFile fromData(immutable(void)[] data, string name = null) { 1169 Cgi.UploadedFile f; 1170 f.filename = name; 1171 f.content = cast(immutable(ubyte)[]) data; 1172 f.contentInMemory = true; 1173 return f; 1174 } 1175 1176 string name; /// The name of the form element. 1177 string filename; /// The filename the user set. 1178 string contentType; /// The MIME type the user's browser reported. (Not reliable.) 1179 1180 /** 1181 For small files, cgi.d will buffer the uploaded file in memory, and make it 1182 directly accessible to you through the content member. I find this very convenient 1183 and somewhat efficient, since it can avoid hitting the disk entirely. (I 1184 often want to inspect and modify the file anyway!) 1185 1186 I find the file is very large, it is undesirable to eat that much memory just 1187 for a file buffer. In those cases, if you pass a large enough value for maxContentLength 1188 to the constructor so they are accepted, cgi.d will write the content to a temporary 1189 file that you can re-read later. 1190 1191 You can override this behavior by subclassing Cgi and overriding the protected 1192 handlePostChunk method. Note that the object is not initialized when you 1193 write that method - the http headers are available, but the cgi.post method 1194 is not. You may parse the file as it streams in using this method. 1195 1196 1197 Anyway, if the file is small enough to be in memory, contentInMemory will be 1198 set to true, and the content is available in the content member. 1199 1200 If not, contentInMemory will be set to false, and the content saved in a file, 1201 whose name will be available in the contentFilename member. 1202 1203 1204 Tip: if you know you are always dealing with small files, and want the convenience 1205 of ignoring this member, construct Cgi with a small maxContentLength. Then, if 1206 a large file comes in, it simply throws an exception (and HTTP error response) 1207 instead of trying to handle it. 1208 1209 The default value of maxContentLength in the constructor is for small files. 1210 */ 1211 bool contentInMemory = true; // the default ought to always be true 1212 immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true 1213 string contentFilename; /// the file where we dumped the content, if contentInMemory == false. Note that if you want to keep it, you MUST move the file, since otherwise it is considered garbage when cgi is disposed. 1214 1215 /// 1216 ulong fileSize() { 1217 if(contentInMemory) 1218 return content.length; 1219 import std.file; 1220 return std.file.getSize(contentFilename); 1221 1222 } 1223 1224 /// 1225 void writeToFile(string filenameToSaveTo) const { 1226 import std.file; 1227 if(contentInMemory) 1228 std.file.write(filenameToSaveTo, content); 1229 else 1230 std.file.rename(contentFilename, filenameToSaveTo); 1231 } 1232 } 1233 1234 // given a content type and length, decide what we're going to do with the data.. 1235 protected void prepareForIncomingDataChunks(string contentType, ulong contentLength) { 1236 pps.expectedLength = contentLength; 1237 1238 auto terminator = contentType.indexOf(";"); 1239 if(terminator == -1) 1240 terminator = contentType.length; 1241 1242 pps.contentType = contentType[0 .. terminator]; 1243 auto b = contentType[terminator .. $]; 1244 if(b.length) { 1245 auto idx = b.indexOf("boundary="); 1246 if(idx != -1) { 1247 pps.boundary = b[idx + "boundary=".length .. $]; 1248 pps.localBoundary = "\r\n--" ~ pps.boundary; 1249 } 1250 } 1251 1252 // while a content type SHOULD be sent according to the RFC, it is 1253 // not required. We're told we SHOULD guess by looking at the content 1254 // but it seems to me that this only happens when it is urlencoded. 1255 if(pps.contentType == "application/x-www-form-urlencoded" || pps.contentType == "") { 1256 pps.isMultipart = false; 1257 pps.needsSavedBody = false; 1258 } else if(pps.contentType == "multipart/form-data") { 1259 pps.isMultipart = true; 1260 enforce(pps.boundary.length, "no boundary"); 1261 } else if(pps.contentType == "text/xml") { // FIXME: could this be special and load the post params 1262 // save the body so the application can handle it 1263 pps.isMultipart = false; 1264 pps.needsSavedBody = true; 1265 } else if(pps.contentType == "application/json") { // FIXME: this could prolly try to load post params too 1266 // save the body so the application can handle it 1267 pps.needsSavedBody = true; 1268 pps.isMultipart = false; 1269 } else { 1270 // the rest is 100% handled by the application. just save the body and send it to them 1271 pps.needsSavedBody = true; 1272 pps.isMultipart = false; 1273 } 1274 } 1275 1276 // handles streaming POST data. If you handle some other content type, you should 1277 // override this. If the data isn't the content type you want, you ought to call 1278 // super.handleIncomingDataChunk so regular forms and files still work. 1279 1280 // FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the 1281 // file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network 1282 // input anyway, so I'm not going to get too worked up about it right now. 1283 protected void handleIncomingDataChunk(const(ubyte)[] chunk) { 1284 if(chunk.length == 0) 1285 return; 1286 assert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so 1287 // if we're passed big chunks, it might throw unnecessarily. 1288 // just pass it smaller chunks at a time. 1289 if(pps.isMultipart) { 1290 // multipart/form-data 1291 1292 1293 // FIXME: this might want to be factored out and factorized 1294 // need to make sure the stream hooks actually work. 1295 void pieceHasNewContent() { 1296 // we just grew the piece's buffer. Do we have to switch to file backing? 1297 if(pps.piece.contentInMemory) { 1298 if(pps.piece.content.length <= 10 * 1024 * 1024) 1299 // meh, I'm ok with it. 1300 return; 1301 else { 1302 // this is too big. 1303 if(!pps.isFile) 1304 throw new Exception("Request entity too large"); // a variable this big is kinda ridiculous, just reject it. 1305 else { 1306 // a file this large is probably acceptable though... let's use a backing file. 1307 pps.piece.contentInMemory = false; 1308 // FIXME: say... how do we intend to delete these things? cgi.dispose perhaps. 1309 1310 int count = 0; 1311 pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); 1312 // odds are this loop will never be entered, but we want it just in case. 1313 while(std.file.exists(pps.piece.contentFilename)) { 1314 count++; 1315 pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); 1316 } 1317 // I hope this creates the file pretty quickly, or the loop might be useless... 1318 // FIXME: maybe I should write some kind of custom transaction here. 1319 std.file.write(pps.piece.contentFilename, pps.piece.content); 1320 1321 pps.piece.content = null; 1322 } 1323 } 1324 } else { 1325 // it's already in a file, so just append it to what we have 1326 if(pps.piece.content.length) { 1327 // FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk... 1328 std.file.append(pps.piece.contentFilename, pps.piece.content); 1329 pps.piece.content = null; 1330 } 1331 } 1332 } 1333 1334 1335 void commitPart() { 1336 if(!pps.weHaveAPart) 1337 return; 1338 1339 pieceHasNewContent(); // be sure the new content is handled every time 1340 1341 if(pps.isFile) { 1342 // I'm not sure if other environments put files in post or not... 1343 // I used to not do it, but I think I should, since it is there... 1344 pps._post[pps.piece.name] ~= pps.piece.filename; 1345 pps._files[pps.piece.name] ~= pps.piece; 1346 1347 allPostNamesInOrder ~= pps.piece.name; 1348 allPostValuesInOrder ~= pps.piece.filename; 1349 } else { 1350 pps._post[pps.piece.name] ~= cast(string) pps.piece.content; 1351 1352 allPostNamesInOrder ~= pps.piece.name; 1353 allPostValuesInOrder ~= cast(string) pps.piece.content; 1354 } 1355 1356 /* 1357 stderr.writeln("RECEIVED: ", pps.piece.name, "=", 1358 pps.piece.content.length < 1000 1359 ? 1360 to!string(pps.piece.content) 1361 : 1362 "too long"); 1363 */ 1364 1365 // FIXME: the limit here 1366 pps.memoryCommitted += pps.piece.content.length; 1367 1368 pps.weHaveAPart = false; 1369 pps.whatDoWeWant = 1; 1370 pps.thisOnesHeaders = null; 1371 pps.thisOnesData = null; 1372 1373 pps.piece = UploadedFile.init; 1374 pps.isFile = false; 1375 } 1376 1377 void acceptChunk() { 1378 pps.buffer ~= chunk; 1379 chunk = null; // we've consumed it into the buffer, so keeping it just brings confusion 1380 } 1381 1382 immutable(ubyte)[] consume(size_t howMuch) { 1383 pps.contentConsumed += howMuch; 1384 auto ret = pps.buffer[0 .. howMuch]; 1385 pps.buffer = pps.buffer[howMuch .. $]; 1386 return ret; 1387 } 1388 1389 dataConsumptionLoop: do { 1390 switch(pps.whatDoWeWant) { 1391 default: assert(0); 1392 case 0: 1393 acceptChunk(); 1394 // the format begins with two extra leading dashes, then we should be at the boundary 1395 if(pps.buffer.length < 2) 1396 return; 1397 assert(pps.buffer[0] == '-', "no leading dash"); 1398 consume(1); 1399 assert(pps.buffer[0] == '-', "no second leading dash"); 1400 consume(1); 1401 1402 pps.whatDoWeWant = 1; 1403 goto case 1; 1404 /* fallthrough */ 1405 case 1: // looking for headers 1406 // here, we should be lined up right at the boundary, which is followed by a \r\n 1407 1408 // want to keep the buffer under control in case we're under attack 1409 //stderr.writeln("here once"); 1410 //if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really.... 1411 // throw new Exception("wtf is up with the huge mime part headers"); 1412 1413 acceptChunk(); 1414 1415 if(pps.buffer.length < pps.boundary.length) 1416 return; // not enough data, since there should always be a boundary here at least 1417 1418 if(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) { 1419 assert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \r\n 1420 // we *should* be at the end here! 1421 assert(pps.buffer[0] == '-'); 1422 consume(1); 1423 assert(pps.buffer[0] == '-'); 1424 consume(1); 1425 1426 // the message is terminated by --BOUNDARY--\r\n (after a \r\n leading to the boundary) 1427 assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, 1428 "not lined up on boundary " ~ pps.boundary); 1429 consume(pps.boundary.length); 1430 1431 assert(pps.buffer[0] == '-'); 1432 consume(1); 1433 assert(pps.buffer[0] == '-'); 1434 consume(1); 1435 1436 assert(pps.buffer[0] == '\r'); 1437 consume(1); 1438 assert(pps.buffer[0] == '\n'); 1439 consume(1); 1440 1441 assert(pps.buffer.length == 0); 1442 assert(pps.contentConsumed == pps.expectedLength); 1443 break dataConsumptionLoop; // we're done! 1444 } else { 1445 // we're not done yet. We should be lined up on a boundary. 1446 1447 // But, we want to ensure the headers are here before we consume anything! 1448 auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); 1449 if(headerEndLocation == -1) 1450 return; // they *should* all be here, so we can handle them all at once. 1451 1452 assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, 1453 "not lined up on boundary " ~ pps.boundary); 1454 1455 consume(pps.boundary.length); 1456 // the boundary is always followed by a \r\n 1457 assert(pps.buffer[0] == '\r'); 1458 consume(1); 1459 assert(pps.buffer[0] == '\n'); 1460 consume(1); 1461 } 1462 1463 // re-running since by consuming the boundary, we invalidate the old index. 1464 auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); 1465 assert(headerEndLocation >= 0, "no header"); 1466 auto thisOnesHeaders = pps.buffer[0..headerEndLocation]; 1467 1468 consume(headerEndLocation + 4); // The +4 is the \r\n\r\n that caps it off 1469 1470 pps.thisOnesHeaders = split(cast(string) thisOnesHeaders, "\r\n"); 1471 1472 // now we'll parse the headers 1473 foreach(h; pps.thisOnesHeaders) { 1474 auto p = h.indexOf(":"); 1475 assert(p != -1, "no colon in header, got " ~ to!string(pps.thisOnesHeaders)); 1476 string hn = h[0..p]; 1477 string hv = h[p+2..$]; 1478 1479 switch(hn.toLower) { 1480 default: assert(0); 1481 case "content-disposition": 1482 auto info = hv.split("; "); 1483 foreach(i; info[1..$]) { // skipping the form-data 1484 auto o = i.split("="); // FIXME 1485 string pn = o[0]; 1486 string pv = o[1][1..$-1]; 1487 1488 if(pn == "name") { 1489 pps.piece.name = pv; 1490 } else if (pn == "filename") { 1491 pps.piece.filename = pv; 1492 pps.isFile = true; 1493 } 1494 } 1495 break; 1496 case "content-type": 1497 pps.piece.contentType = hv; 1498 break; 1499 } 1500 } 1501 1502 pps.whatDoWeWant++; // move to the next step - the data 1503 break; 1504 case 2: 1505 // when we get here, pps.buffer should contain our first chunk of data 1506 1507 if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much 1508 throw new Exception("wtf is up with the huge mime part buffer"); 1509 1510 acceptChunk(); 1511 1512 // so the trick is, we want to process all the data up to the boundary, 1513 // but what if the chunk's end cuts the boundary off? If we're unsure, we 1514 // want to wait for the next chunk. We start by looking for the whole boundary 1515 // in the buffer somewhere. 1516 1517 auto boundaryLocation = locationOf(pps.buffer, pps.localBoundary); 1518 // assert(boundaryLocation != -1, "should have seen "~to!string(cast(ubyte[]) pps.localBoundary)~" in " ~ to!string(pps.buffer)); 1519 if(boundaryLocation != -1) { 1520 // this is easy - we can see it in it's entirety! 1521 1522 pps.piece.content ~= consume(boundaryLocation); 1523 1524 assert(pps.buffer[0] == '\r'); 1525 consume(1); 1526 assert(pps.buffer[0] == '\n'); 1527 consume(1); 1528 assert(pps.buffer[0] == '-'); 1529 consume(1); 1530 assert(pps.buffer[0] == '-'); 1531 consume(1); 1532 // the boundary here is always preceded by \r\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off. 1533 pps.weHaveAPart = true; 1534 pps.whatDoWeWant = 1; // back to getting headers for the next part 1535 1536 commitPart(); // we're done here 1537 } else { 1538 // we can't see the whole thing, but what if there's a partial boundary? 1539 1540 enforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line... 1541 assert(pps.localBoundary.length > 1); // should already be sane but just in case 1542 bool potentialBoundaryFound = false; 1543 1544 boundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) { 1545 // we grow the boundary a bit each time. If we think it looks the 1546 // same, better pull another chunk to be sure it's not the end. 1547 // Starting small because exiting the loop early is desirable, since 1548 // we're not keeping any ambiguity and 1 / 256 chance of exiting is 1549 // the best we can do. 1550 if(a > pps.buffer.length) 1551 break; // FIXME: is this right? 1552 assert(a <= pps.buffer.length); 1553 assert(a > 0); 1554 if(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) { 1555 // ok, there *might* be a boundary here, so let's 1556 // not treat the end as data yet. The rest is good to 1557 // use though, since if there was a boundary there, we'd 1558 // have handled it up above after locationOf. 1559 1560 pps.piece.content ~= pps.buffer[0 .. $ - a]; 1561 consume(pps.buffer.length - a); 1562 pieceHasNewContent(); 1563 potentialBoundaryFound = true; 1564 break boundaryCheck; 1565 } 1566 } 1567 1568 if(!potentialBoundaryFound) { 1569 // we can consume the whole thing 1570 pps.piece.content ~= pps.buffer; 1571 pieceHasNewContent(); 1572 consume(pps.buffer.length); 1573 } else { 1574 // we found a possible boundary, but there was 1575 // insufficient data to be sure. 1576 assert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]); 1577 1578 return; // wait for the next chunk. 1579 } 1580 } 1581 } 1582 } while(pps.buffer.length); 1583 1584 // btw all boundaries except the first should have a \r\n before them 1585 } else { 1586 // application/x-www-form-urlencoded and application/json 1587 1588 // not using maxContentLength because that might be cranked up to allow 1589 // large file uploads. We can handle them, but a huge post[] isn't any good. 1590 if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough 1591 throw new Exception("wtf is up with such a gigantic form submission????"); 1592 1593 pps.buffer ~= chunk; 1594 1595 // simple handling, but it works... until someone bombs us with gigabytes of crap at least... 1596 if(pps.buffer.length == pps.expectedLength) { 1597 if(pps.needsSavedBody) 1598 pps.postBody = cast(string) pps.buffer; 1599 else 1600 pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder); 1601 version(preserveData) 1602 originalPostData = pps.buffer; 1603 } else { 1604 // just for debugging 1605 } 1606 } 1607 } 1608 1609 protected void cleanUpPostDataState() { 1610 pps = PostParserState.init; 1611 } 1612 1613 /// you can override this function to somehow react 1614 /// to an upload in progress. 1615 /// 1616 /// Take note that parts of the CGI object is not yet 1617 /// initialized! Stuff from HTTP headers, including get[], is usable. 1618 /// But, none of post[] is usable, and you cannot write here. That's 1619 /// why this method is const - mutating the object won't do much anyway. 1620 /// 1621 /// My idea here was so you can output a progress bar or 1622 /// something to a cooperative client (see arsd.rtud for a potential helper) 1623 /// 1624 /// The default is to do nothing. Subclass cgi and use the 1625 /// CustomCgiMain mixin to do something here. 1626 void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const { 1627 // This space intentionally left blank. 1628 } 1629 1630 /// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source. 1631 /// *closeConnection will be set to true if you should close the connection after handling this request 1632 this(BufferedInputRange ir, bool* closeConnection) { 1633 isCalledWithCommandLineArguments = false; 1634 import al = std.algorithm; 1635 1636 immutable(ubyte)[] data; 1637 1638 void rdo(const(ubyte)[] d) { 1639 //import std.stdio; writeln(d); 1640 sendAll(ir.source, d); 1641 } 1642 1643 auto ira = ir.source.remoteAddress(); 1644 auto irLocalAddress = ir.source.localAddress(); 1645 1646 ushort port = 80; 1647 if(auto ia = cast(InternetAddress) irLocalAddress) { 1648 port = ia.port; 1649 } else if(auto ia = cast(Internet6Address) irLocalAddress) { 1650 port = ia.port; 1651 } 1652 1653 // that check for UnixAddress is to work around a Phobos bug 1654 // see: https://github.com/dlang/phobos/pull/7383 1655 // but this might be more useful anyway tbh for this case 1656 version(Posix) 1657 this(ir, ira is null ? null : cast(UnixAddress) ira ? "unix:" : ira.toString(), port, 0, false, &rdo, null, closeConnection); 1658 else 1659 this(ir, ira is null ? null : ira.toString(), port, 0, false, &rdo, null, closeConnection); 1660 } 1661 1662 /** 1663 Initializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd. 1664 1665 NOTE: If you are behind a reverse proxy, the values here might not be what you expect.... it will use X-Forwarded-For for remote IP and X-Forwarded-Host for host 1666 1667 Params: 1668 inputData = the incoming data, including headers and other raw http data. 1669 When the constructor exits, it will leave this range exactly at the start of 1670 the next request on the connection (if there is one). 1671 1672 address = the IP address of the remote user 1673 _port = the port number of the connection 1674 pathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins. 1675 _https = if this connection is encrypted (note that the input data must not actually be encrypted) 1676 _rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http. 1677 _flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire 1678 closeConnection = if the request asks to close the connection, *closeConnection == true. 1679 */ 1680 this( 1681 BufferedInputRange inputData, 1682 // string[] headers, immutable(ubyte)[] data, 1683 string address, ushort _port, 1684 int pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment 1685 bool _https = false, 1686 void delegate(const(ubyte)[]) _rawDataOutput = null, 1687 void delegate() _flush = null, 1688 // this pointer tells if the connection is supposed to be closed after we handle this 1689 bool* closeConnection = null) 1690 { 1691 // these are all set locally so the loop works 1692 // without triggering errors in dmd 2.064 1693 // we go ahead and set them at the end of it to the this version 1694 int port; 1695 string referrer; 1696 string remoteAddress; 1697 string userAgent; 1698 string authorization; 1699 string origin; 1700 string accept; 1701 string lastEventId; 1702 bool https; 1703 string host; 1704 RequestMethod requestMethod; 1705 string requestUri; 1706 string pathInfo; 1707 string queryString; 1708 string scriptName; 1709 string[string] get; 1710 string[][string] getArray; 1711 bool keepAliveRequested; 1712 bool acceptsGzip; 1713 string cookie; 1714 1715 1716 1717 environmentVariables = cast(const) environment.toAA; 1718 1719 idlol = inputData; 1720 1721 isCalledWithCommandLineArguments = false; 1722 1723 https = _https; 1724 port = _port; 1725 1726 rawDataOutput = _rawDataOutput; 1727 flushDelegate = _flush; 1728 nph = true; 1729 1730 remoteAddress = address; 1731 1732 // streaming parser 1733 import al = std.algorithm; 1734 1735 // FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason. 1736 auto idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); 1737 while(idx == -1) { 1738 inputData.popFront(0); 1739 idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); 1740 } 1741 1742 assert(idx != -1); 1743 1744 1745 string contentType = ""; 1746 string[string] requestHeadersHere; 1747 1748 size_t contentLength; 1749 1750 bool isChunked; 1751 1752 { 1753 import core.runtime; 1754 scriptFileName = Runtime.args[0]; 1755 } 1756 1757 1758 int headerNumber = 0; 1759 foreach(line; al.splitter(inputData.front()[0 .. idx], "\r\n")) 1760 if(line.length) { 1761 headerNumber++; 1762 auto header = cast(string) line.idup; 1763 if(headerNumber == 1) { 1764 // request line 1765 auto parts = al.splitter(header, " "); 1766 requestMethod = to!RequestMethod(parts.front); 1767 parts.popFront(); 1768 requestUri = parts.front; 1769 1770 // FIXME: the requestUri could be an absolute path!!! should I rename it or something? 1771 scriptName = requestUri[0 .. pathInfoStarts]; 1772 1773 auto question = requestUri.indexOf("?"); 1774 if(question == -1) { 1775 queryString = ""; 1776 // FIXME: double check, this might be wrong since it could be url encoded 1777 pathInfo = requestUri[pathInfoStarts..$]; 1778 } else { 1779 queryString = requestUri[question+1..$]; 1780 pathInfo = requestUri[pathInfoStarts..question]; 1781 } 1782 1783 auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 1784 getArray = cast(string[][string]) assumeUnique(ugh); 1785 1786 if(header.indexOf("HTTP/1.0") != -1) { 1787 http10 = true; 1788 autoBuffer = true; 1789 if(closeConnection) { 1790 // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive) 1791 *closeConnection = true; 1792 } 1793 } 1794 } else { 1795 // other header 1796 auto colon = header.indexOf(":"); 1797 if(colon == -1) 1798 throw new Exception("HTTP headers should have a colon!"); 1799 string name = header[0..colon].toLower; 1800 string value = header[colon+2..$]; // skip the colon and the space 1801 1802 requestHeadersHere[name] = value; 1803 1804 if (name == "accept") { 1805 accept = value; 1806 } 1807 else if (name == "origin") { 1808 origin = value; 1809 } 1810 else if (name == "connection") { 1811 if(value == "close" && closeConnection) 1812 *closeConnection = true; 1813 if(value.asLowerCase().canFind("keep-alive")) { 1814 keepAliveRequested = true; 1815 1816 // on http 1.0, the connection is closed by default, 1817 // but not if they request keep-alive. then we don't close 1818 // anymore - undoing the set above 1819 if(http10 && closeConnection) { 1820 *closeConnection = false; 1821 } 1822 } 1823 } 1824 else if (name == "transfer-encoding") { 1825 if(value == "chunked") 1826 isChunked = true; 1827 } 1828 else if (name == "last-event-id") { 1829 lastEventId = value; 1830 } 1831 else if (name == "authorization") { 1832 authorization = value; 1833 } 1834 else if (name == "content-type") { 1835 contentType = value; 1836 } 1837 else if (name == "content-length") { 1838 contentLength = to!size_t(value); 1839 } 1840 else if (name == "x-forwarded-for") { 1841 remoteAddress = value; 1842 } 1843 else if (name == "x-forwarded-host" || name == "host") { 1844 if(name != "host" || host is null) 1845 host = value; 1846 } 1847 // FIXME: https://tools.ietf.org/html/rfc7239 1848 else if (name == "accept-encoding") { 1849 if(value.indexOf("gzip") != -1) 1850 acceptsGzip = true; 1851 } 1852 else if (name == "user-agent") { 1853 userAgent = value; 1854 } 1855 else if (name == "referer") { 1856 referrer = value; 1857 } 1858 else if (name == "cookie") { 1859 cookie ~= value; 1860 } else if(name == "expect") { 1861 if(value == "100-continue") { 1862 // FIXME we should probably give user code a chance 1863 // to process and reject but that needs to be virtual, 1864 // perhaps part of the CGI redesign. 1865 1866 // FIXME: if size is > max content length it should 1867 // also fail at this point. 1868 _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n"); 1869 1870 // FIXME: let the user write out 103 early hints too 1871 } 1872 } 1873 // else 1874 // ignore it 1875 1876 } 1877 } 1878 1879 inputData.consume(idx + 4); 1880 // done 1881 1882 requestHeaders = assumeUnique(requestHeadersHere); 1883 1884 ByChunkRange dataByChunk; 1885 1886 // reading Content-Length type data 1887 // We need to read up the data we have, and write it out as a chunk. 1888 if(!isChunked) { 1889 dataByChunk = byChunk(inputData, contentLength); 1890 } else { 1891 // chunked requests happen, but not every day. Since we need to know 1892 // the content length (for now, maybe that should change), we'll buffer 1893 // the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes) 1894 auto data = dechunk(inputData); 1895 1896 // set the range here 1897 dataByChunk = byChunk(data); 1898 contentLength = data.length; 1899 } 1900 1901 assert(dataByChunk !is null); 1902 1903 if(contentLength) { 1904 prepareForIncomingDataChunks(contentType, contentLength); 1905 foreach(dataChunk; dataByChunk) { 1906 handleIncomingDataChunk(dataChunk); 1907 } 1908 postArray = assumeUnique(pps._post); 1909 filesArray = assumeUnique(pps._files); 1910 files = keepLastOf(filesArray); 1911 post = keepLastOf(postArray); 1912 postBody = pps.postBody; 1913 cleanUpPostDataState(); 1914 } 1915 1916 this.port = port; 1917 this.referrer = referrer; 1918 this.remoteAddress = remoteAddress; 1919 this.userAgent = userAgent; 1920 this.authorization = authorization; 1921 this.origin = origin; 1922 this.accept = accept; 1923 this.lastEventId = lastEventId; 1924 this.https = https; 1925 this.host = host; 1926 this.requestMethod = requestMethod; 1927 this.requestUri = requestUri; 1928 this.pathInfo = pathInfo; 1929 this.queryString = queryString; 1930 1931 this.scriptName = scriptName; 1932 this.get = keepLastOf(getArray); 1933 this.getArray = cast(immutable) getArray; 1934 this.keepAliveRequested = keepAliveRequested; 1935 this.acceptsGzip = acceptsGzip; 1936 this.cookie = cookie; 1937 1938 cookiesArray = getCookieArray(); 1939 cookies = keepLastOf(cookiesArray); 1940 1941 } 1942 BufferedInputRange idlol; 1943 1944 private immutable(string[string]) keepLastOf(in string[][string] arr) { 1945 string[string] ca; 1946 foreach(k, v; arr) 1947 ca[k] = v[$-1]; 1948 1949 return assumeUnique(ca); 1950 } 1951 1952 // FIXME duplication 1953 private immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) { 1954 UploadedFile[string] ca; 1955 foreach(k, v; arr) 1956 ca[k] = v[$-1]; 1957 1958 return assumeUnique(ca); 1959 } 1960 1961 1962 private immutable(string[][string]) getCookieArray() { 1963 auto forTheLoveOfGod = decodeVariables(cookie, "; "); 1964 return assumeUnique(forTheLoveOfGod); 1965 } 1966 1967 /// Very simple method to require a basic auth username and password. 1968 /// If the http request doesn't include the required credentials, it throws a 1969 /// HTTP 401 error, and an exception. 1970 /// 1971 /// Note: basic auth does not provide great security, especially over unencrypted HTTP; 1972 /// the user's credentials are sent in plain text on every request. 1973 /// 1974 /// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the 1975 /// application. Either use Apache's built in methods for basic authentication, or add 1976 /// something along these lines to your server configuration: 1977 /// 1978 /// RewriteEngine On 1979 /// RewriteCond %{HTTP:Authorization} ^(.*) 1980 /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] 1981 /// 1982 /// To ensure the necessary data is available to cgi.d. 1983 void requireBasicAuth(string user, string pass, string message = null) { 1984 if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) { 1985 setResponseStatus("401 Authorization Required"); 1986 header ("WWW-Authenticate: Basic realm=\""~message~"\""); 1987 close(); 1988 throw new Exception("Not authorized; got " ~ authorization); 1989 } 1990 } 1991 1992 /// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites. 1993 /// setCache(true) means it will always be cached for as long as possible. Best for static content. 1994 /// Use setResponseExpires and updateResponseExpires for more control 1995 void setCache(bool allowCaching) { 1996 noCache = !allowCaching; 1997 } 1998 1999 /// Set to true and use cgi.write(data, true); to send a gzipped response to browsers 2000 /// who can accept it 2001 bool gzipResponse; 2002 2003 immutable bool acceptsGzip; 2004 immutable bool keepAliveRequested; 2005 2006 /// Set to true if and only if this was initialized with command line arguments 2007 immutable bool isCalledWithCommandLineArguments; 2008 2009 /// This gets a full url for the current request, including port, protocol, host, path, and query 2010 string getCurrentCompleteUri() const { 2011 ushort defaultPort = https ? 443 : 80; 2012 2013 string uri = "http"; 2014 if(https) 2015 uri ~= "s"; 2016 uri ~= "://"; 2017 uri ~= host; 2018 /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right hting now 2019 version(none) 2020 if(!(!port || port == defaultPort)) { 2021 uri ~= ":"; 2022 uri ~= to!string(port); 2023 } 2024 +/ 2025 uri ~= requestUri; 2026 return uri; 2027 } 2028 2029 /// You can override this if your site base url isn't the same as the script name 2030 string logicalScriptName() const { 2031 return scriptName; 2032 } 2033 2034 /++ 2035 Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error". 2036 It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation(). 2037 Note setResponseStatus() must be called *before* you write() any data to the output. 2038 2039 History: 2040 The `int` overload was added on January 11, 2021. 2041 +/ 2042 void setResponseStatus(string status) { 2043 assert(!outputtedResponseData); 2044 responseStatus = status; 2045 } 2046 /// ditto 2047 void setResponseStatus(int statusCode) { 2048 setResponseStatus(getHttpCodeText(statusCode)); 2049 } 2050 private string responseStatus = null; 2051 2052 /// Returns true if it is still possible to output headers 2053 bool canOutputHeaders() { 2054 return !isClosed && !outputtedResponseData; 2055 } 2056 2057 /// Sets the location header, which the browser will redirect the user to automatically. 2058 /// Note setResponseLocation() must be called *before* you write() any data to the output. 2059 /// The optional important argument is used if it's a default suggestion rather than something to insist upon. 2060 void setResponseLocation(string uri, bool important = true, string status = null) { 2061 if(!important && isCurrentResponseLocationImportant) 2062 return; // important redirects always override unimportant ones 2063 2064 if(uri is null) { 2065 responseStatus = "200 OK"; 2066 responseLocation = null; 2067 isCurrentResponseLocationImportant = important; 2068 return; // this just cancels the redirect 2069 } 2070 2071 assert(!outputtedResponseData); 2072 if(status is null) 2073 responseStatus = "302 Found"; 2074 else 2075 responseStatus = status; 2076 2077 responseLocation = uri.strip; 2078 isCurrentResponseLocationImportant = important; 2079 } 2080 protected string responseLocation = null; 2081 private bool isCurrentResponseLocationImportant = false; 2082 2083 /// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching 2084 /// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use. 2085 /// Note: the when parameter is different than setCookie's expire parameter. 2086 void setResponseExpires(long when, bool isPublic = false) { 2087 responseExpires = when; 2088 setCache(true); // need to enable caching so the date has meaning 2089 2090 responseIsPublic = isPublic; 2091 responseExpiresRelative = false; 2092 } 2093 2094 /// Sets a cache-control max-age header for whenFromNow, in seconds. 2095 void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) { 2096 responseExpires = whenFromNow; 2097 setCache(true); // need to enable caching so the date has meaning 2098 2099 responseIsPublic = isPublic; 2100 responseExpiresRelative = true; 2101 } 2102 private long responseExpires = long.min; 2103 private bool responseIsPublic = false; 2104 private bool responseExpiresRelative = false; 2105 2106 /// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept. 2107 /// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program 2108 /// output as a whole is as cacheable as the least cachable part in the chain. 2109 2110 /// setCache(false) always overrides this - it is, by definition, the strictest anti-cache statement available. If your site outputs sensitive user data, you should probably call setCache(false) when you do, to ensure no other functions will cache the content, as it may be a privacy risk. 2111 /// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity. 2112 void updateResponseExpires(long when, bool isPublic) { 2113 if(responseExpires == long.min) 2114 setResponseExpires(when, isPublic); 2115 else if(when < responseExpires) 2116 setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is 2117 } 2118 2119 /* 2120 /// Set to true if you want the result to be cached publically - that is, is the content shared? 2121 /// Should generally be false if the user is logged in. It assumes private cache only. 2122 /// setCache(true) also turns on public caching, and setCache(false) sets to private. 2123 void setPublicCaching(bool allowPublicCaches) { 2124 publicCaching = allowPublicCaches; 2125 } 2126 private bool publicCaching = false; 2127 */ 2128 2129 /++ 2130 History: 2131 Added January 11, 2021 2132 +/ 2133 enum SameSitePolicy { 2134 Lax, 2135 Strict, 2136 None 2137 } 2138 2139 /++ 2140 Sets an HTTP cookie, automatically encoding the data to the correct string. 2141 expiresIn is how many milliseconds in the future the cookie will expire. 2142 TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com. 2143 Note setCookie() must be called *before* you write() any data to the output. 2144 2145 History: 2146 Parameter `sameSitePolicy` was added on January 11, 2021. 2147 +/ 2148 void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) { 2149 assert(!outputtedResponseData); 2150 string cookie = std.uri.encodeComponent(name) ~ "="; 2151 cookie ~= std.uri.encodeComponent(data); 2152 if(path !is null) 2153 cookie ~= "; path=" ~ path; 2154 // FIXME: should I just be using max-age here? (also in cache below) 2155 if(expiresIn != 0) 2156 cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!"msecs"(expiresIn)); 2157 if(domain !is null) 2158 cookie ~= "; domain=" ~ domain; 2159 if(secure == true) 2160 cookie ~= "; Secure"; 2161 if(httpOnly == true ) 2162 cookie ~= "; HttpOnly"; 2163 final switch(sameSitePolicy) { 2164 case SameSitePolicy.Lax: 2165 cookie ~= "; SameSite=Lax"; 2166 break; 2167 case SameSitePolicy.Strict: 2168 cookie ~= "; SameSite=Strict"; 2169 break; 2170 case SameSitePolicy.None: 2171 cookie ~= "; SameSite=None"; 2172 assert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite 2173 break; 2174 } 2175 2176 if(auto idx = name in cookieIndexes) { 2177 responseCookies[*idx] = cookie; 2178 } else { 2179 cookieIndexes[name] = responseCookies.length; 2180 responseCookies ~= cookie; 2181 } 2182 } 2183 private string[] responseCookies; 2184 private size_t[string] cookieIndexes; 2185 2186 /// Clears a previously set cookie with the given name, path, and domain. 2187 void clearCookie(string name, string path = null, string domain = null) { 2188 assert(!outputtedResponseData); 2189 setCookie(name, "", 1, path, domain); 2190 } 2191 2192 /// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image 2193 void setResponseContentType(string ct) { 2194 assert(!outputtedResponseData); 2195 responseContentType = ct; 2196 } 2197 private string responseContentType = null; 2198 2199 /// Adds a custom header. It should be the name: value, but without any line terminator. 2200 /// For example: header("X-My-Header: Some value"); 2201 /// Note you should use the specialized functions in this object if possible to avoid 2202 /// duplicates in the output. 2203 void header(string h) { 2204 customHeaders ~= h; 2205 } 2206 2207 /++ 2208 I named the original function `header` after PHP, but this pattern more fits 2209 the rest of the Cgi object. 2210 2211 Either name are allowed. 2212 2213 History: 2214 Alias added June 17, 2022. 2215 +/ 2216 alias setResponseHeader = header; 2217 2218 private string[] customHeaders; 2219 private bool websocketMode; 2220 2221 void flushHeaders(const(void)[] t, bool isAll = false) { 2222 StackBuffer buffer = StackBuffer(0); 2223 2224 prepHeaders(t, isAll, &buffer); 2225 2226 if(rawDataOutput !is null) 2227 rawDataOutput(cast(const(ubyte)[]) buffer.get()); 2228 else { 2229 stdout.rawWrite(buffer.get()); 2230 } 2231 } 2232 2233 private void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) { 2234 string terminator = "\n"; 2235 if(rawDataOutput !is null) 2236 terminator = "\r\n"; 2237 2238 if(responseStatus !is null) { 2239 if(nph) { 2240 if(http10) 2241 buffer.add("HTTP/1.0 ", responseStatus, terminator); 2242 else 2243 buffer.add("HTTP/1.1 ", responseStatus, terminator); 2244 } else 2245 buffer.add("Status: ", responseStatus, terminator); 2246 } else if (nph) { 2247 if(http10) 2248 buffer.add("HTTP/1.0 200 OK", terminator); 2249 else 2250 buffer.add("HTTP/1.1 200 OK", terminator); 2251 } 2252 2253 if(websocketMode) 2254 goto websocket; 2255 2256 if(nph) { // we're responsible for setting the date too according to http 1.1 2257 char[29] db = void; 2258 printDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]); 2259 buffer.add("Date: ", db[], terminator); 2260 } 2261 2262 // FIXME: what if the user wants to set his own content-length? 2263 // The custom header function can do it, so maybe that's best. 2264 // Or we could reuse the isAll param. 2265 if(responseLocation !is null) { 2266 buffer.add("Location: ", responseLocation, terminator); 2267 } 2268 if(!noCache && responseExpires != long.min) { // an explicit expiration date is set 2269 if(responseExpiresRelative) { 2270 buffer.add("Cache-Control: ", responseIsPublic ? "public" : "private", ", max-age="); 2271 buffer.add(responseExpires); 2272 buffer.add(", no-cache=\"set-cookie, set-cookie2\"", terminator); 2273 } else { 2274 auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC()); 2275 char[29] db = void; 2276 printDateToBuffer(cast(DateTime) expires, db[]); 2277 buffer.add("Expires: ", db[], terminator); 2278 // FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily 2279 buffer.add("Cache-Control: ", (responseIsPublic ? "public" : "private"), ", no-cache=\"set-cookie, set-cookie2\""); 2280 buffer.add(terminator); 2281 } 2282 } 2283 if(responseCookies !is null && responseCookies.length > 0) { 2284 foreach(c; responseCookies) 2285 buffer.add("Set-Cookie: ", c, terminator); 2286 } 2287 if(noCache) { // we specifically do not want caching (this is actually the default) 2288 buffer.add("Cache-Control: private, no-cache=\"set-cookie\"", terminator); 2289 buffer.add("Expires: 0", terminator); 2290 buffer.add("Pragma: no-cache", terminator); 2291 } else { 2292 if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever 2293 buffer.add("Cache-Control: public", terminator); 2294 buffer.add("Expires: Tue, 31 Dec 2030 14:00:00 GMT", terminator); // FIXME: should not be more than one year in the future 2295 } 2296 } 2297 if(responseContentType !is null) { 2298 buffer.add("Content-Type: ", responseContentType, terminator); 2299 } else 2300 buffer.add("Content-Type: text/html; charset=utf-8", terminator); 2301 2302 if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary 2303 buffer.add("Content-Encoding: gzip", terminator); 2304 } 2305 2306 2307 if(!isAll) { 2308 if(nph && !http10) { 2309 buffer.add("Transfer-Encoding: chunked", terminator); 2310 responseChunked = true; 2311 } 2312 } else { 2313 buffer.add("Content-Length: "); 2314 buffer.add(t.length); 2315 buffer.add(terminator); 2316 if(nph && keepAliveRequested) { 2317 buffer.add("Connection: Keep-Alive", terminator); 2318 } 2319 } 2320 2321 websocket: 2322 2323 foreach(hd; customHeaders) 2324 buffer.add(hd, terminator); 2325 2326 // FIXME: what about duplicated headers? 2327 2328 // end of header indicator 2329 buffer.add(terminator); 2330 2331 outputtedResponseData = true; 2332 } 2333 2334 /// Writes the data to the output, flushing headers if they have not yet been sent. 2335 void write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) { 2336 assert(!closed, "Output has already been closed"); 2337 2338 StackBuffer buffer = StackBuffer(0); 2339 2340 if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary 2341 // actually gzip the data here 2342 2343 auto c = new Compress(HeaderFormat.gzip); // want gzip 2344 2345 auto data = c.compress(t); 2346 data ~= c.flush(); 2347 2348 // std.file.write("/tmp/last-item", data); 2349 2350 t = data; 2351 } 2352 2353 if(!outputtedResponseData && (!autoBuffer || isAll)) { 2354 prepHeaders(t, isAll, &buffer); 2355 } 2356 2357 if(requestMethod != RequestMethod.HEAD && t.length > 0) { 2358 if (autoBuffer && !isAll) { 2359 outputBuffer ~= cast(ubyte[]) t; 2360 } 2361 if(!autoBuffer || isAll) { 2362 if(rawDataOutput !is null) 2363 if(nph && responseChunked) { 2364 //rawDataOutput(makeChunk(cast(const(ubyte)[]) t)); 2365 // we're making the chunk here instead of in a function 2366 // to avoid unneeded gc pressure 2367 buffer.add(toHex(t.length)); 2368 buffer.add("\r\n"); 2369 buffer.add(cast(char[]) t, "\r\n"); 2370 } else { 2371 buffer.add(cast(char[]) t); 2372 } 2373 else 2374 buffer.add(cast(char[]) t); 2375 } 2376 } 2377 2378 if(rawDataOutput !is null) 2379 rawDataOutput(cast(const(ubyte)[]) buffer.get()); 2380 else 2381 stdout.rawWrite(buffer.get()); 2382 2383 if(maybeAutoClose && isAll) 2384 close(); // if you say it is all, that means we're definitely done 2385 // maybeAutoClose can be false though to avoid this (important if you call from inside close()! 2386 } 2387 2388 /++ 2389 Convenience method to set content type to json and write the string as the complete response. 2390 2391 History: 2392 Added January 16, 2020 2393 +/ 2394 void writeJson(string json) { 2395 this.setResponseContentType("application/json"); 2396 this.write(json, true); 2397 } 2398 2399 /// Flushes the pending buffer, leaving the connection open so you can send more. 2400 void flush() { 2401 if(rawDataOutput is null) 2402 stdout.flush(); 2403 else if(flushDelegate !is null) 2404 flushDelegate(); 2405 } 2406 2407 version(autoBuffer) 2408 bool autoBuffer = true; 2409 else 2410 bool autoBuffer = false; 2411 ubyte[] outputBuffer; 2412 2413 /// Flushes the buffers to the network, signifying that you are done. 2414 /// You should always call this explicitly when you are done outputting data. 2415 void close() { 2416 if(closed) 2417 return; // don't double close 2418 2419 if(!outputtedResponseData) 2420 write("", false, false); 2421 2422 // writing auto buffered data 2423 if(requestMethod != RequestMethod.HEAD && autoBuffer) { 2424 if(!nph) 2425 stdout.rawWrite(outputBuffer); 2426 else 2427 write(outputBuffer, true, false); // tell it this is everything 2428 } 2429 2430 // closing the last chunk... 2431 if(nph && rawDataOutput !is null && responseChunked) 2432 rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n"); 2433 2434 if(flushDelegate) 2435 flushDelegate(); 2436 2437 closed = true; 2438 } 2439 2440 // Closes without doing anything, shouldn't be used often 2441 void rawClose() { 2442 closed = true; 2443 } 2444 2445 /++ 2446 Gets a request variable as a specific type, or the default value of it isn't there 2447 or isn't convertible to the request type. 2448 2449 Checks both GET and POST variables, preferring the POST variable, if available. 2450 2451 A nice trick is using the default value to choose the type: 2452 2453 --- 2454 /* 2455 The return value will match the type of the default. 2456 Here, I gave 10 as a default, so the return value will 2457 be an int. 2458 2459 If the user-supplied value cannot be converted to the 2460 requested type, you will get the default value back. 2461 */ 2462 int a = cgi.request("number", 10); 2463 2464 if(cgi.get["number"] == "11") 2465 assert(a == 11); // conversion succeeds 2466 2467 if("number" !in cgi.get) 2468 assert(a == 10); // no value means you can't convert - give the default 2469 2470 if(cgi.get["number"] == "twelve") 2471 assert(a == 10); // conversion from string to int would fail, so we get the default 2472 --- 2473 2474 You can use an enum as an easy whitelist, too: 2475 2476 --- 2477 enum Operations { 2478 add, remove, query 2479 } 2480 2481 auto op = cgi.request("op", Operations.query); 2482 2483 if(cgi.get["op"] == "add") 2484 assert(op == Operations.add); 2485 if(cgi.get["op"] == "remove") 2486 assert(op == Operations.remove); 2487 if(cgi.get["op"] == "query") 2488 assert(op == Operations.query); 2489 2490 if(cgi.get["op"] == "random string") 2491 assert(op == Operations.query); // the value can't be converted to the enum, so we get the default 2492 --- 2493 +/ 2494 T request(T = string)(in string name, in T def = T.init) const nothrow { 2495 try { 2496 return 2497 (name in post) ? to!T(post[name]) : 2498 (name in get) ? to!T(get[name]) : 2499 def; 2500 } catch(Exception e) { return def; } 2501 } 2502 2503 /// Is the output already closed? 2504 bool isClosed() const { 2505 return closed; 2506 } 2507 2508 /++ 2509 Gets a session object associated with the `cgi` request. You can use different type throughout your application. 2510 +/ 2511 Session!Data getSessionObject(Data)() { 2512 if(testInProcess !is null) { 2513 // test mode 2514 auto obj = testInProcess.getSessionOverride(typeid(typeof(return))); 2515 if(obj !is null) 2516 return cast(typeof(return)) obj; 2517 else { 2518 auto o = new MockSession!Data(); 2519 testInProcess.setSessionOverride(typeid(typeof(return)), o); 2520 return o; 2521 } 2522 } else { 2523 // normal operation 2524 return new BasicDataServerSession!Data(this); 2525 } 2526 } 2527 2528 // if it is in test mode; triggers mock sessions. Used by CgiTester 2529 version(with_breaking_cgi_features) 2530 private CgiTester testInProcess; 2531 2532 /* Hooks for redirecting input and output */ 2533 private void delegate(const(ubyte)[]) rawDataOutput = null; 2534 private void delegate() flushDelegate = null; 2535 2536 /* This info is used when handling a more raw HTTP protocol */ 2537 private bool nph; 2538 private bool http10; 2539 private bool closed; 2540 private bool responseChunked = false; 2541 2542 version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it. 2543 immutable(ubyte)[] originalPostData; 2544 2545 public immutable string postBody; 2546 alias postJson = postBody; // old name 2547 2548 /* Internal state flags */ 2549 private bool outputtedResponseData; 2550 private bool noCache = true; 2551 2552 const(string[string]) environmentVariables; 2553 2554 /** What follows is data gotten from the HTTP request. It is all fully immutable, 2555 partially because it logically is (your code doesn't change what the user requested...) 2556 and partially because I hate how bad programs in PHP change those superglobals to do 2557 all kinds of hard to follow ugliness. I don't want that to ever happen in D. 2558 2559 For some of these, you'll want to refer to the http or cgi specs for more details. 2560 */ 2561 immutable(string[string]) requestHeaders; /// All the raw headers in the request as name/value pairs. The name is stored as all lower case, but otherwise the same as it is in HTTP; words separated by dashes. For example, "cookie" or "accept-encoding". Many HTTP headers have specialized variables below for more convenience and static name checking; you should generally try to use them. 2562 2563 immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them. 2564 immutable(char[]) origin; /// The origin header in the request, if present. Some HTML5 cross-domain apis set this and you should check it on those cross domain requests and websockets. 2565 immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser. 2566 immutable(char[]) pathInfo; /// This is any stuff sent after your program's name on the url, but before the query string. For example, suppose your program is named "app". If the user goes to site.com/app, pathInfo is empty. But, he can also go to site.com/app/some/sub/path; treating your program like a virtual folder. In this case, pathInfo == "/some/sub/path". 2567 immutable(char[]) scriptName; /// The full base path of your program, as seen by the user. If your program is located at site.com/programs/apps, scriptName == "/programs/apps". 2568 immutable(char[]) scriptFileName; /// The physical filename of your script 2569 immutable(char[]) authorization; /// The full authorization string from the header, undigested. Useful for implementing auth schemes such as OAuth 1.0. Note that some web servers do not forward this to the app without taking extra steps. See requireBasicAuth's comment for more info. 2570 immutable(char[]) accept; /// The HTTP accept header is the user agent telling what content types it is willing to accept. This is often */*; they accept everything, so it's not terribly useful. (The similar sounding Accept-Encoding header is handled automatically for chunking and gzipping. Simply set gzipResponse = true and cgi.d handles the details, zipping if the user's browser is willing to accept it.) 2571 immutable(char[]) lastEventId; /// The HTML 5 draft includes an EventSource() object that connects to the server, and remains open to take a stream of events. My arsd.rtud module can help with the server side part of that. The Last-Event-Id http header is defined in the draft to help handle loss of connection. When the browser reconnects to you, it sets this header to the last event id it saw, so you can catch it up. This member has the contents of that header. 2572 2573 immutable(RequestMethod) requestMethod; /// The HTTP request verb: GET, POST, etc. It is represented as an enum in cgi.d (which, like many enums, you can convert back to string with std.conv.to()). A HTTP GET is supposed to, according to the spec, not have side effects; a user can GET something over and over again and always have the same result. On all requests, the get[] and getArray[] members may be filled in. The post[] and postArray[] members are only filled in on POST methods. 2574 immutable(char[]) queryString; /// The unparsed content of the request query string - the stuff after the ? in your URL. See get[] and getArray[] for a parse view of it. Sometimes, the unparsed string is useful though if you want a custom format of data up there (probably not a good idea, unless it is really simple, like "?username" perhaps.) 2575 immutable(char[]) cookie; /// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data. 2576 /** The Referer header from the request. (It is misspelled in the HTTP spec, and thus the actual request and cgi specs too, but I spelled the word correctly here because that's sane. The spec's misspelling is an implementation detail.) It contains the site url that referred the user to your program; the site that linked to you, or if you're serving images, the site that has you as an image. Also, if you're in an iframe, the referrer is the site that is framing you. 2577 2578 Important note: if the user copy/pastes your url, this is blank, and, just like with all other user data, their browsers can also lie to you. Don't rely on it for real security. 2579 */ 2580 immutable(char[]) referrer; 2581 immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : ""); 2582 2583 immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.) 2584 2585 immutable bool https; /// Was the request encrypted via https? 2586 immutable int port; /// On what TCP port number did the server receive the request? 2587 2588 /** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */ 2589 2590 immutable(string[string]) get; /// The data from your query string in the url, only showing the last string of each name. If you want to handle multiple values with the same name, use getArray. This only works right if the query string is x-www-form-urlencoded; the default you see on the web with name=value pairs separated by the & character. 2591 immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself. 2592 immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!) 2593 2594 /** 2595 Represents user uploaded files. 2596 2597 When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html <form> tag attributes. The key into this array is the name attribute on your input tag, just like with other post variables. See the comments on the UploadedFile struct for more information about the data inside, including important notes on max size and content location. 2598 */ 2599 immutable(UploadedFile[][string]) filesArray; 2600 immutable(UploadedFile[string]) files; 2601 2602 /// Use these if you expect multiple items submitted with the same name. btw, assert(get[name] is getArray[name][$-1); should pass. Same for post and cookies. 2603 /// the order of the arrays is the order the data arrives 2604 immutable(string[][string]) getArray; /// like get, but an array of values per name 2605 immutable(string[][string]) postArray; /// ditto for post 2606 immutable(string[][string]) cookiesArray; /// ditto for cookies 2607 2608 // convenience function for appending to a uri without extra ? 2609 // matches the name and effect of javascript's location.search property 2610 string search() const { 2611 if(queryString.length) 2612 return "?" ~ queryString; 2613 return ""; 2614 } 2615 2616 // FIXME: what about multiple files with the same name? 2617 private: 2618 //RequestMethod _requestMethod; 2619 } 2620 2621 /// use this for testing or other isolated things when you want it to be no-ops 2622 Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) { 2623 // we want to ignore, not use stdout 2624 if(outputSink is null) 2625 outputSink = delegate void(const(ubyte)[]) { }; 2626 2627 string[string] env; 2628 env["REQUEST_METHOD"] = to!string(method); 2629 env["CONTENT_LENGTH"] = to!string(data.length); 2630 2631 auto cgi = new Cgi( 2632 0, 2633 env, 2634 { return data; }, 2635 outputSink, 2636 null); 2637 2638 return cgi; 2639 } 2640 2641 /++ 2642 A helper test class for request handler unittests. 2643 +/ 2644 version(with_breaking_cgi_features) 2645 class CgiTester { 2646 private { 2647 SessionObject[TypeInfo] mockSessions; 2648 SessionObject getSessionOverride(TypeInfo ti) { 2649 if(auto o = ti in mockSessions) 2650 return *o; 2651 else 2652 return null; 2653 } 2654 void setSessionOverride(TypeInfo ti, SessionObject so) { 2655 mockSessions[ti] = so; 2656 } 2657 } 2658 2659 /++ 2660 Gets (and creates if necessary) a mock session object for this test. Note 2661 it will be the same one used for any test operations through this CgiTester instance. 2662 +/ 2663 Session!Data getSessionObject(Data)() { 2664 auto obj = getSessionOverride(typeid(typeof(return))); 2665 if(obj !is null) 2666 return cast(typeof(return)) obj; 2667 else { 2668 auto o = new MockSession!Data(); 2669 setSessionOverride(typeid(typeof(return)), o); 2670 return o; 2671 } 2672 } 2673 2674 /++ 2675 Pass a reference to your request handler when creating the tester. 2676 +/ 2677 this(void function(Cgi) requestHandler) { 2678 this.requestHandler = requestHandler; 2679 } 2680 2681 /++ 2682 You can check response information with these methods after you call the request handler. 2683 +/ 2684 struct Response { 2685 int code; 2686 string[string] headers; 2687 string responseText; 2688 ubyte[] responseBody; 2689 } 2690 2691 /++ 2692 Executes a test request on your request handler, and returns the response. 2693 2694 Params: 2695 url = The URL to test. Should be an absolute path, but excluding domain. e.g. `"/test"`. 2696 args = additional arguments. Same format as cgi's command line handler. 2697 +/ 2698 Response GET(string url, string[] args = null) { 2699 return executeTest("GET", url, args); 2700 } 2701 /// ditto 2702 Response POST(string url, string[] args = null) { 2703 return executeTest("POST", url, args); 2704 } 2705 2706 /// ditto 2707 Response executeTest(string method, string url, string[] args) { 2708 ubyte[] outputtedRawData; 2709 void outputSink(const(ubyte)[] data) { 2710 outputtedRawData ~= data; 2711 } 2712 auto cgi = new Cgi(["test", method, url] ~ args, &outputSink); 2713 cgi.testInProcess = this; 2714 scope(exit) cgi.dispose(); 2715 2716 requestHandler(cgi); 2717 2718 cgi.close(); 2719 2720 Response response; 2721 2722 if(outputtedRawData.length) { 2723 enum LINE = "\r\n"; 2724 2725 auto idx = outputtedRawData.locationOf(LINE ~ LINE); 2726 assert(idx != -1, to!string(outputtedRawData)); 2727 auto headers = cast(string) outputtedRawData[0 .. idx]; 2728 response.code = 200; 2729 while(headers.length) { 2730 auto i = headers.locationOf(LINE); 2731 if(i == -1) i = cast(int) headers.length; 2732 2733 auto header = headers[0 .. i]; 2734 2735 auto c = header.locationOf(":"); 2736 if(c != -1) { 2737 auto name = header[0 .. c]; 2738 auto value = header[c + 2 ..$]; 2739 2740 if(name == "Status") 2741 response.code = value[0 .. value.locationOf(" ")].to!int; 2742 2743 response.headers[name] = value; 2744 } else { 2745 assert(0); 2746 } 2747 2748 if(i != headers.length) 2749 i += 2; 2750 headers = headers[i .. $]; 2751 } 2752 response.responseBody = outputtedRawData[idx + 4 .. $]; 2753 response.responseText = cast(string) response.responseBody; 2754 } 2755 2756 return response; 2757 } 2758 2759 private void function(Cgi) requestHandler; 2760 } 2761 2762 2763 // should this be a separate module? Probably, but that's a hassle. 2764 2765 /// Makes a data:// uri that can be used as links in most newer browsers (IE8+). 2766 string makeDataUrl(string mimeType, in void[] data) { 2767 auto data64 = Base64.encode(cast(const(ubyte[])) data); 2768 return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64); 2769 } 2770 2771 // FIXME: I don't think this class correctly decodes/encodes the individual parts 2772 /// Represents a url that can be broken down or built up through properties 2773 struct Uri { 2774 alias toString this; // blargh idk a url really is a string, but should it be implicit? 2775 2776 // scheme//userinfo@host:port/path?query#fragment 2777 2778 string scheme; /// e.g. "http" in "http://example.com/" 2779 string userinfo; /// the username (and possibly a password) in the uri 2780 string host; /// the domain name 2781 int port; /// port number, if given. Will be zero if a port was not explicitly given 2782 string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html" 2783 string query; /// the stuff after the ? in a uri 2784 string fragment; /// the stuff after the # in a uri. 2785 2786 // idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility 2787 // the decode ones need to keep different names anyway because we can't overload on return values... 2788 static string encode(string s) { return std.uri.encodeComponent(s); } 2789 static string encode(string[string] s) { return encodeVariables(s); } 2790 static string encode(string[][string] s) { return encodeVariables(s); } 2791 2792 /// Breaks down a uri string to its components 2793 this(string uri) { 2794 reparse(uri); 2795 } 2796 2797 private void reparse(string uri) { 2798 // from RFC 3986 2799 // the ctRegex triples the compile time and makes ugly errors for no real benefit 2800 // it was a nice experiment but just not worth it. 2801 // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"; 2802 /* 2803 Captures: 2804 0 = whole url 2805 1 = scheme, with : 2806 2 = scheme, no : 2807 3 = authority, with // 2808 4 = authority, no // 2809 5 = path 2810 6 = query string, with ? 2811 7 = query string, no ? 2812 8 = anchor, with # 2813 9 = anchor, no # 2814 */ 2815 // Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer! 2816 // instead, I will DIY and cut that down to 0.6s on the same computer. 2817 /* 2818 2819 Note that authority is 2820 user:password@domain:port 2821 where the user:password@ part is optional, and the :port is optional. 2822 2823 Regex translation: 2824 2825 Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first. 2826 Authority must start with //, but cannot have any other /, ?, or # in it. It is optional. 2827 Path cannot have any ? or # in it. It is optional. 2828 Query must start with ? and must not have # in it. It is optional. 2829 Anchor must start with # and can have anything else in it to end of string. It is optional. 2830 */ 2831 2832 this = Uri.init; // reset all state 2833 2834 // empty uri = nothing special 2835 if(uri.length == 0) { 2836 return; 2837 } 2838 2839 size_t idx; 2840 2841 scheme_loop: foreach(char c; uri[idx .. $]) { 2842 switch(c) { 2843 case ':': 2844 case '/': 2845 case '?': 2846 case '#': 2847 break scheme_loop; 2848 default: 2849 } 2850 idx++; 2851 } 2852 2853 if(idx == 0 && uri[idx] == ':') { 2854 // this is actually a path! we skip way ahead 2855 goto path_loop; 2856 } 2857 2858 if(idx == uri.length) { 2859 // the whole thing is a path, apparently 2860 path = uri; 2861 return; 2862 } 2863 2864 if(idx > 0 && uri[idx] == ':') { 2865 scheme = uri[0 .. idx]; 2866 idx++; 2867 } else { 2868 // we need to rewind; it found a / but no :, so the whole thing is prolly a path... 2869 idx = 0; 2870 } 2871 2872 if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") { 2873 // we have an authority.... 2874 idx += 2; 2875 2876 auto authority_start = idx; 2877 authority_loop: foreach(char c; uri[idx .. $]) { 2878 switch(c) { 2879 case '/': 2880 case '?': 2881 case '#': 2882 break authority_loop; 2883 default: 2884 } 2885 idx++; 2886 } 2887 2888 auto authority = uri[authority_start .. idx]; 2889 2890 auto idx2 = authority.indexOf("@"); 2891 if(idx2 != -1) { 2892 userinfo = authority[0 .. idx2]; 2893 authority = authority[idx2 + 1 .. $]; 2894 } 2895 2896 if(authority.length && authority[0] == '[') { 2897 // ipv6 address special casing 2898 idx2 = authority.indexOf(']'); 2899 if(idx2 != -1) { 2900 auto end = authority[idx2 + 1 .. $]; 2901 if(end.length && end[0] == ':') 2902 idx2 = idx2 + 1; 2903 else 2904 idx2 = -1; 2905 } 2906 } else { 2907 idx2 = authority.indexOf(":"); 2908 } 2909 2910 if(idx2 == -1) { 2911 port = 0; // 0 means not specified; we should use the default for the scheme 2912 host = authority; 2913 } else { 2914 host = authority[0 .. idx2]; 2915 if(idx2 + 1 < authority.length) 2916 port = to!int(authority[idx2 + 1 .. $]); 2917 else 2918 port = 0; 2919 } 2920 } 2921 2922 path_loop: 2923 auto path_start = idx; 2924 2925 foreach(char c; uri[idx .. $]) { 2926 if(c == '?' || c == '#') 2927 break; 2928 idx++; 2929 } 2930 2931 path = uri[path_start .. idx]; 2932 2933 if(idx == uri.length) 2934 return; // nothing more to examine... 2935 2936 if(uri[idx] == '?') { 2937 idx++; 2938 auto query_start = idx; 2939 foreach(char c; uri[idx .. $]) { 2940 if(c == '#') 2941 break; 2942 idx++; 2943 } 2944 query = uri[query_start .. idx]; 2945 } 2946 2947 if(idx < uri.length && uri[idx] == '#') { 2948 idx++; 2949 fragment = uri[idx .. $]; 2950 } 2951 2952 // uriInvalidated = false; 2953 } 2954 2955 private string rebuildUri() const { 2956 string ret; 2957 if(scheme.length) 2958 ret ~= scheme ~ ":"; 2959 if(userinfo.length || host.length) 2960 ret ~= "//"; 2961 if(userinfo.length) 2962 ret ~= userinfo ~ "@"; 2963 if(host.length) 2964 ret ~= host; 2965 if(port) 2966 ret ~= ":" ~ to!string(port); 2967 2968 ret ~= path; 2969 2970 if(query.length) 2971 ret ~= "?" ~ query; 2972 2973 if(fragment.length) 2974 ret ~= "#" ~ fragment; 2975 2976 // uri = ret; 2977 // uriInvalidated = false; 2978 return ret; 2979 } 2980 2981 /// Converts the broken down parts back into a complete string 2982 string toString() const { 2983 // if(uriInvalidated) 2984 return rebuildUri(); 2985 } 2986 2987 /// Returns a new absolute Uri given a base. It treats this one as 2988 /// relative where possible, but absolute if not. (If protocol, domain, or 2989 /// other info is not set, the new one inherits it from the base.) 2990 /// 2991 /// Browsers use a function like this to figure out links in html. 2992 Uri basedOn(in Uri baseUrl) const { 2993 Uri n = this; // copies 2994 if(n.scheme == "data") 2995 return n; 2996 // n.uriInvalidated = true; // make sure we regenerate... 2997 2998 // userinfo is not inherited... is this wrong? 2999 3000 // if anything is given in the existing url, we don't use the base anymore. 3001 if(n.scheme.empty) { 3002 n.scheme = baseUrl.scheme; 3003 if(n.host.empty) { 3004 n.host = baseUrl.host; 3005 if(n.port == 0) { 3006 n.port = baseUrl.port; 3007 if(n.path.length > 0 && n.path[0] != '/') { 3008 auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1]; 3009 if(b.length == 0) 3010 b = "/"; 3011 n.path = b ~ n.path; 3012 } else if(n.path.length == 0) { 3013 n.path = baseUrl.path; 3014 } 3015 } 3016 } 3017 } 3018 3019 n.removeDots(); 3020 3021 return n; 3022 } 3023 3024 void removeDots() { 3025 auto parts = this.path.split("/"); 3026 string[] toKeep; 3027 foreach(part; parts) { 3028 if(part == ".") { 3029 continue; 3030 } else if(part == "..") { 3031 //if(toKeep.length > 1) 3032 toKeep = toKeep[0 .. $-1]; 3033 //else 3034 //toKeep = [""]; 3035 continue; 3036 } else { 3037 //if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0) 3038 //continue; // skip a `//` situation 3039 toKeep ~= part; 3040 } 3041 } 3042 3043 auto path = toKeep.join("/"); 3044 if(path.length && path[0] != '/') 3045 path = "/" ~ path; 3046 3047 this.path = path; 3048 } 3049 3050 unittest { 3051 auto uri = Uri("test.html"); 3052 assert(uri.path == "test.html"); 3053 uri = Uri("path/1/lol"); 3054 assert(uri.path == "path/1/lol"); 3055 uri = Uri("http://me@example.com"); 3056 assert(uri.scheme == "http"); 3057 assert(uri.userinfo == "me"); 3058 assert(uri.host == "example.com"); 3059 uri = Uri("http://example.com/#a"); 3060 assert(uri.scheme == "http"); 3061 assert(uri.host == "example.com"); 3062 assert(uri.fragment == "a"); 3063 uri = Uri("#foo"); 3064 assert(uri.fragment == "foo"); 3065 uri = Uri("?lol"); 3066 assert(uri.query == "lol"); 3067 uri = Uri("#foo?lol"); 3068 assert(uri.fragment == "foo?lol"); 3069 uri = Uri("?lol#foo"); 3070 assert(uri.fragment == "foo"); 3071 assert(uri.query == "lol"); 3072 3073 uri = Uri("http://127.0.0.1/"); 3074 assert(uri.host == "127.0.0.1"); 3075 assert(uri.port == 0); 3076 3077 uri = Uri("http://127.0.0.1:123/"); 3078 assert(uri.host == "127.0.0.1"); 3079 assert(uri.port == 123); 3080 3081 uri = Uri("http://[ff:ff::0]/"); 3082 assert(uri.host == "[ff:ff::0]"); 3083 3084 uri = Uri("http://[ff:ff::0]:123/"); 3085 assert(uri.host == "[ff:ff::0]"); 3086 assert(uri.port == 123); 3087 } 3088 3089 // This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover 3090 // the possibilities. 3091 unittest { 3092 auto url = Uri("cool.html"); // checking relative links 3093 3094 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html"); 3095 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html"); 3096 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html"); 3097 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html"); 3098 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); 3099 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html"); 3100 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html"); 3101 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html"); 3102 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); 3103 3104 url = Uri("/something/cool.html"); // same server, different path 3105 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html"); 3106 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html"); 3107 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html"); 3108 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html"); 3109 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); 3110 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html"); 3111 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html"); 3112 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html"); 3113 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); 3114 3115 url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment 3116 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer"); 3117 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer"); 3118 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer"); 3119 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer"); 3120 assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); 3121 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer"); 3122 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer"); 3123 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer"); 3124 assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); 3125 3126 url = Uri("/test/bar"); 3127 assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url)); 3128 assert(Uri("../").basedOn(url) == "/"); 3129 3130 url = Uri("http://example.com/"); 3131 assert(Uri("../foo").basedOn(url) == "http://example.com/foo"); 3132 3133 //auto uriBefore = url; 3134 url = Uri("#anchor"); // everything should remain the same except the anchor 3135 //uriBefore.anchor = "anchor"); 3136 //assert(url == uriBefore); 3137 3138 url = Uri("//example.com"); // same protocol, but different server. the path here should be blank. 3139 3140 url = Uri("//example.com/example.html"); // same protocol, but different server and path 3141 3142 url = Uri("http://example.com/test.html"); // completely absolute link should never be modified 3143 3144 url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path 3145 3146 // FIXME: add something for port too 3147 } 3148 3149 // these are like javascript's location.search and location.hash 3150 string search() const { 3151 return query.length ? ("?" ~ query) : ""; 3152 } 3153 string hash() const { 3154 return fragment.length ? ("#" ~ fragment) : ""; 3155 } 3156 } 3157 3158 3159 /* 3160 for session, see web.d 3161 */ 3162 3163 /// breaks down a url encoded string 3164 string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) { 3165 auto vars = data.split(separator); 3166 string[][string] _get; 3167 foreach(var; vars) { 3168 auto equal = var.indexOf("="); 3169 string name; 3170 string value; 3171 if(equal == -1) { 3172 name = decodeComponent(var); 3173 value = ""; 3174 } else { 3175 //_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " ")); 3176 // stupid + -> space conversion. 3177 name = decodeComponent(var[0..equal].replace("+", " ")); 3178 value = decodeComponent(var[equal + 1 .. $].replace("+", " ")); 3179 } 3180 3181 _get[name] ~= value; 3182 if(namesInOrder) 3183 (*namesInOrder) ~= name; 3184 if(valuesInOrder) 3185 (*valuesInOrder) ~= value; 3186 } 3187 return _get; 3188 } 3189 3190 /// breaks down a url encoded string, but only returns the last value of any array 3191 string[string] decodeVariablesSingle(string data) { 3192 string[string] va; 3193 auto varArray = decodeVariables(data); 3194 foreach(k, v; varArray) 3195 va[k] = v[$-1]; 3196 3197 return va; 3198 } 3199 3200 /// url encodes the whole string 3201 string encodeVariables(in string[string] data) { 3202 string ret; 3203 3204 bool outputted = false; 3205 foreach(k, v; data) { 3206 if(outputted) 3207 ret ~= "&"; 3208 else 3209 outputted = true; 3210 3211 ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); 3212 } 3213 3214 return ret; 3215 } 3216 3217 /// url encodes a whole string 3218 string encodeVariables(in string[][string] data) { 3219 string ret; 3220 3221 bool outputted = false; 3222 foreach(k, arr; data) { 3223 foreach(v; arr) { 3224 if(outputted) 3225 ret ~= "&"; 3226 else 3227 outputted = true; 3228 ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); 3229 } 3230 } 3231 3232 return ret; 3233 } 3234 3235 /// Encodes all but the explicitly unreserved characters per rfc 3986 3236 /// Alphanumeric and -_.~ are the only ones left unencoded 3237 /// name is borrowed from php 3238 string rawurlencode(in char[] data) { 3239 string ret; 3240 ret.reserve(data.length * 2); 3241 foreach(char c; data) { 3242 if( 3243 (c >= 'a' && c <= 'z') || 3244 (c >= 'A' && c <= 'Z') || 3245 (c >= '0' && c <= '9') || 3246 c == '-' || c == '_' || c == '.' || c == '~') 3247 { 3248 ret ~= c; 3249 } else { 3250 ret ~= '%'; 3251 // since we iterate on char, this should give us the octets of the full utf8 string 3252 ret ~= toHexUpper(c); 3253 } 3254 } 3255 3256 return ret; 3257 } 3258 3259 3260 // http helper functions 3261 3262 // for chunked responses (which embedded http does whenever possible) 3263 version(none) // this is moved up above to avoid making a copy of the data 3264 const(ubyte)[] makeChunk(const(ubyte)[] data) { 3265 const(ubyte)[] ret; 3266 3267 ret = cast(const(ubyte)[]) toHex(data.length); 3268 ret ~= cast(const(ubyte)[]) "\r\n"; 3269 ret ~= data; 3270 ret ~= cast(const(ubyte)[]) "\r\n"; 3271 3272 return ret; 3273 } 3274 3275 string toHex(long num) { 3276 string ret; 3277 while(num) { 3278 int v = num % 16; 3279 num /= 16; 3280 char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a'); 3281 ret ~= d; 3282 } 3283 3284 return to!string(array(ret.retro)); 3285 } 3286 3287 string toHexUpper(long num) { 3288 string ret; 3289 while(num) { 3290 int v = num % 16; 3291 num /= 16; 3292 char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A'); 3293 ret ~= d; 3294 } 3295 3296 if(ret.length == 1) 3297 ret ~= "0"; // url encoding requires two digits and that's what this function is used for... 3298 3299 return to!string(array(ret.retro)); 3300 } 3301 3302 3303 // the generic mixins 3304 3305 /// Use this instead of writing your own main 3306 mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) { 3307 mixin CustomCgiMain!(Cgi, fun, maxContentLength); 3308 } 3309 3310 /++ 3311 Boilerplate mixin for a main function that uses the [dispatcher] function. 3312 3313 You can send `typeof(null)` as the `Presenter` argument to use a generic one. 3314 3315 History: 3316 Added July 9, 2021 3317 +/ 3318 mixin template DispatcherMain(Presenter, DispatcherArgs...) { 3319 /++ 3320 Handler to the generated presenter you can use from your objects, etc. 3321 +/ 3322 Presenter activePresenter; 3323 3324 /++ 3325 Request handler that creates the presenter then forwards to the [dispatcher] function. 3326 Renders 404 if the dispatcher did not handle the request. 3327 3328 Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js" 3329 +/ 3330 void handler(Cgi cgi) { 3331 auto presenter = new Presenter; 3332 activePresenter = presenter; 3333 scope(exit) activePresenter = null; 3334 3335 if(cgi.dispatcher!DispatcherArgs(presenter)) 3336 return; 3337 3338 switch(cgi.pathInfo) { 3339 case "/style.css": 3340 cgi.setCache(true); 3341 cgi.setResponseContentType("text/css"); 3342 cgi.write(presenter.style(), true); 3343 break; 3344 case "/script.js": 3345 cgi.setCache(true); 3346 cgi.setResponseContentType("application/javascript"); 3347 cgi.write(presenter.script(), true); 3348 break; 3349 default: 3350 presenter.renderBasicError(cgi, 404); 3351 } 3352 } 3353 mixin GenericMain!handler; 3354 } 3355 3356 mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { 3357 class GenericPresenter : WebPresenter!GenericPresenter {} 3358 mixin DispatcherMain!(GenericPresenter, DispatcherArgs); 3359 } 3360 3361 private string simpleHtmlEncode(string s) { 3362 return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "<br />\n"); 3363 } 3364 3365 string messageFromException(Throwable t) { 3366 string message; 3367 if(t !is null) { 3368 debug message = t.toString(); 3369 else message = "An unexpected error has occurred."; 3370 } else { 3371 message = "Unknown error"; 3372 } 3373 return message; 3374 } 3375 3376 string plainHttpError(bool isCgi, string type, Throwable t) { 3377 auto message = messageFromException(t); 3378 message = simpleHtmlEncode(message); 3379 3380 return format("%s %s\r\nContent-Length: %s\r\n\r\n%s", 3381 isCgi ? "Status:" : "HTTP/1.0", 3382 type, message.length, message); 3383 } 3384 3385 // returns true if we were able to recover reasonably 3386 bool handleException(Cgi cgi, Throwable t) { 3387 if(cgi.isClosed) { 3388 // if the channel has been explicitly closed, we can't handle it here 3389 return true; 3390 } 3391 3392 if(cgi.outputtedResponseData) { 3393 // the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here. 3394 return false; // but I don't want to, since I don't know what condition the output is in; I don't want to inject something (nor check the content-type for that matter. So we say it was not a clean handling. 3395 } else { 3396 // no headers are sent, we can send a full blown error and recover 3397 cgi.setCache(false); 3398 cgi.setResponseContentType("text/html"); 3399 cgi.setResponseLocation(null); // cancel the redirect 3400 cgi.setResponseStatus("500 Internal Server Error"); 3401 cgi.write(simpleHtmlEncode(messageFromException(t))); 3402 cgi.close(); 3403 return true; 3404 } 3405 } 3406 3407 bool isCgiRequestMethod(string s) { 3408 s = s.toUpper(); 3409 if(s == "COMMANDLINE") 3410 return true; 3411 foreach(member; __traits(allMembers, Cgi.RequestMethod)) 3412 if(s == member) 3413 return true; 3414 return false; 3415 } 3416 3417 /// If you want to use a subclass of Cgi with generic main, use this mixin. 3418 mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) { 3419 // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere 3420 void main(string[] args) { 3421 cgiMainImpl!(fun, CustomCgi, maxContentLength)(args); 3422 } 3423 } 3424 3425 version(embedded_httpd_processes) 3426 __gshared int processPoolSize = 8; 3427 3428 // Returns true if run. You should exit the program after that. 3429 bool tryAddonServers(string[] args) { 3430 if(args.length > 1) { 3431 // run the special separate processes if needed 3432 switch(args[1]) { 3433 case "--websocket-server": 3434 version(with_addon_servers) 3435 websocketServers[args[2]](args[3 .. $]); 3436 else 3437 printf("Add-on servers not compiled in.\n"); 3438 return true; 3439 case "--websocket-servers": 3440 import core.demangle; 3441 version(with_addon_servers_connections) 3442 foreach(k, v; websocketServers) 3443 writeln(k, "\t", demangle(k)); 3444 return true; 3445 case "--session-server": 3446 version(with_addon_servers) 3447 runSessionServer(); 3448 else 3449 printf("Add-on servers not compiled in.\n"); 3450 return true; 3451 case "--event-server": 3452 version(with_addon_servers) 3453 runEventServer(); 3454 else 3455 printf("Add-on servers not compiled in.\n"); 3456 return true; 3457 case "--timer-server": 3458 version(with_addon_servers) 3459 runTimerServer(); 3460 else 3461 printf("Add-on servers not compiled in.\n"); 3462 return true; 3463 case "--timed-jobs": 3464 import core.demangle; 3465 version(with_addon_servers_connections) 3466 foreach(k, v; scheduledJobHandlers) 3467 writeln(k, "\t", demangle(k)); 3468 return true; 3469 case "--timed-job": 3470 scheduledJobHandlers[args[2]](args[3 .. $]); 3471 return true; 3472 default: 3473 // intentionally blank - do nothing and carry on to run normally 3474 } 3475 } 3476 return false; 3477 } 3478 3479 /// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args. 3480 bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) { 3481 // we support command line thing for easy testing everywhere 3482 // it needs to be called ./app method uri [other args...] 3483 if(args.length >= 3 && isCgiRequestMethod(args[1])) { 3484 Cgi cgi = new CustomCgi(args); 3485 scope(exit) cgi.dispose(); 3486 fun(cgi); 3487 cgi.close(); 3488 return true; 3489 } 3490 return false; 3491 } 3492 3493 /++ 3494 A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] to an example of how you can use it. 3495 3496 History: 3497 Added Sept 26, 2020 (release version 8.5). 3498 +/ 3499 struct RequestServer { 3500 /// 3501 string listeningHost = defaultListeningHost(); 3502 /// 3503 ushort listeningPort = defaultListeningPort(); 3504 3505 /++ 3506 Uses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the 3507 other hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But 3508 if you have to, you probably want to set this to false and use an explicit threaded server with [serveEmbeddedHttp]) and 3509 [stop] may not work as well. 3510 3511 History: 3512 Added August 12, 2022 (dub v10.9). Previously, this was only configurable through the `-version=cgi_no_fork` 3513 argument to dmd. That version still defines the value of `cgi_use_fork_default`, used to initialize this, for 3514 compatibility. 3515 +/ 3516 bool useFork = cgi_use_fork_default; 3517 3518 /++ 3519 Determines the number of worker threads to spawn per process, for server modes that use worker threads. 0 will use a 3520 default based on the number of cpus modified by the server mode. 3521 3522 History: 3523 Added August 12, 2022 (dub v10.9) 3524 +/ 3525 int numberOfThreads = 0; 3526 3527 /// 3528 this(string defaultHost, ushort defaultPort) { 3529 this.listeningHost = defaultHost; 3530 this.listeningPort = defaultPort; 3531 } 3532 3533 /// 3534 this(ushort defaultPort) { 3535 listeningPort = defaultPort; 3536 } 3537 3538 /++ 3539 Reads the command line arguments into the values here. 3540 3541 Possible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. 3542 +/ 3543 void configureFromCommandLine(string[] args) { 3544 bool foundPort = false; 3545 bool foundHost = false; 3546 bool foundUid = false; 3547 bool foundGid = false; 3548 foreach(arg; args) { 3549 if(foundPort) { 3550 listeningPort = to!ushort(arg); 3551 foundPort = false; 3552 } 3553 if(foundHost) { 3554 listeningHost = arg; 3555 foundHost = false; 3556 } 3557 if(foundUid) { 3558 privilegesDropToUid = to!uid_t(arg); 3559 foundUid = false; 3560 } 3561 if(foundGid) { 3562 privilegesDropToGid = to!gid_t(arg); 3563 foundGid = false; 3564 } 3565 if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") 3566 foundHost = true; 3567 else if(arg == "--port" || arg == "-p" || arg == "/port" || arg == "--listening-port") 3568 foundPort = true; 3569 else if(arg == "--uid") 3570 foundUid = true; 3571 else if(arg == "--gid") 3572 foundGid = true; 3573 } 3574 } 3575 3576 version(Windows) { 3577 private alias uid_t = int; 3578 private alias gid_t = int; 3579 } 3580 3581 /// user (uid) to drop privileges to 3582 /// 0 … do nothing 3583 uid_t privilegesDropToUid = 0; 3584 /// group (gid) to drop privileges to 3585 /// 0 … do nothing 3586 gid_t privilegesDropToGid = 0; 3587 3588 private void dropPrivileges() { 3589 version(Posix) { 3590 import core.sys.posix.unistd; 3591 3592 if (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0) 3593 throw new Exception("Dropping privileges via setgid() failed."); 3594 3595 if (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0) 3596 throw new Exception("Dropping privileges via setuid() failed."); 3597 } 3598 else { 3599 // FIXME: Windows? 3600 //pragma(msg, "Dropping privileges is not implemented for this platform"); 3601 } 3602 3603 // done, set zero 3604 privilegesDropToGid = 0; 3605 privilegesDropToUid = 0; 3606 } 3607 3608 /++ 3609 Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders 3610 3611 History: 3612 Added Oct 10, 2020. 3613 Example: 3614 3615 --- 3616 import arsd.cgi; 3617 void main() { 3618 RequestServer server = RequestServer("127.0.0.1", 6789); 3619 string oauthCode; 3620 string oauthScope; 3621 server.serveHttpOnce!((cgi) { 3622 oauthCode = cgi.request("code"); 3623 oauthScope = cgi.request("scope"); 3624 cgi.write("Thank you, please return to the application."); 3625 }); 3626 // use the code and scope given 3627 } 3628 --- 3629 +/ 3630 void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 3631 import std.socket; 3632 3633 bool tcp; 3634 void delegate() cleanup; 3635 auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges); 3636 auto connection = socket.accept(); 3637 doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection); 3638 3639 if(cleanup) 3640 cleanup(); 3641 } 3642 3643 /++ 3644 Starts serving requests according to the current configuration. 3645 +/ 3646 void serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 3647 version(netman_httpd) { 3648 // Obsolete! 3649 3650 import arsd.httpd; 3651 // what about forwarding the other constructor args? 3652 // this probably needs a whole redoing... 3653 serveHttp!CustomCgi(&fun, listeningPort);//5005); 3654 return; 3655 } else 3656 version(embedded_httpd_processes) { 3657 serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this); 3658 } else 3659 version(embedded_httpd_threads) { 3660 serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)(); 3661 } else 3662 version(scgi) { 3663 serveScgi!(fun, CustomCgi, maxContentLength)(); 3664 } else 3665 version(fastcgi) { 3666 serveFastCgi!(fun, CustomCgi, maxContentLength)(this); 3667 } else 3668 version(stdio_http) { 3669 serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)(); 3670 } else { 3671 //version=plain_cgi; 3672 handleCgiRequest!(fun, CustomCgi, maxContentLength)(); 3673 } 3674 } 3675 3676 /++ 3677 Runs the embedded HTTP thread server specifically, regardless of which build configuration you have. 3678 3679 If you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though. 3680 +/ 3681 void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(ThisFor!fun _this) { 3682 globalStopFlag = false; 3683 static if(__traits(isStaticFunction, fun)) 3684 alias funToUse = fun; 3685 else 3686 void funToUse(CustomCgi cgi) { 3687 static if(__VERSION__ > 2097) 3688 __traits(child, _this, fun)(cgi); 3689 else static assert(0, "Not implemented in your compiler version!"); 3690 } 3691 auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); 3692 manager.listen(); 3693 } 3694 3695 /++ 3696 Runs the embedded SCGI server specifically, regardless of which build configuration you have. 3697 +/ 3698 void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 3699 globalStopFlag = false; 3700 auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); 3701 manager.listen(); 3702 } 3703 3704 /++ 3705 Serves a single "connection", but the connection is spoken on stdin and stdout instead of on a socket. 3706 3707 Intended for cases like working from systemd, like discussed here: https://forum.dlang.org/post/avmkfdiitirnrenzljwc@forum.dlang.org 3708 3709 History: 3710 Added May 29, 2021 3711 +/ 3712 void serveSingleHttpConnectionOnStdio(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 3713 doThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin()); 3714 } 3715 3716 /++ 3717 The [stop] function sets a flag that request handlers can (and should) check periodically. If a handler doesn't 3718 respond to this flag, the library will force the issue. This determines when and how the issue will be forced. 3719 +/ 3720 enum ForceStop { 3721 /++ 3722 Stops accepting new requests, but lets ones already in the queue start and complete before exiting. 3723 +/ 3724 afterQueuedRequestsComplete, 3725 /++ 3726 Finishes requests already started their handlers, but drops any others in the queue. Streaming handlers 3727 should cooperate and exit gracefully, but if they don't, it will continue waiting for them. 3728 +/ 3729 afterCurrentRequestsComplete, 3730 /++ 3731 Partial response writes will throw an exception, cancelling any streaming response, but complete 3732 writes will continue to process. Request handlers that respect the stop token will also gracefully cancel. 3733 +/ 3734 cancelStreamingRequestsEarly, 3735 /++ 3736 All writes will throw. 3737 +/ 3738 cancelAllRequestsEarly, 3739 /++ 3740 Use OS facilities to forcibly kill running threads. The server process will be in an undefined state after this call (if this call ever returns). 3741 +/ 3742 forciblyTerminate, 3743 } 3744 3745 version(embedded_httpd_processes) {} else 3746 /++ 3747 Stops serving after the current requests are completed. 3748 3749 Bugs: 3750 Not implemented on version=embedded_httpd_processes, version=fastcgi on any system, or embedded_httpd on Windows (it does work on embedded_httpd_hybrid 3751 on Windows however). Only partially implemented on non-Linux posix systems. 3752 3753 You might also try SIGINT perhaps. 3754 3755 The stopPriority is not yet fully implemented. 3756 +/ 3757 static void stop(ForceStop stopPriority = ForceStop.afterCurrentRequestsComplete) { 3758 globalStopFlag = true; 3759 3760 version(Posix) 3761 if(cancelfd > 0) { 3762 ulong a = 1; 3763 core.sys.posix.unistd.write(cancelfd, &a, a.sizeof); 3764 } 3765 version(Windows) 3766 if(iocp) { 3767 foreach(i; 0 .. 16) // FIXME 3768 PostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null); 3769 } 3770 } 3771 } 3772 3773 private alias AliasSeq(T...) = T; 3774 3775 version(with_breaking_cgi_features) 3776 mixin(q{ 3777 template ThisFor(alias t) { 3778 static if(__traits(isStaticFunction, t)) { 3779 alias ThisFor = AliasSeq!(); 3780 } else { 3781 alias ThisFor = __traits(parent, t); 3782 } 3783 } 3784 }); 3785 else 3786 alias ThisFor(alias t) = AliasSeq!(); 3787 3788 private __gshared bool globalStopFlag = false; 3789 3790 version(embedded_httpd_processes) 3791 void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) { 3792 import core.sys.posix.unistd; 3793 import core.sys.posix.sys.socket; 3794 import core.sys.posix.netinet.in_; 3795 //import std.c.linux.socket; 3796 3797 int sock = socket(AF_INET, SOCK_STREAM, 0); 3798 if(sock == -1) 3799 throw new Exception("socket"); 3800 3801 cloexec(sock); 3802 3803 { 3804 3805 sockaddr_in addr; 3806 addr.sin_family = AF_INET; 3807 addr.sin_port = htons(params.listeningPort); 3808 auto lh = params.listeningHost; 3809 if(lh.length) { 3810 if(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1) 3811 throw new Exception("bad listening host given, please use an IP address.\nExample: --listening-host 127.0.0.1 means listen only on Localhost.\nExample: --listening-host 0.0.0.0 means listen on all interfaces.\nOr you can pass any other single numeric IPv4 address."); 3812 } else 3813 addr.sin_addr.s_addr = INADDR_ANY; 3814 3815 // HACKISH 3816 int on = 1; 3817 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof); 3818 // end hack 3819 3820 3821 if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { 3822 close(sock); 3823 throw new Exception("bind"); 3824 } 3825 3826 // FIXME: if this queue is full, it will just ignore it 3827 // and wait for the client to retransmit it. This is an 3828 // obnoxious timeout condition there. 3829 if(sock.listen(128) == -1) { 3830 close(sock); 3831 throw new Exception("listen"); 3832 } 3833 params.dropPrivileges(); 3834 } 3835 3836 version(embedded_httpd_processes_accept_after_fork) {} else { 3837 int pipeReadFd; 3838 int pipeWriteFd; 3839 3840 { 3841 int[2] pipeFd; 3842 if(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) { 3843 import core.stdc.errno; 3844 throw new Exception("pipe failed " ~ to!string(errno)); 3845 } 3846 3847 pipeReadFd = pipeFd[0]; 3848 pipeWriteFd = pipeFd[1]; 3849 } 3850 } 3851 3852 3853 int processCount; 3854 pid_t newPid; 3855 reopen: 3856 while(processCount < processPoolSize) { 3857 newPid = fork(); 3858 if(newPid == 0) { 3859 // start serving on the socket 3860 //ubyte[4096] backingBuffer; 3861 for(;;) { 3862 bool closeConnection; 3863 uint i; 3864 sockaddr addr; 3865 i = addr.sizeof; 3866 version(embedded_httpd_processes_accept_after_fork) { 3867 int s = accept(sock, &addr, &i); 3868 int opt = 1; 3869 import core.sys.posix.netinet.tcp; 3870 // the Cgi class does internal buffering, so disabling this 3871 // helps with latency in many cases... 3872 setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); 3873 cloexec(s); 3874 } else { 3875 int s; 3876 auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s); 3877 if(readret != s.sizeof) { 3878 import core.stdc.errno; 3879 throw new Exception("pipe read failed " ~ to!string(errno)); 3880 } 3881 3882 //writeln("process ", getpid(), " got socket ", s); 3883 } 3884 3885 try { 3886 3887 if(s == -1) 3888 throw new Exception("accept"); 3889 3890 scope(failure) close(s); 3891 //ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer; 3892 auto ir = new BufferedInputRange(s); 3893 //auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer); 3894 3895 while(!ir.empty) { 3896 //ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer; 3897 3898 Cgi cgi; 3899 try { 3900 cgi = new CustomCgi(ir, &closeConnection); 3901 cgi._outputFileHandle = cast(CgiConnectionHandle) s; 3902 // if we have a single process and the browser tries to leave the connection open while concurrently requesting another, it will block everything an deadlock since there's no other server to accept it. By closing after each request in this situation, it tells the browser to serialize for us. 3903 if(processPoolSize <= 1) 3904 closeConnection = true; 3905 //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection); 3906 } catch(Throwable t) { 3907 // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P 3908 // anyway let's kill the connection 3909 version(CRuntime_Musl) { 3910 // LockingTextWriter fails here 3911 // so working around it 3912 auto estr = t.toString(); 3913 stderr.rawWrite(estr); 3914 stderr.rawWrite("\n"); 3915 } else 3916 stderr.writeln(t.toString()); 3917 sendAll(ir.source, plainHttpError(false, "400 Bad Request", t)); 3918 closeConnection = true; 3919 break; 3920 } 3921 assert(cgi !is null); 3922 scope(exit) 3923 cgi.dispose(); 3924 3925 try { 3926 fun(cgi); 3927 cgi.close(); 3928 if(cgi.websocketMode) 3929 closeConnection = true; 3930 } catch(ConnectionException ce) { 3931 closeConnection = true; 3932 } catch(Throwable t) { 3933 // a processing error can be recovered from 3934 version(CRuntime_Musl) { 3935 // LockingTextWriter fails here 3936 // so working around it 3937 auto estr = t.toString(); 3938 stderr.rawWrite(estr); 3939 } else { 3940 stderr.writeln(t.toString); 3941 } 3942 if(!handleException(cgi, t)) 3943 closeConnection = true; 3944 } 3945 3946 if(closeConnection) { 3947 ir.source.close(); 3948 break; 3949 } else { 3950 if(!ir.empty) 3951 ir.popFront(); // get the next 3952 else if(ir.sourceClosed) { 3953 ir.source.close(); 3954 } 3955 } 3956 } 3957 3958 ir.source.close(); 3959 } catch(Throwable t) { 3960 version(CRuntime_Musl) {} else 3961 debug writeln(t); 3962 // most likely cause is a timeout 3963 } 3964 } 3965 } else if(newPid < 0) { 3966 throw new Exception("fork failed"); 3967 } else { 3968 processCount++; 3969 } 3970 } 3971 3972 // the parent should wait for its children... 3973 if(newPid) { 3974 import core.sys.posix.sys.wait; 3975 3976 version(embedded_httpd_processes_accept_after_fork) {} else { 3977 import core.sys.posix.sys.select; 3978 int[] fdQueue; 3979 while(true) { 3980 // writeln("select call"); 3981 int nfds = pipeWriteFd; 3982 if(sock > pipeWriteFd) 3983 nfds = sock; 3984 nfds += 1; 3985 fd_set read_fds; 3986 fd_set write_fds; 3987 FD_ZERO(&read_fds); 3988 FD_ZERO(&write_fds); 3989 FD_SET(sock, &read_fds); 3990 if(fdQueue.length) 3991 FD_SET(pipeWriteFd, &write_fds); 3992 auto ret = select(nfds, &read_fds, &write_fds, null, null); 3993 if(ret == -1) { 3994 import core.stdc.errno; 3995 if(errno == EINTR) 3996 goto try_wait; 3997 else 3998 throw new Exception("wtf select"); 3999 } 4000 4001 int s = -1; 4002 if(FD_ISSET(sock, &read_fds)) { 4003 uint i; 4004 sockaddr addr; 4005 i = addr.sizeof; 4006 s = accept(sock, &addr, &i); 4007 cloexec(s); 4008 import core.sys.posix.netinet.tcp; 4009 int opt = 1; 4010 setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); 4011 } 4012 4013 if(FD_ISSET(pipeWriteFd, &write_fds)) { 4014 if(s == -1 && fdQueue.length) { 4015 s = fdQueue[0]; 4016 fdQueue = fdQueue[1 .. $]; // FIXME reuse buffer 4017 } 4018 write_fd(pipeWriteFd, &s, s.sizeof, s); 4019 close(s); // we are done with it, let the other process take ownership 4020 } else 4021 fdQueue ~= s; 4022 } 4023 } 4024 4025 try_wait: 4026 4027 int status; 4028 while(-1 != wait(&status)) { 4029 version(CRuntime_Musl) {} else { 4030 import std.stdio; writeln("Process died ", status); 4031 } 4032 processCount--; 4033 goto reopen; 4034 } 4035 close(sock); 4036 } 4037 } 4038 4039 version(fastcgi) 4040 void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) { 4041 // SetHandler fcgid-script 4042 FCGX_Stream* input, output, error; 4043 FCGX_ParamArray env; 4044 4045 4046 4047 const(ubyte)[] getFcgiChunk() { 4048 const(ubyte)[] ret; 4049 while(FCGX_HasSeenEOF(input) != -1) 4050 ret ~= cast(ubyte) FCGX_GetChar(input); 4051 return ret; 4052 } 4053 4054 void writeFcgi(const(ubyte)[] data) { 4055 FCGX_PutStr(data.ptr, data.length, output); 4056 } 4057 4058 void doARequest() { 4059 string[string] fcgienv; 4060 4061 for(auto e = env; e !is null && *e !is null; e++) { 4062 string cur = to!string(*e); 4063 auto idx = cur.indexOf("="); 4064 string name, value; 4065 if(idx == -1) 4066 name = cur; 4067 else { 4068 name = cur[0 .. idx]; 4069 value = cur[idx + 1 .. $]; 4070 } 4071 4072 fcgienv[name] = value; 4073 } 4074 4075 void flushFcgi() { 4076 FCGX_FFlush(output); 4077 } 4078 4079 Cgi cgi; 4080 try { 4081 cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); 4082 } catch(Throwable t) { 4083 FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); 4084 writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t)); 4085 return; //continue; 4086 } 4087 assert(cgi !is null); 4088 scope(exit) cgi.dispose(); 4089 try { 4090 fun(cgi); 4091 cgi.close(); 4092 } catch(Throwable t) { 4093 // log it to the error stream 4094 FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); 4095 // handle it for the user, if we can 4096 if(!handleException(cgi, t)) 4097 return; // continue; 4098 } 4099 } 4100 4101 auto lp = params.listeningPort; 4102 auto host = params.listeningHost; 4103 4104 FCGX_Request request; 4105 if(lp || !host.empty) { 4106 // if a listening port was specified on the command line, we want to spawn ourself 4107 // (needed for nginx without spawn-fcgi, e.g. on Windows) 4108 FCGX_Init(); 4109 4110 int sock; 4111 4112 if(host.startsWith("unix:")) { 4113 sock = FCGX_OpenSocket(toStringz(params.listeningHost["unix:".length .. $]), 12); 4114 } else if(host.startsWith("abstract:")) { 4115 sock = FCGX_OpenSocket(toStringz("\0" ~ params.listeningHost["abstract:".length .. $]), 12); 4116 } else { 4117 sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); 4118 } 4119 4120 if(sock < 0) 4121 throw new Exception("Couldn't listen on the port"); 4122 FCGX_InitRequest(&request, sock, 0); 4123 while(FCGX_Accept_r(&request) >= 0) { 4124 input = request.inStream; 4125 output = request.outStream; 4126 error = request.errStream; 4127 env = request.envp; 4128 doARequest(); 4129 } 4130 } else { 4131 // otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd) 4132 // using the version with a global variable since we are separate processes anyway 4133 while(FCGX_Accept(&input, &output, &error, &env) >= 0) { 4134 doARequest(); 4135 } 4136 } 4137 } 4138 4139 /// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others. 4140 ushort defaultListeningPort() { 4141 version(netman_httpd) 4142 return 8080; 4143 else version(embedded_httpd_processes) 4144 return 8085; 4145 else version(embedded_httpd_threads) 4146 return 8085; 4147 else version(scgi) 4148 return 4000; 4149 else 4150 return 0; 4151 } 4152 4153 /// Default host for listening. 127.0.0.1 for scgi, null (aka all interfaces) for all others. If you want the server directly accessible from other computers on the network, normally use null. If not, 127.0.0.1 is a bit better. Settable with default handlers with --listening-host command line argument. 4154 string defaultListeningHost() { 4155 version(netman_httpd) 4156 return null; 4157 else version(embedded_httpd_processes) 4158 return null; 4159 else version(embedded_httpd_threads) 4160 return null; 4161 else version(scgi) 4162 return "127.0.0.1"; 4163 else 4164 return null; 4165 4166 } 4167 4168 /++ 4169 This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`. 4170 4171 Params: 4172 fun = Your request handler 4173 CustomCgi = a subclass of Cgi, if you wise to customize it further 4174 maxContentLength = max POST size you want to allow 4175 args = command-line arguments 4176 4177 History: 4178 Documented Sept 26, 2020. 4179 +/ 4180 void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) { 4181 if(tryAddonServers(args)) 4182 return; 4183 4184 if(trySimulatedRequest!(fun, CustomCgi)(args)) 4185 return; 4186 4187 RequestServer server; 4188 // you can change the port here if you like 4189 // server.listeningPort = 9000; 4190 4191 // then call this to let the command line args override your default 4192 server.configureFromCommandLine(args); 4193 4194 // and serve the request(s). 4195 server.serve!(fun, CustomCgi, maxContentLength)(); 4196 } 4197 4198 //version(plain_cgi) 4199 void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4200 // standard CGI is the default version 4201 4202 4203 // Set stdin to binary mode if necessary to avoid mangled newlines 4204 // the fact that stdin is global means this could be trouble but standard cgi request 4205 // handling is one per process anyway so it shouldn't actually be threaded here or anything. 4206 version(Windows) { 4207 version(Win64) 4208 _setmode(std.stdio.stdin.fileno(), 0x8000); 4209 else 4210 setmode(std.stdio.stdin.fileno(), 0x8000); 4211 } 4212 4213 Cgi cgi; 4214 try { 4215 cgi = new CustomCgi(maxContentLength); 4216 version(Posix) 4217 cgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout 4218 else version(Windows) 4219 cgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE); 4220 else static assert(0); 4221 } catch(Throwable t) { 4222 version(CRuntime_Musl) { 4223 // LockingTextWriter fails here 4224 // so working around it 4225 auto s = t.toString(); 4226 stderr.rawWrite(s); 4227 stdout.rawWrite(plainHttpError(true, "400 Bad Request", t)); 4228 } else { 4229 stderr.writeln(t.msg); 4230 // the real http server will probably handle this; 4231 // most likely, this is a bug in Cgi. But, oh well. 4232 stdout.write(plainHttpError(true, "400 Bad Request", t)); 4233 } 4234 return; 4235 } 4236 assert(cgi !is null); 4237 scope(exit) cgi.dispose(); 4238 4239 try { 4240 fun(cgi); 4241 cgi.close(); 4242 } catch (Throwable t) { 4243 version(CRuntime_Musl) { 4244 // LockingTextWriter fails here 4245 // so working around it 4246 auto s = t.msg; 4247 stderr.rawWrite(s); 4248 } else { 4249 stderr.writeln(t.msg); 4250 } 4251 if(!handleException(cgi, t)) 4252 return; 4253 } 4254 } 4255 4256 private __gshared int cancelfd = -1; 4257 4258 /+ 4259 The event loop for embedded_httpd_threads will prolly fiber dispatch 4260 cgi constructors too, so slow posts will not monopolize a worker thread. 4261 4262 May want to provide the worker task system just need to ensure all the fibers 4263 has a big enough stack for real work... would also ideally like to reuse them. 4264 4265 4266 So prolly bir would switch it to nonblocking. If it would block, it epoll 4267 registers one shot with this existing fiber to take it over. 4268 4269 new connection comes in. it picks a fiber off the free list, 4270 or if there is none, it creates a new one. this fiber handles 4271 this connection the whole time. 4272 4273 epoll triggers the fiber when something comes in. it is called by 4274 a random worker thread, it might change at any time. at least during 4275 the constructor. maybe into the main body it will stay tied to a thread 4276 just so TLS stuff doesn't randomly change in the middle. but I could 4277 specify if you yield all bets are off. 4278 4279 when the request is finished, if there's more data buffered, it just 4280 keeps going. if there is no more data buffered, it epoll ctls to 4281 get triggered when more data comes in. all one shot. 4282 4283 when a connection is closed, the fiber returns and is then reset 4284 and added to the free list. if the free list is full, the fiber is 4285 just freed, this means it will balloon to a certain size but not generally 4286 grow beyond that unless the activity keeps going. 4287 4288 256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory. 4289 4290 So the fiber has its own magic methods to read and write. if they would block, it registers 4291 for epoll and yields. when it returns, it read/writes and then returns back normal control. 4292 4293 basically you issue the command and it tells you when it is done 4294 4295 it needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued 4296 4297 +/ 4298 4299 /++ 4300 The stack size when a fiber is created. You can set this from your main or from a shared static constructor 4301 to optimize your memory use if you know you don't need this much space. Be careful though, some functions use 4302 more stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast! 4303 4304 History: 4305 Added July 10, 2021. Previously, it used the druntime default of 16 KB. 4306 +/ 4307 version(cgi_use_fiber) 4308 __gshared size_t fiberStackSize = 4096 * 100; 4309 4310 version(cgi_use_fiber) 4311 class CgiFiber : Fiber { 4312 private void function(Socket) f_handler; 4313 private void f_handler_dg(Socket s) { // to avoid extra allocation w/ function 4314 f_handler(s); 4315 } 4316 this(void function(Socket) handler) { 4317 this.f_handler = handler; 4318 this(&f_handler_dg); 4319 } 4320 4321 this(void delegate(Socket) handler) { 4322 this.handler = handler; 4323 super(&run, fiberStackSize); 4324 } 4325 4326 Socket connection; 4327 void delegate(Socket) handler; 4328 4329 void run() { 4330 handler(connection); 4331 } 4332 4333 void delegate() postYield; 4334 4335 private void setPostYield(scope void delegate() py) @nogc { 4336 postYield = cast(void delegate()) py; 4337 } 4338 4339 void proceed() { 4340 try { 4341 call(); 4342 auto py = postYield; 4343 postYield = null; 4344 if(py !is null) 4345 py(); 4346 } catch(Exception e) { 4347 if(connection) 4348 connection.close(); 4349 goto terminate; 4350 } 4351 4352 if(state == State.TERM) { 4353 terminate: 4354 import core.memory; 4355 GC.removeRoot(cast(void*) this); 4356 } 4357 } 4358 } 4359 4360 version(cgi_use_fiber) 4361 version(Windows) { 4362 4363 extern(Windows) private { 4364 4365 import core.sys.windows.mswsock; 4366 4367 alias GROUP=uint; 4368 alias LPWSAPROTOCOL_INFOW = void*; 4369 SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags); 4370 int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); 4371 int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); 4372 4373 struct WSABUF { 4374 ULONG len; 4375 CHAR *buf; 4376 } 4377 alias LPWSABUF = WSABUF*; 4378 4379 alias WSAOVERLAPPED = OVERLAPPED; 4380 alias LPWSAOVERLAPPED = LPOVERLAPPED; 4381 /+ 4382 4383 alias LPFN_ACCEPTEX = 4384 BOOL 4385 function( 4386 SOCKET sListenSocket, 4387 SOCKET sAcceptSocket, 4388 //_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, 4389 void* lpOutputBuffer, 4390 WORD dwReceiveDataLength, 4391 WORD dwLocalAddressLength, 4392 WORD dwRemoteAddressLength, 4393 LPDWORD lpdwBytesReceived, 4394 LPOVERLAPPED lpOverlapped 4395 ); 4396 4397 enum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]); 4398 +/ 4399 4400 enum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]); 4401 } 4402 4403 private class PseudoblockingOverlappedSocket : Socket { 4404 SOCKET handle; 4405 4406 CgiFiber fiber; 4407 4408 this(AddressFamily af, SocketType st) { 4409 auto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/); 4410 if(!handle) 4411 throw new Exception("WSASocketW"); 4412 this.handle = handle; 4413 4414 iocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0); 4415 4416 if(iocp is null) { 4417 writeln(GetLastError()); 4418 throw new Exception("CreateIoCompletionPort"); 4419 } 4420 4421 super(cast(socket_t) handle, af); 4422 } 4423 this() pure nothrow @trusted { assert(0); } 4424 4425 override void blocking(bool) {} // meaningless to us, just ignore it. 4426 4427 protected override Socket accepting() pure nothrow { 4428 assert(0); 4429 } 4430 4431 bool addressesParsed; 4432 Address la; 4433 Address ra; 4434 4435 private void populateAddresses() { 4436 if(addressesParsed) 4437 return; 4438 addressesParsed = true; 4439 4440 int lalen, ralen; 4441 4442 sockaddr_in* la; 4443 sockaddr_in* ra; 4444 4445 lpfnGetAcceptExSockaddrs( 4446 scratchBuffer.ptr, 4447 0, // same as in the AcceptEx call! 4448 sockaddr_in.sizeof + 16, 4449 sockaddr_in.sizeof + 16, 4450 cast(sockaddr**) &la, 4451 &lalen, 4452 cast(sockaddr**) &ra, 4453 &ralen 4454 ); 4455 4456 if(la) 4457 this.la = new InternetAddress(*la); 4458 if(ra) 4459 this.ra = new InternetAddress(*ra); 4460 4461 } 4462 4463 override @property @trusted Address localAddress() { 4464 populateAddresses(); 4465 return la; 4466 } 4467 override @property @trusted Address remoteAddress() { 4468 populateAddresses(); 4469 return ra; 4470 } 4471 4472 PseudoblockingOverlappedSocket accepted; 4473 4474 __gshared static LPFN_ACCEPTEX lpfnAcceptEx; 4475 __gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs; 4476 4477 override Socket accept() @trusted { 4478 __gshared static LPFN_ACCEPTEX lpfnAcceptEx; 4479 4480 if(lpfnAcceptEx is null) { 4481 DWORD dwBytes; 4482 GUID GuidAcceptEx = WSAID_ACCEPTEX; 4483 4484 auto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, 4485 &GuidAcceptEx, GuidAcceptEx.sizeof, 4486 &lpfnAcceptEx, lpfnAcceptEx.sizeof, 4487 &dwBytes, null, null); 4488 4489 GuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS; 4490 iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, 4491 &GuidAcceptEx, GuidAcceptEx.sizeof, 4492 &lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof, 4493 &dwBytes, null, null); 4494 4495 } 4496 4497 auto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); 4498 accepted = pfa; 4499 4500 SOCKET pendingForAccept = pfa.handle; 4501 DWORD ignored; 4502 4503 auto ret = lpfnAcceptEx(handle, 4504 pendingForAccept, 4505 // buffer to receive up front 4506 pfa.scratchBuffer.ptr, 4507 0, 4508 // size of local and remote addresses. normally + 16. 4509 sockaddr_in.sizeof + 16, 4510 sockaddr_in.sizeof + 16, 4511 &ignored, // bytes would be given through the iocp instead but im not even requesting the thing 4512 &overlapped 4513 ); 4514 4515 return pfa; 4516 } 4517 4518 override void connect(Address to) { assert(0); } 4519 4520 DWORD lastAnswer; 4521 ubyte[1024] scratchBuffer; 4522 static assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32); 4523 4524 WSABUF[1] buffer; 4525 OVERLAPPED overlapped; 4526 override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted { 4527 overlapped = overlapped.init; 4528 buffer[0].len = cast(DWORD) buf.length; 4529 buffer[0].buf = cast(CHAR*) buf.ptr; 4530 fiber.setPostYield( () { 4531 if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) { 4532 if(GetLastError() != 997) { 4533 //throw new Exception("WSASend fail"); 4534 } 4535 } 4536 }); 4537 4538 Fiber.yield(); 4539 return lastAnswer; 4540 } 4541 override ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted { 4542 overlapped = overlapped.init; 4543 buffer[0].len = cast(DWORD) buf.length; 4544 buffer[0].buf = cast(CHAR*) buf.ptr; 4545 4546 DWORD flags2 = 0; 4547 4548 fiber.setPostYield(() { 4549 if(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) { 4550 if(GetLastError() != 997) { 4551 //writeln("WSARecv ", WSAGetLastError()); 4552 //throw new Exception("WSARecv fail"); 4553 } 4554 } 4555 }); 4556 4557 Fiber.yield(); 4558 return lastAnswer; 4559 } 4560 4561 // I might go back and implement these for udp things. 4562 override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags, ref Address from) @trusted { 4563 assert(0); 4564 } 4565 override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags) @trusted { 4566 assert(0); 4567 } 4568 override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags, Address to) @trusted { 4569 assert(0); 4570 } 4571 override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags) @trusted { 4572 assert(0); 4573 } 4574 4575 // lol overload sets 4576 alias send = typeof(super).send; 4577 alias receive = typeof(super).receive; 4578 alias sendTo = typeof(super).sendTo; 4579 alias receiveFrom = typeof(super).receiveFrom; 4580 4581 } 4582 } 4583 4584 void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { 4585 assert(connection !is null); 4586 version(cgi_use_fiber) { 4587 auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun)); 4588 4589 version(Windows) { 4590 (cast(PseudoblockingOverlappedSocket) connection).fiber = fiber; 4591 } 4592 4593 import core.memory; 4594 GC.addRoot(cast(void*) fiber); 4595 fiber.connection = connection; 4596 fiber.proceed(); 4597 } else { 4598 doThreadHttpConnectionGuts!(CustomCgi, fun)(connection); 4599 } 4600 } 4601 4602 void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) { 4603 scope(failure) { 4604 // catch all for other errors 4605 try { 4606 sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); 4607 connection.close(); 4608 } catch(Exception e) {} // swallow it, we're aborting anyway. 4609 } 4610 4611 bool closeConnection = alwaysCloseConnection; 4612 4613 /+ 4614 ubyte[4096] inputBuffer = void; 4615 ubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void; 4616 ubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void; 4617 4618 birBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[]; 4619 BufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr; 4620 ir.__ctor(connection, inputBuffer[], true); 4621 +/ 4622 4623 auto ir = new BufferedInputRange(connection); 4624 4625 while(!ir.empty) { 4626 4627 if(ir.view.length == 0) { 4628 ir.popFront(); 4629 if(ir.sourceClosed) { 4630 connection.close(); 4631 closeConnection = true; 4632 break; 4633 } 4634 } 4635 4636 Cgi cgi; 4637 try { 4638 cgi = new CustomCgi(ir, &closeConnection); 4639 // There's a bunch of these casts around because the type matches up with 4640 // the -version=.... specifiers, just you can also create a RequestServer 4641 // and instantiate the things where the types don't match up. It isn't exactly 4642 // correct but I also don't care rn. Might FIXME and either remove it later or something. 4643 cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; 4644 } catch(ConnectionClosedException ce) { 4645 closeConnection = true; 4646 break; 4647 } catch(ConnectionException ce) { 4648 // broken pipe or something, just abort the connection 4649 closeConnection = true; 4650 break; 4651 } catch(Throwable t) { 4652 // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P 4653 // anyway let's kill the connection 4654 version(CRuntime_Musl) { 4655 stderr.rawWrite(t.toString()); 4656 stderr.rawWrite("\n"); 4657 } else { 4658 stderr.writeln(t.toString()); 4659 } 4660 sendAll(connection, plainHttpError(false, "400 Bad Request", t)); 4661 closeConnection = true; 4662 break; 4663 } 4664 assert(cgi !is null); 4665 scope(exit) 4666 cgi.dispose(); 4667 4668 try { 4669 fun(cgi); 4670 cgi.close(); 4671 if(cgi.websocketMode) 4672 closeConnection = true; 4673 } catch(ConnectionException ce) { 4674 // broken pipe or something, just abort the connection 4675 closeConnection = true; 4676 } catch(ConnectionClosedException ce) { 4677 // broken pipe or something, just abort the connection 4678 closeConnection = true; 4679 } catch(Throwable t) { 4680 // a processing error can be recovered from 4681 version(CRuntime_Musl) {} else 4682 stderr.writeln(t.toString); 4683 if(!handleException(cgi, t)) 4684 closeConnection = true; 4685 } 4686 4687 if(globalStopFlag) 4688 closeConnection = true; 4689 4690 if(closeConnection || alwaysCloseConnection) { 4691 connection.shutdown(SocketShutdown.BOTH); 4692 connection.close(); 4693 ir.dispose(); 4694 closeConnection = false; // don't reclose after loop 4695 break; 4696 } else { 4697 if(ir.front.length) { 4698 ir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along 4699 } else if(ir.sourceClosed) { 4700 ir.source.shutdown(SocketShutdown.BOTH); 4701 ir.source.close(); 4702 ir.dispose(); 4703 closeConnection = false; 4704 } else { 4705 continue; 4706 // break; // this was for a keepalive experiment 4707 } 4708 } 4709 } 4710 4711 if(closeConnection) { 4712 connection.shutdown(SocketShutdown.BOTH); 4713 connection.close(); 4714 ir.dispose(); 4715 } 4716 4717 // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection! 4718 } 4719 4720 void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) { 4721 // and now we can buffer 4722 scope(failure) 4723 connection.close(); 4724 4725 import al = std.algorithm; 4726 4727 size_t size; 4728 4729 string[string] headers; 4730 4731 auto range = new BufferedInputRange(connection); 4732 more_data: 4733 auto chunk = range.front(); 4734 // waiting for colon for header length 4735 auto idx = indexOf(cast(string) chunk, ':'); 4736 if(idx == -1) { 4737 try { 4738 range.popFront(); 4739 } catch(Exception e) { 4740 // it is just closed, no big deal 4741 connection.close(); 4742 return; 4743 } 4744 goto more_data; 4745 } 4746 4747 size = to!size_t(cast(string) chunk[0 .. idx]); 4748 chunk = range.consume(idx + 1); 4749 // reading headers 4750 if(chunk.length < size) 4751 range.popFront(0, size + 1); 4752 // we are now guaranteed to have enough 4753 chunk = range.front(); 4754 assert(chunk.length > size); 4755 4756 idx = 0; 4757 string key; 4758 string value; 4759 foreach(part; al.splitter(chunk, '\0')) { 4760 if(idx & 1) { // odd is value 4761 value = cast(string)(part.idup); 4762 headers[key] = value; // commit 4763 } else 4764 key = cast(string)(part.idup); 4765 idx++; 4766 } 4767 4768 enforce(chunk[size] == ','); // the terminator 4769 4770 range.consume(size + 1); 4771 // reading data 4772 // this will be done by Cgi 4773 4774 const(ubyte)[] getScgiChunk() { 4775 // we are already primed 4776 auto data = range.front(); 4777 if(data.length == 0 && !range.sourceClosed) { 4778 range.popFront(0); 4779 data = range.front(); 4780 } else if (range.sourceClosed) 4781 range.source.close(); 4782 4783 return data; 4784 } 4785 4786 void writeScgi(const(ubyte)[] data) { 4787 sendAll(connection, data); 4788 } 4789 4790 void flushScgi() { 4791 // I don't *think* I have to do anything.... 4792 } 4793 4794 Cgi cgi; 4795 try { 4796 cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); 4797 cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; 4798 } catch(Throwable t) { 4799 sendAll(connection, plainHttpError(true, "400 Bad Request", t)); 4800 connection.close(); 4801 return; // this connection is dead 4802 } 4803 assert(cgi !is null); 4804 scope(exit) cgi.dispose(); 4805 try { 4806 fun(cgi); 4807 cgi.close(); 4808 connection.close(); 4809 } catch(Throwable t) { 4810 // no std err 4811 if(!handleException(cgi, t)) { 4812 connection.close(); 4813 return; 4814 } else { 4815 connection.close(); 4816 return; 4817 } 4818 } 4819 } 4820 4821 string printDate(DateTime date) { 4822 char[29] buffer = void; 4823 printDateToBuffer(date, buffer[]); 4824 return buffer.idup; 4825 } 4826 4827 int printDateToBuffer(DateTime date, char[] buffer) @nogc { 4828 assert(buffer.length >= 29); 4829 // 29 static length ? 4830 4831 static immutable daysOfWeek = [ 4832 "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" 4833 ]; 4834 4835 static immutable months = [ 4836 null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 4837 ]; 4838 4839 buffer[0 .. 3] = daysOfWeek[date.dayOfWeek]; 4840 buffer[3 .. 5] = ", "; 4841 buffer[5] = date.day / 10 + '0'; 4842 buffer[6] = date.day % 10 + '0'; 4843 buffer[7] = ' '; 4844 buffer[8 .. 11] = months[date.month]; 4845 buffer[11] = ' '; 4846 auto y = date.year; 4847 buffer[12] = cast(char) (y / 1000 + '0'); y %= 1000; 4848 buffer[13] = cast(char) (y / 100 + '0'); y %= 100; 4849 buffer[14] = cast(char) (y / 10 + '0'); y %= 10; 4850 buffer[15] = cast(char) (y + '0'); 4851 buffer[16] = ' '; 4852 buffer[17] = date.hour / 10 + '0'; 4853 buffer[18] = date.hour % 10 + '0'; 4854 buffer[19] = ':'; 4855 buffer[20] = date.minute / 10 + '0'; 4856 buffer[21] = date.minute % 10 + '0'; 4857 buffer[22] = ':'; 4858 buffer[23] = date.second / 10 + '0'; 4859 buffer[24] = date.second % 10 + '0'; 4860 buffer[25 .. $] = " GMT"; 4861 4862 return 29; 4863 } 4864 4865 4866 // Referencing this gigantic typeid seems to remind the compiler 4867 // to actually put the symbol in the object file. I guess the immutable 4868 // assoc array array isn't actually included in druntime 4869 void hackAroundLinkerError() { 4870 stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString()); 4871 stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString()); 4872 stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString()); 4873 stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString()); 4874 stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString()); 4875 stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString()); 4876 stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString()); 4877 // this is getting kinda ridiculous btw. Moving assoc arrays 4878 // to the library is the pain that keeps on coming. 4879 4880 // eh this broke the build on the work server 4881 // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])])); 4882 stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString()); 4883 } 4884 4885 4886 4887 4888 4889 version(fastcgi) { 4890 pragma(lib, "fcgi"); 4891 4892 static if(size_t.sizeof == 8) // 64 bit 4893 alias long c_int; 4894 else 4895 alias int c_int; 4896 4897 extern(C) { 4898 struct FCGX_Stream { 4899 ubyte* rdNext; 4900 ubyte* wrNext; 4901 ubyte* stop; 4902 ubyte* stopUnget; 4903 c_int isReader; 4904 c_int isClosed; 4905 c_int wasFCloseCalled; 4906 c_int FCGI_errno; 4907 void* function(FCGX_Stream* stream) fillBuffProc; 4908 void* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc; 4909 void* data; 4910 } 4911 4912 // note: this is meant to be opaque, so don't access it directly 4913 struct FCGX_Request { 4914 int requestId; 4915 int role; 4916 FCGX_Stream* inStream; 4917 FCGX_Stream* outStream; 4918 FCGX_Stream* errStream; 4919 char** envp; 4920 void* paramsPtr; 4921 int ipcFd; 4922 int isBeginProcessed; 4923 int keepConnection; 4924 int appStatus; 4925 int nWriters; 4926 int flags; 4927 int listen_sock; 4928 } 4929 4930 int FCGX_InitRequest(FCGX_Request *request, int sock, int flags); 4931 void FCGX_Init(); 4932 4933 int FCGX_Accept_r(FCGX_Request *request); 4934 4935 4936 alias char** FCGX_ParamArray; 4937 4938 c_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp); 4939 c_int FCGX_GetChar(FCGX_Stream* stream); 4940 c_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream); 4941 int FCGX_HasSeenEOF(FCGX_Stream* stream); 4942 c_int FCGX_FFlush(FCGX_Stream *stream); 4943 4944 int FCGX_OpenSocket(in char*, int); 4945 } 4946 } 4947 4948 4949 /* This might go int a separate module eventually. It is a network input helper class. */ 4950 4951 import std.socket; 4952 4953 version(cgi_use_fiber) { 4954 import core.thread; 4955 4956 version(linux) { 4957 import core.sys.linux.epoll; 4958 4959 int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. 4960 } else version(Windows) { 4961 // declaring the iocp thing below... 4962 } else static assert(0, "The hybrid fiber server is not implemented on your OS."); 4963 } 4964 4965 version(Windows) 4966 __gshared HANDLE iocp; 4967 4968 version(cgi_use_fiber) { 4969 version(linux) 4970 private enum WakeupEvent { 4971 Read = EPOLLIN, 4972 Write = EPOLLOUT 4973 } 4974 else version(Windows) 4975 private enum WakeupEvent { 4976 Read, Write 4977 } 4978 else static assert(0); 4979 } 4980 4981 version(cgi_use_fiber) 4982 private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc { 4983 4984 // static cast since I know what i have in here and don't want to pay for dynamic cast 4985 auto f = cast(CgiFiber) cast(void*) Fiber.getThis(); 4986 4987 version(linux) { 4988 f.setPostYield = () { 4989 if(*registered) { 4990 // rearm 4991 epoll_event evt; 4992 evt.events = e | EPOLLONESHOT; 4993 evt.data.ptr = cast(void*) f; 4994 if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) 4995 throw new Exception("epoll_ctl"); 4996 } else { 4997 // initial registration 4998 *registered = true ; 4999 int fd = source.handle; 5000 epoll_event evt; 5001 evt.events = e | EPOLLONESHOT; 5002 evt.data.ptr = cast(void*) f; 5003 if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) 5004 throw new Exception("epoll_ctl"); 5005 } 5006 }; 5007 5008 Fiber.yield(); 5009 5010 f.setPostYield(null); 5011 } else version(Windows) { 5012 Fiber.yield(); 5013 } 5014 else static assert(0); 5015 } 5016 5017 version(cgi_use_fiber) 5018 void unregisterSource(Socket s) { 5019 version(linux) { 5020 epoll_event evt; 5021 epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); 5022 } else version(Windows) { 5023 // intentionally blank 5024 } 5025 else static assert(0); 5026 } 5027 5028 // it is a class primarily for reference semantics 5029 // I might change this interface 5030 /// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda. 5031 class BufferedInputRange { 5032 version(Posix) 5033 this(int source, ubyte[] buffer = null) { 5034 this(new Socket(cast(socket_t) source, AddressFamily.INET), buffer); 5035 } 5036 5037 this(Socket source, ubyte[] buffer = null, bool allowGrowth = true) { 5038 // if they connect but never send stuff to us, we don't want it wasting the process 5039 // so setting a time out 5040 version(cgi_use_fiber) 5041 source.blocking = false; 5042 else 5043 source.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(3)); 5044 5045 this.source = source; 5046 if(buffer is null) { 5047 underlyingBuffer = new ubyte[4096]; 5048 this.allowGrowth = true; 5049 } else { 5050 underlyingBuffer = buffer; 5051 this.allowGrowth = allowGrowth; 5052 } 5053 5054 assert(underlyingBuffer.length); 5055 5056 // we assume view.ptr is always inside underlyingBuffer 5057 view = underlyingBuffer[0 .. 0]; 5058 5059 popFront(); // prime 5060 } 5061 5062 version(cgi_use_fiber) { 5063 bool registered; 5064 } 5065 5066 void dispose() { 5067 version(cgi_use_fiber) { 5068 if(registered) 5069 unregisterSource(source); 5070 } 5071 } 5072 5073 /** 5074 A slight difference from regular ranges is you can give it the maximum 5075 number of bytes to consume. 5076 5077 IMPORTANT NOTE: the default is to consume nothing, so if you don't call 5078 consume() yourself and use a regular foreach, it will infinitely loop! 5079 5080 The default is to do what a normal range does, and consume the whole buffer 5081 and wait for additional input. 5082 5083 You can also specify 0, to append to the buffer, or any other number 5084 to remove the front n bytes and wait for more. 5085 */ 5086 void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) { 5087 if(sourceClosed) 5088 throw new ConnectionClosedException("can't get any more data from a closed source"); 5089 if(!skipConsume) 5090 consume(maxBytesToConsume); 5091 5092 // we might have to grow the buffer 5093 if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) { 5094 if(allowGrowth) { 5095 //import std.stdio; writeln("growth"); 5096 auto viewStart = view.ptr - underlyingBuffer.ptr; 5097 size_t growth = 4096; 5098 // make sure we have enough for what we're being asked for 5099 if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth) 5100 growth = minBytesToSettleFor - underlyingBuffer.length; 5101 //import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length); 5102 underlyingBuffer.length += growth; 5103 view = underlyingBuffer[viewStart .. view.length]; 5104 } else 5105 throw new Exception("No room left in the buffer"); 5106 } 5107 5108 do { 5109 auto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $]; 5110 try_again: 5111 auto ret = source.receive(freeSpace); 5112 if(ret == Socket.ERROR) { 5113 if(wouldHaveBlocked()) { 5114 version(cgi_use_fiber) { 5115 registerEventWakeup(®istered, source, WakeupEvent.Read); 5116 goto try_again; 5117 } else { 5118 // gonna treat a timeout here as a close 5119 sourceClosed = true; 5120 return; 5121 } 5122 } 5123 version(Posix) { 5124 import core.stdc.errno; 5125 if(errno == EINTR || errno == EAGAIN) { 5126 goto try_again; 5127 } 5128 if(errno == ECONNRESET) { 5129 sourceClosed = true; 5130 return; 5131 } 5132 } 5133 throw new Exception(lastSocketError); // FIXME 5134 } 5135 if(ret == 0) { 5136 sourceClosed = true; 5137 return; 5138 } 5139 5140 //import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, " ", ret, " = ", view.length + ret); 5141 view = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret]; 5142 //import std.stdio; writeln(cast(string) view); 5143 } while(view.length < minBytesToSettleFor); 5144 } 5145 5146 /// Removes n bytes from the front of the buffer, and returns the new buffer slice. 5147 /// You might want to idup the data you are consuming if you store it, since it may 5148 /// be overwritten on the new popFront. 5149 /// 5150 /// You do not need to call this if you always want to wait for more data when you 5151 /// consume some. 5152 ubyte[] consume(size_t bytes) { 5153 //import std.stdio; writeln("consuime ", bytes, "/", view.length); 5154 view = view[bytes > $ ? $ : bytes .. $]; 5155 if(view.length == 0) { 5156 view = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning 5157 /* 5158 writeln("HERE"); 5159 popFront(0, 0, true); // try to load more if we can, checks if the source is closed 5160 writeln(cast(string)front); 5161 writeln("DONE"); 5162 */ 5163 } 5164 return front; 5165 } 5166 5167 bool empty() { 5168 return sourceClosed && view.length == 0; 5169 } 5170 5171 ubyte[] front() { 5172 return view; 5173 } 5174 5175 invariant() { 5176 assert(view.ptr >= underlyingBuffer.ptr); 5177 // it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer 5178 assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length); 5179 } 5180 5181 ubyte[] underlyingBuffer; 5182 bool allowGrowth; 5183 ubyte[] view; 5184 Socket source; 5185 bool sourceClosed; 5186 } 5187 5188 private class FakeSocketForStdin : Socket { 5189 import std.stdio; 5190 5191 this() { 5192 5193 } 5194 5195 private bool closed; 5196 5197 override ptrdiff_t receive(scope void[] buffer, std.socket.SocketFlags) @trusted { 5198 if(closed) 5199 throw new Exception("Closed"); 5200 return stdin.rawRead(buffer).length; 5201 } 5202 5203 override ptrdiff_t send(const scope void[] buffer, std.socket.SocketFlags) @trusted { 5204 if(closed) 5205 throw new Exception("Closed"); 5206 stdout.rawWrite(buffer); 5207 return buffer.length; 5208 } 5209 5210 override void close() @trusted { 5211 (cast(void delegate() @nogc nothrow) &realClose)(); 5212 } 5213 5214 override void shutdown(SocketShutdown s) { 5215 // FIXME 5216 } 5217 5218 override void setOption(SocketOptionLevel, SocketOption, scope void[]) {} 5219 override void setOption(SocketOptionLevel, SocketOption, Duration) {} 5220 5221 override @property @trusted Address remoteAddress() { return null; } 5222 override @property @trusted Address localAddress() { return null; } 5223 5224 void realClose() { 5225 closed = true; 5226 try { 5227 stdin.close(); 5228 stdout.close(); 5229 } catch(Exception e) { 5230 5231 } 5232 } 5233 } 5234 5235 import core.sync.semaphore; 5236 import core.atomic; 5237 5238 /** 5239 To use this thing: 5240 5241 --- 5242 void handler(Socket s) { do something... } 5243 auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler, &delegateThatDropsPrivileges); 5244 manager.listen(); 5245 --- 5246 5247 The 4th parameter is optional. 5248 5249 I suggest you use BufferedInputRange(connection) to handle the input. As a packet 5250 comes in, you will get control. You can just continue; though to fetch more. 5251 5252 5253 FIXME: should I offer an event based async thing like netman did too? Yeah, probably. 5254 */ 5255 class ListeningConnectionManager { 5256 Semaphore semaphore; 5257 Socket[256] queue; 5258 shared(ubyte) nextIndexFront; 5259 ubyte nextIndexBack; 5260 shared(int) queueLength; 5261 5262 Socket acceptCancelable() { 5263 version(Posix) { 5264 import core.sys.posix.sys.select; 5265 fd_set read_fds; 5266 FD_ZERO(&read_fds); 5267 FD_SET(listener.handle, &read_fds); 5268 FD_SET(cancelfd, &read_fds); 5269 auto max = listener.handle > cancelfd ? listener.handle : cancelfd; 5270 auto ret = select(max + 1, &read_fds, null, null, null); 5271 if(ret == -1) { 5272 import core.stdc.errno; 5273 if(errno == EINTR) 5274 return null; 5275 else 5276 throw new Exception("wtf select"); 5277 } 5278 5279 if(FD_ISSET(cancelfd, &read_fds)) { 5280 return null; 5281 } 5282 5283 if(FD_ISSET(listener.handle, &read_fds)) 5284 return listener.accept(); 5285 5286 return null; 5287 } else 5288 return listener.accept(); // FIXME: check the cancel flag! 5289 } 5290 5291 int defaultNumberOfThreads() { 5292 import std.parallelism; 5293 version(cgi_use_fiber) { 5294 return totalCPUs * 1 + 1; 5295 } else { 5296 // I times 4 here because there's a good chance some will be blocked on i/o. 5297 return totalCPUs * 4; 5298 } 5299 5300 } 5301 5302 void listen() { 5303 shared(int) loopBroken; 5304 5305 version(Posix) { 5306 import core.sys.posix.signal; 5307 signal(SIGPIPE, SIG_IGN); 5308 } 5309 5310 version(linux) { 5311 if(cancelfd == -1) 5312 cancelfd = eventfd(0, 0); 5313 } 5314 5315 version(cgi_no_threads) { 5316 // NEVER USE THIS 5317 // it exists only for debugging and other special occasions 5318 5319 // the thread mode is faster and less likely to stall the whole 5320 // thing when a request is slow 5321 while(!loopBroken && !globalStopFlag) { 5322 auto sn = acceptCancelable(); 5323 if(sn is null) continue; 5324 cloexec(sn); 5325 try { 5326 handler(sn); 5327 } catch(Exception e) { 5328 // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies) 5329 sn.close(); 5330 } 5331 } 5332 } else { 5333 5334 if(useFork) { 5335 version(linux) { 5336 //asm { int 3; } 5337 fork(); 5338 } 5339 } 5340 5341 version(cgi_use_fiber) { 5342 5343 version(Windows) { 5344 listener.accept(); 5345 } 5346 5347 WorkerThread[] threads = new WorkerThread[](numberOfThreads); 5348 foreach(i, ref thread; threads) { 5349 thread = new WorkerThread(this, handler, cast(int) i); 5350 thread.start(); 5351 } 5352 5353 bool fiber_crash_check() { 5354 bool hasAnyRunning; 5355 foreach(thread; threads) { 5356 if(!thread.isRunning) { 5357 thread.join(); 5358 } else hasAnyRunning = true; 5359 } 5360 5361 return (!hasAnyRunning); 5362 } 5363 5364 5365 while(!globalStopFlag) { 5366 Thread.sleep(1.seconds); 5367 if(fiber_crash_check()) 5368 break; 5369 } 5370 5371 } else { 5372 semaphore = new Semaphore(); 5373 5374 ConnectionThread[] threads = new ConnectionThread[](numberOfThreads); 5375 foreach(i, ref thread; threads) { 5376 thread = new ConnectionThread(this, handler, cast(int) i); 5377 thread.start(); 5378 } 5379 5380 while(!loopBroken && !globalStopFlag) { 5381 Socket sn; 5382 5383 bool crash_check() { 5384 bool hasAnyRunning; 5385 foreach(thread; threads) { 5386 if(!thread.isRunning) { 5387 thread.join(); 5388 } else hasAnyRunning = true; 5389 } 5390 5391 return (!hasAnyRunning); 5392 } 5393 5394 5395 void accept_new_connection() { 5396 sn = acceptCancelable(); 5397 if(sn is null) return; 5398 cloexec(sn); 5399 if(tcp) { 5400 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 5401 // on the socket because we do some buffering internally. I think this helps, 5402 // certainly does for small requests, and I think it does for larger ones too 5403 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 5404 5405 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 5406 } 5407 } 5408 5409 void existing_connection_new_data() { 5410 // wait until a slot opens up 5411 //int waited = 0; 5412 while(queueLength >= queue.length) { 5413 Thread.sleep(1.msecs); 5414 //waited ++; 5415 } 5416 //if(waited) {import std.stdio; writeln(waited);} 5417 synchronized(this) { 5418 queue[nextIndexBack] = sn; 5419 nextIndexBack++; 5420 atomicOp!"+="(queueLength, 1); 5421 } 5422 semaphore.notify(); 5423 } 5424 5425 5426 accept_new_connection(); 5427 if(sn !is null) 5428 existing_connection_new_data(); 5429 else if(sn is null && globalStopFlag) { 5430 foreach(thread; threads) { 5431 semaphore.notify(); 5432 } 5433 Thread.sleep(50.msecs); 5434 } 5435 5436 if(crash_check()) 5437 break; 5438 } 5439 } 5440 5441 // FIXME: i typically stop this with ctrl+c which never 5442 // actually gets here. i need to do a sigint handler. 5443 if(cleanup) 5444 cleanup(); 5445 } 5446 } 5447 5448 //version(linux) 5449 //int epoll_fd; 5450 5451 bool tcp; 5452 void delegate() cleanup; 5453 5454 private void function(Socket) fhandler; 5455 private void dg_handler(Socket s) { 5456 fhandler(s); 5457 } 5458 this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 5459 fhandler = handler; 5460 this(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads); 5461 } 5462 5463 this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 5464 this.handler = handler; 5465 this.useFork = useFork; 5466 this.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads(); 5467 5468 listener = startListening(host, port, tcp, cleanup, 128, dropPrivs); 5469 5470 version(cgi_use_fiber) 5471 if(useFork) 5472 listener.blocking = false; 5473 5474 // this is the UI control thread and thus gets more priority 5475 Thread.getThis.priority = Thread.PRIORITY_MAX; 5476 } 5477 5478 Socket listener; 5479 void delegate(Socket) handler; 5480 5481 immutable bool useFork; 5482 int numberOfThreads; 5483 } 5484 5485 Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) { 5486 Socket listener; 5487 if(host.startsWith("unix:")) { 5488 version(Posix) { 5489 listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); 5490 cloexec(listener); 5491 string filename = host["unix:".length .. $].idup; 5492 listener.bind(new UnixAddress(filename)); 5493 cleanup = delegate() { 5494 listener.close(); 5495 import std.file; 5496 remove(filename); 5497 }; 5498 tcp = false; 5499 } else { 5500 throw new Exception("unix sockets not supported on this system"); 5501 } 5502 } else if(host.startsWith("abstract:")) { 5503 version(linux) { 5504 listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); 5505 cloexec(listener); 5506 string filename = "\0" ~ host["abstract:".length .. $]; 5507 import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]); 5508 listener.bind(new UnixAddress(filename)); 5509 tcp = false; 5510 } else { 5511 throw new Exception("abstract unix sockets not supported on this system"); 5512 } 5513 } else { 5514 version(cgi_use_fiber) { 5515 version(Windows) 5516 listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); 5517 else 5518 listener = new TcpSocket(); 5519 } else { 5520 listener = new TcpSocket(); 5521 } 5522 cloexec(listener); 5523 listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 5524 listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port)); 5525 cleanup = delegate() { 5526 listener.close(); 5527 }; 5528 tcp = true; 5529 } 5530 5531 listener.listen(backQueue); 5532 5533 if (dropPrivs !is null) // can be null, backwards compatibility 5534 dropPrivs(); 5535 5536 return listener; 5537 } 5538 5539 // helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something. 5540 void sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) { 5541 if(data.length == 0) return; 5542 ptrdiff_t amount; 5543 //import std.stdio; writeln("***",cast(string) data,"///"); 5544 do { 5545 amount = s.send(data); 5546 if(amount == Socket.ERROR) { 5547 version(cgi_use_fiber) { 5548 if(wouldHaveBlocked()) { 5549 bool registered = true; 5550 registerEventWakeup(®istered, s, WakeupEvent.Write); 5551 continue; 5552 } 5553 } 5554 throw new ConnectionException(s, lastSocketError, file, line); 5555 } 5556 assert(amount > 0); 5557 5558 data = data[amount .. $]; 5559 } while(data.length); 5560 } 5561 5562 class ConnectionException : Exception { 5563 Socket socket; 5564 this(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) { 5565 this.socket = s; 5566 super(msg, file, line); 5567 } 5568 } 5569 5570 alias void delegate(Socket) CMT; 5571 5572 import core.thread; 5573 /+ 5574 cgi.d now uses a hybrid of event i/o and threads at the top level. 5575 5576 Top level thread is responsible for accepting sockets and selecting on them. 5577 5578 It then indicates to a child that a request is pending, and any random worker 5579 thread that is free handles it. It goes into blocking mode and handles that 5580 http request to completion. 5581 5582 At that point, it goes back into the waiting queue. 5583 5584 5585 This concept is only implemented on Linux. On all other systems, it still 5586 uses the worker threads and semaphores (which is perfectly fine for a lot of 5587 things! Just having a great number of keep-alive connections will break that.) 5588 5589 5590 So the algorithm is: 5591 5592 select(accept, event, pending) 5593 if accept -> send socket to free thread, if any. if not, add socket to queue 5594 if event -> send the signaling thread a socket from the queue, if not, mark it free 5595 - event might block until it can be *written* to. it is a fifo sending socket fds! 5596 5597 A worker only does one http request at a time, then signals its availability back to the boss. 5598 5599 The socket the worker was just doing should be added to the one-off epoll read. If it is closed, 5600 great, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the 5601 actual FD will not be kept out here. 5602 5603 So: 5604 queue = sockets we know are ready to read now, but no worker thread is available 5605 idle list = worker threads not doing anything else. they signal back and forth 5606 5607 the workers all read off the event fd. This is the semaphore wait 5608 5609 the boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read, 5610 it puts it in the queue and writes to the event fd. 5611 5612 The child could put the socket back in the epoll thing itself. 5613 5614 The child needs to be able to gracefully handle being given a socket that just closed with no work. 5615 +/ 5616 class ConnectionThread : Thread { 5617 this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { 5618 this.lcm = lcm; 5619 this.dg = dg; 5620 this.myThreadNumber = myThreadNumber; 5621 super(&run); 5622 } 5623 5624 void run() { 5625 while(true) { 5626 // so if there's a bunch of idle keep-alive connections, it can 5627 // consume all the worker threads... just sitting there. 5628 lcm.semaphore.wait(); 5629 if(globalStopFlag) 5630 return; 5631 Socket socket; 5632 synchronized(lcm) { 5633 auto idx = lcm.nextIndexFront; 5634 socket = lcm.queue[idx]; 5635 lcm.queue[idx] = null; 5636 atomicOp!"+="(lcm.nextIndexFront, 1); 5637 atomicOp!"-="(lcm.queueLength, 1); 5638 } 5639 try { 5640 //import std.stdio; writeln(myThreadNumber, " taking it"); 5641 dg(socket); 5642 /+ 5643 if(socket.isAlive) { 5644 // process it more later 5645 version(linux) { 5646 import core.sys.linux.epoll; 5647 epoll_event ev; 5648 ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; 5649 ev.data.fd = socket.handle; 5650 import std.stdio; writeln("adding"); 5651 if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) { 5652 if(errno == EEXIST) { 5653 ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; 5654 ev.data.fd = socket.handle; 5655 if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1) 5656 throw new Exception("epoll_ctl " ~ to!string(errno)); 5657 } else 5658 throw new Exception("epoll_ctl " ~ to!string(errno)); 5659 } 5660 //import std.stdio; writeln("keep alive"); 5661 // writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later 5662 __traits(getMember, socket, "sock") = cast(socket_t) -1; 5663 } else { 5664 continue; // hope it times out in a reasonable amount of time... 5665 } 5666 } 5667 +/ 5668 } catch(ConnectionClosedException e) { 5669 // can just ignore this, it is fairly normal 5670 socket.close(); 5671 } catch(Throwable e) { 5672 import std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite("\n"); 5673 socket.close(); 5674 } 5675 } 5676 } 5677 5678 ListeningConnectionManager lcm; 5679 CMT dg; 5680 int myThreadNumber; 5681 } 5682 5683 version(cgi_use_fiber) 5684 class WorkerThread : Thread { 5685 this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { 5686 this.lcm = lcm; 5687 this.dg = dg; 5688 this.myThreadNumber = myThreadNumber; 5689 super(&run); 5690 } 5691 5692 version(Windows) 5693 void run() { 5694 auto timeout = INFINITE; 5695 PseudoblockingOverlappedSocket key; 5696 OVERLAPPED* overlapped; 5697 DWORD bytes; 5698 while(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) { 5699 if(key is null) 5700 continue; 5701 key.lastAnswer = bytes; 5702 if(key.fiber) { 5703 key.fiber.proceed(); 5704 } else { 5705 // we have a new connection, issue the first receive on it and issue the next accept 5706 5707 auto sn = key.accepted; 5708 5709 key.accept(); 5710 5711 cloexec(sn); 5712 if(lcm.tcp) { 5713 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 5714 // on the socket because we do some buffering internally. I think this helps, 5715 // certainly does for small requests, and I think it does for larger ones too 5716 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 5717 5718 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 5719 } 5720 5721 dg(sn); 5722 } 5723 } 5724 //SleepEx(INFINITE, TRUE); 5725 } 5726 5727 version(linux) 5728 void run() { 5729 5730 import core.sys.linux.epoll; 5731 epfd = epoll_create1(EPOLL_CLOEXEC); 5732 if(epfd == -1) 5733 throw new Exception("epoll_create1 " ~ to!string(errno)); 5734 scope(exit) { 5735 import core.sys.posix.unistd; 5736 close(epfd); 5737 } 5738 5739 { 5740 epoll_event ev; 5741 ev.events = EPOLLIN; 5742 ev.data.fd = cancelfd; 5743 epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); 5744 } 5745 5746 epoll_event ev; 5747 ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. 5748 ev.data.fd = lcm.listener.handle; 5749 if(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1) 5750 throw new Exception("epoll_ctl " ~ to!string(errno)); 5751 5752 5753 5754 while(!globalStopFlag) { 5755 Socket sn; 5756 5757 epoll_event[64] events; 5758 auto nfds = epoll_wait(epfd, events.ptr, events.length, -1); 5759 if(nfds == -1) { 5760 if(errno == EINTR) 5761 continue; 5762 throw new Exception("epoll_wait " ~ to!string(errno)); 5763 } 5764 5765 foreach(idx; 0 .. nfds) { 5766 auto flags = events[idx].events; 5767 5768 if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { 5769 globalStopFlag = true; 5770 //import std.stdio; writeln("exit heard"); 5771 break; 5772 } else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { 5773 //import std.stdio; writeln(myThreadNumber, " woken up ", flags); 5774 // this try/catch is because it is set to non-blocking mode 5775 // and Phobos' stupid api throws an exception instead of returning 5776 // if it would block. Why would it block? because a forked process 5777 // might have beat us to it, but the wakeup event thundered our herds. 5778 try 5779 sn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better 5780 catch(SocketAcceptException e) { continue; } 5781 5782 cloexec(sn); 5783 if(lcm.tcp) { 5784 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 5785 // on the socket because we do some buffering internally. I think this helps, 5786 // certainly does for small requests, and I think it does for larger ones too 5787 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 5788 5789 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 5790 } 5791 5792 dg(sn); 5793 } else { 5794 if(cast(size_t) events[idx].data.ptr < 1024) { 5795 throw new Exception("this doesn't look like a fiber pointer..."); 5796 } 5797 auto fiber = cast(CgiFiber) events[idx].data.ptr; 5798 fiber.proceed(); 5799 } 5800 } 5801 } 5802 } 5803 5804 ListeningConnectionManager lcm; 5805 CMT dg; 5806 int myThreadNumber; 5807 } 5808 5809 5810 /* Done with network helper */ 5811 5812 /* Helpers for doing temporary files. Used both here and in web.d */ 5813 5814 version(Windows) { 5815 import core.sys.windows.windows; 5816 extern(Windows) DWORD GetTempPathW(DWORD, LPWSTR); 5817 alias GetTempPathW GetTempPath; 5818 } 5819 5820 version(Posix) { 5821 static import linux = core.sys.posix.unistd; 5822 } 5823 5824 string getTempDirectory() { 5825 string path; 5826 version(Windows) { 5827 wchar[1024] buffer; 5828 auto len = GetTempPath(1024, buffer.ptr); 5829 if(len == 0) 5830 throw new Exception("couldn't find a temporary path"); 5831 5832 auto b = buffer[0 .. len]; 5833 5834 path = to!string(b); 5835 } else 5836 path = "/tmp/"; 5837 5838 return path; 5839 } 5840 5841 5842 // I like std.date. These functions help keep my old code and data working with phobos changing. 5843 5844 long sysTimeToDTime(in SysTime sysTime) { 5845 return convert!("hnsecs", "msecs")(sysTime.stdTime - 621355968000000000L); 5846 } 5847 5848 long dateTimeToDTime(in DateTime dt) { 5849 return sysTimeToDTime(cast(SysTime) dt); 5850 } 5851 5852 long getUtcTime() { // renamed primarily to avoid conflict with std.date itself 5853 return sysTimeToDTime(Clock.currTime(UTC())); 5854 } 5855 5856 // NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick 5857 SysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) { 5858 immutable hnsecs = convert!("msecs", "hnsecs")(dTime) + 621355968000000000L; 5859 return SysTime(hnsecs, tz); 5860 } 5861 5862 5863 5864 // this is a helper to read HTTP transfer-encoding: chunked responses 5865 immutable(ubyte[]) dechunk(BufferedInputRange ir) { 5866 immutable(ubyte)[] ret; 5867 5868 another_chunk: 5869 // If here, we are at the beginning of a chunk. 5870 auto a = ir.front(); 5871 int chunkSize; 5872 int loc = locationOf(a, "\r\n"); 5873 while(loc == -1) { 5874 ir.popFront(); 5875 a = ir.front(); 5876 loc = locationOf(a, "\r\n"); 5877 } 5878 5879 string hex; 5880 hex = ""; 5881 for(int i = 0; i < loc; i++) { 5882 char c = a[i]; 5883 if(c >= 'A' && c <= 'Z') 5884 c += 0x20; 5885 if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) { 5886 hex ~= c; 5887 } else { 5888 break; 5889 } 5890 } 5891 5892 assert(hex.length); 5893 5894 int power = 1; 5895 int size = 0; 5896 foreach(cc1; retro(hex)) { 5897 dchar cc = cc1; 5898 if(cc >= 'a' && cc <= 'z') 5899 cc -= 0x20; 5900 int val = 0; 5901 if(cc >= '0' && cc <= '9') 5902 val = cc - '0'; 5903 else 5904 val = cc - 'A' + 10; 5905 5906 size += power * val; 5907 power *= 16; 5908 } 5909 5910 chunkSize = size; 5911 assert(size >= 0); 5912 5913 if(loc + 2 > a.length) { 5914 ir.popFront(0, a.length + loc + 2); 5915 a = ir.front(); 5916 } 5917 5918 a = ir.consume(loc + 2); 5919 5920 if(chunkSize == 0) { // we're done with the response 5921 // if we got here, will change must be true.... 5922 more_footers: 5923 loc = locationOf(a, "\r\n"); 5924 if(loc == -1) { 5925 ir.popFront(); 5926 a = ir.front; 5927 goto more_footers; 5928 } else { 5929 assert(loc == 0); 5930 ir.consume(loc + 2); 5931 goto finish; 5932 } 5933 } else { 5934 // if we got here, will change must be true.... 5935 if(a.length < chunkSize + 2) { 5936 ir.popFront(0, chunkSize + 2); 5937 a = ir.front(); 5938 } 5939 5940 ret ~= (a[0..chunkSize]); 5941 5942 if(!(a.length > chunkSize + 2)) { 5943 ir.popFront(0, chunkSize + 2); 5944 a = ir.front(); 5945 } 5946 assert(a[chunkSize] == 13); 5947 assert(a[chunkSize+1] == 10); 5948 a = ir.consume(chunkSize + 2); 5949 chunkSize = 0; 5950 goto another_chunk; 5951 } 5952 5953 finish: 5954 return ret; 5955 } 5956 5957 // I want to be able to get data from multiple sources the same way... 5958 interface ByChunkRange { 5959 bool empty(); 5960 void popFront(); 5961 const(ubyte)[] front(); 5962 } 5963 5964 ByChunkRange byChunk(const(ubyte)[] data) { 5965 return new class ByChunkRange { 5966 override bool empty() { 5967 return !data.length; 5968 } 5969 5970 override void popFront() { 5971 if(data.length > 4096) 5972 data = data[4096 .. $]; 5973 else 5974 data = null; 5975 } 5976 5977 override const(ubyte)[] front() { 5978 return data[0 .. $ > 4096 ? 4096 : $]; 5979 } 5980 }; 5981 } 5982 5983 ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) { 5984 const(ubyte)[] f; 5985 5986 f = ir.front; 5987 if(f.length > atMost) 5988 f = f[0 .. atMost]; 5989 5990 return new class ByChunkRange { 5991 override bool empty() { 5992 return atMost == 0; 5993 } 5994 5995 override const(ubyte)[] front() { 5996 return f; 5997 } 5998 5999 override void popFront() { 6000 ir.consume(f.length); 6001 atMost -= f.length; 6002 auto a = ir.front(); 6003 6004 if(a.length <= atMost) { 6005 f = a; 6006 atMost -= a.length; 6007 a = ir.consume(a.length); 6008 if(atMost != 0) 6009 ir.popFront(); 6010 if(f.length == 0) { 6011 f = ir.front(); 6012 } 6013 } else { 6014 // we actually have *more* here than we need.... 6015 f = a[0..atMost]; 6016 atMost = 0; 6017 ir.consume(atMost); 6018 } 6019 } 6020 }; 6021 } 6022 6023 version(cgi_with_websocket) { 6024 // http://tools.ietf.org/html/rfc6455 6025 6026 /** 6027 WEBSOCKET SUPPORT: 6028 6029 Full example: 6030 --- 6031 import arsd.cgi; 6032 6033 void websocketEcho(Cgi cgi) { 6034 if(cgi.websocketRequested()) { 6035 if(cgi.origin != "http://arsdnet.net") 6036 throw new Exception("bad origin"); 6037 auto websocket = cgi.acceptWebsocket(); 6038 6039 websocket.send("hello"); 6040 websocket.send(" world!"); 6041 6042 auto msg = websocket.recv(); 6043 while(msg.opcode != WebSocketOpcode.close) { 6044 if(msg.opcode == WebSocketOpcode.text) { 6045 websocket.send(msg.textData); 6046 } else if(msg.opcode == WebSocketOpcode.binary) { 6047 websocket.send(msg.data); 6048 } 6049 6050 msg = websocket.recv(); 6051 } 6052 6053 websocket.close(); 6054 } else assert(0, "i want a web socket!"); 6055 } 6056 6057 mixin GenericMain!websocketEcho; 6058 --- 6059 */ 6060 6061 class WebSocket { 6062 Cgi cgi; 6063 6064 private this(Cgi cgi) { 6065 this.cgi = cgi; 6066 6067 Socket socket = cgi.idlol.source; 6068 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5)); 6069 } 6070 6071 // returns true if data available, false if it timed out 6072 bool recvAvailable(Duration timeout = dur!"msecs"(0)) { 6073 if(!waitForNextMessageWouldBlock()) 6074 return true; 6075 if(isDataPending(timeout)) 6076 return true; // this is kinda a lie. 6077 6078 return false; 6079 } 6080 6081 public bool lowLevelReceive() { 6082 auto bfr = cgi.idlol; 6083 top: 6084 auto got = bfr.front; 6085 if(got.length) { 6086 if(receiveBuffer.length < receiveBufferUsedLength + got.length) 6087 receiveBuffer.length += receiveBufferUsedLength + got.length; 6088 6089 receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[]; 6090 receiveBufferUsedLength += got.length; 6091 bfr.consume(got.length); 6092 6093 return true; 6094 } 6095 6096 if(bfr.sourceClosed) 6097 return false; 6098 6099 bfr.popFront(0); 6100 if(bfr.sourceClosed) 6101 return false; 6102 goto top; 6103 } 6104 6105 6106 bool isDataPending(Duration timeout = 0.seconds) { 6107 Socket socket = cgi.idlol.source; 6108 6109 auto check = new SocketSet(); 6110 check.add(socket); 6111 6112 auto got = Socket.select(check, null, null, timeout); 6113 if(got > 0) 6114 return true; 6115 return false; 6116 } 6117 6118 // note: this blocks 6119 WebSocketFrame recv() { 6120 return waitForNextMessage(); 6121 } 6122 6123 6124 6125 6126 private void llclose() { 6127 cgi.close(); 6128 } 6129 6130 private void llsend(ubyte[] data) { 6131 cgi.write(data); 6132 cgi.flush(); 6133 } 6134 6135 void unregisterActiveSocket(WebSocket) {} 6136 6137 /* copy/paste section { */ 6138 6139 private int readyState_; 6140 private ubyte[] receiveBuffer; 6141 private size_t receiveBufferUsedLength; 6142 6143 private Config config; 6144 6145 enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. 6146 enum OPEN = 1; /// The connection is open and ready to communicate. 6147 enum CLOSING = 2; /// The connection is in the process of closing. 6148 enum CLOSED = 3; /// The connection is closed or couldn't be opened. 6149 6150 /++ 6151 6152 +/ 6153 /// Group: foundational 6154 static struct Config { 6155 /++ 6156 These control the size of the receive buffer. 6157 6158 It starts at the initial size, will temporarily 6159 balloon up to the maximum size, and will reuse 6160 a buffer up to the likely size. 6161 6162 Anything larger than the maximum size will cause 6163 the connection to be aborted and an exception thrown. 6164 This is to protect you against a peer trying to 6165 exhaust your memory, while keeping the user-level 6166 processing simple. 6167 +/ 6168 size_t initialReceiveBufferSize = 4096; 6169 size_t likelyReceiveBufferSize = 4096; /// ditto 6170 size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto 6171 6172 /++ 6173 Maximum combined size of a message. 6174 +/ 6175 size_t maximumMessageSize = 10 * 1024 * 1024; 6176 6177 string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; 6178 string origin; /// Origin URL to send with the handshake, if desired. 6179 string protocol; /// the protocol header, if desired. 6180 6181 int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping 6182 } 6183 6184 /++ 6185 Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. 6186 +/ 6187 int readyState() { 6188 return readyState_; 6189 } 6190 6191 /++ 6192 Closes the connection, sending a graceful teardown message to the other side. 6193 +/ 6194 /// Group: foundational 6195 void close(int code = 0, string reason = null) 6196 //in (reason.length < 123) 6197 in { assert(reason.length < 123); } do 6198 { 6199 if(readyState_ != OPEN) 6200 return; // it cool, we done 6201 WebSocketFrame wss; 6202 wss.fin = true; 6203 wss.opcode = WebSocketOpcode.close; 6204 wss.data = cast(ubyte[]) reason.dup; 6205 wss.send(&llsend); 6206 6207 readyState_ = CLOSING; 6208 6209 llclose(); 6210 } 6211 6212 /++ 6213 Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function. 6214 +/ 6215 /// Group: foundational 6216 void ping() { 6217 WebSocketFrame wss; 6218 wss.fin = true; 6219 wss.opcode = WebSocketOpcode.ping; 6220 wss.send(&llsend); 6221 } 6222 6223 // automatically handled.... 6224 void pong() { 6225 WebSocketFrame wss; 6226 wss.fin = true; 6227 wss.opcode = WebSocketOpcode.pong; 6228 wss.send(&llsend); 6229 } 6230 6231 /++ 6232 Sends a text message through the websocket. 6233 +/ 6234 /// Group: foundational 6235 void send(in char[] textData) { 6236 WebSocketFrame wss; 6237 wss.fin = true; 6238 wss.opcode = WebSocketOpcode.text; 6239 wss.data = cast(ubyte[]) textData.dup; 6240 wss.send(&llsend); 6241 } 6242 6243 /++ 6244 Sends a binary message through the websocket. 6245 +/ 6246 /// Group: foundational 6247 void send(in ubyte[] binaryData) { 6248 WebSocketFrame wss; 6249 wss.fin = true; 6250 wss.opcode = WebSocketOpcode.binary; 6251 wss.data = cast(ubyte[]) binaryData.dup; 6252 wss.send(&llsend); 6253 } 6254 6255 /++ 6256 Waits for and returns the next complete message on the socket. 6257 6258 Note that the onmessage function is still called, right before 6259 this returns. 6260 +/ 6261 /// Group: blocking_api 6262 public WebSocketFrame waitForNextMessage() { 6263 do { 6264 auto m = processOnce(); 6265 if(m.populated) 6266 return m; 6267 } while(lowLevelReceive()); 6268 6269 throw new ConnectionClosedException("Websocket receive timed out"); 6270 //return WebSocketFrame.init; // FIXME? maybe. 6271 } 6272 6273 /++ 6274 Tells if [waitForNextMessage] would block. 6275 +/ 6276 /// Group: blocking_api 6277 public bool waitForNextMessageWouldBlock() { 6278 checkAgain: 6279 if(isMessageBuffered()) 6280 return false; 6281 if(!isDataPending()) 6282 return true; 6283 while(isDataPending()) 6284 lowLevelReceive(); 6285 goto checkAgain; 6286 } 6287 6288 /++ 6289 Is there a message in the buffer already? 6290 If `true`, [waitForNextMessage] is guaranteed to return immediately. 6291 If `false`, check [isDataPending] as the next step. 6292 +/ 6293 /// Group: blocking_api 6294 public bool isMessageBuffered() { 6295 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 6296 auto s = d; 6297 if(d.length) { 6298 auto orig = d; 6299 auto m = WebSocketFrame.read(d); 6300 // that's how it indicates that it needs more data 6301 if(d !is orig) 6302 return true; 6303 } 6304 6305 return false; 6306 } 6307 6308 private ubyte continuingType; 6309 private ubyte[] continuingData; 6310 //private size_t continuingDataLength; 6311 6312 private WebSocketFrame processOnce() { 6313 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 6314 auto s = d; 6315 // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. 6316 WebSocketFrame m; 6317 if(d.length) { 6318 auto orig = d; 6319 m = WebSocketFrame.read(d); 6320 // that's how it indicates that it needs more data 6321 if(d is orig) 6322 return WebSocketFrame.init; 6323 m.unmaskInPlace(); 6324 switch(m.opcode) { 6325 case WebSocketOpcode.continuation: 6326 if(continuingData.length + m.data.length > config.maximumMessageSize) 6327 throw new Exception("message size exceeded"); 6328 6329 continuingData ~= m.data; 6330 if(m.fin) { 6331 if(ontextmessage) 6332 ontextmessage(cast(char[]) continuingData); 6333 if(onbinarymessage) 6334 onbinarymessage(continuingData); 6335 6336 continuingData = null; 6337 } 6338 break; 6339 case WebSocketOpcode.text: 6340 if(m.fin) { 6341 if(ontextmessage) 6342 ontextmessage(m.textData); 6343 } else { 6344 continuingType = m.opcode; 6345 //continuingDataLength = 0; 6346 continuingData = null; 6347 continuingData ~= m.data; 6348 } 6349 break; 6350 case WebSocketOpcode.binary: 6351 if(m.fin) { 6352 if(onbinarymessage) 6353 onbinarymessage(m.data); 6354 } else { 6355 continuingType = m.opcode; 6356 //continuingDataLength = 0; 6357 continuingData = null; 6358 continuingData ~= m.data; 6359 } 6360 break; 6361 case WebSocketOpcode.close: 6362 readyState_ = CLOSED; 6363 if(onclose) 6364 onclose(); 6365 6366 unregisterActiveSocket(this); 6367 break; 6368 case WebSocketOpcode.ping: 6369 pong(); 6370 break; 6371 case WebSocketOpcode.pong: 6372 // just really references it is still alive, nbd. 6373 break; 6374 default: // ignore though i could and perhaps should throw too 6375 } 6376 } 6377 6378 // the recv thing can be invalidated so gotta copy it over ugh 6379 if(d.length) { 6380 m.data = m.data.dup(); 6381 } 6382 6383 import core.stdc.string; 6384 memmove(receiveBuffer.ptr, d.ptr, d.length); 6385 receiveBufferUsedLength = d.length; 6386 6387 return m; 6388 } 6389 6390 private void autoprocess() { 6391 // FIXME 6392 do { 6393 processOnce(); 6394 } while(lowLevelReceive()); 6395 } 6396 6397 6398 void delegate() onclose; /// 6399 void delegate() onerror; /// 6400 void delegate(in char[]) ontextmessage; /// 6401 void delegate(in ubyte[]) onbinarymessage; /// 6402 void delegate() onopen; /// 6403 6404 /++ 6405 6406 +/ 6407 /// Group: browser_api 6408 void onmessage(void delegate(in char[]) dg) { 6409 ontextmessage = dg; 6410 } 6411 6412 /// ditto 6413 void onmessage(void delegate(in ubyte[]) dg) { 6414 onbinarymessage = dg; 6415 } 6416 6417 /* } end copy/paste */ 6418 6419 6420 } 6421 6422 bool websocketRequested(Cgi cgi) { 6423 return 6424 "sec-websocket-key" in cgi.requestHeaders 6425 && 6426 "connection" in cgi.requestHeaders && 6427 cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade") 6428 && 6429 "upgrade" in cgi.requestHeaders && 6430 cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket") 6431 ; 6432 } 6433 6434 WebSocket acceptWebsocket(Cgi cgi) { 6435 assert(!cgi.closed); 6436 assert(!cgi.outputtedResponseData); 6437 cgi.setResponseStatus("101 Switching Protocols"); 6438 cgi.header("Upgrade: WebSocket"); 6439 cgi.header("Connection: upgrade"); 6440 6441 string key = cgi.requestHeaders["sec-websocket-key"]; 6442 key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec 6443 6444 import std.digest.sha; 6445 auto hash = sha1Of(key); 6446 auto accept = Base64.encode(hash); 6447 6448 cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup); 6449 6450 cgi.websocketMode = true; 6451 cgi.write(""); 6452 6453 cgi.flush(); 6454 6455 return new WebSocket(cgi); 6456 } 6457 6458 // FIXME get websocket to work on other modes, not just embedded_httpd 6459 6460 /* copy/paste in http2.d { */ 6461 enum WebSocketOpcode : ubyte { 6462 continuation = 0, 6463 text = 1, 6464 binary = 2, 6465 // 3, 4, 5, 6, 7 RESERVED 6466 close = 8, 6467 ping = 9, 6468 pong = 10, 6469 // 11,12,13,14,15 RESERVED 6470 } 6471 6472 public struct WebSocketFrame { 6473 private bool populated; 6474 bool fin; 6475 bool rsv1; 6476 bool rsv2; 6477 bool rsv3; 6478 WebSocketOpcode opcode; // 4 bits 6479 bool masked; 6480 ubyte lengthIndicator; // don't set this when building one to send 6481 ulong realLength; // don't use when sending 6482 ubyte[4] maskingKey; // don't set this when sending 6483 ubyte[] data; 6484 6485 static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) { 6486 WebSocketFrame msg; 6487 msg.fin = true; 6488 msg.opcode = opcode; 6489 msg.data = cast(ubyte[]) data.dup; 6490 6491 return msg; 6492 } 6493 6494 private void send(scope void delegate(ubyte[]) llsend) { 6495 ubyte[64] headerScratch; 6496 int headerScratchPos = 0; 6497 6498 realLength = data.length; 6499 6500 { 6501 ubyte b1; 6502 b1 |= cast(ubyte) opcode; 6503 b1 |= rsv3 ? (1 << 4) : 0; 6504 b1 |= rsv2 ? (1 << 5) : 0; 6505 b1 |= rsv1 ? (1 << 6) : 0; 6506 b1 |= fin ? (1 << 7) : 0; 6507 6508 headerScratch[0] = b1; 6509 headerScratchPos++; 6510 } 6511 6512 { 6513 headerScratchPos++; // we'll set header[1] at the end of this 6514 auto rlc = realLength; 6515 ubyte b2; 6516 b2 |= masked ? (1 << 7) : 0; 6517 6518 assert(headerScratchPos == 2); 6519 6520 if(realLength > 65535) { 6521 // use 64 bit length 6522 b2 |= 0x7f; 6523 6524 // FIXME: double check endinaness 6525 foreach(i; 0 .. 8) { 6526 headerScratch[2 + 7 - i] = rlc & 0x0ff; 6527 rlc >>>= 8; 6528 } 6529 6530 headerScratchPos += 8; 6531 } else if(realLength > 125) { 6532 // use 16 bit length 6533 b2 |= 0x7e; 6534 6535 // FIXME: double check endinaness 6536 foreach(i; 0 .. 2) { 6537 headerScratch[2 + 1 - i] = rlc & 0x0ff; 6538 rlc >>>= 8; 6539 } 6540 6541 headerScratchPos += 2; 6542 } else { 6543 // use 7 bit length 6544 b2 |= realLength & 0b_0111_1111; 6545 } 6546 6547 headerScratch[1] = b2; 6548 } 6549 6550 //assert(!masked, "masking key not properly implemented"); 6551 if(masked) { 6552 // FIXME: randomize this 6553 headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; 6554 headerScratchPos += 4; 6555 6556 // we'll just mask it in place... 6557 int keyIdx = 0; 6558 foreach(i; 0 .. data.length) { 6559 data[i] = data[i] ^ maskingKey[keyIdx]; 6560 if(keyIdx == 3) 6561 keyIdx = 0; 6562 else 6563 keyIdx++; 6564 } 6565 } 6566 6567 //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); 6568 llsend(headerScratch[0 .. headerScratchPos]); 6569 llsend(data); 6570 } 6571 6572 static WebSocketFrame read(ref ubyte[] d) { 6573 WebSocketFrame msg; 6574 6575 auto orig = d; 6576 6577 WebSocketFrame needsMoreData() { 6578 d = orig; 6579 return WebSocketFrame.init; 6580 } 6581 6582 if(d.length < 2) 6583 return needsMoreData(); 6584 6585 ubyte b = d[0]; 6586 6587 msg.populated = true; 6588 6589 msg.opcode = cast(WebSocketOpcode) (b & 0x0f); 6590 b >>= 4; 6591 msg.rsv3 = b & 0x01; 6592 b >>= 1; 6593 msg.rsv2 = b & 0x01; 6594 b >>= 1; 6595 msg.rsv1 = b & 0x01; 6596 b >>= 1; 6597 msg.fin = b & 0x01; 6598 6599 b = d[1]; 6600 msg.masked = (b & 0b1000_0000) ? true : false; 6601 msg.lengthIndicator = b & 0b0111_1111; 6602 6603 d = d[2 .. $]; 6604 6605 if(msg.lengthIndicator == 0x7e) { 6606 // 16 bit length 6607 msg.realLength = 0; 6608 6609 if(d.length < 2) return needsMoreData(); 6610 6611 foreach(i; 0 .. 2) { 6612 msg.realLength |= d[0] << ((1-i) * 8); 6613 d = d[1 .. $]; 6614 } 6615 } else if(msg.lengthIndicator == 0x7f) { 6616 // 64 bit length 6617 msg.realLength = 0; 6618 6619 if(d.length < 8) return needsMoreData(); 6620 6621 foreach(i; 0 .. 8) { 6622 msg.realLength |= ulong(d[0]) << ((7-i) * 8); 6623 d = d[1 .. $]; 6624 } 6625 } else { 6626 // 7 bit length 6627 msg.realLength = msg.lengthIndicator; 6628 } 6629 6630 if(msg.masked) { 6631 6632 if(d.length < 4) return needsMoreData(); 6633 6634 msg.maskingKey = d[0 .. 4]; 6635 d = d[4 .. $]; 6636 } 6637 6638 if(msg.realLength > d.length) { 6639 return needsMoreData(); 6640 } 6641 6642 msg.data = d[0 .. cast(size_t) msg.realLength]; 6643 d = d[cast(size_t) msg.realLength .. $]; 6644 6645 return msg; 6646 } 6647 6648 void unmaskInPlace() { 6649 if(this.masked) { 6650 int keyIdx = 0; 6651 foreach(i; 0 .. this.data.length) { 6652 this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; 6653 if(keyIdx == 3) 6654 keyIdx = 0; 6655 else 6656 keyIdx++; 6657 } 6658 } 6659 } 6660 6661 char[] textData() { 6662 return cast(char[]) data; 6663 } 6664 } 6665 /* } */ 6666 } 6667 6668 6669 version(Windows) 6670 { 6671 version(CRuntime_DigitalMars) 6672 { 6673 extern(C) int setmode(int, int) nothrow @nogc; 6674 } 6675 else version(CRuntime_Microsoft) 6676 { 6677 extern(C) int _setmode(int, int) nothrow @nogc; 6678 alias setmode = _setmode; 6679 } 6680 else static assert(0); 6681 } 6682 6683 version(Posix) { 6684 version(CRuntime_Musl) {} else { 6685 import core.sys.posix.unistd; 6686 private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**); 6687 } 6688 } 6689 6690 6691 // FIXME: these aren't quite public yet. 6692 //private: 6693 6694 // template for laziness 6695 void startAddonServer()(string arg) { 6696 version(OSX) { 6697 assert(0, "Not implemented"); 6698 } else version(linux) { 6699 import core.sys.posix.unistd; 6700 pid_t pid; 6701 const(char)*[16] args; 6702 args[0] = "ARSD_CGI_ADDON_SERVER"; 6703 args[1] = arg.ptr; 6704 posix_spawn(&pid, "/proc/self/exe", 6705 null, 6706 null, 6707 args.ptr, 6708 null // env 6709 ); 6710 } else version(Windows) { 6711 wchar[2048] filename; 6712 auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length); 6713 if(len == 0 || len == filename.length) 6714 throw new Exception("could not get process name to start helper server"); 6715 6716 STARTUPINFOW startupInfo; 6717 startupInfo.cb = cast(DWORD) startupInfo.sizeof; 6718 PROCESS_INFORMATION processInfo; 6719 6720 import std.utf; 6721 6722 // I *MIGHT* need to run it as a new job or a service... 6723 auto ret = CreateProcessW( 6724 filename.ptr, 6725 toUTF16z(arg), 6726 null, // process attributes 6727 null, // thread attributes 6728 false, // inherit handles 6729 0, // creation flags 6730 null, // environment 6731 null, // working directory 6732 &startupInfo, 6733 &processInfo 6734 ); 6735 6736 if(!ret) 6737 throw new Exception("create process failed"); 6738 6739 // when done with those, if we set them 6740 /* 6741 CloseHandle(hStdInput); 6742 CloseHandle(hStdOutput); 6743 CloseHandle(hStdError); 6744 */ 6745 6746 } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)"); 6747 } 6748 6749 // template for laziness 6750 /* 6751 The websocket server is a single-process, single-thread, event 6752 I/O thing. It is passed websockets from other CGI processes 6753 and is then responsible for handling their messages and responses. 6754 Note that the CGI process is responsible for websocket setup, 6755 including authentication, etc. 6756 6757 It also gets data sent to it by other processes and is responsible 6758 for distributing that, as necessary. 6759 */ 6760 void runWebsocketServer()() { 6761 assert(0, "not implemented"); 6762 } 6763 6764 void sendToWebsocketServer(WebSocket ws, string group) { 6765 assert(0, "not implemented"); 6766 } 6767 6768 void sendToWebsocketServer(string content, string group) { 6769 assert(0, "not implemented"); 6770 } 6771 6772 6773 void runEventServer()() { 6774 runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation()); 6775 } 6776 6777 void runTimerServer()() { 6778 runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation()); 6779 } 6780 6781 version(Posix) { 6782 alias LocalServerConnectionHandle = int; 6783 alias CgiConnectionHandle = int; 6784 alias SocketConnectionHandle = int; 6785 6786 enum INVALID_CGI_CONNECTION_HANDLE = -1; 6787 } else version(Windows) { 6788 alias LocalServerConnectionHandle = HANDLE; 6789 version(embedded_httpd_threads) { 6790 alias CgiConnectionHandle = SOCKET; 6791 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 6792 } else version(fastcgi) { 6793 alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point. 6794 enum INVALID_CGI_CONNECTION_HANDLE = null; 6795 } else version(scgi) { 6796 alias CgiConnectionHandle = SOCKET; 6797 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 6798 } else { /* version(plain_cgi) */ 6799 alias CgiConnectionHandle = HANDLE; 6800 enum INVALID_CGI_CONNECTION_HANDLE = null; 6801 } 6802 alias SocketConnectionHandle = SOCKET; 6803 } 6804 6805 version(with_addon_servers_connections) 6806 LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) { 6807 version(Posix) { 6808 import core.sys.posix.unistd; 6809 import core.sys.posix.sys.un; 6810 6811 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 6812 if(sock == -1) 6813 throw new Exception("socket " ~ to!string(errno)); 6814 6815 scope(failure) 6816 close(sock); 6817 6818 cloexec(sock); 6819 6820 // add-on server processes are assumed to be local, and thus will 6821 // use unix domain sockets. Besides, I want to pass sockets to them, 6822 // so it basically must be local (except for the session server, but meh). 6823 sockaddr_un addr; 6824 addr.sun_family = AF_UNIX; 6825 version(linux) { 6826 // on linux, we will use the abstract namespace 6827 addr.sun_path[0] = 0; 6828 addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[]; 6829 } else { 6830 // but otherwise, just use a file cuz we must. 6831 addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[]; 6832 } 6833 6834 bool alreadyTried; 6835 6836 try_again: 6837 6838 if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { 6839 if(!alreadyTried && errno == ECONNREFUSED) { 6840 // try auto-spawning the server, then attempt connection again 6841 startAddonServer(arg); 6842 import core.thread; 6843 Thread.sleep(50.msecs); 6844 alreadyTried = true; 6845 goto try_again; 6846 } else 6847 throw new Exception("connect " ~ to!string(errno)); 6848 } 6849 6850 return sock; 6851 } else version(Windows) { 6852 return null; // FIXME 6853 } 6854 } 6855 6856 version(with_addon_servers_connections) 6857 void closeLocalServerConnection(LocalServerConnectionHandle handle) { 6858 version(Posix) { 6859 import core.sys.posix.unistd; 6860 close(handle); 6861 } else version(Windows) 6862 CloseHandle(handle); 6863 } 6864 6865 void runSessionServer()() { 6866 runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation()); 6867 } 6868 6869 version(Posix) 6870 private void makeNonBlocking(int fd) { 6871 import core.sys.posix.fcntl; 6872 auto flags = fcntl(fd, F_GETFL, 0); 6873 if(flags == -1) 6874 throw new Exception("fcntl get"); 6875 flags |= O_NONBLOCK; 6876 auto s = fcntl(fd, F_SETFL, flags); 6877 if(s == -1) 6878 throw new Exception("fcntl set"); 6879 } 6880 6881 import core.stdc.errno; 6882 6883 struct IoOp { 6884 @disable this(); 6885 @disable this(this); 6886 6887 /* 6888 So we want to be able to eventually handle generic sockets too. 6889 */ 6890 6891 enum Read = 1; 6892 enum Write = 2; 6893 enum Accept = 3; 6894 enum ReadSocketHandle = 4; 6895 6896 // Your handler may be called in a different thread than the one that initiated the IO request! 6897 // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution. 6898 private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed 6899 private void delegate(IoOp*) closeHandler; 6900 private void delegate(IoOp*) completeHandler; 6901 private int internalFd; 6902 private int operation; 6903 private int bufferLengthAllocated; 6904 private int bufferLengthUsed; 6905 private ubyte[1] internalBuffer; // it can be overallocated! 6906 6907 ubyte[] allocatedBuffer() return { 6908 return internalBuffer.ptr[0 .. bufferLengthAllocated]; 6909 } 6910 6911 ubyte[] usedBuffer() return { 6912 return allocatedBuffer[0 .. bufferLengthUsed]; 6913 } 6914 6915 void reset() { 6916 bufferLengthUsed = 0; 6917 } 6918 6919 int fd() { 6920 return internalFd; 6921 } 6922 } 6923 6924 IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) { 6925 import core.stdc.stdlib; 6926 6927 auto ptr = calloc(IoOp.sizeof + bufferSize, 1); 6928 if(ptr is null) 6929 assert(0); // out of memory! 6930 6931 auto op = cast(IoOp*) ptr; 6932 6933 op.handler = handler; 6934 op.internalFd = fd; 6935 op.operation = operation; 6936 op.bufferLengthAllocated = bufferSize; 6937 op.bufferLengthUsed = 0; 6938 6939 import core.memory; 6940 6941 GC.addRoot(ptr); 6942 6943 return op; 6944 } 6945 6946 void freeIoOp(ref IoOp* ptr) { 6947 6948 import core.memory; 6949 GC.removeRoot(ptr); 6950 6951 import core.stdc.stdlib; 6952 free(ptr); 6953 ptr = null; 6954 } 6955 6956 version(Posix) 6957 version(with_addon_servers_connections) 6958 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 6959 6960 //import std.stdio : writeln; writeln(cast(string) data); 6961 6962 import core.sys.posix.unistd; 6963 6964 auto ret = write(connection, data.ptr, data.length); 6965 if(ret != data.length) { 6966 if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { 6967 // the file is closed, remove it 6968 eis.fileClosed(connection); 6969 } else 6970 throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME 6971 } 6972 } 6973 version(Windows) 6974 version(with_addon_servers_connections) 6975 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 6976 // FIXME 6977 } 6978 6979 bool isInvalidHandle(CgiConnectionHandle h) { 6980 return h == INVALID_CGI_CONNECTION_HANDLE; 6981 } 6982 6983 /+ 6984 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv 6985 https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode 6986 https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive 6987 https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports 6988 https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport 6989 https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex 6990 https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects 6991 https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer 6992 https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call 6993 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult 6994 6995 +/ 6996 6997 /++ 6998 You can customize your server by subclassing the appropriate server. Then, register your 6999 subclass at compile time with the [registerEventIoServer] template, or implement your own 7000 main function and call it yourself. 7001 7002 $(TIP If you make your subclass a `final class`, there is a slight performance improvement.) 7003 +/ 7004 version(with_addon_servers_connections) 7005 interface EventIoServer { 7006 bool handleLocalConnectionData(IoOp* op, int receivedFd); 7007 void handleLocalConnectionClose(IoOp* op); 7008 void handleLocalConnectionComplete(IoOp* op); 7009 void wait_timeout(); 7010 void fileClosed(int fd); 7011 7012 void epoll_fd(int fd); 7013 } 7014 7015 // the sink should buffer it 7016 private void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) { 7017 static if(is(T == struct)) { 7018 foreach(member; __traits(allMembers, T)) 7019 serialize(sink, __traits(getMember, t, member)); 7020 } else static if(is(T : int)) { 7021 // no need to think of endianness just because this is only used 7022 // for local, same-machine stuff anyway. thanks private lol 7023 sink((cast(ubyte*) &t)[0 .. t.sizeof]); 7024 } else static if(is(T == string) || is(T : const(ubyte)[])) { 7025 // these are common enough to optimize 7026 int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc. 7027 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7028 sink(cast(ubyte[]) t[]); 7029 } else static if(is(T : A[], A)) { 7030 // generic array is less optimal but still prolly ok 7031 int len = cast(int) t.length; 7032 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7033 foreach(item; t) 7034 serialize(sink, item); 7035 } else static assert(0, T.stringof); 7036 } 7037 7038 // all may be stack buffers, so use cautio 7039 private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) { 7040 static if(is(T == struct)) { 7041 T t; 7042 foreach(member; __traits(allMembers, T)) 7043 deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; }); 7044 dg(t); 7045 } else static if(is(T : int)) { 7046 // no need to think of endianness just because this is only used 7047 // for local, same-machine stuff anyway. thanks private lol 7048 T t; 7049 auto data = get(t.sizeof); 7050 t = (cast(T[]) data)[0]; 7051 dg(t); 7052 } else static if(is(T == string) || is(T : const(ubyte)[])) { 7053 // these are common enough to optimize 7054 int len; 7055 auto data = get(len.sizeof); 7056 len = (cast(int[]) data)[0]; 7057 7058 /* 7059 typeof(T[0])[2000] stackBuffer; 7060 T buffer; 7061 7062 if(len < stackBuffer.length) 7063 buffer = stackBuffer[0 .. len]; 7064 else 7065 buffer = new T(len); 7066 7067 data = get(len * typeof(T[0]).sizeof); 7068 */ 7069 7070 T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof); 7071 7072 dg(t); 7073 } else static if(is(T == E[], E)) { 7074 T t; 7075 int len; 7076 auto data = get(len.sizeof); 7077 len = (cast(int[]) data)[0]; 7078 t.length = len; 7079 foreach(ref e; t) { 7080 deserialize!E(get, (ele) { e = ele; }); 7081 } 7082 dg(t); 7083 } else static assert(0, T.stringof); 7084 } 7085 7086 unittest { 7087 serialize((ubyte[] b) { 7088 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); }); 7089 }, 1); 7090 serialize((ubyte[] b) { 7091 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); }); 7092 }, 56674); 7093 ubyte[1000] buffer; 7094 int bufferPoint; 7095 void add(ubyte[] b) { 7096 buffer[bufferPoint .. bufferPoint + b.length] = b[]; 7097 bufferPoint += b.length; 7098 } 7099 ubyte[] get(int sz) { 7100 auto b = buffer[bufferPoint .. bufferPoint + sz]; 7101 bufferPoint += sz; 7102 return b; 7103 } 7104 serialize(&add, "test here"); 7105 bufferPoint = 0; 7106 deserialize!string(&get, (t) { assert(t == "test here"); }); 7107 bufferPoint = 0; 7108 7109 struct Foo { 7110 int a; 7111 ubyte c; 7112 string d; 7113 } 7114 serialize(&add, Foo(403, 37, "amazing")); 7115 bufferPoint = 0; 7116 deserialize!Foo(&get, (t) { 7117 assert(t.a == 403); 7118 assert(t.c == 37); 7119 assert(t.d == "amazing"); 7120 }); 7121 bufferPoint = 0; 7122 } 7123 7124 /* 7125 Here's the way the RPC interface works: 7126 7127 You define the interface that lists the functions you can call on the remote process. 7128 The interface may also have static methods for convenience. These forward to a singleton 7129 instance of an auto-generated class, which actually sends the args over the pipe. 7130 7131 An impl class actually implements it. A receiving server deserializes down the pipe and 7132 calls methods on the class. 7133 7134 I went with the interface to get some nice compiler checking and documentation stuff. 7135 7136 I could have skipped the interface and just implemented it all from the server class definition 7137 itself, but then the usage may call the method instead of rpcing it; I just like having the user 7138 interface and the implementation separate so you aren't tempted to `new impl` to call the methods. 7139 7140 7141 I fiddled with newlines in the mixin string to ensure the assert line numbers matched up to the source code line number. Idk why dmd didn't do this automatically, but it was important to me. 7142 7143 Realistically though the bodies would just be 7144 connection.call(this.mangleof, args...) sooooo. 7145 7146 FIXME: overloads aren't supported 7147 */ 7148 7149 /// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this. 7150 interface SessionObject {} 7151 7152 private immutable void delegate(string[])[string] scheduledJobHandlers; 7153 private immutable void delegate(string[])[string] websocketServers; 7154 7155 version(with_breaking_cgi_features) 7156 mixin(q{ 7157 7158 mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) { 7159 static import std.traits; 7160 7161 // derivedMembers on an interface seems to give exactly what I want: the virtual functions we need to implement. so I am just going to use it directly without more filtering. 7162 static foreach(idx, member; __traits(derivedMembers, T)) { 7163 static if(__traits(isVirtualFunction, __traits(getMember, T, member))) 7164 mixin( q{ 7165 std.traits.ReturnType!(__traits(getMember, T, member)) 7166 } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params) 7167 { 7168 SerializationBuffer buffer; 7169 auto i = cast(ushort) idx; 7170 serialize(&buffer.sink, i); 7171 serialize(&buffer.sink, __traits(getMember, T, member).mangleof); 7172 foreach(param; params) 7173 serialize(&buffer.sink, param); 7174 7175 auto sendable = buffer.sendable; 7176 7177 version(Posix) {{ 7178 auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0); 7179 7180 if(ret == -1) { 7181 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 7182 } else if(ret == 0) { 7183 throw new Exception("Connection to addon server lost"); 7184 } if(ret < sendable.length) 7185 throw new Exception("Send failed to send all"); 7186 assert(ret == sendable.length); 7187 }} // FIXME Windows impl 7188 7189 static if(!is(typeof(return) == void)) { 7190 // there is a return value; we need to wait for it too 7191 version(Posix) { 7192 ubyte[3000] revBuffer; 7193 auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0); 7194 auto got = revBuffer[0 .. ret]; 7195 7196 int dataLocation; 7197 ubyte[] grab(int sz) { 7198 auto dataLocation1 = dataLocation; 7199 dataLocation += sz; 7200 return got[dataLocation1 .. dataLocation]; 7201 } 7202 7203 typeof(return) retu; 7204 deserialize!(typeof(return))(&grab, (a) { retu = a; }); 7205 return retu; 7206 } else { 7207 // FIXME Windows impl 7208 return typeof(return).init; 7209 } 7210 7211 } 7212 }}); 7213 } 7214 7215 private static typeof(this) singletonInstance; 7216 private LocalServerConnectionHandle connectionHandle; 7217 7218 static typeof(this) connection() { 7219 if(singletonInstance is null) { 7220 singletonInstance = new typeof(this)(); 7221 singletonInstance.connect(); 7222 } 7223 return singletonInstance; 7224 } 7225 7226 void connect() { 7227 connectionHandle = openLocalServerConnection(serverPath, cmdArg); 7228 } 7229 7230 void disconnect() { 7231 closeLocalServerConnection(connectionHandle); 7232 } 7233 } 7234 7235 void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) { 7236 ushort calledIdx; 7237 string calledFunction; 7238 7239 int dataLocation; 7240 ubyte[] grab(int sz) { 7241 if(sz == 0) assert(0); 7242 auto d = data[dataLocation .. dataLocation + sz]; 7243 dataLocation += sz; 7244 return d; 7245 } 7246 7247 again: 7248 7249 deserialize!ushort(&grab, (a) { calledIdx = a; }); 7250 deserialize!string(&grab, (a) { calledFunction = a; }); 7251 7252 import std.traits; 7253 7254 sw: switch(calledIdx) { 7255 foreach(idx, memberName; __traits(derivedMembers, Interface)) 7256 static if(__traits(isVirtualFunction, __traits(getMember, Interface, memberName))) { 7257 case idx: 7258 assert(calledFunction == __traits(getMember, Interface, memberName).mangleof); 7259 7260 Parameters!(__traits(getMember, Interface, memberName)) params; 7261 foreach(ref param; params) 7262 deserialize!(typeof(param))(&grab, (a) { param = a; }); 7263 7264 static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) { 7265 __traits(getMember, this_, memberName)(params); 7266 } else { 7267 auto ret = __traits(getMember, this_, memberName)(params); 7268 SerializationBuffer buffer; 7269 serialize(&buffer.sink, ret); 7270 7271 auto sendable = buffer.sendable; 7272 7273 version(Posix) { 7274 auto r = send(fd, sendable.ptr, sendable.length, 0); 7275 if(r == -1) { 7276 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 7277 } else if(r == 0) { 7278 throw new Exception("Connection to addon client lost"); 7279 } if(r < sendable.length) 7280 throw new Exception("Send failed to send all"); 7281 7282 } // FIXME Windows impl 7283 } 7284 break sw; 7285 } 7286 default: assert(0); 7287 } 7288 7289 if(dataLocation != data.length) 7290 goto again; 7291 } 7292 7293 7294 private struct SerializationBuffer { 7295 ubyte[2048] bufferBacking; 7296 int bufferLocation; 7297 void sink(scope ubyte[] data) { 7298 bufferBacking[bufferLocation .. bufferLocation + data.length] = data[]; 7299 bufferLocation += data.length; 7300 } 7301 7302 ubyte[] sendable() return { 7303 return bufferBacking[0 .. bufferLocation]; 7304 } 7305 } 7306 7307 /* 7308 FIXME: 7309 add a version command line arg 7310 version data in the library 7311 management gui as external program 7312 7313 at server with event_fd for each run 7314 use .mangleof in the at function name 7315 7316 i think the at server will have to: 7317 pipe args to the child 7318 collect child output for logging 7319 get child return value for logging 7320 7321 on windows timers work differently. idk how to best combine with the io stuff. 7322 7323 will have to have dump and restore too, so i can restart without losing stuff. 7324 */ 7325 7326 /++ 7327 A convenience object for talking to the [BasicDataServer] from a higher level. 7328 See: [Cgi.getSessionObject]. 7329 7330 You pass it a `Data` struct describing the data you want saved in the session. 7331 Then, this class will generate getter and setter properties that allow access 7332 to that data. 7333 7334 Note that each load and store will be done as-accessed; it doesn't front-load 7335 mutable data nor does it batch updates out of fear of read-modify-write race 7336 conditions. (In fact, right now it does this for everything, but in the future, 7337 I might batch load `immutable` members of the Data struct.) 7338 7339 At some point in the future, I might also let it do different backends, like 7340 a client-side cookie store too, but idk. 7341 7342 Note that the plain-old-data members of your `Data` struct are wrapped by this 7343 interface via a static foreach to make property functions. 7344 7345 See_Also: [MockSession] 7346 +/ 7347 interface Session(Data) : SessionObject { 7348 @property string sessionId() const; 7349 7350 /++ 7351 Starts a new session. Note that a session is also 7352 implicitly started as soon as you write data to it, 7353 so if you need to alter these parameters from their 7354 defaults, be sure to explicitly call this BEFORE doing 7355 any writes to session data. 7356 7357 Params: 7358 idleLifetime = How long, in seconds, the session 7359 should remain in memory when not being read from 7360 or written to. The default is one day. 7361 7362 NOT IMPLEMENTED 7363 7364 useExtendedLifetimeCookie = The session ID is always 7365 stored in a HTTP cookie, and by default, that cookie 7366 is discarded when the user closes their browser. 7367 7368 But if you set this to true, it will use a non-perishable 7369 cookie for the given idleLifetime. 7370 7371 NOT IMPLEMENTED 7372 +/ 7373 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false); 7374 7375 /++ 7376 Regenerates the session ID and updates the associated 7377 cookie. 7378 7379 This is also your chance to change immutable data 7380 (not yet implemented). 7381 +/ 7382 void regenerateId(); 7383 7384 /++ 7385 Terminates this session, deleting all saved data. 7386 +/ 7387 void terminate(); 7388 7389 /++ 7390 Plain-old-data members of your `Data` struct are wrapped here via 7391 the property getters and setters. 7392 7393 If the member is a non-string array, it returns a magical array proxy 7394 object which allows for atomic appends and replaces via overloaded operators. 7395 You can slice this to get a range representing a $(B const) view of the array. 7396 This is to protect you against read-modify-write race conditions. 7397 +/ 7398 static foreach(memberName; __traits(allMembers, Data)) 7399 static if(is(typeof(__traits(getMember, Data, memberName)))) 7400 mixin(q{ 7401 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout; 7402 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value); 7403 }); 7404 7405 } 7406 7407 /++ 7408 An implementation of [Session] that works on real cgi connections utilizing the 7409 [BasicDataServer]. 7410 7411 As opposed to a [MockSession] which is made for testing purposes. 7412 7413 You will not construct one of these directly. See [Cgi.getSessionObject] instead. 7414 +/ 7415 class BasicDataServerSession(Data) : Session!Data { 7416 private Cgi cgi; 7417 private string sessionId_; 7418 7419 public @property string sessionId() const { 7420 return sessionId_; 7421 } 7422 7423 protected @property string sessionId(string s) { 7424 return this.sessionId_ = s; 7425 } 7426 7427 private this(Cgi cgi) { 7428 this.cgi = cgi; 7429 if(auto ptr = "sessionId" in cgi.cookies) 7430 sessionId = (*ptr).length ? *ptr : null; 7431 } 7432 7433 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { 7434 assert(sessionId is null); 7435 7436 // FIXME: what if there is a session ID cookie, but no corresponding session on the server? 7437 7438 import std.random, std.conv; 7439 sessionId = to!string(uniform(1, long.max)); 7440 7441 BasicDataServer.connection.createSession(sessionId, idleLifetime); 7442 setCookie(); 7443 } 7444 7445 protected void setCookie() { 7446 cgi.setCookie( 7447 "sessionId", sessionId, 7448 0 /* expiration */, 7449 "/" /* path */, 7450 null /* domain */, 7451 true /* http only */, 7452 cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); 7453 } 7454 7455 void regenerateId() { 7456 if(sessionId is null) { 7457 start(); 7458 return; 7459 } 7460 import std.random, std.conv; 7461 auto oldSessionId = sessionId; 7462 sessionId = to!string(uniform(1, long.max)); 7463 BasicDataServer.connection.renameSession(oldSessionId, sessionId); 7464 setCookie(); 7465 } 7466 7467 void terminate() { 7468 BasicDataServer.connection.destroySession(sessionId); 7469 sessionId = null; 7470 setCookie(); 7471 } 7472 7473 static foreach(memberName; __traits(allMembers, Data)) 7474 static if(is(typeof(__traits(getMember, Data, memberName)))) 7475 mixin(q{ 7476 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 7477 if(sessionId is null) 7478 return typeof(return).init; 7479 7480 import std.traits; 7481 auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName); 7482 if(v.length == 0) 7483 return typeof(return).init; 7484 import std.conv; 7485 // why this cast? to doesn't like being given an inout argument. so need to do it without that, then 7486 // we need to return it and that needed the cast. It should be fine since we basically respect constness.. 7487 // basically. Assuming the session is POD this should be fine. 7488 return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v); 7489 } 7490 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 7491 if(sessionId is null) 7492 start(); 7493 import std.conv; 7494 import std.traits; 7495 BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value)); 7496 return value; 7497 } 7498 }); 7499 } 7500 7501 /++ 7502 A mock object that works like the real session, but doesn't actually interact with any actual database or http connection. 7503 Simply stores the data in its instance members. 7504 +/ 7505 class MockSession(Data) : Session!Data { 7506 pure { 7507 @property string sessionId() const { return "mock"; } 7508 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {} 7509 void regenerateId() {} 7510 void terminate() {} 7511 7512 private Data store_; 7513 7514 static foreach(memberName; __traits(allMembers, Data)) 7515 static if(is(typeof(__traits(getMember, Data, memberName)))) 7516 mixin(q{ 7517 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 7518 return __traits(getMember, store_, memberName); 7519 } 7520 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 7521 return __traits(getMember, store_, memberName) = value; 7522 } 7523 }); 7524 } 7525 } 7526 7527 /++ 7528 Direct interface to the basic data add-on server. You can 7529 typically use [Cgi.getSessionObject] as a more convenient interface. 7530 +/ 7531 version(with_addon_servers_connections) 7532 interface BasicDataServer { 7533 /// 7534 void createSession(string sessionId, int lifetime); 7535 /// 7536 void renewSession(string sessionId, int lifetime); 7537 /// 7538 void destroySession(string sessionId); 7539 /// 7540 void renameSession(string oldSessionId, string newSessionId); 7541 7542 /// 7543 void setSessionData(string sessionId, string dataKey, string dataValue); 7544 /// 7545 string getSessionData(string sessionId, string dataKey); 7546 7547 /// 7548 static BasicDataServerConnection connection() { 7549 return BasicDataServerConnection.connection(); 7550 } 7551 } 7552 7553 version(with_addon_servers_connections) 7554 class BasicDataServerConnection : BasicDataServer { 7555 mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server"); 7556 } 7557 7558 version(with_addon_servers) 7559 final class BasicDataServerImplementation : BasicDataServer, EventIoServer { 7560 7561 void createSession(string sessionId, int lifetime) { 7562 sessions[sessionId.idup] = Session(lifetime); 7563 } 7564 void destroySession(string sessionId) { 7565 sessions.remove(sessionId); 7566 } 7567 void renewSession(string sessionId, int lifetime) { 7568 sessions[sessionId].lifetime = lifetime; 7569 } 7570 void renameSession(string oldSessionId, string newSessionId) { 7571 sessions[newSessionId.idup] = sessions[oldSessionId]; 7572 sessions.remove(oldSessionId); 7573 } 7574 void setSessionData(string sessionId, string dataKey, string dataValue) { 7575 if(sessionId !in sessions) 7576 createSession(sessionId, 3600); // FIXME? 7577 sessions[sessionId].values[dataKey.idup] = dataValue.idup; 7578 } 7579 string getSessionData(string sessionId, string dataKey) { 7580 if(auto session = sessionId in sessions) { 7581 if(auto data = dataKey in (*session).values) 7582 return *data; 7583 else 7584 return null; // no such data 7585 7586 } else { 7587 return null; // no session 7588 } 7589 } 7590 7591 7592 protected: 7593 7594 struct Session { 7595 int lifetime; 7596 7597 string[string] values; 7598 } 7599 7600 Session[string] sessions; 7601 7602 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 7603 auto data = op.usedBuffer; 7604 dispatchRpcServer!BasicDataServer(this, data, op.fd); 7605 return false; 7606 } 7607 7608 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 7609 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 7610 void wait_timeout() {} 7611 void fileClosed(int fd) {} // stateless so irrelevant 7612 void epoll_fd(int fd) {} 7613 } 7614 7615 /++ 7616 See [schedule] to make one of these. You then call one of the methods here to set it up: 7617 7618 --- 7619 schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC 7620 schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds 7621 schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it 7622 --- 7623 +/ 7624 version(with_addon_servers_connections) 7625 struct ScheduledJobHelper { 7626 private string func; 7627 private string[] args; 7628 private bool consumed; 7629 7630 private this(string func, string[] args) { 7631 this.func = func; 7632 this.args = args; 7633 } 7634 7635 ~this() { 7636 assert(consumed); 7637 } 7638 7639 /++ 7640 Schedules the job to be run at the given time. 7641 +/ 7642 void at(DateTime when, immutable TimeZone timezone = UTC()) { 7643 consumed = true; 7644 7645 auto conn = ScheduledJobServerConnection.connection; 7646 import std.file; 7647 auto st = SysTime(when, timezone); 7648 auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args); 7649 } 7650 7651 /++ 7652 Schedules the job to run at least after the specified delay. 7653 +/ 7654 void delay(Duration delay) { 7655 consumed = true; 7656 7657 auto conn = ScheduledJobServerConnection.connection; 7658 import std.file; 7659 auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args); 7660 } 7661 7662 /++ 7663 Runs the job in the background ASAP. 7664 7665 $(NOTE It may run in a background thread. Don't segfault!) 7666 +/ 7667 void asap() { 7668 consumed = true; 7669 7670 auto conn = ScheduledJobServerConnection.connection; 7671 import std.file; 7672 auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args); 7673 } 7674 7675 /+ 7676 /++ 7677 Schedules the job to recur on the given pattern. 7678 +/ 7679 void recur(string spec) { 7680 7681 } 7682 +/ 7683 } 7684 7685 /++ 7686 First step to schedule a job on the scheduled job server. 7687 7688 The scheduled job needs to be a top-level function that doesn't read any 7689 variables from outside its arguments because it may be run in a new process, 7690 without any context existing later. 7691 7692 You MUST set details on the returned object to actually do anything! 7693 +/ 7694 template schedule(alias fn, T...) if(is(typeof(fn) == function)) { 7695 /// 7696 ScheduledJobHelper schedule(T args) { 7697 // this isn't meant to ever be called, but instead just to 7698 // get the compiler to type check the arguments passed for us 7699 auto sample = delegate() { 7700 fn(args); 7701 }; 7702 string[] sargs; 7703 foreach(arg; args) 7704 sargs ~= to!string(arg); 7705 return ScheduledJobHelper(fn.mangleof, sargs); 7706 } 7707 7708 shared static this() { 7709 scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) { 7710 import std.traits; 7711 Parameters!fn args; 7712 foreach(idx, ref arg; args) 7713 arg = to!(typeof(arg))(sargs[idx]); 7714 fn(args); 7715 }; 7716 } 7717 } 7718 7719 /// 7720 interface ScheduledJobServer { 7721 /// Use the [schedule] function for a higher-level interface. 7722 int scheduleJob(int whenIs, int when, string executable, string func, string[] args); 7723 /// 7724 void cancelJob(int jobId); 7725 } 7726 7727 version(with_addon_servers_connections) 7728 class ScheduledJobServerConnection : ScheduledJobServer { 7729 mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server"); 7730 } 7731 7732 version(with_addon_servers) 7733 final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer { 7734 // FIXME: we need to handle SIGCHLD in this somehow 7735 // whenIs is 0 for relative, 1 for absolute 7736 protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) { 7737 auto nj = nextJobId; 7738 nextJobId++; 7739 7740 version(linux) { 7741 import core.sys.linux.timerfd; 7742 import core.sys.linux.epoll; 7743 import core.sys.posix.unistd; 7744 7745 7746 auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); 7747 if(fd == -1) 7748 throw new Exception("fd timer create failed"); 7749 7750 foreach(ref arg; args) 7751 arg = arg.idup; 7752 auto job = Job(executable.idup, func.idup, .dup(args), fd, nj); 7753 7754 itimerspec value; 7755 value.it_value.tv_sec = when; 7756 value.it_value.tv_nsec = 0; 7757 7758 value.it_interval.tv_sec = 0; 7759 value.it_interval.tv_nsec = 0; 7760 7761 if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1) 7762 throw new Exception("couldn't set fd timer"); 7763 7764 auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) { 7765 jobs.remove(nj); 7766 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null); 7767 close(fd); 7768 7769 7770 spawnProcess([job.executable, "--timed-job", job.func] ~ job.args); 7771 7772 return true; 7773 }); 7774 scope(failure) 7775 freeIoOp(op); 7776 7777 epoll_event ev; 7778 ev.events = EPOLLIN | EPOLLET; 7779 ev.data.ptr = op; 7780 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) 7781 throw new Exception("epoll_ctl " ~ to!string(errno)); 7782 7783 jobs[nj] = job; 7784 return nj; 7785 } else assert(0); 7786 } 7787 7788 protected void cancelJob(int jobId) { 7789 version(linux) { 7790 auto job = jobId in jobs; 7791 if(job is null) 7792 return; 7793 7794 jobs.remove(jobId); 7795 7796 version(linux) { 7797 import core.sys.linux.timerfd; 7798 import core.sys.linux.epoll; 7799 import core.sys.posix.unistd; 7800 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null); 7801 close(job.timerfd); 7802 } 7803 } 7804 jobs.remove(jobId); 7805 } 7806 7807 int nextJobId = 1; 7808 static struct Job { 7809 string executable; 7810 string func; 7811 string[] args; 7812 int timerfd; 7813 int id; 7814 } 7815 Job[int] jobs; 7816 7817 7818 // event io server methods below 7819 7820 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 7821 auto data = op.usedBuffer; 7822 dispatchRpcServer!ScheduledJobServer(this, data, op.fd); 7823 return false; 7824 } 7825 7826 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 7827 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 7828 void wait_timeout() {} 7829 void fileClosed(int fd) {} // stateless so irrelevant 7830 7831 int epoll_fd_; 7832 void epoll_fd(int fd) {this.epoll_fd_ = fd; } 7833 int epoll_fd() { return epoll_fd_; } 7834 } 7835 7836 /// 7837 version(with_addon_servers_connections) 7838 interface EventSourceServer { 7839 /++ 7840 sends this cgi request to the event server so it will be fed events. You should not do anything else with the cgi object after this. 7841 7842 $(WARNING This API is extremely unstable. I might change it or remove it without notice.) 7843 7844 See_Also: 7845 [sendEvent] 7846 +/ 7847 public static void adoptConnection(Cgi cgi, in char[] eventUrl) { 7848 /* 7849 If lastEventId is missing or empty, you just get new events as they come. 7850 7851 If it is set from something else, it sends all since then (that are still alive) 7852 down the pipe immediately. 7853 7854 The reason it can come from the header is that's what the standard defines for 7855 browser reconnects. The reason it can come from a query string is just convenience 7856 in catching up in a user-defined manner. 7857 7858 The reason the header overrides the query string is if the browser tries to reconnect, 7859 it will send the header AND the query (it reconnects to the same url), so we just 7860 want to do the restart thing. 7861 7862 Note that if you ask for "0" as the lastEventId, it will get ALL still living events. 7863 */ 7864 string lastEventId = cgi.lastEventId; 7865 if(lastEventId.length == 0 && "lastEventId" in cgi.get) 7866 lastEventId = cgi.get["lastEventId"]; 7867 7868 cgi.setResponseContentType("text/event-stream"); 7869 cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later 7870 cgi.flush(); 7871 7872 cgi.closed = true; 7873 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 7874 scope(exit) 7875 closeLocalServerConnection(s); 7876 7877 version(fastcgi) 7878 throw new Exception("sending fcgi connections not supported"); 7879 else { 7880 auto fd = cgi.getOutputFileHandle(); 7881 if(isInvalidHandle(fd)) 7882 throw new Exception("bad fd from cgi!"); 7883 7884 EventSourceServerImplementation.SendableEventConnection sec; 7885 sec.populate(cgi.responseChunked, eventUrl, lastEventId); 7886 7887 version(Posix) { 7888 auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd); 7889 assert(res == sec.sizeof); 7890 } else version(Windows) { 7891 // FIXME 7892 } 7893 } 7894 } 7895 7896 /++ 7897 Sends an event to the event server, starting it if necessary. The event server will distribute it to any listening clients, and store it for `lifetime` seconds for any later listening clients to catch up later. 7898 7899 $(WARNING This API is extremely unstable. I might change it or remove it without notice.) 7900 7901 Params: 7902 url = A string identifying this event "bucket". Listening clients must also connect to this same string. I called it `url` because I envision it being just passed as the url of the request. 7903 event = the event type string, which is used in the Javascript addEventListener API on EventSource 7904 data = the event data. Available in JS as `event.data`. 7905 lifetime = the amount of time to keep this event for replaying on the event server. 7906 7907 See_Also: 7908 [sendEventToEventServer] 7909 +/ 7910 public static void sendEvent(string url, string event, string data, int lifetime) { 7911 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 7912 scope(exit) 7913 closeLocalServerConnection(s); 7914 7915 EventSourceServerImplementation.SendableEvent sev; 7916 sev.populate(url, event, data, lifetime); 7917 7918 version(Posix) { 7919 auto ret = send(s, &sev, sev.sizeof, 0); 7920 assert(ret == sev.sizeof); 7921 } else version(Windows) { 7922 // FIXME 7923 } 7924 } 7925 7926 /++ 7927 Messages sent to `url` will also be sent to anyone listening on `forwardUrl`. 7928 7929 See_Also: [disconnect] 7930 +/ 7931 void connect(string url, string forwardUrl); 7932 7933 /++ 7934 Disconnects `forwardUrl` from `url` 7935 7936 See_Also: [connect] 7937 +/ 7938 void disconnect(string url, string forwardUrl); 7939 } 7940 7941 /// 7942 version(with_addon_servers) 7943 final class EventSourceServerImplementation : EventSourceServer, EventIoServer { 7944 7945 protected: 7946 7947 void connect(string url, string forwardUrl) { 7948 pipes[url] ~= forwardUrl; 7949 } 7950 void disconnect(string url, string forwardUrl) { 7951 auto t = url in pipes; 7952 if(t is null) 7953 return; 7954 foreach(idx, n; (*t)) 7955 if(n == forwardUrl) { 7956 (*t)[idx] = (*t)[$-1]; 7957 (*t) = (*t)[0 .. $-1]; 7958 break; 7959 } 7960 } 7961 7962 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 7963 if(receivedFd != -1) { 7964 //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer); 7965 7966 //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5); 7967 7968 SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr; 7969 7970 auto url = got.url.idup; 7971 eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false); 7972 7973 // FIXME: catch up on past messages here 7974 } else { 7975 auto data = op.usedBuffer; 7976 auto event = cast(SendableEvent*) data.ptr; 7977 7978 if(event.magic == 0xdeadbeef) { 7979 handleInputEvent(event); 7980 7981 if(event.url in pipes) 7982 foreach(pipe; pipes[event.url]) { 7983 event.url = pipe; 7984 handleInputEvent(event); 7985 } 7986 } else { 7987 dispatchRpcServer!EventSourceServer(this, data, op.fd); 7988 } 7989 } 7990 return false; 7991 } 7992 void handleLocalConnectionClose(IoOp* op) { 7993 fileClosed(op.fd); 7994 } 7995 void handleLocalConnectionComplete(IoOp* op) {} 7996 7997 void wait_timeout() { 7998 // just keeping alive 7999 foreach(url, connections; eventConnectionsByUrl) 8000 foreach(connection; connections) 8001 if(connection.needsChunking) 8002 nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); 8003 else 8004 nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); 8005 } 8006 8007 void fileClosed(int fd) { 8008 outer: foreach(url, ref connections; eventConnectionsByUrl) { 8009 foreach(idx, conn; connections) { 8010 if(fd == conn.fd) { 8011 connections[idx] = connections[$-1]; 8012 connections = connections[0 .. $ - 1]; 8013 continue outer; 8014 } 8015 } 8016 } 8017 } 8018 8019 void epoll_fd(int fd) {} 8020 8021 8022 private: 8023 8024 8025 struct SendableEventConnection { 8026 ubyte responseChunked; 8027 8028 int urlLength; 8029 char[256] urlBuffer = 0; 8030 8031 int lastEventIdLength; 8032 char[32] lastEventIdBuffer = 0; 8033 8034 char[] url() return { 8035 return urlBuffer[0 .. urlLength]; 8036 } 8037 void url(in char[] u) { 8038 urlBuffer[0 .. u.length] = u[]; 8039 urlLength = cast(int) u.length; 8040 } 8041 char[] lastEventId() return { 8042 return lastEventIdBuffer[0 .. lastEventIdLength]; 8043 } 8044 void populate(bool responseChunked, in char[] url, in char[] lastEventId) 8045 in { 8046 assert(url.length < this.urlBuffer.length); 8047 assert(lastEventId.length < this.lastEventIdBuffer.length); 8048 } 8049 do { 8050 this.responseChunked = responseChunked ? 1 : 0; 8051 this.urlLength = cast(int) url.length; 8052 this.lastEventIdLength = cast(int) lastEventId.length; 8053 8054 this.urlBuffer[0 .. url.length] = url[]; 8055 this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[]; 8056 } 8057 } 8058 8059 struct SendableEvent { 8060 int magic = 0xdeadbeef; 8061 int urlLength; 8062 char[256] urlBuffer = 0; 8063 int typeLength; 8064 char[32] typeBuffer = 0; 8065 int messageLength; 8066 char[2048 * 4] messageBuffer = 0; // this is an arbitrary limit, it needs to fit comfortably in stack (including in a fiber) and be a single send on the kernel side cuz of the impl... i think this is ok for a unix socket. 8067 int _lifetime; 8068 8069 char[] message() return { 8070 return messageBuffer[0 .. messageLength]; 8071 } 8072 char[] type() return { 8073 return typeBuffer[0 .. typeLength]; 8074 } 8075 char[] url() return { 8076 return urlBuffer[0 .. urlLength]; 8077 } 8078 void url(in char[] u) { 8079 urlBuffer[0 .. u.length] = u[]; 8080 urlLength = cast(int) u.length; 8081 } 8082 int lifetime() { 8083 return _lifetime; 8084 } 8085 8086 /// 8087 void populate(string url, string type, string message, int lifetime) 8088 in { 8089 assert(url.length < this.urlBuffer.length); 8090 assert(type.length < this.typeBuffer.length); 8091 assert(message.length < this.messageBuffer.length); 8092 } 8093 do { 8094 this.urlLength = cast(int) url.length; 8095 this.typeLength = cast(int) type.length; 8096 this.messageLength = cast(int) message.length; 8097 this._lifetime = lifetime; 8098 8099 this.urlBuffer[0 .. url.length] = url[]; 8100 this.typeBuffer[0 .. type.length] = type[]; 8101 this.messageBuffer[0 .. message.length] = message[]; 8102 } 8103 } 8104 8105 struct EventConnection { 8106 int fd; 8107 bool needsChunking; 8108 } 8109 8110 private EventConnection[][string] eventConnectionsByUrl; 8111 private string[][string] pipes; 8112 8113 private void handleInputEvent(scope SendableEvent* event) { 8114 static int eventId; 8115 8116 static struct StoredEvent { 8117 int id; 8118 string type; 8119 string message; 8120 int lifetimeRemaining; 8121 } 8122 8123 StoredEvent[][string] byUrl; 8124 8125 int thisId = ++eventId; 8126 8127 if(event.lifetime) 8128 byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime); 8129 8130 auto connectionsPtr = event.url in eventConnectionsByUrl; 8131 EventConnection[] connections; 8132 if(connectionsPtr is null) 8133 return; 8134 else 8135 connections = *connectionsPtr; 8136 8137 char[4096] buffer; 8138 char[] formattedMessage; 8139 8140 void append(const char[] a) { 8141 // the 6's here are to leave room for a HTTP chunk header, if it proves necessary 8142 buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[]; 8143 formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length]; 8144 } 8145 8146 import std.algorithm.iteration; 8147 8148 if(connections.length) { 8149 append("id: "); 8150 append(to!string(thisId)); 8151 append("\n"); 8152 8153 append("event: "); 8154 append(event.type); 8155 append("\n"); 8156 8157 foreach(line; event.message.splitter("\n")) { 8158 append("data: "); 8159 append(line); 8160 append("\n"); 8161 } 8162 8163 append("\n"); 8164 } 8165 8166 // chunk it for HTTP! 8167 auto len = toHex(formattedMessage.length); 8168 buffer[4 .. 6] = "\r\n"[]; 8169 buffer[4 - len.length .. 4] = len[]; 8170 buffer[6 + formattedMessage.length] = '\r'; 8171 buffer[6 + formattedMessage.length + 1] = '\n'; 8172 8173 auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2]; 8174 // done 8175 8176 // FIXME: send back requests when needed 8177 // FIXME: send a single ":\n" every 15 seconds to keep alive 8178 8179 foreach(connection; connections) { 8180 if(connection.needsChunking) { 8181 nonBlockingWrite(this, connection.fd, chunkedMessage); 8182 } else { 8183 nonBlockingWrite(this, connection.fd, formattedMessage); 8184 } 8185 } 8186 } 8187 } 8188 8189 void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) { 8190 version(Posix) { 8191 8192 import core.sys.posix.unistd; 8193 import core.sys.posix.fcntl; 8194 import core.sys.posix.sys.un; 8195 8196 import core.sys.posix.signal; 8197 signal(SIGPIPE, SIG_IGN); 8198 8199 static extern(C) void sigchldhandler(int) { 8200 int status; 8201 import w = core.sys.posix.sys.wait; 8202 w.wait(&status); 8203 } 8204 signal(SIGCHLD, &sigchldhandler); 8205 8206 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 8207 if(sock == -1) 8208 throw new Exception("socket " ~ to!string(errno)); 8209 8210 scope(failure) 8211 close(sock); 8212 8213 cloexec(sock); 8214 8215 // add-on server processes are assumed to be local, and thus will 8216 // use unix domain sockets. Besides, I want to pass sockets to them, 8217 // so it basically must be local (except for the session server, but meh). 8218 sockaddr_un addr; 8219 addr.sun_family = AF_UNIX; 8220 version(linux) { 8221 // on linux, we will use the abstract namespace 8222 addr.sun_path[0] = 0; 8223 addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[]; 8224 } else { 8225 // but otherwise, just use a file cuz we must. 8226 addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[]; 8227 } 8228 8229 if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) 8230 throw new Exception("bind " ~ to!string(errno)); 8231 8232 if(listen(sock, 128) == -1) 8233 throw new Exception("listen " ~ to!string(errno)); 8234 8235 makeNonBlocking(sock); 8236 8237 version(linux) { 8238 import core.sys.linux.epoll; 8239 auto epoll_fd = epoll_create1(EPOLL_CLOEXEC); 8240 if(epoll_fd == -1) 8241 throw new Exception("epoll_create1 " ~ to!string(errno)); 8242 scope(failure) 8243 close(epoll_fd); 8244 } else { 8245 import core.sys.posix.poll; 8246 } 8247 8248 version(linux) 8249 eis.epoll_fd = epoll_fd; 8250 8251 auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null); 8252 scope(exit) 8253 freeIoOp(acceptOp); 8254 8255 version(linux) { 8256 epoll_event ev; 8257 ev.events = EPOLLIN | EPOLLET; 8258 ev.data.ptr = acceptOp; 8259 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1) 8260 throw new Exception("epoll_ctl " ~ to!string(errno)); 8261 8262 epoll_event[64] events; 8263 } else { 8264 pollfd[] pollfds; 8265 IoOp*[int] ioops; 8266 pollfds ~= pollfd(sock, POLLIN); 8267 ioops[sock] = acceptOp; 8268 } 8269 8270 import core.time : MonoTime, seconds; 8271 8272 MonoTime timeout = MonoTime.currTime + 15.seconds; 8273 8274 while(true) { 8275 8276 // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently 8277 8278 int timeout_milliseconds = 0; // -1; // infinite 8279 8280 timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; 8281 if(timeout_milliseconds < 0) 8282 timeout_milliseconds = 0; 8283 8284 //writeln("waiting for ", name); 8285 8286 version(linux) { 8287 auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds); 8288 if(nfds == -1) { 8289 if(errno == EINTR) 8290 continue; 8291 throw new Exception("epoll_wait " ~ to!string(errno)); 8292 } 8293 } else { 8294 int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds); 8295 size_t lastIdx = 0; 8296 } 8297 8298 if(nfds == 0) { 8299 eis.wait_timeout(); 8300 timeout += 15.seconds; 8301 } 8302 8303 foreach(idx; 0 .. nfds) { 8304 version(linux) { 8305 auto flags = events[idx].events; 8306 auto ioop = cast(IoOp*) events[idx].data.ptr; 8307 } else { 8308 IoOp* ioop; 8309 foreach(tidx, thing; pollfds[lastIdx .. $]) { 8310 if(thing.revents) { 8311 ioop = ioops[thing.fd]; 8312 lastIdx += tidx + 1; 8313 break; 8314 } 8315 } 8316 } 8317 8318 //writeln(flags, " ", ioop.fd); 8319 8320 void newConnection() { 8321 // on edge triggering, it is important that we get it all 8322 while(true) { 8323 version(Android) { 8324 auto size = cast(int) addr.sizeof; 8325 } else { 8326 auto size = cast(uint) addr.sizeof; 8327 } 8328 auto ns = accept(sock, cast(sockaddr*) &addr, &size); 8329 if(ns == -1) { 8330 if(errno == EAGAIN || errno == EWOULDBLOCK) { 8331 // all done, got it all 8332 break; 8333 } 8334 throw new Exception("accept " ~ to!string(errno)); 8335 } 8336 cloexec(ns); 8337 8338 makeNonBlocking(ns); 8339 auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData); 8340 niop.closeHandler = &eis.handleLocalConnectionClose; 8341 niop.completeHandler = &eis.handleLocalConnectionComplete; 8342 scope(failure) freeIoOp(niop); 8343 8344 version(linux) { 8345 epoll_event nev; 8346 nev.events = EPOLLIN | EPOLLET; 8347 nev.data.ptr = niop; 8348 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1) 8349 throw new Exception("epoll_ctl " ~ to!string(errno)); 8350 } else { 8351 bool found = false; 8352 foreach(ref pfd; pollfds) { 8353 if(pfd.fd < 0) { 8354 pfd.fd = ns; 8355 found = true; 8356 } 8357 } 8358 if(!found) 8359 pollfds ~= pollfd(ns, POLLIN); 8360 ioops[ns] = niop; 8361 } 8362 } 8363 } 8364 8365 bool newConnectionCondition() { 8366 version(linux) 8367 return ioop.fd == sock && (flags & EPOLLIN); 8368 else 8369 return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN); 8370 } 8371 8372 if(newConnectionCondition()) { 8373 newConnection(); 8374 } else if(ioop.operation == IoOp.ReadSocketHandle) { 8375 while(true) { 8376 int in_fd; 8377 auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd); 8378 if(got == -1) { 8379 if(errno == EAGAIN || errno == EWOULDBLOCK) { 8380 // all done, got it all 8381 if(ioop.completeHandler) 8382 ioop.completeHandler(ioop); 8383 break; 8384 } 8385 throw new Exception("recv " ~ to!string(errno)); 8386 } 8387 8388 if(got == 0) { 8389 if(ioop.closeHandler) { 8390 ioop.closeHandler(ioop); 8391 version(linux) {} // nothing needed 8392 else { 8393 foreach(ref pfd; pollfds) { 8394 if(pfd.fd == ioop.fd) 8395 pfd.fd = -1; 8396 } 8397 } 8398 } 8399 close(ioop.fd); 8400 freeIoOp(ioop); 8401 break; 8402 } 8403 8404 ioop.bufferLengthUsed = cast(int) got; 8405 ioop.handler(ioop, in_fd); 8406 } 8407 } else if(ioop.operation == IoOp.Read) { 8408 while(true) { 8409 auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length); 8410 if(got == -1) { 8411 if(errno == EAGAIN || errno == EWOULDBLOCK) { 8412 // all done, got it all 8413 if(ioop.completeHandler) 8414 ioop.completeHandler(ioop); 8415 break; 8416 } 8417 throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno)); 8418 } 8419 8420 if(got == 0) { 8421 if(ioop.closeHandler) 8422 ioop.closeHandler(ioop); 8423 close(ioop.fd); 8424 freeIoOp(ioop); 8425 break; 8426 } 8427 8428 ioop.bufferLengthUsed = cast(int) got; 8429 if(ioop.handler(ioop, ioop.fd)) { 8430 close(ioop.fd); 8431 freeIoOp(ioop); 8432 break; 8433 } 8434 } 8435 } 8436 8437 // EPOLLHUP? 8438 } 8439 } 8440 } else version(Windows) { 8441 8442 // set up a named pipe 8443 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx 8444 // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw 8445 // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid 8446 8447 } else static assert(0); 8448 } 8449 8450 8451 version(with_sendfd) 8452 // copied from the web and ported from C 8453 // see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t 8454 ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { 8455 msghdr msg; 8456 iovec[1] iov; 8457 8458 version(OSX) { 8459 //msg.msg_accrights = cast(cattr_t) &sendfd; 8460 //msg.msg_accrightslen = int.sizeof; 8461 } else version(Android) { 8462 } else { 8463 union ControlUnion { 8464 cmsghdr cm; 8465 char[CMSG_SPACE(int.sizeof)] control; 8466 } 8467 8468 ControlUnion control_un; 8469 cmsghdr* cmptr; 8470 8471 msg.msg_control = control_un.control.ptr; 8472 msg.msg_controllen = control_un.control.length; 8473 8474 cmptr = CMSG_FIRSTHDR(&msg); 8475 cmptr.cmsg_len = CMSG_LEN(int.sizeof); 8476 cmptr.cmsg_level = SOL_SOCKET; 8477 cmptr.cmsg_type = SCM_RIGHTS; 8478 *(cast(int *) CMSG_DATA(cmptr)) = sendfd; 8479 } 8480 8481 msg.msg_name = null; 8482 msg.msg_namelen = 0; 8483 8484 iov[0].iov_base = ptr; 8485 iov[0].iov_len = nbytes; 8486 msg.msg_iov = iov.ptr; 8487 msg.msg_iovlen = 1; 8488 8489 return sendmsg(fd, &msg, 0); 8490 } 8491 8492 version(with_sendfd) 8493 // copied from the web and ported from C 8494 ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { 8495 msghdr msg; 8496 iovec[1] iov; 8497 ssize_t n; 8498 int newfd; 8499 8500 version(OSX) { 8501 //msg.msg_accrights = cast(cattr_t) recvfd; 8502 //msg.msg_accrightslen = int.sizeof; 8503 } else version(Android) { 8504 } else { 8505 union ControlUnion { 8506 cmsghdr cm; 8507 char[CMSG_SPACE(int.sizeof)] control; 8508 } 8509 ControlUnion control_un; 8510 cmsghdr* cmptr; 8511 8512 msg.msg_control = control_un.control.ptr; 8513 msg.msg_controllen = control_un.control.length; 8514 } 8515 8516 msg.msg_name = null; 8517 msg.msg_namelen = 0; 8518 8519 iov[0].iov_base = ptr; 8520 iov[0].iov_len = nbytes; 8521 msg.msg_iov = iov.ptr; 8522 msg.msg_iovlen = 1; 8523 8524 if ( (n = recvmsg(fd, &msg, 0)) <= 0) 8525 return n; 8526 8527 version(OSX) { 8528 //if(msg.msg_accrightslen != int.sizeof) 8529 //*recvfd = -1; 8530 } else version(Android) { 8531 } else { 8532 if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && 8533 cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { 8534 if (cmptr.cmsg_level != SOL_SOCKET) 8535 throw new Exception("control level != SOL_SOCKET"); 8536 if (cmptr.cmsg_type != SCM_RIGHTS) 8537 throw new Exception("control type != SCM_RIGHTS"); 8538 *recvfd = *(cast(int *) CMSG_DATA(cmptr)); 8539 } else 8540 *recvfd = -1; /* descriptor was not passed */ 8541 } 8542 8543 return n; 8544 } 8545 /* end read_fd */ 8546 8547 8548 /* 8549 Event source stuff 8550 8551 The api is: 8552 8553 sendEvent(string url, string type, string data, int timeout = 60*10); 8554 8555 attachEventListener(string url, int fd, lastId) 8556 8557 8558 It just sends to all attached listeners, and stores it until the timeout 8559 for replaying via lastEventId. 8560 */ 8561 8562 /* 8563 Session process stuff 8564 8565 it stores it all. the cgi object has a session object that can grab it 8566 8567 session may be done in the same process if possible, there is a version 8568 switch to choose if you want to override. 8569 */ 8570 8571 struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; 8572 alias handler = dispatchHandler; 8573 string urlPrefix; 8574 bool rejectFurther; 8575 immutable(DispatcherDetails) details; 8576 } 8577 8578 private string urlify(string name) pure { 8579 return beautify(name, '-', true); 8580 } 8581 8582 private string beautify(string name, char space = ' ', bool allLowerCase = false) pure { 8583 if(name == "id") 8584 return allLowerCase ? name : "ID"; 8585 8586 char[160] buffer; 8587 int bufferIndex = 0; 8588 bool shouldCap = true; 8589 bool shouldSpace; 8590 bool lastWasCap; 8591 foreach(idx, char ch; name) { 8592 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 8593 8594 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 8595 if(lastWasCap) { 8596 // two caps in a row, don't change. Prolly acronym. 8597 } else { 8598 if(idx) 8599 shouldSpace = true; // new word, add space 8600 } 8601 8602 lastWasCap = true; 8603 } else { 8604 lastWasCap = false; 8605 } 8606 8607 if(shouldSpace) { 8608 buffer[bufferIndex++] = space; 8609 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 8610 shouldSpace = false; 8611 } 8612 if(shouldCap) { 8613 if(ch >= 'a' && ch <= 'z') 8614 ch -= 32; 8615 shouldCap = false; 8616 } 8617 if(allLowerCase && ch >= 'A' && ch <= 'Z') 8618 ch += 32; 8619 buffer[bufferIndex++] = ch; 8620 } 8621 return buffer[0 .. bufferIndex].idup; 8622 } 8623 8624 /* 8625 string urlFor(alias func)() { 8626 return __traits(identifier, func); 8627 } 8628 */ 8629 8630 /++ 8631 UDA: The name displayed to the user in auto-generated HTML. 8632 8633 Default is `beautify(identifier)`. 8634 +/ 8635 struct DisplayName { 8636 string name; 8637 } 8638 8639 /++ 8640 UDA: The name used in the URL or web parameter. 8641 8642 Default is `urlify(identifier)` for functions and `identifier` for parameters and data members. 8643 +/ 8644 struct UrlName { 8645 string name; 8646 } 8647 8648 /++ 8649 UDA: default format to respond for this method 8650 +/ 8651 struct DefaultFormat { string value; } 8652 8653 class MissingArgumentException : Exception { 8654 string functionName; 8655 string argumentName; 8656 string argumentType; 8657 8658 this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 8659 this.functionName = functionName; 8660 this.argumentName = argumentName; 8661 this.argumentType = argumentType; 8662 8663 super("Missing Argument: " ~ this.argumentName, file, line, next); 8664 } 8665 } 8666 8667 /++ 8668 You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. 8669 8670 History: 8671 Added December 15, 2021 (dub v10.5) 8672 +/ 8673 class ResourceNotFoundException : Exception { 8674 string resourceType; 8675 string resourceId; 8676 8677 this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 8678 this.resourceType = resourceType; 8679 this.resourceId = resourceId; 8680 8681 super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); 8682 } 8683 8684 } 8685 8686 /++ 8687 This can be attached to any constructor or function called from the cgi system. 8688 8689 If it is present, the function argument can NOT be set from web params, but instead 8690 is set to the return value of the given `func`. 8691 8692 If `func` can take a parameter of type [Cgi], it will be passed the one representing 8693 the current request. Otherwise, it must take zero arguments. 8694 8695 Any params in your function of type `Cgi` are automatically assumed to take the cgi object 8696 for the connection. Any of type [Session] (with an argument) is also assumed to come from 8697 the cgi object. 8698 8699 const arguments are also supported. 8700 +/ 8701 struct ifCalledFromWeb(alias func) {} 8702 8703 // it only looks at query params for GET requests, the rest must be in the body for a function argument. 8704 auto callFromCgi(alias method, T)(T dg, Cgi cgi) { 8705 8706 // FIXME: any array of structs should also be settable or gettable from csv as well. 8707 8708 // FIXME: think more about checkboxes and bools. 8709 8710 import std.traits; 8711 8712 Parameters!method params; 8713 alias idents = ParameterIdentifierTuple!method; 8714 alias defaults = ParameterDefaults!method; 8715 8716 const(string)[] names; 8717 const(string)[] values; 8718 8719 // first, check for missing arguments and initialize to defaults if necessary 8720 8721 static if(is(typeof(method) P == __parameters)) 8722 foreach(idx, param; P) {{ 8723 // see: mustNotBeSetFromWebParams 8724 static if(is(param : Cgi)) { 8725 static assert(!is(param == immutable)); 8726 cast() params[idx] = cgi; 8727 } else static if(is(param == Session!D, D)) { 8728 static assert(!is(param == immutable)); 8729 cast() params[idx] = cgi.getSessionObject!D(); 8730 } else { 8731 bool populated; 8732 foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) { 8733 static if(is(uda == ifCalledFromWeb!func, alias func)) { 8734 static if(is(typeof(func(cgi)))) 8735 params[idx] = func(cgi); 8736 else 8737 params[idx] = func(); 8738 8739 populated = true; 8740 } 8741 } 8742 8743 if(!populated) { 8744 static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) { 8745 params[idx] = param.getAutomaticallyForCgi(cgi); 8746 populated = true; 8747 } 8748 } 8749 8750 if(!populated) { 8751 auto ident = idents[idx]; 8752 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 8753 if(ident !in cgi.get) { 8754 static if(is(defaults[idx] == void)) { 8755 static if(is(param == bool)) 8756 params[idx] = false; 8757 else 8758 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 8759 } else 8760 params[idx] = defaults[idx]; 8761 } 8762 } else { 8763 if(ident !in cgi.post) { 8764 static if(is(defaults[idx] == void)) { 8765 static if(is(param == bool)) 8766 params[idx] = false; 8767 else 8768 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 8769 } else 8770 params[idx] = defaults[idx]; 8771 } 8772 } 8773 } 8774 } 8775 }} 8776 8777 // second, parse the arguments in order to build up arrays, etc. 8778 8779 static bool setVariable(T)(string name, string paramName, T* what, string value) { 8780 static if(is(T == struct)) { 8781 if(name == paramName) { 8782 *what = T.init; 8783 return true; 8784 } else { 8785 // could be a child. gonna allow either obj.field OR obj[field] 8786 8787 string afterName; 8788 8789 if(name[paramName.length] == '[') { 8790 int count = 1; 8791 auto idx = paramName.length + 1; 8792 while(idx < name.length && count > 0) { 8793 if(name[idx] == '[') 8794 count++; 8795 else if(name[idx] == ']') { 8796 count--; 8797 if(count == 0) break; 8798 } 8799 idx++; 8800 } 8801 8802 if(idx == name.length) 8803 return false; // malformed 8804 8805 auto insideBrackets = name[paramName.length + 1 .. idx]; 8806 afterName = name[idx + 1 .. $]; 8807 8808 name = name[0 .. paramName.length]; 8809 8810 paramName = insideBrackets; 8811 8812 } else if(name[paramName.length] == '.') { 8813 paramName = name[paramName.length + 1 .. $]; 8814 name = paramName; 8815 int p = 0; 8816 foreach(ch; paramName) { 8817 if(ch == '.' || ch == '[') 8818 break; 8819 p++; 8820 } 8821 8822 afterName = paramName[p .. $]; 8823 paramName = paramName[0 .. p]; 8824 } else { 8825 return false; 8826 } 8827 8828 if(paramName.length) 8829 // set the child member 8830 switch(paramName) { 8831 foreach(idx, memberName; __traits(allMembers, T)) 8832 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 8833 // data member! 8834 case memberName: 8835 return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value); 8836 } 8837 default: 8838 // ok, not a member 8839 } 8840 } 8841 8842 return false; 8843 } else static if(is(T == enum)) { 8844 *what = to!T(value); 8845 return true; 8846 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 8847 *what = to!T(value); 8848 return true; 8849 } else static if(is(T == bool)) { 8850 *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on"; 8851 return true; 8852 } else static if(is(T == K[], K)) { 8853 K tmp; 8854 if(name == paramName) { 8855 // direct - set and append 8856 if(setVariable(name, paramName, &tmp, value)) { 8857 (*what) ~= tmp; 8858 return true; 8859 } else { 8860 return false; 8861 } 8862 } else { 8863 // child, append to last element 8864 // FIXME: what about range violations??? 8865 auto ptr = &(*what)[(*what).length - 1]; 8866 return setVariable(name, paramName, ptr, value); 8867 8868 } 8869 } else static if(is(T == V[K], K, V)) { 8870 // assoc array, name[key] is valid 8871 if(name == paramName) { 8872 // no action necessary 8873 return true; 8874 } else if(name[paramName.length] == '[') { 8875 int count = 1; 8876 auto idx = paramName.length + 1; 8877 while(idx < name.length && count > 0) { 8878 if(name[idx] == '[') 8879 count++; 8880 else if(name[idx] == ']') { 8881 count--; 8882 if(count == 0) break; 8883 } 8884 idx++; 8885 } 8886 if(idx == name.length) 8887 return false; // malformed 8888 8889 auto insideBrackets = name[paramName.length + 1 .. idx]; 8890 auto afterName = name[idx + 1 .. $]; 8891 8892 auto k = to!K(insideBrackets); 8893 V v; 8894 if(auto ptr = k in *what) 8895 v = *ptr; 8896 8897 name = name[0 .. paramName.length]; 8898 //writeln(name, afterName, " ", paramName); 8899 8900 auto ret = setVariable(name ~ afterName, paramName, &v, value); 8901 if(ret) { 8902 (*what)[k] = v; 8903 return true; 8904 } 8905 } 8906 8907 return false; 8908 } else { 8909 static assert(0, "unsupported type for cgi call " ~ T.stringof); 8910 } 8911 8912 //return false; 8913 } 8914 8915 void setArgument(string name, string value) { 8916 int p; 8917 foreach(ch; name) { 8918 if(ch == '.' || ch == '[') 8919 break; 8920 p++; 8921 } 8922 8923 auto paramName = name[0 .. p]; 8924 8925 sw: switch(paramName) { 8926 static if(is(typeof(method) P == __parameters)) 8927 foreach(idx, param; P) { 8928 static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) { 8929 // cannot be set from the outside 8930 } else { 8931 case idents[idx]: 8932 static if(is(param == Cgi.UploadedFile)) { 8933 params[idx] = cgi.files[name]; 8934 } else { 8935 setVariable(name, paramName, ¶ms[idx], value); 8936 } 8937 break sw; 8938 } 8939 } 8940 default: 8941 // ignore; not relevant argument 8942 } 8943 } 8944 8945 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 8946 names = cgi.allGetNamesInOrder; 8947 values = cgi.allGetValuesInOrder; 8948 } else { 8949 names = cgi.allPostNamesInOrder; 8950 values = cgi.allPostValuesInOrder; 8951 } 8952 8953 foreach(idx, name; names) { 8954 setArgument(name, values[idx]); 8955 } 8956 8957 static if(is(ReturnType!method == void)) { 8958 typeof(null) ret; 8959 dg(params); 8960 } else { 8961 auto ret = dg(params); 8962 } 8963 8964 // FIXME: format return values 8965 // options are: json, html, csv. 8966 // also may need to wrap in envelope format: none, html, or json. 8967 return ret; 8968 } 8969 8970 private bool mustNotBeSetFromWebParams(T, attrs...)() { 8971 static if(is(T : const(Cgi))) { 8972 return true; 8973 } else static if(is(T : const(Session!D), D)) { 8974 return true; 8975 } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) { 8976 return true; 8977 } else { 8978 foreach(uda; attrs) 8979 static if(is(uda == ifCalledFromWeb!func, alias func)) 8980 return true; 8981 return false; 8982 } 8983 } 8984 8985 private bool hasIfCalledFromWeb(attrs...)() { 8986 foreach(uda; attrs) 8987 static if(is(uda == ifCalledFromWeb!func, alias func)) 8988 return true; 8989 return false; 8990 } 8991 8992 /++ 8993 Implies POST path for the thing itself, then GET will get the automatic form. 8994 8995 The given customizer, if present, will be called as a filter on the Form object. 8996 8997 History: 8998 Added December 27, 2020 8999 +/ 9000 template AutomaticForm(alias customizer) { } 9001 9002 /++ 9003 This is meant to be returned by a function that takes a form POST submission. You 9004 want to set the url of the new resource it created, which is set as the http 9005 Location header for a "201 Created" result, and you can also set a separate 9006 destination for browser users, which it sets via a "Refresh" header. 9007 9008 The `resourceRepresentation` should generally be the thing you just created, and 9009 it will be the body of the http response when formatted through the presenter. 9010 The exact thing is up to you - it could just return an id, or the whole object, or 9011 perhaps a partial object. 9012 9013 Examples: 9014 --- 9015 class Test : WebObject { 9016 @(Cgi.RequestMethod.POST) 9017 CreatedResource!int makeThing(string value) { 9018 return CreatedResource!int(value.to!int, "/resources/id"); 9019 } 9020 } 9021 --- 9022 9023 History: 9024 Added December 18, 2021 9025 +/ 9026 struct CreatedResource(T) { 9027 static if(!is(T == void)) 9028 T resourceRepresentation; 9029 string resourceUrl; 9030 string refreshUrl; 9031 } 9032 9033 /+ 9034 /++ 9035 This can be attached as a UDA to a handler to add a http Refresh header on a 9036 successful run. (It will not be attached if the function throws an exception.) 9037 This will refresh the browser the given number of seconds after the page loads, 9038 to the url returned by `urlFunc`, which can be either a static function or a 9039 member method of the current handler object. 9040 9041 You might use this for a POST handler that is normally used from ajax, but you 9042 want it to degrade gracefully to a temporarily flashed message before reloading 9043 the main page. 9044 9045 History: 9046 Added December 18, 2021 9047 +/ 9048 struct Refresh(alias urlFunc) { 9049 int waitInSeconds; 9050 9051 string url() { 9052 static if(__traits(isStaticFunction, urlFunc)) 9053 return urlFunc(); 9054 else static if(is(urlFunc : string)) 9055 return urlFunc; 9056 } 9057 } 9058 +/ 9059 9060 /+ 9061 /++ 9062 Sets a filter to be run before 9063 9064 A before function can do validations of params and log and stop the function from running. 9065 +/ 9066 template Before(alias b) {} 9067 template After(alias b) {} 9068 +/ 9069 9070 /+ 9071 Argument conversions: for the most part, it is to!Thing(string). 9072 9073 But arrays and structs are a bit different. Arrays come from the cgi array. Thus 9074 they are passed 9075 9076 arr=foo&arr=bar <-- notice the same name. 9077 9078 Structs are first declared with an empty thing, then have their members set individually, 9079 with dot notation. The members are not required, just the initial declaration. 9080 9081 struct Foo { 9082 int a; 9083 string b; 9084 } 9085 void test(Foo foo){} 9086 9087 foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members 9088 9089 Arrays of structs use this declaration. 9090 9091 void test(Foo[] foo) {} 9092 9093 foo&foo.a=5&foo.b=bar&foo&foo.a=9 9094 9095 You can use a hidden input field in HTML forms to achieve this. The value of the naked name 9096 declaration is ignored. 9097 9098 Mind that order matters! The declaration MUST come first in the string. 9099 9100 Arrays of struct members follow this rule recursively. 9101 9102 struct Foo { 9103 int[] a; 9104 } 9105 9106 foo&foo.a=1&foo.a=2&foo&foo.a=1 9107 9108 9109 Associative arrays are formatted with brackets, after a declaration, like structs: 9110 9111 foo&foo[key]=value&foo[other_key]=value 9112 9113 9114 Note: for maximum compatibility with outside code, keep your types simple. Some libraries 9115 do not support the strict ordering requirements to work with these struct protocols. 9116 9117 FIXME: also perhaps accept application/json to better work with outside trash. 9118 9119 9120 Return values are also auto-formatted according to user-requested type: 9121 for json, it loops over and converts. 9122 for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables! 9123 +/ 9124 9125 /++ 9126 A web presenter is responsible for rendering things to HTML to be usable 9127 in a web browser. 9128 9129 They are passed as template arguments to the base classes of [WebObject] 9130 9131 Responsible for displaying stuff as HTML. You can put this into your own aggregate 9132 and override it. Use forwarding and specialization to customize it. 9133 9134 When you inherit from it, pass your own class as the CRTP argument. This lets the base 9135 class templates and your overridden templates work with each other. 9136 9137 --- 9138 class MyPresenter : WebPresenter!(MyPresenter) { 9139 @Override 9140 void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) { 9141 // present the CustomType 9142 } 9143 @Override 9144 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 9145 // handle everything else via the super class, which will call 9146 // back to your class when appropriate 9147 super.presentSuccessfulReturnAsHtml(cgi, ret); 9148 } 9149 } 9150 --- 9151 9152 The meta argument in there can be overridden by your own facility. 9153 9154 +/ 9155 class WebPresenter(CRTP) { 9156 9157 /// A UDA version of the built-in `override`, to be used for static template polymorphism 9158 /// If you override a plain method, use `override`. If a template, use `@Override`. 9159 enum Override; 9160 9161 string script() { 9162 return ` 9163 `; 9164 } 9165 9166 string style() { 9167 return ` 9168 :root { 9169 --mild-border: #ccc; 9170 --middle-border: #999; 9171 --accent-color: #f2f2f2; 9172 --sidebar-color: #fefefe; 9173 } 9174 ` ~ genericFormStyling() ~ genericSiteStyling(); 9175 } 9176 9177 string genericFormStyling() { 9178 return 9179 q"css 9180 table.automatic-data-display { 9181 border-collapse: collapse; 9182 border: solid 1px var(--mild-border); 9183 } 9184 9185 table.automatic-data-display td { 9186 vertical-align: top; 9187 border: solid 1px var(--mild-border); 9188 padding: 2px 4px; 9189 } 9190 9191 table.automatic-data-display th { 9192 border: solid 1px var(--mild-border); 9193 border-bottom: solid 1px var(--middle-border); 9194 padding: 2px 4px; 9195 } 9196 9197 ol.automatic-data-display { 9198 margin: 0px; 9199 list-style-position: inside; 9200 padding: 0px; 9201 } 9202 9203 dl.automatic-data-display { 9204 9205 } 9206 9207 .automatic-form { 9208 max-width: 600px; 9209 } 9210 9211 .form-field { 9212 margin: 0.5em; 9213 padding-left: 0.5em; 9214 } 9215 9216 .label-text { 9217 display: block; 9218 font-weight: bold; 9219 margin-left: -0.5em; 9220 } 9221 9222 .submit-button-holder { 9223 padding-left: 2em; 9224 } 9225 9226 .add-array-button { 9227 9228 } 9229 css"; 9230 } 9231 9232 string genericSiteStyling() { 9233 return 9234 q"css 9235 * { box-sizing: border-box; } 9236 html, body { margin: 0px; } 9237 body { 9238 font-family: sans-serif; 9239 } 9240 header { 9241 background: var(--accent-color); 9242 height: 64px; 9243 } 9244 footer { 9245 background: var(--accent-color); 9246 height: 64px; 9247 } 9248 #site-container { 9249 display: flex; 9250 } 9251 main { 9252 flex: 1 1 auto; 9253 order: 2; 9254 min-height: calc(100vh - 64px - 64px); 9255 padding: 4px; 9256 padding-left: 1em; 9257 } 9258 #sidebar { 9259 flex: 0 0 16em; 9260 order: 1; 9261 background: var(--sidebar-color); 9262 } 9263 css"; 9264 } 9265 9266 import arsd.dom; 9267 Element htmlContainer() { 9268 auto document = new Document(q"html 9269 <!DOCTYPE html> 9270 <html> 9271 <head> 9272 <title>D Application</title> 9273 <link rel="stylesheet" href="style.css" /> 9274 </head> 9275 <body> 9276 <header></header> 9277 <div id="site-container"> 9278 <main></main> 9279 <div id="sidebar"></div> 9280 </div> 9281 <footer></footer> 9282 <script src="script.js"></script> 9283 </body> 9284 </html> 9285 html", true, true); 9286 9287 return document.requireSelector("main"); 9288 } 9289 9290 /// Renders a response as an HTTP error 9291 void renderBasicError(Cgi cgi, int httpErrorCode) { 9292 cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); 9293 auto c = htmlContainer(); 9294 c.innerText = getHttpCodeText(httpErrorCode); 9295 cgi.setResponseContentType("text/html; charset=utf-8"); 9296 cgi.write(c.parentDocument.toString(), true); 9297 } 9298 9299 template methodMeta(alias method) { 9300 enum methodMeta = null; 9301 } 9302 9303 void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) { 9304 // FIXME? format? 9305 (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta); 9306 } 9307 9308 /// typeof(null) (which is also used to represent functions returning `void`) do nothing 9309 /// in the default presenter - allowing the function to have full low-level control over the 9310 /// response. 9311 void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { 9312 // nothing intentionally! 9313 } 9314 9315 /// Redirections are forwarded to [Cgi.setResponseLocation] 9316 void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { 9317 cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); 9318 } 9319 9320 /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. 9321 void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { 9322 cgi.setResponseStatus(getHttpCodeText(201)); 9323 if(ret.resourceUrl.length) 9324 cgi.header("Location: " ~ ret.resourceUrl); 9325 if(ret.refreshUrl.length) 9326 cgi.header("Refresh: 0;" ~ ret.refreshUrl); 9327 static if(!is(R == void)) 9328 presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); 9329 } 9330 9331 /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime 9332 void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { 9333 bool outputted = false; 9334 foreach(index, type; Types) { 9335 if(ret.contains == index) { 9336 assert(!outputted); 9337 outputted = true; 9338 (cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format); 9339 } 9340 } 9341 if(!outputted) 9342 assert(0); 9343 } 9344 9345 /// An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort. 9346 void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { 9347 cgi.setCache(true); // not necessarily true but meh 9348 cgi.setResponseContentType(ret.contentType); 9349 cgi.write(ret.getData(), true); 9350 } 9351 9352 /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer]. 9353 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 9354 auto container = this.htmlContainer(); 9355 container.appendChild(formatReturnValueAsHtml(ret)); 9356 cgi.write(container.parentDocument.toString(), true); 9357 } 9358 9359 /++ 9360 If you override this, you will need to cast the exception type `t` dynamically, 9361 but can then use the template arguments here to refer back to the function. 9362 9363 `func` is an alias to the method itself, and `dg` is a callable delegate to the same 9364 method on the live object. You could, in theory, change arguments and retry, but I 9365 provide that information mostly with the expectation that you will use them to make 9366 useful forms or richer error messages for the user. 9367 +/ 9368 void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) { 9369 Form af; 9370 foreach(attr; __traits(getAttributes, func)) { 9371 static if(__traits(isSame, attr, AutomaticForm)) { 9372 af = createAutomaticFormForFunction!(func)(dg); 9373 } 9374 } 9375 presentExceptionAsHtmlImpl(cgi, t, af); 9376 } 9377 9378 void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { 9379 if(auto e = cast(ResourceNotFoundException) t) { 9380 auto container = this.htmlContainer(); 9381 9382 container.addChild("p", e.msg); 9383 9384 if(!cgi.outputtedResponseData) 9385 cgi.setResponseStatus("404 Not Found"); 9386 cgi.write(container.parentDocument.toString(), true); 9387 } else if(auto mae = cast(MissingArgumentException) t) { 9388 if(automaticForm is null) 9389 goto generic; 9390 auto container = this.htmlContainer(); 9391 if(cgi.requestMethod == Cgi.RequestMethod.POST) 9392 container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); 9393 container.appendChild(automaticForm); 9394 9395 cgi.write(container.parentDocument.toString(), true); 9396 } else { 9397 generic: 9398 auto container = this.htmlContainer(); 9399 9400 // import std.stdio; writeln(t.toString()); 9401 9402 container.appendChild(exceptionToElement(t)); 9403 9404 container.addChild("h4", "GET"); 9405 foreach(k, v; cgi.get) { 9406 auto deets = container.addChild("details"); 9407 deets.addChild("summary", k); 9408 deets.addChild("div", v); 9409 } 9410 9411 container.addChild("h4", "POST"); 9412 foreach(k, v; cgi.post) { 9413 auto deets = container.addChild("details"); 9414 deets.addChild("summary", k); 9415 deets.addChild("div", v); 9416 } 9417 9418 9419 if(!cgi.outputtedResponseData) 9420 cgi.setResponseStatus("500 Internal Server Error"); 9421 cgi.write(container.parentDocument.toString(), true); 9422 } 9423 } 9424 9425 Element exceptionToElement(Throwable t) { 9426 auto div = Element.make("div"); 9427 div.addClass("exception-display"); 9428 9429 div.addChild("p", t.msg); 9430 div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line)); 9431 9432 auto pre = div.addChild("pre"); 9433 string s; 9434 s = t.toString(); 9435 Element currentBox; 9436 bool on = false; 9437 foreach(line; s.splitLines) { 9438 if(!on && line.startsWith("-----")) 9439 on = true; 9440 if(!on) continue; 9441 if(line.indexOf("arsd/") != -1) { 9442 if(currentBox is null) { 9443 currentBox = pre.addChild("details"); 9444 currentBox.addChild("summary", "Framework code"); 9445 } 9446 currentBox.addChild("span", line ~ "\n"); 9447 } else { 9448 pre.addChild("span", line ~ "\n"); 9449 currentBox = null; 9450 } 9451 } 9452 9453 return div; 9454 } 9455 9456 /++ 9457 Returns an element for a particular type 9458 +/ 9459 Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { 9460 import std.traits; 9461 9462 auto div = Element.make("div"); 9463 div.addClass("form-field"); 9464 9465 static if(is(T == Cgi.UploadedFile)) { 9466 Element lbl; 9467 if(displayName !is null) { 9468 lbl = div.addChild("label"); 9469 lbl.addChild("span", displayName, "label-text"); 9470 lbl.appendText(" "); 9471 } else { 9472 lbl = div; 9473 } 9474 auto i = lbl.addChild("input", name); 9475 i.attrs.name = name; 9476 i.attrs.type = "file"; 9477 } else static if(is(T == enum)) { 9478 Element lbl; 9479 if(displayName !is null) { 9480 lbl = div.addChild("label"); 9481 lbl.addChild("span", displayName, "label-text"); 9482 lbl.appendText(" "); 9483 } else { 9484 lbl = div; 9485 } 9486 auto i = lbl.addChild("select", name); 9487 i.attrs.name = name; 9488 9489 foreach(memberName; __traits(allMembers, T)) 9490 i.addChild("option", memberName); 9491 9492 } else static if(is(T == struct)) { 9493 if(displayName !is null) 9494 div.addChild("span", displayName, "label-text"); 9495 auto fieldset = div.addChild("fieldset"); 9496 fieldset.addChild("legend", beautify(T.stringof)); // FIXME 9497 fieldset.addChild("input", name); 9498 foreach(idx, memberName; __traits(allMembers, T)) 9499 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 9500 fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); 9501 } 9502 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 9503 Element lbl; 9504 if(displayName !is null) { 9505 lbl = div.addChild("label"); 9506 lbl.addChild("span", displayName, "label-text"); 9507 lbl.appendText(" "); 9508 } else { 9509 lbl = div; 9510 } 9511 Element i; 9512 if(udaSuggestion) { 9513 i = udaSuggestion(); 9514 lbl.appendChild(i); 9515 } else { 9516 i = lbl.addChild("input", name); 9517 } 9518 i.attrs.name = name; 9519 static if(isSomeString!T) 9520 i.attrs.type = "text"; 9521 else 9522 i.attrs.type = "number"; 9523 if(i.tagName == "textarea") 9524 i.textContent = to!string(T.init); 9525 else 9526 i.attrs.value = to!string(T.init); 9527 } else static if(is(T == bool)) { 9528 Element lbl; 9529 if(displayName !is null) { 9530 lbl = div.addChild("label"); 9531 lbl.addChild("span", displayName, "label-text"); 9532 lbl.appendText(" "); 9533 } else { 9534 lbl = div; 9535 } 9536 auto i = lbl.addChild("input", name); 9537 i.attrs.type = "checkbox"; 9538 i.attrs.value = "true"; 9539 i.attrs.name = name; 9540 } else static if(is(T == K[], K)) { 9541 auto templ = div.addChild("template"); 9542 templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); 9543 if(displayName !is null) 9544 div.addChild("span", displayName, "label-text"); 9545 auto btn = div.addChild("button"); 9546 btn.addClass("add-array-button"); 9547 btn.attrs.type = "button"; 9548 btn.innerText = "Add"; 9549 btn.attrs.onclick = q{ 9550 var a = document.importNode(this.parentNode.firstChild.content, true); 9551 this.parentNode.insertBefore(a, this); 9552 }; 9553 } else static if(is(T == V[K], K, V)) { 9554 div.innerText = "assoc array not implemented for automatic form at this time"; 9555 } else { 9556 static assert(0, "unsupported type for cgi call " ~ T.stringof); 9557 } 9558 9559 9560 return div; 9561 } 9562 9563 /// creates a form for gathering the function's arguments 9564 Form createAutomaticFormForFunction(alias method, T)(T dg) { 9565 9566 auto form = cast(Form) Element.make("form"); 9567 9568 form.method = "POST"; // FIXME 9569 9570 form.addClass("automatic-form"); 9571 9572 string formDisplayName = beautify(__traits(identifier, method)); 9573 foreach(attr; __traits(getAttributes, method)) 9574 static if(is(typeof(attr) == DisplayName)) 9575 formDisplayName = attr.name; 9576 form.addChild("h3", formDisplayName); 9577 9578 import std.traits; 9579 9580 //Parameters!method params; 9581 //alias idents = ParameterIdentifierTuple!method; 9582 //alias defaults = ParameterDefaults!method; 9583 9584 static if(is(typeof(method) P == __parameters)) 9585 foreach(idx, _; P) {{ 9586 9587 alias param = P[idx .. idx + 1]; 9588 9589 static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { 9590 string displayName = beautify(__traits(identifier, param)); 9591 Element function() element; 9592 foreach(attr; __traits(getAttributes, param)) { 9593 static if(is(typeof(attr) == DisplayName)) 9594 displayName = attr.name; 9595 else static if(is(typeof(attr) : typeof(element))) { 9596 element = attr; 9597 } 9598 } 9599 auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); 9600 if(i.querySelector("input[type=file]") !is null) 9601 form.setAttribute("enctype", "multipart/form-data"); 9602 } 9603 }} 9604 9605 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 9606 9607 return form; 9608 } 9609 9610 /// creates a form for gathering object members (for the REST object thing right now) 9611 Form createAutomaticFormForObject(T)(T obj) { 9612 auto form = cast(Form) Element.make("form"); 9613 9614 form.addClass("automatic-form"); 9615 9616 form.addChild("h3", beautify(__traits(identifier, T))); 9617 9618 import std.traits; 9619 9620 //Parameters!method params; 9621 //alias idents = ParameterIdentifierTuple!method; 9622 //alias defaults = ParameterDefaults!method; 9623 9624 foreach(idx, memberName; __traits(derivedMembers, T)) {{ 9625 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 9626 string displayName = beautify(memberName); 9627 Element function() element; 9628 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) 9629 static if(is(typeof(attr) == DisplayName)) 9630 displayName = attr.name; 9631 else static if(is(typeof(attr) : typeof(element))) 9632 element = attr; 9633 form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); 9634 9635 form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); 9636 }}} 9637 9638 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 9639 9640 return form; 9641 } 9642 9643 /// 9644 Element formatReturnValueAsHtml(T)(T t) { 9645 import std.traits; 9646 9647 static if(is(T == typeof(null))) { 9648 return Element.make("span"); 9649 } else static if(is(T : Element)) { 9650 return t; 9651 } else static if(is(T == MultipleResponses!Types, Types...)) { 9652 foreach(index, type; Types) { 9653 if(t.contains == index) 9654 return formatReturnValueAsHtml(t.payload[index]); 9655 } 9656 assert(0); 9657 } else static if(is(T == Paginated!E, E)) { 9658 auto e = Element.make("div").addClass("paginated-result"); 9659 e.appendChild(formatReturnValueAsHtml(t.items)); 9660 if(t.nextPageUrl.length) 9661 e.appendChild(Element.make("a", "Next Page", t.nextPageUrl)); 9662 return e; 9663 } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) { 9664 return Element.make("span", to!string(t), "automatic-data-display"); 9665 } else static if(is(T == V[K], K, V)) { 9666 auto dl = Element.make("dl"); 9667 dl.addClass("automatic-data-display associative-array"); 9668 foreach(k, v; t) { 9669 dl.addChild("dt", to!string(k)); 9670 dl.addChild("dd", formatReturnValueAsHtml(v)); 9671 } 9672 return dl; 9673 } else static if(is(T == struct)) { 9674 auto dl = Element.make("dl"); 9675 dl.addClass("automatic-data-display struct"); 9676 9677 foreach(idx, memberName; __traits(allMembers, T)) 9678 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 9679 dl.addChild("dt", beautify(memberName)); 9680 dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName))); 9681 } 9682 9683 return dl; 9684 } else static if(is(T == bool)) { 9685 return Element.make("span", t ? "true" : "false", "automatic-data-display"); 9686 } else static if(is(T == E[], E)) { 9687 static if(is(E : RestObject!Proxy, Proxy)) { 9688 // treat RestObject similar to struct 9689 auto table = cast(Table) Element.make("table"); 9690 table.addClass("automatic-data-display"); 9691 string[] names; 9692 foreach(idx, memberName; __traits(derivedMembers, E)) 9693 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 9694 names ~= beautify(memberName); 9695 } 9696 table.appendHeaderRow(names); 9697 9698 foreach(l; t) { 9699 auto tr = table.appendRow(); 9700 foreach(idx, memberName; __traits(derivedMembers, E)) 9701 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 9702 static if(memberName == "id") { 9703 string val = to!string(__traits(getMember, l, memberName)); 9704 tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME 9705 } else { 9706 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 9707 } 9708 } 9709 } 9710 9711 return table; 9712 } else static if(is(E == struct)) { 9713 // an array of structs is kinda special in that I like 9714 // having those formatted as tables. 9715 auto table = cast(Table) Element.make("table"); 9716 table.addClass("automatic-data-display"); 9717 string[] names; 9718 foreach(idx, memberName; __traits(allMembers, E)) 9719 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 9720 names ~= beautify(memberName); 9721 } 9722 table.appendHeaderRow(names); 9723 9724 foreach(l; t) { 9725 auto tr = table.appendRow(); 9726 foreach(idx, memberName; __traits(allMembers, E)) 9727 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 9728 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 9729 } 9730 } 9731 9732 return table; 9733 } else { 9734 // otherwise, I will just make a list. 9735 auto ol = Element.make("ol"); 9736 ol.addClass("automatic-data-display"); 9737 foreach(e; t) 9738 ol.addChild("li", formatReturnValueAsHtml(e)); 9739 return ol; 9740 } 9741 } else static if(is(T : Object)) { 9742 static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface 9743 return Element.make("div", t.toHtml()); 9744 else 9745 return Element.make("div", t.toString()); 9746 } else static assert(0, "bad return value for cgi call " ~ T.stringof); 9747 9748 assert(0); 9749 } 9750 9751 } 9752 9753 /++ 9754 The base class for the [dispatcher] function and object support. 9755 +/ 9756 class WebObject { 9757 //protected Cgi cgi; 9758 9759 protected void initialize(Cgi cgi) { 9760 //this.cgi = cgi; 9761 } 9762 } 9763 9764 /++ 9765 Can return one of the given types, decided at runtime. The syntax 9766 is to declare all the possible types in the return value, then you 9767 can `return typeof(return)(...value...)` to construct it. 9768 9769 It has an auto-generated constructor for each value it can hold. 9770 9771 --- 9772 MultipleResponses!(Redirection, string) getData(int how) { 9773 if(how & 1) 9774 return typeof(return)(Redirection("http://dpldocs.info/")); 9775 else 9776 return typeof(return)("hi there!"); 9777 } 9778 --- 9779 9780 If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little. 9781 +/ 9782 struct MultipleResponses(T...) { 9783 private size_t contains; 9784 private union { 9785 private T payload; 9786 } 9787 9788 static foreach(index, type; T) 9789 public this(type t) { 9790 contains = index; 9791 payload[index] = t; 9792 } 9793 9794 /++ 9795 This is primarily for testing. It is your way of getting to the response. 9796 9797 Let's say you wanted to test that one holding a Redirection and a string actually 9798 holds a string, by name of "test": 9799 9800 --- 9801 auto valueToTest = your_test_function(); 9802 9803 valueToTest.visit( 9804 (Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test 9805 (string s) { assert(s == "test"); } // right value, go ahead and test it. 9806 ); 9807 --- 9808 9809 History: 9810 Was horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it. 9811 It tried to use alias lambdas before, but runtime delegates work much better so I changed it. 9812 +/ 9813 void visit(Handlers...)(Handlers handlers) { 9814 template findHandler(type, int count, HandlersToCheck...) { 9815 static if(HandlersToCheck.length == 0) 9816 enum findHandler = -1; 9817 else { 9818 static if(is(typeof(HandlersToCheck[0].init(type.init)))) 9819 enum findHandler = count; 9820 else 9821 enum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]); 9822 } 9823 } 9824 foreach(index, type; T) { 9825 enum handlerIndex = findHandler!(type, 0, Handlers); 9826 static if(handlerIndex == -1) 9827 static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor"); 9828 else { 9829 if(index == this.contains) 9830 handlers[handlerIndex](this.payload[index]); 9831 } 9832 } 9833 } 9834 9835 /+ 9836 auto toArsdJsvar()() { 9837 import arsd.jsvar; 9838 return var(null); 9839 } 9840 +/ 9841 } 9842 9843 // FIXME: implement this somewhere maybe 9844 struct RawResponse { 9845 int code; 9846 string[] headers; 9847 const(ubyte)[] responseBody; 9848 } 9849 9850 /++ 9851 You can return this from [WebObject] subclasses for redirections. 9852 9853 (though note the static types means that class must ALWAYS redirect if 9854 you return this directly. You might want to return [MultipleResponses] if it 9855 can be conditional) 9856 +/ 9857 struct Redirection { 9858 string to; /// The URL to redirect to. 9859 int code = 303; /// The HTTP code to return. 9860 } 9861 9862 /++ 9863 Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. 9864 9865 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overriden 9866 the presenter in the dispatcher. 9867 9868 FIXME: explain this better 9869 9870 You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function, 9871 and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads, 9872 the runtime result of that is undefined. 9873 9874 A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those. 9875 (this might change, like maybe i will use pure as an indicator GET is ok. idk.) 9876 9877 $(WARNING 9878 --- 9879 // legal in D, undefined runtime behavior with cgi.d, it may call either method 9880 // even if you put different URL udas on it, the current code ignores them. 9881 void foo(int a) {} 9882 void foo(string a) {} 9883 --- 9884 ) 9885 9886 See_Also: [serveRestObject], [serveStaticFile] 9887 +/ 9888 auto serveApi(T)(string urlPrefix) { 9889 assert(urlPrefix[$ - 1] == '/'); 9890 return serveApiInternal!T(urlPrefix); 9891 } 9892 9893 private string nextPieceFromSlash(ref string remainingUrl) { 9894 if(remainingUrl.length == 0) 9895 return remainingUrl; 9896 int slash = 0; 9897 while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.') 9898 slash++; 9899 9900 // I am specifically passing `null` to differentiate it vs empty string 9901 // so in your ctor, `items` means new T(null) and `items/` means new T("") 9902 auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash]; 9903 // so if it is the last item, the dot can be used to load an alternative view 9904 // otherwise tho the dot is considered part of the identifier 9905 // FIXME 9906 9907 // again notice "" vs null here! 9908 if(slash == remainingUrl.length) 9909 remainingUrl = null; 9910 else 9911 remainingUrl = remainingUrl[slash + 1 .. $]; 9912 9913 return ident; 9914 } 9915 9916 /++ 9917 UDA used to indicate to the [dispatcher] that a trailing slash should always be added to or removed from the url. It will do it as a redirect header as-needed. 9918 +/ 9919 enum AddTrailingSlash; 9920 /// ditto 9921 enum RemoveTrailingSlash; 9922 9923 private auto serveApiInternal(T)(string urlPrefix) { 9924 9925 import arsd.dom; 9926 import arsd.jsvar; 9927 9928 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 9929 string remainingUrl = cgi.pathInfo[urlPrefix.length .. $]; 9930 9931 try { 9932 // see duplicated code below by searching subresource_ctor 9933 // also see mustNotBeSetFromWebParams 9934 9935 static if(is(typeof(T.__ctor) P == __parameters)) { 9936 P params; 9937 9938 foreach(pidx, param; P) { 9939 static if(is(param : Cgi)) { 9940 static assert(!is(param == immutable)); 9941 cast() params[pidx] = cgi; 9942 } else static if(is(param == Session!D, D)) { 9943 static assert(!is(param == immutable)); 9944 cast() params[pidx] = cgi.getSessionObject!D(); 9945 9946 } else { 9947 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 9948 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 9949 static if(is(uda == ifCalledFromWeb!func, alias func)) { 9950 static if(is(typeof(func(cgi)))) 9951 params[pidx] = func(cgi); 9952 else 9953 params[pidx] = func(); 9954 } 9955 } 9956 } else { 9957 9958 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 9959 params[pidx] = param.getAutomaticallyForCgi(cgi); 9960 } else static if(is(param == string)) { 9961 auto ident = nextPieceFromSlash(remainingUrl); 9962 params[pidx] = ident; 9963 } else static assert(0, "illegal type for subresource " ~ param.stringof); 9964 } 9965 } 9966 } 9967 9968 auto obj = new T(params); 9969 } else { 9970 auto obj = new T(); 9971 } 9972 9973 return internalHandlerWithObject(obj, remainingUrl, cgi, presenter); 9974 } catch(Throwable t) { 9975 switch(cgi.request("format", "html")) { 9976 case "html": 9977 static void dummy() {} 9978 presenter.presentExceptionAsHtml!(dummy)(cgi, t, &dummy); 9979 return true; 9980 case "json": 9981 var envelope = var.emptyObject; 9982 envelope.success = false; 9983 envelope.result = null; 9984 envelope.error = t.toString(); 9985 cgi.setResponseContentType("application/json"); 9986 cgi.write(envelope.toJson(), true); 9987 return true; 9988 default: 9989 throw t; 9990 // return true; 9991 } 9992 // return true; 9993 } 9994 9995 assert(0); 9996 } 9997 9998 static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) { 9999 10000 obj.initialize(cgi); 10001 10002 /+ 10003 Overload rules: 10004 Any unique combination of HTTP verb and url path can be dispatched to function overloads 10005 statically. 10006 10007 Moreover, some args vs no args can be overloaded dynamically. 10008 +/ 10009 10010 auto methodNameFromUrl = nextPieceFromSlash(remainingUrl); 10011 /+ 10012 auto orig = remainingUrl; 10013 assert(0, 10014 (orig is null ? "__null" : orig) 10015 ~ " .. " ~ 10016 (methodNameFromUrl is null ? "__null" : methodNameFromUrl)); 10017 +/ 10018 10019 if(methodNameFromUrl is null) 10020 methodNameFromUrl = "__null"; 10021 10022 string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl; 10023 10024 if(remainingUrl.length) 10025 hack ~= "/"; 10026 10027 switch(hack) { 10028 foreach(methodName; __traits(derivedMembers, T)) 10029 static if(methodName != "__ctor") 10030 foreach(idx, overload; __traits(getOverloads, T, methodName)) { 10031 static if(is(typeof(overload) P == __parameters)) 10032 static if(is(typeof(overload) R == return)) 10033 static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") 10034 { 10035 static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) 10036 case urlNameForMethod: 10037 10038 static if(is(R : WebObject)) { 10039 // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above. 10040 10041 // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string 10042 10043 // subresource_ctor 10044 // also see mustNotBeSetFromWebParams 10045 10046 P params; 10047 10048 string ident; 10049 10050 foreach(pidx, param; P) { 10051 static if(is(param : Cgi)) { 10052 static assert(!is(param == immutable)); 10053 cast() params[pidx] = cgi; 10054 } else static if(is(param == typeof(presenter))) { 10055 cast() param[pidx] = presenter; 10056 } else static if(is(param == Session!D, D)) { 10057 static assert(!is(param == immutable)); 10058 cast() params[pidx] = cgi.getSessionObject!D(); 10059 } else { 10060 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 10061 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 10062 static if(is(uda == ifCalledFromWeb!func, alias func)) { 10063 static if(is(typeof(func(cgi)))) 10064 params[pidx] = func(cgi); 10065 else 10066 params[pidx] = func(); 10067 } 10068 } 10069 } else { 10070 10071 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 10072 params[pidx] = param.getAutomaticallyForCgi(cgi); 10073 } else static if(is(param == string)) { 10074 ident = nextPieceFromSlash(remainingUrl); 10075 if(ident is null) { 10076 // trailing slash mandated on subresources 10077 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 10078 return true; 10079 } else { 10080 params[pidx] = ident; 10081 } 10082 } else static assert(0, "illegal type for subresource " ~ param.stringof); 10083 } 10084 } 10085 } 10086 10087 auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident); 10088 return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter); 10089 } else { 10090 // 404 it if any url left - not a subresource means we don't get to play with that! 10091 if(remainingUrl.length) 10092 return false; 10093 10094 bool automaticForm; 10095 10096 foreach(attr; __traits(getAttributes, overload)) 10097 static if(is(attr == AddTrailingSlash)) { 10098 if(remainingUrl is null) { 10099 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 10100 return true; 10101 } 10102 } else static if(is(attr == RemoveTrailingSlash)) { 10103 if(remainingUrl !is null) { 10104 cgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, "/")]); 10105 return true; 10106 } 10107 10108 } else static if(__traits(isSame, AutomaticForm, attr)) { 10109 automaticForm = true; 10110 } 10111 10112 /+ 10113 int zeroArgOverload = -1; 10114 int overloadCount = cast(int) __traits(getOverloads, T, methodName).length; 10115 bool calledWithZeroArgs = true; 10116 foreach(k, v; cgi.get) 10117 if(k != "format") { 10118 calledWithZeroArgs = false; 10119 break; 10120 } 10121 foreach(k, v; cgi.post) 10122 if(k != "format") { 10123 calledWithZeroArgs = false; 10124 break; 10125 } 10126 10127 // first, we need to go through and see if there is an empty one, since that 10128 // changes inside. But otherwise, all the stuff I care about can be done via 10129 // simple looping (other improper overloads might be flagged for runtime semantic check) 10130 // 10131 // an argument of type Cgi is ignored for these purposes 10132 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 10133 static if(is(typeof(overload) P == __parameters)) 10134 static if(P.length == 0) 10135 zeroArgOverload = cast(int) idx; 10136 else static if(P.length == 1 && is(P[0] : Cgi)) 10137 zeroArgOverload = cast(int) idx; 10138 }} 10139 // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method. 10140 bool overloadHasBeenCalled = false; 10141 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 10142 bool callFunction = true; 10143 // there is a zero arg overload and this is NOT it, and we have zero args - don't call this 10144 if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs) 10145 callFunction = false; 10146 // if this is the zero-arg overload, obviously it cannot be called if we got any args. 10147 if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs) 10148 callFunction = false; 10149 10150 // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea. 10151 10152 bool hadAnyMethodRestrictions = false; 10153 bool foundAcceptableMethod = false; 10154 foreach(attr; __traits(getAttributes, overload)) { 10155 static if(is(typeof(attr) == Cgi.RequestMethod)) { 10156 hadAnyMethodRestrictions = true; 10157 if(attr == cgi.requestMethod) 10158 foundAcceptableMethod = true; 10159 } 10160 } 10161 10162 if(hadAnyMethodRestrictions && !foundAcceptableMethod) 10163 callFunction = false; 10164 10165 /+ 10166 The overloads we really want to allow are the sane ones 10167 from the web perspective. Which is likely on HTTP verbs, 10168 for the most part, but might also be potentially based on 10169 some args vs zero args, or on argument names. Can't really 10170 do argument types very reliable through the web though; those 10171 should probably be different URLs. 10172 10173 Even names I feel is better done inside the function, so I'm not 10174 going to support that here. But the HTTP verbs and zero vs some 10175 args makes sense - it lets you define custom forms pretty easily. 10176 10177 Moreover, I'm of the opinion that empty overload really only makes 10178 sense on GET for this case. On a POST, it is just a missing argument 10179 exception and that should be handled by the presenter. But meh, I'll 10180 let the user define that, D only allows one empty arg thing anyway 10181 so the method UDAs are irrelevant. 10182 +/ 10183 if(callFunction) 10184 +/ 10185 10186 if(automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET) { 10187 // Should I still show the form on a json thing? idk... 10188 auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx])); 10189 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), "html"); 10190 return true; 10191 } 10192 switch(cgi.request("format", defaultFormat!overload())) { 10193 case "html": 10194 // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control. 10195 try { 10196 10197 auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); 10198 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), "html"); 10199 } catch(Throwable t) { 10200 presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx])); 10201 } 10202 return true; 10203 case "json": 10204 auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); 10205 static if(is(typeof(ret) == MultipleResponses!Types, Types...)) { 10206 var json; 10207 foreach(index, type; Types) { 10208 if(ret.contains == index) 10209 json = ret.payload[index]; 10210 } 10211 } else { 10212 var json = ret; 10213 } 10214 var envelope = json; // var.emptyObject; 10215 /* 10216 envelope.success = true; 10217 envelope.result = json; 10218 envelope.error = null; 10219 */ 10220 cgi.setResponseContentType("application/json"); 10221 cgi.write(envelope.toJson(), true); 10222 return true; 10223 default: 10224 cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of. 10225 return true; 10226 } 10227 //}} 10228 10229 //cgi.header("Accept: POST"); // FIXME list the real thing 10230 //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering. 10231 //return true; 10232 } 10233 } 10234 } 10235 case "GET script.js": 10236 cgi.setResponseContentType("text/javascript"); 10237 cgi.gzipResponse = true; 10238 cgi.write(presenter.script(), true); 10239 return true; 10240 case "GET style.css": 10241 cgi.setResponseContentType("text/css"); 10242 cgi.gzipResponse = true; 10243 cgi.write(presenter.style(), true); 10244 return true; 10245 default: 10246 return false; 10247 } 10248 10249 assert(0); 10250 } 10251 return DispatcherDefinition!internalHandler(urlPrefix, false); 10252 } 10253 10254 string defaultFormat(alias method)() { 10255 bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; 10256 foreach(attr; __traits(getAttributes, method)) { 10257 static if(is(typeof(attr) == DefaultFormat)) { 10258 if(nonConstConditionForWorkingAroundASpuriousDmdWarning) 10259 return attr.value; 10260 } 10261 } 10262 return "html"; 10263 } 10264 10265 struct Paginated(T) { 10266 T[] items; 10267 string nextPageUrl; 10268 } 10269 10270 template urlNamesForMethod(alias method, string default_) { 10271 string[] helper() { 10272 auto verb = Cgi.RequestMethod.GET; 10273 bool foundVerb = false; 10274 bool foundNoun = false; 10275 10276 string def = default_; 10277 10278 bool hasAutomaticForm = false; 10279 10280 foreach(attr; __traits(getAttributes, method)) { 10281 static if(is(typeof(attr) == Cgi.RequestMethod)) { 10282 verb = attr; 10283 if(foundVerb) 10284 assert(0, "Multiple http verbs on one function is not currently supported"); 10285 foundVerb = true; 10286 } 10287 static if(is(typeof(attr) == UrlName)) { 10288 if(foundNoun) 10289 assert(0, "Multiple url names on one function is not currently supported"); 10290 foundNoun = true; 10291 def = attr.name; 10292 } 10293 static if(__traits(isSame, attr, AutomaticForm)) { 10294 hasAutomaticForm = true; 10295 } 10296 } 10297 10298 if(def is null) 10299 def = "__null"; 10300 10301 string[] ret; 10302 10303 static if(is(typeof(method) R == return)) { 10304 static if(is(R : WebObject)) { 10305 def ~= "/"; 10306 foreach(v; __traits(allMembers, Cgi.RequestMethod)) 10307 ret ~= v ~ " " ~ def; 10308 } else { 10309 if(hasAutomaticForm) { 10310 ret ~= "GET " ~ def; 10311 ret ~= "POST " ~ def; 10312 } else { 10313 ret ~= to!string(verb) ~ " " ~ def; 10314 } 10315 } 10316 } else static assert(0); 10317 10318 return ret; 10319 } 10320 enum urlNamesForMethod = helper(); 10321 } 10322 10323 10324 enum AccessCheck { 10325 allowed, 10326 denied, 10327 nonExistant, 10328 } 10329 10330 enum Operation { 10331 show, 10332 create, 10333 replace, 10334 remove, 10335 update 10336 } 10337 10338 enum UpdateResult { 10339 accessDenied, 10340 noSuchResource, 10341 success, 10342 failure, 10343 unnecessary 10344 } 10345 10346 enum ValidationResult { 10347 valid, 10348 invalid 10349 } 10350 10351 10352 /++ 10353 The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. 10354 10355 WARNING: this is not stable. 10356 +/ 10357 class RestObject(CRTP) : WebObject { 10358 10359 import arsd.dom; 10360 import arsd.jsvar; 10361 10362 /// Prepare the object to be shown. 10363 void show() {} 10364 /// ditto 10365 void show(string urlId) { 10366 load(urlId); 10367 show(); 10368 } 10369 10370 /// Override this to provide access control to this object. 10371 AccessCheck accessCheck(string urlId, Operation operation) { 10372 return AccessCheck.allowed; 10373 } 10374 10375 ValidationResult validate() { 10376 // FIXME 10377 return ValidationResult.valid; 10378 } 10379 10380 string getUrlSlug() { 10381 import std.conv; 10382 static if(is(typeof(CRTP.id))) 10383 return to!string((cast(CRTP) this).id); 10384 else 10385 return null; 10386 } 10387 10388 // The functions with more arguments are the low-level ones, 10389 // they forward to the ones with fewer arguments by default. 10390 10391 // POST on a parent collection - this is called from a collection class after the members are updated 10392 /++ 10393 Given a populated object, this creates a new entry. Returns the url identifier 10394 of the new object. 10395 +/ 10396 string create(scope void delegate() applyChanges) { 10397 applyChanges(); 10398 save(); 10399 return getUrlSlug(); 10400 } 10401 10402 void replace() { 10403 save(); 10404 } 10405 void replace(string urlId, scope void delegate() applyChanges) { 10406 load(urlId); 10407 applyChanges(); 10408 replace(); 10409 } 10410 10411 void update(string[] fieldList) { 10412 save(); 10413 } 10414 void update(string urlId, scope void delegate() applyChanges, string[] fieldList) { 10415 load(urlId); 10416 applyChanges(); 10417 update(fieldList); 10418 } 10419 10420 void remove() {} 10421 10422 void remove(string urlId) { 10423 load(urlId); 10424 remove(); 10425 } 10426 10427 abstract void load(string urlId); 10428 abstract void save(); 10429 10430 Element toHtml(Presenter)(Presenter presenter) { 10431 import arsd.dom; 10432 import std.conv; 10433 auto obj = cast(CRTP) this; 10434 auto div = Element.make("div"); 10435 div.addClass("Dclass_" ~ CRTP.stringof); 10436 div.dataset.url = getUrlSlug(); 10437 bool first = true; 10438 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 10439 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 10440 if(!first) div.addChild("br"); else first = false; 10441 div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); 10442 } 10443 return div; 10444 } 10445 10446 var toJson() { 10447 import arsd.jsvar; 10448 var v = var.emptyObject(); 10449 auto obj = cast(CRTP) this; 10450 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 10451 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 10452 v[memberName] = __traits(getMember, obj, memberName); 10453 } 10454 return v; 10455 } 10456 10457 /+ 10458 auto structOf(this This) { 10459 10460 } 10461 +/ 10462 } 10463 10464 // FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value 10465 // https://use-the-index-luke.com/sql/partial-results/fetch-next-page 10466 10467 /++ 10468 Base class for REST collections. 10469 +/ 10470 class CollectionOf(Obj) : RestObject!(CollectionOf) { 10471 /// You might subclass this and use the cgi object's query params 10472 /// to implement a search filter, for example. 10473 /// 10474 /// FIXME: design a way to auto-generate that form 10475 /// (other than using the WebObject thing above lol 10476 // it'll prolly just be some searchParams UDA or maybe an enum. 10477 // 10478 // pagination too perhaps. 10479 // 10480 // and sorting too 10481 IndexResult index() { return IndexResult.init; } 10482 10483 string[] sortableFields() { return null; } 10484 string[] searchableFields() { return null; } 10485 10486 struct IndexResult { 10487 Obj[] results; 10488 10489 string[] sortableFields; 10490 10491 string previousPageIdentifier; 10492 string nextPageIdentifier; 10493 string firstPageIdentifier; 10494 string lastPageIdentifier; 10495 10496 int numberOfPages; 10497 } 10498 10499 override string create(scope void delegate() applyChanges) { assert(0); } 10500 override void load(string urlId) { assert(0); } 10501 override void save() { assert(0); } 10502 override void show() { 10503 index(); 10504 } 10505 override void show(string urlId) { 10506 show(); 10507 } 10508 10509 /// Proxy POST requests (create calls) to the child collection 10510 alias PostProxy = Obj; 10511 } 10512 10513 /++ 10514 Serves a REST object, similar to a Ruby on Rails resource. 10515 10516 You put data members in your class. cgi.d will automatically make something out of those. 10517 10518 It will call your constructor with the ID from the URL. This may be null. 10519 It will then populate the data members from the request. 10520 It will then call a method, if present, telling what happened. You don't need to write these! 10521 It finally returns a reply. 10522 10523 Your methods are passed a list of fields it actually set. 10524 10525 The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST 10526 APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better 10527 with relative linking. But meh.) 10528 10529 GET /items -> index. all values not set. 10530 GET /items/id -> get. only ID will be set, other params ignored. 10531 POST /items -> create. values set as given 10532 PUT /items/id -> replace. values set as given 10533 or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation 10534 a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form. 10535 PATCH /items/id -> update. values set as given, list of changed fields passed 10536 or POST /items/id with cgi.post["_method"] == "PATCH" 10537 DELETE /items/id -> destroy. only ID guaranteed to be set 10538 or POST /items/id with cgi.post["_method"] == "DELETE" 10539 10540 Following the stupid convention, there will never be a trailing slash here, and if it is there, it will 10541 redirect you away from it. 10542 10543 API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var. 10544 10545 I will also let you change the default, if you must. 10546 10547 // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes. 10548 10549 You can define sub-resources on your object inside the object. These sub-resources are also REST objects 10550 that follow the same thing. They may be individual resources or collections themselves. 10551 10552 Your class is expected to have at least the following methods: 10553 10554 FIXME: i kinda wanna add a routes object to the initialize call 10555 10556 create 10557 Create returns the new address on success, some code on failure. 10558 show 10559 index 10560 update 10561 remove 10562 10563 You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults 10564 should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that. 10565 10566 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar]. 10567 10568 NOT IMPLEMENTED 10569 10570 10571 Really, a collection is a resource with a bunch of subresources. 10572 10573 GET /items 10574 index because it is GET on the top resource 10575 10576 GET /items/foo 10577 item but different than items? 10578 10579 class Items { 10580 10581 } 10582 10583 ... but meh, a collection can be automated. not worth making it 10584 a separate thing, let's look at a real example. Users has many 10585 items and a virtual one, /users/current. 10586 10587 the individual users have properties and two sub-resources: 10588 session, which is just one, and comments, a collection. 10589 10590 class User : RestObject!() { // no parent 10591 int id; 10592 string name; 10593 10594 // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one. 10595 // but you can override them to do it differently. 10596 10597 // any member which is of type RestObject can be linked automatically via href btw. 10598 10599 void show() {} 10600 void show(string urlId) {} // automated! GET of this specific thing 10601 void create() {} // POST on a parent collection - this is called from a collection class after the members are updated 10602 void replace(string urlId) {} // this is the PUT; really, it just updates all fields. 10603 void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields. 10604 void remove(string urlId) {} // DELETE 10605 10606 void load(string urlId) {} // the default implementation of show() populates the id, then 10607 10608 this() {} 10609 10610 mixin Subresource!Session; 10611 mixin Subresource!Comment; 10612 } 10613 10614 class Session : RestObject!() { 10615 // the parent object may not be fully constructed/loaded 10616 this(User parent) {} 10617 10618 } 10619 10620 class Comment : CollectionOf!Comment { 10621 this(User parent) {} 10622 } 10623 10624 class Users : CollectionOf!User { 10625 // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects. 10626 void index() {} // GET on this specific thing; just like show really, just different name for the different semantics. 10627 User create() {} // You MAY implement this, but the default is to create a new object, populate it from args, and then call create() on the child 10628 } 10629 10630 +/ 10631 auto serveRestObject(T)(string urlPrefix) { 10632 assert(urlPrefix[0] == '/'); 10633 assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); 10634 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 10635 string url = cgi.pathInfo[urlPrefix.length .. $]; 10636 10637 if(url.length && url[$ - 1] == '/') { 10638 // remove the final slash... 10639 cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]); 10640 return true; 10641 } 10642 10643 return restObjectServeHandler!T(cgi, presenter, url); 10644 } 10645 return DispatcherDefinition!internalHandler(urlPrefix, false); 10646 } 10647 10648 /+ 10649 /// Convenience method for serving a collection. It will be named the same 10650 /// as type T, just with an s at the end. If you need any further, just 10651 /// write the class yourself. 10652 auto serveRestCollectionOf(T)(string urlPrefix) { 10653 assert(urlPrefix[0] == '/'); 10654 mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`); 10655 return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); 10656 } 10657 +/ 10658 10659 bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) { 10660 string urlId = null; 10661 if(url.length && url[0] == '/') { 10662 // asking for a subobject 10663 urlId = url[1 .. $]; 10664 foreach(idx, ch; urlId) { 10665 if(ch == '/') { 10666 urlId = urlId[0 .. idx]; 10667 break; 10668 } 10669 } 10670 } 10671 10672 // FIXME handle other subresources 10673 10674 static if(is(T : CollectionOf!(C), C)) { 10675 if(urlId !is null) { 10676 return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId); 10677 } 10678 } 10679 10680 // FIXME: support precondition failed, if-modified-since, expectation failed, etc. 10681 10682 auto obj = new T(); 10683 obj.initialize(cgi); 10684 // FIXME: populate reflection info delegates 10685 10686 10687 // FIXME: I am not happy with this. 10688 switch(urlId) { 10689 case "script.js": 10690 cgi.setResponseContentType("text/javascript"); 10691 cgi.gzipResponse = true; 10692 cgi.write(presenter.script(), true); 10693 return true; 10694 case "style.css": 10695 cgi.setResponseContentType("text/css"); 10696 cgi.gzipResponse = true; 10697 cgi.write(presenter.style(), true); 10698 return true; 10699 default: 10700 // intentionally blank 10701 } 10702 10703 10704 10705 10706 static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) { 10707 foreach(idx, memberName; __traits(derivedMembers, Obj)) 10708 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 10709 __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName)); 10710 } 10711 } 10712 void applyChanges() { 10713 applyChangesTemplate(cgi, obj); 10714 } 10715 10716 string[] modifiedList; 10717 10718 void writeObject(bool addFormLinks) { 10719 if(cgi.request("format") == "json") { 10720 cgi.setResponseContentType("application/json"); 10721 cgi.write(obj.toJson().toString, true); 10722 } else { 10723 auto container = presenter.htmlContainer(); 10724 if(addFormLinks) { 10725 static if(is(T : CollectionOf!(C), C)) 10726 container.appendHtml(` 10727 <form> 10728 <button type="submit" name="_method" value="POST">Create New</button> 10729 </form> 10730 `); 10731 else 10732 container.appendHtml(` 10733 <a href="..">Back</a> 10734 <form> 10735 <button type="submit" name="_method" value="PATCH">Edit</button> 10736 <button type="submit" name="_method" value="DELETE">Delete</button> 10737 </form> 10738 `); 10739 } 10740 container.appendChild(obj.toHtml(presenter)); 10741 cgi.write(container.parentDocument.toString, true); 10742 } 10743 } 10744 10745 // FIXME: I think I need a set type in here.... 10746 // it will be nice to pass sets of members. 10747 10748 try 10749 switch(cgi.requestMethod) { 10750 case Cgi.RequestMethod.GET: 10751 // I could prolly use template this parameters in the implementation above for some reflection stuff. 10752 // sure, it doesn't automatically work in subclasses... but I instantiate here anyway... 10753 10754 // automatic forms here for usable basic auto site from browser. 10755 // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh. 10756 switch(cgi.request("_method", "GET")) { 10757 case "GET": 10758 static if(is(T : CollectionOf!(C), C)) { 10759 auto results = obj.index(); 10760 if(cgi.request("format", "html") == "html") { 10761 auto container = presenter.htmlContainer(); 10762 auto html = presenter.formatReturnValueAsHtml(results.results); 10763 container.appendHtml(` 10764 <form> 10765 <button type="submit" name="_method" value="POST">Create New</button> 10766 </form> 10767 `); 10768 10769 container.appendChild(html); 10770 cgi.write(container.parentDocument.toString, true); 10771 } else { 10772 cgi.setResponseContentType("application/json"); 10773 import arsd.jsvar; 10774 var json = var.emptyArray; 10775 foreach(r; results.results) { 10776 var o = var.emptyObject; 10777 foreach(idx, memberName; __traits(derivedMembers, typeof(r))) 10778 static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) { 10779 o[memberName] = __traits(getMember, r, memberName); 10780 } 10781 10782 json ~= o; 10783 } 10784 cgi.write(json.toJson(), true); 10785 } 10786 } else { 10787 obj.show(urlId); 10788 writeObject(true); 10789 } 10790 break; 10791 case "PATCH": 10792 obj.load(urlId); 10793 goto case; 10794 case "PUT": 10795 case "POST": 10796 // an editing form for the object 10797 auto container = presenter.htmlContainer(); 10798 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 10799 auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj); 10800 } else { 10801 auto form = presenter.createAutomaticFormForObject(obj); 10802 } 10803 form.attrs.method = "POST"; 10804 form.setValue("_method", cgi.request("_method", "GET")); 10805 container.appendChild(form); 10806 cgi.write(container.parentDocument.toString(), true); 10807 break; 10808 case "DELETE": 10809 // FIXME: a delete form for the object (can be phrased "are you sure?") 10810 auto container = presenter.htmlContainer(); 10811 container.appendHtml(` 10812 <form method="POST"> 10813 Are you sure you want to delete this item? 10814 <input type="hidden" name="_method" value="DELETE" /> 10815 <input type="submit" value="Yes, Delete It" /> 10816 </form> 10817 10818 `); 10819 cgi.write(container.parentDocument.toString(), true); 10820 break; 10821 default: 10822 cgi.write("bad method\n", true); 10823 } 10824 break; 10825 case Cgi.RequestMethod.POST: 10826 // this is to allow compatibility with HTML forms 10827 switch(cgi.request("_method", "POST")) { 10828 case "PUT": 10829 goto PUT; 10830 case "PATCH": 10831 goto PATCH; 10832 case "DELETE": 10833 goto DELETE; 10834 case "POST": 10835 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 10836 auto p = new obj.PostProxy(); 10837 void specialApplyChanges() { 10838 applyChangesTemplate(cgi, p); 10839 } 10840 string n = p.create(&specialApplyChanges); 10841 } else { 10842 string n = obj.create(&applyChanges); 10843 } 10844 10845 auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n; 10846 cgi.setResponseLocation(newUrl); 10847 cgi.setResponseStatus("201 Created"); 10848 cgi.write(`The object has been created.`); 10849 break; 10850 default: 10851 cgi.write("bad method\n", true); 10852 } 10853 // FIXME this should be valid on the collection, but not the child.... 10854 // 303 See Other 10855 break; 10856 case Cgi.RequestMethod.PUT: 10857 PUT: 10858 obj.replace(urlId, &applyChanges); 10859 writeObject(false); 10860 break; 10861 case Cgi.RequestMethod.PATCH: 10862 PATCH: 10863 obj.update(urlId, &applyChanges, modifiedList); 10864 writeObject(false); 10865 break; 10866 case Cgi.RequestMethod.DELETE: 10867 DELETE: 10868 obj.remove(urlId); 10869 cgi.setResponseStatus("204 No Content"); 10870 break; 10871 default: 10872 // FIXME: OPTIONS, HEAD 10873 } 10874 catch(Throwable t) { 10875 presenter.presentExceptionAsHtml!(DUMMY)(cgi, t, null); 10876 } 10877 10878 return true; 10879 } 10880 10881 struct DUMMY {} 10882 10883 /+ 10884 struct SetOfFields(T) { 10885 private void[0][string] storage; 10886 void set(string what) { 10887 //storage[what] = 10888 } 10889 void unset(string what) {} 10890 void setAll() {} 10891 void unsetAll() {} 10892 bool isPresent(string what) { return false; } 10893 } 10894 +/ 10895 10896 /+ 10897 enum readonly; 10898 enum hideonindex; 10899 +/ 10900 10901 /++ 10902 Serves a static file. To be used with [dispatcher]. 10903 10904 See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect] 10905 +/ 10906 auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) { 10907 // https://baus.net/on-tcp_cork/ 10908 // man 2 sendfile 10909 assert(urlPrefix[0] == '/'); 10910 if(filename is null) 10911 filename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? 10912 if(contentType is null) { 10913 contentType = contentTypeFromFileExtension(filename); 10914 } 10915 10916 static struct DispatcherDetails { 10917 string filename; 10918 string contentType; 10919 } 10920 10921 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 10922 if(details.contentType.indexOf("image/") == 0) 10923 cgi.setCache(true); 10924 cgi.setResponseContentType(details.contentType); 10925 cgi.write(std.file.read(details.filename), true); 10926 return true; 10927 } 10928 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); 10929 } 10930 10931 /++ 10932 Serves static data. To be used with [dispatcher]. 10933 10934 History: 10935 Added October 31, 2021 10936 +/ 10937 auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { 10938 assert(urlPrefix[0] == '/'); 10939 if(contentType is null) { 10940 contentType = contentTypeFromFileExtension(urlPrefix); 10941 } 10942 10943 static struct DispatcherDetails { 10944 immutable(void)[] data; 10945 string contentType; 10946 } 10947 10948 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 10949 cgi.setCache(true); 10950 cgi.setResponseContentType(details.contentType); 10951 cgi.write(details.data, true); 10952 return true; 10953 } 10954 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); 10955 } 10956 10957 string contentTypeFromFileExtension(string filename) { 10958 if(filename.endsWith(".png")) 10959 return "image/png"; 10960 if(filename.endsWith(".apng")) 10961 return "image/apng"; 10962 if(filename.endsWith(".svg")) 10963 return "image/svg+xml"; 10964 if(filename.endsWith(".jpg")) 10965 return "image/jpeg"; 10966 if(filename.endsWith(".html")) 10967 return "text/html"; 10968 if(filename.endsWith(".css")) 10969 return "text/css"; 10970 if(filename.endsWith(".js")) 10971 return "application/javascript"; 10972 if(filename.endsWith(".wasm")) 10973 return "application/wasm"; 10974 if(filename.endsWith(".mp3")) 10975 return "audio/mpeg"; 10976 return null; 10977 } 10978 10979 /// This serves a directory full of static files, figuring out the content-types from file extensions. 10980 /// It does not let you to descend into subdirectories (or ascend out of it, of course) 10981 auto serveStaticFileDirectory(string urlPrefix, string directory = null) { 10982 assert(urlPrefix[0] == '/'); 10983 assert(urlPrefix[$-1] == '/'); 10984 10985 static struct DispatcherDetails { 10986 string directory; 10987 } 10988 10989 if(directory is null) 10990 directory = urlPrefix[1 .. $]; 10991 10992 assert(directory[$-1] == '/'); 10993 10994 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 10995 auto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct 10996 if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) 10997 return false; 10998 10999 auto contentType = contentTypeFromFileExtension(file); 11000 11001 auto fn = details.directory ~ file; 11002 if(std.file.exists(fn)) { 11003 //if(contentType.indexOf("image/") == 0) 11004 //cgi.setCache(true); 11005 //else if(contentType.indexOf("audio/") == 0) 11006 cgi.setCache(true); 11007 cgi.setResponseContentType(contentType); 11008 cgi.write(std.file.read(fn), true); 11009 return true; 11010 } else { 11011 return false; 11012 } 11013 } 11014 11015 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory)); 11016 } 11017 11018 /++ 11019 Redirects one url to another 11020 11021 See_Also: [dispatcher], [serveStaticFile] 11022 +/ 11023 auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) { 11024 assert(urlPrefix[0] == '/'); 11025 static struct DispatcherDetails { 11026 string redirectTo; 11027 string code; 11028 } 11029 11030 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 11031 cgi.setResponseLocation(details.redirectTo, true, details.code); 11032 return true; 11033 } 11034 11035 11036 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code))); 11037 } 11038 11039 /// Used exclusively with `dispatchTo` 11040 struct DispatcherData(Presenter) { 11041 Cgi cgi; /// You can use this cgi object. 11042 Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher. 11043 size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only. 11044 } 11045 11046 /++ 11047 Dispatches the URL to a specific function. 11048 +/ 11049 auto handleWith(alias handler)(string urlPrefix) { 11050 // cuz I'm too lazy to do it better right now 11051 static class Hack : WebObject { 11052 static import std.traits; 11053 @UrlName("") 11054 auto handle(std.traits.Parameters!handler args) { 11055 return handler(args); 11056 } 11057 } 11058 11059 return urlPrefix.serveApiInternal!Hack; 11060 } 11061 11062 /++ 11063 Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this: 11064 11065 --- 11066 bool other(DD)(DD dd) { 11067 return dd.dispatcher!( 11068 "/whatever".serveRedirect("/success"), 11069 "/api/".serveApi!MyClass 11070 ); 11071 } 11072 --- 11073 11074 The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher 11075 here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters. 11076 Or, of course, you could just use the exact type in your own code. 11077 11078 You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a 11079 good job. 11080 11081 11082 +/ 11083 auto dispatchTo(alias handler)(string urlPrefix) { 11084 assert(urlPrefix[0] == '/'); 11085 assert(urlPrefix[$-1] != '/'); 11086 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { 11087 return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); 11088 } 11089 11090 return DispatcherDefinition!(internalHandler)(urlPrefix, false); 11091 } 11092 11093 /+ 11094 /++ 11095 See [serveStaticFile] if you want to serve a file off disk. 11096 +/ 11097 auto serveStaticData(string urlPrefix, const(void)[] data, string contentType) { 11098 11099 } 11100 +/ 11101 11102 /++ 11103 A URL dispatcher. 11104 11105 --- 11106 if(cgi.dispatcher!( 11107 "/api/".serveApi!MyApiClass, 11108 "/objects/lol".serveRestObject!MyRestObject, 11109 "/file.js".serveStaticFile, 11110 "/admin/".dispatchTo!adminHandler 11111 )) return; 11112 --- 11113 11114 11115 You define a series of url prefixes followed by handlers. 11116 11117 [dispatchTo] will send the request to another function for handling. 11118 You may want to do different pre- and post- processing there, for example, 11119 an authorization check and different page layout. You can use different 11120 presenters and different function chains. NOT IMPLEMENTED 11121 +/ 11122 template dispatcher(definitions...) { 11123 bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) { 11124 static if(is(Presenter == typeof(null))) { 11125 static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {} 11126 auto presenter = new GenericWebPresenter(); 11127 } else 11128 alias presenter = presenterArg; 11129 11130 return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0)); 11131 } 11132 11133 bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) { 11134 // I can prolly make this more efficient later but meh. 11135 foreach(definition; definitions) { 11136 if(definition.rejectFurther) { 11137 if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) { 11138 auto ret = definition.handler( 11139 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 11140 dispatcherData.cgi, dispatcherData.presenter, definition.details); 11141 if(ret) 11142 return true; 11143 } 11144 } else if( 11145 dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) && 11146 // cgi.d dispatcher urls must be complete or have a /; 11147 // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing" 11148 (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length 11149 || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/') 11150 ) { 11151 auto ret = definition.handler( 11152 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 11153 dispatcherData.cgi, dispatcherData.presenter, definition.details); 11154 if(ret) 11155 return true; 11156 } 11157 } 11158 return false; 11159 } 11160 } 11161 11162 }); 11163 11164 private struct StackBuffer { 11165 char[1024] initial = void; 11166 char[] buffer; 11167 size_t position; 11168 11169 this(int a) { 11170 buffer = initial[]; 11171 position = 0; 11172 } 11173 11174 void add(in char[] what) { 11175 if(position + what.length > buffer.length) 11176 buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases 11177 buffer[position .. position + what.length] = what[]; 11178 position += what.length; 11179 } 11180 11181 void add(in char[] w1, in char[] w2, in char[] w3 = null) { 11182 add(w1); 11183 add(w2); 11184 add(w3); 11185 } 11186 11187 void add(long v) { 11188 char[16] buffer = void; 11189 auto pos = buffer.length; 11190 bool negative; 11191 if(v < 0) { 11192 negative = true; 11193 v = -v; 11194 } 11195 do { 11196 buffer[--pos] = cast(char) (v % 10 + '0'); 11197 v /= 10; 11198 } while(v); 11199 11200 if(negative) 11201 buffer[--pos] = '-'; 11202 11203 auto res = buffer[pos .. $]; 11204 11205 add(res[]); 11206 } 11207 11208 char[] get() @nogc { 11209 return buffer[0 .. position]; 11210 } 11211 } 11212 11213 // duplicated in http2.d 11214 private static string getHttpCodeText(int code) pure nothrow @nogc { 11215 switch(code) { 11216 case 200: return "200 OK"; 11217 case 201: return "201 Created"; 11218 case 202: return "202 Accepted"; 11219 case 203: return "203 Non-Authoritative Information"; 11220 case 204: return "204 No Content"; 11221 case 205: return "205 Reset Content"; 11222 case 206: return "206 Partial Content"; 11223 // 11224 case 300: return "300 Multiple Choices"; 11225 case 301: return "301 Moved Permanently"; 11226 case 302: return "302 Found"; 11227 case 303: return "303 See Other"; 11228 case 304: return "304 Not Modified"; 11229 case 305: return "305 Use Proxy"; 11230 case 307: return "307 Temporary Redirect"; 11231 case 308: return "308 Permanent Redirect"; 11232 11233 // 11234 case 400: return "400 Bad Request"; 11235 case 401: return "401 Unauthorized"; 11236 case 402: return "402 Payment Required"; 11237 case 403: return "403 Forbidden"; 11238 case 404: return "404 Not Found"; 11239 case 405: return "405 Method Not Allowed"; 11240 case 406: return "406 Not Acceptable"; 11241 case 407: return "407 Proxy Authentication Required"; 11242 case 408: return "408 Request Timeout"; 11243 case 409: return "409 Conflict"; 11244 case 410: return "410 Gone"; 11245 case 411: return "411 Length Required"; 11246 case 412: return "412 Precondition Failed"; 11247 case 413: return "413 Payload Too Large"; 11248 case 414: return "414 URI Too Long"; 11249 case 415: return "415 Unsupported Media Type"; 11250 case 416: return "416 Range Not Satisfiable"; 11251 case 417: return "417 Expectation Failed"; 11252 case 418: return "418 I'm a teapot"; 11253 case 421: return "421 Misdirected Request"; 11254 case 422: return "422 Unprocessable Entity (WebDAV)"; 11255 case 423: return "423 Locked (WebDAV)"; 11256 case 424: return "424 Failed Dependency (WebDAV)"; 11257 case 425: return "425 Too Early"; 11258 case 426: return "426 Upgrade Required"; 11259 case 428: return "428 Precondition Required"; 11260 case 431: return "431 Request Header Fields Too Large"; 11261 case 451: return "451 Unavailable For Legal Reasons"; 11262 11263 case 500: return "500 Internal Server Error"; 11264 case 501: return "501 Not Implemented"; 11265 case 502: return "502 Bad Gateway"; 11266 case 503: return "503 Service Unavailable"; 11267 case 504: return "504 Gateway Timeout"; 11268 case 505: return "505 HTTP Version Not Supported"; 11269 case 506: return "506 Variant Also Negotiates"; 11270 case 507: return "507 Insufficient Storage (WebDAV)"; 11271 case 508: return "508 Loop Detected (WebDAV)"; 11272 case 510: return "510 Not Extended"; 11273 case 511: return "511 Network Authentication Required"; 11274 // 11275 default: assert(0, "Unsupported http code"); 11276 } 11277 } 11278 11279 11280 /+ 11281 /++ 11282 This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object. 11283 11284 It relies on jsvar.d and dom.d. 11285 11286 11287 You can get javascript out of it to call. The generated functions need to look 11288 like 11289 11290 function name(a,b,c,d,e) { 11291 return _call("name", {"realName":a,"sds":b}); 11292 } 11293 11294 And _call returns an object you can call or set up or whatever. 11295 +/ 11296 bool apiDispatcher()(Cgi cgi) { 11297 import arsd.jsvar; 11298 import arsd.dom; 11299 } 11300 +/ 11301 version(linux) 11302 private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; 11303 /* 11304 Copyright: Adam D. Ruppe, 2008 - 2022 11305 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. 11306 Authors: Adam D. Ruppe 11307 11308 Copyright Adam D. Ruppe 2008 - 2022. 11309 Distributed under the Boost Software License, Version 1.0. 11310 (See accompanying file LICENSE_1_0.txt or copy at 11311 http://www.boost.org/LICENSE_1_0.txt) 11312 */