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