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. Offers both lower- and higher- level api options among other common (optional) things like websocket and event source serving support, session management, and job scheduling. 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 Or: 78 --- 79 import arsd.cgi; 80 81 class MyApi : WebObject { 82 @UrlName("") 83 string hello(string name = null) { 84 if(name is null) 85 return "Hello, world!"; 86 else 87 return "Hello, " ~ name; 88 } 89 } 90 mixin DispatcherMain!( 91 "/".serveApi!MyApi 92 ); 93 --- 94 95 $(NOTE 96 Please note that using the higher-level api will add a dependency on arsd.dom and arsd.jsvar to your application. 97 If you use `dmd -i` or `ldc2 -i` to build, it will just work, but with dub, you will have do `dub add arsd-official:jsvar` 98 and `dub add arsd-official:dom` yourself. 99 ) 100 101 Test on console (works in any interface mode): 102 $(CONSOLE 103 $ ./cgi_hello GET / name=whatever 104 ) 105 106 If using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd): 107 $(CONSOLE 108 $ ./cgi_hello --port 8080 109 # now you can go to http://localhost:8080/?name=whatever 110 ) 111 112 Please note: the default port for http is 8085 and for scgi 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 code your own with [RequestServer], however. 113 114 115 Build_Configurations: 116 117 cgi.d tries to be flexible to meet your needs. It is possible to configure it both at runtime (by writing your own `main` function and constructing a [RequestServer] object) or at compile time using the `version` switch to the compiler or a dub `subConfiguration`. 118 119 If you are using `dub`, use: 120 121 ```sdlang 122 subConfiguration "arsd-official:cgi" "VALUE_HERE" 123 ``` 124 125 or to dub.json: 126 127 ```json 128 "subConfigurations": {"arsd-official:cgi": "VALUE_HERE"} 129 ``` 130 131 to change versions. The possible options for `VALUE_HERE` are: 132 133 $(LIST 134 * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. Note: prior to version 11, this would be embedded_httpd_processes on Linux and embedded_httpd_threads everywhere else. It now means embedded_httpd_hybrid everywhere supported and embedded_httpd_threads everywhere else. 135 * `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests. 136 * `fastcgi` for FastCGI builds. FastCGI is managed from an outside helper, there's one built into Microsoft IIS, Apache httpd, and Lighttpd, and a generic program you can use with nginx called `spawn-fcgi`. If you don't already know how to use it, I suggest you use one of the other modes. 137 * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver. Please note: on nginx make sure you add `scgi_param PATH_INFO $document_uri;` to the config! 138 * `stdio_http` for speaking raw http over stdin and stdout. This is made for systemd services. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information. 139 ) 140 141 With dmd, use: 142 143 $(TABLE_ROWS 144 145 * + Interfaces 146 + (mutually exclusive) 147 148 * - `-version=plain_cgi` 149 - The default building the module alone without dub - a traditional, plain CGI executable will be generated. 150 * - `-version=embedded_httpd` 151 - A HTTP server will be embedded in the generated executable. This is default when building with dub. 152 * - `-version=fastcgi` 153 - A FastCGI executable will be generated. 154 * - `-version=scgi` 155 - A SCGI (SimpleCGI) executable will be generated. 156 * - `-version=embedded_httpd_hybrid` 157 - A HTTP server that uses a combination of processes, threads, and fibers to better handle large numbers of idle connections. Recommended if you are going to serve websockets in a non-local application. 158 * - `-version=embedded_httpd_threads` 159 - 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) 160 * - `-version=embedded_httpd_processes` 161 - The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation) 162 * - `-version=embedded_httpd_processes_accept_after_fork` 163 - 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. 164 * - `-version=stdio_http` 165 - The embedded HTTP server will be spoken over stdin and stdout. 166 167 * + Tweaks 168 + (can be used together with others) 169 170 * - `-version=cgi_with_websocket` 171 - The CGI class has websocket server support. (This is on by default now.) 172 173 * - `-version=with_openssl` 174 - not currently used 175 * - `-version=cgi_embedded_sessions` 176 - The session server will be embedded in the cgi.d server process 177 * - `-version=cgi_session_server_process` 178 - The session will be provided in a separate process, provided by cgi.d. 179 ) 180 181 For example, 182 183 For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory. 184 185 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). 186 187 For SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line. 188 189 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. 190 191 Simulating_requests: 192 193 If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command-ine shell. Call the program like this: 194 195 $(CONSOLE 196 ./yourprogram GET / name=adr 197 ) 198 199 And it will print the result to stdout instead of running a server, regardless of build more.. 200 201 CGI_Setup_tips: 202 203 On Apache, you may do `SetHandler cgi-script` in your `.htaccess` file to set a particular file to be run through the cgi program. Note that all "subdirectories" of it also run the program; if you configure `/foo` to be a cgi script, then going to `/foo/bar` will call your cgi handler function with `cgi.pathInfo == "/bar"`. 204 205 Overview_Of_Basic_Concepts: 206 207 cgi.d offers both lower-level handler apis as well as higher-level auto-dispatcher apis. For a lower-level handler function, you'll probably want to review the following functions: 208 209 Input: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod], 210 and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId]) 211 212 Output: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse] 213 214 Cookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies] 215 216 Caching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache] 217 218 Redirections: [Cgi.setResponseLocation] 219 220 Other Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived] 221 222 Websockets: [Websocket], [websocketRequested], [acceptWebsocket]. For websockets, use the `embedded_httpd_hybrid` build mode for best results, because it is optimized for handling large numbers of idle connections compared to the other build modes. 223 224 Overriding behavior for special cases streaming input data: see the virtual functions [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState] 225 226 A basic program using the lower-level api might look like: 227 228 --- 229 import arsd.cgi; 230 231 // you write a request handler which always takes a Cgi object 232 void handler(Cgi cgi) { 233 /+ 234 when the user goes to your site, suppose you are being hosted at http://example.com/yourapp 235 236 If the user goes to http://example.com/yourapp/test?name=value 237 then the url will be parsed out into the following pieces: 238 239 cgi.pathInfo == "/test". This is everything after yourapp's name. (If you are doing an embedded http server, your app's name is blank, so pathInfo will be the whole path of the url.) 240 241 cgi.scriptName == "yourapp". With an embedded http server, this will be blank. 242 243 cgi.host == "example.com" 244 245 cgi.https == false 246 247 cgi.queryString == "name=value" (there's also cgi.search, which will be "?name=value", including the ?) 248 249 The query string is further parsed into the `get` and `getArray` members, so: 250 251 cgi.get == ["name": "value"], meaning you can do `cgi.get["name"] == "value"` 252 253 And 254 255 cgi.getArray == ["name": ["value"]]. 256 257 Why is there both `get` and `getArray`? The standard allows names to be repeated. This can be very useful, 258 it is how http forms naturally pass multiple items like a set of checkboxes. So `getArray` is the complete data 259 if you need it. But since so often you only care about one value, the `get` member provides more convenient access. 260 261 We can use these members to process the request and build link urls. Other info from the request are in other members, we'll look at them later. 262 +/ 263 switch(cgi.pathInfo) { 264 // the home page will be a small html form that can set a cookie. 265 case "/": 266 cgi.write(`<!DOCTYPE html> 267 <html> 268 <body> 269 <form method="POST" action="set-cookie"> 270 <label>Your name: <input type="text" name="name" /></label> 271 <input type="submit" value="Submit" /> 272 </form> 273 </body> 274 </html> 275 `, true); // the , true tells it that this is the one, complete response i want to send, allowing some optimizations. 276 break; 277 // POSTing to this will set a cookie with our submitted name 278 case "/set-cookie": 279 // HTTP has a number of request methods (also called "verbs") to tell 280 // what you should do with the given resource. 281 // The most common are GET and POST, the ones used in html forms. 282 // You can check which one was used with the `cgi.requestMethod` property. 283 if(cgi.requestMethod == Cgi.RequestMethod.POST) { 284 285 // headers like redirections need to be set before we call `write` 286 cgi.setResponseLocation("read-cookie"); 287 288 // just like how url params go into cgi.get/getArray, form data submitted in a POST 289 // body go to cgi.post/postArray. Please note that a POST request can also have get 290 // params in addition to post params. 291 // 292 // There's also a convenience function `cgi.request("name")` which checks post first, 293 // then get if it isn't found there, and then returns a default value if it is in neither. 294 if("name" in cgi.post) { 295 // we can set cookies with a method too 296 // again, cookies need to be set before calling `cgi.write`, since they 297 // are a kind of header. 298 cgi.setCookie("name" , cgi.post["name"]); 299 } 300 301 // the user will probably never see this, since the response location 302 // is an automatic redirect, but it is still best to say something anyway 303 cgi.write("Redirecting you to see the cookie...", true); 304 } else { 305 // you can write out response codes and headers 306 // as well as response bodies 307 // 308 // But always check the cgi docs before using the generic 309 // `header` method - if there is a specific method for your 310 // header, use it before resorting to the generic one to avoid 311 // a header value from being sent twice. 312 cgi.setResponseLocation("405 Method Not Allowed"); 313 // there is no special accept member, so you can use the generic header function 314 cgi.header("Accept: POST"); 315 // but content type does have a method, so prefer to use it: 316 cgi.setResponseContentType("text/plain"); 317 318 // all the headers are buffered, and will be sent upon the first body 319 // write. you can actually modify some of them before sending if need be. 320 cgi.write("You must use the POST http verb on this resource.", true); 321 } 322 break; 323 // and GETting this will read the cookie back out 324 case "/read-cookie": 325 // I did NOT pass `,true` here because this is writing a partial response. 326 // It is possible to stream data to the user in chunks by writing partial 327 // responses the calling `cgi.flush();` to send the partial response immediately. 328 // normally, you'd only send partial chunks if you have to - it is better to build 329 // a response as a whole and send it as a whole whenever possible - but here I want 330 // to demo that you can. 331 cgi.write("Hello, "); 332 if("name" in cgi.cookies) { 333 import arsd.dom; // dom.d provides a lot of helpers for html 334 // since the cookie is set, we need to write it out properly to 335 // avoid cross-site scripting attacks. 336 // 337 // Getting this stuff right automatically is a benefit of using the higher 338 // level apis, but this demo is to show the fundamental building blocks, so 339 // we're responsible to take care of it. 340 cgi.write(htmlEntitiesEncode(cgi.cookies["name"])); 341 } else { 342 cgi.write("friend"); 343 } 344 345 // note that I never called cgi.setResponseContentType, since the default is text/html. 346 // it doesn't hurt to do it explicitly though, just remember to do it before any cgi.write 347 // calls. 348 break; 349 default: 350 // no path matched 351 cgi.setResponseStatus("404 Not Found"); 352 cgi.write("Resource not found.", true); 353 } 354 } 355 356 // and this adds the boilerplate to set up a server according to the 357 // compile version configuration and call your handler as requests come in 358 mixin GenericMain!handler; // the `handler` here is the name of your function 359 --- 360 361 Even if you plan to always use the higher-level apis, I still recommend you at least familiarize yourself with the lower level functions, since they provide the lightest weight, most flexible options to get down to business if you ever need them. 362 363 In the lower-level api, the [Cgi] object represents your HTTP transaction. It has functions to describe the request and for you to send your response. It leaves the details of how you o it up to you. The general guideline though is to avoid depending any variables outside your handler function, since there's no guarantee they will survive to another handler. You can use global vars as a lazy initialized cache, but you should always be ready in case it is empty. (One exception: if you use `-version=embedded_httpd_threads -version=cgi_no_fork`, then you can rely on it more, but you should still really write things assuming your function won't have anything survive beyond its return for max scalability and compatibility.) 364 365 A basic program using the higher-level apis might look like: 366 367 --- 368 /+ 369 import arsd.cgi; 370 371 struct LoginData { 372 string currentUser; 373 } 374 375 class AppClass : WebObject { 376 string foo() {} 377 } 378 379 mixin DispatcherMain!( 380 "/assets/.serveStaticFileDirectory("assets/", true), // serve the files in the assets subdirectory 381 "/".serveApi!AppClass, 382 "/thing/".serveRestObject, 383 ); 384 +/ 385 --- 386 387 Guide_for_PHP_users: 388 (Please note: I wrote this section in 2008. A lot of PHP hosts still ran 4.x back then, so it was common to avoid using classes - introduced in php 5 - to maintain compatibility! If you're coming from php more recently, this may not be relevant anymore, but still might help you.) 389 390 If you are coming from old-style PHP, here's a quick guide to help you get started: 391 392 $(SIDE_BY_SIDE 393 $(COLUMN 394 ```php 395 <?php 396 $foo = $_POST["foo"]; 397 $bar = $_GET["bar"]; 398 $baz = $_COOKIE["baz"]; 399 400 $user_ip = $_SERVER["REMOTE_ADDR"]; 401 $host = $_SERVER["HTTP_HOST"]; 402 $path = $_SERVER["PATH_INFO"]; 403 404 setcookie("baz", "some value"); 405 406 echo "hello!"; 407 ?> 408 ``` 409 ) 410 $(COLUMN 411 --- 412 import arsd.cgi; 413 void app(Cgi cgi) { 414 string foo = cgi.post["foo"]; 415 string bar = cgi.get["bar"]; 416 string baz = cgi.cookies["baz"]; 417 418 string user_ip = cgi.remoteAddress; 419 string host = cgi.host; 420 string path = cgi.pathInfo; 421 422 cgi.setCookie("baz", "some value"); 423 424 cgi.write("hello!"); 425 } 426 427 mixin GenericMain!app 428 --- 429 ) 430 ) 431 432 $(H3 Array elements) 433 434 435 In PHP, you can give a form element a name like `"something[]"`, and then 436 `$_POST["something"]` gives an array. In D, you can use whatever name 437 you want, and access an array of values with the `cgi.getArray["name"]` and 438 `cgi.postArray["name"]` members. 439 440 $(H3 Databases) 441 442 PHP has a lot of stuff in its standard library. cgi.d doesn't include most 443 of these, but the rest of my arsd repository has much of it. For example, 444 to access a MySQL database, download `database.d` and `mysql.d` from my 445 github repo, and try this code (assuming, of course, your database is 446 set up): 447 448 --- 449 import arsd.cgi; 450 import arsd.mysql; 451 452 void app(Cgi cgi) { 453 auto database = new MySql("localhost", "username", "password", "database_name"); 454 foreach(row; mysql.query("SELECT count(id) FROM people")) 455 cgi.write(row[0] ~ " people in database"); 456 } 457 458 mixin GenericMain!app; 459 --- 460 461 Similar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases, 462 implementing the same basic interface. 463 464 See_Also: 465 466 You may also want to see [arsd.dom], [arsd.webtemplate], and maybe some functions from my old [arsd.html] for more code for making 467 web applications. dom and webtemplate are used by the higher-level api here in cgi.d. 468 469 For working with json, try [arsd.jsvar]. 470 471 [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in 472 accessing databases. 473 474 If you are looking to access a web application via HTTP, try [arsd.http2]. 475 476 Copyright: 477 478 cgi.d copyright 2008-2023, Adam D. Ruppe. Provided under the Boost Software License. 479 480 Yes, this file is old, and yes, it is still actively maintained and used. 481 482 History: 483 An import of `arsd.core` was added on March 21, 2023 (dub v11.0). Prior to this, the module's default configuration was completely stand-alone. You must now include the `core.d` file in your builds with `cgi.d`. 484 485 This change is primarily to integrate the event loops across the library, allowing you to more easily use cgi.d along with my other libraries like simpledisplay and http2.d. Previously, you'd have to run separate helper threads. Now, they can all automatically work together. 486 +/ 487 module arsd.cgi; 488 489 // FIXME: Nullable!T can be a checkbox that enables/disables the T on the automatic form 490 // and a SumType!(T, R) can be a radio box to pick between T and R to disclose the extra boxes on the automatic form 491 492 /++ 493 This micro-example uses the [dispatcher] api to act as a simple http file server, serving files found in the current directory and its children. 494 +/ 495 version(Demo) 496 unittest { 497 import arsd.cgi; 498 499 mixin DispatcherMain!( 500 "/".serveStaticFileDirectory(null, true) 501 ); 502 } 503 504 /++ 505 Same as the previous example, but written out long-form without the use of [DispatcherMain] nor [GenericMain]. 506 +/ 507 version(Demo) 508 unittest { 509 import arsd.cgi; 510 511 void requestHandler(Cgi cgi) { 512 cgi.dispatcher!( 513 "/".serveStaticFileDirectory(null, true) 514 ); 515 } 516 517 // mixin GenericMain!requestHandler would add this function: 518 void main(string[] args) { 519 // this is all the content of [cgiMainImpl] which you can also call 520 521 // cgi.d embeds a few add on functions like real time event forwarders 522 // and session servers it can run in other processes. this spawns them, if needed. 523 if(tryAddonServers(args)) 524 return; 525 526 // cgi.d allows you to easily simulate http requests from the command line, 527 // without actually starting a server. this function will do that. 528 if(trySimulatedRequest!(requestHandler, Cgi)(args)) 529 return; 530 531 RequestServer server; 532 // you can change the default port here if you like 533 // server.listeningPort = 9000; 534 535 // then call this to let the command line args override your default 536 server.configureFromCommandLine(args); 537 538 // here is where you could print out the listeningPort to the user if you wanted 539 540 // and serve the request(s) according to the compile configuration 541 server.serve!(requestHandler)(); 542 543 // or you could explicitly choose a serve mode like this: 544 // server.serveEmbeddedHttp!requestHandler(); 545 } 546 } 547 548 /++ 549 cgi.d has built-in testing helpers too. These will provide mock requests and mock sessions that 550 otherwise run through the rest of the internal mechanisms to call your functions without actually 551 spinning up a server. 552 +/ 553 version(Demo) 554 unittest { 555 import arsd.cgi; 556 557 void requestHandler(Cgi cgi) { 558 559 } 560 561 // D doesn't let me embed a unittest inside an example unittest 562 // so this is a function, but you can do it however in your real program 563 /* unittest */ void runTests() { 564 auto tester = new CgiTester(&requestHandler); 565 566 auto response = tester.GET("/"); 567 assert(response.code == 200); 568 } 569 } 570 571 /++ 572 The session system works via a built-in spawnable server. 573 574 Bugs: 575 Requires addon servers, which are not implemented yet on Windows. 576 +/ 577 version(Posix) 578 version(Demo) 579 unittest { 580 import arsd.cgi; 581 582 struct SessionData { 583 string userId; 584 } 585 586 void handler(Cgi cgi) { 587 auto session = cgi.getSessionObject!SessionData; 588 589 if(cgi.pathInfo == "/login") { 590 session.userId = cgi.queryString; 591 cgi.setResponseLocation("view"); 592 } else { 593 cgi.write(session.userId); 594 } 595 } 596 597 mixin GenericMain!handler; 598 } 599 600 static import std.file; 601 602 static import arsd.core; 603 version(Posix) 604 import arsd.core : makeNonBlocking; 605 606 import arsd.core : encodeUriComponent, decodeUriComponent; 607 608 609 // for a single thread, linear request thing, use: 610 // -version=embedded_httpd_threads -version=cgi_no_threads 611 612 version(Posix) { 613 version(CRuntime_Musl) { 614 615 } else version(minimal) { 616 617 } else { 618 version(FreeBSD) { 619 // I never implemented the fancy stuff there either 620 } else { 621 version=with_breaking_cgi_features; 622 version=with_sendfd; 623 version=with_addon_servers; 624 } 625 } 626 } 627 628 version(Windows) { 629 version(minimal) { 630 631 } else { 632 // not too concerned about gdc here since the mingw version is fairly new as well 633 version=with_breaking_cgi_features; 634 } 635 } 636 637 // FIXME: can use the arsd.core function now but it is trivial anyway tbh 638 void cloexec(int fd) { 639 version(Posix) { 640 import core.sys.posix.fcntl; 641 fcntl(fd, F_SETFD, FD_CLOEXEC); 642 } 643 } 644 645 void cloexec(Socket s) { 646 version(Posix) { 647 import core.sys.posix.fcntl; 648 fcntl(s.handle, F_SETFD, FD_CLOEXEC); 649 } 650 } 651 652 // the servers must know about the connections to talk to them; the interfaces are vital 653 version(with_addon_servers) 654 version=with_addon_servers_connections; 655 656 version(embedded_httpd) { 657 version(OSX) 658 version = embedded_httpd_threads; 659 else 660 version=embedded_httpd_hybrid; 661 /* 662 version(with_openssl) { 663 pragma(lib, "crypto"); 664 pragma(lib, "ssl"); 665 } 666 */ 667 } 668 669 version(embedded_httpd_hybrid) { 670 version=embedded_httpd_threads; 671 version(cgi_no_fork) {} else version(Posix) 672 version=cgi_use_fork; 673 version=cgi_use_fiber; 674 } 675 676 version(cgi_use_fork) 677 enum cgi_use_fork_default = true; 678 else 679 enum cgi_use_fork_default = false; 680 681 version(embedded_httpd_processes) 682 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 683 684 version(embedded_httpd_threads) { 685 // unless the user overrides the default.. 686 version(cgi_session_server_process) 687 {} 688 else 689 version=cgi_embedded_sessions; 690 } 691 version(scgi) { 692 // unless the user overrides the default.. 693 version(cgi_session_server_process) 694 {} 695 else 696 version=cgi_embedded_sessions; 697 } 698 699 // fall back if the other is not defined so we can cleanly version it below 700 version(cgi_embedded_sessions) {} 701 else version=cgi_session_server_process; 702 703 704 version=cgi_with_websocket; 705 706 enum long defaultMaxContentLength = 5_000_000; 707 708 /* 709 710 To do a file download offer in the browser: 711 712 cgi.setResponseContentType("text/csv"); 713 cgi.header("Content-Disposition: attachment; filename=\"customers.csv\""); 714 */ 715 716 // FIXME: the location header is supposed to be an absolute url I guess. 717 718 // FIXME: would be cool to flush part of a dom document before complete 719 // somehow in here and dom.d. 720 721 722 // these are public so you can mixin GenericMain. 723 // FIXME: use a function level import instead! 724 public import std.string; 725 public import std.stdio; 726 public import std.conv; 727 import std.uni; 728 import std.algorithm.comparison; 729 import std.algorithm.searching; 730 import std.exception; 731 import std.base64; 732 static import std.algorithm; 733 import std.datetime; 734 import std.range; 735 736 import std.process; 737 738 import std.zlib; 739 740 741 T[] consume(T)(T[] range, int count) { 742 if(count > range.length) 743 count = range.length; 744 return range[count..$]; 745 } 746 747 int locationOf(T)(T[] data, string item) { 748 const(ubyte[]) d = cast(const(ubyte[])) data; 749 const(ubyte[]) i = cast(const(ubyte[])) item; 750 751 // this is a vague sanity check to ensure we aren't getting insanely 752 // sized input that will infinite loop below. it should never happen; 753 // even huge file uploads ought to come in smaller individual pieces. 754 if(d.length > (int.max/2)) 755 throw new Exception("excessive block of input"); 756 757 for(int a = 0; a < d.length; a++) { 758 if(a + i.length > d.length) 759 return -1; 760 if(d[a..a+i.length] == i) 761 return a; 762 } 763 764 return -1; 765 } 766 767 /// If you are doing a custom cgi class, mixing this in can take care of 768 /// the required constructors for you 769 mixin template ForwardCgiConstructors() { 770 this(long maxContentLength = defaultMaxContentLength, 771 string[string] env = null, 772 const(ubyte)[] delegate() readdata = null, 773 void delegate(const(ubyte)[]) _rawDataOutput = null, 774 void delegate() _flush = null 775 ) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); } 776 777 this(string[] args) { super(args); } 778 779 this( 780 BufferedInputRange inputData, 781 string address, ushort _port, 782 int pathInfoStarts = 0, 783 bool _https = false, 784 void delegate(const(ubyte)[]) _rawDataOutput = null, 785 void delegate() _flush = null, 786 // this pointer tells if the connection is supposed to be closed after we handle this 787 bool* closeConnection = null) 788 { 789 super(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection); 790 } 791 792 this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); } 793 } 794 795 /// thrown when a connection is closed remotely while we waiting on data from it 796 class ConnectionClosedException : Exception { 797 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 798 super(message, file, line, next); 799 } 800 } 801 802 803 version(Windows) { 804 // FIXME: ugly hack to solve stdin exception problems on Windows: 805 // reading stdin results in StdioException (Bad file descriptor) 806 // this is probably due to http://d.puremagic.com/issues/show_bug.cgi?id=3425 807 private struct stdin { 808 struct ByChunk { // Replicates std.stdio.ByChunk 809 private: 810 ubyte[] chunk_; 811 public: 812 this(size_t size) 813 in { 814 assert(size, "size must be larger than 0"); 815 } 816 do { 817 chunk_ = new ubyte[](size); 818 popFront(); 819 } 820 821 @property bool empty() const { 822 return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job 823 } 824 @property nothrow ubyte[] front() { return chunk_; } 825 void popFront() { 826 enforce(!empty, "Cannot call popFront on empty range"); 827 chunk_ = stdin.rawRead(chunk_); 828 } 829 } 830 831 import core.sys.windows.windows; 832 static: 833 834 T[] rawRead(T)(T[] buf) { 835 uint bytesRead; 836 auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); 837 838 if (!result) { 839 auto err = GetLastError(); 840 if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input 841 return buf[0..0]; 842 // Some other error, throw it 843 844 char* buffer; 845 scope(exit) LocalFree(buffer); 846 847 // FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 848 // FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 849 FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null); 850 throw new Exception(to!string(buffer)); 851 } 852 enforce(!(bytesRead % T.sizeof), "I/O error"); 853 return buf[0..bytesRead / T.sizeof]; 854 } 855 856 auto byChunk(size_t sz) { return ByChunk(sz); } 857 858 void close() { 859 std.stdio.stdin.close; 860 } 861 } 862 } 863 864 /// The main interface with the web request 865 class Cgi { 866 public: 867 /// the methods a request can be 868 enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work 869 // these are defined in the standard, but idk if they are useful for anything 870 OPTIONS, TRACE, CONNECT, 871 // These seem new, I have only recently seen them 872 PATCH, MERGE, 873 // this is an extension for when the method is not specified and you want to assume 874 CommandLine } 875 876 877 /+ 878 879 ubyte[] perRequestMemoryPool; 880 void[] perRequestMemoryPoolWithPointers; 881 // might want to just slice the buffer itself too when we happened to have gotten a full request inside it and don't need to decode 882 // then the buffer also can be recycled if it is set. 883 884 // we might also be able to set memory recyclable true by default, but then the property getters set it to false. but not all the things are property getters. but realistically anything except benchmarks are gonna get something lol so meh. 885 886 /+ 887 struct VariableCollection { 888 string[] opIndex(string name) { 889 890 } 891 } 892 893 /++ 894 Call this to indicate that you've not retained any reference to the request-local memory (including all strings returned from the Cgi object) outside the request (you can .idup anything you need to store) and it is thus free to be freed or reused by another request. 895 896 Most handlers should be able to call this; retaining memory is the exception in any cgi program, but since I can't prove it from inside the library, it plays it safe and lets the GC manage it unless you opt into this behavior. All cgi.d functions will duplicate strings if needed (e.g. session ids from cookies) so unless you're doing something yourself, this should be ok. 897 898 History: 899 Added 900 +/ 901 public void recycleMemory() { 902 903 } 904 +/ 905 906 907 /++ 908 Cgi provides a per-request memory pool 909 910 +/ 911 void[] allocateMemory(size_t nBytes) { 912 913 } 914 915 /// ditto 916 void[] reallocateMemory(void[] old, size_t nBytes) { 917 918 } 919 920 /// ditto 921 void freeMemory(void[] memory) { 922 923 } 924 +/ 925 926 927 /* 928 import core.runtime; 929 auto args = Runtime.args(); 930 931 we can call the app a few ways: 932 933 1) set up the environment variables and call the app (manually simulating CGI) 934 2) simulate a call automatically: 935 ./app method 'uri' 936 937 for example: 938 ./app get /path?arg arg2=something 939 940 Anything on the uri is treated as query string etc 941 942 on get method, further args are appended to the query string (encoded automatically) 943 on post method, further args are done as post 944 945 946 @name means import from file "name". if name == -, it uses stdin 947 (so info=@- means set info to the value of stdin) 948 949 950 Other arguments include: 951 --cookie name=value (these are all concated together) 952 --header 'X-Something: cool' 953 --referrer 'something' 954 --port 80 955 --remote-address some.ip.address.here 956 --https yes 957 --user-agent 'something' 958 --userpass 'user:pass' 959 --authorization 'Basic base64encoded_user:pass' 960 --accept 'content' // FIXME: better example 961 --last-event-id 'something' 962 --host 'something.com' 963 --session name=value (these are added to a mock session, changes to the session are printed out as dummy response headers) 964 965 Non-simulation arguments: 966 --port xxx listening port for non-cgi things (valid for the cgi interfaces) 967 --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`. 968 969 */ 970 971 /** Initializes it with command line arguments (for easy testing) */ 972 this(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) { 973 rawDataOutput = _rawDataOutput; 974 // these are all set locally so the loop works 975 // without triggering errors in dmd 2.064 976 // we go ahead and set them at the end of it to the this version 977 int port; 978 string referrer; 979 string remoteAddress; 980 string userAgent; 981 string authorization; 982 string origin; 983 string accept; 984 string lastEventId; 985 bool https; 986 string host; 987 RequestMethod requestMethod; 988 string requestUri; 989 string pathInfo; 990 string queryString; 991 992 bool lookingForMethod; 993 bool lookingForUri; 994 string nextArgIs; 995 996 string _cookie; 997 string _queryString; 998 string[][string] _post; 999 string[string] _headers; 1000 1001 string[] breakUp(string s) { 1002 string k, v; 1003 auto idx = s.indexOf("="); 1004 if(idx == -1) { 1005 k = s; 1006 } else { 1007 k = s[0 .. idx]; 1008 v = s[idx + 1 .. $]; 1009 } 1010 1011 return [k, v]; 1012 } 1013 1014 lookingForMethod = true; 1015 1016 scriptName = args[0]; 1017 scriptFileName = args[0]; 1018 1019 environmentVariables = cast(const) environment.toAA; 1020 1021 foreach(arg; args[1 .. $]) { 1022 if(arg.startsWith("--")) { 1023 nextArgIs = arg[2 .. $]; 1024 } else if(nextArgIs.length) { 1025 if (nextArgIs == "cookie") { 1026 auto info = breakUp(arg); 1027 if(_cookie.length) 1028 _cookie ~= "; "; 1029 _cookie ~= encodeUriComponent(info[0]) ~ "=" ~ encodeUriComponent(info[1]); 1030 } 1031 if (nextArgIs == "session") { 1032 auto info = breakUp(arg); 1033 _commandLineSession[info[0]] = info[1]; 1034 } 1035 1036 else if (nextArgIs == "port") { 1037 port = to!int(arg); 1038 } 1039 else if (nextArgIs == "referrer") { 1040 referrer = arg; 1041 } 1042 else if (nextArgIs == "remote-address") { 1043 remoteAddress = arg; 1044 } 1045 else if (nextArgIs == "user-agent") { 1046 userAgent = arg; 1047 } 1048 else if (nextArgIs == "authorization") { 1049 authorization = arg; 1050 } 1051 else if (nextArgIs == "userpass") { 1052 authorization = "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup; 1053 } 1054 else if (nextArgIs == "origin") { 1055 origin = arg; 1056 } 1057 else if (nextArgIs == "accept") { 1058 accept = arg; 1059 } 1060 else if (nextArgIs == "last-event-id") { 1061 lastEventId = arg; 1062 } 1063 else if (nextArgIs == "https") { 1064 if(arg == "yes") 1065 https = true; 1066 } 1067 else if (nextArgIs == "header") { 1068 string thing, other; 1069 auto idx = arg.indexOf(":"); 1070 if(idx == -1) 1071 throw new Exception("need a colon in a http header"); 1072 thing = arg[0 .. idx]; 1073 other = arg[idx + 1.. $]; 1074 _headers[thing.strip.toLower()] = other.strip; 1075 } 1076 else if (nextArgIs == "host") { 1077 host = arg; 1078 } 1079 // else 1080 // skip, we don't know it but that's ok, it might be used elsewhere so no error 1081 1082 nextArgIs = null; 1083 } else if(lookingForMethod) { 1084 lookingForMethod = false; 1085 lookingForUri = true; 1086 1087 if(arg.asLowerCase().equal("commandline")) 1088 requestMethod = RequestMethod.CommandLine; 1089 else 1090 requestMethod = to!RequestMethod(arg.toUpper()); 1091 } else if(lookingForUri) { 1092 lookingForUri = false; 1093 1094 requestUri = arg; 1095 1096 auto idx = arg.indexOf("?"); 1097 if(idx == -1) 1098 pathInfo = arg; 1099 else { 1100 pathInfo = arg[0 .. idx]; 1101 _queryString = arg[idx + 1 .. $]; 1102 } 1103 } else { 1104 // it is an argument of some sort 1105 if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { 1106 auto parts = breakUp(arg); 1107 _post[parts[0]] ~= parts[1]; 1108 allPostNamesInOrder ~= parts[0]; 1109 allPostValuesInOrder ~= parts[1]; 1110 } else { 1111 if(_queryString.length) 1112 _queryString ~= "&"; 1113 auto parts = breakUp(arg); 1114 _queryString ~= encodeUriComponent(parts[0]) ~ "=" ~ encodeUriComponent(parts[1]); 1115 } 1116 } 1117 } 1118 1119 acceptsGzip = false; 1120 keepAliveRequested = false; 1121 requestHeaders = cast(immutable) _headers; 1122 1123 cookie = _cookie; 1124 cookiesArray = getCookieArray(); 1125 cookies = keepLastOf(cookiesArray); 1126 1127 queryString = _queryString; 1128 getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 1129 get = keepLastOf(getArray); 1130 1131 postArray = cast(immutable) _post; 1132 post = keepLastOf(_post); 1133 1134 // FIXME 1135 filesArray = null; 1136 files = null; 1137 1138 isCalledWithCommandLineArguments = true; 1139 1140 this.port = port; 1141 this.referrer = referrer; 1142 this.remoteAddress = remoteAddress; 1143 this.userAgent = userAgent; 1144 this.authorization = authorization; 1145 this.origin = origin; 1146 this.accept = accept; 1147 this.lastEventId = lastEventId; 1148 this.https = https; 1149 this.host = host; 1150 this.requestMethod = requestMethod; 1151 this.requestUri = requestUri; 1152 this.pathInfo = pathInfo; 1153 this.queryString = queryString; 1154 this.postBody = null; 1155 this.requestContentType = null; 1156 } 1157 1158 private { 1159 string[] allPostNamesInOrder; 1160 string[] allPostValuesInOrder; 1161 string[] allGetNamesInOrder; 1162 string[] allGetValuesInOrder; 1163 } 1164 1165 CgiConnectionHandle getOutputFileHandle() { 1166 return _outputFileHandle; 1167 } 1168 1169 CgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE; 1170 1171 /** Initializes it using a CGI or CGI-like interface */ 1172 this(long maxContentLength = defaultMaxContentLength, 1173 // use this to override the environment variable listing 1174 in string[string] env = null, 1175 // and this should return a chunk of data. return empty when done 1176 const(ubyte)[] delegate() readdata = null, 1177 // finally, use this to do custom output if needed 1178 void delegate(const(ubyte)[]) _rawDataOutput = null, 1179 // to flush teh custom output 1180 void delegate() _flush = null 1181 ) 1182 { 1183 1184 // these are all set locally so the loop works 1185 // without triggering errors in dmd 2.064 1186 // we go ahead and set them at the end of it to the this version 1187 int port; 1188 string referrer; 1189 string remoteAddress; 1190 string userAgent; 1191 string authorization; 1192 string origin; 1193 string accept; 1194 string lastEventId; 1195 bool https; 1196 string host; 1197 RequestMethod requestMethod; 1198 string requestUri; 1199 string pathInfo; 1200 string queryString; 1201 1202 1203 1204 isCalledWithCommandLineArguments = false; 1205 rawDataOutput = _rawDataOutput; 1206 flushDelegate = _flush; 1207 auto getenv = delegate string(string var) { 1208 if(env is null) 1209 return std.process.environment.get(var); 1210 auto e = var in env; 1211 if(e is null) 1212 return null; 1213 return *e; 1214 }; 1215 1216 environmentVariables = env is null ? 1217 cast(const) environment.toAA : 1218 env; 1219 1220 // fetching all the request headers 1221 string[string] requestHeadersHere; 1222 foreach(k, v; env is null ? cast(const) environment.toAA() : env) { 1223 if(k.startsWith("HTTP_")) { 1224 requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v; 1225 } 1226 } 1227 1228 this.requestHeaders = assumeUnique(requestHeadersHere); 1229 1230 requestUri = getenv("REQUEST_URI"); 1231 1232 cookie = getenv("HTTP_COOKIE"); 1233 cookiesArray = getCookieArray(); 1234 cookies = keepLastOf(cookiesArray); 1235 1236 referrer = getenv("HTTP_REFERER"); 1237 userAgent = getenv("HTTP_USER_AGENT"); 1238 remoteAddress = getenv("REMOTE_ADDR"); 1239 host = getenv("HTTP_HOST"); 1240 pathInfo = getenv("PATH_INFO"); 1241 1242 queryString = getenv("QUERY_STRING"); 1243 scriptName = getenv("SCRIPT_NAME"); 1244 { 1245 import core.runtime; 1246 auto sfn = getenv("SCRIPT_FILENAME"); 1247 scriptFileName = sfn.length ? sfn : (Runtime.args.length ? Runtime.args[0] : null); 1248 } 1249 1250 bool iis = false; 1251 1252 // Because IIS doesn't pass requestUri, we simulate it here if it's empty. 1253 if(requestUri.length == 0) { 1254 // IIS sometimes includes the script name as part of the path info - we don't want that 1255 if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName)) 1256 pathInfo = pathInfo[scriptName.length .. $]; 1257 1258 requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : ""); 1259 1260 iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339 1261 1262 // FIXME: this works for apache and iis... but what about others? 1263 } 1264 1265 1266 auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 1267 getArray = assumeUnique(ugh); 1268 get = keepLastOf(getArray); 1269 1270 1271 // NOTE: on shitpache, you need to specifically forward this 1272 authorization = getenv("HTTP_AUTHORIZATION"); 1273 // this is a hack because Apache is a shitload of fuck and 1274 // refuses to send the real header to us. Compatible 1275 // programs should send both the standard and X- versions 1276 1277 // NOTE: if you have access to .htaccess or httpd.conf, you can make this 1278 // unnecessary with mod_rewrite, so it is commented 1279 1280 //if(authorization.length == 0) // if the std is there, use it 1281 // authorization = getenv("HTTP_X_AUTHORIZATION"); 1282 1283 // the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong 1284 if(getenv("SERVER_PORT").length && getenv("REDIRECT_HTTPS") != "on") 1285 port = to!int(getenv("SERVER_PORT")); 1286 else 1287 port = 0; // this was probably called from the command line 1288 1289 auto ae = getenv("HTTP_ACCEPT_ENCODING"); 1290 if(ae.length && ae.indexOf("gzip") != -1) 1291 acceptsGzip = true; 1292 1293 accept = getenv("HTTP_ACCEPT"); 1294 lastEventId = getenv("HTTP_LAST_EVENT_ID"); 1295 1296 auto ka = getenv("HTTP_CONNECTION"); 1297 if(ka.length && ka.asLowerCase().canFind("keep-alive")) 1298 keepAliveRequested = true; 1299 1300 auto or = getenv("HTTP_ORIGIN"); 1301 origin = or; 1302 1303 auto rm = getenv("REQUEST_METHOD"); 1304 if(rm.length) 1305 requestMethod = to!RequestMethod(getenv("REQUEST_METHOD")); 1306 else 1307 requestMethod = RequestMethod.CommandLine; 1308 1309 // 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. 1310 https = (getenv("HTTPS") == "on" || getenv("REDIRECT_HTTPS") == "on"); 1311 1312 // FIXME: DOCUMENT_ROOT? 1313 1314 // FIXME: what about PUT? 1315 if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { 1316 version(preserveData) // a hack to make forwarding simpler 1317 immutable(ubyte)[] data; 1318 size_t amountReceived = 0; 1319 auto contentType = getenv("CONTENT_TYPE"); 1320 1321 // FIXME: is this ever not going to be set? I guess it depends 1322 // on if the server de-chunks and buffers... seems like it has potential 1323 // to be slow if they did that. The spec says it is always there though. 1324 // And it has worked reliably for me all year in the live environment, 1325 // but some servers might be different. 1326 auto cls = getenv("CONTENT_LENGTH"); 1327 auto contentLength = to!size_t(cls.length ? cls : "0"); 1328 1329 immutable originalContentLength = contentLength; 1330 if(contentLength) { 1331 if(maxContentLength > 0 && contentLength > maxContentLength) { 1332 setResponseStatus("413 Request entity too large"); 1333 write("You tried to upload a file that is too large."); 1334 close(); 1335 throw new Exception("POST too large"); 1336 } 1337 prepareForIncomingDataChunks(contentType, contentLength); 1338 1339 1340 int processChunk(in ubyte[] chunk) { 1341 if(chunk.length > contentLength) { 1342 handleIncomingDataChunk(chunk[0..contentLength]); 1343 amountReceived += contentLength; 1344 contentLength = 0; 1345 return 1; 1346 } else { 1347 handleIncomingDataChunk(chunk); 1348 contentLength -= chunk.length; 1349 amountReceived += chunk.length; 1350 } 1351 if(contentLength == 0) 1352 return 1; 1353 1354 onRequestBodyDataReceived(amountReceived, originalContentLength); 1355 return 0; 1356 } 1357 1358 1359 if(readdata is null) { 1360 foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096)) 1361 if(processChunk(chunk)) 1362 break; 1363 } else { 1364 // we have a custom data source.. 1365 auto chunk = readdata(); 1366 while(chunk.length) { 1367 if(processChunk(chunk)) 1368 break; 1369 chunk = readdata(); 1370 } 1371 } 1372 1373 onRequestBodyDataReceived(amountReceived, originalContentLength); 1374 postArray = assumeUnique(pps._post); 1375 filesArray = assumeUnique(pps._files); 1376 files = keepLastOf(filesArray); 1377 post = keepLastOf(postArray); 1378 this.postBody = pps.postBody; 1379 this.requestContentType = contentType; 1380 cleanUpPostDataState(); 1381 } 1382 1383 version(preserveData) 1384 originalPostData = data; 1385 } 1386 // fixme: remote_user script name 1387 1388 1389 this.port = port; 1390 this.referrer = referrer; 1391 this.remoteAddress = remoteAddress; 1392 this.userAgent = userAgent; 1393 this.authorization = authorization; 1394 this.origin = origin; 1395 this.accept = accept; 1396 this.lastEventId = lastEventId; 1397 this.https = https; 1398 this.host = host; 1399 this.requestMethod = requestMethod; 1400 this.requestUri = requestUri; 1401 this.pathInfo = pathInfo; 1402 this.queryString = queryString; 1403 } 1404 1405 /// Cleans up any temporary files. Do not use the object 1406 /// after calling this. 1407 /// 1408 /// NOTE: it is called automatically by GenericMain 1409 // FIXME: this should be called if the constructor fails too, if it has created some garbage... 1410 void dispose() { 1411 foreach(file; files) { 1412 if(!file.contentInMemory) 1413 if(std.file.exists(file.contentFilename)) 1414 std.file.remove(file.contentFilename); 1415 } 1416 } 1417 1418 private { 1419 struct PostParserState { 1420 string contentType; 1421 string boundary; 1422 string localBoundary; // the ones used at the end or something lol 1423 bool isMultipart; 1424 bool needsSavedBody; 1425 1426 ulong expectedLength; 1427 ulong contentConsumed; 1428 immutable(ubyte)[] buffer; 1429 1430 // multipart parsing state 1431 int whatDoWeWant; 1432 bool weHaveAPart; 1433 string[] thisOnesHeaders; 1434 immutable(ubyte)[] thisOnesData; 1435 1436 string postBody; 1437 1438 UploadedFile piece; 1439 bool isFile = false; 1440 1441 size_t memoryCommitted; 1442 1443 // do NOT keep mutable references to these anywhere! 1444 // I assume they are unique in the constructor once we're all done getting data. 1445 string[][string] _post; 1446 UploadedFile[][string] _files; 1447 } 1448 1449 PostParserState pps; 1450 } 1451 1452 /// This represents a file the user uploaded via a POST request. 1453 static struct UploadedFile { 1454 /// If you want to create one of these structs for yourself from some data, 1455 /// use this function. 1456 static UploadedFile fromData(immutable(void)[] data, string name = null) { 1457 Cgi.UploadedFile f; 1458 f.filename = name; 1459 f.content = cast(immutable(ubyte)[]) data; 1460 f.contentInMemory = true; 1461 return f; 1462 } 1463 1464 string name; /// The name of the form element. 1465 string filename; /// The filename the user set. 1466 string contentType; /// The MIME type the user's browser reported. (Not reliable.) 1467 1468 /** 1469 For small files, cgi.d will buffer the uploaded file in memory, and make it 1470 directly accessible to you through the content member. I find this very convenient 1471 and somewhat efficient, since it can avoid hitting the disk entirely. (I 1472 often want to inspect and modify the file anyway!) 1473 1474 I find the file is very large, it is undesirable to eat that much memory just 1475 for a file buffer. In those cases, if you pass a large enough value for maxContentLength 1476 to the constructor so they are accepted, cgi.d will write the content to a temporary 1477 file that you can re-read later. 1478 1479 You can override this behavior by subclassing Cgi and overriding the protected 1480 handlePostChunk method. Note that the object is not initialized when you 1481 write that method - the http headers are available, but the cgi.post method 1482 is not. You may parse the file as it streams in using this method. 1483 1484 1485 Anyway, if the file is small enough to be in memory, contentInMemory will be 1486 set to true, and the content is available in the content member. 1487 1488 If not, contentInMemory will be set to false, and the content saved in a file, 1489 whose name will be available in the contentFilename member. 1490 1491 1492 Tip: if you know you are always dealing with small files, and want the convenience 1493 of ignoring this member, construct Cgi with a small maxContentLength. Then, if 1494 a large file comes in, it simply throws an exception (and HTTP error response) 1495 instead of trying to handle it. 1496 1497 The default value of maxContentLength in the constructor is for small files. 1498 */ 1499 bool contentInMemory = true; // the default ought to always be true 1500 immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true 1501 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. 1502 1503 /// 1504 ulong fileSize() const { 1505 if(contentInMemory) 1506 return content.length; 1507 import std.file; 1508 return std.file.getSize(contentFilename); 1509 1510 } 1511 1512 /// 1513 void writeToFile(string filenameToSaveTo) const { 1514 import std.file; 1515 if(contentInMemory) 1516 std.file.write(filenameToSaveTo, content); 1517 else 1518 std.file.rename(contentFilename, filenameToSaveTo); 1519 } 1520 } 1521 1522 // given a content type and length, decide what we're going to do with the data.. 1523 protected void prepareForIncomingDataChunks(string contentType, ulong contentLength) { 1524 pps.expectedLength = contentLength; 1525 1526 auto terminator = contentType.indexOf(";"); 1527 if(terminator == -1) 1528 terminator = contentType.length; 1529 1530 pps.contentType = contentType[0 .. terminator]; 1531 auto b = contentType[terminator .. $]; 1532 if(b.length) { 1533 auto idx = b.indexOf("boundary="); 1534 if(idx != -1) { 1535 pps.boundary = b[idx + "boundary=".length .. $]; 1536 pps.localBoundary = "\r\n--" ~ pps.boundary; 1537 } 1538 } 1539 1540 // while a content type SHOULD be sent according to the RFC, it is 1541 // not required. We're told we SHOULD guess by looking at the content 1542 // but it seems to me that this only happens when it is urlencoded. 1543 if(pps.contentType == "application/x-www-form-urlencoded" || pps.contentType == "") { 1544 pps.isMultipart = false; 1545 pps.needsSavedBody = false; 1546 } else if(pps.contentType == "multipart/form-data") { 1547 pps.isMultipart = true; 1548 enforce(pps.boundary.length, "no boundary"); 1549 } else if(pps.contentType == "text/xml") { // FIXME: could this be special and load the post params 1550 // save the body so the application can handle it 1551 pps.isMultipart = false; 1552 pps.needsSavedBody = true; 1553 } else if(pps.contentType == "application/json") { // FIXME: this could prolly try to load post params too 1554 // save the body so the application can handle it 1555 pps.needsSavedBody = true; 1556 pps.isMultipart = false; 1557 } else { 1558 // the rest is 100% handled by the application. just save the body and send it to them 1559 pps.needsSavedBody = true; 1560 pps.isMultipart = false; 1561 } 1562 } 1563 1564 // handles streaming POST data. If you handle some other content type, you should 1565 // override this. If the data isn't the content type you want, you ought to call 1566 // super.handleIncomingDataChunk so regular forms and files still work. 1567 1568 // FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the 1569 // file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network 1570 // input anyway, so I'm not going to get too worked up about it right now. 1571 protected void handleIncomingDataChunk(const(ubyte)[] chunk) { 1572 if(chunk.length == 0) 1573 return; 1574 assert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so 1575 // if we're passed big chunks, it might throw unnecessarily. 1576 // just pass it smaller chunks at a time. 1577 if(pps.isMultipart) { 1578 // multipart/form-data 1579 1580 1581 // FIXME: this might want to be factored out and factorized 1582 // need to make sure the stream hooks actually work. 1583 void pieceHasNewContent() { 1584 // we just grew the piece's buffer. Do we have to switch to file backing? 1585 if(pps.piece.contentInMemory) { 1586 if(pps.piece.content.length <= 10 * 1024 * 1024) 1587 // meh, I'm ok with it. 1588 return; 1589 else { 1590 // this is too big. 1591 if(!pps.isFile) 1592 throw new Exception("Request entity too large"); // a variable this big is kinda ridiculous, just reject it. 1593 else { 1594 // a file this large is probably acceptable though... let's use a backing file. 1595 pps.piece.contentInMemory = false; 1596 // FIXME: say... how do we intend to delete these things? cgi.dispose perhaps. 1597 1598 int count = 0; 1599 pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); 1600 // odds are this loop will never be entered, but we want it just in case. 1601 while(std.file.exists(pps.piece.contentFilename)) { 1602 count++; 1603 pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); 1604 } 1605 // I hope this creates the file pretty quickly, or the loop might be useless... 1606 // FIXME: maybe I should write some kind of custom transaction here. 1607 std.file.write(pps.piece.contentFilename, pps.piece.content); 1608 1609 pps.piece.content = null; 1610 } 1611 } 1612 } else { 1613 // it's already in a file, so just append it to what we have 1614 if(pps.piece.content.length) { 1615 // FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk... 1616 std.file.append(pps.piece.contentFilename, pps.piece.content); 1617 pps.piece.content = null; 1618 } 1619 } 1620 } 1621 1622 1623 void commitPart() { 1624 if(!pps.weHaveAPart) 1625 return; 1626 1627 pieceHasNewContent(); // be sure the new content is handled every time 1628 1629 if(pps.isFile) { 1630 // I'm not sure if other environments put files in post or not... 1631 // I used to not do it, but I think I should, since it is there... 1632 pps._post[pps.piece.name] ~= pps.piece.filename; 1633 pps._files[pps.piece.name] ~= pps.piece; 1634 1635 allPostNamesInOrder ~= pps.piece.name; 1636 allPostValuesInOrder ~= pps.piece.filename; 1637 } else { 1638 pps._post[pps.piece.name] ~= cast(string) pps.piece.content; 1639 1640 allPostNamesInOrder ~= pps.piece.name; 1641 allPostValuesInOrder ~= cast(string) pps.piece.content; 1642 } 1643 1644 /* 1645 stderr.writeln("RECEIVED: ", pps.piece.name, "=", 1646 pps.piece.content.length < 1000 1647 ? 1648 to!string(pps.piece.content) 1649 : 1650 "too long"); 1651 */ 1652 1653 // FIXME: the limit here 1654 pps.memoryCommitted += pps.piece.content.length; 1655 1656 pps.weHaveAPart = false; 1657 pps.whatDoWeWant = 1; 1658 pps.thisOnesHeaders = null; 1659 pps.thisOnesData = null; 1660 1661 pps.piece = UploadedFile.init; 1662 pps.isFile = false; 1663 } 1664 1665 void acceptChunk() { 1666 pps.buffer ~= chunk; 1667 chunk = null; // we've consumed it into the buffer, so keeping it just brings confusion 1668 } 1669 1670 immutable(ubyte)[] consume(size_t howMuch) { 1671 pps.contentConsumed += howMuch; 1672 auto ret = pps.buffer[0 .. howMuch]; 1673 pps.buffer = pps.buffer[howMuch .. $]; 1674 return ret; 1675 } 1676 1677 dataConsumptionLoop: do { 1678 switch(pps.whatDoWeWant) { 1679 default: assert(0); 1680 case 0: 1681 acceptChunk(); 1682 // the format begins with two extra leading dashes, then we should be at the boundary 1683 if(pps.buffer.length < 2) 1684 return; 1685 assert(pps.buffer[0] == '-', "no leading dash"); 1686 consume(1); 1687 assert(pps.buffer[0] == '-', "no second leading dash"); 1688 consume(1); 1689 1690 pps.whatDoWeWant = 1; 1691 goto case 1; 1692 /* fallthrough */ 1693 case 1: // looking for headers 1694 // here, we should be lined up right at the boundary, which is followed by a \r\n 1695 1696 // want to keep the buffer under control in case we're under attack 1697 //stderr.writeln("here once"); 1698 //if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really.... 1699 // throw new Exception("wtf is up with the huge mime part headers"); 1700 1701 acceptChunk(); 1702 1703 if(pps.buffer.length < pps.boundary.length) 1704 return; // not enough data, since there should always be a boundary here at least 1705 1706 if(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) { 1707 assert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \r\n 1708 // we *should* be at the end here! 1709 assert(pps.buffer[0] == '-'); 1710 consume(1); 1711 assert(pps.buffer[0] == '-'); 1712 consume(1); 1713 1714 // the message is terminated by --BOUNDARY--\r\n (after a \r\n leading to the boundary) 1715 assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, 1716 "not lined up on boundary " ~ pps.boundary); 1717 consume(pps.boundary.length); 1718 1719 assert(pps.buffer[0] == '-'); 1720 consume(1); 1721 assert(pps.buffer[0] == '-'); 1722 consume(1); 1723 1724 assert(pps.buffer[0] == '\r'); 1725 consume(1); 1726 assert(pps.buffer[0] == '\n'); 1727 consume(1); 1728 1729 assert(pps.buffer.length == 0); 1730 assert(pps.contentConsumed == pps.expectedLength); 1731 break dataConsumptionLoop; // we're done! 1732 } else { 1733 // we're not done yet. We should be lined up on a boundary. 1734 1735 // But, we want to ensure the headers are here before we consume anything! 1736 auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); 1737 if(headerEndLocation == -1) 1738 return; // they *should* all be here, so we can handle them all at once. 1739 1740 assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, 1741 "not lined up on boundary " ~ pps.boundary); 1742 1743 consume(pps.boundary.length); 1744 // the boundary is always followed by a \r\n 1745 assert(pps.buffer[0] == '\r'); 1746 consume(1); 1747 assert(pps.buffer[0] == '\n'); 1748 consume(1); 1749 } 1750 1751 // re-running since by consuming the boundary, we invalidate the old index. 1752 auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); 1753 assert(headerEndLocation >= 0, "no header"); 1754 auto thisOnesHeaders = pps.buffer[0..headerEndLocation]; 1755 1756 consume(headerEndLocation + 4); // The +4 is the \r\n\r\n that caps it off 1757 1758 pps.thisOnesHeaders = split(cast(string) thisOnesHeaders, "\r\n"); 1759 1760 // now we'll parse the headers 1761 foreach(h; pps.thisOnesHeaders) { 1762 auto p = h.indexOf(":"); 1763 assert(p != -1, "no colon in header, got " ~ to!string(pps.thisOnesHeaders)); 1764 string hn = h[0..p]; 1765 string hv = h[p+2..$]; 1766 1767 switch(hn.toLower) { 1768 default: assert(0); 1769 case "content-disposition": 1770 auto info = hv.split("; "); 1771 foreach(i; info[1..$]) { // skipping the form-data 1772 auto o = i.split("="); // FIXME 1773 string pn = o[0]; 1774 string pv = o[1][1..$-1]; 1775 1776 if(pn == "name") { 1777 pps.piece.name = pv; 1778 } else if (pn == "filename") { 1779 pps.piece.filename = pv; 1780 pps.isFile = true; 1781 } 1782 } 1783 break; 1784 case "content-type": 1785 pps.piece.contentType = hv; 1786 break; 1787 } 1788 } 1789 1790 pps.whatDoWeWant++; // move to the next step - the data 1791 break; 1792 case 2: 1793 // when we get here, pps.buffer should contain our first chunk of data 1794 1795 if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much 1796 throw new Exception("wtf is up with the huge mime part buffer"); 1797 1798 acceptChunk(); 1799 1800 // so the trick is, we want to process all the data up to the boundary, 1801 // but what if the chunk's end cuts the boundary off? If we're unsure, we 1802 // want to wait for the next chunk. We start by looking for the whole boundary 1803 // in the buffer somewhere. 1804 1805 auto boundaryLocation = locationOf(pps.buffer, pps.localBoundary); 1806 // assert(boundaryLocation != -1, "should have seen "~to!string(cast(ubyte[]) pps.localBoundary)~" in " ~ to!string(pps.buffer)); 1807 if(boundaryLocation != -1) { 1808 // this is easy - we can see it in it's entirety! 1809 1810 pps.piece.content ~= consume(boundaryLocation); 1811 1812 assert(pps.buffer[0] == '\r'); 1813 consume(1); 1814 assert(pps.buffer[0] == '\n'); 1815 consume(1); 1816 assert(pps.buffer[0] == '-'); 1817 consume(1); 1818 assert(pps.buffer[0] == '-'); 1819 consume(1); 1820 // the boundary here is always preceded by \r\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off. 1821 pps.weHaveAPart = true; 1822 pps.whatDoWeWant = 1; // back to getting headers for the next part 1823 1824 commitPart(); // we're done here 1825 } else { 1826 // we can't see the whole thing, but what if there's a partial boundary? 1827 1828 enforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line... 1829 assert(pps.localBoundary.length > 1); // should already be sane but just in case 1830 bool potentialBoundaryFound = false; 1831 1832 boundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) { 1833 // we grow the boundary a bit each time. If we think it looks the 1834 // same, better pull another chunk to be sure it's not the end. 1835 // Starting small because exiting the loop early is desirable, since 1836 // we're not keeping any ambiguity and 1 / 256 chance of exiting is 1837 // the best we can do. 1838 if(a > pps.buffer.length) 1839 break; // FIXME: is this right? 1840 assert(a <= pps.buffer.length); 1841 assert(a > 0); 1842 if(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) { 1843 // ok, there *might* be a boundary here, so let's 1844 // not treat the end as data yet. The rest is good to 1845 // use though, since if there was a boundary there, we'd 1846 // have handled it up above after locationOf. 1847 1848 pps.piece.content ~= pps.buffer[0 .. $ - a]; 1849 consume(pps.buffer.length - a); 1850 pieceHasNewContent(); 1851 potentialBoundaryFound = true; 1852 break boundaryCheck; 1853 } 1854 } 1855 1856 if(!potentialBoundaryFound) { 1857 // we can consume the whole thing 1858 pps.piece.content ~= pps.buffer; 1859 pieceHasNewContent(); 1860 consume(pps.buffer.length); 1861 } else { 1862 // we found a possible boundary, but there was 1863 // insufficient data to be sure. 1864 assert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]); 1865 1866 return; // wait for the next chunk. 1867 } 1868 } 1869 } 1870 } while(pps.buffer.length); 1871 1872 // btw all boundaries except the first should have a \r\n before them 1873 } else { 1874 // application/x-www-form-urlencoded and application/json 1875 1876 // not using maxContentLength because that might be cranked up to allow 1877 // large file uploads. We can handle them, but a huge post[] isn't any good. 1878 if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough 1879 throw new Exception("wtf is up with such a gigantic form submission????"); 1880 1881 pps.buffer ~= chunk; 1882 1883 // simple handling, but it works... until someone bombs us with gigabytes of crap at least... 1884 if(pps.buffer.length == pps.expectedLength) { 1885 if(pps.needsSavedBody) 1886 pps.postBody = cast(string) pps.buffer; 1887 else 1888 pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder); 1889 version(preserveData) 1890 originalPostData = pps.buffer; 1891 } else { 1892 // just for debugging 1893 } 1894 } 1895 } 1896 1897 protected void cleanUpPostDataState() { 1898 pps = PostParserState.init; 1899 } 1900 1901 /// you can override this function to somehow react 1902 /// to an upload in progress. 1903 /// 1904 /// Take note that parts of the CGI object is not yet 1905 /// initialized! Stuff from HTTP headers, including get[], is usable. 1906 /// But, none of post[] is usable, and you cannot write here. That's 1907 /// why this method is const - mutating the object won't do much anyway. 1908 /// 1909 /// My idea here was so you can output a progress bar or 1910 /// something to a cooperative client (see arsd.rtud for a potential helper) 1911 /// 1912 /// The default is to do nothing. Subclass cgi and use the 1913 /// CustomCgiMain mixin to do something here. 1914 void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const { 1915 // This space intentionally left blank. 1916 } 1917 1918 /// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source. 1919 /// *closeConnection will be set to true if you should close the connection after handling this request 1920 this(BufferedInputRange ir, bool* closeConnection) { 1921 isCalledWithCommandLineArguments = false; 1922 import al = std.algorithm; 1923 1924 immutable(ubyte)[] data; 1925 1926 void rdo(const(ubyte)[] d) { 1927 //import std.stdio; writeln(d); 1928 sendAll(ir.source, d); 1929 } 1930 1931 auto ira = ir.source.remoteAddress(); 1932 auto irLocalAddress = ir.source.localAddress(); 1933 1934 ushort port = 80; 1935 if(auto ia = cast(InternetAddress) irLocalAddress) { 1936 port = ia.port; 1937 } else if(auto ia = cast(Internet6Address) irLocalAddress) { 1938 port = ia.port; 1939 } 1940 1941 // that check for UnixAddress is to work around a Phobos bug 1942 // see: https://github.com/dlang/phobos/pull/7383 1943 // but this might be more useful anyway tbh for this case 1944 version(Posix) 1945 this(ir, ira is null ? null : cast(UnixAddress) ira ? "unix:" : ira.toString(), port, 0, false, &rdo, null, closeConnection); 1946 else 1947 this(ir, ira is null ? null : ira.toString(), port, 0, false, &rdo, null, closeConnection); 1948 } 1949 1950 /** 1951 Initializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd. 1952 1953 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 1954 1955 Params: 1956 inputData = the incoming data, including headers and other raw http data. 1957 When the constructor exits, it will leave this range exactly at the start of 1958 the next request on the connection (if there is one). 1959 1960 address = the IP address of the remote user 1961 _port = the port number of the connection 1962 pathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins. 1963 _https = if this connection is encrypted (note that the input data must not actually be encrypted) 1964 _rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http. 1965 _flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire 1966 closeConnection = if the request asks to close the connection, *closeConnection == true. 1967 */ 1968 this( 1969 BufferedInputRange inputData, 1970 // string[] headers, immutable(ubyte)[] data, 1971 string address, ushort _port, 1972 int pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment 1973 bool _https = false, 1974 void delegate(const(ubyte)[]) _rawDataOutput = null, 1975 void delegate() _flush = null, 1976 // this pointer tells if the connection is supposed to be closed after we handle this 1977 bool* closeConnection = null) 1978 { 1979 // these are all set locally so the loop works 1980 // without triggering errors in dmd 2.064 1981 // we go ahead and set them at the end of it to the this version 1982 int port; 1983 string referrer; 1984 string remoteAddress; 1985 string userAgent; 1986 string authorization; 1987 string origin; 1988 string accept; 1989 string lastEventId; 1990 bool https; 1991 string host; 1992 RequestMethod requestMethod; 1993 string requestUri; 1994 string pathInfo; 1995 string queryString; 1996 string scriptName; 1997 string[string] get; 1998 string[][string] getArray; 1999 bool keepAliveRequested; 2000 bool acceptsGzip; 2001 string cookie; 2002 2003 2004 2005 environmentVariables = cast(const) environment.toAA; 2006 2007 idlol = inputData; 2008 2009 isCalledWithCommandLineArguments = false; 2010 2011 https = _https; 2012 port = _port; 2013 2014 rawDataOutput = _rawDataOutput; 2015 flushDelegate = _flush; 2016 nph = true; 2017 2018 remoteAddress = address; 2019 2020 // streaming parser 2021 import al = std.algorithm; 2022 2023 // FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason. 2024 auto idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); 2025 while(idx == -1) { 2026 inputData.popFront(0); 2027 idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); 2028 } 2029 2030 assert(idx != -1); 2031 2032 2033 string contentType = ""; 2034 string[string] requestHeadersHere; 2035 2036 size_t contentLength; 2037 2038 bool isChunked; 2039 2040 { 2041 import core.runtime; 2042 scriptFileName = Runtime.args.length ? Runtime.args[0] : null; 2043 } 2044 2045 2046 int headerNumber = 0; 2047 foreach(line; al.splitter(inputData.front()[0 .. idx], "\r\n")) 2048 if(line.length) { 2049 headerNumber++; 2050 auto header = cast(string) line.idup; 2051 if(headerNumber == 1) { 2052 // request line 2053 auto parts = al.splitter(header, " "); 2054 if(parts.front == "PRI") { 2055 // this is an HTTP/2.0 line - "PRI * HTTP/2.0" - which indicates their payload will follow 2056 // we're going to immediately refuse this, im not interested in implementing http2 (it is unlikely 2057 // to bring me benefit) 2058 throw new HttpVersionNotSupportedException(); 2059 } 2060 requestMethod = to!RequestMethod(parts.front); 2061 parts.popFront(); 2062 requestUri = parts.front; 2063 2064 // FIXME: the requestUri could be an absolute path!!! should I rename it or something? 2065 scriptName = requestUri[0 .. pathInfoStarts]; 2066 2067 auto question = requestUri.indexOf("?"); 2068 if(question == -1) { 2069 queryString = ""; 2070 // FIXME: double check, this might be wrong since it could be url encoded 2071 pathInfo = requestUri[pathInfoStarts..$]; 2072 } else { 2073 queryString = requestUri[question+1..$]; 2074 pathInfo = requestUri[pathInfoStarts..question]; 2075 } 2076 2077 auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); 2078 getArray = cast(string[][string]) assumeUnique(ugh); 2079 2080 if(header.indexOf("HTTP/1.0") != -1) { 2081 http10 = true; 2082 autoBuffer = true; 2083 if(closeConnection) { 2084 // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive) 2085 *closeConnection = true; 2086 } 2087 } 2088 } else { 2089 // other header 2090 auto colon = header.indexOf(":"); 2091 if(colon == -1) 2092 throw new Exception("HTTP headers should have a colon!"); 2093 string name = header[0..colon].toLower; 2094 string value = header[colon+2..$]; // skip the colon and the space 2095 2096 requestHeadersHere[name] = value; 2097 2098 if (name == "accept") { 2099 accept = value; 2100 } 2101 else if (name == "origin") { 2102 origin = value; 2103 } 2104 else if (name == "connection") { 2105 if(value == "close" && closeConnection) 2106 *closeConnection = true; 2107 if(value.asLowerCase().canFind("keep-alive")) { 2108 keepAliveRequested = true; 2109 2110 // on http 1.0, the connection is closed by default, 2111 // but not if they request keep-alive. then we don't close 2112 // anymore - undoing the set above 2113 if(http10 && closeConnection) { 2114 *closeConnection = false; 2115 } 2116 } 2117 } 2118 else if (name == "transfer-encoding") { 2119 if(value == "chunked") 2120 isChunked = true; 2121 } 2122 else if (name == "last-event-id") { 2123 lastEventId = value; 2124 } 2125 else if (name == "authorization") { 2126 authorization = value; 2127 } 2128 else if (name == "content-type") { 2129 contentType = value; 2130 } 2131 else if (name == "content-length") { 2132 contentLength = to!size_t(value); 2133 } 2134 else if (name == "x-forwarded-for") { 2135 remoteAddress = value; 2136 } 2137 else if (name == "x-forwarded-host" || name == "host") { 2138 if(name != "host" || host is null) 2139 host = value; 2140 } 2141 // FIXME: https://tools.ietf.org/html/rfc7239 2142 else if (name == "accept-encoding") { 2143 if(value.indexOf("gzip") != -1) 2144 acceptsGzip = true; 2145 } 2146 else if (name == "user-agent") { 2147 userAgent = value; 2148 } 2149 else if (name == "referer") { 2150 referrer = value; 2151 } 2152 else if (name == "cookie") { 2153 cookie ~= value; 2154 } else if(name == "expect") { 2155 if(value == "100-continue") { 2156 // FIXME we should probably give user code a chance 2157 // to process and reject but that needs to be virtual, 2158 // perhaps part of the CGI redesign. 2159 2160 // FIXME: if size is > max content length it should 2161 // also fail at this point. 2162 _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n"); 2163 2164 // FIXME: let the user write out 103 early hints too 2165 } 2166 } 2167 // else 2168 // ignore it 2169 2170 } 2171 } 2172 2173 inputData.consume(idx + 4); 2174 // done 2175 2176 requestHeaders = assumeUnique(requestHeadersHere); 2177 2178 ByChunkRange dataByChunk; 2179 2180 // reading Content-Length type data 2181 // We need to read up the data we have, and write it out as a chunk. 2182 if(!isChunked) { 2183 dataByChunk = byChunk(inputData, contentLength); 2184 } else { 2185 // chunked requests happen, but not every day. Since we need to know 2186 // the content length (for now, maybe that should change), we'll buffer 2187 // the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes) 2188 auto data = dechunk(inputData); 2189 2190 // set the range here 2191 dataByChunk = byChunk(data); 2192 contentLength = data.length; 2193 } 2194 2195 assert(dataByChunk !is null); 2196 2197 if(contentLength) { 2198 prepareForIncomingDataChunks(contentType, contentLength); 2199 foreach(dataChunk; dataByChunk) { 2200 handleIncomingDataChunk(dataChunk); 2201 } 2202 postArray = assumeUnique(pps._post); 2203 filesArray = assumeUnique(pps._files); 2204 files = keepLastOf(filesArray); 2205 post = keepLastOf(postArray); 2206 postBody = pps.postBody; 2207 this.requestContentType = contentType; 2208 2209 cleanUpPostDataState(); 2210 } 2211 2212 this.port = port; 2213 this.referrer = referrer; 2214 this.remoteAddress = remoteAddress; 2215 this.userAgent = userAgent; 2216 this.authorization = authorization; 2217 this.origin = origin; 2218 this.accept = accept; 2219 this.lastEventId = lastEventId; 2220 this.https = https; 2221 this.host = host; 2222 this.requestMethod = requestMethod; 2223 this.requestUri = requestUri; 2224 this.pathInfo = pathInfo; 2225 this.queryString = queryString; 2226 2227 this.scriptName = scriptName; 2228 this.get = keepLastOf(getArray); 2229 this.getArray = cast(immutable) getArray; 2230 this.keepAliveRequested = keepAliveRequested; 2231 this.acceptsGzip = acceptsGzip; 2232 this.cookie = cookie; 2233 2234 cookiesArray = getCookieArray(); 2235 cookies = keepLastOf(cookiesArray); 2236 2237 } 2238 BufferedInputRange idlol; 2239 2240 private immutable(string[string]) keepLastOf(in string[][string] arr) { 2241 string[string] ca; 2242 foreach(k, v; arr) 2243 ca[k] = v[$-1]; 2244 2245 return assumeUnique(ca); 2246 } 2247 2248 // FIXME duplication 2249 private immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) { 2250 UploadedFile[string] ca; 2251 foreach(k, v; arr) 2252 ca[k] = v[$-1]; 2253 2254 return assumeUnique(ca); 2255 } 2256 2257 2258 private immutable(string[][string]) getCookieArray() { 2259 auto forTheLoveOfGod = decodeVariables(cookie, "; "); 2260 return assumeUnique(forTheLoveOfGod); 2261 } 2262 2263 /++ 2264 Very simple method to require a basic auth username and password. 2265 If the http request doesn't include the required credentials, it throws a 2266 HTTP 401 error, and an exception to cancel your handler. Do NOT catch the 2267 `AuthorizationRequiredException` exception thrown by this if you want the 2268 http basic auth prompt to work for the user! 2269 2270 Note: basic auth does not provide great security, especially over unencrypted HTTP; 2271 the user's credentials are sent in plain text on every request. 2272 2273 If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the 2274 application. Either use Apache's built in methods for basic authentication, or add 2275 something along these lines to your server configuration: 2276 2277 ``` 2278 RewriteEngine On 2279 RewriteCond %{HTTP:Authorization} ^(.*) 2280 RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] 2281 ``` 2282 2283 To ensure the necessary data is available to cgi.d. 2284 +/ 2285 void requireBasicAuth(string user, string pass, string message = null, string file = __FILE__, size_t line = __LINE__) { 2286 if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) { 2287 throw new AuthorizationRequiredException("Basic", message, file, line); 2288 } 2289 } 2290 2291 /// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites. 2292 /// setCache(true) means it will always be cached for as long as possible. Best for static content. 2293 /// Use setResponseExpires and updateResponseExpires for more control 2294 void setCache(bool allowCaching) { 2295 noCache = !allowCaching; 2296 } 2297 2298 /// Set to true and use cgi.write(data, true); to send a gzipped response to browsers 2299 /// who can accept it 2300 bool gzipResponse; 2301 2302 immutable bool acceptsGzip; 2303 immutable bool keepAliveRequested; 2304 2305 /// Set to true if and only if this was initialized with command line arguments 2306 immutable bool isCalledWithCommandLineArguments; 2307 2308 /// This gets a full url for the current request, including port, protocol, host, path, and query 2309 string getCurrentCompleteUri() const { 2310 ushort defaultPort = https ? 443 : 80; 2311 2312 string uri = "http"; 2313 if(https) 2314 uri ~= "s"; 2315 uri ~= "://"; 2316 uri ~= host; 2317 /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right hting now 2318 version(none) 2319 if(!(!port || port == defaultPort)) { 2320 uri ~= ":"; 2321 uri ~= to!string(port); 2322 } 2323 +/ 2324 uri ~= requestUri; 2325 return uri; 2326 } 2327 2328 /// You can override this if your site base url isn't the same as the script name 2329 string logicalScriptName() const { 2330 return scriptName; 2331 } 2332 2333 /++ 2334 Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error". 2335 It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation(). 2336 Note setResponseStatus() must be called *before* you write() any data to the output. 2337 2338 History: 2339 The `int` overload was added on January 11, 2021. 2340 +/ 2341 void setResponseStatus(string status) { 2342 assert(!outputtedResponseData); 2343 responseStatus = status; 2344 } 2345 /// ditto 2346 void setResponseStatus(int statusCode) { 2347 setResponseStatus(getHttpCodeText(statusCode)); 2348 } 2349 private string responseStatus = null; 2350 2351 /// Returns true if it is still possible to output headers 2352 bool canOutputHeaders() { 2353 return !isClosed && !outputtedResponseData; 2354 } 2355 2356 /// Sets the location header, which the browser will redirect the user to automatically. 2357 /// Note setResponseLocation() must be called *before* you write() any data to the output. 2358 /// The optional important argument is used if it's a default suggestion rather than something to insist upon. 2359 void setResponseLocation(string uri, bool important = true, string status = null) { 2360 if(!important && isCurrentResponseLocationImportant) 2361 return; // important redirects always override unimportant ones 2362 2363 if(uri is null) { 2364 responseStatus = "200 OK"; 2365 responseLocation = null; 2366 isCurrentResponseLocationImportant = important; 2367 return; // this just cancels the redirect 2368 } 2369 2370 assert(!outputtedResponseData); 2371 if(status is null) 2372 responseStatus = "302 Found"; 2373 else 2374 responseStatus = status; 2375 2376 responseLocation = uri.strip; 2377 isCurrentResponseLocationImportant = important; 2378 } 2379 protected string responseLocation = null; 2380 private bool isCurrentResponseLocationImportant = false; 2381 2382 /// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching 2383 /// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use. 2384 /// Note: the when parameter is different than setCookie's expire parameter. 2385 void setResponseExpires(long when, bool isPublic = false) { 2386 responseExpires = when; 2387 setCache(true); // need to enable caching so the date has meaning 2388 2389 responseIsPublic = isPublic; 2390 responseExpiresRelative = false; 2391 } 2392 2393 /// Sets a cache-control max-age header for whenFromNow, in seconds. 2394 void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) { 2395 responseExpires = whenFromNow; 2396 setCache(true); // need to enable caching so the date has meaning 2397 2398 responseIsPublic = isPublic; 2399 responseExpiresRelative = true; 2400 } 2401 private long responseExpires = long.min; 2402 private bool responseIsPublic = false; 2403 private bool responseExpiresRelative = false; 2404 2405 /// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept. 2406 /// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program 2407 /// output as a whole is as cacheable as the least cachable part in the chain. 2408 2409 /// 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. 2410 /// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity. 2411 void updateResponseExpires(long when, bool isPublic) { 2412 if(responseExpires == long.min) 2413 setResponseExpires(when, isPublic); 2414 else if(when < responseExpires) 2415 setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is 2416 } 2417 2418 /* 2419 /// Set to true if you want the result to be cached publically - that is, is the content shared? 2420 /// Should generally be false if the user is logged in. It assumes private cache only. 2421 /// setCache(true) also turns on public caching, and setCache(false) sets to private. 2422 void setPublicCaching(bool allowPublicCaches) { 2423 publicCaching = allowPublicCaches; 2424 } 2425 private bool publicCaching = false; 2426 */ 2427 2428 /++ 2429 History: 2430 Added January 11, 2021 2431 +/ 2432 enum SameSitePolicy { 2433 Lax, 2434 Strict, 2435 None 2436 } 2437 2438 /++ 2439 Sets an HTTP cookie, automatically encoding the data to the correct string. 2440 expiresIn is how many milliseconds in the future the cookie will expire. 2441 TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com. 2442 Note setCookie() must be called *before* you write() any data to the output. 2443 2444 History: 2445 Parameter `sameSitePolicy` was added on January 11, 2021. 2446 +/ 2447 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) { 2448 assert(!outputtedResponseData); 2449 string cookie = encodeUriComponent(name) ~ "="; 2450 cookie ~= encodeUriComponent(data); 2451 if(path !is null) 2452 cookie ~= "; path=" ~ path; 2453 // FIXME: should I just be using max-age here? (also in cache below) 2454 if(expiresIn != 0) 2455 cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!"msecs"(expiresIn)); 2456 if(domain !is null) 2457 cookie ~= "; domain=" ~ domain; 2458 if(secure == true) 2459 cookie ~= "; Secure"; 2460 if(httpOnly == true ) 2461 cookie ~= "; HttpOnly"; 2462 final switch(sameSitePolicy) { 2463 case SameSitePolicy.Lax: 2464 cookie ~= "; SameSite=Lax"; 2465 break; 2466 case SameSitePolicy.Strict: 2467 cookie ~= "; SameSite=Strict"; 2468 break; 2469 case SameSitePolicy.None: 2470 cookie ~= "; SameSite=None"; 2471 assert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite 2472 break; 2473 } 2474 2475 if(auto idx = name in cookieIndexes) { 2476 responseCookies[*idx] = cookie; 2477 } else { 2478 cookieIndexes[name] = responseCookies.length; 2479 responseCookies ~= cookie; 2480 } 2481 } 2482 private string[] responseCookies; 2483 private size_t[string] cookieIndexes; 2484 2485 /// Clears a previously set cookie with the given name, path, and domain. 2486 void clearCookie(string name, string path = null, string domain = null) { 2487 assert(!outputtedResponseData); 2488 setCookie(name, "", 1, path, domain); 2489 } 2490 2491 /// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image 2492 void setResponseContentType(string ct) { 2493 assert(!outputtedResponseData); 2494 responseContentType = ct; 2495 } 2496 private string responseContentType = null; 2497 2498 /// Adds a custom header. It should be the name: value, but without any line terminator. 2499 /// For example: header("X-My-Header: Some value"); 2500 /// Note you should use the specialized functions in this object if possible to avoid 2501 /// duplicates in the output. 2502 void header(string h) { 2503 customHeaders ~= h; 2504 } 2505 2506 /++ 2507 I named the original function `header` after PHP, but this pattern more fits 2508 the rest of the Cgi object. 2509 2510 Either name are allowed. 2511 2512 History: 2513 Alias added June 17, 2022. 2514 +/ 2515 alias setResponseHeader = header; 2516 2517 private string[] customHeaders; 2518 private bool websocketMode; 2519 2520 void flushHeaders(const(void)[] t, bool isAll = false) { 2521 StackBuffer buffer = StackBuffer(0); 2522 2523 prepHeaders(t, isAll, &buffer); 2524 2525 if(rawDataOutput !is null) 2526 rawDataOutput(cast(const(ubyte)[]) buffer.get()); 2527 else { 2528 stdout.rawWrite(buffer.get()); 2529 } 2530 } 2531 2532 private void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) { 2533 string terminator = "\n"; 2534 if(rawDataOutput !is null) 2535 terminator = "\r\n"; 2536 2537 if(responseStatus !is null) { 2538 if(nph) { 2539 if(http10) 2540 buffer.add("HTTP/1.0 ", responseStatus, terminator); 2541 else 2542 buffer.add("HTTP/1.1 ", responseStatus, terminator); 2543 } else 2544 buffer.add("Status: ", responseStatus, terminator); 2545 } else if (nph) { 2546 if(http10) 2547 buffer.add("HTTP/1.0 200 OK", terminator); 2548 else 2549 buffer.add("HTTP/1.1 200 OK", terminator); 2550 } 2551 2552 if(websocketMode) 2553 goto websocket; 2554 2555 if(nph) { // we're responsible for setting the date too according to http 1.1 2556 char[29] db = void; 2557 printDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]); 2558 buffer.add("Date: ", db[], terminator); 2559 } 2560 2561 // FIXME: what if the user wants to set his own content-length? 2562 // The custom header function can do it, so maybe that's best. 2563 // Or we could reuse the isAll param. 2564 if(responseLocation !is null) { 2565 buffer.add("Location: ", responseLocation, terminator); 2566 } 2567 if(!noCache && responseExpires != long.min) { // an explicit expiration date is set 2568 if(responseExpiresRelative) { 2569 buffer.add("Cache-Control: ", responseIsPublic ? "public" : "private", ", max-age="); 2570 buffer.add(responseExpires); 2571 buffer.add(", no-cache=\"set-cookie, set-cookie2\"", terminator); 2572 } else { 2573 auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC()); 2574 char[29] db = void; 2575 printDateToBuffer(cast(DateTime) expires, db[]); 2576 buffer.add("Expires: ", db[], terminator); 2577 // FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily 2578 buffer.add("Cache-Control: ", (responseIsPublic ? "public" : "private"), ", no-cache=\"set-cookie, set-cookie2\""); 2579 buffer.add(terminator); 2580 } 2581 } 2582 if(responseCookies !is null && responseCookies.length > 0) { 2583 foreach(c; responseCookies) 2584 buffer.add("Set-Cookie: ", c, terminator); 2585 } 2586 if(noCache) { // we specifically do not want caching (this is actually the default) 2587 buffer.add("Cache-Control: private, no-cache=\"set-cookie\"", terminator); 2588 buffer.add("Expires: 0", terminator); 2589 buffer.add("Pragma: no-cache", terminator); 2590 } else { 2591 if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever 2592 buffer.add("Cache-Control: public", terminator); 2593 buffer.add("Expires: Tue, 31 Dec 2030 14:00:00 GMT", terminator); // FIXME: should not be more than one year in the future 2594 } 2595 } 2596 if(responseContentType !is null) { 2597 buffer.add("Content-Type: ", responseContentType, terminator); 2598 } else 2599 buffer.add("Content-Type: text/html; charset=utf-8", terminator); 2600 2601 if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary 2602 buffer.add("Content-Encoding: gzip", terminator); 2603 } 2604 2605 2606 if(!isAll) { 2607 if(nph && !http10) { 2608 buffer.add("Transfer-Encoding: chunked", terminator); 2609 responseChunked = true; 2610 } 2611 } else { 2612 buffer.add("Content-Length: "); 2613 buffer.add(t.length); 2614 buffer.add(terminator); 2615 if(nph && keepAliveRequested) { 2616 buffer.add("Connection: Keep-Alive", terminator); 2617 } 2618 } 2619 2620 websocket: 2621 2622 foreach(hd; customHeaders) 2623 buffer.add(hd, terminator); 2624 2625 // FIXME: what about duplicated headers? 2626 2627 // end of header indicator 2628 buffer.add(terminator); 2629 2630 outputtedResponseData = true; 2631 } 2632 2633 /// Writes the data to the output, flushing headers if they have not yet been sent. 2634 void write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) { 2635 assert(!closed, "Output has already been closed"); 2636 2637 StackBuffer buffer = StackBuffer(0); 2638 2639 if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary 2640 // actually gzip the data here 2641 2642 auto c = new Compress(HeaderFormat.gzip); // want gzip 2643 2644 auto data = c.compress(t); 2645 data ~= c.flush(); 2646 2647 // std.file.write("/tmp/last-item", data); 2648 2649 t = data; 2650 } 2651 2652 if(!outputtedResponseData && (!autoBuffer || isAll)) { 2653 prepHeaders(t, isAll, &buffer); 2654 } 2655 2656 if(requestMethod != RequestMethod.HEAD && t.length > 0) { 2657 if (autoBuffer && !isAll) { 2658 outputBuffer ~= cast(ubyte[]) t; 2659 } 2660 if(!autoBuffer || isAll) { 2661 if(rawDataOutput !is null) 2662 if(nph && responseChunked) { 2663 //rawDataOutput(makeChunk(cast(const(ubyte)[]) t)); 2664 // we're making the chunk here instead of in a function 2665 // to avoid unneeded gc pressure 2666 buffer.add(toHex(t.length)); 2667 buffer.add("\r\n"); 2668 buffer.add(cast(char[]) t, "\r\n"); 2669 } else { 2670 buffer.add(cast(char[]) t); 2671 } 2672 else 2673 buffer.add(cast(char[]) t); 2674 } 2675 } 2676 2677 if(rawDataOutput !is null) 2678 rawDataOutput(cast(const(ubyte)[]) buffer.get()); 2679 else 2680 stdout.rawWrite(buffer.get()); 2681 2682 if(maybeAutoClose && isAll) 2683 close(); // if you say it is all, that means we're definitely done 2684 // maybeAutoClose can be false though to avoid this (important if you call from inside close()! 2685 } 2686 2687 /++ 2688 Convenience method to set content type to json and write the string as the complete response. 2689 2690 History: 2691 Added January 16, 2020 2692 +/ 2693 void writeJson(string json) { 2694 this.setResponseContentType("application/json"); 2695 this.write(json, true); 2696 } 2697 2698 /// Flushes the pending buffer, leaving the connection open so you can send more. 2699 void flush() { 2700 if(rawDataOutput is null) 2701 stdout.flush(); 2702 else if(flushDelegate !is null) 2703 flushDelegate(); 2704 } 2705 2706 version(autoBuffer) 2707 bool autoBuffer = true; 2708 else 2709 bool autoBuffer = false; 2710 ubyte[] outputBuffer; 2711 2712 /// Flushes the buffers to the network, signifying that you are done. 2713 /// You should always call this explicitly when you are done outputting data. 2714 void close() { 2715 if(closed) 2716 return; // don't double close 2717 2718 if(!outputtedResponseData) 2719 write("", true, false); 2720 2721 // writing auto buffered data 2722 if(requestMethod != RequestMethod.HEAD && autoBuffer) { 2723 if(!nph) 2724 stdout.rawWrite(outputBuffer); 2725 else 2726 write(outputBuffer, true, false); // tell it this is everything 2727 } 2728 2729 // closing the last chunk... 2730 if(nph && rawDataOutput !is null && responseChunked) 2731 rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n"); 2732 2733 if(flushDelegate) 2734 flushDelegate(); 2735 2736 closed = true; 2737 } 2738 2739 // Closes without doing anything, shouldn't be used often 2740 void rawClose() { 2741 closed = true; 2742 } 2743 2744 /++ 2745 Gets a request variable as a specific type, or the default value of it isn't there 2746 or isn't convertible to the request type. 2747 2748 Checks both GET and POST variables, preferring the POST variable, if available. 2749 2750 A nice trick is using the default value to choose the type: 2751 2752 --- 2753 /* 2754 The return value will match the type of the default. 2755 Here, I gave 10 as a default, so the return value will 2756 be an int. 2757 2758 If the user-supplied value cannot be converted to the 2759 requested type, you will get the default value back. 2760 */ 2761 int a = cgi.request("number", 10); 2762 2763 if(cgi.get["number"] == "11") 2764 assert(a == 11); // conversion succeeds 2765 2766 if("number" !in cgi.get) 2767 assert(a == 10); // no value means you can't convert - give the default 2768 2769 if(cgi.get["number"] == "twelve") 2770 assert(a == 10); // conversion from string to int would fail, so we get the default 2771 --- 2772 2773 You can use an enum as an easy whitelist, too: 2774 2775 --- 2776 enum Operations { 2777 add, remove, query 2778 } 2779 2780 auto op = cgi.request("op", Operations.query); 2781 2782 if(cgi.get["op"] == "add") 2783 assert(op == Operations.add); 2784 if(cgi.get["op"] == "remove") 2785 assert(op == Operations.remove); 2786 if(cgi.get["op"] == "query") 2787 assert(op == Operations.query); 2788 2789 if(cgi.get["op"] == "random string") 2790 assert(op == Operations.query); // the value can't be converted to the enum, so we get the default 2791 --- 2792 +/ 2793 T request(T = string)(in string name, in T def = T.init) const nothrow { 2794 try { 2795 return 2796 (name in post) ? to!T(post[name]) : 2797 (name in get) ? to!T(get[name]) : 2798 def; 2799 } catch(Exception e) { return def; } 2800 } 2801 2802 /// Is the output already closed? 2803 bool isClosed() const { 2804 return closed; 2805 } 2806 2807 private SessionObject commandLineSessionObject; 2808 2809 /++ 2810 Gets a session object associated with the `cgi` request. You can use different type throughout your application. 2811 +/ 2812 Session!Data getSessionObject(Data)() { 2813 if(testInProcess !is null) { 2814 // test mode 2815 auto obj = testInProcess.getSessionOverride(typeid(typeof(return))); 2816 if(obj !is null) 2817 return cast(typeof(return)) obj; 2818 else { 2819 auto o = new MockSession!Data(); 2820 testInProcess.setSessionOverride(typeid(typeof(return)), o); 2821 return o; 2822 } 2823 } else { 2824 // FIXME: the changes are not printed out at the end! 2825 if(_commandLineSession !is null) { 2826 if(commandLineSessionObject is null) { 2827 auto clso = new MockSession!Data(); 2828 commandLineSessionObject = clso; 2829 2830 2831 foreach(memberName; __traits(allMembers, Data)) { 2832 if(auto str = memberName in _commandLineSession) 2833 __traits(getMember, clso.store_, memberName) = to!(typeof(__traits(getMember, Data, memberName)))(*str); 2834 } 2835 } 2836 2837 return cast(typeof(return)) commandLineSessionObject; 2838 } 2839 2840 // normal operation 2841 return new BasicDataServerSession!Data(this); 2842 } 2843 } 2844 2845 // if it is in test mode; triggers mock sessions. Used by CgiTester 2846 version(with_breaking_cgi_features) 2847 private CgiTester testInProcess; 2848 2849 /* Hooks for redirecting input and output */ 2850 private void delegate(const(ubyte)[]) rawDataOutput = null; 2851 private void delegate() flushDelegate = null; 2852 2853 /* This info is used when handling a more raw HTTP protocol */ 2854 private bool nph; 2855 private bool http10; 2856 private bool closed; 2857 private bool responseChunked = false; 2858 2859 version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it. 2860 immutable(ubyte)[] originalPostData; 2861 2862 /++ 2863 This holds the posted body data if it has not been parsed into [post] and [postArray]. 2864 2865 It is intended to be used for JSON and XML request content types, but also may be used 2866 for other content types your application can handle. But it will NOT be populated 2867 for content types application/x-www-form-urlencoded or multipart/form-data, since those are 2868 parsed into the post and postArray members. 2869 2870 Remember that anything beyond your `maxContentLength` param when setting up [GenericMain], etc., 2871 will be discarded to the client with an error. This helps keep this array from being exploded in size 2872 and consuming all your server's memory (though it may still be possible to eat excess ram from a concurrent 2873 client in certain build modes.) 2874 2875 History: 2876 Added January 5, 2021 2877 Documented February 21, 2023 (dub v11.0) 2878 +/ 2879 public immutable string postBody; 2880 alias postJson = postBody; // old name 2881 2882 /++ 2883 The content type header of the request. The [postBody] member may hold the actual data (see [postBody] for details). 2884 2885 History: 2886 Added January 26, 2024 (dub v11.4) 2887 +/ 2888 public immutable string requestContentType; 2889 2890 /* Internal state flags */ 2891 private bool outputtedResponseData; 2892 private bool noCache = true; 2893 2894 const(string[string]) environmentVariables; 2895 2896 /** What follows is data gotten from the HTTP request. It is all fully immutable, 2897 partially because it logically is (your code doesn't change what the user requested...) 2898 and partially because I hate how bad programs in PHP change those superglobals to do 2899 all kinds of hard to follow ugliness. I don't want that to ever happen in D. 2900 2901 For some of these, you'll want to refer to the http or cgi specs for more details. 2902 */ 2903 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. 2904 2905 immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them. 2906 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. 2907 immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser. 2908 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". 2909 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". 2910 immutable(char[]) scriptFileName; /// The physical filename of your script 2911 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. 2912 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.) 2913 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. 2914 2915 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. 2916 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.) 2917 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. 2918 /** 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. 2919 2920 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. 2921 */ 2922 immutable(char[]) referrer; 2923 immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : ""); 2924 2925 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.) 2926 2927 immutable bool https; /// Was the request encrypted via https? 2928 immutable int port; /// On what TCP port number did the server receive the request? 2929 2930 /** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */ 2931 2932 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. 2933 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. 2934 immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!) 2935 2936 /// added later 2937 alias query = get; 2938 2939 /** 2940 Represents user uploaded files. 2941 2942 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. 2943 */ 2944 immutable(UploadedFile[][string]) filesArray; 2945 immutable(UploadedFile[string]) files; 2946 2947 /// 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. 2948 /// the order of the arrays is the order the data arrives 2949 immutable(string[][string]) getArray; /// like get, but an array of values per name 2950 immutable(string[][string]) postArray; /// ditto for post 2951 immutable(string[][string]) cookiesArray; /// ditto for cookies 2952 2953 private string[string] _commandLineSession; 2954 2955 // convenience function for appending to a uri without extra ? 2956 // matches the name and effect of javascript's location.search property 2957 string search() const { 2958 if(queryString.length) 2959 return "?" ~ queryString; 2960 return ""; 2961 } 2962 2963 // FIXME: what about multiple files with the same name? 2964 private: 2965 //RequestMethod _requestMethod; 2966 } 2967 2968 /// use this for testing or other isolated things when you want it to be no-ops 2969 Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) { 2970 // we want to ignore, not use stdout 2971 if(outputSink is null) 2972 outputSink = delegate void(const(ubyte)[]) { }; 2973 2974 string[string] env; 2975 env["REQUEST_METHOD"] = to!string(method); 2976 env["CONTENT_LENGTH"] = to!string(data.length); 2977 2978 auto cgi = new Cgi( 2979 0, 2980 env, 2981 { return data; }, 2982 outputSink, 2983 null); 2984 2985 return cgi; 2986 } 2987 2988 /++ 2989 A helper test class for request handler unittests. 2990 +/ 2991 version(with_breaking_cgi_features) 2992 class CgiTester { 2993 private { 2994 SessionObject[TypeInfo] mockSessions; 2995 SessionObject getSessionOverride(TypeInfo ti) { 2996 if(auto o = ti in mockSessions) 2997 return *o; 2998 else 2999 return null; 3000 } 3001 void setSessionOverride(TypeInfo ti, SessionObject so) { 3002 mockSessions[ti] = so; 3003 } 3004 } 3005 3006 /++ 3007 Gets (and creates if necessary) a mock session object for this test. Note 3008 it will be the same one used for any test operations through this CgiTester instance. 3009 +/ 3010 Session!Data getSessionObject(Data)() { 3011 auto obj = getSessionOverride(typeid(typeof(return))); 3012 if(obj !is null) 3013 return cast(typeof(return)) obj; 3014 else { 3015 auto o = new MockSession!Data(); 3016 setSessionOverride(typeid(typeof(return)), o); 3017 return o; 3018 } 3019 } 3020 3021 /++ 3022 Pass a reference to your request handler when creating the tester. 3023 +/ 3024 this(void function(Cgi) requestHandler) { 3025 this.requestHandler = requestHandler; 3026 } 3027 3028 /++ 3029 You can check response information with these methods after you call the request handler. 3030 +/ 3031 struct Response { 3032 int code; 3033 string[string] headers; 3034 string responseText; 3035 ubyte[] responseBody; 3036 } 3037 3038 /++ 3039 Executes a test request on your request handler, and returns the response. 3040 3041 Params: 3042 url = The URL to test. Should be an absolute path, but excluding domain. e.g. `"/test"`. 3043 args = additional arguments. Same format as cgi's command line handler. 3044 +/ 3045 Response GET(string url, string[] args = null) { 3046 return executeTest("GET", url, args); 3047 } 3048 /// ditto 3049 Response POST(string url, string[] args = null) { 3050 return executeTest("POST", url, args); 3051 } 3052 3053 /// ditto 3054 Response executeTest(string method, string url, string[] args) { 3055 ubyte[] outputtedRawData; 3056 void outputSink(const(ubyte)[] data) { 3057 outputtedRawData ~= data; 3058 } 3059 auto cgi = new Cgi(["test", method, url] ~ args, &outputSink); 3060 cgi.testInProcess = this; 3061 scope(exit) cgi.dispose(); 3062 3063 requestHandler(cgi); 3064 3065 cgi.close(); 3066 3067 Response response; 3068 3069 if(outputtedRawData.length) { 3070 enum LINE = "\r\n"; 3071 3072 auto idx = outputtedRawData.locationOf(LINE ~ LINE); 3073 assert(idx != -1, to!string(outputtedRawData)); 3074 auto headers = cast(string) outputtedRawData[0 .. idx]; 3075 response.code = 200; 3076 while(headers.length) { 3077 auto i = headers.locationOf(LINE); 3078 if(i == -1) i = cast(int) headers.length; 3079 3080 auto header = headers[0 .. i]; 3081 3082 auto c = header.locationOf(":"); 3083 if(c != -1) { 3084 auto name = header[0 .. c]; 3085 auto value = header[c + 2 ..$]; 3086 3087 if(name == "Status") 3088 response.code = value[0 .. value.locationOf(" ")].to!int; 3089 3090 response.headers[name] = value; 3091 } else { 3092 assert(0); 3093 } 3094 3095 if(i != headers.length) 3096 i += 2; 3097 headers = headers[i .. $]; 3098 } 3099 response.responseBody = outputtedRawData[idx + 4 .. $]; 3100 response.responseText = cast(string) response.responseBody; 3101 } 3102 3103 return response; 3104 } 3105 3106 private void function(Cgi) requestHandler; 3107 } 3108 3109 3110 // should this be a separate module? Probably, but that's a hassle. 3111 3112 /// Makes a data:// uri that can be used as links in most newer browsers (IE8+). 3113 string makeDataUrl(string mimeType, in void[] data) { 3114 auto data64 = Base64.encode(cast(const(ubyte[])) data); 3115 return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64); 3116 } 3117 3118 // FIXME: I don't think this class correctly decodes/encodes the individual parts 3119 /// Represents a url that can be broken down or built up through properties 3120 struct Uri { 3121 alias toString this; // blargh idk a url really is a string, but should it be implicit? 3122 3123 // scheme//userinfo@host:port/path?query#fragment 3124 3125 string scheme; /// e.g. "http" in "http://example.com/" 3126 string userinfo; /// the username (and possibly a password) in the uri 3127 string host; /// the domain name 3128 int port; /// port number, if given. Will be zero if a port was not explicitly given 3129 string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html" 3130 string query; /// the stuff after the ? in a uri 3131 string fragment; /// the stuff after the # in a uri. 3132 3133 // 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 3134 // the decode ones need to keep different names anyway because we can't overload on return values... 3135 static string encode(string s) { return encodeUriComponent(s); } 3136 static string encode(string[string] s) { return encodeVariables(s); } 3137 static string encode(string[][string] s) { return encodeVariables(s); } 3138 3139 /// Breaks down a uri string to its components 3140 this(string uri) { 3141 reparse(uri); 3142 } 3143 3144 private void reparse(string uri) { 3145 // from RFC 3986 3146 // the ctRegex triples the compile time and makes ugly errors for no real benefit 3147 // it was a nice experiment but just not worth it. 3148 // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"; 3149 /* 3150 Captures: 3151 0 = whole url 3152 1 = scheme, with : 3153 2 = scheme, no : 3154 3 = authority, with // 3155 4 = authority, no // 3156 5 = path 3157 6 = query string, with ? 3158 7 = query string, no ? 3159 8 = anchor, with # 3160 9 = anchor, no # 3161 */ 3162 // Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer! 3163 // instead, I will DIY and cut that down to 0.6s on the same computer. 3164 /* 3165 3166 Note that authority is 3167 user:password@domain:port 3168 where the user:password@ part is optional, and the :port is optional. 3169 3170 Regex translation: 3171 3172 Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first. 3173 Authority must start with //, but cannot have any other /, ?, or # in it. It is optional. 3174 Path cannot have any ? or # in it. It is optional. 3175 Query must start with ? and must not have # in it. It is optional. 3176 Anchor must start with # and can have anything else in it to end of string. It is optional. 3177 */ 3178 3179 this = Uri.init; // reset all state 3180 3181 // empty uri = nothing special 3182 if(uri.length == 0) { 3183 return; 3184 } 3185 3186 size_t idx; 3187 3188 scheme_loop: foreach(char c; uri[idx .. $]) { 3189 switch(c) { 3190 case ':': 3191 case '/': 3192 case '?': 3193 case '#': 3194 break scheme_loop; 3195 default: 3196 } 3197 idx++; 3198 } 3199 3200 if(idx == 0 && uri[idx] == ':') { 3201 // this is actually a path! we skip way ahead 3202 goto path_loop; 3203 } 3204 3205 if(idx == uri.length) { 3206 // the whole thing is a path, apparently 3207 path = uri; 3208 return; 3209 } 3210 3211 if(idx > 0 && uri[idx] == ':') { 3212 scheme = uri[0 .. idx]; 3213 idx++; 3214 } else { 3215 // we need to rewind; it found a / but no :, so the whole thing is prolly a path... 3216 idx = 0; 3217 } 3218 3219 if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") { 3220 // we have an authority.... 3221 idx += 2; 3222 3223 auto authority_start = idx; 3224 authority_loop: foreach(char c; uri[idx .. $]) { 3225 switch(c) { 3226 case '/': 3227 case '?': 3228 case '#': 3229 break authority_loop; 3230 default: 3231 } 3232 idx++; 3233 } 3234 3235 auto authority = uri[authority_start .. idx]; 3236 3237 auto idx2 = authority.indexOf("@"); 3238 if(idx2 != -1) { 3239 userinfo = authority[0 .. idx2]; 3240 authority = authority[idx2 + 1 .. $]; 3241 } 3242 3243 if(authority.length && authority[0] == '[') { 3244 // ipv6 address special casing 3245 idx2 = authority.indexOf(']'); 3246 if(idx2 != -1) { 3247 auto end = authority[idx2 + 1 .. $]; 3248 if(end.length && end[0] == ':') 3249 idx2 = idx2 + 1; 3250 else 3251 idx2 = -1; 3252 } 3253 } else { 3254 idx2 = authority.indexOf(":"); 3255 } 3256 3257 if(idx2 == -1) { 3258 port = 0; // 0 means not specified; we should use the default for the scheme 3259 host = authority; 3260 } else { 3261 host = authority[0 .. idx2]; 3262 if(idx2 + 1 < authority.length) 3263 port = to!int(authority[idx2 + 1 .. $]); 3264 else 3265 port = 0; 3266 } 3267 } 3268 3269 path_loop: 3270 auto path_start = idx; 3271 3272 foreach(char c; uri[idx .. $]) { 3273 if(c == '?' || c == '#') 3274 break; 3275 idx++; 3276 } 3277 3278 path = uri[path_start .. idx]; 3279 3280 if(idx == uri.length) 3281 return; // nothing more to examine... 3282 3283 if(uri[idx] == '?') { 3284 idx++; 3285 auto query_start = idx; 3286 foreach(char c; uri[idx .. $]) { 3287 if(c == '#') 3288 break; 3289 idx++; 3290 } 3291 query = uri[query_start .. idx]; 3292 } 3293 3294 if(idx < uri.length && uri[idx] == '#') { 3295 idx++; 3296 fragment = uri[idx .. $]; 3297 } 3298 3299 // uriInvalidated = false; 3300 } 3301 3302 private string rebuildUri() const { 3303 string ret; 3304 if(scheme.length) 3305 ret ~= scheme ~ ":"; 3306 if(userinfo.length || host.length) 3307 ret ~= "//"; 3308 if(userinfo.length) 3309 ret ~= userinfo ~ "@"; 3310 if(host.length) 3311 ret ~= host; 3312 if(port) 3313 ret ~= ":" ~ to!string(port); 3314 3315 ret ~= path; 3316 3317 if(query.length) 3318 ret ~= "?" ~ query; 3319 3320 if(fragment.length) 3321 ret ~= "#" ~ fragment; 3322 3323 // uri = ret; 3324 // uriInvalidated = false; 3325 return ret; 3326 } 3327 3328 /// Converts the broken down parts back into a complete string 3329 string toString() const { 3330 // if(uriInvalidated) 3331 return rebuildUri(); 3332 } 3333 3334 /// Returns a new absolute Uri given a base. It treats this one as 3335 /// relative where possible, but absolute if not. (If protocol, domain, or 3336 /// other info is not set, the new one inherits it from the base.) 3337 /// 3338 /// Browsers use a function like this to figure out links in html. 3339 Uri basedOn(in Uri baseUrl) const { 3340 Uri n = this; // copies 3341 if(n.scheme == "data") 3342 return n; 3343 // n.uriInvalidated = true; // make sure we regenerate... 3344 3345 // userinfo is not inherited... is this wrong? 3346 3347 // if anything is given in the existing url, we don't use the base anymore. 3348 if(n.scheme.empty) { 3349 n.scheme = baseUrl.scheme; 3350 if(n.host.empty) { 3351 n.host = baseUrl.host; 3352 if(n.port == 0) { 3353 n.port = baseUrl.port; 3354 if(n.path.length > 0 && n.path[0] != '/') { 3355 auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1]; 3356 if(b.length == 0) 3357 b = "/"; 3358 n.path = b ~ n.path; 3359 } else if(n.path.length == 0) { 3360 n.path = baseUrl.path; 3361 } 3362 } 3363 } 3364 } 3365 3366 n.removeDots(); 3367 3368 return n; 3369 } 3370 3371 void removeDots() { 3372 auto parts = this.path.split("/"); 3373 string[] toKeep; 3374 foreach(part; parts) { 3375 if(part == ".") { 3376 continue; 3377 } else if(part == "..") { 3378 //if(toKeep.length > 1) 3379 toKeep = toKeep[0 .. $-1]; 3380 //else 3381 //toKeep = [""]; 3382 continue; 3383 } else { 3384 //if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0) 3385 //continue; // skip a `//` situation 3386 toKeep ~= part; 3387 } 3388 } 3389 3390 auto path = toKeep.join("/"); 3391 if(path.length && path[0] != '/') 3392 path = "/" ~ path; 3393 3394 this.path = path; 3395 } 3396 3397 unittest { 3398 auto uri = Uri("test.html"); 3399 assert(uri.path == "test.html"); 3400 uri = Uri("path/1/lol"); 3401 assert(uri.path == "path/1/lol"); 3402 uri = Uri("http://me@example.com"); 3403 assert(uri.scheme == "http"); 3404 assert(uri.userinfo == "me"); 3405 assert(uri.host == "example.com"); 3406 uri = Uri("http://example.com/#a"); 3407 assert(uri.scheme == "http"); 3408 assert(uri.host == "example.com"); 3409 assert(uri.fragment == "a"); 3410 uri = Uri("#foo"); 3411 assert(uri.fragment == "foo"); 3412 uri = Uri("?lol"); 3413 assert(uri.query == "lol"); 3414 uri = Uri("#foo?lol"); 3415 assert(uri.fragment == "foo?lol"); 3416 uri = Uri("?lol#foo"); 3417 assert(uri.fragment == "foo"); 3418 assert(uri.query == "lol"); 3419 3420 uri = Uri("http://127.0.0.1/"); 3421 assert(uri.host == "127.0.0.1"); 3422 assert(uri.port == 0); 3423 3424 uri = Uri("http://127.0.0.1:123/"); 3425 assert(uri.host == "127.0.0.1"); 3426 assert(uri.port == 123); 3427 3428 uri = Uri("http://[ff:ff::0]/"); 3429 assert(uri.host == "[ff:ff::0]"); 3430 3431 uri = Uri("http://[ff:ff::0]:123/"); 3432 assert(uri.host == "[ff:ff::0]"); 3433 assert(uri.port == 123); 3434 } 3435 3436 // This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover 3437 // the possibilities. 3438 unittest { 3439 auto url = Uri("cool.html"); // checking relative links 3440 3441 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html"); 3442 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html"); 3443 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html"); 3444 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html"); 3445 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); 3446 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html"); 3447 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html"); 3448 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html"); 3449 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); 3450 3451 url = Uri("/something/cool.html"); // same server, different path 3452 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html"); 3453 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html"); 3454 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html"); 3455 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html"); 3456 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); 3457 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html"); 3458 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html"); 3459 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html"); 3460 assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); 3461 3462 url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment 3463 assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer"); 3464 assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer"); 3465 assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer"); 3466 assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer"); 3467 assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); 3468 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer"); 3469 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer"); 3470 assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer"); 3471 assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); 3472 3473 url = Uri("/test/bar"); 3474 assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url)); 3475 assert(Uri("../").basedOn(url) == "/"); 3476 3477 url = Uri("http://example.com/"); 3478 assert(Uri("../foo").basedOn(url) == "http://example.com/foo"); 3479 3480 //auto uriBefore = url; 3481 url = Uri("#anchor"); // everything should remain the same except the anchor 3482 //uriBefore.anchor = "anchor"); 3483 //assert(url == uriBefore); 3484 3485 url = Uri("//example.com"); // same protocol, but different server. the path here should be blank. 3486 3487 url = Uri("//example.com/example.html"); // same protocol, but different server and path 3488 3489 url = Uri("http://example.com/test.html"); // completely absolute link should never be modified 3490 3491 url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path 3492 3493 // FIXME: add something for port too 3494 } 3495 3496 // these are like javascript's location.search and location.hash 3497 string search() const { 3498 return query.length ? ("?" ~ query) : ""; 3499 } 3500 string hash() const { 3501 return fragment.length ? ("#" ~ fragment) : ""; 3502 } 3503 } 3504 3505 3506 /* 3507 for session, see web.d 3508 */ 3509 3510 /// breaks down a url encoded string 3511 string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) { 3512 auto vars = data.split(separator); 3513 string[][string] _get; 3514 foreach(var; vars) { 3515 auto equal = var.indexOf("="); 3516 string name; 3517 string value; 3518 if(equal == -1) { 3519 name = decodeUriComponent(var); 3520 value = ""; 3521 } else { 3522 //_get[decodeUriComponent(var[0..equal])] ~= decodeUriComponent(var[equal + 1 .. $].replace("+", " ")); 3523 // stupid + -> space conversion. 3524 name = decodeUriComponent(var[0..equal].replace("+", " ")); 3525 value = decodeUriComponent(var[equal + 1 .. $].replace("+", " ")); 3526 } 3527 3528 _get[name] ~= value; 3529 if(namesInOrder) 3530 (*namesInOrder) ~= name; 3531 if(valuesInOrder) 3532 (*valuesInOrder) ~= value; 3533 } 3534 return _get; 3535 } 3536 3537 /// breaks down a url encoded string, but only returns the last value of any array 3538 string[string] decodeVariablesSingle(string data) { 3539 string[string] va; 3540 auto varArray = decodeVariables(data); 3541 foreach(k, v; varArray) 3542 va[k] = v[$-1]; 3543 3544 return va; 3545 } 3546 3547 /// url encodes the whole string 3548 string encodeVariables(in string[string] data) { 3549 string ret; 3550 3551 bool outputted = false; 3552 foreach(k, v; data) { 3553 if(outputted) 3554 ret ~= "&"; 3555 else 3556 outputted = true; 3557 3558 ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v); 3559 } 3560 3561 return ret; 3562 } 3563 3564 /// url encodes a whole string 3565 string encodeVariables(in string[][string] data) { 3566 string ret; 3567 3568 bool outputted = false; 3569 foreach(k, arr; data) { 3570 foreach(v; arr) { 3571 if(outputted) 3572 ret ~= "&"; 3573 else 3574 outputted = true; 3575 ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v); 3576 } 3577 } 3578 3579 return ret; 3580 } 3581 3582 /// Encodes all but the explicitly unreserved characters per rfc 3986 3583 /// Alphanumeric and -_.~ are the only ones left unencoded 3584 /// name is borrowed from php 3585 string rawurlencode(in char[] data) { 3586 string ret; 3587 ret.reserve(data.length * 2); 3588 foreach(char c; data) { 3589 if( 3590 (c >= 'a' && c <= 'z') || 3591 (c >= 'A' && c <= 'Z') || 3592 (c >= '0' && c <= '9') || 3593 c == '-' || c == '_' || c == '.' || c == '~') 3594 { 3595 ret ~= c; 3596 } else { 3597 ret ~= '%'; 3598 // since we iterate on char, this should give us the octets of the full utf8 string 3599 ret ~= toHexUpper(c); 3600 } 3601 } 3602 3603 return ret; 3604 } 3605 3606 3607 // http helper functions 3608 3609 // for chunked responses (which embedded http does whenever possible) 3610 version(none) // this is moved up above to avoid making a copy of the data 3611 const(ubyte)[] makeChunk(const(ubyte)[] data) { 3612 const(ubyte)[] ret; 3613 3614 ret = cast(const(ubyte)[]) toHex(data.length); 3615 ret ~= cast(const(ubyte)[]) "\r\n"; 3616 ret ~= data; 3617 ret ~= cast(const(ubyte)[]) "\r\n"; 3618 3619 return ret; 3620 } 3621 3622 string toHex(long num) { 3623 string ret; 3624 while(num) { 3625 int v = num % 16; 3626 num /= 16; 3627 char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a'); 3628 ret ~= d; 3629 } 3630 3631 return to!string(array(ret.retro)); 3632 } 3633 3634 string toHexUpper(long num) { 3635 string ret; 3636 while(num) { 3637 int v = num % 16; 3638 num /= 16; 3639 char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A'); 3640 ret ~= d; 3641 } 3642 3643 if(ret.length == 1) 3644 ret ~= "0"; // url encoding requires two digits and that's what this function is used for... 3645 3646 return to!string(array(ret.retro)); 3647 } 3648 3649 3650 // the generic mixins 3651 3652 /++ 3653 Use this instead of writing your own main 3654 3655 It ultimately calls [cgiMainImpl] which creates a [RequestServer] for you. 3656 +/ 3657 mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) { 3658 mixin CustomCgiMain!(Cgi, fun, maxContentLength); 3659 } 3660 3661 /++ 3662 Boilerplate mixin for a main function that uses the [dispatcher] function. 3663 3664 You can send `typeof(null)` as the `Presenter` argument to use a generic one. 3665 3666 History: 3667 Added July 9, 2021 3668 +/ 3669 mixin template DispatcherMain(Presenter, DispatcherArgs...) { 3670 /// forwards to [CustomCgiDispatcherMain] with default args 3671 mixin CustomCgiDispatcherMain!(Cgi, defaultMaxContentLength, Presenter, DispatcherArgs); 3672 } 3673 3674 /// ditto 3675 mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { 3676 class GenericPresenter : WebPresenter!GenericPresenter {} 3677 mixin DispatcherMain!(GenericPresenter, DispatcherArgs); 3678 } 3679 3680 /++ 3681 Allows for a generic [DispatcherMain] with custom arguments. Note you can use [defaultMaxContentLength] as the second argument if you like. 3682 3683 History: 3684 Added May 13, 2023 (dub v11.0) 3685 +/ 3686 mixin template CustomCgiDispatcherMain(CustomCgi, size_t maxContentLength, Presenter, DispatcherArgs...) { 3687 /++ 3688 Handler to the generated presenter you can use from your objects, etc. 3689 +/ 3690 Presenter activePresenter; 3691 3692 /++ 3693 Request handler that creates the presenter then forwards to the [dispatcher] function. 3694 Renders 404 if the dispatcher did not handle the request. 3695 3696 Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js" 3697 +/ 3698 void handler(Cgi cgi) { 3699 auto presenter = new Presenter; 3700 activePresenter = presenter; 3701 scope(exit) activePresenter = null; 3702 3703 if(cgi.pathInfo.length == 0) { 3704 cgi.setResponseLocation(cgi.scriptName ~ "/"); 3705 return; 3706 } 3707 3708 if(cgi.dispatcher!DispatcherArgs(presenter)) 3709 return; 3710 3711 switch(cgi.pathInfo) { 3712 case "/style.css": 3713 cgi.setCache(true); 3714 cgi.setResponseContentType("text/css"); 3715 cgi.write(presenter.style(), true); 3716 break; 3717 case "/script.js": 3718 cgi.setCache(true); 3719 cgi.setResponseContentType("application/javascript"); 3720 cgi.write(presenter.script(), true); 3721 break; 3722 default: 3723 presenter.renderBasicError(cgi, 404); 3724 } 3725 } 3726 mixin CustomCgiMain!(CustomCgi, handler, maxContentLength); 3727 } 3728 3729 /// ditto 3730 mixin template CustomCgiDispatcherMain(CustomCgi, size_t maxContentLength, DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { 3731 class GenericPresenter : WebPresenter!GenericPresenter {} 3732 mixin CustomCgiDispatcherMain!(CustomCgi, maxContentLength, GenericPresenter, DispatcherArgs); 3733 3734 } 3735 3736 private string simpleHtmlEncode(string s) { 3737 return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "<br />\n"); 3738 } 3739 3740 string messageFromException(Throwable t) { 3741 string message; 3742 if(t !is null) { 3743 debug message = t.toString(); 3744 else message = "An unexpected error has occurred."; 3745 } else { 3746 message = "Unknown error"; 3747 } 3748 return message; 3749 } 3750 3751 string plainHttpError(bool isCgi, string type, Throwable t) { 3752 auto message = messageFromException(t); 3753 message = simpleHtmlEncode(message); 3754 3755 return format("%s %s\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s", 3756 isCgi ? "Status:" : "HTTP/1.1", 3757 type, message.length, message); 3758 } 3759 3760 // returns true if we were able to recover reasonably 3761 bool handleException(Cgi cgi, Throwable t) { 3762 if(cgi.isClosed) { 3763 // if the channel has been explicitly closed, we can't handle it here 3764 return true; 3765 } 3766 3767 if(cgi.outputtedResponseData) { 3768 // the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here. 3769 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. 3770 } else { 3771 // no headers are sent, we can send a full blown error and recover 3772 cgi.setCache(false); 3773 cgi.setResponseContentType("text/html"); 3774 cgi.setResponseLocation(null); // cancel the redirect 3775 cgi.setResponseStatus("500 Internal Server Error"); 3776 cgi.write(simpleHtmlEncode(messageFromException(t))); 3777 cgi.close(); 3778 return true; 3779 } 3780 } 3781 3782 bool isCgiRequestMethod(string s) { 3783 s = s.toUpper(); 3784 if(s == "COMMANDLINE") 3785 return true; 3786 foreach(member; __traits(allMembers, Cgi.RequestMethod)) 3787 if(s == member) 3788 return true; 3789 return false; 3790 } 3791 3792 /// If you want to use a subclass of Cgi with generic main, use this mixin. 3793 mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) { 3794 // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere 3795 void main(string[] args) { 3796 cgiMainImpl!(fun, CustomCgi, maxContentLength)(args); 3797 } 3798 } 3799 3800 version(embedded_httpd_processes) 3801 __gshared int processPoolSize = 8; 3802 3803 // Returns true if run. You should exit the program after that. 3804 bool tryAddonServers(string[] args) { 3805 if(args.length > 1) { 3806 // run the special separate processes if needed 3807 switch(args[1]) { 3808 case "--websocket-server": 3809 version(with_addon_servers) 3810 websocketServers[args[2]](args[3 .. $]); 3811 else 3812 printf("Add-on servers not compiled in.\n"); 3813 return true; 3814 case "--websocket-servers": 3815 import core.demangle; 3816 version(with_addon_servers_connections) 3817 foreach(k, v; websocketServers) 3818 writeln(k, "\t", demangle(k)); 3819 return true; 3820 case "--session-server": 3821 version(with_addon_servers) 3822 runSessionServer(); 3823 else 3824 printf("Add-on servers not compiled in.\n"); 3825 return true; 3826 case "--event-server": 3827 version(with_addon_servers) 3828 runEventServer(); 3829 else 3830 printf("Add-on servers not compiled in.\n"); 3831 return true; 3832 case "--timer-server": 3833 version(with_addon_servers) 3834 runTimerServer(); 3835 else 3836 printf("Add-on servers not compiled in.\n"); 3837 return true; 3838 case "--timed-jobs": 3839 import core.demangle; 3840 version(with_addon_servers_connections) 3841 foreach(k, v; scheduledJobHandlers) 3842 writeln(k, "\t", demangle(k)); 3843 return true; 3844 case "--timed-job": 3845 scheduledJobHandlers[args[2]](args[3 .. $]); 3846 return true; 3847 default: 3848 // intentionally blank - do nothing and carry on to run normally 3849 } 3850 } 3851 return false; 3852 } 3853 3854 /// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args. 3855 bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) { 3856 // we support command line thing for easy testing everywhere 3857 // it needs to be called ./app method uri [other args...] 3858 if(args.length >= 3 && isCgiRequestMethod(args[1])) { 3859 Cgi cgi = new CustomCgi(args); 3860 scope(exit) cgi.dispose(); 3861 try { 3862 fun(cgi); 3863 cgi.close(); 3864 } catch(AuthorizationRequiredException are) { 3865 cgi.setResponseStatus("401 Authorization Required"); 3866 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 3867 cgi.close(); 3868 } 3869 writeln(); // just to put a blank line before the prompt cuz it annoys me 3870 // FIXME: put in some footers to show what changes happened in the session 3871 // could make the MockSession be some kind of ReflectableSessionObject or something 3872 return true; 3873 } 3874 return false; 3875 } 3876 3877 /++ 3878 A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] for a complete, up-to-date, example of how it is used internally. 3879 3880 As of version 11 (released August 2023), you can also make things like this: 3881 3882 --- 3883 // listens on both a unix domain socket called `foo` and on the loopback interfaces port 8080 3884 RequestServer server = RequestServer(["http://unix:foo", "http://localhost:8080"]); 3885 3886 // can also: 3887 // RequestServer server = RequestServer(0); // listen on an OS-provided port on all interfaces 3888 3889 // NOT IMPLEMENTED YET 3890 // server.initialize(); // explicit initialization will populate any "any port" things and throw if a bind failed 3891 3892 foreach(listenSpec; server.listenSpecs) { 3893 // you can check what it actually bound to here and see your assigned ports 3894 } 3895 3896 // NOT IMPLEMENTED YET 3897 // server.start!handler(); // starts and runs in the arsd.core event loop 3898 3899 server.serve!handler(); // blocks the thread until the server exits 3900 --- 3901 3902 History: 3903 Added Sept 26, 2020 (release version 8.5). 3904 3905 The `listenSpec` member was added July 31, 2023. 3906 +/ 3907 struct RequestServer { 3908 /++ 3909 Sets the host and port the server will listen on. This is semi-deprecated; the new (as of July 31, 2023) [listenSpec] parameter obsoletes these. You cannot use both together; the listeningHost and listeningPort are ONLY used if listenSpec is null. 3910 +/ 3911 string listeningHost = defaultListeningHost(); 3912 /// ditto 3913 ushort listeningPort = defaultListeningPort(); 3914 3915 static struct ListenSpec { 3916 enum Protocol { 3917 http, 3918 https, 3919 scgi 3920 } 3921 Protocol protocol; 3922 3923 enum AddressType { 3924 ip, 3925 unix, 3926 abstract_ 3927 } 3928 AddressType addressType; 3929 3930 string address; 3931 ushort port; 3932 } 3933 3934 /++ 3935 The array of addresses you want to listen on. The format looks like a url but has a few differences. 3936 3937 This ONLY works on embedded_httpd_threads, embedded_httpd_hybrid, and scgi builds at this time. 3938 3939 `http://localhost:8080` 3940 3941 `http://unix:filename/here` 3942 3943 `scgi://abstract:/name/here` 3944 3945 `http://[::1]:4444` 3946 3947 Note that IPv6 addresses must be enclosed in brackets. If you want to listen on an interface called `unix` or `abstract`, contact me, that is not supported but I could add some kind of escape mechanism. 3948 3949 If you leave off the protocol, it assumes the default based on compile flags. If you only give a number, it is assumed to be a port on any tcp interface. 3950 3951 `localhost:8080` serves the default protocol. 3952 3953 `8080` or `:8080` assumes default protocol on localhost. 3954 3955 The protocols can be `http:`, `https:`, and `scgi:`. Original `cgi` is not supported with this, since it is transactional with a single process. 3956 3957 Valid hosts are an IPv4 address (with a mandatory port), an IPv6 address (with a mandatory port), just a port alone, `unix:/path/to/unix/socket` (which may be a relative path without a leading slash), or `abstract:/path/to/linux/abstract/namespace`. 3958 3959 `http://unix:foo` will serve http over the unix domain socket named `foo` in the current working directory. 3960 3961 $(PITFALL 3962 If you set this to anything non-null (including a non-null, zero-length array) any `listenSpec` entries, [listeningHost] and [listeningPort] are ignored. 3963 ) 3964 3965 Bugs: 3966 The implementation currently ignores the protocol spec in favor of the default compiled in option. 3967 3968 History: 3969 Added July 31, 2023 (dub v11.0) 3970 +/ 3971 string[] listenSpec; 3972 3973 /++ 3974 Uses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the 3975 other hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But 3976 if you have to, you probably want to set this to false and use an explicit threaded server with [serveEmbeddedHttp]) and 3977 [stop] may not work as well. 3978 3979 History: 3980 Added August 12, 2022 (dub v10.9). Previously, this was only configurable through the `-version=cgi_no_fork` 3981 argument to dmd. That version still defines the value of `cgi_use_fork_default`, used to initialize this, for 3982 compatibility. 3983 +/ 3984 bool useFork = cgi_use_fork_default; 3985 3986 /++ 3987 Determines the number of worker threads to spawn per process, for server modes that use worker threads. 0 will use a 3988 default based on the number of cpus modified by the server mode. 3989 3990 History: 3991 Added August 12, 2022 (dub v10.9) 3992 +/ 3993 int numberOfThreads = 0; 3994 3995 /++ 3996 Creates a server configured to listen to multiple URLs. 3997 3998 History: 3999 Added July 31, 2023 (dub v11.0) 4000 +/ 4001 this(string[] listenTo) { 4002 this.listenSpec = listenTo; 4003 } 4004 4005 /// Creates a server object configured to listen on a single host and port. 4006 this(string defaultHost, ushort defaultPort) { 4007 this.listeningHost = defaultHost; 4008 this.listeningPort = defaultPort; 4009 } 4010 4011 /// ditto 4012 this(ushort defaultPort) { 4013 listeningPort = defaultPort; 4014 } 4015 4016 /++ 4017 Reads the command line arguments into the values here. 4018 4019 Possible arguments are `--listen` (can appear multiple times), `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. 4020 4021 Please note you cannot combine `--listen` with `--listening-host` or `--listening-port` / `--port`. Use one or the other style. 4022 +/ 4023 void configureFromCommandLine(string[] args) { 4024 bool portOrHostFound = false; 4025 4026 bool foundPort = false; 4027 bool foundHost = false; 4028 bool foundUid = false; 4029 bool foundGid = false; 4030 bool foundListen = false; 4031 foreach(arg; args) { 4032 if(foundPort) { 4033 listeningPort = to!ushort(arg); 4034 portOrHostFound = true; 4035 foundPort = false; 4036 continue; 4037 } 4038 if(foundHost) { 4039 listeningHost = arg; 4040 portOrHostFound = true; 4041 foundHost = false; 4042 continue; 4043 } 4044 if(foundUid) { 4045 privilegesDropToUid = to!uid_t(arg); 4046 foundUid = false; 4047 continue; 4048 } 4049 if(foundGid) { 4050 privilegesDropToGid = to!gid_t(arg); 4051 foundGid = false; 4052 continue; 4053 } 4054 if(foundListen) { 4055 this.listenSpec ~= arg; 4056 foundListen = false; 4057 continue; 4058 } 4059 if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") 4060 foundHost = true; 4061 else if(arg == "--port" || arg == "-p" || arg == "/port" || arg == "--listening-port") 4062 foundPort = true; 4063 else if(arg == "--uid") 4064 foundUid = true; 4065 else if(arg == "--gid") 4066 foundGid = true; 4067 else if(arg == "--listen") 4068 foundListen = true; 4069 } 4070 4071 if(portOrHostFound && listenSpec.length) { 4072 throw new Exception("You passed both a --listening-host or --listening-port and a --listen argument. You should fix your script to ONLY use --listen arguments."); 4073 } 4074 } 4075 4076 version(Windows) { 4077 private alias uid_t = int; 4078 private alias gid_t = int; 4079 } 4080 4081 /// user (uid) to drop privileges to 4082 /// 0 … do nothing 4083 uid_t privilegesDropToUid = 0; 4084 /// group (gid) to drop privileges to 4085 /// 0 … do nothing 4086 gid_t privilegesDropToGid = 0; 4087 4088 private void dropPrivileges() { 4089 version(Posix) { 4090 import core.sys.posix.unistd; 4091 4092 if (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0) 4093 throw new Exception("Dropping privileges via setgid() failed."); 4094 4095 if (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0) 4096 throw new Exception("Dropping privileges via setuid() failed."); 4097 } 4098 else { 4099 // FIXME: Windows? 4100 //pragma(msg, "Dropping privileges is not implemented for this platform"); 4101 } 4102 4103 // done, set zero 4104 privilegesDropToGid = 0; 4105 privilegesDropToUid = 0; 4106 } 4107 4108 /++ 4109 Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders 4110 4111 History: 4112 Added Oct 10, 2020. 4113 Example: 4114 4115 --- 4116 import arsd.cgi; 4117 void main() { 4118 RequestServer server = RequestServer("127.0.0.1", 6789); 4119 string oauthCode; 4120 string oauthScope; 4121 server.serveHttpOnce!((cgi) { 4122 oauthCode = cgi.request("code"); 4123 oauthScope = cgi.request("scope"); 4124 cgi.write("Thank you, please return to the application."); 4125 }); 4126 // use the code and scope given 4127 } 4128 --- 4129 +/ 4130 void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4131 import std.socket; 4132 4133 bool tcp; 4134 void delegate() cleanup; 4135 auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges); 4136 auto connection = socket.accept(); 4137 doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection); 4138 4139 if(cleanup) 4140 cleanup(); 4141 } 4142 4143 /++ 4144 Starts serving requests according to the current configuration. 4145 +/ 4146 void serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4147 version(netman_httpd) { 4148 // Obsolete! 4149 4150 import arsd.httpd; 4151 // what about forwarding the other constructor args? 4152 // this probably needs a whole redoing... 4153 serveHttp!CustomCgi(&fun, listeningPort);//5005); 4154 return; 4155 } else 4156 version(embedded_httpd_processes) { 4157 serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this); 4158 } else 4159 version(embedded_httpd_threads) { 4160 serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)(); 4161 } else 4162 version(scgi) { 4163 serveScgi!(fun, CustomCgi, maxContentLength)(); 4164 } else 4165 version(fastcgi) { 4166 serveFastCgi!(fun, CustomCgi, maxContentLength)(this); 4167 } else 4168 version(stdio_http) { 4169 serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)(); 4170 } else { 4171 //version=plain_cgi; 4172 handleCgiRequest!(fun, CustomCgi, maxContentLength)(); 4173 } 4174 } 4175 4176 /++ 4177 Runs the embedded HTTP thread server specifically, regardless of which build configuration you have. 4178 4179 If you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though. 4180 +/ 4181 void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(ThisFor!fun _this) { 4182 globalStopFlag = false; 4183 static if(__traits(isStaticFunction, fun)) 4184 alias funToUse = fun; 4185 else 4186 void funToUse(CustomCgi cgi) { 4187 static if(__VERSION__ > 2097) 4188 __traits(child, _this, fun)(cgi); 4189 else static assert(0, "Not implemented in your compiler version!"); 4190 } 4191 auto manager = this.listenSpec is null ? 4192 new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads) : 4193 new ListeningConnectionManager(this.listenSpec, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); 4194 manager.listen(); 4195 } 4196 4197 /++ 4198 Runs the embedded SCGI server specifically, regardless of which build configuration you have. 4199 +/ 4200 void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4201 globalStopFlag = false; 4202 auto manager = this.listenSpec is null ? 4203 new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads) : 4204 new ListeningConnectionManager(this.listenSpec, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); 4205 manager.listen(); 4206 } 4207 4208 /++ 4209 Serves a single "connection", but the connection is spoken on stdin and stdout instead of on a socket. 4210 4211 Intended for cases like working from systemd, like discussed here: [https://forum.dlang.org/post/avmkfdiitirnrenzljwc@forum.dlang.org] 4212 4213 History: 4214 Added May 29, 2021 4215 +/ 4216 void serveSingleHttpConnectionOnStdio(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4217 doThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin()); 4218 } 4219 4220 /++ 4221 The [stop] function sets a flag that request handlers can (and should) check periodically. If a handler doesn't 4222 respond to this flag, the library will force the issue. This determines when and how the issue will be forced. 4223 +/ 4224 enum ForceStop { 4225 /++ 4226 Stops accepting new requests, but lets ones already in the queue start and complete before exiting. 4227 +/ 4228 afterQueuedRequestsComplete, 4229 /++ 4230 Finishes requests already started their handlers, but drops any others in the queue. Streaming handlers 4231 should cooperate and exit gracefully, but if they don't, it will continue waiting for them. 4232 +/ 4233 afterCurrentRequestsComplete, 4234 /++ 4235 Partial response writes will throw an exception, cancelling any streaming response, but complete 4236 writes will continue to process. Request handlers that respect the stop token will also gracefully cancel. 4237 +/ 4238 cancelStreamingRequestsEarly, 4239 /++ 4240 All writes will throw. 4241 +/ 4242 cancelAllRequestsEarly, 4243 /++ 4244 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). 4245 +/ 4246 forciblyTerminate, 4247 } 4248 4249 version(embedded_httpd_processes) {} else 4250 /++ 4251 Stops serving after the current requests are completed. 4252 4253 Bugs: 4254 Not implemented on version=embedded_httpd_processes, version=fastcgi on any system, or embedded_httpd on Windows (it does work on embedded_httpd_hybrid 4255 on Windows however). Only partially implemented on non-Linux posix systems. 4256 4257 You might also try SIGINT perhaps. 4258 4259 The stopPriority is not yet fully implemented. 4260 +/ 4261 static void stop(ForceStop stopPriority = ForceStop.afterCurrentRequestsComplete) { 4262 globalStopFlag = true; 4263 4264 version(Posix) { 4265 if(cancelfd > 0) { 4266 ulong a = 1; 4267 core.sys.posix.unistd.write(cancelfd, &a, a.sizeof); 4268 } 4269 } 4270 version(Windows) { 4271 if(iocp) { 4272 foreach(i; 0 .. 16) // FIXME 4273 PostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null); 4274 } 4275 } 4276 } 4277 } 4278 4279 class AuthorizationRequiredException : Exception { 4280 string type; 4281 string realm; 4282 this(string type, string realm, string file, size_t line) { 4283 this.type = type; 4284 this.realm = realm; 4285 4286 super("Authorization Required", file, line); 4287 } 4288 } 4289 4290 private alias AliasSeq(T...) = T; 4291 4292 version(with_breaking_cgi_features) 4293 mixin(q{ 4294 template ThisFor(alias t) { 4295 static if(__traits(isStaticFunction, t)) { 4296 alias ThisFor = AliasSeq!(); 4297 } else { 4298 alias ThisFor = __traits(parent, t); 4299 } 4300 } 4301 }); 4302 else 4303 alias ThisFor(alias t) = AliasSeq!(); 4304 4305 private __gshared bool globalStopFlag = false; 4306 4307 version(embedded_httpd_processes) 4308 void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) { 4309 import core.sys.posix.unistd; 4310 import core.sys.posix.sys.socket; 4311 import core.sys.posix.netinet.in_; 4312 //import std.c.linux.socket; 4313 4314 int sock = socket(AF_INET, SOCK_STREAM, 0); 4315 if(sock == -1) 4316 throw new Exception("socket"); 4317 4318 cloexec(sock); 4319 4320 { 4321 4322 sockaddr_in addr; 4323 addr.sin_family = AF_INET; 4324 addr.sin_port = htons(params.listeningPort); 4325 auto lh = params.listeningHost; 4326 if(lh.length) { 4327 if(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1) 4328 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."); 4329 } else 4330 addr.sin_addr.s_addr = INADDR_ANY; 4331 4332 // HACKISH 4333 int on = 1; 4334 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof); 4335 // end hack 4336 4337 4338 if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { 4339 close(sock); 4340 throw new Exception("bind"); 4341 } 4342 4343 // FIXME: if this queue is full, it will just ignore it 4344 // and wait for the client to retransmit it. This is an 4345 // obnoxious timeout condition there. 4346 if(sock.listen(128) == -1) { 4347 close(sock); 4348 throw new Exception("listen"); 4349 } 4350 params.dropPrivileges(); 4351 } 4352 4353 version(embedded_httpd_processes_accept_after_fork) {} else { 4354 int pipeReadFd; 4355 int pipeWriteFd; 4356 4357 { 4358 int[2] pipeFd; 4359 if(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) { 4360 import core.stdc.errno; 4361 throw new Exception("pipe failed " ~ to!string(errno)); 4362 } 4363 4364 pipeReadFd = pipeFd[0]; 4365 pipeWriteFd = pipeFd[1]; 4366 } 4367 } 4368 4369 4370 int processCount; 4371 pid_t newPid; 4372 reopen: 4373 while(processCount < processPoolSize) { 4374 newPid = fork(); 4375 if(newPid == 0) { 4376 // start serving on the socket 4377 //ubyte[4096] backingBuffer; 4378 for(;;) { 4379 bool closeConnection; 4380 uint i; 4381 sockaddr addr; 4382 i = addr.sizeof; 4383 version(embedded_httpd_processes_accept_after_fork) { 4384 int s = accept(sock, &addr, &i); 4385 int opt = 1; 4386 import core.sys.posix.netinet.tcp; 4387 // the Cgi class does internal buffering, so disabling this 4388 // helps with latency in many cases... 4389 setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); 4390 cloexec(s); 4391 } else { 4392 int s; 4393 auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s); 4394 if(readret != s.sizeof) { 4395 import core.stdc.errno; 4396 throw new Exception("pipe read failed " ~ to!string(errno)); 4397 } 4398 4399 //writeln("process ", getpid(), " got socket ", s); 4400 } 4401 4402 try { 4403 4404 if(s == -1) 4405 throw new Exception("accept"); 4406 4407 scope(failure) close(s); 4408 //ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer; 4409 auto ir = new BufferedInputRange(s); 4410 //auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer); 4411 4412 while(!ir.empty) { 4413 //ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer; 4414 4415 Cgi cgi; 4416 try { 4417 cgi = new CustomCgi(ir, &closeConnection); 4418 cgi._outputFileHandle = cast(CgiConnectionHandle) s; 4419 // 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. 4420 if(processPoolSize <= 1) 4421 closeConnection = true; 4422 //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection); 4423 } catch(HttpVersionNotSupportedException he) { 4424 sendAll(ir.source, plainHttpError(false, "505 HTTP Version Not Supported", he)); 4425 closeConnection = true; 4426 break; 4427 } catch(Throwable t) { 4428 // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P 4429 // anyway let's kill the connection 4430 version(CRuntime_Musl) { 4431 // LockingTextWriter fails here 4432 // so working around it 4433 auto estr = t.toString(); 4434 stderr.rawWrite(estr); 4435 stderr.rawWrite("\n"); 4436 } else 4437 stderr.writeln(t.toString()); 4438 sendAll(ir.source, plainHttpError(false, "400 Bad Request", t)); 4439 closeConnection = true; 4440 break; 4441 } 4442 assert(cgi !is null); 4443 scope(exit) 4444 cgi.dispose(); 4445 4446 try { 4447 fun(cgi); 4448 cgi.close(); 4449 if(cgi.websocketMode) 4450 closeConnection = true; 4451 4452 } catch(AuthorizationRequiredException are) { 4453 cgi.setResponseStatus("401 Authorization Required"); 4454 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 4455 cgi.close(); 4456 } catch(ConnectionException ce) { 4457 closeConnection = true; 4458 } catch(Throwable t) { 4459 // a processing error can be recovered from 4460 version(CRuntime_Musl) { 4461 // LockingTextWriter fails here 4462 // so working around it 4463 auto estr = t.toString(); 4464 stderr.rawWrite(estr); 4465 } else { 4466 stderr.writeln(t.toString); 4467 } 4468 if(!handleException(cgi, t)) 4469 closeConnection = true; 4470 } 4471 4472 if(closeConnection) { 4473 ir.source.close(); 4474 break; 4475 } else { 4476 if(!ir.empty) 4477 ir.popFront(); // get the next 4478 else if(ir.sourceClosed) { 4479 ir.source.close(); 4480 } 4481 } 4482 } 4483 4484 ir.source.close(); 4485 } catch(Throwable t) { 4486 version(CRuntime_Musl) {} else 4487 debug writeln(t); 4488 // most likely cause is a timeout 4489 } 4490 } 4491 } else if(newPid < 0) { 4492 throw new Exception("fork failed"); 4493 } else { 4494 processCount++; 4495 } 4496 } 4497 4498 // the parent should wait for its children... 4499 if(newPid) { 4500 import core.sys.posix.sys.wait; 4501 4502 version(embedded_httpd_processes_accept_after_fork) {} else { 4503 import core.sys.posix.sys.select; 4504 int[] fdQueue; 4505 while(true) { 4506 // writeln("select call"); 4507 int nfds = pipeWriteFd; 4508 if(sock > pipeWriteFd) 4509 nfds = sock; 4510 nfds += 1; 4511 fd_set read_fds; 4512 fd_set write_fds; 4513 FD_ZERO(&read_fds); 4514 FD_ZERO(&write_fds); 4515 FD_SET(sock, &read_fds); 4516 if(fdQueue.length) 4517 FD_SET(pipeWriteFd, &write_fds); 4518 auto ret = select(nfds, &read_fds, &write_fds, null, null); 4519 if(ret == -1) { 4520 import core.stdc.errno; 4521 if(errno == EINTR) 4522 goto try_wait; 4523 else 4524 throw new Exception("wtf select"); 4525 } 4526 4527 int s = -1; 4528 if(FD_ISSET(sock, &read_fds)) { 4529 uint i; 4530 sockaddr addr; 4531 i = addr.sizeof; 4532 s = accept(sock, &addr, &i); 4533 cloexec(s); 4534 import core.sys.posix.netinet.tcp; 4535 int opt = 1; 4536 setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); 4537 } 4538 4539 if(FD_ISSET(pipeWriteFd, &write_fds)) { 4540 if(s == -1 && fdQueue.length) { 4541 s = fdQueue[0]; 4542 fdQueue = fdQueue[1 .. $]; // FIXME reuse buffer 4543 } 4544 write_fd(pipeWriteFd, &s, s.sizeof, s); 4545 close(s); // we are done with it, let the other process take ownership 4546 } else 4547 fdQueue ~= s; 4548 } 4549 } 4550 4551 try_wait: 4552 4553 int status; 4554 while(-1 != wait(&status)) { 4555 version(CRuntime_Musl) {} else { 4556 import std.stdio; writeln("Process died ", status); 4557 } 4558 processCount--; 4559 goto reopen; 4560 } 4561 close(sock); 4562 } 4563 } 4564 4565 version(fastcgi) 4566 void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) { 4567 // SetHandler fcgid-script 4568 FCGX_Stream* input, output, error; 4569 FCGX_ParamArray env; 4570 4571 4572 4573 const(ubyte)[] getFcgiChunk() { 4574 const(ubyte)[] ret; 4575 while(FCGX_HasSeenEOF(input) != -1) 4576 ret ~= cast(ubyte) FCGX_GetChar(input); 4577 return ret; 4578 } 4579 4580 void writeFcgi(const(ubyte)[] data) { 4581 FCGX_PutStr(data.ptr, data.length, output); 4582 } 4583 4584 void doARequest() { 4585 string[string] fcgienv; 4586 4587 for(auto e = env; e !is null && *e !is null; e++) { 4588 string cur = to!string(*e); 4589 auto idx = cur.indexOf("="); 4590 string name, value; 4591 if(idx == -1) 4592 name = cur; 4593 else { 4594 name = cur[0 .. idx]; 4595 value = cur[idx + 1 .. $]; 4596 } 4597 4598 fcgienv[name] = value; 4599 } 4600 4601 void flushFcgi() { 4602 FCGX_FFlush(output); 4603 } 4604 4605 Cgi cgi; 4606 try { 4607 cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); 4608 } catch(Throwable t) { 4609 FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); 4610 writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t)); 4611 return; //continue; 4612 } 4613 assert(cgi !is null); 4614 scope(exit) cgi.dispose(); 4615 try { 4616 fun(cgi); 4617 cgi.close(); 4618 } catch(AuthorizationRequiredException are) { 4619 cgi.setResponseStatus("401 Authorization Required"); 4620 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 4621 cgi.close(); 4622 } catch(Throwable t) { 4623 // log it to the error stream 4624 FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); 4625 // handle it for the user, if we can 4626 if(!handleException(cgi, t)) 4627 return; // continue; 4628 } 4629 } 4630 4631 auto lp = params.listeningPort; 4632 auto host = params.listeningHost; 4633 4634 FCGX_Request request; 4635 if(lp || !host.empty) { 4636 // if a listening port was specified on the command line, we want to spawn ourself 4637 // (needed for nginx without spawn-fcgi, e.g. on Windows) 4638 FCGX_Init(); 4639 4640 int sock; 4641 4642 if(host.startsWith("unix:")) { 4643 sock = FCGX_OpenSocket(toStringz(params.listeningHost["unix:".length .. $]), 12); 4644 } else if(host.startsWith("abstract:")) { 4645 sock = FCGX_OpenSocket(toStringz("\0" ~ params.listeningHost["abstract:".length .. $]), 12); 4646 } else { 4647 sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); 4648 } 4649 4650 if(sock < 0) 4651 throw new Exception("Couldn't listen on the port"); 4652 FCGX_InitRequest(&request, sock, 0); 4653 while(FCGX_Accept_r(&request) >= 0) { 4654 input = request.inStream; 4655 output = request.outStream; 4656 error = request.errStream; 4657 env = request.envp; 4658 doARequest(); 4659 } 4660 } else { 4661 // otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd) 4662 // using the version with a global variable since we are separate processes anyway 4663 while(FCGX_Accept(&input, &output, &error, &env) >= 0) { 4664 doARequest(); 4665 } 4666 } 4667 } 4668 4669 /// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others. 4670 ushort defaultListeningPort() @safe { 4671 version(netman_httpd) 4672 return 8080; 4673 else version(embedded_httpd_processes) 4674 return 8085; 4675 else version(embedded_httpd_threads) 4676 return 8085; 4677 else version(scgi) 4678 return 4000; 4679 else 4680 return 0; 4681 } 4682 4683 /// 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. 4684 string defaultListeningHost() @safe { 4685 version(netman_httpd) 4686 return null; 4687 else version(embedded_httpd_processes) 4688 return null; 4689 else version(embedded_httpd_threads) 4690 return null; 4691 else version(scgi) 4692 return "127.0.0.1"; 4693 else 4694 return null; 4695 4696 } 4697 4698 /++ 4699 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`. 4700 4701 Please note that this may spawn other helper processes that will call `main` again. It does this currently for the timer server and event source server (and the quasi-deprecated web socket server). 4702 4703 Params: 4704 fun = Your request handler 4705 CustomCgi = a subclass of Cgi, if you wise to customize it further 4706 maxContentLength = max POST size you want to allow 4707 args = command-line arguments 4708 4709 History: 4710 Documented Sept 26, 2020. 4711 +/ 4712 void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) { 4713 if(tryAddonServers(args)) 4714 return; 4715 4716 if(trySimulatedRequest!(fun, CustomCgi)(args)) 4717 return; 4718 4719 RequestServer server; 4720 // you can change the port here if you like 4721 // server.listeningPort = 9000; 4722 4723 // then call this to let the command line args override your default 4724 server.configureFromCommandLine(args); 4725 4726 // and serve the request(s). 4727 server.serve!(fun, CustomCgi, maxContentLength)(); 4728 } 4729 4730 //version(plain_cgi) 4731 void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { 4732 // standard CGI is the default version 4733 4734 4735 // Set stdin to binary mode if necessary to avoid mangled newlines 4736 // the fact that stdin is global means this could be trouble but standard cgi request 4737 // handling is one per process anyway so it shouldn't actually be threaded here or anything. 4738 version(Windows) { 4739 version(Win64) 4740 _setmode(std.stdio.stdin.fileno(), 0x8000); 4741 else 4742 setmode(std.stdio.stdin.fileno(), 0x8000); 4743 } 4744 4745 Cgi cgi; 4746 try { 4747 cgi = new CustomCgi(maxContentLength); 4748 version(Posix) 4749 cgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout 4750 else version(Windows) 4751 cgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE); 4752 else static assert(0); 4753 } catch(Throwable t) { 4754 version(CRuntime_Musl) { 4755 // LockingTextWriter fails here 4756 // so working around it 4757 auto s = t.toString(); 4758 stderr.rawWrite(s); 4759 stdout.rawWrite(plainHttpError(true, "400 Bad Request", t)); 4760 } else { 4761 stderr.writeln(t.msg); 4762 // the real http server will probably handle this; 4763 // most likely, this is a bug in Cgi. But, oh well. 4764 stdout.write(plainHttpError(true, "400 Bad Request", t)); 4765 } 4766 return; 4767 } 4768 assert(cgi !is null); 4769 scope(exit) cgi.dispose(); 4770 4771 try { 4772 fun(cgi); 4773 cgi.close(); 4774 } catch(AuthorizationRequiredException are) { 4775 cgi.setResponseStatus("401 Authorization Required"); 4776 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 4777 cgi.close(); 4778 } catch (Throwable t) { 4779 version(CRuntime_Musl) { 4780 // LockingTextWriter fails here 4781 // so working around it 4782 auto s = t.msg; 4783 stderr.rawWrite(s); 4784 } else { 4785 stderr.writeln(t.msg); 4786 } 4787 if(!handleException(cgi, t)) 4788 return; 4789 } 4790 } 4791 4792 private __gshared int cancelfd = -1; 4793 4794 /+ 4795 The event loop for embedded_httpd_threads will prolly fiber dispatch 4796 cgi constructors too, so slow posts will not monopolize a worker thread. 4797 4798 May want to provide the worker task system just need to ensure all the fibers 4799 has a big enough stack for real work... would also ideally like to reuse them. 4800 4801 4802 So prolly bir would switch it to nonblocking. If it would block, it epoll 4803 registers one shot with this existing fiber to take it over. 4804 4805 new connection comes in. it picks a fiber off the free list, 4806 or if there is none, it creates a new one. this fiber handles 4807 this connection the whole time. 4808 4809 epoll triggers the fiber when something comes in. it is called by 4810 a random worker thread, it might change at any time. at least during 4811 the constructor. maybe into the main body it will stay tied to a thread 4812 just so TLS stuff doesn't randomly change in the middle. but I could 4813 specify if you yield all bets are off. 4814 4815 when the request is finished, if there's more data buffered, it just 4816 keeps going. if there is no more data buffered, it epoll ctls to 4817 get triggered when more data comes in. all one shot. 4818 4819 when a connection is closed, the fiber returns and is then reset 4820 and added to the free list. if the free list is full, the fiber is 4821 just freed, this means it will balloon to a certain size but not generally 4822 grow beyond that unless the activity keeps going. 4823 4824 256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory. 4825 4826 So the fiber has its own magic methods to read and write. if they would block, it registers 4827 for epoll and yields. when it returns, it read/writes and then returns back normal control. 4828 4829 basically you issue the command and it tells you when it is done 4830 4831 it needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued 4832 4833 +/ 4834 4835 /++ 4836 The stack size when a fiber is created. You can set this from your main or from a shared static constructor 4837 to optimize your memory use if you know you don't need this much space. Be careful though, some functions use 4838 more stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast! 4839 4840 History: 4841 Added July 10, 2021. Previously, it used the druntime default of 16 KB. 4842 +/ 4843 version(cgi_use_fiber) 4844 __gshared size_t fiberStackSize = 4096 * 100; 4845 4846 version(cgi_use_fiber) 4847 class CgiFiber : Fiber { 4848 private void function(Socket) f_handler; 4849 private void f_handler_dg(Socket s) { // to avoid extra allocation w/ function 4850 f_handler(s); 4851 } 4852 this(void function(Socket) handler) { 4853 this.f_handler = handler; 4854 this(&f_handler_dg); 4855 } 4856 4857 this(void delegate(Socket) handler) { 4858 this.handler = handler; 4859 super(&run, fiberStackSize); 4860 } 4861 4862 Socket connection; 4863 void delegate(Socket) handler; 4864 4865 void run() { 4866 handler(connection); 4867 } 4868 4869 void delegate() postYield; 4870 4871 private void setPostYield(scope void delegate() py) @nogc { 4872 postYield = cast(void delegate()) py; 4873 } 4874 4875 void proceed() { 4876 try { 4877 call(); 4878 auto py = postYield; 4879 postYield = null; 4880 if(py !is null) 4881 py(); 4882 } catch(Exception e) { 4883 if(connection) 4884 connection.close(); 4885 goto terminate; 4886 } 4887 4888 if(state == State.TERM) { 4889 terminate: 4890 import core.memory; 4891 GC.removeRoot(cast(void*) this); 4892 } 4893 } 4894 } 4895 4896 version(cgi_use_fiber) 4897 version(Windows) { 4898 4899 extern(Windows) private { 4900 4901 import core.sys.windows.mswsock; 4902 4903 alias GROUP=uint; 4904 alias LPWSAPROTOCOL_INFOW = void*; 4905 SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags); 4906 alias WSASend = arsd.core.WSASend; 4907 alias WSARecv = arsd.core.WSARecv; 4908 alias WSABUF = arsd.core.WSABUF; 4909 4910 /+ 4911 int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); 4912 int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); 4913 4914 struct WSABUF { 4915 ULONG len; 4916 CHAR *buf; 4917 } 4918 +/ 4919 alias LPWSABUF = WSABUF*; 4920 4921 alias WSAOVERLAPPED = OVERLAPPED; 4922 alias LPWSAOVERLAPPED = LPOVERLAPPED; 4923 /+ 4924 4925 alias LPFN_ACCEPTEX = 4926 BOOL 4927 function( 4928 SOCKET sListenSocket, 4929 SOCKET sAcceptSocket, 4930 //_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, 4931 void* lpOutputBuffer, 4932 WORD dwReceiveDataLength, 4933 WORD dwLocalAddressLength, 4934 WORD dwRemoteAddressLength, 4935 LPDWORD lpdwBytesReceived, 4936 LPOVERLAPPED lpOverlapped 4937 ); 4938 4939 enum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]); 4940 +/ 4941 4942 enum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]); 4943 } 4944 4945 private class PseudoblockingOverlappedSocket : Socket { 4946 SOCKET handle; 4947 4948 CgiFiber fiber; 4949 4950 this(AddressFamily af, SocketType st) { 4951 auto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/); 4952 if(!handle) 4953 throw new Exception("WSASocketW"); 4954 this.handle = handle; 4955 4956 iocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0); 4957 4958 if(iocp is null) { 4959 writeln(GetLastError()); 4960 throw new Exception("CreateIoCompletionPort"); 4961 } 4962 4963 super(cast(socket_t) handle, af); 4964 } 4965 this() pure nothrow @trusted { assert(0); } 4966 4967 override void blocking(bool) {} // meaningless to us, just ignore it. 4968 4969 protected override Socket accepting() pure nothrow { 4970 assert(0); 4971 } 4972 4973 bool addressesParsed; 4974 Address la; 4975 Address ra; 4976 4977 private void populateAddresses() { 4978 if(addressesParsed) 4979 return; 4980 addressesParsed = true; 4981 4982 int lalen, ralen; 4983 4984 sockaddr_in* la; 4985 sockaddr_in* ra; 4986 4987 lpfnGetAcceptExSockaddrs( 4988 scratchBuffer.ptr, 4989 0, // same as in the AcceptEx call! 4990 sockaddr_in.sizeof + 16, 4991 sockaddr_in.sizeof + 16, 4992 cast(sockaddr**) &la, 4993 &lalen, 4994 cast(sockaddr**) &ra, 4995 &ralen 4996 ); 4997 4998 if(la) 4999 this.la = new InternetAddress(*la); 5000 if(ra) 5001 this.ra = new InternetAddress(*ra); 5002 5003 } 5004 5005 override @property @trusted Address localAddress() { 5006 populateAddresses(); 5007 return la; 5008 } 5009 override @property @trusted Address remoteAddress() { 5010 populateAddresses(); 5011 return ra; 5012 } 5013 5014 PseudoblockingOverlappedSocket accepted; 5015 5016 __gshared static LPFN_ACCEPTEX lpfnAcceptEx; 5017 __gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs; 5018 5019 override Socket accept() @trusted { 5020 __gshared static LPFN_ACCEPTEX lpfnAcceptEx; 5021 5022 if(lpfnAcceptEx is null) { 5023 DWORD dwBytes; 5024 GUID GuidAcceptEx = WSAID_ACCEPTEX; 5025 5026 auto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, 5027 &GuidAcceptEx, GuidAcceptEx.sizeof, 5028 &lpfnAcceptEx, lpfnAcceptEx.sizeof, 5029 &dwBytes, null, null); 5030 5031 GuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS; 5032 iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, 5033 &GuidAcceptEx, GuidAcceptEx.sizeof, 5034 &lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof, 5035 &dwBytes, null, null); 5036 5037 } 5038 5039 auto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); 5040 accepted = pfa; 5041 5042 SOCKET pendingForAccept = pfa.handle; 5043 DWORD ignored; 5044 5045 auto ret = lpfnAcceptEx(handle, 5046 pendingForAccept, 5047 // buffer to receive up front 5048 pfa.scratchBuffer.ptr, 5049 0, 5050 // size of local and remote addresses. normally + 16. 5051 sockaddr_in.sizeof + 16, 5052 sockaddr_in.sizeof + 16, 5053 &ignored, // bytes would be given through the iocp instead but im not even requesting the thing 5054 &overlapped 5055 ); 5056 5057 return pfa; 5058 } 5059 5060 override void connect(Address to) { assert(0); } 5061 5062 DWORD lastAnswer; 5063 ubyte[1024] scratchBuffer; 5064 static assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32); 5065 5066 WSABUF[1] buffer; 5067 OVERLAPPED overlapped; 5068 override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted { 5069 overlapped = overlapped.init; 5070 buffer[0].len = cast(DWORD) buf.length; 5071 buffer[0].buf = cast(ubyte*) buf.ptr; 5072 fiber.setPostYield( () { 5073 if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) { 5074 if(GetLastError() != 997) { 5075 //throw new Exception("WSASend fail"); 5076 } 5077 } 5078 }); 5079 5080 Fiber.yield(); 5081 return lastAnswer; 5082 } 5083 override ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted { 5084 overlapped = overlapped.init; 5085 buffer[0].len = cast(DWORD) buf.length; 5086 buffer[0].buf = cast(ubyte*) buf.ptr; 5087 5088 DWORD flags2 = 0; 5089 5090 fiber.setPostYield(() { 5091 if(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) { 5092 if(GetLastError() != 997) { 5093 //writeln("WSARecv ", WSAGetLastError()); 5094 //throw new Exception("WSARecv fail"); 5095 } 5096 } 5097 }); 5098 5099 Fiber.yield(); 5100 return lastAnswer; 5101 } 5102 5103 // I might go back and implement these for udp things. 5104 override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags, ref Address from) @trusted { 5105 assert(0); 5106 } 5107 override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags) @trusted { 5108 assert(0); 5109 } 5110 override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags, Address to) @trusted { 5111 assert(0); 5112 } 5113 override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags) @trusted { 5114 assert(0); 5115 } 5116 5117 // lol overload sets 5118 alias send = typeof(super).send; 5119 alias receive = typeof(super).receive; 5120 alias sendTo = typeof(super).sendTo; 5121 alias receiveFrom = typeof(super).receiveFrom; 5122 5123 } 5124 } 5125 5126 void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { 5127 assert(connection !is null); 5128 version(cgi_use_fiber) { 5129 auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun)); 5130 5131 version(Windows) { 5132 (cast(PseudoblockingOverlappedSocket) connection).fiber = fiber; 5133 } 5134 5135 import core.memory; 5136 GC.addRoot(cast(void*) fiber); 5137 fiber.connection = connection; 5138 fiber.proceed(); 5139 } else { 5140 doThreadHttpConnectionGuts!(CustomCgi, fun)(connection); 5141 } 5142 } 5143 5144 /+ 5145 5146 /+ 5147 The represents a recyclable per-task arena allocator. The default is to let the GC manage the whole block as a large array, meaning if a reference into it is escaped, it waste memory but is not dangerous. If you don't escape any references to it and don't do anything special, the GC collects it. 5148 5149 But, if you call `cgi.recyclable = true`, the memory is retained for the next request on the thread. If a reference is escaped, it is the user's problem; it can be modified (and break the `immutable` guarantees!) and thus be memory unsafe. They're taking responsibility for doing it right when they call `escape`. But if they do it right and opt into recycling, the memory is all reused to give a potential boost without requiring the GC's involvement. 5150 5151 What if one request used an abnormally large amount of memory though? Will recycling it keep that pinned forever? No, that's why it keeps track of some stats. If a working set was significantly above average and not fully utilized for a while, it will just let the GC have it again despite your suggestion to recycle it. 5152 5153 Be warned that growing the memory block may release the old, smaller block for garbage collection. If you retained references to it, it may not be collectable and lead to some unnecessary memory growth. It is probably best to try to keep the things sized in a continuous block that doesn't have to grow often. 5154 5155 Internally, it is broken up into a few blocks: 5156 * the request block. This holds the incoming request and associated data (parsed headers, variables, etc). 5157 * the scannable block. this holds pointers arrays, classes, etc. associated with this request, so named because the GC scans it. 5158 * the response block. This holds the output buffer. 5159 5160 And I may add more later if I decide to open this up to outside user code. 5161 5162 The scannable block is separate to limit the amount of work the GC has to do; no point asking it to scan that which need not be scanned. 5163 5164 The request and response blocks are separated because they will have different typical sizes, with the request likely being less predictable. Being able to release one to the GC while recycling the other might help, and having them grow independently (if needed) may also prevent some pain. 5165 5166 All of this are internal implementation details subject to change at any time without notice. It is valid for my recycle method to do absolutely nothing; the GC also eventually recycles memory! 5167 5168 Each active task can have its own recyclable memory object. When you recycle it, it is added to a thread-local freelist. If the list is excessively large, entries maybe discarded at random and left for the GC to prevent a temporary burst of activity from leading to a permanent waste of memory. 5169 +/ 5170 struct RecyclableMemory { 5171 private ubyte[] inputBuffer; 5172 private ubyte[] processedRequestBlock; 5173 private void[] scannableBlock; 5174 private ubyte[] outputBuffer; 5175 5176 RecyclableMemory* next; 5177 } 5178 5179 /++ 5180 This emulates the D associative array interface with a different internal implementation. 5181 5182 string s = cgi.get["foo"]; // just does cgi.getArray[x][$-1]; 5183 string[] arr = cgi.getArray["foo"]; 5184 5185 "foo" in cgi.get 5186 5187 foreach(k, v; cgi.get) 5188 5189 cgi.get.toAA // for compatibility 5190 5191 // and this can urldecode lazily tbh... in-place even, since %xx is always longer than a single char thing it turns into... 5192 ... but how does it mark that it has already been processed in-place? it'd have to just add it to the index then. 5193 5194 deprecated alias toAA this; 5195 +/ 5196 struct VariableCollection { 5197 private VariableArrayCollection* vac; 5198 5199 const(char[]) opIndex(scope const char[] key) { 5200 return (*vac)[key][$-1]; 5201 } 5202 5203 const(char[]*) opBinaryRight(string op : "in")(scope const char[] key) { 5204 return key in (*vac); 5205 } 5206 5207 int opApply(int delegate(scope const(char)[] key, scope const(char)[] value) dg) { 5208 foreach(k, v; *vac) { 5209 if(auto res = dg(k, v[$-1])) 5210 return res; 5211 } 5212 return 0; 5213 } 5214 5215 immutable(string[string]) toAA() { 5216 string[string] aa; 5217 foreach(k, v; *vac) 5218 aa[k.idup] = v[$-1].idup; 5219 return aa; 5220 } 5221 5222 deprecated alias toAA this; 5223 } 5224 5225 struct VariableArrayCollection { 5226 /+ 5227 This needs the actual implementation of looking it up. As it pulls data, it should 5228 decode and index for later. 5229 5230 The index will go into a block attached to the cgi object and it should prolly be sorted 5231 something like 5232 5233 [count of names] 5234 [slice to name][count of values][slice to value, decoded in-place, ...] 5235 ... 5236 +/ 5237 private Cgi cgi; 5238 5239 const(char[][]) opIndex(scope const char[] key) { 5240 return null; 5241 } 5242 5243 const(char[][]*) opBinaryRight(string op : "in")(scope const char[] key) { 5244 return null; 5245 } 5246 5247 // int opApply(int delegate(scope const(char)[] key, scope const(char)[][] value) dg) 5248 5249 immutable(string[string]) toAA() { 5250 return null; 5251 } 5252 5253 deprecated alias toAA this; 5254 5255 } 5256 5257 struct HeaderCollection { 5258 5259 } 5260 +/ 5261 5262 void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) { 5263 scope(failure) { 5264 // catch all for other errors 5265 try { 5266 sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); 5267 connection.close(); 5268 } catch(Exception e) {} // swallow it, we're aborting anyway. 5269 } 5270 5271 bool closeConnection = alwaysCloseConnection; 5272 5273 /+ 5274 ubyte[4096] inputBuffer = void; 5275 ubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void; 5276 ubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void; 5277 5278 birBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[]; 5279 BufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr; 5280 ir.__ctor(connection, inputBuffer[], true); 5281 +/ 5282 5283 auto ir = new BufferedInputRange(connection); 5284 5285 while(!ir.empty) { 5286 5287 if(ir.view.length == 0) { 5288 ir.popFront(); 5289 if(ir.sourceClosed) { 5290 connection.close(); 5291 closeConnection = true; 5292 break; 5293 } 5294 } 5295 5296 Cgi cgi; 5297 try { 5298 cgi = new CustomCgi(ir, &closeConnection); 5299 // There's a bunch of these casts around because the type matches up with 5300 // the -version=.... specifiers, just you can also create a RequestServer 5301 // and instantiate the things where the types don't match up. It isn't exactly 5302 // correct but I also don't care rn. Might FIXME and either remove it later or something. 5303 cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; 5304 } catch(ConnectionClosedException ce) { 5305 closeConnection = true; 5306 break; 5307 } catch(ConnectionException ce) { 5308 // broken pipe or something, just abort the connection 5309 closeConnection = true; 5310 break; 5311 } catch(HttpVersionNotSupportedException ve) { 5312 sendAll(connection, plainHttpError(false, "505 HTTP Version Not Supported", ve)); 5313 closeConnection = true; 5314 break; 5315 } catch(Throwable t) { 5316 // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P 5317 // anyway let's kill the connection 5318 version(CRuntime_Musl) { 5319 stderr.rawWrite(t.toString()); 5320 stderr.rawWrite("\n"); 5321 } else { 5322 stderr.writeln(t.toString()); 5323 } 5324 sendAll(connection, plainHttpError(false, "400 Bad Request", t)); 5325 closeConnection = true; 5326 break; 5327 } 5328 assert(cgi !is null); 5329 scope(exit) 5330 cgi.dispose(); 5331 5332 try { 5333 fun(cgi); 5334 cgi.close(); 5335 if(cgi.websocketMode) 5336 closeConnection = true; 5337 } catch(AuthorizationRequiredException are) { 5338 cgi.setResponseStatus("401 Authorization Required"); 5339 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 5340 cgi.close(); 5341 } catch(ConnectionException ce) { 5342 // broken pipe or something, just abort the connection 5343 closeConnection = true; 5344 } catch(ConnectionClosedException ce) { 5345 // broken pipe or something, just abort the connection 5346 closeConnection = true; 5347 } catch(Throwable t) { 5348 // a processing error can be recovered from 5349 version(CRuntime_Musl) {} else 5350 stderr.writeln(t.toString); 5351 if(!handleException(cgi, t)) 5352 closeConnection = true; 5353 } 5354 5355 if(globalStopFlag) 5356 closeConnection = true; 5357 5358 if(closeConnection || alwaysCloseConnection) { 5359 connection.shutdown(SocketShutdown.BOTH); 5360 connection.close(); 5361 ir.dispose(); 5362 closeConnection = false; // don't reclose after loop 5363 break; 5364 } else { 5365 if(ir.front.length) { 5366 ir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along 5367 } else if(ir.sourceClosed) { 5368 ir.source.shutdown(SocketShutdown.BOTH); 5369 ir.source.close(); 5370 ir.dispose(); 5371 closeConnection = false; 5372 } else { 5373 continue; 5374 // break; // this was for a keepalive experiment 5375 } 5376 } 5377 } 5378 5379 if(closeConnection) { 5380 connection.shutdown(SocketShutdown.BOTH); 5381 connection.close(); 5382 ir.dispose(); 5383 } 5384 5385 // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection! 5386 } 5387 5388 void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) { 5389 // and now we can buffer 5390 scope(failure) 5391 connection.close(); 5392 5393 import al = std.algorithm; 5394 5395 size_t size; 5396 5397 string[string] headers; 5398 5399 auto range = new BufferedInputRange(connection); 5400 more_data: 5401 auto chunk = range.front(); 5402 // waiting for colon for header length 5403 auto idx = indexOf(cast(string) chunk, ':'); 5404 if(idx == -1) { 5405 try { 5406 range.popFront(); 5407 } catch(Exception e) { 5408 // it is just closed, no big deal 5409 connection.close(); 5410 return; 5411 } 5412 goto more_data; 5413 } 5414 5415 size = to!size_t(cast(string) chunk[0 .. idx]); 5416 chunk = range.consume(idx + 1); 5417 // reading headers 5418 if(chunk.length < size) 5419 range.popFront(0, size + 1); 5420 // we are now guaranteed to have enough 5421 chunk = range.front(); 5422 assert(chunk.length > size); 5423 5424 idx = 0; 5425 string key; 5426 string value; 5427 foreach(part; al.splitter(chunk, '\0')) { 5428 if(idx & 1) { // odd is value 5429 value = cast(string)(part.idup); 5430 headers[key] = value; // commit 5431 } else 5432 key = cast(string)(part.idup); 5433 idx++; 5434 } 5435 5436 enforce(chunk[size] == ','); // the terminator 5437 5438 range.consume(size + 1); 5439 // reading data 5440 // this will be done by Cgi 5441 5442 const(ubyte)[] getScgiChunk() { 5443 // we are already primed 5444 auto data = range.front(); 5445 if(data.length == 0 && !range.sourceClosed) { 5446 range.popFront(0); 5447 data = range.front(); 5448 } else if (range.sourceClosed) 5449 range.source.close(); 5450 5451 return data; 5452 } 5453 5454 void writeScgi(const(ubyte)[] data) { 5455 sendAll(connection, data); 5456 } 5457 5458 void flushScgi() { 5459 // I don't *think* I have to do anything.... 5460 } 5461 5462 Cgi cgi; 5463 try { 5464 cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); 5465 cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; 5466 } catch(Throwable t) { 5467 sendAll(connection, plainHttpError(true, "400 Bad Request", t)); 5468 connection.close(); 5469 return; // this connection is dead 5470 } 5471 assert(cgi !is null); 5472 scope(exit) cgi.dispose(); 5473 try { 5474 fun(cgi); 5475 cgi.close(); 5476 connection.close(); 5477 5478 } catch(AuthorizationRequiredException are) { 5479 cgi.setResponseStatus("401 Authorization Required"); 5480 cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\""); 5481 cgi.close(); 5482 } catch(Throwable t) { 5483 // no std err 5484 if(!handleException(cgi, t)) { 5485 connection.close(); 5486 return; 5487 } else { 5488 connection.close(); 5489 return; 5490 } 5491 } 5492 } 5493 5494 string printDate(DateTime date) { 5495 char[29] buffer = void; 5496 printDateToBuffer(date, buffer[]); 5497 return buffer.idup; 5498 } 5499 5500 int printDateToBuffer(DateTime date, char[] buffer) @nogc { 5501 assert(buffer.length >= 29); 5502 // 29 static length ? 5503 5504 static immutable daysOfWeek = [ 5505 "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" 5506 ]; 5507 5508 static immutable months = [ 5509 null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 5510 ]; 5511 5512 buffer[0 .. 3] = daysOfWeek[date.dayOfWeek]; 5513 buffer[3 .. 5] = ", "; 5514 buffer[5] = date.day / 10 + '0'; 5515 buffer[6] = date.day % 10 + '0'; 5516 buffer[7] = ' '; 5517 buffer[8 .. 11] = months[date.month]; 5518 buffer[11] = ' '; 5519 auto y = date.year; 5520 buffer[12] = cast(char) (y / 1000 + '0'); y %= 1000; 5521 buffer[13] = cast(char) (y / 100 + '0'); y %= 100; 5522 buffer[14] = cast(char) (y / 10 + '0'); y %= 10; 5523 buffer[15] = cast(char) (y + '0'); 5524 buffer[16] = ' '; 5525 buffer[17] = date.hour / 10 + '0'; 5526 buffer[18] = date.hour % 10 + '0'; 5527 buffer[19] = ':'; 5528 buffer[20] = date.minute / 10 + '0'; 5529 buffer[21] = date.minute % 10 + '0'; 5530 buffer[22] = ':'; 5531 buffer[23] = date.second / 10 + '0'; 5532 buffer[24] = date.second % 10 + '0'; 5533 buffer[25 .. $] = " GMT"; 5534 5535 return 29; 5536 } 5537 5538 5539 // Referencing this gigantic typeid seems to remind the compiler 5540 // to actually put the symbol in the object file. I guess the immutable 5541 // assoc array array isn't actually included in druntime 5542 void hackAroundLinkerError() { 5543 stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString()); 5544 stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString()); 5545 stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString()); 5546 stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString()); 5547 stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString()); 5548 stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString()); 5549 stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString()); 5550 // this is getting kinda ridiculous btw. Moving assoc arrays 5551 // to the library is the pain that keeps on coming. 5552 5553 // eh this broke the build on the work server 5554 // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])])); 5555 stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString()); 5556 } 5557 5558 5559 5560 5561 5562 version(fastcgi) { 5563 pragma(lib, "fcgi"); 5564 5565 static if(size_t.sizeof == 8) // 64 bit 5566 alias long c_int; 5567 else 5568 alias int c_int; 5569 5570 extern(C) { 5571 struct FCGX_Stream { 5572 ubyte* rdNext; 5573 ubyte* wrNext; 5574 ubyte* stop; 5575 ubyte* stopUnget; 5576 c_int isReader; 5577 c_int isClosed; 5578 c_int wasFCloseCalled; 5579 c_int FCGI_errno; 5580 void* function(FCGX_Stream* stream) fillBuffProc; 5581 void* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc; 5582 void* data; 5583 } 5584 5585 // note: this is meant to be opaque, so don't access it directly 5586 struct FCGX_Request { 5587 int requestId; 5588 int role; 5589 FCGX_Stream* inStream; 5590 FCGX_Stream* outStream; 5591 FCGX_Stream* errStream; 5592 char** envp; 5593 void* paramsPtr; 5594 int ipcFd; 5595 int isBeginProcessed; 5596 int keepConnection; 5597 int appStatus; 5598 int nWriters; 5599 int flags; 5600 int listen_sock; 5601 } 5602 5603 int FCGX_InitRequest(FCGX_Request *request, int sock, int flags); 5604 void FCGX_Init(); 5605 5606 int FCGX_Accept_r(FCGX_Request *request); 5607 5608 5609 alias char** FCGX_ParamArray; 5610 5611 c_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp); 5612 c_int FCGX_GetChar(FCGX_Stream* stream); 5613 c_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream); 5614 int FCGX_HasSeenEOF(FCGX_Stream* stream); 5615 c_int FCGX_FFlush(FCGX_Stream *stream); 5616 5617 int FCGX_OpenSocket(const char*, int); 5618 } 5619 } 5620 5621 5622 /* This might go int a separate module eventually. It is a network input helper class. */ 5623 5624 import std.socket; 5625 5626 version(cgi_use_fiber) { 5627 import core.thread; 5628 5629 version(linux) { 5630 import core.sys.linux.epoll; 5631 5632 int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. 5633 } else version(Windows) { 5634 // declaring the iocp thing below... 5635 } else static assert(0, "The hybrid fiber server is not implemented on your OS."); 5636 } 5637 5638 version(Windows) 5639 __gshared HANDLE iocp; 5640 5641 version(cgi_use_fiber) { 5642 version(linux) 5643 private enum WakeupEvent { 5644 Read = EPOLLIN, 5645 Write = EPOLLOUT 5646 } 5647 else version(Windows) 5648 private enum WakeupEvent { 5649 Read, Write 5650 } 5651 else static assert(0); 5652 } 5653 5654 version(cgi_use_fiber) 5655 private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc { 5656 5657 // static cast since I know what i have in here and don't want to pay for dynamic cast 5658 auto f = cast(CgiFiber) cast(void*) Fiber.getThis(); 5659 5660 version(linux) { 5661 f.setPostYield = () { 5662 if(*registered) { 5663 // rearm 5664 epoll_event evt; 5665 evt.events = e | EPOLLONESHOT; 5666 evt.data.ptr = cast(void*) f; 5667 if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) 5668 throw new Exception("epoll_ctl"); 5669 } else { 5670 // initial registration 5671 *registered = true ; 5672 int fd = source.handle; 5673 epoll_event evt; 5674 evt.events = e | EPOLLONESHOT; 5675 evt.data.ptr = cast(void*) f; 5676 if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) 5677 throw new Exception("epoll_ctl"); 5678 } 5679 }; 5680 5681 Fiber.yield(); 5682 5683 f.setPostYield(null); 5684 } else version(Windows) { 5685 Fiber.yield(); 5686 } 5687 else static assert(0); 5688 } 5689 5690 version(cgi_use_fiber) 5691 void unregisterSource(Socket s) { 5692 version(linux) { 5693 epoll_event evt; 5694 epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); 5695 } else version(Windows) { 5696 // intentionally blank 5697 } 5698 else static assert(0); 5699 } 5700 5701 // it is a class primarily for reference semantics 5702 // I might change this interface 5703 /// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda. 5704 class BufferedInputRange { 5705 version(Posix) 5706 this(int source, ubyte[] buffer = null) { 5707 this(new Socket(cast(socket_t) source, AddressFamily.INET), buffer); 5708 } 5709 5710 this(Socket source, ubyte[] buffer = null, bool allowGrowth = true) { 5711 // if they connect but never send stuff to us, we don't want it wasting the process 5712 // so setting a time out 5713 version(cgi_use_fiber) 5714 source.blocking = false; 5715 else 5716 source.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(3)); 5717 5718 this.source = source; 5719 if(buffer is null) { 5720 underlyingBuffer = new ubyte[4096]; 5721 this.allowGrowth = true; 5722 } else { 5723 underlyingBuffer = buffer; 5724 this.allowGrowth = allowGrowth; 5725 } 5726 5727 assert(underlyingBuffer.length); 5728 5729 // we assume view.ptr is always inside underlyingBuffer 5730 view = underlyingBuffer[0 .. 0]; 5731 5732 popFront(); // prime 5733 } 5734 5735 version(cgi_use_fiber) { 5736 bool registered; 5737 } 5738 5739 void dispose() { 5740 version(cgi_use_fiber) { 5741 if(registered) 5742 unregisterSource(source); 5743 } 5744 } 5745 5746 /** 5747 A slight difference from regular ranges is you can give it the maximum 5748 number of bytes to consume. 5749 5750 IMPORTANT NOTE: the default is to consume nothing, so if you don't call 5751 consume() yourself and use a regular foreach, it will infinitely loop! 5752 5753 The default is to do what a normal range does, and consume the whole buffer 5754 and wait for additional input. 5755 5756 You can also specify 0, to append to the buffer, or any other number 5757 to remove the front n bytes and wait for more. 5758 */ 5759 void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) { 5760 if(sourceClosed) 5761 throw new ConnectionClosedException("can't get any more data from a closed source"); 5762 if(!skipConsume) 5763 consume(maxBytesToConsume); 5764 5765 // we might have to grow the buffer 5766 if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) { 5767 if(allowGrowth) { 5768 //import std.stdio; writeln("growth"); 5769 auto viewStart = view.ptr - underlyingBuffer.ptr; 5770 size_t growth = 4096; 5771 // make sure we have enough for what we're being asked for 5772 if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth) 5773 growth = minBytesToSettleFor - underlyingBuffer.length; 5774 //import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length); 5775 underlyingBuffer.length += growth; 5776 view = underlyingBuffer[viewStart .. view.length]; 5777 } else 5778 throw new Exception("No room left in the buffer"); 5779 } 5780 5781 do { 5782 auto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $]; 5783 try_again: 5784 auto ret = source.receive(freeSpace); 5785 if(ret == Socket.ERROR) { 5786 if(wouldHaveBlocked()) { 5787 version(cgi_use_fiber) { 5788 registerEventWakeup(®istered, source, WakeupEvent.Read); 5789 goto try_again; 5790 } else { 5791 // gonna treat a timeout here as a close 5792 sourceClosed = true; 5793 return; 5794 } 5795 } 5796 version(Posix) { 5797 import core.stdc.errno; 5798 if(errno == EINTR || errno == EAGAIN) { 5799 goto try_again; 5800 } 5801 if(errno == ECONNRESET) { 5802 sourceClosed = true; 5803 return; 5804 } 5805 } 5806 throw new Exception(lastSocketError); // FIXME 5807 } 5808 if(ret == 0) { 5809 sourceClosed = true; 5810 return; 5811 } 5812 5813 //import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, " ", ret, " = ", view.length + ret); 5814 view = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret]; 5815 //import std.stdio; writeln(cast(string) view); 5816 } while(view.length < minBytesToSettleFor); 5817 } 5818 5819 /// Removes n bytes from the front of the buffer, and returns the new buffer slice. 5820 /// You might want to idup the data you are consuming if you store it, since it may 5821 /// be overwritten on the new popFront. 5822 /// 5823 /// You do not need to call this if you always want to wait for more data when you 5824 /// consume some. 5825 ubyte[] consume(size_t bytes) { 5826 //import std.stdio; writeln("consuime ", bytes, "/", view.length); 5827 view = view[bytes > $ ? $ : bytes .. $]; 5828 if(view.length == 0) { 5829 view = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning 5830 /* 5831 writeln("HERE"); 5832 popFront(0, 0, true); // try to load more if we can, checks if the source is closed 5833 writeln(cast(string)front); 5834 writeln("DONE"); 5835 */ 5836 } 5837 return front; 5838 } 5839 5840 bool empty() { 5841 return sourceClosed && view.length == 0; 5842 } 5843 5844 ubyte[] front() { 5845 return view; 5846 } 5847 5848 @system invariant() { 5849 assert(view.ptr >= underlyingBuffer.ptr); 5850 // it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer 5851 assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length); 5852 } 5853 5854 ubyte[] underlyingBuffer; 5855 bool allowGrowth; 5856 ubyte[] view; 5857 Socket source; 5858 bool sourceClosed; 5859 } 5860 5861 private class FakeSocketForStdin : Socket { 5862 import std.stdio; 5863 5864 this() { 5865 5866 } 5867 5868 private bool closed; 5869 5870 override ptrdiff_t receive(scope void[] buffer, std.socket.SocketFlags) @trusted { 5871 if(closed) 5872 throw new Exception("Closed"); 5873 return stdin.rawRead(buffer).length; 5874 } 5875 5876 override ptrdiff_t send(const scope void[] buffer, std.socket.SocketFlags) @trusted { 5877 if(closed) 5878 throw new Exception("Closed"); 5879 stdout.rawWrite(buffer); 5880 return buffer.length; 5881 } 5882 5883 override void close() @trusted scope { 5884 (cast(void delegate() @nogc nothrow) &realClose)(); 5885 } 5886 5887 override void shutdown(SocketShutdown s) { 5888 // FIXME 5889 } 5890 5891 override void setOption(SocketOptionLevel, SocketOption, scope void[]) {} 5892 override void setOption(SocketOptionLevel, SocketOption, Duration) {} 5893 5894 override @property @trusted Address remoteAddress() { return null; } 5895 override @property @trusted Address localAddress() { return null; } 5896 5897 void realClose() { 5898 closed = true; 5899 try { 5900 stdin.close(); 5901 stdout.close(); 5902 } catch(Exception e) { 5903 5904 } 5905 } 5906 } 5907 5908 import core.sync.semaphore; 5909 import core.atomic; 5910 5911 /** 5912 To use this thing: 5913 5914 --- 5915 void handler(Socket s) { do something... } 5916 auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler, &delegateThatDropsPrivileges); 5917 manager.listen(); 5918 --- 5919 5920 The 4th parameter is optional. 5921 5922 I suggest you use BufferedInputRange(connection) to handle the input. As a packet 5923 comes in, you will get control. You can just continue; though to fetch more. 5924 5925 5926 FIXME: should I offer an event based async thing like netman did too? Yeah, probably. 5927 */ 5928 class ListeningConnectionManager { 5929 Semaphore semaphore; 5930 Socket[256] queue; 5931 shared(ubyte) nextIndexFront; 5932 ubyte nextIndexBack; 5933 shared(int) queueLength; 5934 5935 Socket acceptCancelable() { 5936 version(Posix) { 5937 import core.sys.posix.sys.select; 5938 fd_set read_fds; 5939 FD_ZERO(&read_fds); 5940 int max = 0; 5941 foreach(listener; listeners) { 5942 FD_SET(listener.handle, &read_fds); 5943 if(listener.handle > max) 5944 max = listener.handle; 5945 } 5946 if(cancelfd != -1) { 5947 FD_SET(cancelfd, &read_fds); 5948 if(cancelfd > max) 5949 max = cancelfd; 5950 } 5951 auto ret = select(max + 1, &read_fds, null, null, null); 5952 if(ret == -1) { 5953 import core.stdc.errno; 5954 if(errno == EINTR) 5955 return null; 5956 else 5957 throw new Exception("wtf select"); 5958 } 5959 5960 if(cancelfd != -1 && FD_ISSET(cancelfd, &read_fds)) { 5961 return null; 5962 } 5963 5964 foreach(listener; listeners) { 5965 if(FD_ISSET(listener.handle, &read_fds)) 5966 return listener.accept(); 5967 } 5968 5969 return null; 5970 } else { 5971 5972 auto check = new SocketSet(); 5973 5974 keep_looping: 5975 check.reset(); 5976 foreach(listener; listeners) 5977 check.add(listener); 5978 5979 // just to check the stop flag on a kinda busy loop. i hate this FIXME 5980 auto got = Socket.select(check, null, null, 3.seconds); 5981 if(got > 0) 5982 foreach(listener; listeners) 5983 if(check.isSet(listener)) 5984 return listener.accept(); 5985 if(globalStopFlag) 5986 return null; 5987 else 5988 goto keep_looping; 5989 } 5990 } 5991 5992 int defaultNumberOfThreads() { 5993 import std.parallelism; 5994 version(cgi_use_fiber) { 5995 return totalCPUs * 2 + 1; // still chance some will be pointlessly blocked anyway 5996 } else { 5997 // I times 4 here because there's a good chance some will be blocked on i/o. 5998 return totalCPUs * 4; 5999 } 6000 6001 } 6002 6003 void listen() { 6004 shared(int) loopBroken; 6005 6006 version(Posix) { 6007 import core.sys.posix.signal; 6008 signal(SIGPIPE, SIG_IGN); 6009 } 6010 6011 version(linux) { 6012 if(cancelfd == -1) 6013 cancelfd = eventfd(0, 0); 6014 } 6015 6016 version(cgi_no_threads) { 6017 // NEVER USE THIS 6018 // it exists only for debugging and other special occasions 6019 6020 // the thread mode is faster and less likely to stall the whole 6021 // thing when a request is slow 6022 while(!loopBroken && !globalStopFlag) { 6023 auto sn = acceptCancelable(); 6024 if(sn is null) continue; 6025 cloexec(sn); 6026 try { 6027 handler(sn); 6028 } catch(Exception e) { 6029 // 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) 6030 sn.close(); 6031 } 6032 } 6033 } else { 6034 6035 if(useFork) { 6036 version(linux) { 6037 //asm { int 3; } 6038 fork(); 6039 } 6040 } 6041 6042 version(cgi_use_fiber) { 6043 6044 version(Windows) { 6045 // please note these are overlapped sockets! so the accept just kicks things off 6046 foreach(listener; listeners) 6047 listener.accept(); 6048 } 6049 6050 WorkerThread[] threads = new WorkerThread[](numberOfThreads); 6051 foreach(i, ref thread; threads) { 6052 thread = new WorkerThread(this, handler, cast(int) i); 6053 thread.start(); 6054 } 6055 6056 bool fiber_crash_check() { 6057 bool hasAnyRunning; 6058 foreach(thread; threads) { 6059 if(!thread.isRunning) { 6060 thread.join(); 6061 } else hasAnyRunning = true; 6062 } 6063 6064 return (!hasAnyRunning); 6065 } 6066 6067 6068 while(!globalStopFlag) { 6069 Thread.sleep(1.seconds); 6070 if(fiber_crash_check()) 6071 break; 6072 } 6073 6074 } else { 6075 semaphore = new Semaphore(); 6076 6077 ConnectionThread[] threads = new ConnectionThread[](numberOfThreads); 6078 foreach(i, ref thread; threads) { 6079 thread = new ConnectionThread(this, handler, cast(int) i); 6080 thread.start(); 6081 } 6082 6083 while(!loopBroken && !globalStopFlag) { 6084 Socket sn; 6085 6086 bool crash_check() { 6087 bool hasAnyRunning; 6088 foreach(thread; threads) { 6089 if(!thread.isRunning) { 6090 thread.join(); 6091 } else hasAnyRunning = true; 6092 } 6093 6094 return (!hasAnyRunning); 6095 } 6096 6097 6098 void accept_new_connection() { 6099 sn = acceptCancelable(); 6100 if(sn is null) return; 6101 cloexec(sn); 6102 if(tcp) { 6103 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 6104 // on the socket because we do some buffering internally. I think this helps, 6105 // certainly does for small requests, and I think it does for larger ones too 6106 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 6107 6108 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 6109 } 6110 } 6111 6112 void existing_connection_new_data() { 6113 // wait until a slot opens up 6114 // int waited = 0; 6115 while(queueLength >= queue.length) { 6116 Thread.sleep(1.msecs); 6117 // waited ++; 6118 } 6119 // if(waited) {import std.stdio; writeln(waited);} 6120 synchronized(this) { 6121 queue[nextIndexBack] = sn; 6122 nextIndexBack++; 6123 atomicOp!"+="(queueLength, 1); 6124 } 6125 semaphore.notify(); 6126 } 6127 6128 6129 accept_new_connection(); 6130 if(sn !is null) 6131 existing_connection_new_data(); 6132 else if(sn is null && globalStopFlag) { 6133 foreach(thread; threads) { 6134 semaphore.notify(); 6135 } 6136 Thread.sleep(50.msecs); 6137 } 6138 6139 if(crash_check()) 6140 break; 6141 } 6142 } 6143 6144 // FIXME: i typically stop this with ctrl+c which never 6145 // actually gets here. i need to do a sigint handler. 6146 if(cleanup) 6147 cleanup(); 6148 } 6149 } 6150 6151 //version(linux) 6152 //int epoll_fd; 6153 6154 bool tcp; 6155 void delegate() cleanup; 6156 6157 private void function(Socket) fhandler; 6158 private void dg_handler(Socket s) { 6159 fhandler(s); 6160 } 6161 6162 6163 this(string[] listenSpec, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6164 fhandler = handler; 6165 this(listenSpec, &dg_handler, dropPrivs, useFork, numberOfThreads); 6166 } 6167 this(string[] listenSpec, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6168 string[] host; 6169 ushort[] port; 6170 6171 foreach(spec; listenSpec) { 6172 /+ 6173 The format: 6174 6175 protocol:// 6176 address_spec 6177 6178 Protocol is optional. Must be http, https, scgi, or fastcgi. 6179 6180 address_spec is either: 6181 ipv4 address : port 6182 [ipv6 address] : port 6183 unix:filename 6184 abstract:name 6185 port <which is tcp but on any interface> 6186 +/ 6187 6188 string protocol; 6189 string address_spec; 6190 6191 auto protocolIdx = spec.indexOf("://"); 6192 if(protocolIdx != -1) { 6193 protocol = spec[0 .. protocolIdx]; 6194 address_spec = spec[protocolIdx + "://".length .. $]; 6195 } else { 6196 address_spec = spec; 6197 } 6198 6199 if(address_spec.startsWith("unix:") || address_spec.startsWith("abstract:")) { 6200 host ~= address_spec; 6201 port ~= 0; 6202 } else { 6203 auto idx = address_spec.lastIndexOf(":"); 6204 if(idx == -1) { 6205 host ~= null; 6206 } else { 6207 auto as = address_spec[0 .. idx]; 6208 if(as.length >= 3 && as[0] == '[' && as[$-1] == ']') 6209 as = as[1 .. $-1]; 6210 host ~= as; 6211 } 6212 port ~= address_spec[idx + 1 .. $].to!ushort; 6213 } 6214 6215 } 6216 6217 this(host, port, handler, dropPrivs, useFork, numberOfThreads); 6218 } 6219 6220 this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6221 this([host], [port], handler, dropPrivs, useFork, numberOfThreads); 6222 } 6223 this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6224 this([host], [port], handler, dropPrivs, useFork, numberOfThreads); 6225 } 6226 6227 this(string[] host, ushort[] port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6228 fhandler = handler; 6229 this(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads); 6230 } 6231 6232 this(string[] host, ushort[] port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { 6233 assert(host.length == port.length); 6234 6235 this.handler = handler; 6236 this.useFork = useFork; 6237 this.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads(); 6238 6239 listeners.reserve(host.length); 6240 6241 foreach(i; 0 .. host.length) 6242 if(host[i] == "localhost") { 6243 listeners ~= startListening("127.0.0.1", port[i], tcp, cleanup, 128, dropPrivs); 6244 listeners ~= startListening("::1", port[i], tcp, cleanup, 128, dropPrivs); 6245 } else { 6246 listeners ~= startListening(host[i], port[i], tcp, cleanup, 128, dropPrivs); 6247 } 6248 6249 version(cgi_use_fiber) 6250 if(useFork) { 6251 foreach(listener; listeners) 6252 listener.blocking = false; 6253 } 6254 6255 // this is the UI control thread and thus gets more priority 6256 Thread.getThis.priority = Thread.PRIORITY_MAX; 6257 } 6258 6259 Socket[] listeners; 6260 void delegate(Socket) handler; 6261 6262 immutable bool useFork; 6263 int numberOfThreads; 6264 } 6265 6266 Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) { 6267 Socket listener; 6268 if(host.startsWith("unix:")) { 6269 version(Posix) { 6270 listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); 6271 cloexec(listener); 6272 string filename = host["unix:".length .. $].idup; 6273 listener.bind(new UnixAddress(filename)); 6274 cleanup = delegate() { 6275 listener.close(); 6276 import std.file; 6277 remove(filename); 6278 }; 6279 tcp = false; 6280 } else { 6281 throw new Exception("unix sockets not supported on this system"); 6282 } 6283 } else if(host.startsWith("abstract:")) { 6284 version(linux) { 6285 listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); 6286 cloexec(listener); 6287 string filename = "\0" ~ host["abstract:".length .. $]; 6288 import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]); 6289 listener.bind(new UnixAddress(filename)); 6290 tcp = false; 6291 } else { 6292 throw new Exception("abstract unix sockets not supported on this system"); 6293 } 6294 } else { 6295 auto address = host.length ? parseAddress(host, port) : new InternetAddress(port); 6296 version(cgi_use_fiber) { 6297 version(Windows) 6298 listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); 6299 else 6300 listener = new Socket(address.addressFamily, SocketType.STREAM); 6301 } else { 6302 listener = new Socket(address.addressFamily, SocketType.STREAM); 6303 } 6304 cloexec(listener); 6305 listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 6306 if(address.addressFamily == AddressFamily.INET6) 6307 listener.setOption(SocketOptionLevel.IPV6, SocketOption.IPV6_V6ONLY, true); 6308 listener.bind(address); 6309 cleanup = delegate() { 6310 listener.close(); 6311 }; 6312 tcp = true; 6313 } 6314 6315 listener.listen(backQueue); 6316 6317 if (dropPrivs !is null) // can be null, backwards compatibility 6318 dropPrivs(); 6319 6320 return listener; 6321 } 6322 6323 // 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. 6324 void sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) { 6325 if(data.length == 0) return; 6326 ptrdiff_t amount; 6327 //import std.stdio; writeln("***",cast(string) data,"///"); 6328 do { 6329 amount = s.send(data); 6330 if(amount == Socket.ERROR) { 6331 version(cgi_use_fiber) { 6332 if(wouldHaveBlocked()) { 6333 bool registered = true; 6334 registerEventWakeup(®istered, s, WakeupEvent.Write); 6335 continue; 6336 } 6337 } 6338 throw new ConnectionException(s, lastSocketError, file, line); 6339 } 6340 assert(amount > 0); 6341 6342 data = data[amount .. $]; 6343 } while(data.length); 6344 } 6345 6346 class ConnectionException : Exception { 6347 Socket socket; 6348 this(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) { 6349 this.socket = s; 6350 super(msg, file, line); 6351 } 6352 } 6353 6354 class HttpVersionNotSupportedException : Exception { 6355 this(string file = __FILE__, size_t line = __LINE__) { 6356 super("HTTP Version Not Supported", file, line); 6357 } 6358 } 6359 6360 alias void delegate(Socket) CMT; 6361 6362 import core.thread; 6363 /+ 6364 cgi.d now uses a hybrid of event i/o and threads at the top level. 6365 6366 Top level thread is responsible for accepting sockets and selecting on them. 6367 6368 It then indicates to a child that a request is pending, and any random worker 6369 thread that is free handles it. It goes into blocking mode and handles that 6370 http request to completion. 6371 6372 At that point, it goes back into the waiting queue. 6373 6374 6375 This concept is only implemented on Linux. On all other systems, it still 6376 uses the worker threads and semaphores (which is perfectly fine for a lot of 6377 things! Just having a great number of keep-alive connections will break that.) 6378 6379 6380 So the algorithm is: 6381 6382 select(accept, event, pending) 6383 if accept -> send socket to free thread, if any. if not, add socket to queue 6384 if event -> send the signaling thread a socket from the queue, if not, mark it free 6385 - event might block until it can be *written* to. it is a fifo sending socket fds! 6386 6387 A worker only does one http request at a time, then signals its availability back to the boss. 6388 6389 The socket the worker was just doing should be added to the one-off epoll read. If it is closed, 6390 great, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the 6391 actual FD will not be kept out here. 6392 6393 So: 6394 queue = sockets we know are ready to read now, but no worker thread is available 6395 idle list = worker threads not doing anything else. they signal back and forth 6396 6397 the workers all read off the event fd. This is the semaphore wait 6398 6399 the boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read, 6400 it puts it in the queue and writes to the event fd. 6401 6402 The child could put the socket back in the epoll thing itself. 6403 6404 The child needs to be able to gracefully handle being given a socket that just closed with no work. 6405 +/ 6406 class ConnectionThread : Thread { 6407 this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { 6408 this.lcm = lcm; 6409 this.dg = dg; 6410 this.myThreadNumber = myThreadNumber; 6411 super(&run); 6412 } 6413 6414 void run() { 6415 while(true) { 6416 // so if there's a bunch of idle keep-alive connections, it can 6417 // consume all the worker threads... just sitting there. 6418 lcm.semaphore.wait(); 6419 if(globalStopFlag) 6420 return; 6421 Socket socket; 6422 synchronized(lcm) { 6423 auto idx = lcm.nextIndexFront; 6424 socket = lcm.queue[idx]; 6425 lcm.queue[idx] = null; 6426 atomicOp!"+="(lcm.nextIndexFront, 1); 6427 atomicOp!"-="(lcm.queueLength, 1); 6428 } 6429 try { 6430 //import std.stdio; writeln(myThreadNumber, " taking it"); 6431 dg(socket); 6432 /+ 6433 if(socket.isAlive) { 6434 // process it more later 6435 version(linux) { 6436 import core.sys.linux.epoll; 6437 epoll_event ev; 6438 ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; 6439 ev.data.fd = socket.handle; 6440 import std.stdio; writeln("adding"); 6441 if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) { 6442 if(errno == EEXIST) { 6443 ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; 6444 ev.data.fd = socket.handle; 6445 if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1) 6446 throw new Exception("epoll_ctl " ~ to!string(errno)); 6447 } else 6448 throw new Exception("epoll_ctl " ~ to!string(errno)); 6449 } 6450 //import std.stdio; writeln("keep alive"); 6451 // writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later 6452 __traits(getMember, socket, "sock") = cast(socket_t) -1; 6453 } else { 6454 continue; // hope it times out in a reasonable amount of time... 6455 } 6456 } 6457 +/ 6458 } catch(ConnectionClosedException e) { 6459 // can just ignore this, it is fairly normal 6460 socket.close(); 6461 } catch(Throwable e) { 6462 import std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite("\n"); 6463 socket.close(); 6464 } 6465 } 6466 } 6467 6468 ListeningConnectionManager lcm; 6469 CMT dg; 6470 int myThreadNumber; 6471 } 6472 6473 version(cgi_use_fiber) 6474 class WorkerThread : Thread { 6475 this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { 6476 this.lcm = lcm; 6477 this.dg = dg; 6478 this.myThreadNumber = myThreadNumber; 6479 super(&run); 6480 } 6481 6482 version(Windows) 6483 void run() { 6484 auto timeout = INFINITE; 6485 PseudoblockingOverlappedSocket key; 6486 OVERLAPPED* overlapped; 6487 DWORD bytes; 6488 while(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) { 6489 if(key is null) 6490 continue; 6491 key.lastAnswer = bytes; 6492 if(key.fiber) { 6493 key.fiber.proceed(); 6494 } else { 6495 // we have a new connection, issue the first receive on it and issue the next accept 6496 6497 auto sn = key.accepted; 6498 6499 key.accept(); 6500 6501 cloexec(sn); 6502 if(lcm.tcp) { 6503 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 6504 // on the socket because we do some buffering internally. I think this helps, 6505 // certainly does for small requests, and I think it does for larger ones too 6506 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 6507 6508 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 6509 } 6510 6511 dg(sn); 6512 } 6513 } 6514 //SleepEx(INFINITE, TRUE); 6515 } 6516 6517 version(linux) 6518 void run() { 6519 6520 import core.sys.linux.epoll; 6521 epfd = epoll_create1(EPOLL_CLOEXEC); 6522 if(epfd == -1) 6523 throw new Exception("epoll_create1 " ~ to!string(errno)); 6524 scope(exit) { 6525 import core.sys.posix.unistd; 6526 close(epfd); 6527 } 6528 6529 { 6530 epoll_event ev; 6531 ev.events = EPOLLIN; 6532 ev.data.fd = cancelfd; 6533 epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); 6534 } 6535 6536 foreach(listener; lcm.listeners) { 6537 epoll_event ev; 6538 ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. 6539 ev.data.fd = listener.handle; 6540 if(epoll_ctl(epfd, EPOLL_CTL_ADD, listener.handle, &ev) == -1) 6541 throw new Exception("epoll_ctl " ~ to!string(errno)); 6542 } 6543 6544 6545 6546 while(!globalStopFlag) { 6547 Socket sn; 6548 6549 epoll_event[64] events; 6550 auto nfds = epoll_wait(epfd, events.ptr, events.length, -1); 6551 if(nfds == -1) { 6552 if(errno == EINTR) 6553 continue; 6554 throw new Exception("epoll_wait " ~ to!string(errno)); 6555 } 6556 6557 outer: foreach(idx; 0 .. nfds) { 6558 auto flags = events[idx].events; 6559 6560 if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { 6561 globalStopFlag = true; 6562 //import std.stdio; writeln("exit heard"); 6563 break; 6564 } else { 6565 foreach(listener; lcm.listeners) { 6566 if(cast(size_t) events[idx].data.ptr == cast(size_t) listener.handle) { 6567 //import std.stdio; writeln(myThreadNumber, " woken up ", flags); 6568 // this try/catch is because it is set to non-blocking mode 6569 // and Phobos' stupid api throws an exception instead of returning 6570 // if it would block. Why would it block? because a forked process 6571 // might have beat us to it, but the wakeup event thundered our herds. 6572 try 6573 sn = listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better 6574 catch(SocketAcceptException e) { continue outer; } 6575 6576 cloexec(sn); 6577 if(lcm.tcp) { 6578 // disable Nagle's algorithm to avoid a 40ms delay when we send/recv 6579 // on the socket because we do some buffering internally. I think this helps, 6580 // certainly does for small requests, and I think it does for larger ones too 6581 sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 6582 6583 sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); 6584 } 6585 6586 dg(sn); 6587 continue outer; 6588 } else { 6589 // writeln(events[idx].data.ptr); 6590 } 6591 } 6592 6593 if(cast(size_t) events[idx].data.ptr < 1024) { 6594 throw arsd.core.ArsdException!"this doesn't look like a fiber pointer... "(cast(size_t) events[idx].data.ptr); 6595 } 6596 auto fiber = cast(CgiFiber) events[idx].data.ptr; 6597 fiber.proceed(); 6598 } 6599 } 6600 } 6601 } 6602 6603 ListeningConnectionManager lcm; 6604 CMT dg; 6605 int myThreadNumber; 6606 } 6607 6608 6609 /* Done with network helper */ 6610 6611 /* Helpers for doing temporary files. Used both here and in web.d */ 6612 6613 version(Windows) { 6614 import core.sys.windows.windows; 6615 extern(Windows) DWORD GetTempPathW(DWORD, LPWSTR); 6616 alias GetTempPathW GetTempPath; 6617 } 6618 6619 version(Posix) { 6620 static import linux = core.sys.posix.unistd; 6621 } 6622 6623 string getTempDirectory() { 6624 string path; 6625 version(Windows) { 6626 wchar[1024] buffer; 6627 auto len = GetTempPath(1024, buffer.ptr); 6628 if(len == 0) 6629 throw new Exception("couldn't find a temporary path"); 6630 6631 auto b = buffer[0 .. len]; 6632 6633 path = to!string(b); 6634 } else 6635 path = "/tmp/"; 6636 6637 return path; 6638 } 6639 6640 6641 // I like std.date. These functions help keep my old code and data working with phobos changing. 6642 6643 long sysTimeToDTime(in SysTime sysTime) { 6644 return convert!("hnsecs", "msecs")(sysTime.stdTime - 621355968000000000L); 6645 } 6646 6647 long dateTimeToDTime(in DateTime dt) { 6648 return sysTimeToDTime(cast(SysTime) dt); 6649 } 6650 6651 long getUtcTime() { // renamed primarily to avoid conflict with std.date itself 6652 return sysTimeToDTime(Clock.currTime(UTC())); 6653 } 6654 6655 // NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick 6656 SysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) { 6657 immutable hnsecs = convert!("msecs", "hnsecs")(dTime) + 621355968000000000L; 6658 return SysTime(hnsecs, tz); 6659 } 6660 6661 6662 6663 // this is a helper to read HTTP transfer-encoding: chunked responses 6664 immutable(ubyte[]) dechunk(BufferedInputRange ir) { 6665 immutable(ubyte)[] ret; 6666 6667 another_chunk: 6668 // If here, we are at the beginning of a chunk. 6669 auto a = ir.front(); 6670 int chunkSize; 6671 int loc = locationOf(a, "\r\n"); 6672 while(loc == -1) { 6673 ir.popFront(); 6674 a = ir.front(); 6675 loc = locationOf(a, "\r\n"); 6676 } 6677 6678 string hex; 6679 hex = ""; 6680 for(int i = 0; i < loc; i++) { 6681 char c = a[i]; 6682 if(c >= 'A' && c <= 'Z') 6683 c += 0x20; 6684 if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) { 6685 hex ~= c; 6686 } else { 6687 break; 6688 } 6689 } 6690 6691 assert(hex.length); 6692 6693 int power = 1; 6694 int size = 0; 6695 foreach(cc1; retro(hex)) { 6696 dchar cc = cc1; 6697 if(cc >= 'a' && cc <= 'z') 6698 cc -= 0x20; 6699 int val = 0; 6700 if(cc >= '0' && cc <= '9') 6701 val = cc - '0'; 6702 else 6703 val = cc - 'A' + 10; 6704 6705 size += power * val; 6706 power *= 16; 6707 } 6708 6709 chunkSize = size; 6710 assert(size >= 0); 6711 6712 if(loc + 2 > a.length) { 6713 ir.popFront(0, a.length + loc + 2); 6714 a = ir.front(); 6715 } 6716 6717 a = ir.consume(loc + 2); 6718 6719 if(chunkSize == 0) { // we're done with the response 6720 // if we got here, will change must be true.... 6721 more_footers: 6722 loc = locationOf(a, "\r\n"); 6723 if(loc == -1) { 6724 ir.popFront(); 6725 a = ir.front; 6726 goto more_footers; 6727 } else { 6728 assert(loc == 0); 6729 ir.consume(loc + 2); 6730 goto finish; 6731 } 6732 } else { 6733 // if we got here, will change must be true.... 6734 if(a.length < chunkSize + 2) { 6735 ir.popFront(0, chunkSize + 2); 6736 a = ir.front(); 6737 } 6738 6739 ret ~= (a[0..chunkSize]); 6740 6741 if(!(a.length > chunkSize + 2)) { 6742 ir.popFront(0, chunkSize + 2); 6743 a = ir.front(); 6744 } 6745 assert(a[chunkSize] == 13); 6746 assert(a[chunkSize+1] == 10); 6747 a = ir.consume(chunkSize + 2); 6748 chunkSize = 0; 6749 goto another_chunk; 6750 } 6751 6752 finish: 6753 return ret; 6754 } 6755 6756 // I want to be able to get data from multiple sources the same way... 6757 interface ByChunkRange { 6758 bool empty(); 6759 void popFront(); 6760 const(ubyte)[] front(); 6761 } 6762 6763 ByChunkRange byChunk(const(ubyte)[] data) { 6764 return new class ByChunkRange { 6765 override bool empty() { 6766 return !data.length; 6767 } 6768 6769 override void popFront() { 6770 if(data.length > 4096) 6771 data = data[4096 .. $]; 6772 else 6773 data = null; 6774 } 6775 6776 override const(ubyte)[] front() { 6777 return data[0 .. $ > 4096 ? 4096 : $]; 6778 } 6779 }; 6780 } 6781 6782 ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) { 6783 const(ubyte)[] f; 6784 6785 f = ir.front; 6786 if(f.length > atMost) 6787 f = f[0 .. atMost]; 6788 6789 return new class ByChunkRange { 6790 override bool empty() { 6791 return atMost == 0; 6792 } 6793 6794 override const(ubyte)[] front() { 6795 return f; 6796 } 6797 6798 override void popFront() { 6799 ir.consume(f.length); 6800 atMost -= f.length; 6801 auto a = ir.front(); 6802 6803 if(a.length <= atMost) { 6804 f = a; 6805 atMost -= a.length; 6806 a = ir.consume(a.length); 6807 if(atMost != 0) 6808 ir.popFront(); 6809 if(f.length == 0) { 6810 f = ir.front(); 6811 } 6812 } else { 6813 // we actually have *more* here than we need.... 6814 f = a[0..atMost]; 6815 atMost = 0; 6816 ir.consume(atMost); 6817 } 6818 } 6819 }; 6820 } 6821 6822 version(cgi_with_websocket) { 6823 // http://tools.ietf.org/html/rfc6455 6824 6825 /++ 6826 WEBSOCKET SUPPORT: 6827 6828 Full example: 6829 --- 6830 import arsd.cgi; 6831 6832 void websocketEcho(Cgi cgi) { 6833 if(cgi.websocketRequested()) { 6834 if(cgi.origin != "http://arsdnet.net") 6835 throw new Exception("bad origin"); 6836 auto websocket = cgi.acceptWebsocket(); 6837 6838 websocket.send("hello"); 6839 websocket.send(" world!"); 6840 6841 auto msg = websocket.recv(); 6842 while(msg.opcode != WebSocketOpcode.close) { 6843 if(msg.opcode == WebSocketOpcode.text) { 6844 websocket.send(msg.textData); 6845 } else if(msg.opcode == WebSocketOpcode.binary) { 6846 websocket.send(msg.data); 6847 } 6848 6849 msg = websocket.recv(); 6850 } 6851 6852 websocket.close(); 6853 } else { 6854 cgi.write("You are loading the websocket endpoint in a browser instead of a websocket client. Use a websocket client on this url instead.\n", true); 6855 } 6856 } 6857 6858 mixin GenericMain!websocketEcho; 6859 --- 6860 +/ 6861 6862 class WebSocket { 6863 Cgi cgi; 6864 6865 private bool isClient = false; 6866 6867 private this(Cgi cgi) { 6868 this.cgi = cgi; 6869 6870 Socket socket = cgi.idlol.source; 6871 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5)); 6872 } 6873 6874 // returns true if data available, false if it timed out 6875 bool recvAvailable(Duration timeout = dur!"msecs"(0)) { 6876 if(!waitForNextMessageWouldBlock()) 6877 return true; 6878 if(isDataPending(timeout)) 6879 return true; // this is kinda a lie. 6880 6881 return false; 6882 } 6883 6884 public bool lowLevelReceive() { 6885 auto bfr = cgi.idlol; 6886 top: 6887 auto got = bfr.front; 6888 if(got.length) { 6889 if(receiveBuffer.length < receiveBufferUsedLength + got.length) 6890 receiveBuffer.length += receiveBufferUsedLength + got.length; 6891 6892 receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[]; 6893 receiveBufferUsedLength += got.length; 6894 bfr.consume(got.length); 6895 6896 return true; 6897 } 6898 6899 if(bfr.sourceClosed) 6900 return false; 6901 6902 bfr.popFront(0); 6903 if(bfr.sourceClosed) 6904 return false; 6905 goto top; 6906 } 6907 6908 6909 bool isDataPending(Duration timeout = 0.seconds) { 6910 Socket socket = cgi.idlol.source; 6911 6912 auto check = new SocketSet(); 6913 check.add(socket); 6914 6915 auto got = Socket.select(check, null, null, timeout); 6916 if(got > 0) 6917 return true; 6918 return false; 6919 } 6920 6921 // note: this blocks 6922 WebSocketFrame recv() { 6923 return waitForNextMessage(); 6924 } 6925 6926 6927 6928 6929 private void llclose() { 6930 cgi.close(); 6931 } 6932 6933 private void llsend(ubyte[] data) { 6934 cgi.write(data); 6935 cgi.flush(); 6936 } 6937 6938 void unregisterActiveSocket(WebSocket) {} 6939 6940 /* copy/paste section { */ 6941 6942 private int readyState_; 6943 private ubyte[] receiveBuffer; 6944 private size_t receiveBufferUsedLength; 6945 6946 private Config config; 6947 6948 enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. 6949 enum OPEN = 1; /// The connection is open and ready to communicate. 6950 enum CLOSING = 2; /// The connection is in the process of closing. 6951 enum CLOSED = 3; /// The connection is closed or couldn't be opened. 6952 6953 /++ 6954 6955 +/ 6956 /// Group: foundational 6957 static struct Config { 6958 /++ 6959 These control the size of the receive buffer. 6960 6961 It starts at the initial size, will temporarily 6962 balloon up to the maximum size, and will reuse 6963 a buffer up to the likely size. 6964 6965 Anything larger than the maximum size will cause 6966 the connection to be aborted and an exception thrown. 6967 This is to protect you against a peer trying to 6968 exhaust your memory, while keeping the user-level 6969 processing simple. 6970 +/ 6971 size_t initialReceiveBufferSize = 4096; 6972 size_t likelyReceiveBufferSize = 4096; /// ditto 6973 size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto 6974 6975 /++ 6976 Maximum combined size of a message. 6977 +/ 6978 size_t maximumMessageSize = 10 * 1024 * 1024; 6979 6980 string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; 6981 string origin; /// Origin URL to send with the handshake, if desired. 6982 string protocol; /// the protocol header, if desired. 6983 6984 /++ 6985 Additional headers to put in the HTTP request. These should be formatted `Name: value`, like for example: 6986 6987 --- 6988 Config config; 6989 config.additionalHeaders ~= "Authorization: Bearer your_auth_token_here"; 6990 --- 6991 6992 History: 6993 Added February 19, 2021 (included in dub version 9.2) 6994 +/ 6995 string[] additionalHeaders; 6996 6997 /++ 6998 Amount of time (in msecs) of idleness after which to send an automatic ping 6999 7000 Please note how this interacts with [timeoutFromInactivity] - a ping counts as activity that 7001 keeps the socket alive. 7002 +/ 7003 int pingFrequency = 5000; 7004 7005 /++ 7006 Amount of time to disconnect when there's no activity. Note that automatic pings will keep the connection alive; this timeout only occurs if there's absolutely nothing, including no responses to websocket ping frames. Since the default [pingFrequency] is only seconds, this one minute should never elapse unless the connection is actually dead. 7007 7008 The one thing to keep in mind is if your program is busy and doesn't check input, it might consider this a time out since there's no activity. The reason is that your program was busy rather than a connection failure, but it doesn't care. You should avoid long processing periods anyway though! 7009 7010 History: 7011 Added March 31, 2021 (included in dub version 9.4) 7012 +/ 7013 Duration timeoutFromInactivity = 1.minutes; 7014 7015 /++ 7016 For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be 7017 verified. Setting this to `false` will skip this check and allow the connection to continue anyway. 7018 7019 History: 7020 Added April 5, 2022 (dub v10.8) 7021 7022 Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes 7023 even if it was true, it would skip the verification. Now, it always respects this local setting. 7024 +/ 7025 bool verifyPeer = true; 7026 } 7027 7028 /++ 7029 Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. 7030 +/ 7031 int readyState() { 7032 return readyState_; 7033 } 7034 7035 /++ 7036 Closes the connection, sending a graceful teardown message to the other side. 7037 7038 Code 1000 is the normal closure code. 7039 7040 History: 7041 The default `code` was changed to 1000 on January 9, 2023. Previously it was 0, 7042 but also ignored anyway. 7043 +/ 7044 /// Group: foundational 7045 void close(int code = 1000, string reason = null) 7046 //in (reason.length < 123) 7047 in { assert(reason.length < 123); } do 7048 { 7049 if(readyState_ != OPEN) 7050 return; // it cool, we done 7051 WebSocketFrame wss; 7052 wss.fin = true; 7053 wss.masked = this.isClient; 7054 wss.opcode = WebSocketOpcode.close; 7055 wss.data = [ubyte((code >> 8) & 0xff), ubyte(code & 0xff)] ~ cast(ubyte[]) reason.dup; 7056 wss.send(&llsend); 7057 7058 readyState_ = CLOSING; 7059 7060 closeCalled = true; 7061 7062 llclose(); 7063 } 7064 7065 private bool closeCalled; 7066 7067 /++ 7068 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. 7069 +/ 7070 /// Group: foundational 7071 void ping(in ubyte[] data = null) { 7072 WebSocketFrame wss; 7073 wss.fin = true; 7074 wss.masked = this.isClient; 7075 wss.opcode = WebSocketOpcode.ping; 7076 if(data !is null) wss.data = data.dup; 7077 wss.send(&llsend); 7078 } 7079 7080 /++ 7081 Sends a pong message to the server. This is normally done automatically in response to pings. 7082 +/ 7083 /// Group: foundational 7084 void pong(in ubyte[] data = null) { 7085 WebSocketFrame wss; 7086 wss.fin = true; 7087 wss.masked = this.isClient; 7088 wss.opcode = WebSocketOpcode.pong; 7089 if(data !is null) wss.data = data.dup; 7090 wss.send(&llsend); 7091 } 7092 7093 /++ 7094 Sends a text message through the websocket. 7095 +/ 7096 /// Group: foundational 7097 void send(in char[] textData) { 7098 WebSocketFrame wss; 7099 wss.fin = true; 7100 wss.masked = this.isClient; 7101 wss.opcode = WebSocketOpcode.text; 7102 wss.data = cast(ubyte[]) textData.dup; 7103 wss.send(&llsend); 7104 } 7105 7106 /++ 7107 Sends a binary message through the websocket. 7108 +/ 7109 /// Group: foundational 7110 void send(in ubyte[] binaryData) { 7111 WebSocketFrame wss; 7112 wss.masked = this.isClient; 7113 wss.fin = true; 7114 wss.opcode = WebSocketOpcode.binary; 7115 wss.data = cast(ubyte[]) binaryData.dup; 7116 wss.send(&llsend); 7117 } 7118 7119 /++ 7120 Waits for and returns the next complete message on the socket. 7121 7122 Note that the onmessage function is still called, right before 7123 this returns. 7124 +/ 7125 /// Group: blocking_api 7126 public WebSocketFrame waitForNextMessage() { 7127 do { 7128 auto m = processOnce(); 7129 if(m.populated) 7130 return m; 7131 } while(lowLevelReceive()); 7132 7133 throw new ConnectionClosedException("Websocket receive timed out"); 7134 //return WebSocketFrame.init; // FIXME? maybe. 7135 } 7136 7137 /++ 7138 Tells if [waitForNextMessage] would block. 7139 +/ 7140 /// Group: blocking_api 7141 public bool waitForNextMessageWouldBlock() { 7142 checkAgain: 7143 if(isMessageBuffered()) 7144 return false; 7145 if(!isDataPending()) 7146 return true; 7147 7148 while(isDataPending()) { 7149 if(lowLevelReceive() == false) 7150 throw new ConnectionClosedException("Connection closed in middle of message"); 7151 } 7152 7153 goto checkAgain; 7154 } 7155 7156 /++ 7157 Is there a message in the buffer already? 7158 If `true`, [waitForNextMessage] is guaranteed to return immediately. 7159 If `false`, check [isDataPending] as the next step. 7160 +/ 7161 /// Group: blocking_api 7162 public bool isMessageBuffered() { 7163 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 7164 auto s = d; 7165 if(d.length) { 7166 auto orig = d; 7167 auto m = WebSocketFrame.read(d); 7168 // that's how it indicates that it needs more data 7169 if(d !is orig) 7170 return true; 7171 } 7172 7173 return false; 7174 } 7175 7176 private ubyte continuingType; 7177 private ubyte[] continuingData; 7178 //private size_t continuingDataLength; 7179 7180 private WebSocketFrame processOnce() { 7181 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 7182 auto s = d; 7183 // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. 7184 WebSocketFrame m; 7185 if(d.length) { 7186 auto orig = d; 7187 m = WebSocketFrame.read(d); 7188 // that's how it indicates that it needs more data 7189 if(d is orig) 7190 return WebSocketFrame.init; 7191 m.unmaskInPlace(); 7192 switch(m.opcode) { 7193 case WebSocketOpcode.continuation: 7194 if(continuingData.length + m.data.length > config.maximumMessageSize) 7195 throw new Exception("message size exceeded"); 7196 7197 continuingData ~= m.data; 7198 if(m.fin) { 7199 if(ontextmessage) 7200 ontextmessage(cast(char[]) continuingData); 7201 if(onbinarymessage) 7202 onbinarymessage(continuingData); 7203 7204 continuingData = null; 7205 } 7206 break; 7207 case WebSocketOpcode.text: 7208 if(m.fin) { 7209 if(ontextmessage) 7210 ontextmessage(m.textData); 7211 } else { 7212 continuingType = m.opcode; 7213 //continuingDataLength = 0; 7214 continuingData = null; 7215 continuingData ~= m.data; 7216 } 7217 break; 7218 case WebSocketOpcode.binary: 7219 if(m.fin) { 7220 if(onbinarymessage) 7221 onbinarymessage(m.data); 7222 } else { 7223 continuingType = m.opcode; 7224 //continuingDataLength = 0; 7225 continuingData = null; 7226 continuingData ~= m.data; 7227 } 7228 break; 7229 case WebSocketOpcode.close: 7230 7231 //import std.stdio; writeln("closed ", cast(string) m.data); 7232 7233 ushort code = CloseEvent.StandardCloseCodes.noStatusCodePresent; 7234 const(char)[] reason; 7235 7236 if(m.data.length >= 2) { 7237 code = (m.data[0] << 8) | m.data[1]; 7238 reason = (cast(char[]) m.data[2 .. $]); 7239 } 7240 7241 if(onclose) 7242 onclose(CloseEvent(code, reason, true)); 7243 7244 // if we receive one and haven't sent one back we're supposed to echo it back and close. 7245 if(!closeCalled) 7246 close(code, reason.idup); 7247 7248 readyState_ = CLOSED; 7249 7250 unregisterActiveSocket(this); 7251 break; 7252 case WebSocketOpcode.ping: 7253 // import std.stdio; writeln("ping received ", m.data); 7254 pong(m.data); 7255 break; 7256 case WebSocketOpcode.pong: 7257 // import std.stdio; writeln("pong received ", m.data); 7258 // just really references it is still alive, nbd. 7259 break; 7260 default: // ignore though i could and perhaps should throw too 7261 } 7262 } 7263 7264 if(d.length) { 7265 m.data = m.data.dup(); 7266 } 7267 7268 import core.stdc.string; 7269 memmove(receiveBuffer.ptr, d.ptr, d.length); 7270 receiveBufferUsedLength = d.length; 7271 7272 return m; 7273 } 7274 7275 private void autoprocess() { 7276 // FIXME 7277 do { 7278 processOnce(); 7279 } while(lowLevelReceive()); 7280 } 7281 7282 /++ 7283 Arguments for the close event. The `code` and `reason` are provided from the close message on the websocket, if they are present. The spec says code 1000 indicates a normal, default reason close, but reserves the code range from 3000-5000 for future definition; the 3000s can be registered with IANA and the 4000's are application private use. The `reason` should be user readable, but not displayed to the end user. `wasClean` is true if the server actually sent a close event, false if it just disconnected. 7284 7285 $(PITFALL 7286 The `reason` argument references a temporary buffer and there's no guarantee it will remain valid once your callback returns. It may be freed and will very likely be overwritten. If you want to keep the reason beyond the callback, make sure you `.idup` it. 7287 ) 7288 7289 History: 7290 Added March 19, 2023 (dub v11.0). 7291 +/ 7292 static struct CloseEvent { 7293 ushort code; 7294 const(char)[] reason; 7295 bool wasClean; 7296 7297 string extendedErrorInformationUnstable; 7298 7299 /++ 7300 See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 for details. 7301 +/ 7302 enum StandardCloseCodes { 7303 purposeFulfilled = 1000, 7304 goingAway = 1001, 7305 protocolError = 1002, 7306 unacceptableData = 1003, // e.g. got text message when you can only handle binary 7307 Reserved = 1004, 7308 noStatusCodePresent = 1005, // not set by endpoint. 7309 abnormalClosure = 1006, // not set by endpoint. closed without a Close control. FIXME: maybe keep a copy of errno around for these 7310 inconsistentData = 1007, // e.g. utf8 validation failed 7311 genericPolicyViolation = 1008, 7312 messageTooBig = 1009, 7313 clientRequiredExtensionMissing = 1010, // only the client should send this 7314 unnexpectedCondition = 1011, 7315 unverifiedCertificate = 1015, // not set by client 7316 } 7317 } 7318 7319 /++ 7320 The `CloseEvent` you get references a temporary buffer that may be overwritten after your handler returns. If you want to keep it or the `event.reason` member, remember to `.idup` it. 7321 7322 History: 7323 The `CloseEvent` was changed to a [arsd.core.FlexibleDelegate] on March 19, 2023 (dub v11.0). Before that, `onclose` was a public member of type `void delegate()`. This change means setters still work with or without the [CloseEvent] argument. 7324 7325 Your onclose method is now also called on abnormal terminations. Check the `wasClean` member of the `CloseEvent` to know if it came from a close frame or other cause. 7326 +/ 7327 arsd.core.FlexibleDelegate!(void delegate(CloseEvent event)) onclose; 7328 void delegate() onerror; /// 7329 void delegate(in char[]) ontextmessage; /// 7330 void delegate(in ubyte[]) onbinarymessage; /// 7331 void delegate() onopen; /// 7332 7333 /++ 7334 7335 +/ 7336 /// Group: browser_api 7337 void onmessage(void delegate(in char[]) dg) { 7338 ontextmessage = dg; 7339 } 7340 7341 /// ditto 7342 void onmessage(void delegate(in ubyte[]) dg) { 7343 onbinarymessage = dg; 7344 } 7345 7346 /* } end copy/paste */ 7347 7348 7349 7350 } 7351 7352 /++ 7353 Returns true if the request headers are asking for a websocket upgrade. 7354 7355 If this returns true, and you want to accept it, call [acceptWebsocket]. 7356 +/ 7357 bool websocketRequested(Cgi cgi) { 7358 return 7359 "sec-websocket-key" in cgi.requestHeaders 7360 && 7361 "connection" in cgi.requestHeaders && 7362 cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade") 7363 && 7364 "upgrade" in cgi.requestHeaders && 7365 cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket") 7366 ; 7367 } 7368 7369 /++ 7370 If [websocketRequested], you can call this to accept it and upgrade the connection. It returns the new [WebSocket] object you use for future communication on this connection; the `cgi` object should no longer be used. 7371 +/ 7372 WebSocket acceptWebsocket(Cgi cgi) { 7373 assert(!cgi.closed); 7374 assert(!cgi.outputtedResponseData); 7375 cgi.setResponseStatus("101 Switching Protocols"); 7376 cgi.header("Upgrade: WebSocket"); 7377 cgi.header("Connection: upgrade"); 7378 7379 string key = cgi.requestHeaders["sec-websocket-key"]; 7380 key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec 7381 7382 import std.digest.sha; 7383 auto hash = sha1Of(key); 7384 auto accept = Base64.encode(hash); 7385 7386 cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup); 7387 7388 cgi.websocketMode = true; 7389 cgi.write(""); 7390 7391 cgi.flush(); 7392 7393 auto ws = new WebSocket(cgi); 7394 ws.readyState_ = WebSocket.OPEN; 7395 return ws; 7396 } 7397 7398 // FIXME get websocket to work on other modes, not just embedded_httpd 7399 7400 /* copy/paste in http2.d { */ 7401 enum WebSocketOpcode : ubyte { 7402 continuation = 0, 7403 text = 1, 7404 binary = 2, 7405 // 3, 4, 5, 6, 7 RESERVED 7406 close = 8, 7407 ping = 9, 7408 pong = 10, 7409 // 11,12,13,14,15 RESERVED 7410 } 7411 7412 public struct WebSocketFrame { 7413 private bool populated; 7414 bool fin; 7415 bool rsv1; 7416 bool rsv2; 7417 bool rsv3; 7418 WebSocketOpcode opcode; // 4 bits 7419 bool masked; 7420 ubyte lengthIndicator; // don't set this when building one to send 7421 ulong realLength; // don't use when sending 7422 ubyte[4] maskingKey; // don't set this when sending 7423 ubyte[] data; 7424 7425 static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) { 7426 WebSocketFrame msg; 7427 msg.fin = true; 7428 msg.opcode = opcode; 7429 msg.data = cast(ubyte[]) data.dup; 7430 7431 return msg; 7432 } 7433 7434 private void send(scope void delegate(ubyte[]) llsend) { 7435 ubyte[64] headerScratch; 7436 int headerScratchPos = 0; 7437 7438 realLength = data.length; 7439 7440 { 7441 ubyte b1; 7442 b1 |= cast(ubyte) opcode; 7443 b1 |= rsv3 ? (1 << 4) : 0; 7444 b1 |= rsv2 ? (1 << 5) : 0; 7445 b1 |= rsv1 ? (1 << 6) : 0; 7446 b1 |= fin ? (1 << 7) : 0; 7447 7448 headerScratch[0] = b1; 7449 headerScratchPos++; 7450 } 7451 7452 { 7453 headerScratchPos++; // we'll set header[1] at the end of this 7454 auto rlc = realLength; 7455 ubyte b2; 7456 b2 |= masked ? (1 << 7) : 0; 7457 7458 assert(headerScratchPos == 2); 7459 7460 if(realLength > 65535) { 7461 // use 64 bit length 7462 b2 |= 0x7f; 7463 7464 // FIXME: double check endinaness 7465 foreach(i; 0 .. 8) { 7466 headerScratch[2 + 7 - i] = rlc & 0x0ff; 7467 rlc >>>= 8; 7468 } 7469 7470 headerScratchPos += 8; 7471 } else if(realLength > 125) { 7472 // use 16 bit length 7473 b2 |= 0x7e; 7474 7475 // FIXME: double check endinaness 7476 foreach(i; 0 .. 2) { 7477 headerScratch[2 + 1 - i] = rlc & 0x0ff; 7478 rlc >>>= 8; 7479 } 7480 7481 headerScratchPos += 2; 7482 } else { 7483 // use 7 bit length 7484 b2 |= realLength & 0b_0111_1111; 7485 } 7486 7487 headerScratch[1] = b2; 7488 } 7489 7490 //assert(!masked, "masking key not properly implemented"); 7491 if(masked) { 7492 // FIXME: randomize this 7493 headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; 7494 headerScratchPos += 4; 7495 7496 // we'll just mask it in place... 7497 int keyIdx = 0; 7498 foreach(i; 0 .. data.length) { 7499 data[i] = data[i] ^ maskingKey[keyIdx]; 7500 if(keyIdx == 3) 7501 keyIdx = 0; 7502 else 7503 keyIdx++; 7504 } 7505 } 7506 7507 //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); 7508 llsend(headerScratch[0 .. headerScratchPos]); 7509 llsend(data); 7510 } 7511 7512 static WebSocketFrame read(ref ubyte[] d) { 7513 WebSocketFrame msg; 7514 7515 auto orig = d; 7516 7517 WebSocketFrame needsMoreData() { 7518 d = orig; 7519 return WebSocketFrame.init; 7520 } 7521 7522 if(d.length < 2) 7523 return needsMoreData(); 7524 7525 ubyte b = d[0]; 7526 7527 msg.populated = true; 7528 7529 msg.opcode = cast(WebSocketOpcode) (b & 0x0f); 7530 b >>= 4; 7531 msg.rsv3 = b & 0x01; 7532 b >>= 1; 7533 msg.rsv2 = b & 0x01; 7534 b >>= 1; 7535 msg.rsv1 = b & 0x01; 7536 b >>= 1; 7537 msg.fin = b & 0x01; 7538 7539 b = d[1]; 7540 msg.masked = (b & 0b1000_0000) ? true : false; 7541 msg.lengthIndicator = b & 0b0111_1111; 7542 7543 d = d[2 .. $]; 7544 7545 if(msg.lengthIndicator == 0x7e) { 7546 // 16 bit length 7547 msg.realLength = 0; 7548 7549 if(d.length < 2) return needsMoreData(); 7550 7551 foreach(i; 0 .. 2) { 7552 msg.realLength |= d[0] << ((1-i) * 8); 7553 d = d[1 .. $]; 7554 } 7555 } else if(msg.lengthIndicator == 0x7f) { 7556 // 64 bit length 7557 msg.realLength = 0; 7558 7559 if(d.length < 8) return needsMoreData(); 7560 7561 foreach(i; 0 .. 8) { 7562 msg.realLength |= ulong(d[0]) << ((7-i) * 8); 7563 d = d[1 .. $]; 7564 } 7565 } else { 7566 // 7 bit length 7567 msg.realLength = msg.lengthIndicator; 7568 } 7569 7570 if(msg.masked) { 7571 7572 if(d.length < 4) return needsMoreData(); 7573 7574 msg.maskingKey = d[0 .. 4]; 7575 d = d[4 .. $]; 7576 } 7577 7578 if(msg.realLength > d.length) { 7579 return needsMoreData(); 7580 } 7581 7582 msg.data = d[0 .. cast(size_t) msg.realLength]; 7583 d = d[cast(size_t) msg.realLength .. $]; 7584 7585 return msg; 7586 } 7587 7588 void unmaskInPlace() { 7589 if(this.masked) { 7590 int keyIdx = 0; 7591 foreach(i; 0 .. this.data.length) { 7592 this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; 7593 if(keyIdx == 3) 7594 keyIdx = 0; 7595 else 7596 keyIdx++; 7597 } 7598 } 7599 } 7600 7601 char[] textData() { 7602 return cast(char[]) data; 7603 } 7604 } 7605 /* } */ 7606 } 7607 7608 7609 version(Windows) 7610 { 7611 version(CRuntime_DigitalMars) 7612 { 7613 extern(C) int setmode(int, int) nothrow @nogc; 7614 } 7615 else version(CRuntime_Microsoft) 7616 { 7617 extern(C) int _setmode(int, int) nothrow @nogc; 7618 alias setmode = _setmode; 7619 } 7620 else static assert(0); 7621 } 7622 7623 version(Posix) { 7624 import core.sys.posix.unistd; 7625 version(CRuntime_Musl) {} else { 7626 private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**); 7627 } 7628 } 7629 7630 7631 // FIXME: these aren't quite public yet. 7632 //private: 7633 7634 // template for laziness 7635 void startAddonServer()(string arg) { 7636 version(OSX) { 7637 assert(0, "Not implemented"); 7638 } else version(linux) { 7639 import core.sys.posix.unistd; 7640 pid_t pid; 7641 const(char)*[16] args; 7642 args[0] = "ARSD_CGI_ADDON_SERVER"; 7643 args[1] = arg.ptr; 7644 posix_spawn(&pid, "/proc/self/exe", 7645 null, 7646 null, 7647 args.ptr, 7648 null // env 7649 ); 7650 } else version(Windows) { 7651 wchar[2048] filename; 7652 auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length); 7653 if(len == 0 || len == filename.length) 7654 throw new Exception("could not get process name to start helper server"); 7655 7656 STARTUPINFOW startupInfo; 7657 startupInfo.cb = cast(DWORD) startupInfo.sizeof; 7658 PROCESS_INFORMATION processInfo; 7659 7660 import std.utf; 7661 7662 // I *MIGHT* need to run it as a new job or a service... 7663 auto ret = CreateProcessW( 7664 filename.ptr, 7665 toUTF16z(arg), 7666 null, // process attributes 7667 null, // thread attributes 7668 false, // inherit handles 7669 0, // creation flags 7670 null, // environment 7671 null, // working directory 7672 &startupInfo, 7673 &processInfo 7674 ); 7675 7676 if(!ret) 7677 throw new Exception("create process failed"); 7678 7679 // when done with those, if we set them 7680 /* 7681 CloseHandle(hStdInput); 7682 CloseHandle(hStdOutput); 7683 CloseHandle(hStdError); 7684 */ 7685 7686 } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)"); 7687 } 7688 7689 // template for laziness 7690 /* 7691 The websocket server is a single-process, single-thread, event 7692 I/O thing. It is passed websockets from other CGI processes 7693 and is then responsible for handling their messages and responses. 7694 Note that the CGI process is responsible for websocket setup, 7695 including authentication, etc. 7696 7697 It also gets data sent to it by other processes and is responsible 7698 for distributing that, as necessary. 7699 */ 7700 void runWebsocketServer()() { 7701 assert(0, "not implemented"); 7702 } 7703 7704 void sendToWebsocketServer(WebSocket ws, string group) { 7705 assert(0, "not implemented"); 7706 } 7707 7708 void sendToWebsocketServer(string content, string group) { 7709 assert(0, "not implemented"); 7710 } 7711 7712 7713 void runEventServer()() { 7714 runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation()); 7715 } 7716 7717 void runTimerServer()() { 7718 runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation()); 7719 } 7720 7721 version(Posix) { 7722 alias LocalServerConnectionHandle = int; 7723 alias CgiConnectionHandle = int; 7724 alias SocketConnectionHandle = int; 7725 7726 enum INVALID_CGI_CONNECTION_HANDLE = -1; 7727 } else version(Windows) { 7728 alias LocalServerConnectionHandle = HANDLE; 7729 version(embedded_httpd_threads) { 7730 alias CgiConnectionHandle = SOCKET; 7731 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 7732 } else version(fastcgi) { 7733 alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point. 7734 enum INVALID_CGI_CONNECTION_HANDLE = null; 7735 } else version(scgi) { 7736 alias CgiConnectionHandle = SOCKET; 7737 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 7738 } else { /* version(plain_cgi) */ 7739 alias CgiConnectionHandle = HANDLE; 7740 enum INVALID_CGI_CONNECTION_HANDLE = null; 7741 } 7742 alias SocketConnectionHandle = SOCKET; 7743 } 7744 7745 version(with_addon_servers_connections) 7746 LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) { 7747 version(Posix) { 7748 import core.sys.posix.unistd; 7749 import core.sys.posix.sys.un; 7750 7751 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 7752 if(sock == -1) 7753 throw new Exception("socket " ~ to!string(errno)); 7754 7755 scope(failure) 7756 close(sock); 7757 7758 cloexec(sock); 7759 7760 // add-on server processes are assumed to be local, and thus will 7761 // use unix domain sockets. Besides, I want to pass sockets to them, 7762 // so it basically must be local (except for the session server, but meh). 7763 sockaddr_un addr; 7764 addr.sun_family = AF_UNIX; 7765 version(linux) { 7766 // on linux, we will use the abstract namespace 7767 addr.sun_path[0] = 0; 7768 addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[]; 7769 } else { 7770 // but otherwise, just use a file cuz we must. 7771 addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[]; 7772 } 7773 7774 bool alreadyTried; 7775 7776 try_again: 7777 7778 if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { 7779 if(!alreadyTried && errno == ECONNREFUSED) { 7780 // try auto-spawning the server, then attempt connection again 7781 startAddonServer(arg); 7782 import core.thread; 7783 Thread.sleep(50.msecs); 7784 alreadyTried = true; 7785 goto try_again; 7786 } else 7787 throw new Exception("connect " ~ to!string(errno)); 7788 } 7789 7790 return sock; 7791 } else version(Windows) { 7792 return null; // FIXME 7793 } 7794 } 7795 7796 version(with_addon_servers_connections) 7797 void closeLocalServerConnection(LocalServerConnectionHandle handle) { 7798 version(Posix) { 7799 import core.sys.posix.unistd; 7800 close(handle); 7801 } else version(Windows) 7802 CloseHandle(handle); 7803 } 7804 7805 void runSessionServer()() { 7806 runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation()); 7807 } 7808 7809 import core.stdc.errno; 7810 7811 struct IoOp { 7812 @disable this(); 7813 @disable this(this); 7814 7815 /* 7816 So we want to be able to eventually handle generic sockets too. 7817 */ 7818 7819 enum Read = 1; 7820 enum Write = 2; 7821 enum Accept = 3; 7822 enum ReadSocketHandle = 4; 7823 7824 // Your handler may be called in a different thread than the one that initiated the IO request! 7825 // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution. 7826 private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed 7827 private void delegate(IoOp*) closeHandler; 7828 private void delegate(IoOp*) completeHandler; 7829 private int internalFd; 7830 private int operation; 7831 private int bufferLengthAllocated; 7832 private int bufferLengthUsed; 7833 private ubyte[1] internalBuffer; // it can be overallocated! 7834 7835 ubyte[] allocatedBuffer() return { 7836 return internalBuffer.ptr[0 .. bufferLengthAllocated]; 7837 } 7838 7839 ubyte[] usedBuffer() return { 7840 return allocatedBuffer[0 .. bufferLengthUsed]; 7841 } 7842 7843 void reset() { 7844 bufferLengthUsed = 0; 7845 } 7846 7847 int fd() { 7848 return internalFd; 7849 } 7850 } 7851 7852 IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) { 7853 import core.stdc.stdlib; 7854 7855 auto ptr = calloc(IoOp.sizeof + bufferSize, 1); 7856 if(ptr is null) 7857 assert(0); // out of memory! 7858 7859 auto op = cast(IoOp*) ptr; 7860 7861 op.handler = handler; 7862 op.internalFd = fd; 7863 op.operation = operation; 7864 op.bufferLengthAllocated = bufferSize; 7865 op.bufferLengthUsed = 0; 7866 7867 import core.memory; 7868 7869 GC.addRoot(ptr); 7870 7871 return op; 7872 } 7873 7874 void freeIoOp(ref IoOp* ptr) { 7875 7876 import core.memory; 7877 GC.removeRoot(ptr); 7878 7879 import core.stdc.stdlib; 7880 free(ptr); 7881 ptr = null; 7882 } 7883 7884 version(Posix) 7885 version(with_addon_servers_connections) 7886 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 7887 7888 //import std.stdio : writeln; writeln(cast(string) data); 7889 7890 import core.sys.posix.unistd; 7891 7892 auto ret = write(connection, data.ptr, data.length); 7893 if(ret != data.length) { 7894 if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { 7895 // the file is closed, remove it 7896 eis.fileClosed(connection); 7897 } else 7898 throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME 7899 } 7900 } 7901 version(Windows) 7902 version(with_addon_servers_connections) 7903 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 7904 // FIXME 7905 } 7906 7907 bool isInvalidHandle(CgiConnectionHandle h) { 7908 return h == INVALID_CGI_CONNECTION_HANDLE; 7909 } 7910 7911 /+ 7912 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv 7913 https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode 7914 https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive 7915 https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports 7916 https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport 7917 https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex 7918 https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects 7919 https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer 7920 https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call 7921 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult 7922 7923 +/ 7924 7925 /++ 7926 You can customize your server by subclassing the appropriate server. Then, register your 7927 subclass at compile time with the [registerEventIoServer] template, or implement your own 7928 main function and call it yourself. 7929 7930 $(TIP If you make your subclass a `final class`, there is a slight performance improvement.) 7931 +/ 7932 version(with_addon_servers_connections) 7933 interface EventIoServer { 7934 bool handleLocalConnectionData(IoOp* op, int receivedFd); 7935 void handleLocalConnectionClose(IoOp* op); 7936 void handleLocalConnectionComplete(IoOp* op); 7937 void wait_timeout(); 7938 void fileClosed(int fd); 7939 7940 void epoll_fd(int fd); 7941 } 7942 7943 // the sink should buffer it 7944 private void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) { 7945 static if(is(T == struct)) { 7946 foreach(member; __traits(allMembers, T)) 7947 serialize(sink, __traits(getMember, t, member)); 7948 } else static if(is(T : int)) { 7949 // no need to think of endianness just because this is only used 7950 // for local, same-machine stuff anyway. thanks private lol 7951 sink((cast(ubyte*) &t)[0 .. t.sizeof]); 7952 } else static if(is(T == string) || is(T : const(ubyte)[])) { 7953 // these are common enough to optimize 7954 int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc. 7955 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7956 sink(cast(ubyte[]) t[]); 7957 } else static if(is(T : A[], A)) { 7958 // generic array is less optimal but still prolly ok 7959 int len = cast(int) t.length; 7960 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7961 foreach(item; t) 7962 serialize(sink, item); 7963 } else static assert(0, T.stringof); 7964 } 7965 7966 // all may be stack buffers, so use cautio 7967 private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) { 7968 static if(is(T == struct)) { 7969 T t; 7970 foreach(member; __traits(allMembers, T)) 7971 deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; }); 7972 dg(t); 7973 } else static if(is(T : int)) { 7974 // no need to think of endianness just because this is only used 7975 // for local, same-machine stuff anyway. thanks private lol 7976 T t; 7977 auto data = get(t.sizeof); 7978 t = (cast(T[]) data)[0]; 7979 dg(t); 7980 } else static if(is(T == string) || is(T : const(ubyte)[])) { 7981 // these are common enough to optimize 7982 int len; 7983 auto data = get(len.sizeof); 7984 len = (cast(int[]) data)[0]; 7985 7986 /* 7987 typeof(T[0])[2000] stackBuffer; 7988 T buffer; 7989 7990 if(len < stackBuffer.length) 7991 buffer = stackBuffer[0 .. len]; 7992 else 7993 buffer = new T(len); 7994 7995 data = get(len * typeof(T[0]).sizeof); 7996 */ 7997 7998 T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof); 7999 8000 dg(t); 8001 } else static if(is(T == E[], E)) { 8002 T t; 8003 int len; 8004 auto data = get(len.sizeof); 8005 len = (cast(int[]) data)[0]; 8006 t.length = len; 8007 foreach(ref e; t) { 8008 deserialize!E(get, (ele) { e = ele; }); 8009 } 8010 dg(t); 8011 } else static assert(0, T.stringof); 8012 } 8013 8014 unittest { 8015 serialize((ubyte[] b) { 8016 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); }); 8017 }, 1); 8018 serialize((ubyte[] b) { 8019 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); }); 8020 }, 56674); 8021 ubyte[1000] buffer; 8022 int bufferPoint; 8023 void add(scope ubyte[] b) { 8024 buffer[bufferPoint .. bufferPoint + b.length] = b[]; 8025 bufferPoint += b.length; 8026 } 8027 ubyte[] get(int sz) { 8028 auto b = buffer[bufferPoint .. bufferPoint + sz]; 8029 bufferPoint += sz; 8030 return b; 8031 } 8032 serialize(&add, "test here"); 8033 bufferPoint = 0; 8034 deserialize!string(&get, (t) { assert(t == "test here"); }); 8035 bufferPoint = 0; 8036 8037 struct Foo { 8038 int a; 8039 ubyte c; 8040 string d; 8041 } 8042 serialize(&add, Foo(403, 37, "amazing")); 8043 bufferPoint = 0; 8044 deserialize!Foo(&get, (t) { 8045 assert(t.a == 403); 8046 assert(t.c == 37); 8047 assert(t.d == "amazing"); 8048 }); 8049 bufferPoint = 0; 8050 } 8051 8052 /* 8053 Here's the way the RPC interface works: 8054 8055 You define the interface that lists the functions you can call on the remote process. 8056 The interface may also have static methods for convenience. These forward to a singleton 8057 instance of an auto-generated class, which actually sends the args over the pipe. 8058 8059 An impl class actually implements it. A receiving server deserializes down the pipe and 8060 calls methods on the class. 8061 8062 I went with the interface to get some nice compiler checking and documentation stuff. 8063 8064 I could have skipped the interface and just implemented it all from the server class definition 8065 itself, but then the usage may call the method instead of rpcing it; I just like having the user 8066 interface and the implementation separate so you aren't tempted to `new impl` to call the methods. 8067 8068 8069 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. 8070 8071 Realistically though the bodies would just be 8072 connection.call(this.mangleof, args...) sooooo. 8073 8074 FIXME: overloads aren't supported 8075 */ 8076 8077 /// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this. 8078 interface SessionObject {} 8079 8080 private immutable void delegate(string[])[string] scheduledJobHandlers; 8081 private immutable void delegate(string[])[string] websocketServers; 8082 8083 version(with_breaking_cgi_features) 8084 mixin(q{ 8085 8086 mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) { 8087 static import std.traits; 8088 8089 // 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. 8090 static foreach(idx, member; __traits(derivedMembers, T)) { 8091 static if(__traits(isVirtualMethod, __traits(getMember, T, member))) 8092 mixin( q{ 8093 std.traits.ReturnType!(__traits(getMember, T, member)) 8094 } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params) 8095 { 8096 SerializationBuffer buffer; 8097 auto i = cast(ushort) idx; 8098 serialize(&buffer.sink, i); 8099 serialize(&buffer.sink, __traits(getMember, T, member).mangleof); 8100 foreach(param; params) 8101 serialize(&buffer.sink, param); 8102 8103 auto sendable = buffer.sendable; 8104 8105 version(Posix) {{ 8106 auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0); 8107 8108 if(ret == -1) { 8109 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 8110 } else if(ret == 0) { 8111 throw new Exception("Connection to addon server lost"); 8112 } if(ret < sendable.length) 8113 throw new Exception("Send failed to send all"); 8114 assert(ret == sendable.length); 8115 }} // FIXME Windows impl 8116 8117 static if(!is(typeof(return) == void)) { 8118 // there is a return value; we need to wait for it too 8119 version(Posix) { 8120 ubyte[3000] revBuffer; 8121 auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0); 8122 auto got = revBuffer[0 .. ret]; 8123 8124 int dataLocation; 8125 ubyte[] grab(int sz) { 8126 auto dataLocation1 = dataLocation; 8127 dataLocation += sz; 8128 return got[dataLocation1 .. dataLocation]; 8129 } 8130 8131 typeof(return) retu; 8132 deserialize!(typeof(return))(&grab, (a) { retu = a; }); 8133 return retu; 8134 } else { 8135 // FIXME Windows impl 8136 return typeof(return).init; 8137 } 8138 8139 } 8140 }}); 8141 } 8142 8143 private static typeof(this) singletonInstance; 8144 private LocalServerConnectionHandle connectionHandle; 8145 8146 static typeof(this) connection() { 8147 if(singletonInstance is null) { 8148 singletonInstance = new typeof(this)(); 8149 singletonInstance.connect(); 8150 } 8151 return singletonInstance; 8152 } 8153 8154 void connect() { 8155 connectionHandle = openLocalServerConnection(serverPath, cmdArg); 8156 } 8157 8158 void disconnect() { 8159 closeLocalServerConnection(connectionHandle); 8160 } 8161 } 8162 8163 void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) { 8164 ushort calledIdx; 8165 string calledFunction; 8166 8167 int dataLocation; 8168 ubyte[] grab(int sz) { 8169 if(sz == 0) assert(0); 8170 auto d = data[dataLocation .. dataLocation + sz]; 8171 dataLocation += sz; 8172 return d; 8173 } 8174 8175 again: 8176 8177 deserialize!ushort(&grab, (a) { calledIdx = a; }); 8178 deserialize!string(&grab, (a) { calledFunction = a; }); 8179 8180 import std.traits; 8181 8182 sw: switch(calledIdx) { 8183 foreach(idx, memberName; __traits(derivedMembers, Interface)) 8184 static if(__traits(isVirtualMethod, __traits(getMember, Interface, memberName))) { 8185 case idx: 8186 assert(calledFunction == __traits(getMember, Interface, memberName).mangleof); 8187 8188 Parameters!(__traits(getMember, Interface, memberName)) params; 8189 foreach(ref param; params) 8190 deserialize!(typeof(param))(&grab, (a) { param = a; }); 8191 8192 static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) { 8193 __traits(getMember, this_, memberName)(params); 8194 } else { 8195 auto ret = __traits(getMember, this_, memberName)(params); 8196 SerializationBuffer buffer; 8197 serialize(&buffer.sink, ret); 8198 8199 auto sendable = buffer.sendable; 8200 8201 version(Posix) { 8202 auto r = send(fd, sendable.ptr, sendable.length, 0); 8203 if(r == -1) { 8204 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 8205 } else if(r == 0) { 8206 throw new Exception("Connection to addon client lost"); 8207 } if(r < sendable.length) 8208 throw new Exception("Send failed to send all"); 8209 8210 } // FIXME Windows impl 8211 } 8212 break sw; 8213 } 8214 default: assert(0); 8215 } 8216 8217 if(dataLocation != data.length) 8218 goto again; 8219 } 8220 8221 8222 private struct SerializationBuffer { 8223 ubyte[2048] bufferBacking; 8224 int bufferLocation; 8225 void sink(scope ubyte[] data) { 8226 bufferBacking[bufferLocation .. bufferLocation + data.length] = data[]; 8227 bufferLocation += data.length; 8228 } 8229 8230 ubyte[] sendable() return { 8231 return bufferBacking[0 .. bufferLocation]; 8232 } 8233 } 8234 8235 /* 8236 FIXME: 8237 add a version command line arg 8238 version data in the library 8239 management gui as external program 8240 8241 at server with event_fd for each run 8242 use .mangleof in the at function name 8243 8244 i think the at server will have to: 8245 pipe args to the child 8246 collect child output for logging 8247 get child return value for logging 8248 8249 on windows timers work differently. idk how to best combine with the io stuff. 8250 8251 will have to have dump and restore too, so i can restart without losing stuff. 8252 */ 8253 8254 /++ 8255 A convenience object for talking to the [BasicDataServer] from a higher level. 8256 See: [Cgi.getSessionObject]. 8257 8258 You pass it a `Data` struct describing the data you want saved in the session. 8259 Then, this class will generate getter and setter properties that allow access 8260 to that data. 8261 8262 Note that each load and store will be done as-accessed; it doesn't front-load 8263 mutable data nor does it batch updates out of fear of read-modify-write race 8264 conditions. (In fact, right now it does this for everything, but in the future, 8265 I might batch load `immutable` members of the Data struct.) 8266 8267 At some point in the future, I might also let it do different backends, like 8268 a client-side cookie store too, but idk. 8269 8270 Note that the plain-old-data members of your `Data` struct are wrapped by this 8271 interface via a static foreach to make property functions. 8272 8273 See_Also: [MockSession] 8274 +/ 8275 interface Session(Data) : SessionObject { 8276 @property string sessionId() const; 8277 8278 /++ 8279 Starts a new session. Note that a session is also 8280 implicitly started as soon as you write data to it, 8281 so if you need to alter these parameters from their 8282 defaults, be sure to explicitly call this BEFORE doing 8283 any writes to session data. 8284 8285 Params: 8286 idleLifetime = How long, in seconds, the session 8287 should remain in memory when not being read from 8288 or written to. The default is one day. 8289 8290 NOT IMPLEMENTED 8291 8292 useExtendedLifetimeCookie = The session ID is always 8293 stored in a HTTP cookie, and by default, that cookie 8294 is discarded when the user closes their browser. 8295 8296 But if you set this to true, it will use a non-perishable 8297 cookie for the given idleLifetime. 8298 8299 NOT IMPLEMENTED 8300 +/ 8301 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false); 8302 8303 /++ 8304 Regenerates the session ID and updates the associated 8305 cookie. 8306 8307 This is also your chance to change immutable data 8308 (not yet implemented). 8309 +/ 8310 void regenerateId(); 8311 8312 /++ 8313 Terminates this session, deleting all saved data. 8314 +/ 8315 void terminate(); 8316 8317 /++ 8318 Plain-old-data members of your `Data` struct are wrapped here via 8319 the property getters and setters. 8320 8321 If the member is a non-string array, it returns a magical array proxy 8322 object which allows for atomic appends and replaces via overloaded operators. 8323 You can slice this to get a range representing a $(B const) view of the array. 8324 This is to protect you against read-modify-write race conditions. 8325 +/ 8326 static foreach(memberName; __traits(allMembers, Data)) 8327 static if(is(typeof(__traits(getMember, Data, memberName)))) 8328 mixin(q{ 8329 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout; 8330 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value); 8331 }); 8332 8333 } 8334 8335 /++ 8336 An implementation of [Session] that works on real cgi connections utilizing the 8337 [BasicDataServer]. 8338 8339 As opposed to a [MockSession] which is made for testing purposes. 8340 8341 You will not construct one of these directly. See [Cgi.getSessionObject] instead. 8342 +/ 8343 class BasicDataServerSession(Data) : Session!Data { 8344 private Cgi cgi; 8345 private string sessionId_; 8346 8347 public @property string sessionId() const { 8348 return sessionId_; 8349 } 8350 8351 protected @property string sessionId(string s) { 8352 return this.sessionId_ = s; 8353 } 8354 8355 private this(Cgi cgi) { 8356 this.cgi = cgi; 8357 if(auto ptr = "sessionId" in cgi.cookies) 8358 sessionId = (*ptr).length ? *ptr : null; 8359 } 8360 8361 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { 8362 assert(sessionId is null); 8363 8364 // FIXME: what if there is a session ID cookie, but no corresponding session on the server? 8365 8366 import std.random, std.conv; 8367 sessionId = to!string(uniform(1, long.max)); 8368 8369 BasicDataServer.connection.createSession(sessionId, idleLifetime); 8370 setCookie(); 8371 } 8372 8373 protected void setCookie() { 8374 cgi.setCookie( 8375 "sessionId", sessionId, 8376 0 /* expiration */, 8377 "/" /* path */, 8378 null /* domain */, 8379 true /* http only */, 8380 cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); 8381 } 8382 8383 void regenerateId() { 8384 if(sessionId is null) { 8385 start(); 8386 return; 8387 } 8388 import std.random, std.conv; 8389 auto oldSessionId = sessionId; 8390 sessionId = to!string(uniform(1, long.max)); 8391 BasicDataServer.connection.renameSession(oldSessionId, sessionId); 8392 setCookie(); 8393 } 8394 8395 void terminate() { 8396 BasicDataServer.connection.destroySession(sessionId); 8397 sessionId = null; 8398 setCookie(); 8399 } 8400 8401 static foreach(memberName; __traits(allMembers, Data)) 8402 static if(is(typeof(__traits(getMember, Data, memberName)))) 8403 mixin(q{ 8404 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 8405 if(sessionId is null) 8406 return typeof(return).init; 8407 8408 import std.traits; 8409 auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName); 8410 if(v.length == 0) 8411 return typeof(return).init; 8412 import std.conv; 8413 // why this cast? to doesn't like being given an inout argument. so need to do it without that, then 8414 // we need to return it and that needed the cast. It should be fine since we basically respect constness.. 8415 // basically. Assuming the session is POD this should be fine. 8416 return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v); 8417 } 8418 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 8419 if(sessionId is null) 8420 start(); 8421 import std.conv; 8422 import std.traits; 8423 BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value)); 8424 return value; 8425 } 8426 }); 8427 } 8428 8429 /++ 8430 A mock object that works like the real session, but doesn't actually interact with any actual database or http connection. 8431 Simply stores the data in its instance members. 8432 +/ 8433 class MockSession(Data) : Session!Data { 8434 pure { 8435 @property string sessionId() const { return "mock"; } 8436 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {} 8437 void regenerateId() {} 8438 void terminate() {} 8439 8440 private Data store_; 8441 8442 static foreach(memberName; __traits(allMembers, Data)) 8443 static if(is(typeof(__traits(getMember, Data, memberName)))) 8444 mixin(q{ 8445 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 8446 return __traits(getMember, store_, memberName); 8447 } 8448 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 8449 return __traits(getMember, store_, memberName) = value; 8450 } 8451 }); 8452 } 8453 } 8454 8455 /++ 8456 Direct interface to the basic data add-on server. You can 8457 typically use [Cgi.getSessionObject] as a more convenient interface. 8458 +/ 8459 version(with_addon_servers_connections) 8460 interface BasicDataServer { 8461 /// 8462 void createSession(string sessionId, int lifetime); 8463 /// 8464 void renewSession(string sessionId, int lifetime); 8465 /// 8466 void destroySession(string sessionId); 8467 /// 8468 void renameSession(string oldSessionId, string newSessionId); 8469 8470 /// 8471 void setSessionData(string sessionId, string dataKey, string dataValue); 8472 /// 8473 string getSessionData(string sessionId, string dataKey); 8474 8475 /// 8476 static BasicDataServerConnection connection() { 8477 return BasicDataServerConnection.connection(); 8478 } 8479 } 8480 8481 version(with_addon_servers_connections) 8482 class BasicDataServerConnection : BasicDataServer { 8483 mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server"); 8484 } 8485 8486 version(with_addon_servers) 8487 final class BasicDataServerImplementation : BasicDataServer, EventIoServer { 8488 8489 void createSession(string sessionId, int lifetime) { 8490 sessions[sessionId.idup] = Session(lifetime); 8491 } 8492 void destroySession(string sessionId) { 8493 sessions.remove(sessionId); 8494 } 8495 void renewSession(string sessionId, int lifetime) { 8496 sessions[sessionId].lifetime = lifetime; 8497 } 8498 void renameSession(string oldSessionId, string newSessionId) { 8499 sessions[newSessionId.idup] = sessions[oldSessionId]; 8500 sessions.remove(oldSessionId); 8501 } 8502 void setSessionData(string sessionId, string dataKey, string dataValue) { 8503 if(sessionId !in sessions) 8504 createSession(sessionId, 3600); // FIXME? 8505 sessions[sessionId].values[dataKey.idup] = dataValue.idup; 8506 } 8507 string getSessionData(string sessionId, string dataKey) { 8508 if(auto session = sessionId in sessions) { 8509 if(auto data = dataKey in (*session).values) 8510 return *data; 8511 else 8512 return null; // no such data 8513 8514 } else { 8515 return null; // no session 8516 } 8517 } 8518 8519 8520 protected: 8521 8522 struct Session { 8523 int lifetime; 8524 8525 string[string] values; 8526 } 8527 8528 Session[string] sessions; 8529 8530 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8531 auto data = op.usedBuffer; 8532 dispatchRpcServer!BasicDataServer(this, data, op.fd); 8533 return false; 8534 } 8535 8536 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 8537 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 8538 void wait_timeout() {} 8539 void fileClosed(int fd) {} // stateless so irrelevant 8540 void epoll_fd(int fd) {} 8541 } 8542 8543 /++ 8544 See [schedule] to make one of these. You then call one of the methods here to set it up: 8545 8546 --- 8547 schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC 8548 schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds 8549 schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it 8550 --- 8551 +/ 8552 version(with_addon_servers_connections) 8553 struct ScheduledJobHelper { 8554 private string func; 8555 private string[] args; 8556 private bool consumed; 8557 8558 private this(string func, string[] args) { 8559 this.func = func; 8560 this.args = args; 8561 } 8562 8563 ~this() { 8564 assert(consumed); 8565 } 8566 8567 /++ 8568 Schedules the job to be run at the given time. 8569 +/ 8570 void at(DateTime when, immutable TimeZone timezone = UTC()) { 8571 consumed = true; 8572 8573 auto conn = ScheduledJobServerConnection.connection; 8574 import std.file; 8575 auto st = SysTime(when, timezone); 8576 auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args); 8577 } 8578 8579 /++ 8580 Schedules the job to run at least after the specified delay. 8581 +/ 8582 void delay(Duration delay) { 8583 consumed = true; 8584 8585 auto conn = ScheduledJobServerConnection.connection; 8586 import std.file; 8587 auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args); 8588 } 8589 8590 /++ 8591 Runs the job in the background ASAP. 8592 8593 $(NOTE It may run in a background thread. Don't segfault!) 8594 +/ 8595 void asap() { 8596 consumed = true; 8597 8598 auto conn = ScheduledJobServerConnection.connection; 8599 import std.file; 8600 auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args); 8601 } 8602 8603 /+ 8604 /++ 8605 Schedules the job to recur on the given pattern. 8606 +/ 8607 void recur(string spec) { 8608 8609 } 8610 +/ 8611 } 8612 8613 /++ 8614 First step to schedule a job on the scheduled job server. 8615 8616 The scheduled job needs to be a top-level function that doesn't read any 8617 variables from outside its arguments because it may be run in a new process, 8618 without any context existing later. 8619 8620 You MUST set details on the returned object to actually do anything! 8621 +/ 8622 template schedule(alias fn, T...) if(is(typeof(fn) == function)) { 8623 /// 8624 ScheduledJobHelper schedule(T args) { 8625 // this isn't meant to ever be called, but instead just to 8626 // get the compiler to type check the arguments passed for us 8627 auto sample = delegate() { 8628 fn(args); 8629 }; 8630 string[] sargs; 8631 foreach(arg; args) 8632 sargs ~= to!string(arg); 8633 return ScheduledJobHelper(fn.mangleof, sargs); 8634 } 8635 8636 shared static this() { 8637 scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) { 8638 import std.traits; 8639 Parameters!fn args; 8640 foreach(idx, ref arg; args) 8641 arg = to!(typeof(arg))(sargs[idx]); 8642 fn(args); 8643 }; 8644 } 8645 } 8646 8647 /// 8648 interface ScheduledJobServer { 8649 /// Use the [schedule] function for a higher-level interface. 8650 int scheduleJob(int whenIs, int when, string executable, string func, string[] args); 8651 /// 8652 void cancelJob(int jobId); 8653 } 8654 8655 version(with_addon_servers_connections) 8656 class ScheduledJobServerConnection : ScheduledJobServer { 8657 mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server"); 8658 } 8659 8660 version(with_addon_servers) 8661 final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer { 8662 // FIXME: we need to handle SIGCHLD in this somehow 8663 // whenIs is 0 for relative, 1 for absolute 8664 protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) { 8665 auto nj = nextJobId; 8666 nextJobId++; 8667 8668 version(linux) { 8669 import core.sys.linux.timerfd; 8670 import core.sys.linux.epoll; 8671 import core.sys.posix.unistd; 8672 8673 8674 auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); 8675 if(fd == -1) 8676 throw new Exception("fd timer create failed"); 8677 8678 foreach(ref arg; args) 8679 arg = arg.idup; 8680 auto job = Job(executable.idup, func.idup, .dup(args), fd, nj); 8681 8682 itimerspec value; 8683 value.it_value.tv_sec = when; 8684 value.it_value.tv_nsec = 0; 8685 8686 value.it_interval.tv_sec = 0; 8687 value.it_interval.tv_nsec = 0; 8688 8689 if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1) 8690 throw new Exception("couldn't set fd timer"); 8691 8692 auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) { 8693 jobs.remove(nj); 8694 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null); 8695 close(fd); 8696 8697 8698 spawnProcess([job.executable, "--timed-job", job.func] ~ job.args); 8699 8700 return true; 8701 }); 8702 scope(failure) 8703 freeIoOp(op); 8704 8705 epoll_event ev; 8706 ev.events = EPOLLIN | EPOLLET; 8707 ev.data.ptr = op; 8708 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) 8709 throw new Exception("epoll_ctl " ~ to!string(errno)); 8710 8711 jobs[nj] = job; 8712 return nj; 8713 } else assert(0); 8714 } 8715 8716 protected void cancelJob(int jobId) { 8717 version(linux) { 8718 auto job = jobId in jobs; 8719 if(job is null) 8720 return; 8721 8722 jobs.remove(jobId); 8723 8724 version(linux) { 8725 import core.sys.linux.timerfd; 8726 import core.sys.linux.epoll; 8727 import core.sys.posix.unistd; 8728 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null); 8729 close(job.timerfd); 8730 } 8731 } 8732 jobs.remove(jobId); 8733 } 8734 8735 int nextJobId = 1; 8736 static struct Job { 8737 string executable; 8738 string func; 8739 string[] args; 8740 int timerfd; 8741 int id; 8742 } 8743 Job[int] jobs; 8744 8745 8746 // event io server methods below 8747 8748 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8749 auto data = op.usedBuffer; 8750 dispatchRpcServer!ScheduledJobServer(this, data, op.fd); 8751 return false; 8752 } 8753 8754 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 8755 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 8756 void wait_timeout() {} 8757 void fileClosed(int fd) {} // stateless so irrelevant 8758 8759 int epoll_fd_; 8760 void epoll_fd(int fd) {this.epoll_fd_ = fd; } 8761 int epoll_fd() { return epoll_fd_; } 8762 } 8763 8764 /++ 8765 History: 8766 Added January 6, 2019 8767 +/ 8768 version(with_addon_servers_connections) 8769 interface EventSourceServer { 8770 /++ 8771 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. 8772 8773 See_Also: 8774 [sendEvent] 8775 8776 Bugs: 8777 Not implemented on Windows! 8778 8779 History: 8780 Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design. 8781 +/ 8782 public static void adoptConnection(Cgi cgi, in char[] eventUrl) { 8783 /* 8784 If lastEventId is missing or empty, you just get new events as they come. 8785 8786 If it is set from something else, it sends all since then (that are still alive) 8787 down the pipe immediately. 8788 8789 The reason it can come from the header is that's what the standard defines for 8790 browser reconnects. The reason it can come from a query string is just convenience 8791 in catching up in a user-defined manner. 8792 8793 The reason the header overrides the query string is if the browser tries to reconnect, 8794 it will send the header AND the query (it reconnects to the same url), so we just 8795 want to do the restart thing. 8796 8797 Note that if you ask for "0" as the lastEventId, it will get ALL still living events. 8798 */ 8799 string lastEventId = cgi.lastEventId; 8800 if(lastEventId.length == 0 && "lastEventId" in cgi.get) 8801 lastEventId = cgi.get["lastEventId"]; 8802 8803 cgi.setResponseContentType("text/event-stream"); 8804 cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later 8805 cgi.flush(); 8806 8807 cgi.closed = true; 8808 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 8809 scope(exit) 8810 closeLocalServerConnection(s); 8811 8812 version(fastcgi) 8813 throw new Exception("sending fcgi connections not supported"); 8814 else { 8815 auto fd = cgi.getOutputFileHandle(); 8816 if(isInvalidHandle(fd)) 8817 throw new Exception("bad fd from cgi!"); 8818 8819 EventSourceServerImplementation.SendableEventConnection sec; 8820 sec.populate(cgi.responseChunked, eventUrl, lastEventId); 8821 8822 version(Posix) { 8823 auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd); 8824 assert(res == sec.sizeof); 8825 } else version(Windows) { 8826 // FIXME 8827 } 8828 } 8829 } 8830 8831 /++ 8832 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. 8833 8834 Params: 8835 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. 8836 event = the event type string, which is used in the Javascript addEventListener API on EventSource 8837 data = the event data. Available in JS as `event.data`. 8838 lifetime = the amount of time to keep this event for replaying on the event server. 8839 8840 Bugs: 8841 Not implemented on Windows! 8842 8843 History: 8844 Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design. 8845 +/ 8846 public static void sendEvent(string url, string event, string data, int lifetime) { 8847 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 8848 scope(exit) 8849 closeLocalServerConnection(s); 8850 8851 EventSourceServerImplementation.SendableEvent sev; 8852 sev.populate(url, event, data, lifetime); 8853 8854 version(Posix) { 8855 auto ret = send(s, &sev, sev.sizeof, 0); 8856 assert(ret == sev.sizeof); 8857 } else version(Windows) { 8858 // FIXME 8859 } 8860 } 8861 8862 /++ 8863 Messages sent to `url` will also be sent to anyone listening on `forwardUrl`. 8864 8865 See_Also: [disconnect] 8866 +/ 8867 void connect(string url, string forwardUrl); 8868 8869 /++ 8870 Disconnects `forwardUrl` from `url` 8871 8872 See_Also: [connect] 8873 +/ 8874 void disconnect(string url, string forwardUrl); 8875 } 8876 8877 /// 8878 version(with_addon_servers) 8879 final class EventSourceServerImplementation : EventSourceServer, EventIoServer { 8880 8881 protected: 8882 8883 void connect(string url, string forwardUrl) { 8884 pipes[url] ~= forwardUrl; 8885 } 8886 void disconnect(string url, string forwardUrl) { 8887 auto t = url in pipes; 8888 if(t is null) 8889 return; 8890 foreach(idx, n; (*t)) 8891 if(n == forwardUrl) { 8892 (*t)[idx] = (*t)[$-1]; 8893 (*t) = (*t)[0 .. $-1]; 8894 break; 8895 } 8896 } 8897 8898 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8899 if(receivedFd != -1) { 8900 //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer); 8901 8902 //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5); 8903 8904 SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr; 8905 8906 auto url = got.url.idup; 8907 eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false); 8908 8909 // FIXME: catch up on past messages here 8910 } else { 8911 auto data = op.usedBuffer; 8912 auto event = cast(SendableEvent*) data.ptr; 8913 8914 if(event.magic == 0xdeadbeef) { 8915 handleInputEvent(event); 8916 8917 if(event.url in pipes) 8918 foreach(pipe; pipes[event.url]) { 8919 event.url = pipe; 8920 handleInputEvent(event); 8921 } 8922 } else { 8923 dispatchRpcServer!EventSourceServer(this, data, op.fd); 8924 } 8925 } 8926 return false; 8927 } 8928 void handleLocalConnectionClose(IoOp* op) { 8929 fileClosed(op.fd); 8930 } 8931 void handleLocalConnectionComplete(IoOp* op) {} 8932 8933 void wait_timeout() { 8934 // just keeping alive 8935 foreach(url, connections; eventConnectionsByUrl) 8936 foreach(connection; connections) 8937 if(connection.needsChunking) 8938 nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); 8939 else 8940 nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); 8941 } 8942 8943 void fileClosed(int fd) { 8944 outer: foreach(url, ref connections; eventConnectionsByUrl) { 8945 foreach(idx, conn; connections) { 8946 if(fd == conn.fd) { 8947 connections[idx] = connections[$-1]; 8948 connections = connections[0 .. $ - 1]; 8949 continue outer; 8950 } 8951 } 8952 } 8953 } 8954 8955 void epoll_fd(int fd) {} 8956 8957 8958 private: 8959 8960 8961 struct SendableEventConnection { 8962 ubyte responseChunked; 8963 8964 int urlLength; 8965 char[256] urlBuffer = 0; 8966 8967 int lastEventIdLength; 8968 char[32] lastEventIdBuffer = 0; 8969 8970 char[] url() return { 8971 return urlBuffer[0 .. urlLength]; 8972 } 8973 void url(in char[] u) { 8974 urlBuffer[0 .. u.length] = u[]; 8975 urlLength = cast(int) u.length; 8976 } 8977 char[] lastEventId() return { 8978 return lastEventIdBuffer[0 .. lastEventIdLength]; 8979 } 8980 void populate(bool responseChunked, in char[] url, in char[] lastEventId) 8981 in { 8982 assert(url.length < this.urlBuffer.length); 8983 assert(lastEventId.length < this.lastEventIdBuffer.length); 8984 } 8985 do { 8986 this.responseChunked = responseChunked ? 1 : 0; 8987 this.urlLength = cast(int) url.length; 8988 this.lastEventIdLength = cast(int) lastEventId.length; 8989 8990 this.urlBuffer[0 .. url.length] = url[]; 8991 this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[]; 8992 } 8993 } 8994 8995 struct SendableEvent { 8996 int magic = 0xdeadbeef; 8997 int urlLength; 8998 char[256] urlBuffer = 0; 8999 int typeLength; 9000 char[32] typeBuffer = 0; 9001 int messageLength; 9002 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. 9003 int _lifetime; 9004 9005 char[] message() return { 9006 return messageBuffer[0 .. messageLength]; 9007 } 9008 char[] type() return { 9009 return typeBuffer[0 .. typeLength]; 9010 } 9011 char[] url() return { 9012 return urlBuffer[0 .. urlLength]; 9013 } 9014 void url(in char[] u) { 9015 urlBuffer[0 .. u.length] = u[]; 9016 urlLength = cast(int) u.length; 9017 } 9018 int lifetime() { 9019 return _lifetime; 9020 } 9021 9022 /// 9023 void populate(string url, string type, string message, int lifetime) 9024 in { 9025 assert(url.length < this.urlBuffer.length); 9026 assert(type.length < this.typeBuffer.length); 9027 assert(message.length < this.messageBuffer.length); 9028 } 9029 do { 9030 this.urlLength = cast(int) url.length; 9031 this.typeLength = cast(int) type.length; 9032 this.messageLength = cast(int) message.length; 9033 this._lifetime = lifetime; 9034 9035 this.urlBuffer[0 .. url.length] = url[]; 9036 this.typeBuffer[0 .. type.length] = type[]; 9037 this.messageBuffer[0 .. message.length] = message[]; 9038 } 9039 } 9040 9041 struct EventConnection { 9042 int fd; 9043 bool needsChunking; 9044 } 9045 9046 private EventConnection[][string] eventConnectionsByUrl; 9047 private string[][string] pipes; 9048 9049 private void handleInputEvent(scope SendableEvent* event) { 9050 static int eventId; 9051 9052 static struct StoredEvent { 9053 int id; 9054 string type; 9055 string message; 9056 int lifetimeRemaining; 9057 } 9058 9059 StoredEvent[][string] byUrl; 9060 9061 int thisId = ++eventId; 9062 9063 if(event.lifetime) 9064 byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime); 9065 9066 auto connectionsPtr = event.url in eventConnectionsByUrl; 9067 EventConnection[] connections; 9068 if(connectionsPtr is null) 9069 return; 9070 else 9071 connections = *connectionsPtr; 9072 9073 char[4096] buffer; 9074 char[] formattedMessage; 9075 9076 void append(const char[] a) { 9077 // the 6's here are to leave room for a HTTP chunk header, if it proves necessary 9078 buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[]; 9079 formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length]; 9080 } 9081 9082 import std.algorithm.iteration; 9083 9084 if(connections.length) { 9085 append("id: "); 9086 append(to!string(thisId)); 9087 append("\n"); 9088 9089 append("event: "); 9090 append(event.type); 9091 append("\n"); 9092 9093 foreach(line; event.message.splitter("\n")) { 9094 append("data: "); 9095 append(line); 9096 append("\n"); 9097 } 9098 9099 append("\n"); 9100 } 9101 9102 // chunk it for HTTP! 9103 auto len = toHex(formattedMessage.length); 9104 buffer[4 .. 6] = "\r\n"[]; 9105 buffer[4 - len.length .. 4] = len[]; 9106 buffer[6 + formattedMessage.length] = '\r'; 9107 buffer[6 + formattedMessage.length + 1] = '\n'; 9108 9109 auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2]; 9110 // done 9111 9112 // FIXME: send back requests when needed 9113 // FIXME: send a single ":\n" every 15 seconds to keep alive 9114 9115 foreach(connection; connections) { 9116 if(connection.needsChunking) { 9117 nonBlockingWrite(this, connection.fd, chunkedMessage); 9118 } else { 9119 nonBlockingWrite(this, connection.fd, formattedMessage); 9120 } 9121 } 9122 } 9123 } 9124 9125 void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) { 9126 version(Posix) { 9127 9128 import core.sys.posix.unistd; 9129 import core.sys.posix.fcntl; 9130 import core.sys.posix.sys.un; 9131 9132 import core.sys.posix.signal; 9133 signal(SIGPIPE, SIG_IGN); 9134 9135 static extern(C) void sigchldhandler(int) { 9136 int status; 9137 import w = core.sys.posix.sys.wait; 9138 w.wait(&status); 9139 } 9140 signal(SIGCHLD, &sigchldhandler); 9141 9142 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 9143 if(sock == -1) 9144 throw new Exception("socket " ~ to!string(errno)); 9145 9146 scope(failure) 9147 close(sock); 9148 9149 cloexec(sock); 9150 9151 // add-on server processes are assumed to be local, and thus will 9152 // use unix domain sockets. Besides, I want to pass sockets to them, 9153 // so it basically must be local (except for the session server, but meh). 9154 sockaddr_un addr; 9155 addr.sun_family = AF_UNIX; 9156 version(linux) { 9157 // on linux, we will use the abstract namespace 9158 addr.sun_path[0] = 0; 9159 addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[]; 9160 } else { 9161 // but otherwise, just use a file cuz we must. 9162 addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[]; 9163 } 9164 9165 if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) 9166 throw new Exception("bind " ~ to!string(errno)); 9167 9168 if(listen(sock, 128) == -1) 9169 throw new Exception("listen " ~ to!string(errno)); 9170 9171 makeNonBlocking(sock); 9172 9173 version(linux) { 9174 import core.sys.linux.epoll; 9175 auto epoll_fd = epoll_create1(EPOLL_CLOEXEC); 9176 if(epoll_fd == -1) 9177 throw new Exception("epoll_create1 " ~ to!string(errno)); 9178 scope(failure) 9179 close(epoll_fd); 9180 } else { 9181 import core.sys.posix.poll; 9182 } 9183 9184 version(linux) 9185 eis.epoll_fd = epoll_fd; 9186 9187 auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null); 9188 scope(exit) 9189 freeIoOp(acceptOp); 9190 9191 version(linux) { 9192 epoll_event ev; 9193 ev.events = EPOLLIN | EPOLLET; 9194 ev.data.ptr = acceptOp; 9195 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1) 9196 throw new Exception("epoll_ctl " ~ to!string(errno)); 9197 9198 epoll_event[64] events; 9199 } else { 9200 pollfd[] pollfds; 9201 IoOp*[int] ioops; 9202 pollfds ~= pollfd(sock, POLLIN); 9203 ioops[sock] = acceptOp; 9204 } 9205 9206 import core.time : MonoTime, seconds; 9207 9208 MonoTime timeout = MonoTime.currTime + 15.seconds; 9209 9210 while(true) { 9211 9212 // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently 9213 9214 int timeout_milliseconds = 0; // -1; // infinite 9215 9216 timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; 9217 if(timeout_milliseconds < 0) 9218 timeout_milliseconds = 0; 9219 9220 //writeln("waiting for ", name); 9221 9222 version(linux) { 9223 auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds); 9224 if(nfds == -1) { 9225 if(errno == EINTR) 9226 continue; 9227 throw new Exception("epoll_wait " ~ to!string(errno)); 9228 } 9229 } else { 9230 int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds); 9231 size_t lastIdx = 0; 9232 } 9233 9234 if(nfds == 0) { 9235 eis.wait_timeout(); 9236 timeout += 15.seconds; 9237 } 9238 9239 foreach(idx; 0 .. nfds) { 9240 version(linux) { 9241 auto flags = events[idx].events; 9242 auto ioop = cast(IoOp*) events[idx].data.ptr; 9243 } else { 9244 IoOp* ioop; 9245 foreach(tidx, thing; pollfds[lastIdx .. $]) { 9246 if(thing.revents) { 9247 ioop = ioops[thing.fd]; 9248 lastIdx += tidx + 1; 9249 break; 9250 } 9251 } 9252 } 9253 9254 //writeln(flags, " ", ioop.fd); 9255 9256 void newConnection() { 9257 // on edge triggering, it is important that we get it all 9258 while(true) { 9259 auto size = cast(socklen_t) addr.sizeof; 9260 auto ns = accept(sock, cast(sockaddr*) &addr, &size); 9261 if(ns == -1) { 9262 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9263 // all done, got it all 9264 break; 9265 } 9266 throw new Exception("accept " ~ to!string(errno)); 9267 } 9268 cloexec(ns); 9269 9270 makeNonBlocking(ns); 9271 auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData); 9272 niop.closeHandler = &eis.handleLocalConnectionClose; 9273 niop.completeHandler = &eis.handleLocalConnectionComplete; 9274 scope(failure) freeIoOp(niop); 9275 9276 version(linux) { 9277 epoll_event nev; 9278 nev.events = EPOLLIN | EPOLLET; 9279 nev.data.ptr = niop; 9280 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1) 9281 throw new Exception("epoll_ctl " ~ to!string(errno)); 9282 } else { 9283 bool found = false; 9284 foreach(ref pfd; pollfds) { 9285 if(pfd.fd < 0) { 9286 pfd.fd = ns; 9287 found = true; 9288 } 9289 } 9290 if(!found) 9291 pollfds ~= pollfd(ns, POLLIN); 9292 ioops[ns] = niop; 9293 } 9294 } 9295 } 9296 9297 bool newConnectionCondition() { 9298 version(linux) 9299 return ioop.fd == sock && (flags & EPOLLIN); 9300 else 9301 return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN); 9302 } 9303 9304 if(newConnectionCondition()) { 9305 newConnection(); 9306 } else if(ioop.operation == IoOp.ReadSocketHandle) { 9307 while(true) { 9308 int in_fd; 9309 auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd); 9310 if(got == -1) { 9311 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9312 // all done, got it all 9313 if(ioop.completeHandler) 9314 ioop.completeHandler(ioop); 9315 break; 9316 } 9317 throw new Exception("recv " ~ to!string(errno)); 9318 } 9319 9320 if(got == 0) { 9321 if(ioop.closeHandler) { 9322 ioop.closeHandler(ioop); 9323 version(linux) {} // nothing needed 9324 else { 9325 foreach(ref pfd; pollfds) { 9326 if(pfd.fd == ioop.fd) 9327 pfd.fd = -1; 9328 } 9329 } 9330 } 9331 close(ioop.fd); 9332 freeIoOp(ioop); 9333 break; 9334 } 9335 9336 ioop.bufferLengthUsed = cast(int) got; 9337 ioop.handler(ioop, in_fd); 9338 } 9339 } else if(ioop.operation == IoOp.Read) { 9340 while(true) { 9341 auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length); 9342 if(got == -1) { 9343 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9344 // all done, got it all 9345 if(ioop.completeHandler) 9346 ioop.completeHandler(ioop); 9347 break; 9348 } 9349 throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno)); 9350 } 9351 9352 if(got == 0) { 9353 if(ioop.closeHandler) 9354 ioop.closeHandler(ioop); 9355 close(ioop.fd); 9356 freeIoOp(ioop); 9357 break; 9358 } 9359 9360 ioop.bufferLengthUsed = cast(int) got; 9361 if(ioop.handler(ioop, ioop.fd)) { 9362 close(ioop.fd); 9363 freeIoOp(ioop); 9364 break; 9365 } 9366 } 9367 } 9368 9369 // EPOLLHUP? 9370 } 9371 } 9372 } else version(Windows) { 9373 9374 // set up a named pipe 9375 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx 9376 // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw 9377 // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid 9378 9379 } else static assert(0); 9380 } 9381 9382 9383 version(with_sendfd) 9384 // copied from the web and ported from C 9385 // see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t 9386 ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { 9387 msghdr msg; 9388 iovec[1] iov; 9389 9390 version(OSX) { 9391 //msg.msg_accrights = cast(cattr_t) &sendfd; 9392 //msg.msg_accrightslen = int.sizeof; 9393 } else version(Android) { 9394 } else { 9395 union ControlUnion { 9396 cmsghdr cm; 9397 char[CMSG_SPACE(int.sizeof)] control; 9398 } 9399 9400 ControlUnion control_un; 9401 cmsghdr* cmptr; 9402 9403 msg.msg_control = control_un.control.ptr; 9404 msg.msg_controllen = control_un.control.length; 9405 9406 cmptr = CMSG_FIRSTHDR(&msg); 9407 cmptr.cmsg_len = CMSG_LEN(int.sizeof); 9408 cmptr.cmsg_level = SOL_SOCKET; 9409 cmptr.cmsg_type = SCM_RIGHTS; 9410 *(cast(int *) CMSG_DATA(cmptr)) = sendfd; 9411 } 9412 9413 msg.msg_name = null; 9414 msg.msg_namelen = 0; 9415 9416 iov[0].iov_base = ptr; 9417 iov[0].iov_len = nbytes; 9418 msg.msg_iov = iov.ptr; 9419 msg.msg_iovlen = 1; 9420 9421 return sendmsg(fd, &msg, 0); 9422 } 9423 9424 version(with_sendfd) 9425 // copied from the web and ported from C 9426 ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { 9427 msghdr msg; 9428 iovec[1] iov; 9429 ssize_t n; 9430 int newfd; 9431 9432 version(OSX) { 9433 //msg.msg_accrights = cast(cattr_t) recvfd; 9434 //msg.msg_accrightslen = int.sizeof; 9435 } else version(Android) { 9436 } else { 9437 union ControlUnion { 9438 cmsghdr cm; 9439 char[CMSG_SPACE(int.sizeof)] control; 9440 } 9441 ControlUnion control_un; 9442 cmsghdr* cmptr; 9443 9444 msg.msg_control = control_un.control.ptr; 9445 msg.msg_controllen = control_un.control.length; 9446 } 9447 9448 msg.msg_name = null; 9449 msg.msg_namelen = 0; 9450 9451 iov[0].iov_base = ptr; 9452 iov[0].iov_len = nbytes; 9453 msg.msg_iov = iov.ptr; 9454 msg.msg_iovlen = 1; 9455 9456 if ( (n = recvmsg(fd, &msg, 0)) <= 0) 9457 return n; 9458 9459 version(OSX) { 9460 //if(msg.msg_accrightslen != int.sizeof) 9461 //*recvfd = -1; 9462 } else version(Android) { 9463 } else { 9464 if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && 9465 cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { 9466 if (cmptr.cmsg_level != SOL_SOCKET) 9467 throw new Exception("control level != SOL_SOCKET"); 9468 if (cmptr.cmsg_type != SCM_RIGHTS) 9469 throw new Exception("control type != SCM_RIGHTS"); 9470 *recvfd = *(cast(int *) CMSG_DATA(cmptr)); 9471 } else 9472 *recvfd = -1; /* descriptor was not passed */ 9473 } 9474 9475 return n; 9476 } 9477 /* end read_fd */ 9478 9479 9480 /* 9481 Event source stuff 9482 9483 The api is: 9484 9485 sendEvent(string url, string type, string data, int timeout = 60*10); 9486 9487 attachEventListener(string url, int fd, lastId) 9488 9489 9490 It just sends to all attached listeners, and stores it until the timeout 9491 for replaying via lastEventId. 9492 */ 9493 9494 /* 9495 Session process stuff 9496 9497 it stores it all. the cgi object has a session object that can grab it 9498 9499 session may be done in the same process if possible, there is a version 9500 switch to choose if you want to override. 9501 */ 9502 9503 struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; 9504 alias handler = dispatchHandler; 9505 string urlPrefix; 9506 bool rejectFurther; 9507 immutable(DispatcherDetails) details; 9508 } 9509 9510 private string urlify(string name) pure { 9511 return beautify(name, '-', true); 9512 } 9513 9514 private string beautify(string name, char space = ' ', bool allLowerCase = false) pure { 9515 if(name == "id") 9516 return allLowerCase ? name : "ID"; 9517 9518 char[160] buffer; 9519 int bufferIndex = 0; 9520 bool shouldCap = true; 9521 bool shouldSpace; 9522 bool lastWasCap; 9523 foreach(idx, char ch; name) { 9524 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9525 9526 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 9527 if(lastWasCap) { 9528 // two caps in a row, don't change. Prolly acronym. 9529 } else { 9530 if(idx) 9531 shouldSpace = true; // new word, add space 9532 } 9533 9534 lastWasCap = true; 9535 } else { 9536 lastWasCap = false; 9537 } 9538 9539 if(shouldSpace) { 9540 buffer[bufferIndex++] = space; 9541 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9542 shouldSpace = false; 9543 } 9544 if(shouldCap) { 9545 if(ch >= 'a' && ch <= 'z') 9546 ch -= 32; 9547 shouldCap = false; 9548 } 9549 if(allLowerCase && ch >= 'A' && ch <= 'Z') 9550 ch += 32; 9551 buffer[bufferIndex++] = ch; 9552 } 9553 return buffer[0 .. bufferIndex].idup; 9554 } 9555 9556 /* 9557 string urlFor(alias func)() { 9558 return __traits(identifier, func); 9559 } 9560 */ 9561 9562 /++ 9563 UDA: The name displayed to the user in auto-generated HTML. 9564 9565 Default is `beautify(identifier)`. 9566 +/ 9567 struct DisplayName { 9568 string name; 9569 } 9570 9571 /++ 9572 UDA: The name used in the URL or web parameter. 9573 9574 Default is `urlify(identifier)` for functions and `identifier` for parameters and data members. 9575 +/ 9576 struct UrlName { 9577 string name; 9578 } 9579 9580 /++ 9581 UDA: default format to respond for this method 9582 +/ 9583 struct DefaultFormat { string value; } 9584 9585 class MissingArgumentException : Exception { 9586 string functionName; 9587 string argumentName; 9588 string argumentType; 9589 9590 this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 9591 this.functionName = functionName; 9592 this.argumentName = argumentName; 9593 this.argumentType = argumentType; 9594 9595 super("Missing Argument: " ~ this.argumentName, file, line, next); 9596 } 9597 } 9598 9599 /++ 9600 You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. 9601 9602 History: 9603 Added December 15, 2021 (dub v10.5) 9604 +/ 9605 class ResourceNotFoundException : Exception { 9606 string resourceType; 9607 string resourceId; 9608 9609 this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 9610 this.resourceType = resourceType; 9611 this.resourceId = resourceId; 9612 9613 super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); 9614 } 9615 9616 } 9617 9618 /++ 9619 This can be attached to any constructor or function called from the cgi system. 9620 9621 If it is present, the function argument can NOT be set from web params, but instead 9622 is set to the return value of the given `func`. 9623 9624 If `func` can take a parameter of type [Cgi], it will be passed the one representing 9625 the current request. Otherwise, it must take zero arguments. 9626 9627 Any params in your function of type `Cgi` are automatically assumed to take the cgi object 9628 for the connection. Any of type [Session] (with an argument) is also assumed to come from 9629 the cgi object. 9630 9631 const arguments are also supported. 9632 +/ 9633 struct ifCalledFromWeb(alias func) {} 9634 9635 // it only looks at query params for GET requests, the rest must be in the body for a function argument. 9636 auto callFromCgi(alias method, T)(T dg, Cgi cgi) { 9637 9638 // FIXME: any array of structs should also be settable or gettable from csv as well. 9639 9640 // FIXME: think more about checkboxes and bools. 9641 9642 import std.traits; 9643 9644 Parameters!method params; 9645 alias idents = ParameterIdentifierTuple!method; 9646 alias defaults = ParameterDefaults!method; 9647 9648 const(string)[] names; 9649 const(string)[] values; 9650 9651 // first, check for missing arguments and initialize to defaults if necessary 9652 9653 static if(is(typeof(method) P == __parameters)) 9654 foreach(idx, param; P) {{ 9655 // see: mustNotBeSetFromWebParams 9656 static if(is(param : Cgi)) { 9657 static assert(!is(param == immutable)); 9658 cast() params[idx] = cgi; 9659 } else static if(is(param == Session!D, D)) { 9660 static assert(!is(param == immutable)); 9661 cast() params[idx] = cgi.getSessionObject!D(); 9662 } else { 9663 bool populated; 9664 foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) { 9665 static if(is(uda == ifCalledFromWeb!func, alias func)) { 9666 static if(is(typeof(func(cgi)))) 9667 params[idx] = func(cgi); 9668 else 9669 params[idx] = func(); 9670 9671 populated = true; 9672 } 9673 } 9674 9675 if(!populated) { 9676 static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) { 9677 params[idx] = param.getAutomaticallyForCgi(cgi); 9678 populated = true; 9679 } 9680 } 9681 9682 if(!populated) { 9683 auto ident = idents[idx]; 9684 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 9685 if(ident !in cgi.get) { 9686 static if(is(defaults[idx] == void)) { 9687 static if(is(param == bool)) 9688 params[idx] = false; 9689 else 9690 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 9691 } else 9692 params[idx] = defaults[idx]; 9693 } 9694 } else { 9695 if(ident !in cgi.post) { 9696 static if(is(defaults[idx] == void)) { 9697 static if(is(param == bool)) 9698 params[idx] = false; 9699 else 9700 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 9701 } else 9702 params[idx] = defaults[idx]; 9703 } 9704 } 9705 } 9706 } 9707 }} 9708 9709 // second, parse the arguments in order to build up arrays, etc. 9710 9711 static bool setVariable(T)(string name, string paramName, T* what, string value) { 9712 static if(is(T == struct)) { 9713 if(name == paramName) { 9714 *what = T.init; 9715 return true; 9716 } else { 9717 // could be a child. gonna allow either obj.field OR obj[field] 9718 9719 string afterName; 9720 9721 if(name[paramName.length] == '[') { 9722 int count = 1; 9723 auto idx = paramName.length + 1; 9724 while(idx < name.length && count > 0) { 9725 if(name[idx] == '[') 9726 count++; 9727 else if(name[idx] == ']') { 9728 count--; 9729 if(count == 0) break; 9730 } 9731 idx++; 9732 } 9733 9734 if(idx == name.length) 9735 return false; // malformed 9736 9737 auto insideBrackets = name[paramName.length + 1 .. idx]; 9738 afterName = name[idx + 1 .. $]; 9739 9740 name = name[0 .. paramName.length]; 9741 9742 paramName = insideBrackets; 9743 9744 } else if(name[paramName.length] == '.') { 9745 paramName = name[paramName.length + 1 .. $]; 9746 name = paramName; 9747 int p = 0; 9748 foreach(ch; paramName) { 9749 if(ch == '.' || ch == '[') 9750 break; 9751 p++; 9752 } 9753 9754 afterName = paramName[p .. $]; 9755 paramName = paramName[0 .. p]; 9756 } else { 9757 return false; 9758 } 9759 9760 if(paramName.length) 9761 // set the child member 9762 switch(paramName) { 9763 foreach(idx, memberName; __traits(allMembers, T)) 9764 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 9765 // data member! 9766 case memberName: 9767 return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value); 9768 } 9769 default: 9770 // ok, not a member 9771 } 9772 } 9773 9774 return false; 9775 } else static if(is(T == enum)) { 9776 *what = to!T(value); 9777 return true; 9778 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 9779 *what = to!T(value); 9780 return true; 9781 } else static if(is(T == bool)) { 9782 *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on"; 9783 return true; 9784 } else static if(is(T == K[], K)) { 9785 K tmp; 9786 if(name == paramName) { 9787 // direct - set and append 9788 if(setVariable(name, paramName, &tmp, value)) { 9789 (*what) ~= tmp; 9790 return true; 9791 } else { 9792 return false; 9793 } 9794 } else { 9795 // child, append to last element 9796 // FIXME: what about range violations??? 9797 auto ptr = &(*what)[(*what).length - 1]; 9798 return setVariable(name, paramName, ptr, value); 9799 9800 } 9801 } else static if(is(T == V[K], K, V)) { 9802 // assoc array, name[key] is valid 9803 if(name == paramName) { 9804 // no action necessary 9805 return true; 9806 } else if(name[paramName.length] == '[') { 9807 int count = 1; 9808 auto idx = paramName.length + 1; 9809 while(idx < name.length && count > 0) { 9810 if(name[idx] == '[') 9811 count++; 9812 else if(name[idx] == ']') { 9813 count--; 9814 if(count == 0) break; 9815 } 9816 idx++; 9817 } 9818 if(idx == name.length) 9819 return false; // malformed 9820 9821 auto insideBrackets = name[paramName.length + 1 .. idx]; 9822 auto afterName = name[idx + 1 .. $]; 9823 9824 auto k = to!K(insideBrackets); 9825 V v; 9826 if(auto ptr = k in *what) 9827 v = *ptr; 9828 9829 name = name[0 .. paramName.length]; 9830 //writeln(name, afterName, " ", paramName); 9831 9832 auto ret = setVariable(name ~ afterName, paramName, &v, value); 9833 if(ret) { 9834 (*what)[k] = v; 9835 return true; 9836 } 9837 } 9838 9839 return false; 9840 } else { 9841 static assert(0, "unsupported type for cgi call " ~ T.stringof); 9842 } 9843 9844 //return false; 9845 } 9846 9847 void setArgument(string name, string value) { 9848 int p; 9849 foreach(ch; name) { 9850 if(ch == '.' || ch == '[') 9851 break; 9852 p++; 9853 } 9854 9855 auto paramName = name[0 .. p]; 9856 9857 sw: switch(paramName) { 9858 static if(is(typeof(method) P == __parameters)) 9859 foreach(idx, param; P) { 9860 static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) { 9861 // cannot be set from the outside 9862 } else { 9863 case idents[idx]: 9864 static if(is(param == Cgi.UploadedFile)) { 9865 params[idx] = cgi.files[name]; 9866 } else static if(is(param : const Cgi.UploadedFile[])) { 9867 (cast() params[idx]) = cgi.filesArray[name]; 9868 } else { 9869 setVariable(name, paramName, ¶ms[idx], value); 9870 } 9871 break sw; 9872 } 9873 } 9874 default: 9875 // ignore; not relevant argument 9876 } 9877 } 9878 9879 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 9880 names = cgi.allGetNamesInOrder; 9881 values = cgi.allGetValuesInOrder; 9882 } else { 9883 names = cgi.allPostNamesInOrder; 9884 values = cgi.allPostValuesInOrder; 9885 } 9886 9887 foreach(idx, name; names) { 9888 setArgument(name, values[idx]); 9889 } 9890 9891 static if(is(ReturnType!method == void)) { 9892 typeof(null) ret; 9893 dg(params); 9894 } else { 9895 auto ret = dg(params); 9896 } 9897 9898 // FIXME: format return values 9899 // options are: json, html, csv. 9900 // also may need to wrap in envelope format: none, html, or json. 9901 return ret; 9902 } 9903 9904 private bool mustNotBeSetFromWebParams(T, attrs...)() { 9905 static if(is(T : const(Cgi))) { 9906 return true; 9907 } else static if(is(T : const(Session!D), D)) { 9908 return true; 9909 } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) { 9910 return true; 9911 } else { 9912 foreach(uda; attrs) 9913 static if(is(uda == ifCalledFromWeb!func, alias func)) 9914 return true; 9915 return false; 9916 } 9917 } 9918 9919 private bool hasIfCalledFromWeb(attrs...)() { 9920 foreach(uda; attrs) 9921 static if(is(uda == ifCalledFromWeb!func, alias func)) 9922 return true; 9923 return false; 9924 } 9925 9926 /++ 9927 Implies POST path for the thing itself, then GET will get the automatic form. 9928 9929 The given customizer, if present, will be called as a filter on the Form object. 9930 9931 History: 9932 Added December 27, 2020 9933 +/ 9934 template AutomaticForm(alias customizer) { } 9935 9936 /++ 9937 This is meant to be returned by a function that takes a form POST submission. You 9938 want to set the url of the new resource it created, which is set as the http 9939 Location header for a "201 Created" result, and you can also set a separate 9940 destination for browser users, which it sets via a "Refresh" header. 9941 9942 The `resourceRepresentation` should generally be the thing you just created, and 9943 it will be the body of the http response when formatted through the presenter. 9944 The exact thing is up to you - it could just return an id, or the whole object, or 9945 perhaps a partial object. 9946 9947 Examples: 9948 --- 9949 class Test : WebObject { 9950 @(Cgi.RequestMethod.POST) 9951 CreatedResource!int makeThing(string value) { 9952 return CreatedResource!int(value.to!int, "/resources/id"); 9953 } 9954 } 9955 --- 9956 9957 History: 9958 Added December 18, 2021 9959 +/ 9960 struct CreatedResource(T) { 9961 static if(!is(T == void)) 9962 T resourceRepresentation; 9963 string resourceUrl; 9964 string refreshUrl; 9965 } 9966 9967 /+ 9968 /++ 9969 This can be attached as a UDA to a handler to add a http Refresh header on a 9970 successful run. (It will not be attached if the function throws an exception.) 9971 This will refresh the browser the given number of seconds after the page loads, 9972 to the url returned by `urlFunc`, which can be either a static function or a 9973 member method of the current handler object. 9974 9975 You might use this for a POST handler that is normally used from ajax, but you 9976 want it to degrade gracefully to a temporarily flashed message before reloading 9977 the main page. 9978 9979 History: 9980 Added December 18, 2021 9981 +/ 9982 struct Refresh(alias urlFunc) { 9983 int waitInSeconds; 9984 9985 string url() { 9986 static if(__traits(isStaticFunction, urlFunc)) 9987 return urlFunc(); 9988 else static if(is(urlFunc : string)) 9989 return urlFunc; 9990 } 9991 } 9992 +/ 9993 9994 /+ 9995 /++ 9996 Sets a filter to be run before 9997 9998 A before function can do validations of params and log and stop the function from running. 9999 +/ 10000 template Before(alias b) {} 10001 template After(alias b) {} 10002 +/ 10003 10004 /+ 10005 Argument conversions: for the most part, it is to!Thing(string). 10006 10007 But arrays and structs are a bit different. Arrays come from the cgi array. Thus 10008 they are passed 10009 10010 arr=foo&arr=bar <-- notice the same name. 10011 10012 Structs are first declared with an empty thing, then have their members set individually, 10013 with dot notation. The members are not required, just the initial declaration. 10014 10015 struct Foo { 10016 int a; 10017 string b; 10018 } 10019 void test(Foo foo){} 10020 10021 foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members 10022 10023 Arrays of structs use this declaration. 10024 10025 void test(Foo[] foo) {} 10026 10027 foo&foo.a=5&foo.b=bar&foo&foo.a=9 10028 10029 You can use a hidden input field in HTML forms to achieve this. The value of the naked name 10030 declaration is ignored. 10031 10032 Mind that order matters! The declaration MUST come first in the string. 10033 10034 Arrays of struct members follow this rule recursively. 10035 10036 struct Foo { 10037 int[] a; 10038 } 10039 10040 foo&foo.a=1&foo.a=2&foo&foo.a=1 10041 10042 10043 Associative arrays are formatted with brackets, after a declaration, like structs: 10044 10045 foo&foo[key]=value&foo[other_key]=value 10046 10047 10048 Note: for maximum compatibility with outside code, keep your types simple. Some libraries 10049 do not support the strict ordering requirements to work with these struct protocols. 10050 10051 FIXME: also perhaps accept application/json to better work with outside trash. 10052 10053 10054 Return values are also auto-formatted according to user-requested type: 10055 for json, it loops over and converts. 10056 for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables! 10057 +/ 10058 10059 /++ 10060 A web presenter is responsible for rendering things to HTML to be usable 10061 in a web browser. 10062 10063 They are passed as template arguments to the base classes of [WebObject] 10064 10065 Responsible for displaying stuff as HTML. You can put this into your own aggregate 10066 and override it. Use forwarding and specialization to customize it. 10067 10068 When you inherit from it, pass your own class as the CRTP argument. This lets the base 10069 class templates and your overridden templates work with each other. 10070 10071 --- 10072 class MyPresenter : WebPresenter!(MyPresenter) { 10073 @Override 10074 void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) { 10075 // present the CustomType 10076 } 10077 @Override 10078 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 10079 // handle everything else via the super class, which will call 10080 // back to your class when appropriate 10081 super.presentSuccessfulReturnAsHtml(cgi, ret); 10082 } 10083 } 10084 --- 10085 10086 The meta argument in there can be overridden by your own facility. 10087 10088 +/ 10089 class WebPresenter(CRTP) { 10090 10091 /// A UDA version of the built-in `override`, to be used for static template polymorphism 10092 /// If you override a plain method, use `override`. If a template, use `@Override`. 10093 enum Override; 10094 10095 string script() { 10096 return ` 10097 `; 10098 } 10099 10100 string style() { 10101 return ` 10102 :root { 10103 --mild-border: #ccc; 10104 --middle-border: #999; 10105 --accent-color: #f2f2f2; 10106 --sidebar-color: #fefefe; 10107 } 10108 ` ~ genericFormStyling() ~ genericSiteStyling(); 10109 } 10110 10111 string genericFormStyling() { 10112 return 10113 q"css 10114 table.automatic-data-display { 10115 border-collapse: collapse; 10116 border: solid 1px var(--mild-border); 10117 } 10118 10119 table.automatic-data-display td { 10120 vertical-align: top; 10121 border: solid 1px var(--mild-border); 10122 padding: 2px 4px; 10123 } 10124 10125 table.automatic-data-display th { 10126 border: solid 1px var(--mild-border); 10127 border-bottom: solid 1px var(--middle-border); 10128 padding: 2px 4px; 10129 } 10130 10131 ol.automatic-data-display { 10132 margin: 0px; 10133 /* 10134 list-style-position: inside; 10135 padding: 0px; 10136 */ 10137 } 10138 10139 dl.automatic-data-display { 10140 10141 } 10142 10143 .automatic-form { 10144 max-width: 600px; 10145 } 10146 10147 .form-field { 10148 margin: 0.5em; 10149 padding-left: 0.5em; 10150 } 10151 10152 .label-text { 10153 display: block; 10154 font-weight: bold; 10155 margin-left: -0.5em; 10156 } 10157 10158 .submit-button-holder { 10159 padding-left: 2em; 10160 } 10161 10162 .add-array-button { 10163 10164 } 10165 css"; 10166 } 10167 10168 string genericSiteStyling() { 10169 return 10170 q"css 10171 * { box-sizing: border-box; } 10172 html, body { margin: 0px; } 10173 body { 10174 font-family: sans-serif; 10175 } 10176 header { 10177 background: var(--accent-color); 10178 height: 64px; 10179 } 10180 footer { 10181 background: var(--accent-color); 10182 height: 64px; 10183 } 10184 #site-container { 10185 display: flex; 10186 flex-wrap: wrap; 10187 } 10188 main { 10189 flex: 1 1 auto; 10190 order: 2; 10191 min-height: calc(100vh - 64px - 64px); 10192 min-width: 80ch; 10193 padding: 4px; 10194 padding-left: 1em; 10195 } 10196 #sidebar { 10197 flex: 0 0 16em; 10198 order: 1; 10199 background: var(--sidebar-color); 10200 } 10201 css"; 10202 } 10203 10204 import arsd.dom; 10205 Element htmlContainer() { 10206 auto document = new Document(q"html 10207 <!DOCTYPE html> 10208 <html class="no-script"> 10209 <head> 10210 <script>document.documentElement.classList.remove("no-script");</script> 10211 <style>.no-script requires-script { display: none; }</style> 10212 <title>D Application</title> 10213 <meta name="viewport" content="initial-scale=1, width=device-width" /> 10214 <link rel="stylesheet" href="style.css" /> 10215 </head> 10216 <body> 10217 <header></header> 10218 <div id="site-container"> 10219 <main></main> 10220 <div id="sidebar"></div> 10221 </div> 10222 <footer></footer> 10223 <script src="script.js"></script> 10224 </body> 10225 </html> 10226 html", true, true); 10227 10228 return document.requireSelector("main"); 10229 } 10230 10231 /// Renders a response as an HTTP error with associated html body 10232 void renderBasicError(Cgi cgi, int httpErrorCode) { 10233 cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); 10234 auto c = htmlContainer(); 10235 c.innerText = getHttpCodeText(httpErrorCode); 10236 cgi.setResponseContentType("text/html; charset=utf-8"); 10237 cgi.write(c.parentDocument.toString(), true); 10238 } 10239 10240 template methodMeta(alias method) { 10241 enum methodMeta = null; 10242 } 10243 10244 void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10245 switch(format) { 10246 case "html": 10247 (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta); 10248 break; 10249 case "json": 10250 import arsd.jsvar; 10251 static if(is(typeof(ret) == MultipleResponses!Types, Types...)) { 10252 var json; 10253 foreach(index, type; Types) { 10254 if(ret.contains == index) 10255 json = ret.payload[index]; 10256 } 10257 } else { 10258 var json = ret; 10259 } 10260 var envelope = json; // var.emptyObject; 10261 /* 10262 envelope.success = true; 10263 envelope.result = json; 10264 envelope.error = null; 10265 */ 10266 cgi.setResponseContentType("application/json"); 10267 cgi.write(envelope.toJson(), true); 10268 break; 10269 default: 10270 cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of. 10271 } 10272 } 10273 10274 /// typeof(null) (which is also used to represent functions returning `void`) do nothing 10275 /// in the default presenter - allowing the function to have full low-level control over the 10276 /// response. 10277 void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { 10278 // nothing intentionally! 10279 } 10280 10281 /// Redirections are forwarded to [Cgi.setResponseLocation] 10282 void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10283 cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); 10284 } 10285 10286 /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. 10287 void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { 10288 cgi.setResponseStatus(getHttpCodeText(201)); 10289 if(ret.resourceUrl.length) 10290 cgi.header("Location: " ~ ret.resourceUrl); 10291 if(ret.refreshUrl.length) 10292 cgi.header("Refresh: 0;" ~ ret.refreshUrl); 10293 static if(!is(R == void)) 10294 presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); 10295 } 10296 10297 /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime 10298 void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { 10299 bool outputted = false; 10300 foreach(index, type; Types) { 10301 if(ret.contains == index) { 10302 assert(!outputted); 10303 outputted = true; 10304 (cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format); 10305 } 10306 } 10307 if(!outputted) 10308 assert(0); 10309 } 10310 10311 /++ 10312 An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort if the filename member is non-null of the FileResource interface. 10313 +/ 10314 void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10315 cgi.setCache(true); // not necessarily true but meh 10316 if(auto fn = ret.filename()) { 10317 cgi.header("Content-Disposition: attachment; filename="~fn~";"); 10318 } 10319 cgi.setResponseContentType(ret.contentType); 10320 cgi.write(ret.getData(), true); 10321 } 10322 10323 /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer]. 10324 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 10325 auto container = this.htmlContainer(); 10326 container.appendChild(formatReturnValueAsHtml(ret)); 10327 cgi.write(container.parentDocument.toString(), true); 10328 } 10329 10330 /++ 10331 10332 History: 10333 Added January 23, 2023 (dub v11.0) 10334 +/ 10335 void presentExceptionalReturn(Meta)(Cgi cgi, Throwable t, Meta meta, string format) { 10336 switch(format) { 10337 case "html": 10338 presentExceptionAsHtml(cgi, t, meta); 10339 break; 10340 case "json": 10341 presentExceptionAsJsonImpl(cgi, t); 10342 break; 10343 default: 10344 } 10345 } 10346 10347 private void presentExceptionAsJsonImpl()(Cgi cgi, Throwable t) { 10348 cgi.setResponseStatus("500 Internal Server Error"); 10349 cgi.setResponseContentType("application/json"); 10350 import arsd.jsvar; 10351 var v = var.emptyObject; 10352 v.type = typeid(t).toString; 10353 v.msg = t.msg; 10354 v.fullString = t.toString(); 10355 cgi.write(v.toJson(), true); 10356 } 10357 10358 10359 /++ 10360 If you override this, you will need to cast the exception type `t` dynamically, 10361 but can then use the template arguments here to refer back to the function. 10362 10363 `func` is an alias to the method itself, and `dg` is a callable delegate to the same 10364 method on the live object. You could, in theory, change arguments and retry, but I 10365 provide that information mostly with the expectation that you will use them to make 10366 useful forms or richer error messages for the user. 10367 10368 History: 10369 BREAKING CHANGE on January 23, 2023 (v11.0 ): it previously took an `alias func` and `T dg` to call the function again. 10370 I removed this in favor of a `Meta` param. 10371 10372 Before: `void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg)` 10373 10374 After: `void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta)` 10375 10376 If you used the func for something, move that something into your `methodMeta` template. 10377 10378 What is the benefit of this change? Somewhat smaller executables and faster builds thanks to more reused functions, together with 10379 enabling an easier implementation of [presentExceptionalReturn]. 10380 +/ 10381 void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta) { 10382 Form af; 10383 /+ 10384 foreach(attr; __traits(getAttributes, func)) { 10385 static if(__traits(isSame, attr, AutomaticForm)) { 10386 af = createAutomaticFormForFunction!(func)(dg); 10387 } 10388 } 10389 +/ 10390 presentExceptionAsHtmlImpl(cgi, t, af); 10391 } 10392 10393 void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { 10394 if(auto e = cast(ResourceNotFoundException) t) { 10395 auto container = this.htmlContainer(); 10396 10397 container.addChild("p", e.msg); 10398 10399 if(!cgi.outputtedResponseData) 10400 cgi.setResponseStatus("404 Not Found"); 10401 cgi.write(container.parentDocument.toString(), true); 10402 } else if(auto mae = cast(MissingArgumentException) t) { 10403 if(automaticForm is null) 10404 goto generic; 10405 auto container = this.htmlContainer(); 10406 if(cgi.requestMethod == Cgi.RequestMethod.POST) 10407 container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); 10408 container.appendChild(automaticForm); 10409 10410 cgi.write(container.parentDocument.toString(), true); 10411 } else { 10412 generic: 10413 auto container = this.htmlContainer(); 10414 10415 // import std.stdio; writeln(t.toString()); 10416 10417 container.appendChild(exceptionToElement(t)); 10418 10419 container.addChild("h4", "GET"); 10420 foreach(k, v; cgi.get) { 10421 auto deets = container.addChild("details"); 10422 deets.addChild("summary", k); 10423 deets.addChild("div", v); 10424 } 10425 10426 container.addChild("h4", "POST"); 10427 foreach(k, v; cgi.post) { 10428 auto deets = container.addChild("details"); 10429 deets.addChild("summary", k); 10430 deets.addChild("div", v); 10431 } 10432 10433 10434 if(!cgi.outputtedResponseData) 10435 cgi.setResponseStatus("500 Internal Server Error"); 10436 cgi.write(container.parentDocument.toString(), true); 10437 } 10438 } 10439 10440 Element exceptionToElement(Throwable t) { 10441 auto div = Element.make("div"); 10442 div.addClass("exception-display"); 10443 10444 div.addChild("p", t.msg); 10445 div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line)); 10446 10447 auto pre = div.addChild("pre"); 10448 string s; 10449 s = t.toString(); 10450 Element currentBox; 10451 bool on = false; 10452 foreach(line; s.splitLines) { 10453 if(!on && line.startsWith("-----")) 10454 on = true; 10455 if(!on) continue; 10456 if(line.indexOf("arsd/") != -1) { 10457 if(currentBox is null) { 10458 currentBox = pre.addChild("details"); 10459 currentBox.addChild("summary", "Framework code"); 10460 } 10461 currentBox.addChild("span", line ~ "\n"); 10462 } else { 10463 pre.addChild("span", line ~ "\n"); 10464 currentBox = null; 10465 } 10466 } 10467 10468 return div; 10469 } 10470 10471 /++ 10472 Returns an element for a particular type 10473 +/ 10474 Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { 10475 import std.traits; 10476 10477 auto div = Element.make("div"); 10478 div.addClass("form-field"); 10479 10480 static if(is(T : const Cgi.UploadedFile)) { 10481 Element lbl; 10482 if(displayName !is null) { 10483 lbl = div.addChild("label"); 10484 lbl.addChild("span", displayName, "label-text"); 10485 lbl.appendText(" "); 10486 } else { 10487 lbl = div; 10488 } 10489 auto i = lbl.addChild("input", name); 10490 i.attrs.name = name; 10491 i.attrs.type = "file"; 10492 i.attrs.multiple = "multiple"; 10493 } else static if(is(T == Cgi.UploadedFile)) { 10494 Element lbl; 10495 if(displayName !is null) { 10496 lbl = div.addChild("label"); 10497 lbl.addChild("span", displayName, "label-text"); 10498 lbl.appendText(" "); 10499 } else { 10500 lbl = div; 10501 } 10502 auto i = lbl.addChild("input", name); 10503 i.attrs.name = name; 10504 i.attrs.type = "file"; 10505 } else static if(is(T == enum)) { 10506 Element lbl; 10507 if(displayName !is null) { 10508 lbl = div.addChild("label"); 10509 lbl.addChild("span", displayName, "label-text"); 10510 lbl.appendText(" "); 10511 } else { 10512 lbl = div; 10513 } 10514 auto i = lbl.addChild("select", name); 10515 i.attrs.name = name; 10516 10517 foreach(memberName; __traits(allMembers, T)) 10518 i.addChild("option", memberName); 10519 10520 } else static if(is(T == struct)) { 10521 if(displayName !is null) 10522 div.addChild("span", displayName, "label-text"); 10523 auto fieldset = div.addChild("fieldset"); 10524 fieldset.addChild("legend", beautify(T.stringof)); // FIXME 10525 fieldset.addChild("input", name); 10526 foreach(idx, memberName; __traits(allMembers, T)) 10527 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 10528 fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); 10529 } 10530 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 10531 Element lbl; 10532 if(displayName !is null) { 10533 lbl = div.addChild("label"); 10534 lbl.addChild("span", displayName, "label-text"); 10535 lbl.appendText(" "); 10536 } else { 10537 lbl = div; 10538 } 10539 Element i; 10540 if(udaSuggestion) { 10541 i = udaSuggestion(); 10542 lbl.appendChild(i); 10543 } else { 10544 i = lbl.addChild("input", name); 10545 } 10546 i.attrs.name = name; 10547 static if(isSomeString!T) 10548 i.attrs.type = "text"; 10549 else 10550 i.attrs.type = "number"; 10551 if(i.tagName == "textarea") 10552 i.textContent = to!string(T.init); 10553 else 10554 i.attrs.value = to!string(T.init); 10555 } else static if(is(T == bool)) { 10556 Element lbl; 10557 if(displayName !is null) { 10558 lbl = div.addChild("label"); 10559 lbl.addChild("span", displayName, "label-text"); 10560 lbl.appendText(" "); 10561 } else { 10562 lbl = div; 10563 } 10564 auto i = lbl.addChild("input", name); 10565 i.attrs.type = "checkbox"; 10566 i.attrs.value = "true"; 10567 i.attrs.name = name; 10568 } else static if(is(T == K[], K)) { 10569 auto templ = div.addChild("template"); 10570 templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); 10571 if(displayName !is null) 10572 div.addChild("span", displayName, "label-text"); 10573 auto btn = div.addChild("button"); 10574 btn.addClass("add-array-button"); 10575 btn.attrs.type = "button"; 10576 btn.innerText = "Add"; 10577 btn.attrs.onclick = q{ 10578 var a = document.importNode(this.parentNode.firstChild.content, true); 10579 this.parentNode.insertBefore(a, this); 10580 }; 10581 } else static if(is(T == V[K], K, V)) { 10582 div.innerText = "assoc array not implemented for automatic form at this time"; 10583 } else { 10584 static assert(0, "unsupported type for cgi call " ~ T.stringof); 10585 } 10586 10587 10588 return div; 10589 } 10590 10591 /// creates a form for gathering the function's arguments 10592 Form createAutomaticFormForFunction(alias method, T)(T dg) { 10593 10594 auto form = cast(Form) Element.make("form"); 10595 10596 form.method = "POST"; // FIXME 10597 10598 form.addClass("automatic-form"); 10599 10600 string formDisplayName = beautify(__traits(identifier, method)); 10601 foreach(attr; __traits(getAttributes, method)) 10602 static if(is(typeof(attr) == DisplayName)) 10603 formDisplayName = attr.name; 10604 form.addChild("h3", formDisplayName); 10605 10606 import std.traits; 10607 10608 //Parameters!method params; 10609 //alias idents = ParameterIdentifierTuple!method; 10610 //alias defaults = ParameterDefaults!method; 10611 10612 static if(is(typeof(method) P == __parameters)) 10613 foreach(idx, _; P) {{ 10614 10615 alias param = P[idx .. idx + 1]; 10616 10617 static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { 10618 string displayName = beautify(__traits(identifier, param)); 10619 Element function() element; 10620 foreach(attr; __traits(getAttributes, param)) { 10621 static if(is(typeof(attr) == DisplayName)) 10622 displayName = attr.name; 10623 else static if(is(typeof(attr) : typeof(element))) { 10624 element = attr; 10625 } 10626 } 10627 auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); 10628 if(i.querySelector("input[type=file]") !is null) 10629 form.setAttribute("enctype", "multipart/form-data"); 10630 } 10631 }} 10632 10633 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 10634 10635 return form; 10636 } 10637 10638 /// creates a form for gathering object members (for the REST object thing right now) 10639 Form createAutomaticFormForObject(T)(T obj) { 10640 auto form = cast(Form) Element.make("form"); 10641 10642 form.addClass("automatic-form"); 10643 10644 form.addChild("h3", beautify(__traits(identifier, T))); 10645 10646 import std.traits; 10647 10648 //Parameters!method params; 10649 //alias idents = ParameterIdentifierTuple!method; 10650 //alias defaults = ParameterDefaults!method; 10651 10652 foreach(idx, memberName; __traits(derivedMembers, T)) {{ 10653 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 10654 string displayName = beautify(memberName); 10655 Element function() element; 10656 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) 10657 static if(is(typeof(attr) == DisplayName)) 10658 displayName = attr.name; 10659 else static if(is(typeof(attr) : typeof(element))) 10660 element = attr; 10661 form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); 10662 10663 form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); 10664 }}} 10665 10666 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 10667 10668 return form; 10669 } 10670 10671 /// 10672 Element formatReturnValueAsHtml(T)(T t) { 10673 import std.traits; 10674 10675 static if(is(T == typeof(null))) { 10676 return Element.make("span"); 10677 } else static if(is(T : Element)) { 10678 return t; 10679 } else static if(is(T == MultipleResponses!Types, Types...)) { 10680 foreach(index, type; Types) { 10681 if(t.contains == index) 10682 return formatReturnValueAsHtml(t.payload[index]); 10683 } 10684 assert(0); 10685 } else static if(is(T == Paginated!E, E)) { 10686 auto e = Element.make("div").addClass("paginated-result"); 10687 e.appendChild(formatReturnValueAsHtml(t.items)); 10688 if(t.nextPageUrl.length) 10689 e.appendChild(Element.make("a", "Next Page", t.nextPageUrl)); 10690 return e; 10691 } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) { 10692 return Element.make("span", to!string(t), "automatic-data-display"); 10693 } else static if(is(T == V[K], K, V)) { 10694 auto dl = Element.make("dl"); 10695 dl.addClass("automatic-data-display associative-array"); 10696 foreach(k, v; t) { 10697 dl.addChild("dt", to!string(k)); 10698 dl.addChild("dd", formatReturnValueAsHtml(v)); 10699 } 10700 return dl; 10701 } else static if(is(T == struct)) { 10702 auto dl = Element.make("dl"); 10703 dl.addClass("automatic-data-display struct"); 10704 10705 foreach(idx, memberName; __traits(allMembers, T)) 10706 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 10707 dl.addChild("dt", beautify(memberName)); 10708 dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName))); 10709 } 10710 10711 return dl; 10712 } else static if(is(T == bool)) { 10713 return Element.make("span", t ? "true" : "false", "automatic-data-display"); 10714 } else static if(is(T == E[], E)) { 10715 static if(is(E : RestObject!Proxy, Proxy)) { 10716 // treat RestObject similar to struct 10717 auto table = cast(Table) Element.make("table"); 10718 table.addClass("automatic-data-display"); 10719 string[] names; 10720 foreach(idx, memberName; __traits(derivedMembers, E)) 10721 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10722 names ~= beautify(memberName); 10723 } 10724 table.appendHeaderRow(names); 10725 10726 foreach(l; t) { 10727 auto tr = table.appendRow(); 10728 foreach(idx, memberName; __traits(derivedMembers, E)) 10729 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10730 static if(memberName == "id") { 10731 string val = to!string(__traits(getMember, l, memberName)); 10732 tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME 10733 } else { 10734 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 10735 } 10736 } 10737 } 10738 10739 return table; 10740 } else static if(is(E == struct)) { 10741 // an array of structs is kinda special in that I like 10742 // having those formatted as tables. 10743 auto table = cast(Table) Element.make("table"); 10744 table.addClass("automatic-data-display"); 10745 string[] names; 10746 foreach(idx, memberName; __traits(allMembers, E)) 10747 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10748 names ~= beautify(memberName); 10749 } 10750 table.appendHeaderRow(names); 10751 10752 foreach(l; t) { 10753 auto tr = table.appendRow(); 10754 foreach(idx, memberName; __traits(allMembers, E)) 10755 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10756 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 10757 } 10758 } 10759 10760 return table; 10761 } else { 10762 // otherwise, I will just make a list. 10763 auto ol = Element.make("ol"); 10764 ol.addClass("automatic-data-display"); 10765 foreach(e; t) 10766 ol.addChild("li", formatReturnValueAsHtml(e)); 10767 return ol; 10768 } 10769 } else static if(is(T : Object)) { 10770 static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface 10771 return Element.make("div", t.toHtml()); 10772 else 10773 return Element.make("div", t.toString()); 10774 } else static assert(0, "bad return value for cgi call " ~ T.stringof); 10775 10776 assert(0); 10777 } 10778 10779 } 10780 10781 /++ 10782 The base class for the [dispatcher] function and object support. 10783 +/ 10784 class WebObject { 10785 //protected Cgi cgi; 10786 10787 protected void initialize(Cgi cgi) { 10788 //this.cgi = cgi; 10789 } 10790 } 10791 10792 /++ 10793 Can return one of the given types, decided at runtime. The syntax 10794 is to declare all the possible types in the return value, then you 10795 can `return typeof(return)(...value...)` to construct it. 10796 10797 It has an auto-generated constructor for each value it can hold. 10798 10799 --- 10800 MultipleResponses!(Redirection, string) getData(int how) { 10801 if(how & 1) 10802 return typeof(return)(Redirection("http://dpldocs.info/")); 10803 else 10804 return typeof(return)("hi there!"); 10805 } 10806 --- 10807 10808 If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little. 10809 +/ 10810 struct MultipleResponses(T...) { 10811 private size_t contains; 10812 private union { 10813 private T payload; 10814 } 10815 10816 static foreach(index, type; T) 10817 public this(type t) { 10818 contains = index; 10819 payload[index] = t; 10820 } 10821 10822 /++ 10823 This is primarily for testing. It is your way of getting to the response. 10824 10825 Let's say you wanted to test that one holding a Redirection and a string actually 10826 holds a string, by name of "test": 10827 10828 --- 10829 auto valueToTest = your_test_function(); 10830 10831 valueToTest.visit( 10832 (Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test 10833 (string s) { assert(s == "test"); } // right value, go ahead and test it. 10834 ); 10835 --- 10836 10837 History: 10838 Was horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it. 10839 It tried to use alias lambdas before, but runtime delegates work much better so I changed it. 10840 +/ 10841 void visit(Handlers...)(Handlers handlers) { 10842 template findHandler(type, int count, HandlersToCheck...) { 10843 static if(HandlersToCheck.length == 0) 10844 enum findHandler = -1; 10845 else { 10846 static if(is(typeof(HandlersToCheck[0].init(type.init)))) 10847 enum findHandler = count; 10848 else 10849 enum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]); 10850 } 10851 } 10852 foreach(index, type; T) { 10853 enum handlerIndex = findHandler!(type, 0, Handlers); 10854 static if(handlerIndex == -1) 10855 static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor"); 10856 else { 10857 if(index == this.contains) 10858 handlers[handlerIndex](this.payload[index]); 10859 } 10860 } 10861 } 10862 10863 /+ 10864 auto toArsdJsvar()() { 10865 import arsd.jsvar; 10866 return var(null); 10867 } 10868 +/ 10869 } 10870 10871 // FIXME: implement this somewhere maybe 10872 struct RawResponse { 10873 int code; 10874 string[] headers; 10875 const(ubyte)[] responseBody; 10876 } 10877 10878 /++ 10879 You can return this from [WebObject] subclasses for redirections. 10880 10881 (though note the static types means that class must ALWAYS redirect if 10882 you return this directly. You might want to return [MultipleResponses] if it 10883 can be conditional) 10884 +/ 10885 struct Redirection { 10886 string to; /// The URL to redirect to. 10887 int code = 303; /// The HTTP code to return. 10888 } 10889 10890 /++ 10891 Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. 10892 10893 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overriden 10894 the presenter in the dispatcher. 10895 10896 FIXME: explain this better 10897 10898 You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function, 10899 and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads, 10900 the runtime result of that is undefined. 10901 10902 A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those. 10903 (this might change, like maybe i will use pure as an indicator GET is ok. idk.) 10904 10905 $(WARNING 10906 --- 10907 // legal in D, undefined runtime behavior with cgi.d, it may call either method 10908 // even if you put different URL udas on it, the current code ignores them. 10909 void foo(int a) {} 10910 void foo(string a) {} 10911 --- 10912 ) 10913 10914 See_Also: [serveRestObject], [serveStaticFile] 10915 +/ 10916 auto serveApi(T)(string urlPrefix) { 10917 assert(urlPrefix[$ - 1] == '/'); 10918 return serveApiInternal!T(urlPrefix); 10919 } 10920 10921 private string nextPieceFromSlash(ref string remainingUrl) { 10922 if(remainingUrl.length == 0) 10923 return remainingUrl; 10924 int slash = 0; 10925 while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.') 10926 slash++; 10927 10928 // I am specifically passing `null` to differentiate it vs empty string 10929 // so in your ctor, `items` means new T(null) and `items/` means new T("") 10930 auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash]; 10931 // so if it is the last item, the dot can be used to load an alternative view 10932 // otherwise tho the dot is considered part of the identifier 10933 // FIXME 10934 10935 // again notice "" vs null here! 10936 if(slash == remainingUrl.length) 10937 remainingUrl = null; 10938 else 10939 remainingUrl = remainingUrl[slash + 1 .. $]; 10940 10941 return ident; 10942 } 10943 10944 /++ 10945 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. 10946 +/ 10947 enum AddTrailingSlash; 10948 /// ditto 10949 enum RemoveTrailingSlash; 10950 10951 private auto serveApiInternal(T)(string urlPrefix) { 10952 10953 import arsd.dom; 10954 import arsd.jsvar; 10955 10956 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 10957 string remainingUrl = cgi.pathInfo[urlPrefix.length .. $]; 10958 10959 try { 10960 // see duplicated code below by searching subresource_ctor 10961 // also see mustNotBeSetFromWebParams 10962 10963 static if(is(typeof(T.__ctor) P == __parameters)) { 10964 P params; 10965 10966 foreach(pidx, param; P) { 10967 static if(is(param : Cgi)) { 10968 static assert(!is(param == immutable)); 10969 cast() params[pidx] = cgi; 10970 } else static if(is(param == Session!D, D)) { 10971 static assert(!is(param == immutable)); 10972 cast() params[pidx] = cgi.getSessionObject!D(); 10973 10974 } else { 10975 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 10976 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 10977 static if(is(uda == ifCalledFromWeb!func, alias func)) { 10978 static if(is(typeof(func(cgi)))) 10979 params[pidx] = func(cgi); 10980 else 10981 params[pidx] = func(); 10982 } 10983 } 10984 } else { 10985 10986 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 10987 params[pidx] = param.getAutomaticallyForCgi(cgi); 10988 } else static if(is(param == string)) { 10989 auto ident = nextPieceFromSlash(remainingUrl); 10990 params[pidx] = ident; 10991 } else static assert(0, "illegal type for subresource " ~ param.stringof); 10992 } 10993 } 10994 } 10995 10996 auto obj = new T(params); 10997 } else { 10998 auto obj = new T(); 10999 } 11000 11001 return internalHandlerWithObject(obj, remainingUrl, cgi, presenter); 11002 } catch(Throwable t) { 11003 switch(cgi.request("format", "html")) { 11004 case "html": 11005 static void dummy() {} 11006 presenter.presentExceptionAsHtml(cgi, t, null); 11007 return true; 11008 case "json": 11009 var envelope = var.emptyObject; 11010 envelope.success = false; 11011 envelope.result = null; 11012 envelope.error = t.toString(); 11013 cgi.setResponseContentType("application/json"); 11014 cgi.write(envelope.toJson(), true); 11015 return true; 11016 default: 11017 throw t; 11018 // return true; 11019 } 11020 // return true; 11021 } 11022 11023 assert(0); 11024 } 11025 11026 static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) { 11027 11028 obj.initialize(cgi); 11029 11030 /+ 11031 Overload rules: 11032 Any unique combination of HTTP verb and url path can be dispatched to function overloads 11033 statically. 11034 11035 Moreover, some args vs no args can be overloaded dynamically. 11036 +/ 11037 11038 auto methodNameFromUrl = nextPieceFromSlash(remainingUrl); 11039 /+ 11040 auto orig = remainingUrl; 11041 assert(0, 11042 (orig is null ? "__null" : orig) 11043 ~ " .. " ~ 11044 (methodNameFromUrl is null ? "__null" : methodNameFromUrl)); 11045 +/ 11046 11047 if(methodNameFromUrl is null) 11048 methodNameFromUrl = "__null"; 11049 11050 string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl; 11051 11052 if(remainingUrl.length) 11053 hack ~= "/"; 11054 11055 switch(hack) { 11056 foreach(methodName; __traits(derivedMembers, T)) 11057 static if(methodName != "__ctor") 11058 foreach(idx, overload; __traits(getOverloads, T, methodName)) { 11059 static if(is(typeof(overload) P == __parameters)) 11060 static if(is(typeof(overload) R == return)) 11061 static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") 11062 { 11063 static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) 11064 case urlNameForMethod: 11065 11066 static if(is(R : WebObject)) { 11067 // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above. 11068 11069 // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string 11070 11071 // subresource_ctor 11072 // also see mustNotBeSetFromWebParams 11073 11074 P params; 11075 11076 string ident; 11077 11078 foreach(pidx, param; P) { 11079 static if(is(param : Cgi)) { 11080 static assert(!is(param == immutable)); 11081 cast() params[pidx] = cgi; 11082 } else static if(is(param == typeof(presenter))) { 11083 cast() param[pidx] = presenter; 11084 } else static if(is(param == Session!D, D)) { 11085 static assert(!is(param == immutable)); 11086 cast() params[pidx] = cgi.getSessionObject!D(); 11087 } else { 11088 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 11089 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 11090 static if(is(uda == ifCalledFromWeb!func, alias func)) { 11091 static if(is(typeof(func(cgi)))) 11092 params[pidx] = func(cgi); 11093 else 11094 params[pidx] = func(); 11095 } 11096 } 11097 } else { 11098 11099 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 11100 params[pidx] = param.getAutomaticallyForCgi(cgi); 11101 } else static if(is(param == string)) { 11102 ident = nextPieceFromSlash(remainingUrl); 11103 if(ident is null) { 11104 // trailing slash mandated on subresources 11105 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 11106 return true; 11107 } else { 11108 params[pidx] = ident; 11109 } 11110 } else static assert(0, "illegal type for subresource " ~ param.stringof); 11111 } 11112 } 11113 } 11114 11115 auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident); 11116 return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter); 11117 } else { 11118 // 404 it if any url left - not a subresource means we don't get to play with that! 11119 if(remainingUrl.length) 11120 return false; 11121 11122 bool automaticForm; 11123 11124 foreach(attr; __traits(getAttributes, overload)) 11125 static if(is(attr == AddTrailingSlash)) { 11126 if(remainingUrl is null) { 11127 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 11128 return true; 11129 } 11130 } else static if(is(attr == RemoveTrailingSlash)) { 11131 if(remainingUrl !is null) { 11132 cgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, "/")]); 11133 return true; 11134 } 11135 11136 } else static if(__traits(isSame, AutomaticForm, attr)) { 11137 automaticForm = true; 11138 } 11139 11140 /+ 11141 int zeroArgOverload = -1; 11142 int overloadCount = cast(int) __traits(getOverloads, T, methodName).length; 11143 bool calledWithZeroArgs = true; 11144 foreach(k, v; cgi.get) 11145 if(k != "format") { 11146 calledWithZeroArgs = false; 11147 break; 11148 } 11149 foreach(k, v; cgi.post) 11150 if(k != "format") { 11151 calledWithZeroArgs = false; 11152 break; 11153 } 11154 11155 // first, we need to go through and see if there is an empty one, since that 11156 // changes inside. But otherwise, all the stuff I care about can be done via 11157 // simple looping (other improper overloads might be flagged for runtime semantic check) 11158 // 11159 // an argument of type Cgi is ignored for these purposes 11160 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 11161 static if(is(typeof(overload) P == __parameters)) 11162 static if(P.length == 0) 11163 zeroArgOverload = cast(int) idx; 11164 else static if(P.length == 1 && is(P[0] : Cgi)) 11165 zeroArgOverload = cast(int) idx; 11166 }} 11167 // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method. 11168 bool overloadHasBeenCalled = false; 11169 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 11170 bool callFunction = true; 11171 // there is a zero arg overload and this is NOT it, and we have zero args - don't call this 11172 if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs) 11173 callFunction = false; 11174 // if this is the zero-arg overload, obviously it cannot be called if we got any args. 11175 if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs) 11176 callFunction = false; 11177 11178 // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea. 11179 11180 bool hadAnyMethodRestrictions = false; 11181 bool foundAcceptableMethod = false; 11182 foreach(attr; __traits(getAttributes, overload)) { 11183 static if(is(typeof(attr) == Cgi.RequestMethod)) { 11184 hadAnyMethodRestrictions = true; 11185 if(attr == cgi.requestMethod) 11186 foundAcceptableMethod = true; 11187 } 11188 } 11189 11190 if(hadAnyMethodRestrictions && !foundAcceptableMethod) 11191 callFunction = false; 11192 11193 /+ 11194 The overloads we really want to allow are the sane ones 11195 from the web perspective. Which is likely on HTTP verbs, 11196 for the most part, but might also be potentially based on 11197 some args vs zero args, or on argument names. Can't really 11198 do argument types very reliable through the web though; those 11199 should probably be different URLs. 11200 11201 Even names I feel is better done inside the function, so I'm not 11202 going to support that here. But the HTTP verbs and zero vs some 11203 args makes sense - it lets you define custom forms pretty easily. 11204 11205 Moreover, I'm of the opinion that empty overload really only makes 11206 sense on GET for this case. On a POST, it is just a missing argument 11207 exception and that should be handled by the presenter. But meh, I'll 11208 let the user define that, D only allows one empty arg thing anyway 11209 so the method UDAs are irrelevant. 11210 +/ 11211 if(callFunction) 11212 +/ 11213 11214 auto format = cgi.request("format", defaultFormat!overload()); 11215 auto wantsFormFormat = format.startsWith("form-"); 11216 11217 if(wantsFormFormat || (automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET)) { 11218 // Should I still show the form on a json thing? idk... 11219 auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx])); 11220 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), wantsFormFormat ? format["form_".length .. $] : "html"); 11221 return true; 11222 } 11223 11224 try { 11225 // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control. 11226 auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); 11227 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); 11228 } catch(Throwable t) { 11229 // presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx])); 11230 presenter.presentExceptionalReturn(cgi, t, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); 11231 } 11232 return true; 11233 //}} 11234 11235 //cgi.header("Accept: POST"); // FIXME list the real thing 11236 //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering. 11237 //return true; 11238 } 11239 } 11240 } 11241 case "GET script.js": 11242 cgi.setResponseContentType("text/javascript"); 11243 cgi.gzipResponse = true; 11244 cgi.write(presenter.script(), true); 11245 return true; 11246 case "GET style.css": 11247 cgi.setResponseContentType("text/css"); 11248 cgi.gzipResponse = true; 11249 cgi.write(presenter.style(), true); 11250 return true; 11251 default: 11252 return false; 11253 } 11254 11255 assert(0); 11256 } 11257 return DispatcherDefinition!internalHandler(urlPrefix, false); 11258 } 11259 11260 string defaultFormat(alias method)() { 11261 bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; 11262 foreach(attr; __traits(getAttributes, method)) { 11263 static if(is(typeof(attr) == DefaultFormat)) { 11264 if(nonConstConditionForWorkingAroundASpuriousDmdWarning) 11265 return attr.value; 11266 } 11267 } 11268 return "html"; 11269 } 11270 11271 struct Paginated(T) { 11272 T[] items; 11273 string nextPageUrl; 11274 } 11275 11276 template urlNamesForMethod(alias method, string default_) { 11277 string[] helper() { 11278 auto verb = Cgi.RequestMethod.GET; 11279 bool foundVerb = false; 11280 bool foundNoun = false; 11281 11282 string def = default_; 11283 11284 bool hasAutomaticForm = false; 11285 11286 foreach(attr; __traits(getAttributes, method)) { 11287 static if(is(typeof(attr) == Cgi.RequestMethod)) { 11288 verb = attr; 11289 if(foundVerb) 11290 assert(0, "Multiple http verbs on one function is not currently supported"); 11291 foundVerb = true; 11292 } 11293 static if(is(typeof(attr) == UrlName)) { 11294 if(foundNoun) 11295 assert(0, "Multiple url names on one function is not currently supported"); 11296 foundNoun = true; 11297 def = attr.name; 11298 } 11299 static if(__traits(isSame, attr, AutomaticForm)) { 11300 hasAutomaticForm = true; 11301 } 11302 } 11303 11304 if(def is null) 11305 def = "__null"; 11306 11307 string[] ret; 11308 11309 static if(is(typeof(method) R == return)) { 11310 static if(is(R : WebObject)) { 11311 def ~= "/"; 11312 foreach(v; __traits(allMembers, Cgi.RequestMethod)) 11313 ret ~= v ~ " " ~ def; 11314 } else { 11315 if(hasAutomaticForm) { 11316 ret ~= "GET " ~ def; 11317 ret ~= "POST " ~ def; 11318 } else { 11319 ret ~= to!string(verb) ~ " " ~ def; 11320 } 11321 } 11322 } else static assert(0); 11323 11324 return ret; 11325 } 11326 enum urlNamesForMethod = helper(); 11327 } 11328 11329 11330 enum AccessCheck { 11331 allowed, 11332 denied, 11333 nonExistant, 11334 } 11335 11336 enum Operation { 11337 show, 11338 create, 11339 replace, 11340 remove, 11341 update 11342 } 11343 11344 enum UpdateResult { 11345 accessDenied, 11346 noSuchResource, 11347 success, 11348 failure, 11349 unnecessary 11350 } 11351 11352 enum ValidationResult { 11353 valid, 11354 invalid 11355 } 11356 11357 11358 /++ 11359 The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. 11360 11361 WARNING: this is not stable. 11362 +/ 11363 class RestObject(CRTP) : WebObject { 11364 11365 import arsd.dom; 11366 import arsd.jsvar; 11367 11368 /// Prepare the object to be shown. 11369 void show() {} 11370 /// ditto 11371 void show(string urlId) { 11372 load(urlId); 11373 show(); 11374 } 11375 11376 /// Override this to provide access control to this object. 11377 AccessCheck accessCheck(string urlId, Operation operation) { 11378 return AccessCheck.allowed; 11379 } 11380 11381 ValidationResult validate() { 11382 // FIXME 11383 return ValidationResult.valid; 11384 } 11385 11386 string getUrlSlug() { 11387 import std.conv; 11388 static if(is(typeof(CRTP.id))) 11389 return to!string((cast(CRTP) this).id); 11390 else 11391 return null; 11392 } 11393 11394 // The functions with more arguments are the low-level ones, 11395 // they forward to the ones with fewer arguments by default. 11396 11397 // POST on a parent collection - this is called from a collection class after the members are updated 11398 /++ 11399 Given a populated object, this creates a new entry. Returns the url identifier 11400 of the new object. 11401 +/ 11402 string create(scope void delegate() applyChanges) { 11403 applyChanges(); 11404 save(); 11405 return getUrlSlug(); 11406 } 11407 11408 void replace() { 11409 save(); 11410 } 11411 void replace(string urlId, scope void delegate() applyChanges) { 11412 load(urlId); 11413 applyChanges(); 11414 replace(); 11415 } 11416 11417 void update(string[] fieldList) { 11418 save(); 11419 } 11420 void update(string urlId, scope void delegate() applyChanges, string[] fieldList) { 11421 load(urlId); 11422 applyChanges(); 11423 update(fieldList); 11424 } 11425 11426 void remove() {} 11427 11428 void remove(string urlId) { 11429 load(urlId); 11430 remove(); 11431 } 11432 11433 abstract void load(string urlId); 11434 abstract void save(); 11435 11436 Element toHtml(Presenter)(Presenter presenter) { 11437 import arsd.dom; 11438 import std.conv; 11439 auto obj = cast(CRTP) this; 11440 auto div = Element.make("div"); 11441 div.addClass("Dclass_" ~ CRTP.stringof); 11442 div.dataset.url = getUrlSlug(); 11443 bool first = true; 11444 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 11445 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11446 if(!first) div.addChild("br"); else first = false; 11447 div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); 11448 } 11449 return div; 11450 } 11451 11452 var toJson() { 11453 import arsd.jsvar; 11454 var v = var.emptyObject(); 11455 auto obj = cast(CRTP) this; 11456 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 11457 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11458 v[memberName] = __traits(getMember, obj, memberName); 11459 } 11460 return v; 11461 } 11462 11463 /+ 11464 auto structOf(this This) { 11465 11466 } 11467 +/ 11468 } 11469 11470 // FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value 11471 // https://use-the-index-luke.com/sql/partial-results/fetch-next-page 11472 11473 /++ 11474 Base class for REST collections. 11475 +/ 11476 class CollectionOf(Obj) : RestObject!(CollectionOf) { 11477 /// You might subclass this and use the cgi object's query params 11478 /// to implement a search filter, for example. 11479 /// 11480 /// FIXME: design a way to auto-generate that form 11481 /// (other than using the WebObject thing above lol 11482 // it'll prolly just be some searchParams UDA or maybe an enum. 11483 // 11484 // pagination too perhaps. 11485 // 11486 // and sorting too 11487 IndexResult index() { return IndexResult.init; } 11488 11489 string[] sortableFields() { return null; } 11490 string[] searchableFields() { return null; } 11491 11492 struct IndexResult { 11493 Obj[] results; 11494 11495 string[] sortableFields; 11496 11497 string previousPageIdentifier; 11498 string nextPageIdentifier; 11499 string firstPageIdentifier; 11500 string lastPageIdentifier; 11501 11502 int numberOfPages; 11503 } 11504 11505 override string create(scope void delegate() applyChanges) { assert(0); } 11506 override void load(string urlId) { assert(0); } 11507 override void save() { assert(0); } 11508 override void show() { 11509 index(); 11510 } 11511 override void show(string urlId) { 11512 show(); 11513 } 11514 11515 /// Proxy POST requests (create calls) to the child collection 11516 alias PostProxy = Obj; 11517 } 11518 11519 /++ 11520 Serves a REST object, similar to a Ruby on Rails resource. 11521 11522 You put data members in your class. cgi.d will automatically make something out of those. 11523 11524 It will call your constructor with the ID from the URL. This may be null. 11525 It will then populate the data members from the request. 11526 It will then call a method, if present, telling what happened. You don't need to write these! 11527 It finally returns a reply. 11528 11529 Your methods are passed a list of fields it actually set. 11530 11531 The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST 11532 APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better 11533 with relative linking. But meh.) 11534 11535 GET /items -> index. all values not set. 11536 GET /items/id -> get. only ID will be set, other params ignored. 11537 POST /items -> create. values set as given 11538 PUT /items/id -> replace. values set as given 11539 or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation 11540 a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form. 11541 PATCH /items/id -> update. values set as given, list of changed fields passed 11542 or POST /items/id with cgi.post["_method"] == "PATCH" 11543 DELETE /items/id -> destroy. only ID guaranteed to be set 11544 or POST /items/id with cgi.post["_method"] == "DELETE" 11545 11546 Following the stupid convention, there will never be a trailing slash here, and if it is there, it will 11547 redirect you away from it. 11548 11549 API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var. 11550 11551 I will also let you change the default, if you must. 11552 11553 // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes. 11554 11555 You can define sub-resources on your object inside the object. These sub-resources are also REST objects 11556 that follow the same thing. They may be individual resources or collections themselves. 11557 11558 Your class is expected to have at least the following methods: 11559 11560 FIXME: i kinda wanna add a routes object to the initialize call 11561 11562 create 11563 Create returns the new address on success, some code on failure. 11564 show 11565 index 11566 update 11567 remove 11568 11569 You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults 11570 should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that. 11571 11572 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar]. 11573 11574 NOT IMPLEMENTED 11575 11576 11577 Really, a collection is a resource with a bunch of subresources. 11578 11579 GET /items 11580 index because it is GET on the top resource 11581 11582 GET /items/foo 11583 item but different than items? 11584 11585 class Items { 11586 11587 } 11588 11589 ... but meh, a collection can be automated. not worth making it 11590 a separate thing, let's look at a real example. Users has many 11591 items and a virtual one, /users/current. 11592 11593 the individual users have properties and two sub-resources: 11594 session, which is just one, and comments, a collection. 11595 11596 class User : RestObject!() { // no parent 11597 int id; 11598 string name; 11599 11600 // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one. 11601 // but you can override them to do it differently. 11602 11603 // any member which is of type RestObject can be linked automatically via href btw. 11604 11605 void show() {} 11606 void show(string urlId) {} // automated! GET of this specific thing 11607 void create() {} // POST on a parent collection - this is called from a collection class after the members are updated 11608 void replace(string urlId) {} // this is the PUT; really, it just updates all fields. 11609 void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields. 11610 void remove(string urlId) {} // DELETE 11611 11612 void load(string urlId) {} // the default implementation of show() populates the id, then 11613 11614 this() {} 11615 11616 mixin Subresource!Session; 11617 mixin Subresource!Comment; 11618 } 11619 11620 class Session : RestObject!() { 11621 // the parent object may not be fully constructed/loaded 11622 this(User parent) {} 11623 11624 } 11625 11626 class Comment : CollectionOf!Comment { 11627 this(User parent) {} 11628 } 11629 11630 class Users : CollectionOf!User { 11631 // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects. 11632 void index() {} // GET on this specific thing; just like show really, just different name for the different semantics. 11633 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 11634 } 11635 11636 +/ 11637 auto serveRestObject(T)(string urlPrefix) { 11638 assert(urlPrefix[0] == '/'); 11639 assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); 11640 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 11641 string url = cgi.pathInfo[urlPrefix.length .. $]; 11642 11643 if(url.length && url[$ - 1] == '/') { 11644 // remove the final slash... 11645 cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]); 11646 return true; 11647 } 11648 11649 return restObjectServeHandler!T(cgi, presenter, url); 11650 } 11651 return DispatcherDefinition!internalHandler(urlPrefix, false); 11652 } 11653 11654 /+ 11655 /// Convenience method for serving a collection. It will be named the same 11656 /// as type T, just with an s at the end. If you need any further, just 11657 /// write the class yourself. 11658 auto serveRestCollectionOf(T)(string urlPrefix) { 11659 assert(urlPrefix[0] == '/'); 11660 mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`); 11661 return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); 11662 } 11663 +/ 11664 11665 bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) { 11666 string urlId = null; 11667 if(url.length && url[0] == '/') { 11668 // asking for a subobject 11669 urlId = url[1 .. $]; 11670 foreach(idx, ch; urlId) { 11671 if(ch == '/') { 11672 urlId = urlId[0 .. idx]; 11673 break; 11674 } 11675 } 11676 } 11677 11678 // FIXME handle other subresources 11679 11680 static if(is(T : CollectionOf!(C), C)) { 11681 if(urlId !is null) { 11682 return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId); 11683 } 11684 } 11685 11686 // FIXME: support precondition failed, if-modified-since, expectation failed, etc. 11687 11688 auto obj = new T(); 11689 obj.initialize(cgi); 11690 // FIXME: populate reflection info delegates 11691 11692 11693 // FIXME: I am not happy with this. 11694 switch(urlId) { 11695 case "script.js": 11696 cgi.setResponseContentType("text/javascript"); 11697 cgi.gzipResponse = true; 11698 cgi.write(presenter.script(), true); 11699 return true; 11700 case "style.css": 11701 cgi.setResponseContentType("text/css"); 11702 cgi.gzipResponse = true; 11703 cgi.write(presenter.style(), true); 11704 return true; 11705 default: 11706 // intentionally blank 11707 } 11708 11709 11710 11711 11712 static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) { 11713 foreach(idx, memberName; __traits(derivedMembers, Obj)) 11714 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11715 __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName)); 11716 } 11717 } 11718 void applyChanges() { 11719 applyChangesTemplate(cgi, obj); 11720 } 11721 11722 string[] modifiedList; 11723 11724 void writeObject(bool addFormLinks) { 11725 if(cgi.request("format") == "json") { 11726 cgi.setResponseContentType("application/json"); 11727 cgi.write(obj.toJson().toString, true); 11728 } else { 11729 auto container = presenter.htmlContainer(); 11730 if(addFormLinks) { 11731 static if(is(T : CollectionOf!(C), C)) 11732 container.appendHtml(` 11733 <form> 11734 <button type="submit" name="_method" value="POST">Create New</button> 11735 </form> 11736 `); 11737 else 11738 container.appendHtml(` 11739 <a href="..">Back</a> 11740 <form> 11741 <button type="submit" name="_method" value="PATCH">Edit</button> 11742 <button type="submit" name="_method" value="DELETE">Delete</button> 11743 </form> 11744 `); 11745 } 11746 container.appendChild(obj.toHtml(presenter)); 11747 cgi.write(container.parentDocument.toString, true); 11748 } 11749 } 11750 11751 // FIXME: I think I need a set type in here.... 11752 // it will be nice to pass sets of members. 11753 11754 try 11755 switch(cgi.requestMethod) { 11756 case Cgi.RequestMethod.GET: 11757 // I could prolly use template this parameters in the implementation above for some reflection stuff. 11758 // sure, it doesn't automatically work in subclasses... but I instantiate here anyway... 11759 11760 // automatic forms here for usable basic auto site from browser. 11761 // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh. 11762 switch(cgi.request("_method", "GET")) { 11763 case "GET": 11764 static if(is(T : CollectionOf!(C), C)) { 11765 auto results = obj.index(); 11766 if(cgi.request("format", "html") == "html") { 11767 auto container = presenter.htmlContainer(); 11768 auto html = presenter.formatReturnValueAsHtml(results.results); 11769 container.appendHtml(` 11770 <form> 11771 <button type="submit" name="_method" value="POST">Create New</button> 11772 </form> 11773 `); 11774 11775 container.appendChild(html); 11776 cgi.write(container.parentDocument.toString, true); 11777 } else { 11778 cgi.setResponseContentType("application/json"); 11779 import arsd.jsvar; 11780 var json = var.emptyArray; 11781 foreach(r; results.results) { 11782 var o = var.emptyObject; 11783 foreach(idx, memberName; __traits(derivedMembers, typeof(r))) 11784 static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) { 11785 o[memberName] = __traits(getMember, r, memberName); 11786 } 11787 11788 json ~= o; 11789 } 11790 cgi.write(json.toJson(), true); 11791 } 11792 } else { 11793 obj.show(urlId); 11794 writeObject(true); 11795 } 11796 break; 11797 case "PATCH": 11798 obj.load(urlId); 11799 goto case; 11800 case "PUT": 11801 case "POST": 11802 // an editing form for the object 11803 auto container = presenter.htmlContainer(); 11804 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 11805 auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj); 11806 } else { 11807 auto form = presenter.createAutomaticFormForObject(obj); 11808 } 11809 form.attrs.method = "POST"; 11810 form.setValue("_method", cgi.request("_method", "GET")); 11811 container.appendChild(form); 11812 cgi.write(container.parentDocument.toString(), true); 11813 break; 11814 case "DELETE": 11815 // FIXME: a delete form for the object (can be phrased "are you sure?") 11816 auto container = presenter.htmlContainer(); 11817 container.appendHtml(` 11818 <form method="POST"> 11819 Are you sure you want to delete this item? 11820 <input type="hidden" name="_method" value="DELETE" /> 11821 <input type="submit" value="Yes, Delete It" /> 11822 </form> 11823 11824 `); 11825 cgi.write(container.parentDocument.toString(), true); 11826 break; 11827 default: 11828 cgi.write("bad method\n", true); 11829 } 11830 break; 11831 case Cgi.RequestMethod.POST: 11832 // this is to allow compatibility with HTML forms 11833 switch(cgi.request("_method", "POST")) { 11834 case "PUT": 11835 goto PUT; 11836 case "PATCH": 11837 goto PATCH; 11838 case "DELETE": 11839 goto DELETE; 11840 case "POST": 11841 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 11842 auto p = new obj.PostProxy(); 11843 void specialApplyChanges() { 11844 applyChangesTemplate(cgi, p); 11845 } 11846 string n = p.create(&specialApplyChanges); 11847 } else { 11848 string n = obj.create(&applyChanges); 11849 } 11850 11851 auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n; 11852 cgi.setResponseLocation(newUrl); 11853 cgi.setResponseStatus("201 Created"); 11854 cgi.write(`The object has been created.`); 11855 break; 11856 default: 11857 cgi.write("bad method\n", true); 11858 } 11859 // FIXME this should be valid on the collection, but not the child.... 11860 // 303 See Other 11861 break; 11862 case Cgi.RequestMethod.PUT: 11863 PUT: 11864 obj.replace(urlId, &applyChanges); 11865 writeObject(false); 11866 break; 11867 case Cgi.RequestMethod.PATCH: 11868 PATCH: 11869 obj.update(urlId, &applyChanges, modifiedList); 11870 writeObject(false); 11871 break; 11872 case Cgi.RequestMethod.DELETE: 11873 DELETE: 11874 obj.remove(urlId); 11875 cgi.setResponseStatus("204 No Content"); 11876 break; 11877 default: 11878 // FIXME: OPTIONS, HEAD 11879 } 11880 catch(Throwable t) { 11881 presenter.presentExceptionAsHtml(cgi, t); 11882 } 11883 11884 return true; 11885 } 11886 11887 /+ 11888 struct SetOfFields(T) { 11889 private void[0][string] storage; 11890 void set(string what) { 11891 //storage[what] = 11892 } 11893 void unset(string what) {} 11894 void setAll() {} 11895 void unsetAll() {} 11896 bool isPresent(string what) { return false; } 11897 } 11898 +/ 11899 11900 /+ 11901 enum readonly; 11902 enum hideonindex; 11903 +/ 11904 11905 /++ 11906 Returns true if I recommend gzipping content of this type. You might 11907 want to call it from your Presenter classes before calling cgi.write. 11908 11909 --- 11910 cgi.setResponseContentType(yourContentType); 11911 cgi.gzipResponse = gzipRecommendedForContentType(yourContentType); 11912 cgi.write(yourData, true); 11913 --- 11914 11915 This is used internally by [serveStaticFile], [serveStaticFileDirectory], [serveStaticData], and maybe others I forgot to update this doc about. 11916 11917 11918 The implementation considers text content to be recommended to gzip. This may change, but it seems reasonable enough for now. 11919 11920 History: 11921 Added January 28, 2023 (dub v11.0) 11922 +/ 11923 bool gzipRecommendedForContentType(string contentType) { 11924 if(contentType.startsWith("text/")) 11925 return true; 11926 if(contentType.startsWith("application/javascript")) 11927 return true; 11928 11929 return false; 11930 } 11931 11932 /++ 11933 Serves a static file. To be used with [dispatcher]. 11934 11935 See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect] 11936 +/ 11937 auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) { 11938 // https://baus.net/on-tcp_cork/ 11939 // man 2 sendfile 11940 assert(urlPrefix[0] == '/'); 11941 if(filename is null) 11942 filename = decodeUriComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? 11943 if(contentType is null) { 11944 contentType = contentTypeFromFileExtension(filename); 11945 } 11946 11947 static struct DispatcherDetails { 11948 string filename; 11949 string contentType; 11950 } 11951 11952 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 11953 if(details.contentType.indexOf("image/") == 0 || details.contentType.indexOf("audio/") == 0) 11954 cgi.setCache(true); 11955 cgi.setResponseContentType(details.contentType); 11956 cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); 11957 cgi.write(std.file.read(details.filename), true); 11958 return true; 11959 } 11960 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); 11961 } 11962 11963 /++ 11964 Serves static data. To be used with [dispatcher]. 11965 11966 History: 11967 Added October 31, 2021 11968 +/ 11969 auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { 11970 assert(urlPrefix[0] == '/'); 11971 if(contentType is null) { 11972 contentType = contentTypeFromFileExtension(urlPrefix); 11973 } 11974 11975 static struct DispatcherDetails { 11976 immutable(void)[] data; 11977 string contentType; 11978 } 11979 11980 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 11981 cgi.setCache(true); 11982 cgi.setResponseContentType(details.contentType); 11983 cgi.write(details.data, true); 11984 return true; 11985 } 11986 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); 11987 } 11988 11989 string contentTypeFromFileExtension(string filename) { 11990 if(filename.endsWith(".png")) 11991 return "image/png"; 11992 if(filename.endsWith(".apng")) 11993 return "image/apng"; 11994 if(filename.endsWith(".svg")) 11995 return "image/svg+xml"; 11996 if(filename.endsWith(".jpg")) 11997 return "image/jpeg"; 11998 if(filename.endsWith(".html")) 11999 return "text/html"; 12000 if(filename.endsWith(".css")) 12001 return "text/css"; 12002 if(filename.endsWith(".js")) 12003 return "application/javascript"; 12004 if(filename.endsWith(".wasm")) 12005 return "application/wasm"; 12006 if(filename.endsWith(".mp3")) 12007 return "audio/mpeg"; 12008 if(filename.endsWith(".pdf")) 12009 return "application/pdf"; 12010 return null; 12011 } 12012 12013 /// This serves a directory full of static files, figuring out the content-types from file extensions. 12014 /// It does not let you to descend into subdirectories (or ascend out of it, of course) 12015 auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool recursive = false) { 12016 assert(urlPrefix[0] == '/'); 12017 assert(urlPrefix[$-1] == '/'); 12018 12019 static struct DispatcherDetails { 12020 string directory; 12021 bool recursive; 12022 } 12023 12024 if(directory is null) 12025 directory = urlPrefix[1 .. $]; 12026 12027 if(directory.length == 0) 12028 directory = "./"; 12029 12030 assert(directory[$-1] == '/'); 12031 12032 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12033 auto file = decodeUriComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct 12034 12035 if(details.recursive) { 12036 // never allow a backslash since it isn't in a typical url anyway and makes the following checks easier 12037 if(file.indexOf("\\") != -1) 12038 return false; 12039 12040 import std.path; 12041 12042 file = std.path.buildNormalizedPath(file); 12043 enum upOneDir = ".." ~ std.path.dirSeparator; 12044 12045 // also no point doing any kind of up directory things since that makes it more likely to break out of the parent 12046 if(file == ".." || file.startsWith(upOneDir)) 12047 return false; 12048 if(std.path.isAbsolute(file)) 12049 return false; 12050 12051 // FIXME: if it has slashes and stuff, should we redirect to the canonical resource? or what? 12052 12053 // once it passes these filters it is probably ok. 12054 } else { 12055 if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) 12056 return false; 12057 } 12058 12059 if(file.length == 0) 12060 return false; 12061 12062 auto contentType = contentTypeFromFileExtension(file); 12063 12064 auto fn = details.directory ~ file; 12065 if(std.file.exists(fn)) { 12066 //if(contentType.indexOf("image/") == 0) 12067 //cgi.setCache(true); 12068 //else if(contentType.indexOf("audio/") == 0) 12069 cgi.setCache(true); 12070 cgi.setResponseContentType(contentType); 12071 cgi.gzipResponse = gzipRecommendedForContentType(contentType); 12072 cgi.write(std.file.read(fn), true); 12073 return true; 12074 } else { 12075 return false; 12076 } 12077 } 12078 12079 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, recursive)); 12080 } 12081 12082 /++ 12083 Redirects one url to another 12084 12085 See_Also: [dispatcher], [serveStaticFile] 12086 +/ 12087 auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) { 12088 assert(urlPrefix[0] == '/'); 12089 static struct DispatcherDetails { 12090 string redirectTo; 12091 string code; 12092 } 12093 12094 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12095 cgi.setResponseLocation(details.redirectTo, true, details.code); 12096 return true; 12097 } 12098 12099 12100 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code))); 12101 } 12102 12103 /// Used exclusively with `dispatchTo` 12104 struct DispatcherData(Presenter) { 12105 Cgi cgi; /// You can use this cgi object. 12106 Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher. 12107 size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only. 12108 } 12109 12110 /++ 12111 Dispatches the URL to a specific function. 12112 +/ 12113 auto handleWith(alias handler)(string urlPrefix) { 12114 // cuz I'm too lazy to do it better right now 12115 static class Hack : WebObject { 12116 static import std.traits; 12117 @UrlName("") 12118 auto handle(std.traits.Parameters!handler args) { 12119 return handler(args); 12120 } 12121 } 12122 12123 return urlPrefix.serveApiInternal!Hack; 12124 } 12125 12126 /++ 12127 Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this: 12128 12129 --- 12130 bool other(DD)(DD dd) { 12131 return dd.dispatcher!( 12132 "/whatever".serveRedirect("/success"), 12133 "/api/".serveApi!MyClass 12134 ); 12135 } 12136 --- 12137 12138 The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher 12139 here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters. 12140 Or, of course, you could just use the exact type in your own code. 12141 12142 You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a 12143 good job. 12144 12145 12146 +/ 12147 auto dispatchTo(alias handler)(string urlPrefix) { 12148 assert(urlPrefix[0] == '/'); 12149 assert(urlPrefix[$-1] != '/'); 12150 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { 12151 return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); 12152 } 12153 12154 return DispatcherDefinition!(internalHandler)(urlPrefix, false); 12155 } 12156 12157 /++ 12158 See [serveStaticFile] if you want to serve a file off disk. 12159 12160 History: 12161 Added January 28, 2023 (dub v11.0) 12162 +/ 12163 auto serveStaticData(string urlPrefix, immutable(ubyte)[] data, string contentType, string filenameToSuggestAsDownload = null) { 12164 assert(urlPrefix[0] == '/'); 12165 12166 static struct DispatcherDetails { 12167 immutable(ubyte)[] data; 12168 string contentType; 12169 string filenameToSuggestAsDownload; 12170 } 12171 12172 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12173 cgi.setCache(true); 12174 cgi.setResponseContentType(details.contentType); 12175 if(details.filenameToSuggestAsDownload.length) 12176 cgi.header("Content-Disposition: attachment; filename=\""~details.filenameToSuggestAsDownload~"\""); 12177 cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); 12178 cgi.write(details.data, true); 12179 return true; 12180 } 12181 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType, filenameToSuggestAsDownload)); 12182 } 12183 12184 /++ 12185 Placeholder for use with [dispatchSubsection]'s `NewPresenter` argument to indicate you want to keep the parent's presenter. 12186 12187 History: 12188 Added January 28, 2023 (dub v11.0) 12189 +/ 12190 alias KeepExistingPresenter = typeof(null); 12191 12192 /++ 12193 For use with [dispatchSubsection]. Calls your filter with the request and if your filter returns false, 12194 this issues the given errorCode and stops processing. 12195 12196 --- 12197 bool hasAdminPermissions(Cgi cgi) { 12198 return true; 12199 } 12200 12201 mixin DispatcherMain!( 12202 "/admin".dispatchSubsection!( 12203 passFilterOrIssueError!(hasAdminPermissions, 403), 12204 KeepExistingPresenter, 12205 "/".serveApi!AdminFunctions 12206 ) 12207 ); 12208 --- 12209 12210 History: 12211 Added January 28, 2023 (dub v11.0) 12212 +/ 12213 template passFilterOrIssueError(alias filter, int errorCode) { 12214 bool passFilterOrIssueError(DispatcherDetails)(DispatcherDetails dd) { 12215 if(filter(dd.cgi)) 12216 return true; 12217 dd.presenter.renderBasicError(dd.cgi, errorCode); 12218 return false; 12219 } 12220 } 12221 12222 /++ 12223 Allows for a subsection of your dispatched urls to be passed through other a pre-request filter, optionally pick up an new presenter class, 12224 and then be dispatched to their own handlers. 12225 12226 --- 12227 /+ 12228 // a long-form filter function 12229 bool permissionCheck(DispatcherData)(DispatcherData dd) { 12230 // you are permitted to call mutable methods on the Cgi object 12231 // Note if you use a Cgi subclass, you can try dynamic casting it back to your custom type to attach per-request data 12232 // though much of the request is immutable so there's only so much you're allowed to do to modify it. 12233 12234 if(checkPermissionOnRequest(dd.cgi)) { 12235 return true; // OK, allow processing to continue 12236 } else { 12237 dd.presenter.renderBasicError(dd.cgi, 403); // reply forbidden to the requester 12238 return false; // and stop further processing into this subsection 12239 } 12240 } 12241 +/ 12242 12243 // but you can also do short-form filters: 12244 12245 bool permissionCheck(Cgi cgi) { 12246 return ("ok" in cgi.get) !is null; 12247 } 12248 12249 // handler for the subsection 12250 class AdminClass : WebObject { 12251 int foo() { return 5; } 12252 } 12253 12254 // handler for the main site 12255 class TheMainSite : WebObject {} 12256 12257 mixin DispatcherMain!( 12258 "/admin".dispatchSubsection!( 12259 // converts our short-form filter into a long-form filter 12260 passFilterOrIssueError!(permissionCheck, 403), 12261 // can use a new presenter if wanted for the subsection 12262 KeepExistingPresenter, 12263 // and then provide child route dispatchers 12264 "/".serveApi!AdminClass 12265 ), 12266 // and back to the top level 12267 "/".serveApi!TheMainSite 12268 ); 12269 --- 12270 12271 Note you can encapsulate sections in files like this: 12272 12273 --- 12274 auto adminDispatcher(string urlPrefix) { 12275 return urlPrefix.dispatchSubsection!( 12276 .... 12277 ); 12278 } 12279 12280 mixin DispatcherMain!( 12281 "/admin".adminDispatcher, 12282 // and so on 12283 ) 12284 --- 12285 12286 If you want no filter, you can pass `(cgi) => true` as the filter to approve all requests. 12287 12288 If you want to keep the same presenter as the parent, use [KeepExistingPresenter] as the presenter argument. 12289 12290 12291 History: 12292 Added January 28, 2023 (dub v11.0) 12293 +/ 12294 auto dispatchSubsection(alias PreRequestFilter, NewPresenter, definitions...)(string urlPrefix) { 12295 assert(urlPrefix[0] == '/'); 12296 assert(urlPrefix[$-1] != '/'); 12297 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { 12298 static if(!is(PreRequestFilter == typeof(null))) { 12299 if(!PreRequestFilter(DispatcherData!Presenter(cgi, presenter, urlPrefix.length))) 12300 return true; // we handled it by rejecting it 12301 } 12302 12303 static if(is(NewPresenter == Presenter) || is(NewPresenter == typeof(null))) { 12304 return dispatcher!definitions(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); 12305 } else { 12306 auto newPresenter = new NewPresenter(); 12307 return dispatcher!(definitions(DispatcherData!NewPresenter(cgi, presenter, urlPrefix.length))); 12308 } 12309 } 12310 12311 return DispatcherDefinition!(internalHandler)(urlPrefix, false); 12312 } 12313 12314 /++ 12315 A URL dispatcher. 12316 12317 --- 12318 if(cgi.dispatcher!( 12319 "/api/".serveApi!MyApiClass, 12320 "/objects/lol".serveRestObject!MyRestObject, 12321 "/file.js".serveStaticFile, 12322 "/admin/".dispatchTo!adminHandler 12323 )) return; 12324 --- 12325 12326 12327 You define a series of url prefixes followed by handlers. 12328 12329 You may want to do different pre- and post- processing there, for example, 12330 an authorization check and different page layout. You can use different 12331 presenters and different function chains. See [dispatchSubsection] for details. 12332 12333 [dispatchTo] will send the request to another function for handling. 12334 +/ 12335 template dispatcher(definitions...) { 12336 bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) { 12337 static if(is(Presenter == typeof(null))) { 12338 static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {} 12339 auto presenter = new GenericWebPresenter(); 12340 } else 12341 alias presenter = presenterArg; 12342 12343 return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0)); 12344 } 12345 12346 bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) { 12347 // I can prolly make this more efficient later but meh. 12348 foreach(definition; definitions) { 12349 if(definition.rejectFurther) { 12350 if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) { 12351 auto ret = definition.handler( 12352 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 12353 dispatcherData.cgi, dispatcherData.presenter, definition.details); 12354 if(ret) 12355 return true; 12356 } 12357 } else if( 12358 dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) && 12359 // cgi.d dispatcher urls must be complete or have a /; 12360 // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing" 12361 (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length 12362 || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/') 12363 ) { 12364 auto ret = definition.handler( 12365 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 12366 dispatcherData.cgi, dispatcherData.presenter, definition.details); 12367 if(ret) 12368 return true; 12369 } 12370 } 12371 return false; 12372 } 12373 } 12374 12375 }); 12376 12377 private struct StackBuffer { 12378 char[1024] initial = void; 12379 char[] buffer; 12380 size_t position; 12381 12382 this(int a) { 12383 buffer = initial[]; 12384 position = 0; 12385 } 12386 12387 void add(in char[] what) { 12388 if(position + what.length > buffer.length) 12389 buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases 12390 buffer[position .. position + what.length] = what[]; 12391 position += what.length; 12392 } 12393 12394 void add(in char[] w1, in char[] w2, in char[] w3 = null) { 12395 add(w1); 12396 add(w2); 12397 add(w3); 12398 } 12399 12400 void add(long v) { 12401 char[16] buffer = void; 12402 auto pos = buffer.length; 12403 bool negative; 12404 if(v < 0) { 12405 negative = true; 12406 v = -v; 12407 } 12408 do { 12409 buffer[--pos] = cast(char) (v % 10 + '0'); 12410 v /= 10; 12411 } while(v); 12412 12413 if(negative) 12414 buffer[--pos] = '-'; 12415 12416 auto res = buffer[pos .. $]; 12417 12418 add(res[]); 12419 } 12420 12421 char[] get() @nogc { 12422 return buffer[0 .. position]; 12423 } 12424 } 12425 12426 // duplicated in http2.d 12427 private static string getHttpCodeText(int code) pure nothrow @nogc { 12428 switch(code) { 12429 case 200: return "200 OK"; 12430 case 201: return "201 Created"; 12431 case 202: return "202 Accepted"; 12432 case 203: return "203 Non-Authoritative Information"; 12433 case 204: return "204 No Content"; 12434 case 205: return "205 Reset Content"; 12435 case 206: return "206 Partial Content"; 12436 // 12437 case 300: return "300 Multiple Choices"; 12438 case 301: return "301 Moved Permanently"; 12439 case 302: return "302 Found"; 12440 case 303: return "303 See Other"; 12441 case 304: return "304 Not Modified"; 12442 case 305: return "305 Use Proxy"; 12443 case 307: return "307 Temporary Redirect"; 12444 case 308: return "308 Permanent Redirect"; 12445 12446 // 12447 case 400: return "400 Bad Request"; 12448 case 401: return "401 Unauthorized"; 12449 case 402: return "402 Payment Required"; 12450 case 403: return "403 Forbidden"; 12451 case 404: return "404 Not Found"; 12452 case 405: return "405 Method Not Allowed"; 12453 case 406: return "406 Not Acceptable"; 12454 case 407: return "407 Proxy Authentication Required"; 12455 case 408: return "408 Request Timeout"; 12456 case 409: return "409 Conflict"; 12457 case 410: return "410 Gone"; 12458 case 411: return "411 Length Required"; 12459 case 412: return "412 Precondition Failed"; 12460 case 413: return "413 Payload Too Large"; 12461 case 414: return "414 URI Too Long"; 12462 case 415: return "415 Unsupported Media Type"; 12463 case 416: return "416 Range Not Satisfiable"; 12464 case 417: return "417 Expectation Failed"; 12465 case 418: return "418 I'm a teapot"; 12466 case 421: return "421 Misdirected Request"; 12467 case 422: return "422 Unprocessable Entity (WebDAV)"; 12468 case 423: return "423 Locked (WebDAV)"; 12469 case 424: return "424 Failed Dependency (WebDAV)"; 12470 case 425: return "425 Too Early"; 12471 case 426: return "426 Upgrade Required"; 12472 case 428: return "428 Precondition Required"; 12473 case 431: return "431 Request Header Fields Too Large"; 12474 case 451: return "451 Unavailable For Legal Reasons"; 12475 12476 case 500: return "500 Internal Server Error"; 12477 case 501: return "501 Not Implemented"; 12478 case 502: return "502 Bad Gateway"; 12479 case 503: return "503 Service Unavailable"; 12480 case 504: return "504 Gateway Timeout"; 12481 case 505: return "505 HTTP Version Not Supported"; 12482 case 506: return "506 Variant Also Negotiates"; 12483 case 507: return "507 Insufficient Storage (WebDAV)"; 12484 case 508: return "508 Loop Detected (WebDAV)"; 12485 case 510: return "510 Not Extended"; 12486 case 511: return "511 Network Authentication Required"; 12487 // 12488 default: assert(0, "Unsupported http code"); 12489 } 12490 } 12491 12492 12493 /+ 12494 /++ 12495 This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object. 12496 12497 It relies on jsvar.d and dom.d. 12498 12499 12500 You can get javascript out of it to call. The generated functions need to look 12501 like 12502 12503 function name(a,b,c,d,e) { 12504 return _call("name", {"realName":a,"sds":b}); 12505 } 12506 12507 And _call returns an object you can call or set up or whatever. 12508 +/ 12509 bool apiDispatcher()(Cgi cgi) { 12510 import arsd.jsvar; 12511 import arsd.dom; 12512 } 12513 +/ 12514 version(linux) 12515 private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; 12516 /* 12517 Copyright: Adam D. Ruppe, 2008 - 2023 12518 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. 12519 Authors: Adam D. Ruppe 12520 12521 Copyright Adam D. Ruppe 2008 - 2023. 12522 Distributed under the Boost Software License, Version 1.0. 12523 (See accompanying file LICENSE_1_0.txt or copy at 12524 http://www.boost.org/LICENSE_1_0.txt) 12525 */