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 bfr.popFront(0); 6936 if(bfr.sourceClosed) 6937 return false; 6938 goto top; 6939 } 6940 6941 6942 bool isDataPending(Duration timeout = 0.seconds) { 6943 Socket socket = cgi.idlol.source; 6944 6945 auto check = new SocketSet(); 6946 check.add(socket); 6947 6948 auto got = Socket.select(check, null, null, timeout); 6949 if(got > 0) 6950 return true; 6951 return false; 6952 } 6953 6954 // note: this blocks 6955 WebSocketFrame recv() { 6956 return waitForNextMessage(); 6957 } 6958 6959 6960 6961 6962 private void llclose() { 6963 cgi.close(); 6964 } 6965 6966 private void llsend(ubyte[] data) { 6967 cgi.write(data); 6968 cgi.flush(); 6969 } 6970 6971 void unregisterActiveSocket(WebSocket) {} 6972 6973 /* copy/paste section { */ 6974 6975 private int readyState_; 6976 private ubyte[] receiveBuffer; 6977 private size_t receiveBufferUsedLength; 6978 6979 private Config config; 6980 6981 enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. 6982 enum OPEN = 1; /// The connection is open and ready to communicate. 6983 enum CLOSING = 2; /// The connection is in the process of closing. 6984 enum CLOSED = 3; /// The connection is closed or couldn't be opened. 6985 6986 /++ 6987 6988 +/ 6989 /// Group: foundational 6990 static struct Config { 6991 /++ 6992 These control the size of the receive buffer. 6993 6994 It starts at the initial size, will temporarily 6995 balloon up to the maximum size, and will reuse 6996 a buffer up to the likely size. 6997 6998 Anything larger than the maximum size will cause 6999 the connection to be aborted and an exception thrown. 7000 This is to protect you against a peer trying to 7001 exhaust your memory, while keeping the user-level 7002 processing simple. 7003 +/ 7004 size_t initialReceiveBufferSize = 4096; 7005 size_t likelyReceiveBufferSize = 4096; /// ditto 7006 size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto 7007 7008 /++ 7009 Maximum combined size of a message. 7010 +/ 7011 size_t maximumMessageSize = 10 * 1024 * 1024; 7012 7013 string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; 7014 string origin; /// Origin URL to send with the handshake, if desired. 7015 string protocol; /// the protocol header, if desired. 7016 7017 /++ 7018 Additional headers to put in the HTTP request. These should be formatted `Name: value`, like for example: 7019 7020 --- 7021 Config config; 7022 config.additionalHeaders ~= "Authorization: Bearer your_auth_token_here"; 7023 --- 7024 7025 History: 7026 Added February 19, 2021 (included in dub version 9.2) 7027 +/ 7028 string[] additionalHeaders; 7029 7030 /++ 7031 Amount of time (in msecs) of idleness after which to send an automatic ping 7032 7033 Please note how this interacts with [timeoutFromInactivity] - a ping counts as activity that 7034 keeps the socket alive. 7035 +/ 7036 int pingFrequency = 5000; 7037 7038 /++ 7039 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. 7040 7041 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! 7042 7043 History: 7044 Added March 31, 2021 (included in dub version 9.4) 7045 +/ 7046 Duration timeoutFromInactivity = 1.minutes; 7047 7048 /++ 7049 For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be 7050 verified. Setting this to `false` will skip this check and allow the connection to continue anyway. 7051 7052 History: 7053 Added April 5, 2022 (dub v10.8) 7054 7055 Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes 7056 even if it was true, it would skip the verification. Now, it always respects this local setting. 7057 +/ 7058 bool verifyPeer = true; 7059 } 7060 7061 /++ 7062 Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. 7063 +/ 7064 int readyState() { 7065 return readyState_; 7066 } 7067 7068 /++ 7069 Closes the connection, sending a graceful teardown message to the other side. 7070 7071 Code 1000 is the normal closure code. 7072 7073 History: 7074 The default `code` was changed to 1000 on January 9, 2023. Previously it was 0, 7075 but also ignored anyway. 7076 +/ 7077 /// Group: foundational 7078 void close(int code = 1000, string reason = null) 7079 //in (reason.length < 123) 7080 in { assert(reason.length < 123); } do 7081 { 7082 if(readyState_ != OPEN) 7083 return; // it cool, we done 7084 WebSocketFrame wss; 7085 wss.fin = true; 7086 wss.masked = this.isClient; 7087 wss.opcode = WebSocketOpcode.close; 7088 wss.data = [ubyte((code >> 8) & 0xff), ubyte(code & 0xff)] ~ cast(ubyte[]) reason.dup; 7089 wss.send(&llsend); 7090 7091 readyState_ = CLOSING; 7092 7093 closeCalled = true; 7094 7095 llclose(); 7096 } 7097 7098 private bool closeCalled; 7099 7100 /++ 7101 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. 7102 +/ 7103 /// Group: foundational 7104 void ping(in ubyte[] data = null) { 7105 WebSocketFrame wss; 7106 wss.fin = true; 7107 wss.masked = this.isClient; 7108 wss.opcode = WebSocketOpcode.ping; 7109 if(data !is null) wss.data = data.dup; 7110 wss.send(&llsend); 7111 } 7112 7113 /++ 7114 Sends a pong message to the server. This is normally done automatically in response to pings. 7115 +/ 7116 /// Group: foundational 7117 void pong(in ubyte[] data = null) { 7118 WebSocketFrame wss; 7119 wss.fin = true; 7120 wss.masked = this.isClient; 7121 wss.opcode = WebSocketOpcode.pong; 7122 if(data !is null) wss.data = data.dup; 7123 wss.send(&llsend); 7124 } 7125 7126 /++ 7127 Sends a text message through the websocket. 7128 +/ 7129 /// Group: foundational 7130 void send(in char[] textData) { 7131 WebSocketFrame wss; 7132 wss.fin = true; 7133 wss.masked = this.isClient; 7134 wss.opcode = WebSocketOpcode.text; 7135 wss.data = cast(ubyte[]) textData.dup; 7136 wss.send(&llsend); 7137 } 7138 7139 /++ 7140 Sends a binary message through the websocket. 7141 +/ 7142 /// Group: foundational 7143 void send(in ubyte[] binaryData) { 7144 WebSocketFrame wss; 7145 wss.masked = this.isClient; 7146 wss.fin = true; 7147 wss.opcode = WebSocketOpcode.binary; 7148 wss.data = cast(ubyte[]) binaryData.dup; 7149 wss.send(&llsend); 7150 } 7151 7152 /++ 7153 Waits for and returns the next complete message on the socket. 7154 7155 Note that the onmessage function is still called, right before 7156 this returns. 7157 +/ 7158 /// Group: blocking_api 7159 public WebSocketFrame waitForNextMessage() { 7160 do { 7161 auto m = processOnce(); 7162 if(m.populated) 7163 return m; 7164 } while(lowLevelReceive()); 7165 7166 throw new ConnectionClosedException("Websocket receive timed out"); 7167 //return WebSocketFrame.init; // FIXME? maybe. 7168 } 7169 7170 /++ 7171 Tells if [waitForNextMessage] would block. 7172 +/ 7173 /// Group: blocking_api 7174 public bool waitForNextMessageWouldBlock() { 7175 checkAgain: 7176 if(isMessageBuffered()) 7177 return false; 7178 if(!isDataPending()) 7179 return true; 7180 7181 while(isDataPending()) { 7182 if(lowLevelReceive() == false) 7183 throw new ConnectionClosedException("Connection closed in middle of message"); 7184 } 7185 7186 goto checkAgain; 7187 } 7188 7189 /++ 7190 Is there a message in the buffer already? 7191 If `true`, [waitForNextMessage] is guaranteed to return immediately. 7192 If `false`, check [isDataPending] as the next step. 7193 +/ 7194 /// Group: blocking_api 7195 public bool isMessageBuffered() { 7196 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 7197 auto s = d; 7198 if(d.length) { 7199 auto orig = d; 7200 auto m = WebSocketFrame.read(d); 7201 // that's how it indicates that it needs more data 7202 if(d !is orig) 7203 return true; 7204 } 7205 7206 return false; 7207 } 7208 7209 private ubyte continuingType; 7210 private ubyte[] continuingData; 7211 //private size_t continuingDataLength; 7212 7213 private WebSocketFrame processOnce() { 7214 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 7215 auto s = d; 7216 // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. 7217 WebSocketFrame m; 7218 if(d.length) { 7219 auto orig = d; 7220 m = WebSocketFrame.read(d); 7221 // that's how it indicates that it needs more data 7222 if(d is orig) 7223 return WebSocketFrame.init; 7224 m.unmaskInPlace(); 7225 switch(m.opcode) { 7226 case WebSocketOpcode.continuation: 7227 if(continuingData.length + m.data.length > config.maximumMessageSize) 7228 throw new Exception("message size exceeded"); 7229 7230 continuingData ~= m.data; 7231 if(m.fin) { 7232 if(ontextmessage) 7233 ontextmessage(cast(char[]) continuingData); 7234 if(onbinarymessage) 7235 onbinarymessage(continuingData); 7236 7237 continuingData = null; 7238 } 7239 break; 7240 case WebSocketOpcode.text: 7241 if(m.fin) { 7242 if(ontextmessage) 7243 ontextmessage(m.textData); 7244 } else { 7245 continuingType = m.opcode; 7246 //continuingDataLength = 0; 7247 continuingData = null; 7248 continuingData ~= m.data; 7249 } 7250 break; 7251 case WebSocketOpcode.binary: 7252 if(m.fin) { 7253 if(onbinarymessage) 7254 onbinarymessage(m.data); 7255 } else { 7256 continuingType = m.opcode; 7257 //continuingDataLength = 0; 7258 continuingData = null; 7259 continuingData ~= m.data; 7260 } 7261 break; 7262 case WebSocketOpcode.close: 7263 7264 //import std.stdio; writeln("closed ", cast(string) m.data); 7265 7266 ushort code = CloseEvent.StandardCloseCodes.noStatusCodePresent; 7267 const(char)[] reason; 7268 7269 if(m.data.length >= 2) { 7270 code = (m.data[0] << 8) | m.data[1]; 7271 reason = (cast(char[]) m.data[2 .. $]); 7272 } 7273 7274 if(onclose) 7275 onclose(CloseEvent(code, reason, true)); 7276 7277 // if we receive one and haven't sent one back we're supposed to echo it back and close. 7278 if(!closeCalled) 7279 close(code, reason.idup); 7280 7281 readyState_ = CLOSED; 7282 7283 unregisterActiveSocket(this); 7284 break; 7285 case WebSocketOpcode.ping: 7286 // import std.stdio; writeln("ping received ", m.data); 7287 pong(m.data); 7288 break; 7289 case WebSocketOpcode.pong: 7290 // import std.stdio; writeln("pong received ", m.data); 7291 // just really references it is still alive, nbd. 7292 break; 7293 default: // ignore though i could and perhaps should throw too 7294 } 7295 } 7296 7297 if(d.length) { 7298 m.data = m.data.dup(); 7299 } 7300 7301 import core.stdc.string; 7302 memmove(receiveBuffer.ptr, d.ptr, d.length); 7303 receiveBufferUsedLength = d.length; 7304 7305 return m; 7306 } 7307 7308 private void autoprocess() { 7309 // FIXME 7310 do { 7311 processOnce(); 7312 } while(lowLevelReceive()); 7313 } 7314 7315 /++ 7316 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. 7317 7318 $(PITFALL 7319 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. 7320 ) 7321 7322 History: 7323 Added March 19, 2023 (dub v11.0). 7324 +/ 7325 static struct CloseEvent { 7326 ushort code; 7327 const(char)[] reason; 7328 bool wasClean; 7329 7330 string extendedErrorInformationUnstable; 7331 7332 /++ 7333 See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 for details. 7334 +/ 7335 enum StandardCloseCodes { 7336 purposeFulfilled = 1000, 7337 goingAway = 1001, 7338 protocolError = 1002, 7339 unacceptableData = 1003, // e.g. got text message when you can only handle binary 7340 Reserved = 1004, 7341 noStatusCodePresent = 1005, // not set by endpoint. 7342 abnormalClosure = 1006, // not set by endpoint. closed without a Close control. FIXME: maybe keep a copy of errno around for these 7343 inconsistentData = 1007, // e.g. utf8 validation failed 7344 genericPolicyViolation = 1008, 7345 messageTooBig = 1009, 7346 clientRequiredExtensionMissing = 1010, // only the client should send this 7347 unnexpectedCondition = 1011, 7348 unverifiedCertificate = 1015, // not set by client 7349 } 7350 } 7351 7352 /++ 7353 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. 7354 7355 History: 7356 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. 7357 7358 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. 7359 +/ 7360 arsd.core.FlexibleDelegate!(void delegate(CloseEvent event)) onclose; 7361 void delegate() onerror; /// 7362 void delegate(in char[]) ontextmessage; /// 7363 void delegate(in ubyte[]) onbinarymessage; /// 7364 void delegate() onopen; /// 7365 7366 /++ 7367 7368 +/ 7369 /// Group: browser_api 7370 void onmessage(void delegate(in char[]) dg) { 7371 ontextmessage = dg; 7372 } 7373 7374 /// ditto 7375 void onmessage(void delegate(in ubyte[]) dg) { 7376 onbinarymessage = dg; 7377 } 7378 7379 /* } end copy/paste */ 7380 7381 7382 7383 } 7384 7385 /++ 7386 Returns true if the request headers are asking for a websocket upgrade. 7387 7388 If this returns true, and you want to accept it, call [acceptWebsocket]. 7389 +/ 7390 bool websocketRequested(Cgi cgi) { 7391 return 7392 "sec-websocket-key" in cgi.requestHeaders 7393 && 7394 "connection" in cgi.requestHeaders && 7395 cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade") 7396 && 7397 "upgrade" in cgi.requestHeaders && 7398 cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket") 7399 ; 7400 } 7401 7402 /++ 7403 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. 7404 +/ 7405 WebSocket acceptWebsocket(Cgi cgi) { 7406 assert(!cgi.closed); 7407 assert(!cgi.outputtedResponseData); 7408 cgi.setResponseStatus("101 Switching Protocols"); 7409 cgi.header("Upgrade: WebSocket"); 7410 cgi.header("Connection: upgrade"); 7411 7412 string key = cgi.requestHeaders["sec-websocket-key"]; 7413 key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec 7414 7415 import std.digest.sha; 7416 auto hash = sha1Of(key); 7417 auto accept = Base64.encode(hash); 7418 7419 cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup); 7420 7421 cgi.websocketMode = true; 7422 cgi.write(""); 7423 7424 cgi.flush(); 7425 7426 auto ws = new WebSocket(cgi); 7427 ws.readyState_ = WebSocket.OPEN; 7428 return ws; 7429 } 7430 7431 // FIXME get websocket to work on other modes, not just embedded_httpd 7432 7433 /* copy/paste in http2.d { */ 7434 enum WebSocketOpcode : ubyte { 7435 continuation = 0, 7436 text = 1, 7437 binary = 2, 7438 // 3, 4, 5, 6, 7 RESERVED 7439 close = 8, 7440 ping = 9, 7441 pong = 10, 7442 // 11,12,13,14,15 RESERVED 7443 } 7444 7445 public struct WebSocketFrame { 7446 private bool populated; 7447 bool fin; 7448 bool rsv1; 7449 bool rsv2; 7450 bool rsv3; 7451 WebSocketOpcode opcode; // 4 bits 7452 bool masked; 7453 ubyte lengthIndicator; // don't set this when building one to send 7454 ulong realLength; // don't use when sending 7455 ubyte[4] maskingKey; // don't set this when sending 7456 ubyte[] data; 7457 7458 static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) { 7459 WebSocketFrame msg; 7460 msg.fin = true; 7461 msg.opcode = opcode; 7462 msg.data = cast(ubyte[]) data.dup; 7463 7464 return msg; 7465 } 7466 7467 private void send(scope void delegate(ubyte[]) llsend) { 7468 ubyte[64] headerScratch; 7469 int headerScratchPos = 0; 7470 7471 realLength = data.length; 7472 7473 { 7474 ubyte b1; 7475 b1 |= cast(ubyte) opcode; 7476 b1 |= rsv3 ? (1 << 4) : 0; 7477 b1 |= rsv2 ? (1 << 5) : 0; 7478 b1 |= rsv1 ? (1 << 6) : 0; 7479 b1 |= fin ? (1 << 7) : 0; 7480 7481 headerScratch[0] = b1; 7482 headerScratchPos++; 7483 } 7484 7485 { 7486 headerScratchPos++; // we'll set header[1] at the end of this 7487 auto rlc = realLength; 7488 ubyte b2; 7489 b2 |= masked ? (1 << 7) : 0; 7490 7491 assert(headerScratchPos == 2); 7492 7493 if(realLength > 65535) { 7494 // use 64 bit length 7495 b2 |= 0x7f; 7496 7497 // FIXME: double check endinaness 7498 foreach(i; 0 .. 8) { 7499 headerScratch[2 + 7 - i] = rlc & 0x0ff; 7500 rlc >>>= 8; 7501 } 7502 7503 headerScratchPos += 8; 7504 } else if(realLength > 125) { 7505 // use 16 bit length 7506 b2 |= 0x7e; 7507 7508 // FIXME: double check endinaness 7509 foreach(i; 0 .. 2) { 7510 headerScratch[2 + 1 - i] = rlc & 0x0ff; 7511 rlc >>>= 8; 7512 } 7513 7514 headerScratchPos += 2; 7515 } else { 7516 // use 7 bit length 7517 b2 |= realLength & 0b_0111_1111; 7518 } 7519 7520 headerScratch[1] = b2; 7521 } 7522 7523 //assert(!masked, "masking key not properly implemented"); 7524 if(masked) { 7525 // FIXME: randomize this 7526 headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; 7527 headerScratchPos += 4; 7528 7529 // we'll just mask it in place... 7530 int keyIdx = 0; 7531 foreach(i; 0 .. data.length) { 7532 data[i] = data[i] ^ maskingKey[keyIdx]; 7533 if(keyIdx == 3) 7534 keyIdx = 0; 7535 else 7536 keyIdx++; 7537 } 7538 } 7539 7540 //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); 7541 llsend(headerScratch[0 .. headerScratchPos]); 7542 llsend(data); 7543 } 7544 7545 static WebSocketFrame read(ref ubyte[] d) { 7546 WebSocketFrame msg; 7547 7548 auto orig = d; 7549 7550 WebSocketFrame needsMoreData() { 7551 d = orig; 7552 return WebSocketFrame.init; 7553 } 7554 7555 if(d.length < 2) 7556 return needsMoreData(); 7557 7558 ubyte b = d[0]; 7559 7560 msg.populated = true; 7561 7562 msg.opcode = cast(WebSocketOpcode) (b & 0x0f); 7563 b >>= 4; 7564 msg.rsv3 = b & 0x01; 7565 b >>= 1; 7566 msg.rsv2 = b & 0x01; 7567 b >>= 1; 7568 msg.rsv1 = b & 0x01; 7569 b >>= 1; 7570 msg.fin = b & 0x01; 7571 7572 b = d[1]; 7573 msg.masked = (b & 0b1000_0000) ? true : false; 7574 msg.lengthIndicator = b & 0b0111_1111; 7575 7576 d = d[2 .. $]; 7577 7578 if(msg.lengthIndicator == 0x7e) { 7579 // 16 bit length 7580 msg.realLength = 0; 7581 7582 if(d.length < 2) return needsMoreData(); 7583 7584 foreach(i; 0 .. 2) { 7585 msg.realLength |= d[0] << ((1-i) * 8); 7586 d = d[1 .. $]; 7587 } 7588 } else if(msg.lengthIndicator == 0x7f) { 7589 // 64 bit length 7590 msg.realLength = 0; 7591 7592 if(d.length < 8) return needsMoreData(); 7593 7594 foreach(i; 0 .. 8) { 7595 msg.realLength |= ulong(d[0]) << ((7-i) * 8); 7596 d = d[1 .. $]; 7597 } 7598 } else { 7599 // 7 bit length 7600 msg.realLength = msg.lengthIndicator; 7601 } 7602 7603 if(msg.masked) { 7604 7605 if(d.length < 4) return needsMoreData(); 7606 7607 msg.maskingKey = d[0 .. 4]; 7608 d = d[4 .. $]; 7609 } 7610 7611 if(msg.realLength > d.length) { 7612 return needsMoreData(); 7613 } 7614 7615 msg.data = d[0 .. cast(size_t) msg.realLength]; 7616 d = d[cast(size_t) msg.realLength .. $]; 7617 7618 return msg; 7619 } 7620 7621 void unmaskInPlace() { 7622 if(this.masked) { 7623 int keyIdx = 0; 7624 foreach(i; 0 .. this.data.length) { 7625 this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; 7626 if(keyIdx == 3) 7627 keyIdx = 0; 7628 else 7629 keyIdx++; 7630 } 7631 } 7632 } 7633 7634 char[] textData() { 7635 return cast(char[]) data; 7636 } 7637 } 7638 /* } */ 7639 } 7640 7641 7642 version(Windows) 7643 { 7644 version(CRuntime_DigitalMars) 7645 { 7646 extern(C) int setmode(int, int) nothrow @nogc; 7647 } 7648 else version(CRuntime_Microsoft) 7649 { 7650 extern(C) int _setmode(int, int) nothrow @nogc; 7651 alias setmode = _setmode; 7652 } 7653 else static assert(0); 7654 } 7655 7656 version(Posix) { 7657 import core.sys.posix.unistd; 7658 version(CRuntime_Musl) {} else { 7659 private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**); 7660 } 7661 } 7662 7663 7664 // FIXME: these aren't quite public yet. 7665 //private: 7666 7667 // template for laziness 7668 void startAddonServer()(string arg) { 7669 version(OSX) { 7670 assert(0, "Not implemented"); 7671 } else version(linux) { 7672 import core.sys.posix.unistd; 7673 pid_t pid; 7674 const(char)*[16] args; 7675 args[0] = "ARSD_CGI_ADDON_SERVER"; 7676 args[1] = arg.ptr; 7677 posix_spawn(&pid, "/proc/self/exe", 7678 null, 7679 null, 7680 args.ptr, 7681 null // env 7682 ); 7683 } else version(Windows) { 7684 wchar[2048] filename; 7685 auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length); 7686 if(len == 0 || len == filename.length) 7687 throw new Exception("could not get process name to start helper server"); 7688 7689 STARTUPINFOW startupInfo; 7690 startupInfo.cb = cast(DWORD) startupInfo.sizeof; 7691 PROCESS_INFORMATION processInfo; 7692 7693 import std.utf; 7694 7695 // I *MIGHT* need to run it as a new job or a service... 7696 auto ret = CreateProcessW( 7697 filename.ptr, 7698 toUTF16z(arg), 7699 null, // process attributes 7700 null, // thread attributes 7701 false, // inherit handles 7702 0, // creation flags 7703 null, // environment 7704 null, // working directory 7705 &startupInfo, 7706 &processInfo 7707 ); 7708 7709 if(!ret) 7710 throw new Exception("create process failed"); 7711 7712 // when done with those, if we set them 7713 /* 7714 CloseHandle(hStdInput); 7715 CloseHandle(hStdOutput); 7716 CloseHandle(hStdError); 7717 */ 7718 7719 } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)"); 7720 } 7721 7722 // template for laziness 7723 /* 7724 The websocket server is a single-process, single-thread, event 7725 I/O thing. It is passed websockets from other CGI processes 7726 and is then responsible for handling their messages and responses. 7727 Note that the CGI process is responsible for websocket setup, 7728 including authentication, etc. 7729 7730 It also gets data sent to it by other processes and is responsible 7731 for distributing that, as necessary. 7732 */ 7733 void runWebsocketServer()() { 7734 assert(0, "not implemented"); 7735 } 7736 7737 void sendToWebsocketServer(WebSocket ws, string group) { 7738 assert(0, "not implemented"); 7739 } 7740 7741 void sendToWebsocketServer(string content, string group) { 7742 assert(0, "not implemented"); 7743 } 7744 7745 7746 void runEventServer()() { 7747 runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation()); 7748 } 7749 7750 void runTimerServer()() { 7751 runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation()); 7752 } 7753 7754 version(Posix) { 7755 alias LocalServerConnectionHandle = int; 7756 alias CgiConnectionHandle = int; 7757 alias SocketConnectionHandle = int; 7758 7759 enum INVALID_CGI_CONNECTION_HANDLE = -1; 7760 } else version(Windows) { 7761 alias LocalServerConnectionHandle = HANDLE; 7762 version(embedded_httpd_threads) { 7763 alias CgiConnectionHandle = SOCKET; 7764 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 7765 } else version(fastcgi) { 7766 alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point. 7767 enum INVALID_CGI_CONNECTION_HANDLE = null; 7768 } else version(scgi) { 7769 alias CgiConnectionHandle = SOCKET; 7770 enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; 7771 } else { /* version(plain_cgi) */ 7772 alias CgiConnectionHandle = HANDLE; 7773 enum INVALID_CGI_CONNECTION_HANDLE = null; 7774 } 7775 alias SocketConnectionHandle = SOCKET; 7776 } 7777 7778 version(with_addon_servers_connections) 7779 LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) { 7780 version(Posix) { 7781 import core.sys.posix.unistd; 7782 import core.sys.posix.sys.un; 7783 7784 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 7785 if(sock == -1) 7786 throw new Exception("socket " ~ to!string(errno)); 7787 7788 scope(failure) 7789 close(sock); 7790 7791 cloexec(sock); 7792 7793 // add-on server processes are assumed to be local, and thus will 7794 // use unix domain sockets. Besides, I want to pass sockets to them, 7795 // so it basically must be local (except for the session server, but meh). 7796 sockaddr_un addr; 7797 addr.sun_family = AF_UNIX; 7798 version(linux) { 7799 // on linux, we will use the abstract namespace 7800 addr.sun_path[0] = 0; 7801 addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[]; 7802 } else { 7803 // but otherwise, just use a file cuz we must. 7804 addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[]; 7805 } 7806 7807 bool alreadyTried; 7808 7809 try_again: 7810 7811 if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { 7812 if(!alreadyTried && errno == ECONNREFUSED) { 7813 // try auto-spawning the server, then attempt connection again 7814 startAddonServer(arg); 7815 import core.thread; 7816 Thread.sleep(50.msecs); 7817 alreadyTried = true; 7818 goto try_again; 7819 } else 7820 throw new Exception("connect " ~ to!string(errno)); 7821 } 7822 7823 return sock; 7824 } else version(Windows) { 7825 return null; // FIXME 7826 } 7827 } 7828 7829 version(with_addon_servers_connections) 7830 void closeLocalServerConnection(LocalServerConnectionHandle handle) { 7831 version(Posix) { 7832 import core.sys.posix.unistd; 7833 close(handle); 7834 } else version(Windows) 7835 CloseHandle(handle); 7836 } 7837 7838 void runSessionServer()() { 7839 runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation()); 7840 } 7841 7842 import core.stdc.errno; 7843 7844 struct IoOp { 7845 @disable this(); 7846 @disable this(this); 7847 7848 /* 7849 So we want to be able to eventually handle generic sockets too. 7850 */ 7851 7852 enum Read = 1; 7853 enum Write = 2; 7854 enum Accept = 3; 7855 enum ReadSocketHandle = 4; 7856 7857 // Your handler may be called in a different thread than the one that initiated the IO request! 7858 // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution. 7859 private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed 7860 private void delegate(IoOp*) closeHandler; 7861 private void delegate(IoOp*) completeHandler; 7862 private int internalFd; 7863 private int operation; 7864 private int bufferLengthAllocated; 7865 private int bufferLengthUsed; 7866 private ubyte[1] internalBuffer; // it can be overallocated! 7867 7868 ubyte[] allocatedBuffer() return { 7869 return internalBuffer.ptr[0 .. bufferLengthAllocated]; 7870 } 7871 7872 ubyte[] usedBuffer() return { 7873 return allocatedBuffer[0 .. bufferLengthUsed]; 7874 } 7875 7876 void reset() { 7877 bufferLengthUsed = 0; 7878 } 7879 7880 int fd() { 7881 return internalFd; 7882 } 7883 } 7884 7885 IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) { 7886 import core.stdc.stdlib; 7887 7888 auto ptr = calloc(IoOp.sizeof + bufferSize, 1); 7889 if(ptr is null) 7890 assert(0); // out of memory! 7891 7892 auto op = cast(IoOp*) ptr; 7893 7894 op.handler = handler; 7895 op.internalFd = fd; 7896 op.operation = operation; 7897 op.bufferLengthAllocated = bufferSize; 7898 op.bufferLengthUsed = 0; 7899 7900 import core.memory; 7901 7902 GC.addRoot(ptr); 7903 7904 return op; 7905 } 7906 7907 void freeIoOp(ref IoOp* ptr) { 7908 7909 import core.memory; 7910 GC.removeRoot(ptr); 7911 7912 import core.stdc.stdlib; 7913 free(ptr); 7914 ptr = null; 7915 } 7916 7917 version(Posix) 7918 version(with_addon_servers_connections) 7919 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 7920 7921 //import std.stdio : writeln; writeln(cast(string) data); 7922 7923 import core.sys.posix.unistd; 7924 7925 auto ret = write(connection, data.ptr, data.length); 7926 if(ret != data.length) { 7927 if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { 7928 // the file is closed, remove it 7929 eis.fileClosed(connection); 7930 } else 7931 throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME 7932 } 7933 } 7934 version(Windows) 7935 version(with_addon_servers_connections) 7936 void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { 7937 // FIXME 7938 } 7939 7940 bool isInvalidHandle(CgiConnectionHandle h) { 7941 return h == INVALID_CGI_CONNECTION_HANDLE; 7942 } 7943 7944 /+ 7945 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv 7946 https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode 7947 https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive 7948 https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports 7949 https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport 7950 https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex 7951 https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects 7952 https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer 7953 https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call 7954 https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult 7955 7956 +/ 7957 7958 /++ 7959 You can customize your server by subclassing the appropriate server. Then, register your 7960 subclass at compile time with the [registerEventIoServer] template, or implement your own 7961 main function and call it yourself. 7962 7963 $(TIP If you make your subclass a `final class`, there is a slight performance improvement.) 7964 +/ 7965 version(with_addon_servers_connections) 7966 interface EventIoServer { 7967 bool handleLocalConnectionData(IoOp* op, int receivedFd); 7968 void handleLocalConnectionClose(IoOp* op); 7969 void handleLocalConnectionComplete(IoOp* op); 7970 void wait_timeout(); 7971 void fileClosed(int fd); 7972 7973 void epoll_fd(int fd); 7974 } 7975 7976 // the sink should buffer it 7977 private void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) { 7978 static if(is(T == struct)) { 7979 foreach(member; __traits(allMembers, T)) 7980 serialize(sink, __traits(getMember, t, member)); 7981 } else static if(is(T : int)) { 7982 // no need to think of endianness just because this is only used 7983 // for local, same-machine stuff anyway. thanks private lol 7984 sink((cast(ubyte*) &t)[0 .. t.sizeof]); 7985 } else static if(is(T == string) || is(T : const(ubyte)[])) { 7986 // these are common enough to optimize 7987 int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc. 7988 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7989 sink(cast(ubyte[]) t[]); 7990 } else static if(is(T : A[], A)) { 7991 // generic array is less optimal but still prolly ok 7992 int len = cast(int) t.length; 7993 sink((cast(ubyte*) &len)[0 .. int.sizeof]); 7994 foreach(item; t) 7995 serialize(sink, item); 7996 } else static assert(0, T.stringof); 7997 } 7998 7999 // all may be stack buffers, so use cautio 8000 private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) { 8001 static if(is(T == struct)) { 8002 T t; 8003 foreach(member; __traits(allMembers, T)) 8004 deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; }); 8005 dg(t); 8006 } else static if(is(T : int)) { 8007 // no need to think of endianness just because this is only used 8008 // for local, same-machine stuff anyway. thanks private lol 8009 T t; 8010 auto data = get(t.sizeof); 8011 t = (cast(T[]) data)[0]; 8012 dg(t); 8013 } else static if(is(T == string) || is(T : const(ubyte)[])) { 8014 // these are common enough to optimize 8015 int len; 8016 auto data = get(len.sizeof); 8017 len = (cast(int[]) data)[0]; 8018 8019 /* 8020 typeof(T[0])[2000] stackBuffer; 8021 T buffer; 8022 8023 if(len < stackBuffer.length) 8024 buffer = stackBuffer[0 .. len]; 8025 else 8026 buffer = new T(len); 8027 8028 data = get(len * typeof(T[0]).sizeof); 8029 */ 8030 8031 T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof); 8032 8033 dg(t); 8034 } else static if(is(T == E[], E)) { 8035 T t; 8036 int len; 8037 auto data = get(len.sizeof); 8038 len = (cast(int[]) data)[0]; 8039 t.length = len; 8040 foreach(ref e; t) { 8041 deserialize!E(get, (ele) { e = ele; }); 8042 } 8043 dg(t); 8044 } else static assert(0, T.stringof); 8045 } 8046 8047 unittest { 8048 serialize((ubyte[] b) { 8049 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); }); 8050 }, 1); 8051 serialize((ubyte[] b) { 8052 deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); }); 8053 }, 56674); 8054 ubyte[1000] buffer; 8055 int bufferPoint; 8056 void add(scope ubyte[] b) { 8057 buffer[bufferPoint .. bufferPoint + b.length] = b[]; 8058 bufferPoint += b.length; 8059 } 8060 ubyte[] get(int sz) { 8061 auto b = buffer[bufferPoint .. bufferPoint + sz]; 8062 bufferPoint += sz; 8063 return b; 8064 } 8065 serialize(&add, "test here"); 8066 bufferPoint = 0; 8067 deserialize!string(&get, (t) { assert(t == "test here"); }); 8068 bufferPoint = 0; 8069 8070 struct Foo { 8071 int a; 8072 ubyte c; 8073 string d; 8074 } 8075 serialize(&add, Foo(403, 37, "amazing")); 8076 bufferPoint = 0; 8077 deserialize!Foo(&get, (t) { 8078 assert(t.a == 403); 8079 assert(t.c == 37); 8080 assert(t.d == "amazing"); 8081 }); 8082 bufferPoint = 0; 8083 } 8084 8085 /* 8086 Here's the way the RPC interface works: 8087 8088 You define the interface that lists the functions you can call on the remote process. 8089 The interface may also have static methods for convenience. These forward to a singleton 8090 instance of an auto-generated class, which actually sends the args over the pipe. 8091 8092 An impl class actually implements it. A receiving server deserializes down the pipe and 8093 calls methods on the class. 8094 8095 I went with the interface to get some nice compiler checking and documentation stuff. 8096 8097 I could have skipped the interface and just implemented it all from the server class definition 8098 itself, but then the usage may call the method instead of rpcing it; I just like having the user 8099 interface and the implementation separate so you aren't tempted to `new impl` to call the methods. 8100 8101 8102 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. 8103 8104 Realistically though the bodies would just be 8105 connection.call(this.mangleof, args...) sooooo. 8106 8107 FIXME: overloads aren't supported 8108 */ 8109 8110 /// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this. 8111 interface SessionObject {} 8112 8113 private immutable void delegate(string[])[string] scheduledJobHandlers; 8114 private immutable void delegate(string[])[string] websocketServers; 8115 8116 version(with_breaking_cgi_features) 8117 mixin(q{ 8118 8119 mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) { 8120 static import std.traits; 8121 8122 // 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. 8123 static foreach(idx, member; __traits(derivedMembers, T)) { 8124 static if(__traits(isVirtualMethod, __traits(getMember, T, member))) 8125 mixin( q{ 8126 std.traits.ReturnType!(__traits(getMember, T, member)) 8127 } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params) 8128 { 8129 SerializationBuffer buffer; 8130 auto i = cast(ushort) idx; 8131 serialize(&buffer.sink, i); 8132 serialize(&buffer.sink, __traits(getMember, T, member).mangleof); 8133 foreach(param; params) 8134 serialize(&buffer.sink, param); 8135 8136 auto sendable = buffer.sendable; 8137 8138 version(Posix) {{ 8139 auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0); 8140 8141 if(ret == -1) { 8142 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 8143 } else if(ret == 0) { 8144 throw new Exception("Connection to addon server lost"); 8145 } if(ret < sendable.length) 8146 throw new Exception("Send failed to send all"); 8147 assert(ret == sendable.length); 8148 }} // FIXME Windows impl 8149 8150 static if(!is(typeof(return) == void)) { 8151 // there is a return value; we need to wait for it too 8152 version(Posix) { 8153 ubyte[3000] revBuffer; 8154 auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0); 8155 auto got = revBuffer[0 .. ret]; 8156 8157 int dataLocation; 8158 ubyte[] grab(int sz) { 8159 auto dataLocation1 = dataLocation; 8160 dataLocation += sz; 8161 return got[dataLocation1 .. dataLocation]; 8162 } 8163 8164 typeof(return) retu; 8165 deserialize!(typeof(return))(&grab, (a) { retu = a; }); 8166 return retu; 8167 } else { 8168 // FIXME Windows impl 8169 return typeof(return).init; 8170 } 8171 8172 } 8173 }}); 8174 } 8175 8176 private static typeof(this) singletonInstance; 8177 private LocalServerConnectionHandle connectionHandle; 8178 8179 static typeof(this) connection() { 8180 if(singletonInstance is null) { 8181 singletonInstance = new typeof(this)(); 8182 singletonInstance.connect(); 8183 } 8184 return singletonInstance; 8185 } 8186 8187 void connect() { 8188 connectionHandle = openLocalServerConnection(serverPath, cmdArg); 8189 } 8190 8191 void disconnect() { 8192 closeLocalServerConnection(connectionHandle); 8193 } 8194 } 8195 8196 void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) { 8197 ushort calledIdx; 8198 string calledFunction; 8199 8200 int dataLocation; 8201 ubyte[] grab(int sz) { 8202 if(sz == 0) assert(0); 8203 auto d = data[dataLocation .. dataLocation + sz]; 8204 dataLocation += sz; 8205 return d; 8206 } 8207 8208 again: 8209 8210 deserialize!ushort(&grab, (a) { calledIdx = a; }); 8211 deserialize!string(&grab, (a) { calledFunction = a; }); 8212 8213 import std.traits; 8214 8215 sw: switch(calledIdx) { 8216 foreach(idx, memberName; __traits(derivedMembers, Interface)) 8217 static if(__traits(isVirtualMethod, __traits(getMember, Interface, memberName))) { 8218 case idx: 8219 assert(calledFunction == __traits(getMember, Interface, memberName).mangleof); 8220 8221 Parameters!(__traits(getMember, Interface, memberName)) params; 8222 foreach(ref param; params) 8223 deserialize!(typeof(param))(&grab, (a) { param = a; }); 8224 8225 static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) { 8226 __traits(getMember, this_, memberName)(params); 8227 } else { 8228 auto ret = __traits(getMember, this_, memberName)(params); 8229 SerializationBuffer buffer; 8230 serialize(&buffer.sink, ret); 8231 8232 auto sendable = buffer.sendable; 8233 8234 version(Posix) { 8235 auto r = send(fd, sendable.ptr, sendable.length, 0); 8236 if(r == -1) { 8237 throw new Exception("send returned -1, errno: " ~ to!string(errno)); 8238 } else if(r == 0) { 8239 throw new Exception("Connection to addon client lost"); 8240 } if(r < sendable.length) 8241 throw new Exception("Send failed to send all"); 8242 8243 } // FIXME Windows impl 8244 } 8245 break sw; 8246 } 8247 default: assert(0); 8248 } 8249 8250 if(dataLocation != data.length) 8251 goto again; 8252 } 8253 8254 8255 private struct SerializationBuffer { 8256 ubyte[2048] bufferBacking; 8257 int bufferLocation; 8258 void sink(scope ubyte[] data) { 8259 bufferBacking[bufferLocation .. bufferLocation + data.length] = data[]; 8260 bufferLocation += data.length; 8261 } 8262 8263 ubyte[] sendable() return { 8264 return bufferBacking[0 .. bufferLocation]; 8265 } 8266 } 8267 8268 /* 8269 FIXME: 8270 add a version command line arg 8271 version data in the library 8272 management gui as external program 8273 8274 at server with event_fd for each run 8275 use .mangleof in the at function name 8276 8277 i think the at server will have to: 8278 pipe args to the child 8279 collect child output for logging 8280 get child return value for logging 8281 8282 on windows timers work differently. idk how to best combine with the io stuff. 8283 8284 will have to have dump and restore too, so i can restart without losing stuff. 8285 */ 8286 8287 /++ 8288 A convenience object for talking to the [BasicDataServer] from a higher level. 8289 See: [Cgi.getSessionObject]. 8290 8291 You pass it a `Data` struct describing the data you want saved in the session. 8292 Then, this class will generate getter and setter properties that allow access 8293 to that data. 8294 8295 Note that each load and store will be done as-accessed; it doesn't front-load 8296 mutable data nor does it batch updates out of fear of read-modify-write race 8297 conditions. (In fact, right now it does this for everything, but in the future, 8298 I might batch load `immutable` members of the Data struct.) 8299 8300 At some point in the future, I might also let it do different backends, like 8301 a client-side cookie store too, but idk. 8302 8303 Note that the plain-old-data members of your `Data` struct are wrapped by this 8304 interface via a static foreach to make property functions. 8305 8306 See_Also: [MockSession] 8307 +/ 8308 interface Session(Data) : SessionObject { 8309 @property string sessionId() const; 8310 8311 /++ 8312 Starts a new session. Note that a session is also 8313 implicitly started as soon as you write data to it, 8314 so if you need to alter these parameters from their 8315 defaults, be sure to explicitly call this BEFORE doing 8316 any writes to session data. 8317 8318 Params: 8319 idleLifetime = How long, in seconds, the session 8320 should remain in memory when not being read from 8321 or written to. The default is one day. 8322 8323 NOT IMPLEMENTED 8324 8325 useExtendedLifetimeCookie = The session ID is always 8326 stored in a HTTP cookie, and by default, that cookie 8327 is discarded when the user closes their browser. 8328 8329 But if you set this to true, it will use a non-perishable 8330 cookie for the given idleLifetime. 8331 8332 NOT IMPLEMENTED 8333 +/ 8334 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false); 8335 8336 /++ 8337 Regenerates the session ID and updates the associated 8338 cookie. 8339 8340 This is also your chance to change immutable data 8341 (not yet implemented). 8342 +/ 8343 void regenerateId(); 8344 8345 /++ 8346 Terminates this session, deleting all saved data. 8347 +/ 8348 void terminate(); 8349 8350 /++ 8351 Plain-old-data members of your `Data` struct are wrapped here via 8352 the property getters and setters. 8353 8354 If the member is a non-string array, it returns a magical array proxy 8355 object which allows for atomic appends and replaces via overloaded operators. 8356 You can slice this to get a range representing a $(B const) view of the array. 8357 This is to protect you against read-modify-write race conditions. 8358 +/ 8359 static foreach(memberName; __traits(allMembers, Data)) 8360 static if(is(typeof(__traits(getMember, Data, memberName)))) 8361 mixin(q{ 8362 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout; 8363 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value); 8364 }); 8365 8366 } 8367 8368 /++ 8369 An implementation of [Session] that works on real cgi connections utilizing the 8370 [BasicDataServer]. 8371 8372 As opposed to a [MockSession] which is made for testing purposes. 8373 8374 You will not construct one of these directly. See [Cgi.getSessionObject] instead. 8375 +/ 8376 class BasicDataServerSession(Data) : Session!Data { 8377 private Cgi cgi; 8378 private string sessionId_; 8379 8380 public @property string sessionId() const { 8381 return sessionId_; 8382 } 8383 8384 protected @property string sessionId(string s) { 8385 return this.sessionId_ = s; 8386 } 8387 8388 private this(Cgi cgi) { 8389 this.cgi = cgi; 8390 if(auto ptr = "sessionId" in cgi.cookies) 8391 sessionId = (*ptr).length ? *ptr : null; 8392 } 8393 8394 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { 8395 assert(sessionId is null); 8396 8397 // FIXME: what if there is a session ID cookie, but no corresponding session on the server? 8398 8399 import std.random, std.conv; 8400 sessionId = to!string(uniform(1, long.max)); 8401 8402 BasicDataServer.connection.createSession(sessionId, idleLifetime); 8403 setCookie(); 8404 } 8405 8406 protected void setCookie() { 8407 cgi.setCookie( 8408 "sessionId", sessionId, 8409 0 /* expiration */, 8410 "/" /* path */, 8411 null /* domain */, 8412 true /* http only */, 8413 cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); 8414 } 8415 8416 void regenerateId() { 8417 if(sessionId is null) { 8418 start(); 8419 return; 8420 } 8421 import std.random, std.conv; 8422 auto oldSessionId = sessionId; 8423 sessionId = to!string(uniform(1, long.max)); 8424 BasicDataServer.connection.renameSession(oldSessionId, sessionId); 8425 setCookie(); 8426 } 8427 8428 void terminate() { 8429 BasicDataServer.connection.destroySession(sessionId); 8430 sessionId = null; 8431 setCookie(); 8432 } 8433 8434 static foreach(memberName; __traits(allMembers, Data)) 8435 static if(is(typeof(__traits(getMember, Data, memberName)))) 8436 mixin(q{ 8437 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 8438 if(sessionId is null) 8439 return typeof(return).init; 8440 8441 import std.traits; 8442 auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName); 8443 if(v.length == 0) 8444 return typeof(return).init; 8445 import std.conv; 8446 // why this cast? to doesn't like being given an inout argument. so need to do it without that, then 8447 // we need to return it and that needed the cast. It should be fine since we basically respect constness.. 8448 // basically. Assuming the session is POD this should be fine. 8449 return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v); 8450 } 8451 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 8452 if(sessionId is null) 8453 start(); 8454 import std.conv; 8455 import std.traits; 8456 BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value)); 8457 return value; 8458 } 8459 }); 8460 } 8461 8462 /++ 8463 A mock object that works like the real session, but doesn't actually interact with any actual database or http connection. 8464 Simply stores the data in its instance members. 8465 +/ 8466 class MockSession(Data) : Session!Data { 8467 pure { 8468 @property string sessionId() const { return "mock"; } 8469 void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {} 8470 void regenerateId() {} 8471 void terminate() {} 8472 8473 private Data store_; 8474 8475 static foreach(memberName; __traits(allMembers, Data)) 8476 static if(is(typeof(__traits(getMember, Data, memberName)))) 8477 mixin(q{ 8478 @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { 8479 return __traits(getMember, store_, memberName); 8480 } 8481 @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { 8482 return __traits(getMember, store_, memberName) = value; 8483 } 8484 }); 8485 } 8486 } 8487 8488 /++ 8489 Direct interface to the basic data add-on server. You can 8490 typically use [Cgi.getSessionObject] as a more convenient interface. 8491 +/ 8492 version(with_addon_servers_connections) 8493 interface BasicDataServer { 8494 /// 8495 void createSession(string sessionId, int lifetime); 8496 /// 8497 void renewSession(string sessionId, int lifetime); 8498 /// 8499 void destroySession(string sessionId); 8500 /// 8501 void renameSession(string oldSessionId, string newSessionId); 8502 8503 /// 8504 void setSessionData(string sessionId, string dataKey, string dataValue); 8505 /// 8506 string getSessionData(string sessionId, string dataKey); 8507 8508 /// 8509 static BasicDataServerConnection connection() { 8510 return BasicDataServerConnection.connection(); 8511 } 8512 } 8513 8514 version(with_addon_servers_connections) 8515 class BasicDataServerConnection : BasicDataServer { 8516 mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server"); 8517 } 8518 8519 version(with_addon_servers) 8520 final class BasicDataServerImplementation : BasicDataServer, EventIoServer { 8521 8522 void createSession(string sessionId, int lifetime) { 8523 sessions[sessionId.idup] = Session(lifetime); 8524 } 8525 void destroySession(string sessionId) { 8526 sessions.remove(sessionId); 8527 } 8528 void renewSession(string sessionId, int lifetime) { 8529 sessions[sessionId].lifetime = lifetime; 8530 } 8531 void renameSession(string oldSessionId, string newSessionId) { 8532 sessions[newSessionId.idup] = sessions[oldSessionId]; 8533 sessions.remove(oldSessionId); 8534 } 8535 void setSessionData(string sessionId, string dataKey, string dataValue) { 8536 if(sessionId !in sessions) 8537 createSession(sessionId, 3600); // FIXME? 8538 sessions[sessionId].values[dataKey.idup] = dataValue.idup; 8539 } 8540 string getSessionData(string sessionId, string dataKey) { 8541 if(auto session = sessionId in sessions) { 8542 if(auto data = dataKey in (*session).values) 8543 return *data; 8544 else 8545 return null; // no such data 8546 8547 } else { 8548 return null; // no session 8549 } 8550 } 8551 8552 8553 protected: 8554 8555 struct Session { 8556 int lifetime; 8557 8558 string[string] values; 8559 } 8560 8561 Session[string] sessions; 8562 8563 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8564 auto data = op.usedBuffer; 8565 dispatchRpcServer!BasicDataServer(this, data, op.fd); 8566 return false; 8567 } 8568 8569 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 8570 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 8571 void wait_timeout() {} 8572 void fileClosed(int fd) {} // stateless so irrelevant 8573 void epoll_fd(int fd) {} 8574 } 8575 8576 /++ 8577 See [schedule] to make one of these. You then call one of the methods here to set it up: 8578 8579 --- 8580 schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC 8581 schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds 8582 schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it 8583 --- 8584 +/ 8585 version(with_addon_servers_connections) 8586 struct ScheduledJobHelper { 8587 private string func; 8588 private string[] args; 8589 private bool consumed; 8590 8591 private this(string func, string[] args) { 8592 this.func = func; 8593 this.args = args; 8594 } 8595 8596 ~this() { 8597 assert(consumed); 8598 } 8599 8600 /++ 8601 Schedules the job to be run at the given time. 8602 +/ 8603 void at(DateTime when, immutable TimeZone timezone = UTC()) { 8604 consumed = true; 8605 8606 auto conn = ScheduledJobServerConnection.connection; 8607 import std.file; 8608 auto st = SysTime(when, timezone); 8609 auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args); 8610 } 8611 8612 /++ 8613 Schedules the job to run at least after the specified delay. 8614 +/ 8615 void delay(Duration delay) { 8616 consumed = true; 8617 8618 auto conn = ScheduledJobServerConnection.connection; 8619 import std.file; 8620 auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args); 8621 } 8622 8623 /++ 8624 Runs the job in the background ASAP. 8625 8626 $(NOTE It may run in a background thread. Don't segfault!) 8627 +/ 8628 void asap() { 8629 consumed = true; 8630 8631 auto conn = ScheduledJobServerConnection.connection; 8632 import std.file; 8633 auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args); 8634 } 8635 8636 /+ 8637 /++ 8638 Schedules the job to recur on the given pattern. 8639 +/ 8640 void recur(string spec) { 8641 8642 } 8643 +/ 8644 } 8645 8646 /++ 8647 First step to schedule a job on the scheduled job server. 8648 8649 The scheduled job needs to be a top-level function that doesn't read any 8650 variables from outside its arguments because it may be run in a new process, 8651 without any context existing later. 8652 8653 You MUST set details on the returned object to actually do anything! 8654 +/ 8655 template schedule(alias fn, T...) if(is(typeof(fn) == function)) { 8656 /// 8657 ScheduledJobHelper schedule(T args) { 8658 // this isn't meant to ever be called, but instead just to 8659 // get the compiler to type check the arguments passed for us 8660 auto sample = delegate() { 8661 fn(args); 8662 }; 8663 string[] sargs; 8664 foreach(arg; args) 8665 sargs ~= to!string(arg); 8666 return ScheduledJobHelper(fn.mangleof, sargs); 8667 } 8668 8669 shared static this() { 8670 scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) { 8671 import std.traits; 8672 Parameters!fn args; 8673 foreach(idx, ref arg; args) 8674 arg = to!(typeof(arg))(sargs[idx]); 8675 fn(args); 8676 }; 8677 } 8678 } 8679 8680 /// 8681 interface ScheduledJobServer { 8682 /// Use the [schedule] function for a higher-level interface. 8683 int scheduleJob(int whenIs, int when, string executable, string func, string[] args); 8684 /// 8685 void cancelJob(int jobId); 8686 } 8687 8688 version(with_addon_servers_connections) 8689 class ScheduledJobServerConnection : ScheduledJobServer { 8690 mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server"); 8691 } 8692 8693 version(with_addon_servers) 8694 final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer { 8695 // FIXME: we need to handle SIGCHLD in this somehow 8696 // whenIs is 0 for relative, 1 for absolute 8697 protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) { 8698 auto nj = nextJobId; 8699 nextJobId++; 8700 8701 version(linux) { 8702 import core.sys.linux.timerfd; 8703 import core.sys.linux.epoll; 8704 import core.sys.posix.unistd; 8705 8706 8707 auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); 8708 if(fd == -1) 8709 throw new Exception("fd timer create failed"); 8710 8711 foreach(ref arg; args) 8712 arg = arg.idup; 8713 auto job = Job(executable.idup, func.idup, .dup(args), fd, nj); 8714 8715 itimerspec value; 8716 value.it_value.tv_sec = when; 8717 value.it_value.tv_nsec = 0; 8718 8719 value.it_interval.tv_sec = 0; 8720 value.it_interval.tv_nsec = 0; 8721 8722 if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1) 8723 throw new Exception("couldn't set fd timer"); 8724 8725 auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) { 8726 jobs.remove(nj); 8727 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null); 8728 close(fd); 8729 8730 8731 spawnProcess([job.executable, "--timed-job", job.func] ~ job.args); 8732 8733 return true; 8734 }); 8735 scope(failure) 8736 freeIoOp(op); 8737 8738 epoll_event ev; 8739 ev.events = EPOLLIN | EPOLLET; 8740 ev.data.ptr = op; 8741 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) 8742 throw new Exception("epoll_ctl " ~ to!string(errno)); 8743 8744 jobs[nj] = job; 8745 return nj; 8746 } else assert(0); 8747 } 8748 8749 protected void cancelJob(int jobId) { 8750 version(linux) { 8751 auto job = jobId in jobs; 8752 if(job is null) 8753 return; 8754 8755 jobs.remove(jobId); 8756 8757 version(linux) { 8758 import core.sys.linux.timerfd; 8759 import core.sys.linux.epoll; 8760 import core.sys.posix.unistd; 8761 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null); 8762 close(job.timerfd); 8763 } 8764 } 8765 jobs.remove(jobId); 8766 } 8767 8768 int nextJobId = 1; 8769 static struct Job { 8770 string executable; 8771 string func; 8772 string[] args; 8773 int timerfd; 8774 int id; 8775 } 8776 Job[int] jobs; 8777 8778 8779 // event io server methods below 8780 8781 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8782 auto data = op.usedBuffer; 8783 dispatchRpcServer!ScheduledJobServer(this, data, op.fd); 8784 return false; 8785 } 8786 8787 void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go 8788 void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant 8789 void wait_timeout() {} 8790 void fileClosed(int fd) {} // stateless so irrelevant 8791 8792 int epoll_fd_; 8793 void epoll_fd(int fd) {this.epoll_fd_ = fd; } 8794 int epoll_fd() { return epoll_fd_; } 8795 } 8796 8797 /++ 8798 History: 8799 Added January 6, 2019 8800 +/ 8801 version(with_addon_servers_connections) 8802 interface EventSourceServer { 8803 /++ 8804 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. 8805 8806 See_Also: 8807 [sendEvent] 8808 8809 Bugs: 8810 Not implemented on Windows! 8811 8812 History: 8813 Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design. 8814 +/ 8815 public static void adoptConnection(Cgi cgi, in char[] eventUrl) { 8816 /* 8817 If lastEventId is missing or empty, you just get new events as they come. 8818 8819 If it is set from something else, it sends all since then (that are still alive) 8820 down the pipe immediately. 8821 8822 The reason it can come from the header is that's what the standard defines for 8823 browser reconnects. The reason it can come from a query string is just convenience 8824 in catching up in a user-defined manner. 8825 8826 The reason the header overrides the query string is if the browser tries to reconnect, 8827 it will send the header AND the query (it reconnects to the same url), so we just 8828 want to do the restart thing. 8829 8830 Note that if you ask for "0" as the lastEventId, it will get ALL still living events. 8831 */ 8832 string lastEventId = cgi.lastEventId; 8833 if(lastEventId.length == 0 && "lastEventId" in cgi.get) 8834 lastEventId = cgi.get["lastEventId"]; 8835 8836 cgi.setResponseContentType("text/event-stream"); 8837 cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later 8838 cgi.flush(); 8839 8840 cgi.closed = true; 8841 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 8842 scope(exit) 8843 closeLocalServerConnection(s); 8844 8845 version(fastcgi) 8846 throw new Exception("sending fcgi connections not supported"); 8847 else { 8848 auto fd = cgi.getOutputFileHandle(); 8849 if(isInvalidHandle(fd)) 8850 throw new Exception("bad fd from cgi!"); 8851 8852 EventSourceServerImplementation.SendableEventConnection sec; 8853 sec.populate(cgi.responseChunked, eventUrl, lastEventId); 8854 8855 version(Posix) { 8856 auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd); 8857 assert(res == sec.sizeof); 8858 } else version(Windows) { 8859 // FIXME 8860 } 8861 } 8862 } 8863 8864 /++ 8865 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. 8866 8867 Params: 8868 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. 8869 event = the event type string, which is used in the Javascript addEventListener API on EventSource 8870 data = the event data. Available in JS as `event.data`. 8871 lifetime = the amount of time to keep this event for replaying on the event server. 8872 8873 Bugs: 8874 Not implemented on Windows! 8875 8876 History: 8877 Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design. 8878 +/ 8879 public static void sendEvent(string url, string event, string data, int lifetime) { 8880 auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); 8881 scope(exit) 8882 closeLocalServerConnection(s); 8883 8884 EventSourceServerImplementation.SendableEvent sev; 8885 sev.populate(url, event, data, lifetime); 8886 8887 version(Posix) { 8888 auto ret = send(s, &sev, sev.sizeof, 0); 8889 assert(ret == sev.sizeof); 8890 } else version(Windows) { 8891 // FIXME 8892 } 8893 } 8894 8895 /++ 8896 Messages sent to `url` will also be sent to anyone listening on `forwardUrl`. 8897 8898 See_Also: [disconnect] 8899 +/ 8900 void connect(string url, string forwardUrl); 8901 8902 /++ 8903 Disconnects `forwardUrl` from `url` 8904 8905 See_Also: [connect] 8906 +/ 8907 void disconnect(string url, string forwardUrl); 8908 } 8909 8910 /// 8911 version(with_addon_servers) 8912 final class EventSourceServerImplementation : EventSourceServer, EventIoServer { 8913 8914 protected: 8915 8916 void connect(string url, string forwardUrl) { 8917 pipes[url] ~= forwardUrl; 8918 } 8919 void disconnect(string url, string forwardUrl) { 8920 auto t = url in pipes; 8921 if(t is null) 8922 return; 8923 foreach(idx, n; (*t)) 8924 if(n == forwardUrl) { 8925 (*t)[idx] = (*t)[$-1]; 8926 (*t) = (*t)[0 .. $-1]; 8927 break; 8928 } 8929 } 8930 8931 bool handleLocalConnectionData(IoOp* op, int receivedFd) { 8932 if(receivedFd != -1) { 8933 //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer); 8934 8935 //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5); 8936 8937 SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr; 8938 8939 auto url = got.url.idup; 8940 eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false); 8941 8942 // FIXME: catch up on past messages here 8943 } else { 8944 auto data = op.usedBuffer; 8945 auto event = cast(SendableEvent*) data.ptr; 8946 8947 if(event.magic == 0xdeadbeef) { 8948 handleInputEvent(event); 8949 8950 if(event.url in pipes) 8951 foreach(pipe; pipes[event.url]) { 8952 event.url = pipe; 8953 handleInputEvent(event); 8954 } 8955 } else { 8956 dispatchRpcServer!EventSourceServer(this, data, op.fd); 8957 } 8958 } 8959 return false; 8960 } 8961 void handleLocalConnectionClose(IoOp* op) { 8962 fileClosed(op.fd); 8963 } 8964 void handleLocalConnectionComplete(IoOp* op) {} 8965 8966 void wait_timeout() { 8967 // just keeping alive 8968 foreach(url, connections; eventConnectionsByUrl) 8969 foreach(connection; connections) 8970 if(connection.needsChunking) 8971 nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); 8972 else 8973 nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); 8974 } 8975 8976 void fileClosed(int fd) { 8977 outer: foreach(url, ref connections; eventConnectionsByUrl) { 8978 foreach(idx, conn; connections) { 8979 if(fd == conn.fd) { 8980 connections[idx] = connections[$-1]; 8981 connections = connections[0 .. $ - 1]; 8982 continue outer; 8983 } 8984 } 8985 } 8986 } 8987 8988 void epoll_fd(int fd) {} 8989 8990 8991 private: 8992 8993 8994 struct SendableEventConnection { 8995 ubyte responseChunked; 8996 8997 int urlLength; 8998 char[256] urlBuffer = 0; 8999 9000 int lastEventIdLength; 9001 char[32] lastEventIdBuffer = 0; 9002 9003 char[] url() return { 9004 return urlBuffer[0 .. urlLength]; 9005 } 9006 void url(in char[] u) { 9007 urlBuffer[0 .. u.length] = u[]; 9008 urlLength = cast(int) u.length; 9009 } 9010 char[] lastEventId() return { 9011 return lastEventIdBuffer[0 .. lastEventIdLength]; 9012 } 9013 void populate(bool responseChunked, in char[] url, in char[] lastEventId) 9014 in { 9015 assert(url.length < this.urlBuffer.length); 9016 assert(lastEventId.length < this.lastEventIdBuffer.length); 9017 } 9018 do { 9019 this.responseChunked = responseChunked ? 1 : 0; 9020 this.urlLength = cast(int) url.length; 9021 this.lastEventIdLength = cast(int) lastEventId.length; 9022 9023 this.urlBuffer[0 .. url.length] = url[]; 9024 this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[]; 9025 } 9026 } 9027 9028 struct SendableEvent { 9029 int magic = 0xdeadbeef; 9030 int urlLength; 9031 char[256] urlBuffer = 0; 9032 int typeLength; 9033 char[32] typeBuffer = 0; 9034 int messageLength; 9035 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. 9036 int _lifetime; 9037 9038 char[] message() return { 9039 return messageBuffer[0 .. messageLength]; 9040 } 9041 char[] type() return { 9042 return typeBuffer[0 .. typeLength]; 9043 } 9044 char[] url() return { 9045 return urlBuffer[0 .. urlLength]; 9046 } 9047 void url(in char[] u) { 9048 urlBuffer[0 .. u.length] = u[]; 9049 urlLength = cast(int) u.length; 9050 } 9051 int lifetime() { 9052 return _lifetime; 9053 } 9054 9055 /// 9056 void populate(string url, string type, string message, int lifetime) 9057 in { 9058 assert(url.length < this.urlBuffer.length); 9059 assert(type.length < this.typeBuffer.length); 9060 assert(message.length < this.messageBuffer.length); 9061 } 9062 do { 9063 this.urlLength = cast(int) url.length; 9064 this.typeLength = cast(int) type.length; 9065 this.messageLength = cast(int) message.length; 9066 this._lifetime = lifetime; 9067 9068 this.urlBuffer[0 .. url.length] = url[]; 9069 this.typeBuffer[0 .. type.length] = type[]; 9070 this.messageBuffer[0 .. message.length] = message[]; 9071 } 9072 } 9073 9074 struct EventConnection { 9075 int fd; 9076 bool needsChunking; 9077 } 9078 9079 private EventConnection[][string] eventConnectionsByUrl; 9080 private string[][string] pipes; 9081 9082 private void handleInputEvent(scope SendableEvent* event) { 9083 static int eventId; 9084 9085 static struct StoredEvent { 9086 int id; 9087 string type; 9088 string message; 9089 int lifetimeRemaining; 9090 } 9091 9092 StoredEvent[][string] byUrl; 9093 9094 int thisId = ++eventId; 9095 9096 if(event.lifetime) 9097 byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime); 9098 9099 auto connectionsPtr = event.url in eventConnectionsByUrl; 9100 EventConnection[] connections; 9101 if(connectionsPtr is null) 9102 return; 9103 else 9104 connections = *connectionsPtr; 9105 9106 char[4096] buffer; 9107 char[] formattedMessage; 9108 9109 void append(const char[] a) { 9110 // the 6's here are to leave room for a HTTP chunk header, if it proves necessary 9111 buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[]; 9112 formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length]; 9113 } 9114 9115 import std.algorithm.iteration; 9116 9117 if(connections.length) { 9118 append("id: "); 9119 append(to!string(thisId)); 9120 append("\n"); 9121 9122 append("event: "); 9123 append(event.type); 9124 append("\n"); 9125 9126 foreach(line; event.message.splitter("\n")) { 9127 append("data: "); 9128 append(line); 9129 append("\n"); 9130 } 9131 9132 append("\n"); 9133 } 9134 9135 // chunk it for HTTP! 9136 auto len = toHex(formattedMessage.length); 9137 buffer[4 .. 6] = "\r\n"[]; 9138 buffer[4 - len.length .. 4] = len[]; 9139 buffer[6 + formattedMessage.length] = '\r'; 9140 buffer[6 + formattedMessage.length + 1] = '\n'; 9141 9142 auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2]; 9143 // done 9144 9145 // FIXME: send back requests when needed 9146 // FIXME: send a single ":\n" every 15 seconds to keep alive 9147 9148 foreach(connection; connections) { 9149 if(connection.needsChunking) { 9150 nonBlockingWrite(this, connection.fd, chunkedMessage); 9151 } else { 9152 nonBlockingWrite(this, connection.fd, formattedMessage); 9153 } 9154 } 9155 } 9156 } 9157 9158 void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) { 9159 version(Posix) { 9160 9161 import core.sys.posix.unistd; 9162 import core.sys.posix.fcntl; 9163 import core.sys.posix.sys.un; 9164 9165 import core.sys.posix.signal; 9166 signal(SIGPIPE, SIG_IGN); 9167 9168 static extern(C) void sigchldhandler(int) { 9169 int status; 9170 import w = core.sys.posix.sys.wait; 9171 w.wait(&status); 9172 } 9173 signal(SIGCHLD, &sigchldhandler); 9174 9175 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 9176 if(sock == -1) 9177 throw new Exception("socket " ~ to!string(errno)); 9178 9179 scope(failure) 9180 close(sock); 9181 9182 cloexec(sock); 9183 9184 // add-on server processes are assumed to be local, and thus will 9185 // use unix domain sockets. Besides, I want to pass sockets to them, 9186 // so it basically must be local (except for the session server, but meh). 9187 sockaddr_un addr; 9188 addr.sun_family = AF_UNIX; 9189 version(linux) { 9190 // on linux, we will use the abstract namespace 9191 addr.sun_path[0] = 0; 9192 addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[]; 9193 } else { 9194 // but otherwise, just use a file cuz we must. 9195 addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[]; 9196 } 9197 9198 if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) 9199 throw new Exception("bind " ~ to!string(errno)); 9200 9201 if(listen(sock, 128) == -1) 9202 throw new Exception("listen " ~ to!string(errno)); 9203 9204 makeNonBlocking(sock); 9205 9206 version(linux) { 9207 import core.sys.linux.epoll; 9208 auto epoll_fd = epoll_create1(EPOLL_CLOEXEC); 9209 if(epoll_fd == -1) 9210 throw new Exception("epoll_create1 " ~ to!string(errno)); 9211 scope(failure) 9212 close(epoll_fd); 9213 } else { 9214 import core.sys.posix.poll; 9215 } 9216 9217 version(linux) 9218 eis.epoll_fd = epoll_fd; 9219 9220 auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null); 9221 scope(exit) 9222 freeIoOp(acceptOp); 9223 9224 version(linux) { 9225 epoll_event ev; 9226 ev.events = EPOLLIN | EPOLLET; 9227 ev.data.ptr = acceptOp; 9228 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1) 9229 throw new Exception("epoll_ctl " ~ to!string(errno)); 9230 9231 epoll_event[64] events; 9232 } else { 9233 pollfd[] pollfds; 9234 IoOp*[int] ioops; 9235 pollfds ~= pollfd(sock, POLLIN); 9236 ioops[sock] = acceptOp; 9237 } 9238 9239 import core.time : MonoTime, seconds; 9240 9241 MonoTime timeout = MonoTime.currTime + 15.seconds; 9242 9243 while(true) { 9244 9245 // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently 9246 9247 int timeout_milliseconds = 0; // -1; // infinite 9248 9249 timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; 9250 if(timeout_milliseconds < 0) 9251 timeout_milliseconds = 0; 9252 9253 //writeln("waiting for ", name); 9254 9255 version(linux) { 9256 auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds); 9257 if(nfds == -1) { 9258 if(errno == EINTR) 9259 continue; 9260 throw new Exception("epoll_wait " ~ to!string(errno)); 9261 } 9262 } else { 9263 int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds); 9264 size_t lastIdx = 0; 9265 } 9266 9267 if(nfds == 0) { 9268 eis.wait_timeout(); 9269 timeout += 15.seconds; 9270 } 9271 9272 foreach(idx; 0 .. nfds) { 9273 version(linux) { 9274 auto flags = events[idx].events; 9275 auto ioop = cast(IoOp*) events[idx].data.ptr; 9276 } else { 9277 IoOp* ioop; 9278 foreach(tidx, thing; pollfds[lastIdx .. $]) { 9279 if(thing.revents) { 9280 ioop = ioops[thing.fd]; 9281 lastIdx += tidx + 1; 9282 break; 9283 } 9284 } 9285 } 9286 9287 //writeln(flags, " ", ioop.fd); 9288 9289 void newConnection() { 9290 // on edge triggering, it is important that we get it all 9291 while(true) { 9292 auto size = cast(socklen_t) addr.sizeof; 9293 auto ns = accept(sock, cast(sockaddr*) &addr, &size); 9294 if(ns == -1) { 9295 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9296 // all done, got it all 9297 break; 9298 } 9299 throw new Exception("accept " ~ to!string(errno)); 9300 } 9301 cloexec(ns); 9302 9303 makeNonBlocking(ns); 9304 auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData); 9305 niop.closeHandler = &eis.handleLocalConnectionClose; 9306 niop.completeHandler = &eis.handleLocalConnectionComplete; 9307 scope(failure) freeIoOp(niop); 9308 9309 version(linux) { 9310 epoll_event nev; 9311 nev.events = EPOLLIN | EPOLLET; 9312 nev.data.ptr = niop; 9313 if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1) 9314 throw new Exception("epoll_ctl " ~ to!string(errno)); 9315 } else { 9316 bool found = false; 9317 foreach(ref pfd; pollfds) { 9318 if(pfd.fd < 0) { 9319 pfd.fd = ns; 9320 found = true; 9321 } 9322 } 9323 if(!found) 9324 pollfds ~= pollfd(ns, POLLIN); 9325 ioops[ns] = niop; 9326 } 9327 } 9328 } 9329 9330 bool newConnectionCondition() { 9331 version(linux) 9332 return ioop.fd == sock && (flags & EPOLLIN); 9333 else 9334 return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN); 9335 } 9336 9337 if(newConnectionCondition()) { 9338 newConnection(); 9339 } else if(ioop.operation == IoOp.ReadSocketHandle) { 9340 while(true) { 9341 int in_fd; 9342 auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd); 9343 if(got == -1) { 9344 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9345 // all done, got it all 9346 if(ioop.completeHandler) 9347 ioop.completeHandler(ioop); 9348 break; 9349 } 9350 throw new Exception("recv " ~ to!string(errno)); 9351 } 9352 9353 if(got == 0) { 9354 if(ioop.closeHandler) { 9355 ioop.closeHandler(ioop); 9356 version(linux) {} // nothing needed 9357 else { 9358 foreach(ref pfd; pollfds) { 9359 if(pfd.fd == ioop.fd) 9360 pfd.fd = -1; 9361 } 9362 } 9363 } 9364 close(ioop.fd); 9365 freeIoOp(ioop); 9366 break; 9367 } 9368 9369 ioop.bufferLengthUsed = cast(int) got; 9370 ioop.handler(ioop, in_fd); 9371 } 9372 } else if(ioop.operation == IoOp.Read) { 9373 while(true) { 9374 auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length); 9375 if(got == -1) { 9376 if(errno == EAGAIN || errno == EWOULDBLOCK) { 9377 // all done, got it all 9378 if(ioop.completeHandler) 9379 ioop.completeHandler(ioop); 9380 break; 9381 } 9382 throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno)); 9383 } 9384 9385 if(got == 0) { 9386 if(ioop.closeHandler) 9387 ioop.closeHandler(ioop); 9388 close(ioop.fd); 9389 freeIoOp(ioop); 9390 break; 9391 } 9392 9393 ioop.bufferLengthUsed = cast(int) got; 9394 if(ioop.handler(ioop, ioop.fd)) { 9395 close(ioop.fd); 9396 freeIoOp(ioop); 9397 break; 9398 } 9399 } 9400 } 9401 9402 // EPOLLHUP? 9403 } 9404 } 9405 } else version(Windows) { 9406 9407 // set up a named pipe 9408 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx 9409 // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw 9410 // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid 9411 9412 } else static assert(0); 9413 } 9414 9415 9416 version(with_sendfd) 9417 // copied from the web and ported from C 9418 // see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t 9419 ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { 9420 msghdr msg; 9421 iovec[1] iov; 9422 9423 version(OSX) { 9424 //msg.msg_accrights = cast(cattr_t) &sendfd; 9425 //msg.msg_accrightslen = int.sizeof; 9426 } else version(Android) { 9427 } else { 9428 union ControlUnion { 9429 cmsghdr cm; 9430 char[CMSG_SPACE(int.sizeof)] control; 9431 } 9432 9433 ControlUnion control_un; 9434 cmsghdr* cmptr; 9435 9436 msg.msg_control = control_un.control.ptr; 9437 msg.msg_controllen = control_un.control.length; 9438 9439 cmptr = CMSG_FIRSTHDR(&msg); 9440 cmptr.cmsg_len = CMSG_LEN(int.sizeof); 9441 cmptr.cmsg_level = SOL_SOCKET; 9442 cmptr.cmsg_type = SCM_RIGHTS; 9443 *(cast(int *) CMSG_DATA(cmptr)) = sendfd; 9444 } 9445 9446 msg.msg_name = null; 9447 msg.msg_namelen = 0; 9448 9449 iov[0].iov_base = ptr; 9450 iov[0].iov_len = nbytes; 9451 msg.msg_iov = iov.ptr; 9452 msg.msg_iovlen = 1; 9453 9454 return sendmsg(fd, &msg, 0); 9455 } 9456 9457 version(with_sendfd) 9458 // copied from the web and ported from C 9459 ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { 9460 msghdr msg; 9461 iovec[1] iov; 9462 ssize_t n; 9463 int newfd; 9464 9465 version(OSX) { 9466 //msg.msg_accrights = cast(cattr_t) recvfd; 9467 //msg.msg_accrightslen = int.sizeof; 9468 } else version(Android) { 9469 } else { 9470 union ControlUnion { 9471 cmsghdr cm; 9472 char[CMSG_SPACE(int.sizeof)] control; 9473 } 9474 ControlUnion control_un; 9475 cmsghdr* cmptr; 9476 9477 msg.msg_control = control_un.control.ptr; 9478 msg.msg_controllen = control_un.control.length; 9479 } 9480 9481 msg.msg_name = null; 9482 msg.msg_namelen = 0; 9483 9484 iov[0].iov_base = ptr; 9485 iov[0].iov_len = nbytes; 9486 msg.msg_iov = iov.ptr; 9487 msg.msg_iovlen = 1; 9488 9489 if ( (n = recvmsg(fd, &msg, 0)) <= 0) 9490 return n; 9491 9492 version(OSX) { 9493 //if(msg.msg_accrightslen != int.sizeof) 9494 //*recvfd = -1; 9495 } else version(Android) { 9496 } else { 9497 if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && 9498 cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { 9499 if (cmptr.cmsg_level != SOL_SOCKET) 9500 throw new Exception("control level != SOL_SOCKET"); 9501 if (cmptr.cmsg_type != SCM_RIGHTS) 9502 throw new Exception("control type != SCM_RIGHTS"); 9503 *recvfd = *(cast(int *) CMSG_DATA(cmptr)); 9504 } else 9505 *recvfd = -1; /* descriptor was not passed */ 9506 } 9507 9508 return n; 9509 } 9510 /* end read_fd */ 9511 9512 9513 /* 9514 Event source stuff 9515 9516 The api is: 9517 9518 sendEvent(string url, string type, string data, int timeout = 60*10); 9519 9520 attachEventListener(string url, int fd, lastId) 9521 9522 9523 It just sends to all attached listeners, and stores it until the timeout 9524 for replaying via lastEventId. 9525 */ 9526 9527 /* 9528 Session process stuff 9529 9530 it stores it all. the cgi object has a session object that can grab it 9531 9532 session may be done in the same process if possible, there is a version 9533 switch to choose if you want to override. 9534 */ 9535 9536 struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; 9537 alias handler = dispatchHandler; 9538 string urlPrefix; 9539 bool rejectFurther; 9540 immutable(DispatcherDetails) details; 9541 } 9542 9543 private string urlify(string name) pure { 9544 return beautify(name, '-', true); 9545 } 9546 9547 private string beautify(string name, char space = ' ', bool allLowerCase = false) pure { 9548 if(name == "id") 9549 return allLowerCase ? name : "ID"; 9550 9551 char[160] buffer; 9552 int bufferIndex = 0; 9553 bool shouldCap = true; 9554 bool shouldSpace; 9555 bool lastWasCap; 9556 foreach(idx, char ch; name) { 9557 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9558 9559 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 9560 if(lastWasCap) { 9561 // two caps in a row, don't change. Prolly acronym. 9562 } else { 9563 if(idx) 9564 shouldSpace = true; // new word, add space 9565 } 9566 9567 lastWasCap = true; 9568 } else { 9569 lastWasCap = false; 9570 } 9571 9572 if(shouldSpace) { 9573 buffer[bufferIndex++] = space; 9574 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 9575 shouldSpace = false; 9576 } 9577 if(shouldCap) { 9578 if(ch >= 'a' && ch <= 'z') 9579 ch -= 32; 9580 shouldCap = false; 9581 } 9582 if(allLowerCase && ch >= 'A' && ch <= 'Z') 9583 ch += 32; 9584 buffer[bufferIndex++] = ch; 9585 } 9586 return buffer[0 .. bufferIndex].idup; 9587 } 9588 9589 /* 9590 string urlFor(alias func)() { 9591 return __traits(identifier, func); 9592 } 9593 */ 9594 9595 /++ 9596 UDA: The name displayed to the user in auto-generated HTML. 9597 9598 Default is `beautify(identifier)`. 9599 +/ 9600 struct DisplayName { 9601 string name; 9602 } 9603 9604 /++ 9605 UDA: The name used in the URL or web parameter. 9606 9607 Default is `urlify(identifier)` for functions and `identifier` for parameters and data members. 9608 +/ 9609 struct UrlName { 9610 string name; 9611 } 9612 9613 /++ 9614 UDA: default format to respond for this method 9615 +/ 9616 struct DefaultFormat { string value; } 9617 9618 class MissingArgumentException : Exception { 9619 string functionName; 9620 string argumentName; 9621 string argumentType; 9622 9623 this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 9624 this.functionName = functionName; 9625 this.argumentName = argumentName; 9626 this.argumentType = argumentType; 9627 9628 super("Missing Argument: " ~ this.argumentName, file, line, next); 9629 } 9630 } 9631 9632 /++ 9633 You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. 9634 9635 History: 9636 Added December 15, 2021 (dub v10.5) 9637 +/ 9638 class ResourceNotFoundException : Exception { 9639 string resourceType; 9640 string resourceId; 9641 9642 this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 9643 this.resourceType = resourceType; 9644 this.resourceId = resourceId; 9645 9646 super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); 9647 } 9648 9649 } 9650 9651 /++ 9652 This can be attached to any constructor or function called from the cgi system. 9653 9654 If it is present, the function argument can NOT be set from web params, but instead 9655 is set to the return value of the given `func`. 9656 9657 If `func` can take a parameter of type [Cgi], it will be passed the one representing 9658 the current request. Otherwise, it must take zero arguments. 9659 9660 Any params in your function of type `Cgi` are automatically assumed to take the cgi object 9661 for the connection. Any of type [Session] (with an argument) is also assumed to come from 9662 the cgi object. 9663 9664 const arguments are also supported. 9665 +/ 9666 struct ifCalledFromWeb(alias func) {} 9667 9668 // it only looks at query params for GET requests, the rest must be in the body for a function argument. 9669 auto callFromCgi(alias method, T)(T dg, Cgi cgi) { 9670 9671 // FIXME: any array of structs should also be settable or gettable from csv as well. 9672 9673 // FIXME: think more about checkboxes and bools. 9674 9675 import std.traits; 9676 9677 Parameters!method params; 9678 alias idents = ParameterIdentifierTuple!method; 9679 alias defaults = ParameterDefaults!method; 9680 9681 const(string)[] names; 9682 const(string)[] values; 9683 9684 // first, check for missing arguments and initialize to defaults if necessary 9685 9686 static if(is(typeof(method) P == __parameters)) 9687 foreach(idx, param; P) {{ 9688 // see: mustNotBeSetFromWebParams 9689 static if(is(param : Cgi)) { 9690 static assert(!is(param == immutable)); 9691 cast() params[idx] = cgi; 9692 } else static if(is(param == Session!D, D)) { 9693 static assert(!is(param == immutable)); 9694 cast() params[idx] = cgi.getSessionObject!D(); 9695 } else { 9696 bool populated; 9697 foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) { 9698 static if(is(uda == ifCalledFromWeb!func, alias func)) { 9699 static if(is(typeof(func(cgi)))) 9700 params[idx] = func(cgi); 9701 else 9702 params[idx] = func(); 9703 9704 populated = true; 9705 } 9706 } 9707 9708 if(!populated) { 9709 static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) { 9710 params[idx] = param.getAutomaticallyForCgi(cgi); 9711 populated = true; 9712 } 9713 } 9714 9715 if(!populated) { 9716 auto ident = idents[idx]; 9717 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 9718 if(ident !in cgi.get) { 9719 static if(is(defaults[idx] == void)) { 9720 static if(is(param == bool)) 9721 params[idx] = false; 9722 else 9723 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 9724 } else 9725 params[idx] = defaults[idx]; 9726 } 9727 } else { 9728 if(ident !in cgi.post) { 9729 static if(is(defaults[idx] == void)) { 9730 static if(is(param == bool)) 9731 params[idx] = false; 9732 else 9733 throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); 9734 } else 9735 params[idx] = defaults[idx]; 9736 } 9737 } 9738 } 9739 } 9740 }} 9741 9742 // second, parse the arguments in order to build up arrays, etc. 9743 9744 static bool setVariable(T)(string name, string paramName, T* what, string value) { 9745 static if(is(T == struct)) { 9746 if(name == paramName) { 9747 *what = T.init; 9748 return true; 9749 } else { 9750 // could be a child. gonna allow either obj.field OR obj[field] 9751 9752 string afterName; 9753 9754 if(name[paramName.length] == '[') { 9755 int count = 1; 9756 auto idx = paramName.length + 1; 9757 while(idx < name.length && count > 0) { 9758 if(name[idx] == '[') 9759 count++; 9760 else if(name[idx] == ']') { 9761 count--; 9762 if(count == 0) break; 9763 } 9764 idx++; 9765 } 9766 9767 if(idx == name.length) 9768 return false; // malformed 9769 9770 auto insideBrackets = name[paramName.length + 1 .. idx]; 9771 afterName = name[idx + 1 .. $]; 9772 9773 name = name[0 .. paramName.length]; 9774 9775 paramName = insideBrackets; 9776 9777 } else if(name[paramName.length] == '.') { 9778 paramName = name[paramName.length + 1 .. $]; 9779 name = paramName; 9780 int p = 0; 9781 foreach(ch; paramName) { 9782 if(ch == '.' || ch == '[') 9783 break; 9784 p++; 9785 } 9786 9787 afterName = paramName[p .. $]; 9788 paramName = paramName[0 .. p]; 9789 } else { 9790 return false; 9791 } 9792 9793 if(paramName.length) 9794 // set the child member 9795 switch(paramName) { 9796 foreach(idx, memberName; __traits(allMembers, T)) 9797 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 9798 // data member! 9799 case memberName: 9800 return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value); 9801 } 9802 default: 9803 // ok, not a member 9804 } 9805 } 9806 9807 return false; 9808 } else static if(is(T == enum)) { 9809 *what = to!T(value); 9810 return true; 9811 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 9812 *what = to!T(value); 9813 return true; 9814 } else static if(is(T == bool)) { 9815 *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on"; 9816 return true; 9817 } else static if(is(T == K[], K)) { 9818 K tmp; 9819 if(name == paramName) { 9820 // direct - set and append 9821 if(setVariable(name, paramName, &tmp, value)) { 9822 (*what) ~= tmp; 9823 return true; 9824 } else { 9825 return false; 9826 } 9827 } else { 9828 // child, append to last element 9829 // FIXME: what about range violations??? 9830 auto ptr = &(*what)[(*what).length - 1]; 9831 return setVariable(name, paramName, ptr, value); 9832 9833 } 9834 } else static if(is(T == V[K], K, V)) { 9835 // assoc array, name[key] is valid 9836 if(name == paramName) { 9837 // no action necessary 9838 return true; 9839 } else if(name[paramName.length] == '[') { 9840 int count = 1; 9841 auto idx = paramName.length + 1; 9842 while(idx < name.length && count > 0) { 9843 if(name[idx] == '[') 9844 count++; 9845 else if(name[idx] == ']') { 9846 count--; 9847 if(count == 0) break; 9848 } 9849 idx++; 9850 } 9851 if(idx == name.length) 9852 return false; // malformed 9853 9854 auto insideBrackets = name[paramName.length + 1 .. idx]; 9855 auto afterName = name[idx + 1 .. $]; 9856 9857 auto k = to!K(insideBrackets); 9858 V v; 9859 if(auto ptr = k in *what) 9860 v = *ptr; 9861 9862 name = name[0 .. paramName.length]; 9863 //writeln(name, afterName, " ", paramName); 9864 9865 auto ret = setVariable(name ~ afterName, paramName, &v, value); 9866 if(ret) { 9867 (*what)[k] = v; 9868 return true; 9869 } 9870 } 9871 9872 return false; 9873 } else { 9874 static assert(0, "unsupported type for cgi call " ~ T.stringof); 9875 } 9876 9877 //return false; 9878 } 9879 9880 void setArgument(string name, string value) { 9881 int p; 9882 foreach(ch; name) { 9883 if(ch == '.' || ch == '[') 9884 break; 9885 p++; 9886 } 9887 9888 auto paramName = name[0 .. p]; 9889 9890 sw: switch(paramName) { 9891 static if(is(typeof(method) P == __parameters)) 9892 foreach(idx, param; P) { 9893 static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) { 9894 // cannot be set from the outside 9895 } else { 9896 case idents[idx]: 9897 static if(is(param == Cgi.UploadedFile)) { 9898 params[idx] = cgi.files[name]; 9899 } else static if(is(param : const Cgi.UploadedFile[])) { 9900 (cast() params[idx]) = cgi.filesArray[name]; 9901 } else { 9902 setVariable(name, paramName, ¶ms[idx], value); 9903 } 9904 break sw; 9905 } 9906 } 9907 default: 9908 // ignore; not relevant argument 9909 } 9910 } 9911 9912 if(cgi.requestMethod == Cgi.RequestMethod.GET) { 9913 names = cgi.allGetNamesInOrder; 9914 values = cgi.allGetValuesInOrder; 9915 } else { 9916 names = cgi.allPostNamesInOrder; 9917 values = cgi.allPostValuesInOrder; 9918 } 9919 9920 foreach(idx, name; names) { 9921 setArgument(name, values[idx]); 9922 } 9923 9924 static if(is(ReturnType!method == void)) { 9925 typeof(null) ret; 9926 dg(params); 9927 } else { 9928 auto ret = dg(params); 9929 } 9930 9931 // FIXME: format return values 9932 // options are: json, html, csv. 9933 // also may need to wrap in envelope format: none, html, or json. 9934 return ret; 9935 } 9936 9937 private bool mustNotBeSetFromWebParams(T, attrs...)() { 9938 static if(is(T : const(Cgi))) { 9939 return true; 9940 } else static if(is(T : const(Session!D), D)) { 9941 return true; 9942 } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) { 9943 return true; 9944 } else { 9945 foreach(uda; attrs) 9946 static if(is(uda == ifCalledFromWeb!func, alias func)) 9947 return true; 9948 return false; 9949 } 9950 } 9951 9952 private bool hasIfCalledFromWeb(attrs...)() { 9953 foreach(uda; attrs) 9954 static if(is(uda == ifCalledFromWeb!func, alias func)) 9955 return true; 9956 return false; 9957 } 9958 9959 /++ 9960 Implies POST path for the thing itself, then GET will get the automatic form. 9961 9962 The given customizer, if present, will be called as a filter on the Form object. 9963 9964 History: 9965 Added December 27, 2020 9966 +/ 9967 template AutomaticForm(alias customizer) { } 9968 9969 /++ 9970 This is meant to be returned by a function that takes a form POST submission. You 9971 want to set the url of the new resource it created, which is set as the http 9972 Location header for a "201 Created" result, and you can also set a separate 9973 destination for browser users, which it sets via a "Refresh" header. 9974 9975 The `resourceRepresentation` should generally be the thing you just created, and 9976 it will be the body of the http response when formatted through the presenter. 9977 The exact thing is up to you - it could just return an id, or the whole object, or 9978 perhaps a partial object. 9979 9980 Examples: 9981 --- 9982 class Test : WebObject { 9983 @(Cgi.RequestMethod.POST) 9984 CreatedResource!int makeThing(string value) { 9985 return CreatedResource!int(value.to!int, "/resources/id"); 9986 } 9987 } 9988 --- 9989 9990 History: 9991 Added December 18, 2021 9992 +/ 9993 struct CreatedResource(T) { 9994 static if(!is(T == void)) 9995 T resourceRepresentation; 9996 string resourceUrl; 9997 string refreshUrl; 9998 } 9999 10000 /+ 10001 /++ 10002 This can be attached as a UDA to a handler to add a http Refresh header on a 10003 successful run. (It will not be attached if the function throws an exception.) 10004 This will refresh the browser the given number of seconds after the page loads, 10005 to the url returned by `urlFunc`, which can be either a static function or a 10006 member method of the current handler object. 10007 10008 You might use this for a POST handler that is normally used from ajax, but you 10009 want it to degrade gracefully to a temporarily flashed message before reloading 10010 the main page. 10011 10012 History: 10013 Added December 18, 2021 10014 +/ 10015 struct Refresh(alias urlFunc) { 10016 int waitInSeconds; 10017 10018 string url() { 10019 static if(__traits(isStaticFunction, urlFunc)) 10020 return urlFunc(); 10021 else static if(is(urlFunc : string)) 10022 return urlFunc; 10023 } 10024 } 10025 +/ 10026 10027 /+ 10028 /++ 10029 Sets a filter to be run before 10030 10031 A before function can do validations of params and log and stop the function from running. 10032 +/ 10033 template Before(alias b) {} 10034 template After(alias b) {} 10035 +/ 10036 10037 /+ 10038 Argument conversions: for the most part, it is to!Thing(string). 10039 10040 But arrays and structs are a bit different. Arrays come from the cgi array. Thus 10041 they are passed 10042 10043 arr=foo&arr=bar <-- notice the same name. 10044 10045 Structs are first declared with an empty thing, then have their members set individually, 10046 with dot notation. The members are not required, just the initial declaration. 10047 10048 struct Foo { 10049 int a; 10050 string b; 10051 } 10052 void test(Foo foo){} 10053 10054 foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members 10055 10056 Arrays of structs use this declaration. 10057 10058 void test(Foo[] foo) {} 10059 10060 foo&foo.a=5&foo.b=bar&foo&foo.a=9 10061 10062 You can use a hidden input field in HTML forms to achieve this. The value of the naked name 10063 declaration is ignored. 10064 10065 Mind that order matters! The declaration MUST come first in the string. 10066 10067 Arrays of struct members follow this rule recursively. 10068 10069 struct Foo { 10070 int[] a; 10071 } 10072 10073 foo&foo.a=1&foo.a=2&foo&foo.a=1 10074 10075 10076 Associative arrays are formatted with brackets, after a declaration, like structs: 10077 10078 foo&foo[key]=value&foo[other_key]=value 10079 10080 10081 Note: for maximum compatibility with outside code, keep your types simple. Some libraries 10082 do not support the strict ordering requirements to work with these struct protocols. 10083 10084 FIXME: also perhaps accept application/json to better work with outside trash. 10085 10086 10087 Return values are also auto-formatted according to user-requested type: 10088 for json, it loops over and converts. 10089 for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables! 10090 +/ 10091 10092 /++ 10093 A web presenter is responsible for rendering things to HTML to be usable 10094 in a web browser. 10095 10096 They are passed as template arguments to the base classes of [WebObject] 10097 10098 Responsible for displaying stuff as HTML. You can put this into your own aggregate 10099 and override it. Use forwarding and specialization to customize it. 10100 10101 When you inherit from it, pass your own class as the CRTP argument. This lets the base 10102 class templates and your overridden templates work with each other. 10103 10104 --- 10105 class MyPresenter : WebPresenter!(MyPresenter) { 10106 @Override 10107 void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) { 10108 // present the CustomType 10109 } 10110 @Override 10111 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 10112 // handle everything else via the super class, which will call 10113 // back to your class when appropriate 10114 super.presentSuccessfulReturnAsHtml(cgi, ret); 10115 } 10116 } 10117 --- 10118 10119 The meta argument in there can be overridden by your own facility. 10120 10121 +/ 10122 class WebPresenter(CRTP) { 10123 10124 /// A UDA version of the built-in `override`, to be used for static template polymorphism 10125 /// If you override a plain method, use `override`. If a template, use `@Override`. 10126 enum Override; 10127 10128 string script() { 10129 return ` 10130 `; 10131 } 10132 10133 string style() { 10134 return ` 10135 :root { 10136 --mild-border: #ccc; 10137 --middle-border: #999; 10138 --accent-color: #f2f2f2; 10139 --sidebar-color: #fefefe; 10140 } 10141 ` ~ genericFormStyling() ~ genericSiteStyling(); 10142 } 10143 10144 string genericFormStyling() { 10145 return 10146 q"css 10147 table.automatic-data-display { 10148 border-collapse: collapse; 10149 border: solid 1px var(--mild-border); 10150 } 10151 10152 table.automatic-data-display td { 10153 vertical-align: top; 10154 border: solid 1px var(--mild-border); 10155 padding: 2px 4px; 10156 } 10157 10158 table.automatic-data-display th { 10159 border: solid 1px var(--mild-border); 10160 border-bottom: solid 1px var(--middle-border); 10161 padding: 2px 4px; 10162 } 10163 10164 ol.automatic-data-display { 10165 margin: 0px; 10166 /* 10167 list-style-position: inside; 10168 padding: 0px; 10169 */ 10170 } 10171 10172 dl.automatic-data-display { 10173 10174 } 10175 10176 .automatic-form { 10177 max-width: 600px; 10178 } 10179 10180 .form-field { 10181 margin: 0.5em; 10182 padding-left: 0.5em; 10183 } 10184 10185 .label-text { 10186 display: block; 10187 font-weight: bold; 10188 margin-left: -0.5em; 10189 } 10190 10191 .submit-button-holder { 10192 padding-left: 2em; 10193 } 10194 10195 .add-array-button { 10196 10197 } 10198 css"; 10199 } 10200 10201 string genericSiteStyling() { 10202 return 10203 q"css 10204 * { box-sizing: border-box; } 10205 html, body { margin: 0px; } 10206 body { 10207 font-family: sans-serif; 10208 } 10209 header { 10210 background: var(--accent-color); 10211 height: 64px; 10212 } 10213 footer { 10214 background: var(--accent-color); 10215 height: 64px; 10216 } 10217 #site-container { 10218 display: flex; 10219 flex-wrap: wrap; 10220 } 10221 main { 10222 flex: 1 1 auto; 10223 order: 2; 10224 min-height: calc(100vh - 64px - 64px); 10225 min-width: 80ch; 10226 padding: 4px; 10227 padding-left: 1em; 10228 } 10229 #sidebar { 10230 flex: 0 0 16em; 10231 order: 1; 10232 background: var(--sidebar-color); 10233 } 10234 css"; 10235 } 10236 10237 import arsd.dom; 10238 Element htmlContainer() { 10239 auto document = new Document(q"html 10240 <!DOCTYPE html> 10241 <html class="no-script"> 10242 <head> 10243 <script>document.documentElement.classList.remove("no-script");</script> 10244 <style>.no-script requires-script { display: none; }</style> 10245 <title>D Application</title> 10246 <meta name="viewport" content="initial-scale=1, width=device-width" /> 10247 <link rel="stylesheet" href="style.css" /> 10248 </head> 10249 <body> 10250 <header></header> 10251 <div id="site-container"> 10252 <main></main> 10253 <div id="sidebar"></div> 10254 </div> 10255 <footer></footer> 10256 <script src="script.js"></script> 10257 </body> 10258 </html> 10259 html", true, true); 10260 10261 return document.requireSelector("main"); 10262 } 10263 10264 /// Renders a response as an HTTP error with associated html body 10265 void renderBasicError(Cgi cgi, int httpErrorCode) { 10266 cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); 10267 auto c = htmlContainer(); 10268 c.innerText = getHttpCodeText(httpErrorCode); 10269 cgi.setResponseContentType("text/html; charset=utf-8"); 10270 cgi.write(c.parentDocument.toString(), true); 10271 } 10272 10273 template methodMeta(alias method) { 10274 enum methodMeta = null; 10275 } 10276 10277 void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10278 switch(format) { 10279 case "html": 10280 (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta); 10281 break; 10282 case "json": 10283 import arsd.jsvar; 10284 static if(is(typeof(ret) == MultipleResponses!Types, Types...)) { 10285 var json; 10286 foreach(index, type; Types) { 10287 if(ret.contains == index) 10288 json = ret.payload[index]; 10289 } 10290 } else { 10291 var json = ret; 10292 } 10293 var envelope = json; // var.emptyObject; 10294 /* 10295 envelope.success = true; 10296 envelope.result = json; 10297 envelope.error = null; 10298 */ 10299 cgi.setResponseContentType("application/json"); 10300 cgi.write(envelope.toJson(), true); 10301 break; 10302 default: 10303 cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of. 10304 } 10305 } 10306 10307 /// typeof(null) (which is also used to represent functions returning `void`) do nothing 10308 /// in the default presenter - allowing the function to have full low-level control over the 10309 /// response. 10310 void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { 10311 // nothing intentionally! 10312 } 10313 10314 /// Redirections are forwarded to [Cgi.setResponseLocation] 10315 void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10316 cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); 10317 } 10318 10319 /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. 10320 void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { 10321 cgi.setResponseStatus(getHttpCodeText(201)); 10322 if(ret.resourceUrl.length) 10323 cgi.header("Location: " ~ ret.resourceUrl); 10324 if(ret.refreshUrl.length) 10325 cgi.header("Refresh: 0;" ~ ret.refreshUrl); 10326 static if(!is(R == void)) 10327 presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); 10328 } 10329 10330 /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime 10331 void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { 10332 bool outputted = false; 10333 foreach(index, type; Types) { 10334 if(ret.contains == index) { 10335 assert(!outputted); 10336 outputted = true; 10337 (cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format); 10338 } 10339 } 10340 if(!outputted) 10341 assert(0); 10342 } 10343 10344 /++ 10345 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. 10346 +/ 10347 void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { 10348 cgi.setCache(true); // not necessarily true but meh 10349 if(auto fn = ret.filename()) { 10350 cgi.header("Content-Disposition: attachment; filename="~fn~";"); 10351 } 10352 cgi.setResponseContentType(ret.contentType); 10353 cgi.write(ret.getData(), true); 10354 } 10355 10356 /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer]. 10357 void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { 10358 auto container = this.htmlContainer(); 10359 container.appendChild(formatReturnValueAsHtml(ret)); 10360 cgi.write(container.parentDocument.toString(), true); 10361 } 10362 10363 /++ 10364 10365 History: 10366 Added January 23, 2023 (dub v11.0) 10367 +/ 10368 void presentExceptionalReturn(Meta)(Cgi cgi, Throwable t, Meta meta, string format) { 10369 switch(format) { 10370 case "html": 10371 presentExceptionAsHtml(cgi, t, meta); 10372 break; 10373 case "json": 10374 presentExceptionAsJsonImpl(cgi, t); 10375 break; 10376 default: 10377 } 10378 } 10379 10380 private void presentExceptionAsJsonImpl()(Cgi cgi, Throwable t) { 10381 cgi.setResponseStatus("500 Internal Server Error"); 10382 cgi.setResponseContentType("application/json"); 10383 import arsd.jsvar; 10384 var v = var.emptyObject; 10385 v.type = typeid(t).toString; 10386 v.msg = t.msg; 10387 v.fullString = t.toString(); 10388 cgi.write(v.toJson(), true); 10389 } 10390 10391 10392 /++ 10393 If you override this, you will need to cast the exception type `t` dynamically, 10394 but can then use the template arguments here to refer back to the function. 10395 10396 `func` is an alias to the method itself, and `dg` is a callable delegate to the same 10397 method on the live object. You could, in theory, change arguments and retry, but I 10398 provide that information mostly with the expectation that you will use them to make 10399 useful forms or richer error messages for the user. 10400 10401 History: 10402 BREAKING CHANGE on January 23, 2023 (v11.0 ): it previously took an `alias func` and `T dg` to call the function again. 10403 I removed this in favor of a `Meta` param. 10404 10405 Before: `void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg)` 10406 10407 After: `void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta)` 10408 10409 If you used the func for something, move that something into your `methodMeta` template. 10410 10411 What is the benefit of this change? Somewhat smaller executables and faster builds thanks to more reused functions, together with 10412 enabling an easier implementation of [presentExceptionalReturn]. 10413 +/ 10414 void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta) { 10415 Form af; 10416 /+ 10417 foreach(attr; __traits(getAttributes, func)) { 10418 static if(__traits(isSame, attr, AutomaticForm)) { 10419 af = createAutomaticFormForFunction!(func)(dg); 10420 } 10421 } 10422 +/ 10423 presentExceptionAsHtmlImpl(cgi, t, af); 10424 } 10425 10426 void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { 10427 if(auto e = cast(ResourceNotFoundException) t) { 10428 auto container = this.htmlContainer(); 10429 10430 container.addChild("p", e.msg); 10431 10432 if(!cgi.outputtedResponseData) 10433 cgi.setResponseStatus("404 Not Found"); 10434 cgi.write(container.parentDocument.toString(), true); 10435 } else if(auto mae = cast(MissingArgumentException) t) { 10436 if(automaticForm is null) 10437 goto generic; 10438 auto container = this.htmlContainer(); 10439 if(cgi.requestMethod == Cgi.RequestMethod.POST) 10440 container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); 10441 container.appendChild(automaticForm); 10442 10443 cgi.write(container.parentDocument.toString(), true); 10444 } else { 10445 generic: 10446 auto container = this.htmlContainer(); 10447 10448 // import std.stdio; writeln(t.toString()); 10449 10450 container.appendChild(exceptionToElement(t)); 10451 10452 container.addChild("h4", "GET"); 10453 foreach(k, v; cgi.get) { 10454 auto deets = container.addChild("details"); 10455 deets.addChild("summary", k); 10456 deets.addChild("div", v); 10457 } 10458 10459 container.addChild("h4", "POST"); 10460 foreach(k, v; cgi.post) { 10461 auto deets = container.addChild("details"); 10462 deets.addChild("summary", k); 10463 deets.addChild("div", v); 10464 } 10465 10466 10467 if(!cgi.outputtedResponseData) 10468 cgi.setResponseStatus("500 Internal Server Error"); 10469 cgi.write(container.parentDocument.toString(), true); 10470 } 10471 } 10472 10473 Element exceptionToElement(Throwable t) { 10474 auto div = Element.make("div"); 10475 div.addClass("exception-display"); 10476 10477 div.addChild("p", t.msg); 10478 div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line)); 10479 10480 auto pre = div.addChild("pre"); 10481 string s; 10482 s = t.toString(); 10483 Element currentBox; 10484 bool on = false; 10485 foreach(line; s.splitLines) { 10486 if(!on && line.startsWith("-----")) 10487 on = true; 10488 if(!on) continue; 10489 if(line.indexOf("arsd/") != -1) { 10490 if(currentBox is null) { 10491 currentBox = pre.addChild("details"); 10492 currentBox.addChild("summary", "Framework code"); 10493 } 10494 currentBox.addChild("span", line ~ "\n"); 10495 } else { 10496 pre.addChild("span", line ~ "\n"); 10497 currentBox = null; 10498 } 10499 } 10500 10501 return div; 10502 } 10503 10504 /++ 10505 Returns an element for a particular type 10506 +/ 10507 Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { 10508 import std.traits; 10509 10510 auto div = Element.make("div"); 10511 div.addClass("form-field"); 10512 10513 static if(is(T : const Cgi.UploadedFile)) { 10514 Element lbl; 10515 if(displayName !is null) { 10516 lbl = div.addChild("label"); 10517 lbl.addChild("span", displayName, "label-text"); 10518 lbl.appendText(" "); 10519 } else { 10520 lbl = div; 10521 } 10522 auto i = lbl.addChild("input", name); 10523 i.attrs.name = name; 10524 i.attrs.type = "file"; 10525 i.attrs.multiple = "multiple"; 10526 } else static if(is(T == Cgi.UploadedFile)) { 10527 Element lbl; 10528 if(displayName !is null) { 10529 lbl = div.addChild("label"); 10530 lbl.addChild("span", displayName, "label-text"); 10531 lbl.appendText(" "); 10532 } else { 10533 lbl = div; 10534 } 10535 auto i = lbl.addChild("input", name); 10536 i.attrs.name = name; 10537 i.attrs.type = "file"; 10538 } else static if(is(T == enum)) { 10539 Element lbl; 10540 if(displayName !is null) { 10541 lbl = div.addChild("label"); 10542 lbl.addChild("span", displayName, "label-text"); 10543 lbl.appendText(" "); 10544 } else { 10545 lbl = div; 10546 } 10547 auto i = lbl.addChild("select", name); 10548 i.attrs.name = name; 10549 10550 foreach(memberName; __traits(allMembers, T)) 10551 i.addChild("option", memberName); 10552 10553 } else static if(is(T == struct)) { 10554 if(displayName !is null) 10555 div.addChild("span", displayName, "label-text"); 10556 auto fieldset = div.addChild("fieldset"); 10557 fieldset.addChild("legend", beautify(T.stringof)); // FIXME 10558 fieldset.addChild("input", name); 10559 foreach(idx, memberName; __traits(allMembers, T)) 10560 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 10561 fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); 10562 } 10563 } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { 10564 Element lbl; 10565 if(displayName !is null) { 10566 lbl = div.addChild("label"); 10567 lbl.addChild("span", displayName, "label-text"); 10568 lbl.appendText(" "); 10569 } else { 10570 lbl = div; 10571 } 10572 Element i; 10573 if(udaSuggestion) { 10574 i = udaSuggestion(); 10575 lbl.appendChild(i); 10576 } else { 10577 i = lbl.addChild("input", name); 10578 } 10579 i.attrs.name = name; 10580 static if(isSomeString!T) 10581 i.attrs.type = "text"; 10582 else 10583 i.attrs.type = "number"; 10584 if(i.tagName == "textarea") 10585 i.textContent = to!string(T.init); 10586 else 10587 i.attrs.value = to!string(T.init); 10588 } else static if(is(T == bool)) { 10589 Element lbl; 10590 if(displayName !is null) { 10591 lbl = div.addChild("label"); 10592 lbl.addChild("span", displayName, "label-text"); 10593 lbl.appendText(" "); 10594 } else { 10595 lbl = div; 10596 } 10597 auto i = lbl.addChild("input", name); 10598 i.attrs.type = "checkbox"; 10599 i.attrs.value = "true"; 10600 i.attrs.name = name; 10601 } else static if(is(T == K[], K)) { 10602 auto templ = div.addChild("template"); 10603 templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); 10604 if(displayName !is null) 10605 div.addChild("span", displayName, "label-text"); 10606 auto btn = div.addChild("button"); 10607 btn.addClass("add-array-button"); 10608 btn.attrs.type = "button"; 10609 btn.innerText = "Add"; 10610 btn.attrs.onclick = q{ 10611 var a = document.importNode(this.parentNode.firstChild.content, true); 10612 this.parentNode.insertBefore(a, this); 10613 }; 10614 } else static if(is(T == V[K], K, V)) { 10615 div.innerText = "assoc array not implemented for automatic form at this time"; 10616 } else { 10617 static assert(0, "unsupported type for cgi call " ~ T.stringof); 10618 } 10619 10620 10621 return div; 10622 } 10623 10624 /// creates a form for gathering the function's arguments 10625 Form createAutomaticFormForFunction(alias method, T)(T dg) { 10626 10627 auto form = cast(Form) Element.make("form"); 10628 10629 form.method = "POST"; // FIXME 10630 10631 form.addClass("automatic-form"); 10632 10633 string formDisplayName = beautify(__traits(identifier, method)); 10634 foreach(attr; __traits(getAttributes, method)) 10635 static if(is(typeof(attr) == DisplayName)) 10636 formDisplayName = attr.name; 10637 form.addChild("h3", formDisplayName); 10638 10639 import std.traits; 10640 10641 //Parameters!method params; 10642 //alias idents = ParameterIdentifierTuple!method; 10643 //alias defaults = ParameterDefaults!method; 10644 10645 static if(is(typeof(method) P == __parameters)) 10646 foreach(idx, _; P) {{ 10647 10648 alias param = P[idx .. idx + 1]; 10649 10650 static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { 10651 string displayName = beautify(__traits(identifier, param)); 10652 Element function() element; 10653 foreach(attr; __traits(getAttributes, param)) { 10654 static if(is(typeof(attr) == DisplayName)) 10655 displayName = attr.name; 10656 else static if(is(typeof(attr) : typeof(element))) { 10657 element = attr; 10658 } 10659 } 10660 auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); 10661 if(i.querySelector("input[type=file]") !is null) 10662 form.setAttribute("enctype", "multipart/form-data"); 10663 } 10664 }} 10665 10666 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 10667 10668 return form; 10669 } 10670 10671 /// creates a form for gathering object members (for the REST object thing right now) 10672 Form createAutomaticFormForObject(T)(T obj) { 10673 auto form = cast(Form) Element.make("form"); 10674 10675 form.addClass("automatic-form"); 10676 10677 form.addChild("h3", beautify(__traits(identifier, T))); 10678 10679 import std.traits; 10680 10681 //Parameters!method params; 10682 //alias idents = ParameterIdentifierTuple!method; 10683 //alias defaults = ParameterDefaults!method; 10684 10685 foreach(idx, memberName; __traits(derivedMembers, T)) {{ 10686 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 10687 string displayName = beautify(memberName); 10688 Element function() element; 10689 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) 10690 static if(is(typeof(attr) == DisplayName)) 10691 displayName = attr.name; 10692 else static if(is(typeof(attr) : typeof(element))) 10693 element = attr; 10694 form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); 10695 10696 form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); 10697 }}} 10698 10699 form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder"); 10700 10701 return form; 10702 } 10703 10704 /// 10705 Element formatReturnValueAsHtml(T)(T t) { 10706 import std.traits; 10707 10708 static if(is(T == typeof(null))) { 10709 return Element.make("span"); 10710 } else static if(is(T : Element)) { 10711 return t; 10712 } else static if(is(T == MultipleResponses!Types, Types...)) { 10713 foreach(index, type; Types) { 10714 if(t.contains == index) 10715 return formatReturnValueAsHtml(t.payload[index]); 10716 } 10717 assert(0); 10718 } else static if(is(T == Paginated!E, E)) { 10719 auto e = Element.make("div").addClass("paginated-result"); 10720 e.appendChild(formatReturnValueAsHtml(t.items)); 10721 if(t.nextPageUrl.length) 10722 e.appendChild(Element.make("a", "Next Page", t.nextPageUrl)); 10723 return e; 10724 } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) { 10725 return Element.make("span", to!string(t), "automatic-data-display"); 10726 } else static if(is(T == V[K], K, V)) { 10727 auto dl = Element.make("dl"); 10728 dl.addClass("automatic-data-display associative-array"); 10729 foreach(k, v; t) { 10730 dl.addChild("dt", to!string(k)); 10731 dl.addChild("dd", formatReturnValueAsHtml(v)); 10732 } 10733 return dl; 10734 } else static if(is(T == struct)) { 10735 auto dl = Element.make("dl"); 10736 dl.addClass("automatic-data-display struct"); 10737 10738 foreach(idx, memberName; __traits(allMembers, T)) 10739 static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { 10740 dl.addChild("dt", beautify(memberName)); 10741 dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName))); 10742 } 10743 10744 return dl; 10745 } else static if(is(T == bool)) { 10746 return Element.make("span", t ? "true" : "false", "automatic-data-display"); 10747 } else static if(is(T == E[], E) || is(T == E[N], E, size_t N)) { 10748 static if(is(E : RestObject!Proxy, Proxy)) { 10749 // treat RestObject similar to struct 10750 auto table = cast(Table) Element.make("table"); 10751 table.addClass("automatic-data-display"); 10752 string[] names; 10753 foreach(idx, memberName; __traits(derivedMembers, E)) 10754 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10755 names ~= beautify(memberName); 10756 } 10757 table.appendHeaderRow(names); 10758 10759 foreach(l; t) { 10760 auto tr = table.appendRow(); 10761 foreach(idx, memberName; __traits(derivedMembers, E)) 10762 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10763 static if(memberName == "id") { 10764 string val = to!string(__traits(getMember, l, memberName)); 10765 tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME 10766 } else { 10767 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 10768 } 10769 } 10770 } 10771 10772 return table; 10773 } else static if(is(E == struct)) { 10774 // an array of structs is kinda special in that I like 10775 // having those formatted as tables. 10776 auto table = cast(Table) Element.make("table"); 10777 table.addClass("automatic-data-display"); 10778 string[] names; 10779 foreach(idx, memberName; __traits(allMembers, E)) 10780 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10781 names ~= beautify(memberName); 10782 } 10783 table.appendHeaderRow(names); 10784 10785 foreach(l; t) { 10786 auto tr = table.appendRow(); 10787 foreach(idx, memberName; __traits(allMembers, E)) 10788 static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { 10789 tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); 10790 } 10791 } 10792 10793 return table; 10794 } else { 10795 // otherwise, I will just make a list. 10796 auto ol = Element.make("ol"); 10797 ol.addClass("automatic-data-display"); 10798 foreach(e; t) 10799 ol.addChild("li", formatReturnValueAsHtml(e)); 10800 return ol; 10801 } 10802 } else static if(is(T : Object)) { 10803 static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface 10804 return Element.make("div", t.toHtml()); 10805 else 10806 return Element.make("div", t.toString()); 10807 } else static assert(0, "bad return value for cgi call " ~ T.stringof); 10808 10809 assert(0); 10810 } 10811 10812 } 10813 10814 /++ 10815 The base class for the [dispatcher] function and object support. 10816 +/ 10817 class WebObject { 10818 //protected Cgi cgi; 10819 10820 protected void initialize(Cgi cgi) { 10821 //this.cgi = cgi; 10822 } 10823 } 10824 10825 /++ 10826 Can return one of the given types, decided at runtime. The syntax 10827 is to declare all the possible types in the return value, then you 10828 can `return typeof(return)(...value...)` to construct it. 10829 10830 It has an auto-generated constructor for each value it can hold. 10831 10832 --- 10833 MultipleResponses!(Redirection, string) getData(int how) { 10834 if(how & 1) 10835 return typeof(return)(Redirection("http://dpldocs.info/")); 10836 else 10837 return typeof(return)("hi there!"); 10838 } 10839 --- 10840 10841 If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little. 10842 +/ 10843 struct MultipleResponses(T...) { 10844 private size_t contains; 10845 private union { 10846 private T payload; 10847 } 10848 10849 static foreach(index, type; T) 10850 public this(type t) { 10851 contains = index; 10852 payload[index] = t; 10853 } 10854 10855 /++ 10856 This is primarily for testing. It is your way of getting to the response. 10857 10858 Let's say you wanted to test that one holding a Redirection and a string actually 10859 holds a string, by name of "test": 10860 10861 --- 10862 auto valueToTest = your_test_function(); 10863 10864 valueToTest.visit( 10865 (Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test 10866 (string s) { assert(s == "test"); } // right value, go ahead and test it. 10867 ); 10868 --- 10869 10870 History: 10871 Was horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it. 10872 It tried to use alias lambdas before, but runtime delegates work much better so I changed it. 10873 +/ 10874 void visit(Handlers...)(Handlers handlers) { 10875 template findHandler(type, int count, HandlersToCheck...) { 10876 static if(HandlersToCheck.length == 0) 10877 enum findHandler = -1; 10878 else { 10879 static if(is(typeof(HandlersToCheck[0].init(type.init)))) 10880 enum findHandler = count; 10881 else 10882 enum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]); 10883 } 10884 } 10885 foreach(index, type; T) { 10886 enum handlerIndex = findHandler!(type, 0, Handlers); 10887 static if(handlerIndex == -1) 10888 static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor"); 10889 else { 10890 if(index == this.contains) 10891 handlers[handlerIndex](this.payload[index]); 10892 } 10893 } 10894 } 10895 10896 /+ 10897 auto toArsdJsvar()() { 10898 import arsd.jsvar; 10899 return var(null); 10900 } 10901 +/ 10902 } 10903 10904 // FIXME: implement this somewhere maybe 10905 struct RawResponse { 10906 int code; 10907 string[] headers; 10908 const(ubyte)[] responseBody; 10909 } 10910 10911 /++ 10912 You can return this from [WebObject] subclasses for redirections. 10913 10914 (though note the static types means that class must ALWAYS redirect if 10915 you return this directly. You might want to return [MultipleResponses] if it 10916 can be conditional) 10917 +/ 10918 struct Redirection { 10919 string to; /// The URL to redirect to. 10920 int code = 303; /// The HTTP code to return. 10921 } 10922 10923 /++ 10924 Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. 10925 10926 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overriden 10927 the presenter in the dispatcher. 10928 10929 FIXME: explain this better 10930 10931 You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function, 10932 and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads, 10933 the runtime result of that is undefined. 10934 10935 A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those. 10936 (this might change, like maybe i will use pure as an indicator GET is ok. idk.) 10937 10938 $(WARNING 10939 --- 10940 // legal in D, undefined runtime behavior with cgi.d, it may call either method 10941 // even if you put different URL udas on it, the current code ignores them. 10942 void foo(int a) {} 10943 void foo(string a) {} 10944 --- 10945 ) 10946 10947 See_Also: [serveRestObject], [serveStaticFile] 10948 +/ 10949 auto serveApi(T)(string urlPrefix) { 10950 assert(urlPrefix[$ - 1] == '/'); 10951 return serveApiInternal!T(urlPrefix); 10952 } 10953 10954 private string nextPieceFromSlash(ref string remainingUrl) { 10955 if(remainingUrl.length == 0) 10956 return remainingUrl; 10957 int slash = 0; 10958 while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.') 10959 slash++; 10960 10961 // I am specifically passing `null` to differentiate it vs empty string 10962 // so in your ctor, `items` means new T(null) and `items/` means new T("") 10963 auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash]; 10964 // so if it is the last item, the dot can be used to load an alternative view 10965 // otherwise tho the dot is considered part of the identifier 10966 // FIXME 10967 10968 // again notice "" vs null here! 10969 if(slash == remainingUrl.length) 10970 remainingUrl = null; 10971 else 10972 remainingUrl = remainingUrl[slash + 1 .. $]; 10973 10974 return ident; 10975 } 10976 10977 /++ 10978 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. 10979 +/ 10980 enum AddTrailingSlash; 10981 /// ditto 10982 enum RemoveTrailingSlash; 10983 10984 private auto serveApiInternal(T)(string urlPrefix) { 10985 10986 import arsd.dom; 10987 import arsd.jsvar; 10988 10989 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 10990 string remainingUrl = cgi.pathInfo[urlPrefix.length .. $]; 10991 10992 try { 10993 // see duplicated code below by searching subresource_ctor 10994 // also see mustNotBeSetFromWebParams 10995 10996 static if(is(typeof(T.__ctor) P == __parameters)) { 10997 P params; 10998 10999 foreach(pidx, param; P) { 11000 static if(is(param : Cgi)) { 11001 static assert(!is(param == immutable)); 11002 cast() params[pidx] = cgi; 11003 } else static if(is(param == Session!D, D)) { 11004 static assert(!is(param == immutable)); 11005 cast() params[pidx] = cgi.getSessionObject!D(); 11006 11007 } else { 11008 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 11009 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 11010 static if(is(uda == ifCalledFromWeb!func, alias func)) { 11011 static if(is(typeof(func(cgi)))) 11012 params[pidx] = func(cgi); 11013 else 11014 params[pidx] = func(); 11015 } 11016 } 11017 } else { 11018 11019 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 11020 params[pidx] = param.getAutomaticallyForCgi(cgi); 11021 } else static if(is(param == string)) { 11022 auto ident = nextPieceFromSlash(remainingUrl); 11023 params[pidx] = ident; 11024 } else static assert(0, "illegal type for subresource " ~ param.stringof); 11025 } 11026 } 11027 } 11028 11029 auto obj = new T(params); 11030 } else { 11031 auto obj = new T(); 11032 } 11033 11034 return internalHandlerWithObject(obj, remainingUrl, cgi, presenter); 11035 } catch(Throwable t) { 11036 switch(cgi.request("format", "html")) { 11037 case "html": 11038 static void dummy() {} 11039 presenter.presentExceptionAsHtml(cgi, t, null); 11040 return true; 11041 case "json": 11042 var envelope = var.emptyObject; 11043 envelope.success = false; 11044 envelope.result = null; 11045 envelope.error = t.toString(); 11046 cgi.setResponseContentType("application/json"); 11047 cgi.write(envelope.toJson(), true); 11048 return true; 11049 default: 11050 throw t; 11051 // return true; 11052 } 11053 // return true; 11054 } 11055 11056 assert(0); 11057 } 11058 11059 static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) { 11060 11061 obj.initialize(cgi); 11062 11063 /+ 11064 Overload rules: 11065 Any unique combination of HTTP verb and url path can be dispatched to function overloads 11066 statically. 11067 11068 Moreover, some args vs no args can be overloaded dynamically. 11069 +/ 11070 11071 auto methodNameFromUrl = nextPieceFromSlash(remainingUrl); 11072 /+ 11073 auto orig = remainingUrl; 11074 assert(0, 11075 (orig is null ? "__null" : orig) 11076 ~ " .. " ~ 11077 (methodNameFromUrl is null ? "__null" : methodNameFromUrl)); 11078 +/ 11079 11080 if(methodNameFromUrl is null) 11081 methodNameFromUrl = "__null"; 11082 11083 string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl; 11084 11085 if(remainingUrl.length) 11086 hack ~= "/"; 11087 11088 switch(hack) { 11089 foreach(methodName; __traits(derivedMembers, T)) 11090 static if(methodName != "__ctor") 11091 foreach(idx, overload; __traits(getOverloads, T, methodName)) { 11092 static if(is(typeof(overload) P == __parameters)) 11093 static if(is(typeof(overload) R == return)) 11094 static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") 11095 { 11096 static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) 11097 case urlNameForMethod: 11098 11099 static if(is(R : WebObject)) { 11100 // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above. 11101 11102 // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string 11103 11104 // subresource_ctor 11105 // also see mustNotBeSetFromWebParams 11106 11107 P params; 11108 11109 string ident; 11110 11111 foreach(pidx, param; P) { 11112 static if(is(param : Cgi)) { 11113 static assert(!is(param == immutable)); 11114 cast() params[pidx] = cgi; 11115 } else static if(is(param == typeof(presenter))) { 11116 cast() param[pidx] = presenter; 11117 } else static if(is(param == Session!D, D)) { 11118 static assert(!is(param == immutable)); 11119 cast() params[pidx] = cgi.getSessionObject!D(); 11120 } else { 11121 static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { 11122 foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { 11123 static if(is(uda == ifCalledFromWeb!func, alias func)) { 11124 static if(is(typeof(func(cgi)))) 11125 params[pidx] = func(cgi); 11126 else 11127 params[pidx] = func(); 11128 } 11129 } 11130 } else { 11131 11132 static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { 11133 params[pidx] = param.getAutomaticallyForCgi(cgi); 11134 } else static if(is(param == string)) { 11135 ident = nextPieceFromSlash(remainingUrl); 11136 if(ident is null) { 11137 // trailing slash mandated on subresources 11138 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 11139 return true; 11140 } else { 11141 params[pidx] = ident; 11142 } 11143 } else static assert(0, "illegal type for subresource " ~ param.stringof); 11144 } 11145 } 11146 } 11147 11148 auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident); 11149 return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter); 11150 } else { 11151 // 404 it if any url left - not a subresource means we don't get to play with that! 11152 if(remainingUrl.length) 11153 return false; 11154 11155 bool automaticForm; 11156 11157 foreach(attr; __traits(getAttributes, overload)) 11158 static if(is(attr == AddTrailingSlash)) { 11159 if(remainingUrl is null) { 11160 cgi.setResponseLocation(cgi.pathInfo ~ "/"); 11161 return true; 11162 } 11163 } else static if(is(attr == RemoveTrailingSlash)) { 11164 if(remainingUrl !is null) { 11165 cgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, "/")]); 11166 return true; 11167 } 11168 11169 } else static if(__traits(isSame, AutomaticForm, attr)) { 11170 automaticForm = true; 11171 } 11172 11173 /+ 11174 int zeroArgOverload = -1; 11175 int overloadCount = cast(int) __traits(getOverloads, T, methodName).length; 11176 bool calledWithZeroArgs = true; 11177 foreach(k, v; cgi.get) 11178 if(k != "format") { 11179 calledWithZeroArgs = false; 11180 break; 11181 } 11182 foreach(k, v; cgi.post) 11183 if(k != "format") { 11184 calledWithZeroArgs = false; 11185 break; 11186 } 11187 11188 // first, we need to go through and see if there is an empty one, since that 11189 // changes inside. But otherwise, all the stuff I care about can be done via 11190 // simple looping (other improper overloads might be flagged for runtime semantic check) 11191 // 11192 // an argument of type Cgi is ignored for these purposes 11193 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 11194 static if(is(typeof(overload) P == __parameters)) 11195 static if(P.length == 0) 11196 zeroArgOverload = cast(int) idx; 11197 else static if(P.length == 1 && is(P[0] : Cgi)) 11198 zeroArgOverload = cast(int) idx; 11199 }} 11200 // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method. 11201 bool overloadHasBeenCalled = false; 11202 static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ 11203 bool callFunction = true; 11204 // there is a zero arg overload and this is NOT it, and we have zero args - don't call this 11205 if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs) 11206 callFunction = false; 11207 // if this is the zero-arg overload, obviously it cannot be called if we got any args. 11208 if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs) 11209 callFunction = false; 11210 11211 // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea. 11212 11213 bool hadAnyMethodRestrictions = false; 11214 bool foundAcceptableMethod = false; 11215 foreach(attr; __traits(getAttributes, overload)) { 11216 static if(is(typeof(attr) == Cgi.RequestMethod)) { 11217 hadAnyMethodRestrictions = true; 11218 if(attr == cgi.requestMethod) 11219 foundAcceptableMethod = true; 11220 } 11221 } 11222 11223 if(hadAnyMethodRestrictions && !foundAcceptableMethod) 11224 callFunction = false; 11225 11226 /+ 11227 The overloads we really want to allow are the sane ones 11228 from the web perspective. Which is likely on HTTP verbs, 11229 for the most part, but might also be potentially based on 11230 some args vs zero args, or on argument names. Can't really 11231 do argument types very reliable through the web though; those 11232 should probably be different URLs. 11233 11234 Even names I feel is better done inside the function, so I'm not 11235 going to support that here. But the HTTP verbs and zero vs some 11236 args makes sense - it lets you define custom forms pretty easily. 11237 11238 Moreover, I'm of the opinion that empty overload really only makes 11239 sense on GET for this case. On a POST, it is just a missing argument 11240 exception and that should be handled by the presenter. But meh, I'll 11241 let the user define that, D only allows one empty arg thing anyway 11242 so the method UDAs are irrelevant. 11243 +/ 11244 if(callFunction) 11245 +/ 11246 11247 auto format = cgi.request("format", defaultFormat!overload()); 11248 auto wantsFormFormat = format.startsWith("form-"); 11249 11250 if(wantsFormFormat || (automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET)) { 11251 // Should I still show the form on a json thing? idk... 11252 auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx])); 11253 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), wantsFormFormat ? format["form_".length .. $] : "html"); 11254 return true; 11255 } 11256 11257 try { 11258 // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control. 11259 auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); 11260 presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); 11261 } catch(Throwable t) { 11262 // presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx])); 11263 presenter.presentExceptionalReturn(cgi, t, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); 11264 } 11265 return true; 11266 //}} 11267 11268 //cgi.header("Accept: POST"); // FIXME list the real thing 11269 //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering. 11270 //return true; 11271 } 11272 } 11273 } 11274 case "GET script.js": 11275 cgi.setResponseContentType("text/javascript"); 11276 cgi.gzipResponse = true; 11277 cgi.write(presenter.script(), true); 11278 return true; 11279 case "GET style.css": 11280 cgi.setResponseContentType("text/css"); 11281 cgi.gzipResponse = true; 11282 cgi.write(presenter.style(), true); 11283 return true; 11284 default: 11285 return false; 11286 } 11287 11288 assert(0); 11289 } 11290 return DispatcherDefinition!internalHandler(urlPrefix, false); 11291 } 11292 11293 string defaultFormat(alias method)() { 11294 bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; 11295 foreach(attr; __traits(getAttributes, method)) { 11296 static if(is(typeof(attr) == DefaultFormat)) { 11297 if(nonConstConditionForWorkingAroundASpuriousDmdWarning) 11298 return attr.value; 11299 } 11300 } 11301 return "html"; 11302 } 11303 11304 struct Paginated(T) { 11305 T[] items; 11306 string nextPageUrl; 11307 } 11308 11309 template urlNamesForMethod(alias method, string default_) { 11310 string[] helper() { 11311 auto verb = Cgi.RequestMethod.GET; 11312 bool foundVerb = false; 11313 bool foundNoun = false; 11314 11315 string def = default_; 11316 11317 bool hasAutomaticForm = false; 11318 11319 foreach(attr; __traits(getAttributes, method)) { 11320 static if(is(typeof(attr) == Cgi.RequestMethod)) { 11321 verb = attr; 11322 if(foundVerb) 11323 assert(0, "Multiple http verbs on one function is not currently supported"); 11324 foundVerb = true; 11325 } 11326 static if(is(typeof(attr) == UrlName)) { 11327 if(foundNoun) 11328 assert(0, "Multiple url names on one function is not currently supported"); 11329 foundNoun = true; 11330 def = attr.name; 11331 } 11332 static if(__traits(isSame, attr, AutomaticForm)) { 11333 hasAutomaticForm = true; 11334 } 11335 } 11336 11337 if(def is null) 11338 def = "__null"; 11339 11340 string[] ret; 11341 11342 static if(is(typeof(method) R == return)) { 11343 static if(is(R : WebObject)) { 11344 def ~= "/"; 11345 foreach(v; __traits(allMembers, Cgi.RequestMethod)) 11346 ret ~= v ~ " " ~ def; 11347 } else { 11348 if(hasAutomaticForm) { 11349 ret ~= "GET " ~ def; 11350 ret ~= "POST " ~ def; 11351 } else { 11352 ret ~= to!string(verb) ~ " " ~ def; 11353 } 11354 } 11355 } else static assert(0); 11356 11357 return ret; 11358 } 11359 enum urlNamesForMethod = helper(); 11360 } 11361 11362 11363 enum AccessCheck { 11364 allowed, 11365 denied, 11366 nonExistant, 11367 } 11368 11369 enum Operation { 11370 show, 11371 create, 11372 replace, 11373 remove, 11374 update 11375 } 11376 11377 enum UpdateResult { 11378 accessDenied, 11379 noSuchResource, 11380 success, 11381 failure, 11382 unnecessary 11383 } 11384 11385 enum ValidationResult { 11386 valid, 11387 invalid 11388 } 11389 11390 11391 /++ 11392 The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. 11393 11394 WARNING: this is not stable. 11395 +/ 11396 class RestObject(CRTP) : WebObject { 11397 11398 import arsd.dom; 11399 import arsd.jsvar; 11400 11401 /// Prepare the object to be shown. 11402 void show() {} 11403 /// ditto 11404 void show(string urlId) { 11405 load(urlId); 11406 show(); 11407 } 11408 11409 /// Override this to provide access control to this object. 11410 AccessCheck accessCheck(string urlId, Operation operation) { 11411 return AccessCheck.allowed; 11412 } 11413 11414 ValidationResult validate() { 11415 // FIXME 11416 return ValidationResult.valid; 11417 } 11418 11419 string getUrlSlug() { 11420 import std.conv; 11421 static if(is(typeof(CRTP.id))) 11422 return to!string((cast(CRTP) this).id); 11423 else 11424 return null; 11425 } 11426 11427 // The functions with more arguments are the low-level ones, 11428 // they forward to the ones with fewer arguments by default. 11429 11430 // POST on a parent collection - this is called from a collection class after the members are updated 11431 /++ 11432 Given a populated object, this creates a new entry. Returns the url identifier 11433 of the new object. 11434 +/ 11435 string create(scope void delegate() applyChanges) { 11436 applyChanges(); 11437 save(); 11438 return getUrlSlug(); 11439 } 11440 11441 void replace() { 11442 save(); 11443 } 11444 void replace(string urlId, scope void delegate() applyChanges) { 11445 load(urlId); 11446 applyChanges(); 11447 replace(); 11448 } 11449 11450 void update(string[] fieldList) { 11451 save(); 11452 } 11453 void update(string urlId, scope void delegate() applyChanges, string[] fieldList) { 11454 load(urlId); 11455 applyChanges(); 11456 update(fieldList); 11457 } 11458 11459 void remove() {} 11460 11461 void remove(string urlId) { 11462 load(urlId); 11463 remove(); 11464 } 11465 11466 abstract void load(string urlId); 11467 abstract void save(); 11468 11469 Element toHtml(Presenter)(Presenter presenter) { 11470 import arsd.dom; 11471 import std.conv; 11472 auto obj = cast(CRTP) this; 11473 auto div = Element.make("div"); 11474 div.addClass("Dclass_" ~ CRTP.stringof); 11475 div.dataset.url = getUrlSlug(); 11476 bool first = true; 11477 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 11478 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11479 if(!first) div.addChild("br"); else first = false; 11480 div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); 11481 } 11482 return div; 11483 } 11484 11485 var toJson() { 11486 import arsd.jsvar; 11487 var v = var.emptyObject(); 11488 auto obj = cast(CRTP) this; 11489 foreach(idx, memberName; __traits(derivedMembers, CRTP)) 11490 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11491 v[memberName] = __traits(getMember, obj, memberName); 11492 } 11493 return v; 11494 } 11495 11496 /+ 11497 auto structOf(this This) { 11498 11499 } 11500 +/ 11501 } 11502 11503 // FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value 11504 // https://use-the-index-luke.com/sql/partial-results/fetch-next-page 11505 11506 /++ 11507 Base class for REST collections. 11508 +/ 11509 class CollectionOf(Obj) : RestObject!(CollectionOf) { 11510 /// You might subclass this and use the cgi object's query params 11511 /// to implement a search filter, for example. 11512 /// 11513 /// FIXME: design a way to auto-generate that form 11514 /// (other than using the WebObject thing above lol 11515 // it'll prolly just be some searchParams UDA or maybe an enum. 11516 // 11517 // pagination too perhaps. 11518 // 11519 // and sorting too 11520 IndexResult index() { return IndexResult.init; } 11521 11522 string[] sortableFields() { return null; } 11523 string[] searchableFields() { return null; } 11524 11525 struct IndexResult { 11526 Obj[] results; 11527 11528 string[] sortableFields; 11529 11530 string previousPageIdentifier; 11531 string nextPageIdentifier; 11532 string firstPageIdentifier; 11533 string lastPageIdentifier; 11534 11535 int numberOfPages; 11536 } 11537 11538 override string create(scope void delegate() applyChanges) { assert(0); } 11539 override void load(string urlId) { assert(0); } 11540 override void save() { assert(0); } 11541 override void show() { 11542 index(); 11543 } 11544 override void show(string urlId) { 11545 show(); 11546 } 11547 11548 /// Proxy POST requests (create calls) to the child collection 11549 alias PostProxy = Obj; 11550 } 11551 11552 /++ 11553 Serves a REST object, similar to a Ruby on Rails resource. 11554 11555 You put data members in your class. cgi.d will automatically make something out of those. 11556 11557 It will call your constructor with the ID from the URL. This may be null. 11558 It will then populate the data members from the request. 11559 It will then call a method, if present, telling what happened. You don't need to write these! 11560 It finally returns a reply. 11561 11562 Your methods are passed a list of fields it actually set. 11563 11564 The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST 11565 APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better 11566 with relative linking. But meh.) 11567 11568 GET /items -> index. all values not set. 11569 GET /items/id -> get. only ID will be set, other params ignored. 11570 POST /items -> create. values set as given 11571 PUT /items/id -> replace. values set as given 11572 or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation 11573 a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form. 11574 PATCH /items/id -> update. values set as given, list of changed fields passed 11575 or POST /items/id with cgi.post["_method"] == "PATCH" 11576 DELETE /items/id -> destroy. only ID guaranteed to be set 11577 or POST /items/id with cgi.post["_method"] == "DELETE" 11578 11579 Following the stupid convention, there will never be a trailing slash here, and if it is there, it will 11580 redirect you away from it. 11581 11582 API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var. 11583 11584 I will also let you change the default, if you must. 11585 11586 // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes. 11587 11588 You can define sub-resources on your object inside the object. These sub-resources are also REST objects 11589 that follow the same thing. They may be individual resources or collections themselves. 11590 11591 Your class is expected to have at least the following methods: 11592 11593 FIXME: i kinda wanna add a routes object to the initialize call 11594 11595 create 11596 Create returns the new address on success, some code on failure. 11597 show 11598 index 11599 update 11600 remove 11601 11602 You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults 11603 should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that. 11604 11605 Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar]. 11606 11607 NOT IMPLEMENTED 11608 11609 11610 Really, a collection is a resource with a bunch of subresources. 11611 11612 GET /items 11613 index because it is GET on the top resource 11614 11615 GET /items/foo 11616 item but different than items? 11617 11618 class Items { 11619 11620 } 11621 11622 ... but meh, a collection can be automated. not worth making it 11623 a separate thing, let's look at a real example. Users has many 11624 items and a virtual one, /users/current. 11625 11626 the individual users have properties and two sub-resources: 11627 session, which is just one, and comments, a collection. 11628 11629 class User : RestObject!() { // no parent 11630 int id; 11631 string name; 11632 11633 // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one. 11634 // but you can override them to do it differently. 11635 11636 // any member which is of type RestObject can be linked automatically via href btw. 11637 11638 void show() {} 11639 void show(string urlId) {} // automated! GET of this specific thing 11640 void create() {} // POST on a parent collection - this is called from a collection class after the members are updated 11641 void replace(string urlId) {} // this is the PUT; really, it just updates all fields. 11642 void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields. 11643 void remove(string urlId) {} // DELETE 11644 11645 void load(string urlId) {} // the default implementation of show() populates the id, then 11646 11647 this() {} 11648 11649 mixin Subresource!Session; 11650 mixin Subresource!Comment; 11651 } 11652 11653 class Session : RestObject!() { 11654 // the parent object may not be fully constructed/loaded 11655 this(User parent) {} 11656 11657 } 11658 11659 class Comment : CollectionOf!Comment { 11660 this(User parent) {} 11661 } 11662 11663 class Users : CollectionOf!User { 11664 // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects. 11665 void index() {} // GET on this specific thing; just like show really, just different name for the different semantics. 11666 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 11667 } 11668 11669 +/ 11670 auto serveRestObject(T)(string urlPrefix) { 11671 assert(urlPrefix[0] == '/'); 11672 assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); 11673 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { 11674 string url = cgi.pathInfo[urlPrefix.length .. $]; 11675 11676 if(url.length && url[$ - 1] == '/') { 11677 // remove the final slash... 11678 cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]); 11679 return true; 11680 } 11681 11682 return restObjectServeHandler!T(cgi, presenter, url); 11683 } 11684 return DispatcherDefinition!internalHandler(urlPrefix, false); 11685 } 11686 11687 /+ 11688 /// Convenience method for serving a collection. It will be named the same 11689 /// as type T, just with an s at the end. If you need any further, just 11690 /// write the class yourself. 11691 auto serveRestCollectionOf(T)(string urlPrefix) { 11692 assert(urlPrefix[0] == '/'); 11693 mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`); 11694 return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); 11695 } 11696 +/ 11697 11698 bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) { 11699 string urlId = null; 11700 if(url.length && url[0] == '/') { 11701 // asking for a subobject 11702 urlId = url[1 .. $]; 11703 foreach(idx, ch; urlId) { 11704 if(ch == '/') { 11705 urlId = urlId[0 .. idx]; 11706 break; 11707 } 11708 } 11709 } 11710 11711 // FIXME handle other subresources 11712 11713 static if(is(T : CollectionOf!(C), C)) { 11714 if(urlId !is null) { 11715 return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId); 11716 } 11717 } 11718 11719 // FIXME: support precondition failed, if-modified-since, expectation failed, etc. 11720 11721 auto obj = new T(); 11722 obj.initialize(cgi); 11723 // FIXME: populate reflection info delegates 11724 11725 11726 // FIXME: I am not happy with this. 11727 switch(urlId) { 11728 case "script.js": 11729 cgi.setResponseContentType("text/javascript"); 11730 cgi.gzipResponse = true; 11731 cgi.write(presenter.script(), true); 11732 return true; 11733 case "style.css": 11734 cgi.setResponseContentType("text/css"); 11735 cgi.gzipResponse = true; 11736 cgi.write(presenter.style(), true); 11737 return true; 11738 default: 11739 // intentionally blank 11740 } 11741 11742 11743 11744 11745 static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) { 11746 foreach(idx, memberName; __traits(derivedMembers, Obj)) 11747 static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { 11748 __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName)); 11749 } 11750 } 11751 void applyChanges() { 11752 applyChangesTemplate(cgi, obj); 11753 } 11754 11755 string[] modifiedList; 11756 11757 void writeObject(bool addFormLinks) { 11758 if(cgi.request("format") == "json") { 11759 cgi.setResponseContentType("application/json"); 11760 cgi.write(obj.toJson().toString, true); 11761 } else { 11762 auto container = presenter.htmlContainer(); 11763 if(addFormLinks) { 11764 static if(is(T : CollectionOf!(C), C)) 11765 container.appendHtml(` 11766 <form> 11767 <button type="submit" name="_method" value="POST">Create New</button> 11768 </form> 11769 `); 11770 else 11771 container.appendHtml(` 11772 <a href="..">Back</a> 11773 <form> 11774 <button type="submit" name="_method" value="PATCH">Edit</button> 11775 <button type="submit" name="_method" value="DELETE">Delete</button> 11776 </form> 11777 `); 11778 } 11779 container.appendChild(obj.toHtml(presenter)); 11780 cgi.write(container.parentDocument.toString, true); 11781 } 11782 } 11783 11784 // FIXME: I think I need a set type in here.... 11785 // it will be nice to pass sets of members. 11786 11787 try 11788 switch(cgi.requestMethod) { 11789 case Cgi.RequestMethod.GET: 11790 // I could prolly use template this parameters in the implementation above for some reflection stuff. 11791 // sure, it doesn't automatically work in subclasses... but I instantiate here anyway... 11792 11793 // automatic forms here for usable basic auto site from browser. 11794 // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh. 11795 switch(cgi.request("_method", "GET")) { 11796 case "GET": 11797 static if(is(T : CollectionOf!(C), C)) { 11798 auto results = obj.index(); 11799 if(cgi.request("format", "html") == "html") { 11800 auto container = presenter.htmlContainer(); 11801 auto html = presenter.formatReturnValueAsHtml(results.results); 11802 container.appendHtml(` 11803 <form> 11804 <button type="submit" name="_method" value="POST">Create New</button> 11805 </form> 11806 `); 11807 11808 container.appendChild(html); 11809 cgi.write(container.parentDocument.toString, true); 11810 } else { 11811 cgi.setResponseContentType("application/json"); 11812 import arsd.jsvar; 11813 var json = var.emptyArray; 11814 foreach(r; results.results) { 11815 var o = var.emptyObject; 11816 foreach(idx, memberName; __traits(derivedMembers, typeof(r))) 11817 static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) { 11818 o[memberName] = __traits(getMember, r, memberName); 11819 } 11820 11821 json ~= o; 11822 } 11823 cgi.write(json.toJson(), true); 11824 } 11825 } else { 11826 obj.show(urlId); 11827 writeObject(true); 11828 } 11829 break; 11830 case "PATCH": 11831 obj.load(urlId); 11832 goto case; 11833 case "PUT": 11834 case "POST": 11835 // an editing form for the object 11836 auto container = presenter.htmlContainer(); 11837 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 11838 auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj); 11839 } else { 11840 auto form = presenter.createAutomaticFormForObject(obj); 11841 } 11842 form.attrs.method = "POST"; 11843 form.setValue("_method", cgi.request("_method", "GET")); 11844 container.appendChild(form); 11845 cgi.write(container.parentDocument.toString(), true); 11846 break; 11847 case "DELETE": 11848 // FIXME: a delete form for the object (can be phrased "are you sure?") 11849 auto container = presenter.htmlContainer(); 11850 container.appendHtml(` 11851 <form method="POST"> 11852 Are you sure you want to delete this item? 11853 <input type="hidden" name="_method" value="DELETE" /> 11854 <input type="submit" value="Yes, Delete It" /> 11855 </form> 11856 11857 `); 11858 cgi.write(container.parentDocument.toString(), true); 11859 break; 11860 default: 11861 cgi.write("bad method\n", true); 11862 } 11863 break; 11864 case Cgi.RequestMethod.POST: 11865 // this is to allow compatibility with HTML forms 11866 switch(cgi.request("_method", "POST")) { 11867 case "PUT": 11868 goto PUT; 11869 case "PATCH": 11870 goto PATCH; 11871 case "DELETE": 11872 goto DELETE; 11873 case "POST": 11874 static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { 11875 auto p = new obj.PostProxy(); 11876 void specialApplyChanges() { 11877 applyChangesTemplate(cgi, p); 11878 } 11879 string n = p.create(&specialApplyChanges); 11880 } else { 11881 string n = obj.create(&applyChanges); 11882 } 11883 11884 auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n; 11885 cgi.setResponseLocation(newUrl); 11886 cgi.setResponseStatus("201 Created"); 11887 cgi.write(`The object has been created.`); 11888 break; 11889 default: 11890 cgi.write("bad method\n", true); 11891 } 11892 // FIXME this should be valid on the collection, but not the child.... 11893 // 303 See Other 11894 break; 11895 case Cgi.RequestMethod.PUT: 11896 PUT: 11897 obj.replace(urlId, &applyChanges); 11898 writeObject(false); 11899 break; 11900 case Cgi.RequestMethod.PATCH: 11901 PATCH: 11902 obj.update(urlId, &applyChanges, modifiedList); 11903 writeObject(false); 11904 break; 11905 case Cgi.RequestMethod.DELETE: 11906 DELETE: 11907 obj.remove(urlId); 11908 cgi.setResponseStatus("204 No Content"); 11909 break; 11910 default: 11911 // FIXME: OPTIONS, HEAD 11912 } 11913 catch(Throwable t) { 11914 presenter.presentExceptionAsHtml(cgi, t); 11915 } 11916 11917 return true; 11918 } 11919 11920 /+ 11921 struct SetOfFields(T) { 11922 private void[0][string] storage; 11923 void set(string what) { 11924 //storage[what] = 11925 } 11926 void unset(string what) {} 11927 void setAll() {} 11928 void unsetAll() {} 11929 bool isPresent(string what) { return false; } 11930 } 11931 +/ 11932 11933 /+ 11934 enum readonly; 11935 enum hideonindex; 11936 +/ 11937 11938 /++ 11939 Returns true if I recommend gzipping content of this type. You might 11940 want to call it from your Presenter classes before calling cgi.write. 11941 11942 --- 11943 cgi.setResponseContentType(yourContentType); 11944 cgi.gzipResponse = gzipRecommendedForContentType(yourContentType); 11945 cgi.write(yourData, true); 11946 --- 11947 11948 This is used internally by [serveStaticFile], [serveStaticFileDirectory], [serveStaticData], and maybe others I forgot to update this doc about. 11949 11950 11951 The implementation considers text content to be recommended to gzip. This may change, but it seems reasonable enough for now. 11952 11953 History: 11954 Added January 28, 2023 (dub v11.0) 11955 +/ 11956 bool gzipRecommendedForContentType(string contentType) { 11957 if(contentType.startsWith("text/")) 11958 return true; 11959 if(contentType.startsWith("application/javascript")) 11960 return true; 11961 11962 return false; 11963 } 11964 11965 /++ 11966 Serves a static file. To be used with [dispatcher]. 11967 11968 See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect] 11969 +/ 11970 auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) { 11971 // https://baus.net/on-tcp_cork/ 11972 // man 2 sendfile 11973 assert(urlPrefix[0] == '/'); 11974 if(filename is null) 11975 filename = decodeUriComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? 11976 if(contentType is null) { 11977 contentType = contentTypeFromFileExtension(filename); 11978 } 11979 11980 static struct DispatcherDetails { 11981 string filename; 11982 string contentType; 11983 } 11984 11985 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 11986 if(details.contentType.indexOf("image/") == 0 || details.contentType.indexOf("audio/") == 0) 11987 cgi.setCache(true); 11988 cgi.setResponseContentType(details.contentType); 11989 cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); 11990 cgi.write(std.file.read(details.filename), true); 11991 return true; 11992 } 11993 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); 11994 } 11995 11996 /++ 11997 Serves static data. To be used with [dispatcher]. 11998 11999 History: 12000 Added October 31, 2021 12001 +/ 12002 auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { 12003 assert(urlPrefix[0] == '/'); 12004 if(contentType is null) { 12005 contentType = contentTypeFromFileExtension(urlPrefix); 12006 } 12007 12008 static struct DispatcherDetails { 12009 immutable(void)[] data; 12010 string contentType; 12011 } 12012 12013 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12014 cgi.setCache(true); 12015 cgi.setResponseContentType(details.contentType); 12016 cgi.write(details.data, true); 12017 return true; 12018 } 12019 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); 12020 } 12021 12022 string contentTypeFromFileExtension(string filename) { 12023 import arsd.core; 12024 return FilePath(filename).contentTypeFromFileExtension(); 12025 } 12026 12027 /// This serves a directory full of static files, figuring out the content-types from file extensions. 12028 /// It does not let you to descend into subdirectories (or ascend out of it, of course) 12029 auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool recursive = false) { 12030 assert(urlPrefix[0] == '/'); 12031 assert(urlPrefix[$-1] == '/'); 12032 12033 static struct DispatcherDetails { 12034 string directory; 12035 bool recursive; 12036 } 12037 12038 if(directory is null) 12039 directory = urlPrefix[1 .. $]; 12040 12041 if(directory.length == 0) 12042 directory = "./"; 12043 12044 assert(directory[$-1] == '/'); 12045 12046 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12047 auto file = decodeUriComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct 12048 12049 if(details.recursive) { 12050 // never allow a backslash since it isn't in a typical url anyway and makes the following checks easier 12051 if(file.indexOf("\\") != -1) 12052 return false; 12053 12054 import std.path; 12055 12056 file = std.path.buildNormalizedPath(file); 12057 enum upOneDir = ".." ~ std.path.dirSeparator; 12058 12059 // also no point doing any kind of up directory things since that makes it more likely to break out of the parent 12060 if(file == ".." || file.startsWith(upOneDir)) 12061 return false; 12062 if(std.path.isAbsolute(file)) 12063 return false; 12064 12065 // FIXME: if it has slashes and stuff, should we redirect to the canonical resource? or what? 12066 12067 // once it passes these filters it is probably ok. 12068 } else { 12069 if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) 12070 return false; 12071 } 12072 12073 if(file.length == 0) 12074 return false; 12075 12076 auto contentType = contentTypeFromFileExtension(file); 12077 12078 auto fn = details.directory ~ file; 12079 if(std.file.exists(fn)) { 12080 //if(contentType.indexOf("image/") == 0) 12081 //cgi.setCache(true); 12082 //else if(contentType.indexOf("audio/") == 0) 12083 cgi.setCache(true); 12084 cgi.setResponseContentType(contentType); 12085 cgi.gzipResponse = gzipRecommendedForContentType(contentType); 12086 cgi.write(std.file.read(fn), true); 12087 return true; 12088 } else { 12089 return false; 12090 } 12091 } 12092 12093 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, recursive)); 12094 } 12095 12096 /++ 12097 Redirects one url to another 12098 12099 See_Also: [dispatcher], [serveStaticFile] 12100 +/ 12101 auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) { 12102 assert(urlPrefix[0] == '/'); 12103 static struct DispatcherDetails { 12104 string redirectTo; 12105 string code; 12106 } 12107 12108 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12109 cgi.setResponseLocation(details.redirectTo, true, details.code); 12110 return true; 12111 } 12112 12113 12114 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code))); 12115 } 12116 12117 /// Used exclusively with `dispatchTo` 12118 struct DispatcherData(Presenter) { 12119 Cgi cgi; /// You can use this cgi object. 12120 Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher. 12121 size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only. 12122 } 12123 12124 /++ 12125 Dispatches the URL to a specific function. 12126 +/ 12127 auto handleWith(alias handler)(string urlPrefix) { 12128 // cuz I'm too lazy to do it better right now 12129 static class Hack : WebObject { 12130 static import std.traits; 12131 @UrlName("") 12132 auto handle(std.traits.Parameters!handler args) { 12133 return handler(args); 12134 } 12135 } 12136 12137 return urlPrefix.serveApiInternal!Hack; 12138 } 12139 12140 /++ 12141 Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this: 12142 12143 --- 12144 bool other(DD)(DD dd) { 12145 return dd.dispatcher!( 12146 "/whatever".serveRedirect("/success"), 12147 "/api/".serveApi!MyClass 12148 ); 12149 } 12150 --- 12151 12152 The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher 12153 here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters. 12154 Or, of course, you could just use the exact type in your own code. 12155 12156 You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a 12157 good job. 12158 12159 12160 +/ 12161 auto dispatchTo(alias handler)(string urlPrefix) { 12162 assert(urlPrefix[0] == '/'); 12163 assert(urlPrefix[$-1] != '/'); 12164 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { 12165 return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); 12166 } 12167 12168 return DispatcherDefinition!(internalHandler)(urlPrefix, false); 12169 } 12170 12171 /++ 12172 See [serveStaticFile] if you want to serve a file off disk. 12173 12174 History: 12175 Added January 28, 2023 (dub v11.0) 12176 +/ 12177 auto serveStaticData(string urlPrefix, immutable(ubyte)[] data, string contentType, string filenameToSuggestAsDownload = null) { 12178 assert(urlPrefix[0] == '/'); 12179 12180 static struct DispatcherDetails { 12181 immutable(ubyte)[] data; 12182 string contentType; 12183 string filenameToSuggestAsDownload; 12184 } 12185 12186 static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { 12187 cgi.setCache(true); 12188 cgi.setResponseContentType(details.contentType); 12189 if(details.filenameToSuggestAsDownload.length) 12190 cgi.header("Content-Disposition: attachment; filename=\""~details.filenameToSuggestAsDownload~"\""); 12191 cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); 12192 cgi.write(details.data, true); 12193 return true; 12194 } 12195 return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType, filenameToSuggestAsDownload)); 12196 } 12197 12198 /++ 12199 Placeholder for use with [dispatchSubsection]'s `NewPresenter` argument to indicate you want to keep the parent's presenter. 12200 12201 History: 12202 Added January 28, 2023 (dub v11.0) 12203 +/ 12204 alias KeepExistingPresenter = typeof(null); 12205 12206 /++ 12207 For use with [dispatchSubsection]. Calls your filter with the request and if your filter returns false, 12208 this issues the given errorCode and stops processing. 12209 12210 --- 12211 bool hasAdminPermissions(Cgi cgi) { 12212 return true; 12213 } 12214 12215 mixin DispatcherMain!( 12216 "/admin".dispatchSubsection!( 12217 passFilterOrIssueError!(hasAdminPermissions, 403), 12218 KeepExistingPresenter, 12219 "/".serveApi!AdminFunctions 12220 ) 12221 ); 12222 --- 12223 12224 History: 12225 Added January 28, 2023 (dub v11.0) 12226 +/ 12227 template passFilterOrIssueError(alias filter, int errorCode) { 12228 bool passFilterOrIssueError(DispatcherDetails)(DispatcherDetails dd) { 12229 if(filter(dd.cgi)) 12230 return true; 12231 dd.presenter.renderBasicError(dd.cgi, errorCode); 12232 return false; 12233 } 12234 } 12235 12236 /++ 12237 Allows for a subsection of your dispatched urls to be passed through other a pre-request filter, optionally pick up an new presenter class, 12238 and then be dispatched to their own handlers. 12239 12240 --- 12241 /+ 12242 // a long-form filter function 12243 bool permissionCheck(DispatcherData)(DispatcherData dd) { 12244 // you are permitted to call mutable methods on the Cgi object 12245 // Note if you use a Cgi subclass, you can try dynamic casting it back to your custom type to attach per-request data 12246 // though much of the request is immutable so there's only so much you're allowed to do to modify it. 12247 12248 if(checkPermissionOnRequest(dd.cgi)) { 12249 return true; // OK, allow processing to continue 12250 } else { 12251 dd.presenter.renderBasicError(dd.cgi, 403); // reply forbidden to the requester 12252 return false; // and stop further processing into this subsection 12253 } 12254 } 12255 +/ 12256 12257 // but you can also do short-form filters: 12258 12259 bool permissionCheck(Cgi cgi) { 12260 return ("ok" in cgi.get) !is null; 12261 } 12262 12263 // handler for the subsection 12264 class AdminClass : WebObject { 12265 int foo() { return 5; } 12266 } 12267 12268 // handler for the main site 12269 class TheMainSite : WebObject {} 12270 12271 mixin DispatcherMain!( 12272 "/admin".dispatchSubsection!( 12273 // converts our short-form filter into a long-form filter 12274 passFilterOrIssueError!(permissionCheck, 403), 12275 // can use a new presenter if wanted for the subsection 12276 KeepExistingPresenter, 12277 // and then provide child route dispatchers 12278 "/".serveApi!AdminClass 12279 ), 12280 // and back to the top level 12281 "/".serveApi!TheMainSite 12282 ); 12283 --- 12284 12285 Note you can encapsulate sections in files like this: 12286 12287 --- 12288 auto adminDispatcher(string urlPrefix) { 12289 return urlPrefix.dispatchSubsection!( 12290 .... 12291 ); 12292 } 12293 12294 mixin DispatcherMain!( 12295 "/admin".adminDispatcher, 12296 // and so on 12297 ) 12298 --- 12299 12300 If you want no filter, you can pass `(cgi) => true` as the filter to approve all requests. 12301 12302 If you want to keep the same presenter as the parent, use [KeepExistingPresenter] as the presenter argument. 12303 12304 12305 History: 12306 Added January 28, 2023 (dub v11.0) 12307 +/ 12308 auto dispatchSubsection(alias PreRequestFilter, NewPresenter, definitions...)(string urlPrefix) { 12309 assert(urlPrefix[0] == '/'); 12310 assert(urlPrefix[$-1] != '/'); 12311 static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { 12312 static if(!is(PreRequestFilter == typeof(null))) { 12313 if(!PreRequestFilter(DispatcherData!Presenter(cgi, presenter, urlPrefix.length))) 12314 return true; // we handled it by rejecting it 12315 } 12316 12317 static if(is(NewPresenter == Presenter) || is(NewPresenter == typeof(null))) { 12318 return dispatcher!definitions(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); 12319 } else { 12320 auto newPresenter = new NewPresenter(); 12321 return dispatcher!(definitions(DispatcherData!NewPresenter(cgi, presenter, urlPrefix.length))); 12322 } 12323 } 12324 12325 return DispatcherDefinition!(internalHandler)(urlPrefix, false); 12326 } 12327 12328 /++ 12329 A URL dispatcher. 12330 12331 --- 12332 if(cgi.dispatcher!( 12333 "/api/".serveApi!MyApiClass, 12334 "/objects/lol".serveRestObject!MyRestObject, 12335 "/file.js".serveStaticFile, 12336 "/admin/".dispatchTo!adminHandler 12337 )) return; 12338 --- 12339 12340 12341 You define a series of url prefixes followed by handlers. 12342 12343 You may want to do different pre- and post- processing there, for example, 12344 an authorization check and different page layout. You can use different 12345 presenters and different function chains. See [dispatchSubsection] for details. 12346 12347 [dispatchTo] will send the request to another function for handling. 12348 +/ 12349 template dispatcher(definitions...) { 12350 bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) { 12351 static if(is(Presenter == typeof(null))) { 12352 static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {} 12353 auto presenter = new GenericWebPresenter(); 12354 } else 12355 alias presenter = presenterArg; 12356 12357 return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0)); 12358 } 12359 12360 bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) { 12361 // I can prolly make this more efficient later but meh. 12362 foreach(definition; definitions) { 12363 if(definition.rejectFurther) { 12364 if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) { 12365 auto ret = definition.handler( 12366 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 12367 dispatcherData.cgi, dispatcherData.presenter, definition.details); 12368 if(ret) 12369 return true; 12370 } 12371 } else if( 12372 dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) && 12373 // cgi.d dispatcher urls must be complete or have a /; 12374 // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing" 12375 (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length 12376 || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/') 12377 ) { 12378 auto ret = definition.handler( 12379 dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], 12380 dispatcherData.cgi, dispatcherData.presenter, definition.details); 12381 if(ret) 12382 return true; 12383 } 12384 } 12385 return false; 12386 } 12387 } 12388 12389 }); 12390 12391 private struct StackBuffer { 12392 char[1024] initial = void; 12393 char[] buffer; 12394 size_t position; 12395 12396 this(int a) { 12397 buffer = initial[]; 12398 position = 0; 12399 } 12400 12401 void add(in char[] what) { 12402 if(position + what.length > buffer.length) 12403 buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases 12404 buffer[position .. position + what.length] = what[]; 12405 position += what.length; 12406 } 12407 12408 void add(in char[] w1, in char[] w2, in char[] w3 = null) { 12409 add(w1); 12410 add(w2); 12411 add(w3); 12412 } 12413 12414 void add(long v) { 12415 char[16] buffer = void; 12416 auto pos = buffer.length; 12417 bool negative; 12418 if(v < 0) { 12419 negative = true; 12420 v = -v; 12421 } 12422 do { 12423 buffer[--pos] = cast(char) (v % 10 + '0'); 12424 v /= 10; 12425 } while(v); 12426 12427 if(negative) 12428 buffer[--pos] = '-'; 12429 12430 auto res = buffer[pos .. $]; 12431 12432 add(res[]); 12433 } 12434 12435 char[] get() @nogc { 12436 return buffer[0 .. position]; 12437 } 12438 } 12439 12440 // duplicated in http2.d 12441 private static string getHttpCodeText(int code) pure nothrow @nogc { 12442 switch(code) { 12443 case 200: return "200 OK"; 12444 case 201: return "201 Created"; 12445 case 202: return "202 Accepted"; 12446 case 203: return "203 Non-Authoritative Information"; 12447 case 204: return "204 No Content"; 12448 case 205: return "205 Reset Content"; 12449 case 206: return "206 Partial Content"; 12450 // 12451 case 300: return "300 Multiple Choices"; 12452 case 301: return "301 Moved Permanently"; 12453 case 302: return "302 Found"; 12454 case 303: return "303 See Other"; 12455 case 304: return "304 Not Modified"; 12456 case 305: return "305 Use Proxy"; 12457 case 307: return "307 Temporary Redirect"; 12458 case 308: return "308 Permanent Redirect"; 12459 12460 // 12461 case 400: return "400 Bad Request"; 12462 case 401: return "401 Unauthorized"; 12463 case 402: return "402 Payment Required"; 12464 case 403: return "403 Forbidden"; 12465 case 404: return "404 Not Found"; 12466 case 405: return "405 Method Not Allowed"; 12467 case 406: return "406 Not Acceptable"; 12468 case 407: return "407 Proxy Authentication Required"; 12469 case 408: return "408 Request Timeout"; 12470 case 409: return "409 Conflict"; 12471 case 410: return "410 Gone"; 12472 case 411: return "411 Length Required"; 12473 case 412: return "412 Precondition Failed"; 12474 case 413: return "413 Payload Too Large"; 12475 case 414: return "414 URI Too Long"; 12476 case 415: return "415 Unsupported Media Type"; 12477 case 416: return "416 Range Not Satisfiable"; 12478 case 417: return "417 Expectation Failed"; 12479 case 418: return "418 I'm a teapot"; 12480 case 421: return "421 Misdirected Request"; 12481 case 422: return "422 Unprocessable Entity (WebDAV)"; 12482 case 423: return "423 Locked (WebDAV)"; 12483 case 424: return "424 Failed Dependency (WebDAV)"; 12484 case 425: return "425 Too Early"; 12485 case 426: return "426 Upgrade Required"; 12486 case 428: return "428 Precondition Required"; 12487 case 431: return "431 Request Header Fields Too Large"; 12488 case 451: return "451 Unavailable For Legal Reasons"; 12489 12490 case 500: return "500 Internal Server Error"; 12491 case 501: return "501 Not Implemented"; 12492 case 502: return "502 Bad Gateway"; 12493 case 503: return "503 Service Unavailable"; 12494 case 504: return "504 Gateway Timeout"; 12495 case 505: return "505 HTTP Version Not Supported"; 12496 case 506: return "506 Variant Also Negotiates"; 12497 case 507: return "507 Insufficient Storage (WebDAV)"; 12498 case 508: return "508 Loop Detected (WebDAV)"; 12499 case 510: return "510 Not Extended"; 12500 case 511: return "511 Network Authentication Required"; 12501 // 12502 default: assert(0, "Unsupported http code"); 12503 } 12504 } 12505 12506 12507 /+ 12508 /++ 12509 This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object. 12510 12511 It relies on jsvar.d and dom.d. 12512 12513 12514 You can get javascript out of it to call. The generated functions need to look 12515 like 12516 12517 function name(a,b,c,d,e) { 12518 return _call("name", {"realName":a,"sds":b}); 12519 } 12520 12521 And _call returns an object you can call or set up or whatever. 12522 +/ 12523 bool apiDispatcher()(Cgi cgi) { 12524 import arsd.jsvar; 12525 import arsd.dom; 12526 } 12527 +/ 12528 version(linux) 12529 private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; 12530 /* 12531 Copyright: Adam D. Ruppe, 2008 - 2023 12532 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. 12533 Authors: Adam D. Ruppe 12534 12535 Copyright Adam D. Ruppe 2008 - 2023. 12536 Distributed under the Boost Software License, Version 1.0. 12537 (See accompanying file LICENSE_1_0.txt or copy at 12538 http://www.boost.org/LICENSE_1_0.txt) 12539 */