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