├── .github └── workflows │ └── d.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── api ├── README.md ├── danode.d ├── danode.php ├── danode.pm └── danode.r ├── danode ├── cgi.d ├── client.d ├── filesystem.d ├── functions.d ├── http.d ├── https.d ├── imports.d ├── interfaces.d ├── log.d ├── mimetypes.d ├── payload.d ├── post.d ├── process.d ├── request.d ├── response.d ├── router.d ├── server.d ├── serverconfig.d ├── signals.d ├── ssl.d ├── statuscode.d └── webconfig.d ├── dub.json ├── sh ├── compile ├── console ├── debug ├── doc ├── letsEncrypt ├── run ├── rundebug ├── selfSignedKey └── stop ├── test ├── dmd.in ├── empty.req ├── gdb.in ├── ldc2compile ├── malformed.sh ├── server.files │ └── server.conf ├── ssl.conf │ ├── README.md │ ├── localhost.cnf │ └── wordpress.test.cnf └── stress.d └── www ├── bludit.test └── web.config ├── localhost ├── ISE1.d ├── ISE2.d ├── ISE3.d ├── data.ill ├── dmd.d ├── etc │ └── style.css ├── index.html ├── keepalive.d ├── long.d ├── noheader.d ├── perl.pl ├── perlinfo.pl ├── php-cgi.fphp ├── php.php ├── phpinfo.fphp ├── rscript.r ├── test.ada ├── test.bf ├── test.txt ├── test │ ├── 1.txt │ └── 2.txt └── web.config └── wordpress.test ├── sql ├── createWordpress.sql └── deleteWordpress.sql └── web.config /.github/workflows/d.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | name: D 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7 21 | 22 | - name: 'Build & Test' 23 | run: | 24 | dub build --compiler=$DC 25 | dub test --compiler=$DC 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | www/dannyarends.nl 2 | www/dannyarends.test 3 | www/rotationz.nl 4 | www/rotationz.test 5 | www/grassland.test 6 | www/localhost/ddoc 7 | danode/server 8 | danode/server.exe 9 | danode/danode-test-default.exe 10 | server 11 | server.exe 12 | 13 | www/danny.test/ 14 | www/localhost/123 123.txt 15 | 16 | server.crt 17 | server.csr 18 | server.key 19 | server.key.org 20 | 21 | .ssl 22 | .vscode 23 | 24 | test/ssl.conf/dannyarends.nl.cnf 25 | test/stress 26 | stress 27 | 28 | www/financial.nl/db 29 | www/financial.nl/img 30 | 31 | www/qrcode.test/QR/cache/* 32 | www/qrcode.test/QR/gen/* 33 | 34 | *.in 35 | *.err 36 | *.lst 37 | *.log 38 | *.def 39 | *.err 40 | *.out 41 | *~ 42 | *.pdf 43 | *.zip 44 | 45 | .sass-cache 46 | .htaccess 47 | 48 | .dub/ 49 | dub.selections.json 50 | 51 | letsEncrypt_da.nl 52 | www/*.nl/ 53 | 54 | www/bludit.test/*.php 55 | www/bludit.test/LICENSE 56 | www/bludit.test/bl-content 57 | www/bludit.test/bl-kernel 58 | www/bludit.test/bl-languages 59 | www/bludit.test/bl-plugins 60 | www/bludit.test/bl-themes 61 | 62 | www/wordpress.test/*.php 63 | www/wordpress.test/*.txt 64 | www/wordpress.test/*.html 65 | www/wordpress.test/wp-admin/ 66 | www/wordpress.test/wp-content/ 67 | www/wordpress.test/wp-includes/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DaNode - A secure and small footprint web server for D 2 | ------------------------------------------------------ 3 | master: [![D](https://github.com/DannyArends/DaNode/actions/workflows/d.yml/badge.svg?branch=master)](https://github.com/DannyArends/DaNode/actions/workflows/d.yml) 4 | licence: [![license](https://img.shields.io/github/license/DannyArends/DaNode.svg?style=flat)](https://github.com/DannyArends/DaNode/blob/master/LICENSE.txt) 5 | 6 | Web server written in the [D programming language](https://dlang.org/) to 7 | host server side web applications (written in any programming language) on multiple domains. 8 | It provides encrypted HTTP content over SSL using 9 | [Server Name Identification](https://en.wikipedia.org/wiki/Server_Name_Indication), 10 | and has been battle tested in production for over 5 years hosting several of my 11 | web domains, such as my own [personal website](https://www.dannyarends.nl/). 12 | 13 | Written because I was looking for a quick way of sharing 14 | [Rscript](https://www.r-project.org/about.html) output with other researchers at 15 | different universities. Once working I wanted to use other programming languages as 16 | well and added generic support for other languages. Use any language to 17 | write your SSL encrypted homepage, why not 18 | [brainfuck](https://en.wikipedia.org/wiki/Brainfuck), however 19 | [Ada](https://en.wikipedia.org/wiki/Ada), 20 | [R](https://www.r-project.org) or 21 | [PHP](https://en.wikipedia.org/wiki/PHP) are also fine. 22 | 23 | The web servers main features / aims summarized: 24 | - Support server side web applications written in any programming language 25 | - SSL/HTTPs support by [openSSL](https://www.openssl.org/) and [Deimos bindings](https://github.com/D-Programming-Deimos/openssl) 26 | - [Server Name Identification](https://en.wikipedia.org/wiki/Server_Name_Indication) by using multiple free [Let's encrypt](https://letsencrypt.org/) certificates 27 | - Small footprint: Code, CPU and RAM 28 | - API support for PHP, Python, D, R, or add your own in: [api/](api/) 29 | - [Example](www/localhost/) web applications, including [PHP](www/localhost/php.php), [Perl](www/localhost/perl.pl), [D](www/localhost/keepalive.d), [R](www/localhost/rscript.r), [brainfuck](www/localhost/test.bf) and [Ada](www/localhost/test.ada). 30 | 31 | ##### Get DaNode 32 | 33 | Install the DMD compiler from [https://dlang.org/](https://dlang.org/download.html) 34 | 35 | Clone the source code from Github 36 | 37 | git clone https://github.com/DannyArends/DaNode.git 38 | cd DaNode 39 | 40 | Build DaNode using the dub package manager 41 | 42 | dub build 43 | 44 | Another option is to compile using the compile script 45 | 46 | ./sh/compile 47 | 48 | Start the web server at a specific port (e.g. 8080) 49 | 50 | ./danode/server -p 8080 51 | 52 | Confirm that the web server is running by going to: http://localhost:8080/ 53 | 54 | ##### Enable HTTPs support 55 | 56 | To compile the server with HTTPS support (binds to port 443), use dub and specify 57 | the _ssl_ configuration: 58 | 59 | dub build --config=ssl 60 | 61 | or, compile using the compile script: 62 | 63 | ./sh/compile ssl 64 | 65 | Start the web server on port 80 and 443: 66 | 67 | ./danode/server 68 | 69 | After starting the server, confirm that the web server is running by going to http://127.0.0.1/ 70 | and https://127.0.0.1/ and make sure you have enough user rights to bind port 80 and 443, a server 71 | private key and domain certificates are required. I use Let's Encrypt to secure my own homepage. 72 | Setup instructions for Let's Encrypt can be found in the [sh/letsEncrypt](sh/letsEncrypt) file. 73 | 74 | ##### Troubleshooting: [ERROR] unable to bind socket on port 80 75 | 76 | Starting the server on port 80 and 443 might fail, when you do not have appropriate 77 | rights on the system. First check if you can start the server on another port: 78 | 79 | ./danode/server -p 8080 80 | 81 | I use _nohup_ and _authbind_ to start the web server in deamon (background) mode at port 80, and 443 (SSL). 82 | First, install _nohup_ and _authbind_ via your package manager, configure _authbind_ to allow 83 | connections to port 80 (and 443, when using the ssl version), then start the webserver by running: 84 | 85 | ./sh/run 86 | 87 | ##### Command-line parameters 88 | 89 | The content of the ./sh/run shell script: 90 | 91 | nohup authbind danode/server -k -b 100 -v 2 > server.log 2>&1 & 92 | 93 | This starts the server, does not allow for keyboard command (-k) has a backlog (-b) 94 | of 100 simultaneous connection (per port), and produces more log output (-v 2). 95 | 96 | --port -p HTTP port to listen on (integer) 97 | --backlog -b Backlog of clients supported simultaneously per port (integer) 98 | --keyoff -k Keyboard input via STDIN (boolean) 99 | --certDir Location of folder with SSL certificates (string) 100 | --keyFile Server private key location (string) 101 | --wwwRoot Server www root folder holding website domains (string) 102 | --verbose -v Verbose level, logs on STDOUT (integer) 103 | 104 | ##### Example websites 105 | 106 | See the [www/](www/) folder for a number of example web sites. After compiling the web 107 | server, run the web server and the [www/localhost/](www/localhost/) folder is available 108 | at http://localhost/ or http://127.0.0.1/ from the browser. For the other examples in 109 | the [www/](www/) folder you will have to update your hosts file. 110 | 111 | ##### Create a PHP enabled website 112 | 113 | To create a simple PHP enabled web site first download and install DaNode, the next 114 | step is to create a directory for the new website, by executing the following commands 115 | from the DaNode directory: 116 | 117 | mkdir www/domain.xxx 118 | touch www/domain.xxx/index.php 119 | 120 | Add some php / html content to the index page, and create a web.config file: 121 | 122 | touch www/domain.xxx/web.config 123 | 124 | Add the following configuration settings to the web.config file, if you want to use 125 | scripting languages such as PHP, you have to manually allow the execution of cgi file. 126 | Add the following lines in your web.cofig file to redirect to the index.php file, and 127 | allow the webserver to execute the php script, and redirect the incomming requests to 128 | the index.php page: 129 | 130 | allowcgi = yes 131 | redirecturl = index.php 132 | 133 | ##### Update the hosts file 134 | 135 | If you do not own the domain name you want to host, use the /etc/hosts file to redirect 136 | requests from the domain name to your local IP address using the hosts file: 137 | 138 | sudo nano /etc/hosts 139 | 140 | Then add the following lines to this hostfile using your favourite editor: 141 | 142 | 127.0.0.1 domain.xxx 143 | 127.0.0.1 www.domain.xxx 144 | 145 | Save the file with these lines added, then open a browser and navigate to: 146 | http://www.domain.xxx, you should now see the content of your php / html file. 147 | 148 | ##### Supported back-end languages 149 | 150 | Languages with supported APIs: PHP, PYTHON, D, R 151 | 152 | See: [api/README.md](api/README.md) 153 | 154 | ##### Contributing 155 | 156 | Want to contribute? Great! Contribute to DaNode by starring or forking on Github, 157 | and feel free to start an issue or sending a pull request. 158 | 159 | Fell free to also post comments on commits. 160 | 161 | Or be a maintainer, and adopt (the documentation of) a function. 162 | 163 | ##### License 164 | 165 | DaNode is written by Danny Arends and is released under the GNU GENERAL PUBLIC 166 | LICENSE Version 3 (GPLv3). See [LICENSE.txt](LICENSE.txt). 167 | 168 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # APIs 2 | 3 | The APIs provide access to POST and GET data in hosted CGI applications. 4 | 5 | ## PHP 6 | See a working example [HERE](../www/localhost/php.php). 7 | 8 | ```PHP 9 | 10 | ``` 11 | 12 | Common variables, are available after including the api: 13 | - $_CONFIG - The variables loaded from the web.config file 14 | - $_SERVER - Server related information 15 | - $_GET - GET variables 16 | - $_POST - POST variables 17 | - $_COOKIE - Retrieve cookies set using the setcookie() function 18 | 19 | ## PERL 20 | See a working example [HERE](../www/localhost/perl.pl) 21 | 22 | ```Perl 23 | use api::danode; 24 | ``` 25 | 26 | ## D 27 | See a working example [HERE](../www/localhost/dmd.d) 28 | 29 | ```D 30 | import api.danode; 31 | 32 | void main(string[] args){ 33 | setGET(args); // Set the GET variables from the cmd args 34 | } 35 | ``` 36 | 37 | ## R 38 | See a working example [HERE](../www/localhost/rscript.r) 39 | 40 | ```R 41 | source("api/danode.r") 42 | ``` 43 | -------------------------------------------------------------------------------- /api/danode.d: -------------------------------------------------------------------------------- 1 | module api.danode; 2 | import std.stdio, std.getopt, std.conv,std.utf, std.string, std.file; 3 | 4 | void setGET(string[] args){ 5 | foreach(arg;args[1..$]){ 6 | string[] s = arg.split("="); 7 | if(s.length > 1) GET[toUTF8(s[0])] = toUTF8(s[1]); 8 | } 9 | } 10 | 11 | void setCONFIG() { 12 | string myloc = "./"; 13 | if(SERVER) myloc = SERVER["SCRIPT_FILENAME"]; 14 | string configfile = myloc[0 .. (myloc.lastIndexOf("/"))] ~ "/web.config"; 15 | if(exists(configfile)){ 16 | string[] configcont = to!string(std.file.read(configfile)).split("\n"); 17 | foreach(line; configcont){ 18 | if(chomp(line) != "" && line[0] != '#'){ 19 | string[] s = line.split("="); 20 | CONFIG[chomp(strip(s[0]))] = chomp(strip(s[1])); 21 | } 22 | } 23 | } 24 | } 25 | 26 | void move_upload_file(string tmp, string to){ 27 | if(tmp != "") return copy(tmp, to); 28 | } 29 | 30 | struct FileInfo{ 31 | string name; 32 | string mime; 33 | string loc; 34 | } 35 | 36 | string[string] CONFIG; 37 | string[string] COOKIES; 38 | string[string] GET; 39 | string[string] POST; 40 | string[string] SERVER; 41 | FileInfo[string] FILES; 42 | 43 | void setPOST(){ 44 | char[] buf; 45 | if(ftell(stdin.getFP()) == -1) return; 46 | while(stdin.readln(buf)){ 47 | string s = toUTF8(chomp(to!string(buf))); 48 | if(s == "") return; 49 | string[] splitted = s.split("="); 50 | if(splitted.length > 2){ 51 | if(splitted[0] == "S") SERVER[splitted[1]] = chomp(strip(splitted[2])); 52 | if(splitted[0] == "P") POST[splitted[1]] = chomp(strip(splitted[2])); 53 | if(splitted[0] == "F"){ 54 | POST[splitted[1]] = chomp(strip(splitted[2])); 55 | FILES[splitted[1]] = FileInfo(chomp(strip(splitted[2])), chomp(strip(splitted[3])),chomp(strip(splitted[4]))); 56 | } 57 | if(splitted[0] == "C") COOKIES[splitted[1]] = chomp(strip(splitted[2])); 58 | } 59 | } 60 | } 61 | 62 | static this(){ 63 | setPOST(); 64 | setCONFIG(); 65 | } 66 | 67 | -------------------------------------------------------------------------------- /api/danode.php: -------------------------------------------------------------------------------- 1 | $a){ 9 | $ret .= '"'. $i . '":"' . $a .'"'; 10 | if($size > 1){ $ret .= ', '; $size--; } 11 | } 12 | return($ret . ']'); 13 | } 14 | 15 | function readConfig($argv){ 16 | $config = []; 17 | $idx = strpos(strrev($argv[0]),"/"); 18 | $idx = strlen($argv[0])-strlen("/")-$idx; 19 | $configloc = str_split($argv[0],$idx); 20 | $configloc = $configloc[0]."/web.config"; 21 | if(file_exists($configloc)){ 22 | $configcont = explode("\n", file_get_contents($configloc)); 23 | foreach($configcont as $line){ 24 | if(substr($line,0,1) != '#' && chop($line) != ""){ 25 | $marray = explode('=', $line); 26 | $config[chop($marray[0])] = strrev(chop(strrev(chop($marray[1])))); 27 | } 28 | } 29 | } 30 | return $config; 31 | } 32 | 33 | function move_upload_file($tmp, $to){ 34 | if($tmp != '') return copy($tmp, $to); 35 | } 36 | 37 | $_REQUEST = $_GET; 38 | $_SERVER = Array(); 39 | $_COOKIE = Array(); 40 | $_FILES = Array(); 41 | 42 | $_CONFIG = readConfig($argv); 43 | $f = fopen( 'php://stdin', 'r' ); 44 | stream_set_blocking($f, 0); 45 | 46 | while(false !== ($line = fgets($f))){ 47 | $marray = explode('=', $line); 48 | if(isset($marray[0]) && isset($marray[1]) && isset($marray[2])){ 49 | if($marray[0] == "S"){ 50 | $_SERVER[urldecode($marray[1])] = urldecode(chop(join("=", array_slice($marray, 2)))); 51 | }else if($marray[0] == "C"){ 52 | $_COOKIE[urldecode($marray[1])] = urldecode(chop(join("=", array_slice($marray, 2)))); 53 | }else if($marray[0] == "F"){ 54 | $_FILES[urldecode($marray[1])]["name"][urldecode(chop($marray[2]))] = urldecode(chop($marray[2])); 55 | $_FILES[urldecode($marray[1])]["mime"][urldecode(chop($marray[2]))] = urldecode(chop($marray[3])); 56 | $_FILES[urldecode($marray[1])]['error'][urldecode(chop($marray[2]))] = 0; 57 | $_FILES[urldecode($marray[1])]["tmp_name"][urldecode(chop($marray[2]))] = urldecode(chop($marray[4])); 58 | }else{ 59 | $_POST[urldecode($marray[1])] = urldecode(chop(join("=", array_slice($marray, 2)))); 60 | } 61 | } 62 | } 63 | fclose($f); 64 | 65 | $api_loaded = true; 66 | return 1; 67 | ?> 68 | -------------------------------------------------------------------------------- /api/danode.pm: -------------------------------------------------------------------------------- 1 | package api::danode; 2 | use strict; 3 | use Exporter 'import'; 4 | use URI::Escape; 5 | 6 | our @EXPORT = qw/&toS $_GET $_POST/; 7 | our $_GET = getGET(); 8 | our $_POST = getPOST(); 9 | 10 | sub getGET{ 11 | my $pref = {}; 12 | foreach my $p (@ARGV){ 13 | my @array = split(/=/,$p); 14 | $pref->{$array[0]} = uri_unescape($array[1]); 15 | } 16 | return $pref; 17 | } 18 | 19 | sub toS{ 20 | my $HREF = shift; 21 | my $ret = "["; 22 | my $cnt = keys(%$HREF); 23 | for(keys %$HREF){ 24 | $ret .= "\"" . $_ . "\":\"" . $HREF->{$_} . "\""; 25 | if($cnt > 1){ $ret .= ", "; $cnt--; } 26 | } 27 | $ret .= "]"; 28 | return $ret; 29 | } 30 | 31 | sub getPOST{ 32 | my $pref = {}; 33 | if(not(-t STDIN)){ 34 | my @lines = ; 35 | foreach my $line (@lines){ 36 | chomp($line); 37 | my @array = split(/=/,$line); 38 | $pref->{$array[1]} = uri_unescape($array[2]); 39 | } 40 | } 41 | return $pref; 42 | } 43 | 44 | return 1; 45 | -------------------------------------------------------------------------------- /api/danode.r: -------------------------------------------------------------------------------- 1 | 2 | getGET <- function(){ 3 | entval <- strsplit(commandArgs(TRUE),"=") 4 | GET <- unlist(lapply(entval,"[[",2)) 5 | names(GET) <- unlist(lapply(entval,"[[",1)) 6 | return(GET) 7 | } 8 | 9 | POST <- NULL; pnames <- NULL; 10 | SERVER <- NULL; snames <- NULL; 11 | f <- file("stdin") 12 | open(f,"rt") 13 | while(length(line <- readLines(f,1)) > 0) { 14 | elems <- strsplit(line,"=")[[1]] 15 | if(length(elems) >= 2){ 16 | if(elems[1] %in% c("P","F")){ 17 | pnames <- c(pnames, elems[2]) 18 | if(length(elems) == 3){ 19 | POST <- c(POST, URLdecode(elems[3])) 20 | }else{ 21 | POST <- c(POST, "") 22 | } } 23 | if(elems[1] == "S"){ 24 | snames <- c(snames, elems[2]) 25 | if(length(elems) == 3){ 26 | SERVER <- c(SERVER, URLdecode(elems[3])) 27 | }else{ 28 | SERVER <- c(SERVER, "") 29 | } } 30 | } 31 | } 32 | names(POST) <- pnames 33 | names(SERVER) <- snames 34 | 35 | toS <- function(args){ 36 | return(paste("['",paste(names(args),args,collapse="','",sep="':'"),"']",sep="")) 37 | } 38 | 39 | GET <- getGET() 40 | 41 | -------------------------------------------------------------------------------- /danode/cgi.d: -------------------------------------------------------------------------------- 1 | module danode.cgi; 2 | 3 | import danode.imports; 4 | import danode.functions : bodystart, endofheader, fullheader; 5 | import danode.log : error; 6 | import danode.process : Process; 7 | import danode.statuscode : StatusCode; 8 | import danode.payload : HeaderType, Payload, PayloadType; 9 | 10 | // Class structure for common gateway interface (CGI) scripts 11 | class CGI : Payload { 12 | private: 13 | Process external; 14 | 15 | public: 16 | string command; 17 | string path; 18 | 19 | this(string command, string path, bool removeInput = true, long maxtime = 4500){ 20 | this.command = command; 21 | this.path = path; 22 | external = new Process(command, path, removeInput, maxtime); 23 | external.start(); 24 | } 25 | 26 | // The sort of payload carried (PayLoadType.Script) 27 | final @property PayloadType type() const { return(PayloadType.Script); } 28 | 29 | // Is the payload ready ? 30 | final @property long ready() { return(external.finished); } 31 | 32 | // length of the message portion of the output (generated HTML headers are detected and substracted) 33 | final @property ptrdiff_t length() const { 34 | if (!external.running) { 35 | ptrdiff_t msglength = to!ptrdiff_t(external.length); 36 | if(endOfHeader > 0) msglength = msglength - bodyStart; 37 | return(getHeader!ptrdiff_t("Content-Length", msglength)); 38 | } 39 | return -1; 40 | } 41 | 42 | @property void notifyovertime() { external.notifyovertime(); } 43 | 44 | // Last modified time (not interesting for scripts) 45 | final @property SysTime mtime() { return Clock.currTime(); } 46 | 47 | // MIME type of the content: "Content-Type: text/html; charset=utf-8" 48 | // split by ; since the Content-Type might be combined with a charset 49 | final @property string mimetype() const { 50 | auto type = getHeader("Content-Type", "text/html"); 51 | return(type.split(";")[0]); 52 | } 53 | 54 | // Get a header value from the header generated by the script 55 | final T getHeader(T)(string key, T def = T.init) const { 56 | if (endOfHeader > 0) { 57 | foreach (line; fullHeader().split("\n")) { 58 | string[] elems = line.split(": "); 59 | if (elems.length == 2) { 60 | if (toLower(elems[0]) == toLower(key)) return to!T(strip(elems[1])); 61 | } 62 | } 63 | } 64 | return(def); 65 | } 66 | 67 | // Type of header returned by the script: FastCGI, HTTP10, HTTP11 68 | @property final HeaderType headerType() const { 69 | if (endOfHeader <= 0) return HeaderType.None; 70 | string[] values = firstHeaderLine().split(" "); 71 | if (values.length >= 3 && values[0] == "HTTP/1.0") return HeaderType.HTTP10; 72 | if (values.length >= 3 && values[0] == "HTTP/1.1") return HeaderType.HTTP11; 73 | if (getHeader("Status", "") != "") return HeaderType.FastCGI; 74 | //if (getHeader("Content-Type", "") != "") return HeaderType.FastCGI; 75 | return HeaderType.None; 76 | } 77 | 78 | // Get the full header generated by the script 79 | @property final string fullHeader() const { return(fullheader(external.output(0))); } 80 | 81 | // Get the first line of the header 82 | @property final string firstHeaderLine() const { 83 | string outputSoFar = to!string(external.output(0)); 84 | return(outputSoFar[0 .. outputSoFar.indexOf("\n")]); 85 | } 86 | 87 | // Return the status code provided by the external script 88 | @property final StatusCode statuscode() const { 89 | string status = ""; 90 | if (headerType() == HeaderType.FastCGI) { 91 | status = getHeader!string("Status", ""); // Fast-CGI provides: "Status: Code Reason" 92 | status = status.split(" ")[0]; 93 | } 94 | if (headerType() == HeaderType.HTTP10 || headerType() == HeaderType.HTTP11) { 95 | string[] values = firstHeaderLine().split(" "); // Normal HTTP header: "Version Code Reason" 96 | if(values.length >= 3) status = values[1]; 97 | } 98 | if (status == "") 99 | return((external.status == 0)? StatusCode.Ok : StatusCode.ISE); 100 | StatusCode s = StatusCode.ISE; 101 | try { 102 | s = to!StatusCode(to!int(status)); 103 | } catch (Exception e){ 104 | error("unable to get statuscode from script"); 105 | } 106 | return(s); 107 | } 108 | 109 | // Stream of message bytes, skips the script generated header since the webserver 110 | // parses the header and generates it's own 111 | const(char)[] bytes(ptrdiff_t from, ptrdiff_t maxsize = 1024) { 112 | if (from + endOfHeader > from) 113 | from += bodyStart; 114 | return(external.output(from)[0 .. to!ptrdiff_t(min(from+maxsize, $))]); 115 | } 116 | 117 | // Position of the end of the header 118 | @property final ptrdiff_t endOfHeader() const { return(endofheader(external.output(0))); } 119 | // Position of the start of the body 120 | @property final ptrdiff_t bodyStart() const { return(bodystart(external.output(0))); } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /danode/client.d: -------------------------------------------------------------------------------- 1 | module danode.client; 2 | 3 | import danode.imports; 4 | import danode.router : Router, runRequest; 5 | import danode.statuscode : StatusCode; 6 | import danode.interfaces : DriverInterface, ClientInterface, StringDriver; 7 | import danode.response : Response, setTimedOut; 8 | import danode.request : Request; 9 | import danode.payload : Message; 10 | import danode.log : custom, info, trace, warning, NOTSET, NORMAL; 11 | 12 | class Client : Thread, ClientInterface { 13 | private: 14 | Router router; /// Router class from server 15 | DriverInterface driver; /// Driver 16 | long maxtime; /// Maximum quiet time before we cut the connection 17 | public: 18 | bool terminated; /// Is the client / connection terminated 19 | 20 | this(Router router, DriverInterface driver, long maxtime = 5000) { 21 | custom(3, "CLIENT", "client constructor"); 22 | this.router = router; 23 | this.driver = driver; 24 | this.maxtime = maxtime; 25 | super(&run); // initialize the thread 26 | } 27 | 28 | final void run() { 29 | trace("new connection established %s:%d", ip(), port() ); 30 | try { 31 | if (driver.openConnection() == false) { 32 | warning("new connection aborted: unable to open connection"); 33 | stop(); 34 | } 35 | scope (exit) { 36 | if (driver.isAlive()) driver.closeConnection(); 37 | } 38 | Request request; 39 | Response response; 40 | while (running) { 41 | if (driver.receive(driver.socket) > 0) { // We've received new data 42 | if (!response.ready) { // If we're not ready to respond yet 43 | // Parse the data and try to create a response (Could fail multiple times) 44 | router.route(driver, request, response, maxtime); 45 | } 46 | if (response.ready && !response.completed) { // We know what to respond, but haven't send all of it yet 47 | driver.send(response, driver.socket); // Send the response, hit multiple times, send what you can and return 48 | } 49 | if (response.ready && response.completed) { // We've completed the request, response cycle 50 | router.logRequest(this, request, response); // Log the response to the request 51 | request.clearUploadFiles(); // Remove any upload files left over 52 | request.destroy(); // Clear the request structure 53 | driver.inbuffer.destroy(); // Clear the input buffer 54 | driver.requests++; 55 | if(!response.keepalive) stop(); // No keep alive, then stop this client 56 | response.destroy(); // Clear the response structure 57 | } 58 | } 59 | if (lastmodified >= maxtime) { // Client are not allowed to be silent for more than maxtime 60 | custom(2, "CLIENT", "inactivity: %s > %s", lastmodified, maxtime); 61 | if (!response.ready && request !is Request.init) { // We have an unhandled request 62 | driver.setTimedOut(response); 63 | router.logRequest(this, request, response); // Log the response to the request 64 | } 65 | stop(); 66 | } 67 | custom(3, "CLIENT", "connection %s:%s (%s msecs) %s", ip, port, starttime, to!string(driver.inbuffer.data)); 68 | Thread.sleep(dur!"msecs"(2)); 69 | } 70 | } catch(Exception e) { 71 | warning("unknown client exception: %s", e); 72 | stop(); 73 | } 74 | custom(1, "CLIENT", "connection %s:%s (%s) closed after %d requests %s (%s msecs)", ip, port, (driver.isSecure() ? "SSL" : "HTTP"), 75 | driver.requests, driver.senddata, starttime); 76 | driver.destroy(); // Clear the response structure 77 | } 78 | 79 | // Is the client still running, if the socket was gone it's not otherwise check the terminated flag 80 | final @property bool running() const { 81 | if (driver.socket is null) return(false); 82 | return(!terminated && driver.socket.isAlive()); 83 | } 84 | 85 | // Stop the client by setting the terminated flag 86 | final @property void stop() { 87 | trace("connection %s:%s stop called", ip, port); 88 | terminated = true; 89 | } 90 | 91 | // Start time of the client in mseconds (stored in the connection driver) 92 | final @property long starttime() const { return(driver.starttime); } 93 | // When was the client last modified in mseconds (stored in the connection driver) 94 | final @property long lastmodified() const { return(driver.lastmodified); } 95 | // Port of the client 96 | final @property long port() const { return(driver.port()); } 97 | // ip address of the client 98 | final @property string ip() const { return(driver.ip()); } 99 | } 100 | 101 | unittest { 102 | custom(0, "FILE", "%s", __FILE__); 103 | auto router = new Router("./www/", Address.init, NORMAL); 104 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: localhost\n\n"); 105 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: localhost\r\n\r\n"); 106 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: www.localhost\n\n"); 107 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: www.localhost\r\n\r\n"); 108 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: notfound\n\n"); 109 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: notfound\r\n\r\n"); 110 | 111 | router.runRequest("GET /dmd.d\nHost: localhost\n\n"); 112 | router.runRequest("GET /dmd.d\nHost: notfound\n\n"); 113 | 114 | router.runRequest("GET\nHost: localhost\n\n"); 115 | router.runRequest("GET\nHost: notfound\n\n"); 116 | 117 | router.runRequest("GET /php.php HTTP/1.1\nHost: localhost\n\n"); 118 | router.runRequest("GET /php.php HTTP/1.1\nHost: localhost\r\n\r\n"); 119 | 120 | router.runRequest("GET /php-cgi.fphp HTTP/1.1\nHost: localhost\n\n"); 121 | router.runRequest("GET /php-cgi.fphp HTTP/1.1\nHost: localhost\r\n\r\n"); 122 | 123 | router.runRequest("GET /phpinfo.fphp HTTP/1.1\nHost: localhost\n\n"); 124 | router.runRequest("GET /phpinfo.fphp HTTP/1.1\nHost: localhost\r\n\r\n"); 125 | 126 | router.runRequest("GET /dmd.d HTTP/1.2\nHost: localhost\n\n"); 127 | router.runRequest("GET /keepalive.d HTTP/1.1\nHost: localhost\nConnection: keep-alive\n\n"); 128 | 129 | // Test all available RequestMethods, and an invalid one 130 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: localhost\n\n"); 131 | router.runRequest("HEAD /dmd.d HTTP/1.1\nHost: localhost\n\n"); 132 | router.runRequest("POST /dmd.d HTTP/1.1\nHost: localhost\n\n"); 133 | router.runRequest("PUT /dmd.d HTTP/1.1\nHost: localhost\n\n"); 134 | router.runRequest("DELETE /dmd.d HTTP/1.1\nHost: localhost\n\n"); 135 | router.runRequest("CONNECT /dmd.d HTTP/1.1\nHost: localhost\n\n"); 136 | router.runRequest("OPTIONS * HTTP/1.1\n\n"); 137 | router.runRequest("TRACE /dmd.d HTTP/1.1\nHost: localhost\n\n"); 138 | router.runRequest("NO /dmd.d HTTP/1.1\nHost: localhost\n\n"); 139 | } 140 | 141 | -------------------------------------------------------------------------------- /danode/filesystem.d: -------------------------------------------------------------------------------- 1 | module danode.filesystem; 2 | 3 | import danode.imports; 4 | import danode.statuscode : StatusCode; 5 | import danode.mimetypes : mime; 6 | import danode.payload : Payload, FilePayload, PayloadType; 7 | import danode.functions : has, isCGI; 8 | import danode.log : custom, info, Log, warning, trace, cverbose, NOTSET, NORMAL, DEBUG; 9 | 10 | /* Domain name structure containing files in that domain 11 | Domains are loaded by the FileSystem from the -wwwRoot variable (set to www/ by default) 12 | Note 1: Domains are named as requested by the HTTP client so SSL keynames must match domainnames (e.g.: localhost / 127.0.0.1 / XX.XX.XX.XX or xxx.xx) 13 | Note 2: ./www/localhost existing is required for unit testing */ 14 | struct Domain { 15 | FilePayload[string] files; 16 | long entries; 17 | long buffered; 18 | 19 | @property long buffersize() const { long sum = 0; foreach(ref f; files.byKey){ sum += files[f].buffersize(); } return sum; } 20 | @property long size() const { long sum = 0; foreach(ref f; files.byKey){ sum += files[f].length(); } return sum; } 21 | } 22 | 23 | /* File system class that manages the underlying domains 24 | Note: Should this really be thread synchronized access ? 25 | */ 26 | class FileSystem { 27 | private: 28 | string root; 29 | Domain[string] domains; 30 | Log logger; 31 | size_t maxsize; 32 | 33 | public: 34 | this(Log logger, string root = "./www/", size_t maxsize = 1024 * 512){ 35 | this.logger = logger; 36 | this.root = root; 37 | this.maxsize = maxsize; 38 | scan(); 39 | } 40 | 41 | /* Scan the whole filesystem for changes */ 42 | final void scan(){ synchronized { 43 | foreach (DirEntry d; dirEntries(root, SpanMode.shallow)){ if(d.isDir()){ 44 | domains[d.name] = scan(d.name); 45 | } } 46 | } } 47 | 48 | /* Scan a single folder */ 49 | final Domain scan(string dname){ synchronized { 50 | Domain domain; 51 | foreach (DirEntry f; dirEntries(dname, SpanMode.depth)) { 52 | if (f.isFile()) { 53 | string shortname = replace(f.name[dname.length .. $], "\\", "/"); 54 | custom(1, "SCAN", "file: %s -> %s", f.name, shortname); 55 | if (!domain.files.has(shortname)) { 56 | domain.files[shortname] = new FilePayload(f.name, maxsize); 57 | domain.entries++; 58 | if (domain.files[shortname].needsupdate()) { 59 | domain.files[shortname].buffer(); 60 | domain.buffered++; 61 | } 62 | } 63 | } 64 | } 65 | custom(1, "SCAN", "domain: %s, files %s|%s", dname, domain.buffered, domain.entries); 66 | custom(1, "SCAN", "%s = size: %.2f/%.2f kB", dname, domain.buffersize / 1024.0, domain.size / 1024.0); 67 | return(domain); 68 | } } 69 | 70 | /* Get the localroot of the domain (TODO: Is there a bug, did I require that this.root should always end in a '/' ?) */ 71 | final string localroot(string hostname) const { return(format("%s%s", this.root, hostname)); } 72 | 73 | /* Get the FilePayload at path from the localroot, with update check on buffers */ 74 | final FilePayload file(string localroot, string path){ synchronized { 75 | // New file created after last scan ? -> Scan the whole folder for changes 76 | if (!domains[localroot].files.has(path) && exists(format("%s%s", localroot, path))) { 77 | custom(1, "SCAN", "New file %s, rescanning index: %s", path, localroot); 78 | domains[localroot] = scan(localroot); 79 | } 80 | // File exists, buffer the individual file if modified after buffer date 81 | if (domains[localroot].files.has(path)) { 82 | if (domains[localroot].files[path].needsupdate) domains[localroot].files[path].buffer(); 83 | return(domains[localroot].files[path]); 84 | } 85 | custom(1, "SCAN", "should not be here not in index, but exists %s, %s", path, localroot); 86 | return new FilePayload("", maxsize); 87 | } } 88 | 89 | /* Rebuffer all file domains from disk, 90 | By reusing domain keys so, we don't buffer new domains. This is ok since we would need to load SSL */ 91 | final void rebuffer() { 92 | foreach(ref d; domains.byKey){ foreach(ref f; domains[d].files.byKey){ 93 | domains[d].files[f].buffer(); 94 | } } 95 | } 96 | } 97 | 98 | /* Basic unit-tests should be extended */ 99 | unittest { 100 | custom(0, "FILE", "%s", __FILE__); 101 | Log logger = new Log(NORMAL); 102 | FileSystem filesystem = new FileSystem(logger, "./www/"); 103 | custom(0, "TEST", "./www/localhost/dmd.d (6 bytes) = %s", filesystem.file("./www/localhost", "/dmd.d").bytes(0,6)); 104 | custom(0, "TEST", "filesystem.localroot('localhost') = %s", filesystem.localroot("localhost")); 105 | Domain localhost = filesystem.scan("www/localhost"); 106 | custom(0, "TEST", "localhost.buffersize() = %s", localhost.buffersize()); 107 | custom(0, "TEST", "localhost.size() = %s", localhost.size()); 108 | auto file = filesystem.file(filesystem.localroot("localhost"), "localhost/dmd.d"); 109 | custom(0, "TEST", "file.asStream(0) = %s", file.asStream(0)); 110 | custom(0, "TEST", "file.statuscode() = %s", file.statuscode()); 111 | custom(0, "TEST", "file.mimetype() = %s", file.mimetype()); 112 | custom(0, "TEST", "file.mtime() = %s", file.mtime()); 113 | custom(0, "TEST", "file.ready() = %s", file.ready()); 114 | custom(0, "TEST", "file.type() = %s", file.type()); 115 | custom(0, "TEST", "file.content() = %s", file.content()); 116 | } 117 | 118 | -------------------------------------------------------------------------------- /danode/functions.d: -------------------------------------------------------------------------------- 1 | module danode.functions; 2 | 3 | import danode.imports; 4 | import danode.log : error, warning, trace, custom; 5 | import danode.mimetypes : CGI_FILE, mime, UNSUPPORTED_FILE; 6 | 7 | immutable string timeFmt = "%s %s %s %s:%s:%s %s"; 8 | immutable string[int] months; 9 | shared static this(){ 10 | months = [ 1 : "Jan", 2 : "Feb", 3 : "Mar", 4 : "Apr", 11 | 5 : "May", 6 : "Jun", 7 : "Jul", 8 : "Aug", 12 | 9 : "Sep", 10: "Oct", 11: "Nov", 12: "Dec"]; 13 | } 14 | 15 | // Try to convert a HTML date in a string into a SysTime 16 | // Structure that we expect: "21 Apr 2014 20:20:13 CET" 17 | SysTime parseHtmlDate(const string datestr) { 18 | SysTime ts = SysTime(DateTime(-7, 1, 1, 1, 0, 0)); 19 | auto dateregex = regex(r"([0-9]{1,2}) ([a-z]{1,3}) ([0-9]{4}) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}) cet", "g"); 20 | auto m = match(datestr.toLower(), dateregex); 21 | if(m.captures.length == 7){ 22 | try { 23 | ts = SysTime(DateTime(to!int(m.captures[3]), monthToIndex(m.captures[2]), to!int(m.captures[1]), // 21 Apr 2014 24 | to!int(m.captures[4]), to!int(m.captures[5]), to!int(m.captures[6]))); // 20:20:13 25 | } catch(Exception e) { 26 | warning("parseHtmlDate exception, could not parse '%s'", datestr); 27 | } 28 | } 29 | return(ts); 30 | } 31 | 32 | // Month to index of the year 33 | pure int monthToIndex(in string m) { 34 | for (int x = 1; x < 12; ++x) { 35 | if(m.toLower() == months[x].toLower()) return x; 36 | } 37 | return -1; 38 | } 39 | 40 | pure string toD(T, U)(in T x, in U digits = 6) nothrow { 41 | string s = to!string(x); 42 | while (s.length < digits) { s = "0" ~ s; } 43 | return s; 44 | } 45 | 46 | @nogc pure long Msecs(in SysTime t, in SysTime t0 = Clock.currTime()) nothrow { 47 | return((t0 - t).total!"msecs"()); 48 | } 49 | 50 | @nogc pure bool has(T,K)(in T[K] buffer, in K key) nothrow { 51 | return((key in buffer) !is null); 52 | } 53 | 54 | @nogc pure bool has(T)(in T[] buffer, in T key) nothrow { 55 | foreach(T i; buffer) { 56 | if(i == key) return(true); 57 | } 58 | return false; 59 | } 60 | 61 | @nogc pure T from(T,K)(in T[K] buffer, in K key, T def = T.init) nothrow { 62 | T* p = cast(T*)(key in buffer); 63 | if(p is null) return def; 64 | return(*p); 65 | } 66 | 67 | void writeinfile(in string localpath, in string content) { 68 | if (content.length > 0) { 69 | try { 70 | auto fp = File(localpath, "wb"); 71 | fp.rawWrite(content); 72 | fp.close(); 73 | trace("writeinfile: %d bytes to: %s", content.length, localpath); 74 | } catch(Exception e) { 75 | error("writeinfile: I/O exception '%s'", e.msg); 76 | } 77 | } 78 | } 79 | 80 | string htmltime(in SysTime d = Clock.currTime()) { 81 | return format(timeFmt, d.day(), months[d.month()], d.year(), d.hour(), toD(d.minute(),2), toD(d.second(),2), "CET"); 82 | } 83 | 84 | bool isFILE(in string path) { 85 | try { 86 | if (exists(path) && isFile(path)) return true; 87 | } catch(Exception e) { 88 | error("isFILE: I/O exception '%s'", e.msg); 89 | } 90 | return false; 91 | } 92 | 93 | bool isDIR(in string path) { 94 | try { 95 | if (exists(path) && isDir(path)) return true; 96 | } catch(Exception e) { 97 | error("isDIR: I/O exception '%s'", e.msg); 98 | } 99 | return false; 100 | } 101 | 102 | bool isCGI(in string path) { 103 | try { 104 | if (exists(path) && isFile(path) && mime(path).indexOf(CGI_FILE) >= 0) return true; 105 | } catch(Exception e) { 106 | error("isCGI: I/O exception '%s'", e.msg); 107 | } 108 | return false; 109 | } 110 | 111 | pure bool isAllowed(in string path) { 112 | if (mime(path) == UNSUPPORTED_FILE) return false; 113 | return true; 114 | } 115 | 116 | // Where does the HTML request header end ? 117 | pure ptrdiff_t endofheader(T)(const(T) buffer) { 118 | auto str = to!string(buffer); 119 | ptrdiff_t idx = str.indexOf("\r\n\r\n"); 120 | if(idx <= 0) idx = str.indexOf("\n\n"); 121 | return(idx); 122 | } 123 | 124 | // Where does the HTML request body start ? 125 | pure ptrdiff_t bodystart(T)(const(T) buffer) { 126 | auto str = to!string(buffer); 127 | ptrdiff_t idx = str.indexOf("\r\n\r\n"); 128 | if (idx > 0) return (idx + 4); 129 | idx = str.indexOf("\n\n"); 130 | if (idx > 0) return (idx + 2); 131 | return(-1); 132 | } 133 | 134 | // get the HTML header contained in the buffer 135 | pure string fullheader(T)(const(T) buffer) { 136 | auto i = bodystart(buffer); 137 | if (i > 0 && i <= buffer.length) 138 | return(to!string(buffer[0 .. i])); 139 | return []; 140 | } 141 | 142 | // Which interpreter (if any) should be used for the path ? 143 | pure string interpreter(in string path) { 144 | string[] mime = mime(path).split("/"); 145 | if(mime.length > 1) return(mime[1]); 146 | return []; 147 | } 148 | 149 | // Browse the content of a directory, generate a rudimentairy HTML file 150 | string browseDir(in string root, in string localpath) { 151 | Appender!(string) content; 152 | content.put(format("Content of: %s
\n", localpath)); 153 | foreach (DirEntry d; dirEntries(localpath, SpanMode.shallow)) { 154 | content.put(format("%s
", d.name[root.length .. $], d.name[root.length .. $])); 155 | } 156 | return(format("200 - Allowed directory%s", content.data)); 157 | } 158 | 159 | // Reset the socketset and add a server socket to the set 160 | int sISelect(SocketSet set, Socket socket, int timeout = 10) { 161 | set.reset(); 162 | set.add(socket); 163 | return Socket.select(set, null, null, dur!"msecs"(timeout)); 164 | } 165 | 166 | unittest { 167 | custom(0, "FILE", "%s", __FILE__); 168 | custom(0, "TEST", "monthToIndex('Feb') = %s", monthToIndex("Feb")); 169 | custom(0, "TEST", "toD(5, 4) = %s", toD(5, 4)); 170 | custom(0, "TEST", "toD(12, 3) = %s", toD(12, 3)); 171 | custom(0, "TEST", "htmltime() = %s", htmltime()); 172 | custom(0, "TEST", "isFILE('danode/functions.d') = %s", isFILE("danode/functions.d")); 173 | custom(0, "TEST", "isDIR('danode') = %s", isDIR("danode")); 174 | custom(0, "TEST", "isCGI('www/localhost/dmd.d') = %s", isCGI("www/localhost/dmd.d")); 175 | custom(0, "TEST", "isAllowed('www/localhost/data.ill') = %s", isAllowed("www/localhost/data.ill")); 176 | custom(0, "TEST", "isAllowed('www/localhost/index.html') = %s", isAllowed("www/localhost/index.html")); 177 | custom(0, "TEST", "interpreter('www/localhost/dmd.d') = %s", interpreter("www/localhost/dmd.d")); 178 | custom(0, "TEST", "interpreter('www/localhost/php.php') = %s", interpreter("www/localhost/php.php")); 179 | custom(0, "TEST", "browseDir('www', 'localhost') = %s", browseDir("www", "www/localhost")); 180 | } 181 | 182 | -------------------------------------------------------------------------------- /danode/http.d: -------------------------------------------------------------------------------- 1 | module danode.http; 2 | 3 | import danode.imports; 4 | import danode.interfaces : DriverInterface; 5 | import danode.response : Response; 6 | import danode.log : custom, warning, error; 7 | 8 | class HTTP : DriverInterface { 9 | public: 10 | this(Socket socket, bool blocking = false) { 11 | custom(3, "HTTP", "HTTP constructor"); 12 | this.socket = socket; 13 | this.blocking = blocking; 14 | this.systime = Clock.currTime(); // Time in ms since this process came alive 15 | this.modtime = Clock.currTime(); // Time in ms since this process was modified 16 | } 17 | 18 | // Open the connection by setting the socket to non blocking I/O, and registering the origin address 19 | override bool openConnection() { 20 | try { 21 | socket.blocking = this.blocking; 22 | } catch(Exception e) { 23 | error("unable to accept socket: %s", e.msg); 24 | return(false); 25 | } 26 | try { 27 | this.address = socket.remoteAddress(); 28 | } catch(Exception e) { 29 | warning("unable to resolve requesting origin: %s", e.msg); 30 | } 31 | return(true); 32 | } 33 | 34 | // Receive upto maxsize of bytes from the client into the input buffer 35 | override ptrdiff_t receive(Socket socket, ptrdiff_t maxsize = 4096) { 36 | if(socket is null) return(-1); 37 | if(!socket.isAlive()) return(-1); 38 | ptrdiff_t received; 39 | char[] tmpbuffer = new char[](maxsize); 40 | if ((received = socket.receive(tmpbuffer)) > 0) { 41 | inbuffer.put(tmpbuffer[0 .. received]); modtime = Clock.currTime(); 42 | } 43 | if(received > 0) custom(3, "HTTP", "received %d bytes of data", received); 44 | return(inbuffer.data.length); 45 | } 46 | 47 | // Send upto maxsize bytes from the response to the client 48 | override void send(ref Response response, Socket socket, ptrdiff_t maxsize = 4096) { synchronized { 49 | if(socket is null) return; 50 | if(!socket.isAlive()) return; 51 | ptrdiff_t send = socket.send(response.bytes(maxsize)); 52 | if (send >= 0) { 53 | if (send > 0) modtime = Clock.currTime(); 54 | response.index += send; senddata[requests] += send; 55 | if(response.index >= response.length) response.completed = true; 56 | } 57 | if(send > 0) custom(3, "HTTP", "send %d bytes of data", send); 58 | } } 59 | 60 | // Close the connection, by shutting down the socket 61 | override void closeConnection() nothrow { 62 | if (socket !is null) { 63 | try { 64 | socket.shutdown(SocketShutdown.BOTH); 65 | socket.close(); 66 | } catch(Exception e) { 67 | warning("unable to close socket: %s", e.msg); 68 | } 69 | } 70 | } 71 | 72 | // Is the connection alive ?, make sure we check for null 73 | override bool isAlive() { 74 | if (socket !is null) return(socket.isAlive()); 75 | return false; 76 | } 77 | 78 | @nogc override bool isSecure() const nothrow { return(false); } 79 | } 80 | 81 | unittest { 82 | custom(0, "FILE", "%s", __FILE__); 83 | } 84 | 85 | -------------------------------------------------------------------------------- /danode/https.d: -------------------------------------------------------------------------------- 1 | module danode.https; 2 | 3 | version(SSL) { 4 | import deimos.openssl.ssl; 5 | import deimos.openssl.err; 6 | 7 | import danode.imports; 8 | import danode.functions : Msecs; 9 | import danode.response : Response; 10 | import danode.log : NORMAL, INFO, DEBUG; 11 | import danode.interfaces : DriverInterface; 12 | import danode.log : custom, warning, error; 13 | import danode.ssl; 14 | 15 | class HTTPS : DriverInterface { 16 | private: 17 | SSL* ssl = null; 18 | 19 | public: 20 | this(Socket socket, bool blocking = false) { 21 | custom(3, "HTTPS", "HTTPS constructor"); 22 | this.socket = socket; 23 | this.blocking = blocking; 24 | this.systime = Clock.currTime(); // Time in ms since this process came alive 25 | this.modtime = Clock.currTime(); // Time in ms since this process was modified 26 | } 27 | 28 | // Perform the SSL handshake 29 | bool performHandshake() { 30 | custom(2, "HTTPS", "performing handshake"); 31 | bool handshaked = false; 32 | int ret_accept, ret_error; 33 | while (!handshaked && starttime < 500) { 34 | ret_accept = SSL_accept(ssl); 35 | if (ret_accept == 1) { 36 | handshaked = true; 37 | } else { 38 | ret_error = ssl.checkForError(socket, ret_accept); 39 | if (ret_accept == 0) return(false); 40 | if (ret_error == SSL_ERROR_SSL) return(false); 41 | if (ret_error == SSL_ERROR_WANT_READ) Thread.sleep(5.msecs); 42 | if (ret_error == SSL_ERROR_WANT_WRITE) Thread.sleep(5.msecs); 43 | } 44 | } 45 | custom(2, "HTTPS", "handshake: %s", handshaked); 46 | return(handshaked); 47 | } 48 | 49 | // Open the connection by setting the socket to non blocking I/O, and registering the origin address 50 | override bool openConnection() { synchronized { 51 | custom(1, "HTTPS", "Opening HTTPS connection"); 52 | if (ncontext > 0) { 53 | custom(1, "HTTPS", "Number of SSL contexts: %d", ncontext); 54 | try { 55 | if (this.socket is null) { 56 | error("SSL was not given a valid socket (null)"); 57 | return(false); 58 | } 59 | 60 | custom(1, "HTTPS", "set the socket the blocking mode"); 61 | this.socket.blocking = this.blocking; 62 | 63 | custom(1, "HTTPS", "creating a new ssl connection from context[0]"); 64 | this.ssl = SSL_new(contexts[0].context); 65 | 66 | custom(1, "HTTPS", "setting the socket handle I/O to SSL* object"); 67 | this.ssl.SSL_set_fd(to!int(socket.handle())); 68 | 69 | custom(1, "HTTPS", "SSL_set_accept_state to server mode"); 70 | SSL_set_accept_state(this.ssl); 71 | 72 | bool handshaked = performHandshake(); 73 | if (!handshaked) { 74 | error("couldn't handshake SSL connection"); 75 | return(false); 76 | } 77 | } catch (Exception e) { 78 | error("couldn't open SSL connection : %s", e.msg); 79 | return(false); 80 | } 81 | try { 82 | if (this.socket !is null) { 83 | this.address = this.socket.remoteAddress(); 84 | } 85 | } catch (Exception e) { 86 | warning("unable to resolve requesting origin: %s", e.msg); 87 | } 88 | custom(1, "HTTPS", "HTTPS connection opened"); 89 | return(true); 90 | } else { 91 | error("HTTPS driver failed, reason: Server has no certificates loaded"); 92 | } 93 | return(false); 94 | } } 95 | 96 | // Close the connection, by shutting down the SSL and Socket object 97 | override void closeConnection() { synchronized { 98 | if (socket !is null) { 99 | try { 100 | if (socket.isAlive()) { 101 | if (ssl) { 102 | SSL_shutdown(ssl); 103 | } else { 104 | error("No SSL object to close, are certificates available?"); 105 | } 106 | } 107 | socket.shutdown(SocketShutdown.BOTH); 108 | socket.close(); 109 | } catch(Exception e) { 110 | warning("unable to close socket: %s", e.msg); 111 | } 112 | } 113 | } } 114 | 115 | // Is the connection alive ?, make sure we check for null 116 | override bool isAlive() { 117 | if(socket !is null) return socket.isAlive(); 118 | return false; 119 | } 120 | 121 | // Receive upto maxsize of bytes from the client into the input buffer 122 | override ptrdiff_t receive(Socket socket, ptrdiff_t maxsize = 4096){ synchronized { 123 | if(socket is null) return -1; 124 | if(!socket.isAlive()) return -1; 125 | if(ssl is null) return -1; 126 | 127 | ptrdiff_t received; 128 | char[] tmpbuffer = new char[](maxsize); 129 | if ((received = SSL_read(ssl, cast(void*) tmpbuffer, cast(int)maxsize)) > 0) { 130 | inbuffer.put(tmpbuffer[0 .. received]); modtime = Clock.currTime(); 131 | } 132 | if(received > 0) custom(3, "HTTPS", "received %d bytes of data", received); 133 | return(inbuffer.data.length); 134 | } } 135 | 136 | // Send upto maxsize bytes from the response to the client 137 | override void send(ref Response response, Socket socket, ptrdiff_t maxsize = 4096){ synchronized { 138 | if(socket is null) return; 139 | if(!socket.isAlive()) return; 140 | if(ssl is null) return; 141 | 142 | auto slice = response.bytes(maxsize); 143 | ptrdiff_t send = SSL_write(ssl, cast(void*) slice, cast(int) slice.length); 144 | if(send >= 0) { 145 | if(send > 0) modtime = Clock.currTime(); 146 | response.index += send; senddata[requests] += send; 147 | if(response.index >= response.length) response.completed = true; 148 | } 149 | if(send > 0) custom(3, "HTTPS", "send %d bytes of data", send); 150 | } } 151 | 152 | @nogc override bool isSecure() const nothrow { return(true); } 153 | } 154 | 155 | unittest { 156 | custom(0, "FILE", "%s", __FILE__); 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /danode/imports.d: -------------------------------------------------------------------------------- 1 | module danode.imports; 2 | 3 | // Public imported function from core 4 | public import core.stdc.stdlib : exit, free, malloc, realloc; 5 | public import core.stdc.stdio : fileno, printf; 6 | 7 | // Public imported function from std 8 | public import std.algorithm : mean, canFind, min; 9 | public import std.array : appender, join; 10 | public import std.compiler : name, version_major, version_minor; 11 | public import std.conv : to; 12 | public import std.datetime : dur, msecs; 13 | public import std.getopt : getopt; 14 | public import std.path : baseName, extension; 15 | public import std.process : pipe, spawnShell, executeShell, tryWait, wait, kill; 16 | public import std.file : dirEntries, exists, remove, isFile, isDir, timeLastModified, getSize; 17 | public import std.format : format, formatValue; 18 | public import std.regex : regex, match; 19 | public import std.stdio : fgetc, fflush, ftell, stderr, stdin, stdout, writef, writefln, write, writeln; 20 | public import std.string : chomp, endsWith, empty, format, indexOf, join, replace, split, startsWith, strip, toLower, toStringz; 21 | public import std.uuid : md5UUID; 22 | public import std.uri : decodeComponent; 23 | public import std.zlib : compress; 24 | 25 | // Public imported structures and enums from core 26 | public import core.thread : Thread; 27 | 28 | // Public imported structures and enums from std 29 | public import std.array : Appender; 30 | public import std.datetime : Clock, DateTime, Duration, SysTime; 31 | public import std.format : FormatSpec; 32 | public import std.file : DirEntry, SpanMode; 33 | public import std.process : Pid, Config, Pipe; 34 | public import std.stdio : EOF, File; 35 | public import std.socket : Address, AddressFamily, InternetAddress, ProtocolType, Socket, SocketOption, SocketOptionLevel, SocketSet, SocketShutdown, SocketType; 36 | public import std.traits: SetFunctionAttributes, functionAttributes, EnumMembers; 37 | public import std.uuid : UUID; 38 | -------------------------------------------------------------------------------- /danode/interfaces.d: -------------------------------------------------------------------------------- 1 | module danode.interfaces; 2 | 3 | import danode.imports; 4 | import danode.functions : Msecs, bodystart, endofheader, fullheader; 5 | import danode.response : Response; 6 | import danode.log : NORMAL, INFO, DEBUG; 7 | 8 | /* Client interface used by the server */ 9 | interface ClientInterface { 10 | @property bool running(); /// Is the client still handling requests 11 | @property long starttime(); /// When was the client last started 12 | @property long lastmodified(); /// When was the client last modified 13 | @property void stop(); /// Stop the client 14 | 15 | @property string ip() const; /// IP location of the client 16 | @property long port() const; /// Port at which the client is connected 17 | 18 | void run(); /// Main client loop and logic 19 | } 20 | 21 | /* Connection/Driver interface available to the client */ 22 | abstract class DriverInterface { 23 | public: 24 | Appender!(char[]) inbuffer; /// Input appender buffer 25 | Socket socket; /// Client socket for reading and writing 26 | long requests = 0; /// Number of requests we handled 27 | long[long] senddata; /// Size of data send per request 28 | SysTime systime; /// Time in ms since this process came alive 29 | SysTime modtime; /// Time in ms since this process was last modified 30 | Address address; /// Private address field 31 | bool blocking = false; /// Blocking communication ? 32 | int verbose = NORMAL; /// Verbose level 33 | 34 | bool openConnection(); /// Open the connection 35 | void closeConnection(); /// Close the connection 36 | bool isAlive(); /// Is the connection alive ? 37 | @nogc bool isSecure() const nothrow; /// Are we secure ? 38 | 39 | // Receive upto maxsize of bytes from the client into the input buffer 40 | ptrdiff_t receive(Socket conn, ptrdiff_t maxsize = 4096); 41 | 42 | // Send upto maxsize bytes from the response to the client 43 | void send(ref Response response, Socket conn, ptrdiff_t maxsize = 4096); 44 | 45 | // port being used for communication 46 | final @property long port() const { 47 | if (address !is null) return(to!long(address.toPortString())); 48 | return(-1); 49 | } 50 | 51 | // IP address connected to 52 | final @property string ip() const { 53 | if (address !is null) return(address.toAddrString()); 54 | return("0.0.0.0"); 55 | } 56 | 57 | // Milliseconds since start of connection 58 | final @property long starttime() const { return(Msecs(systime)); } 59 | 60 | // Milliseconds since last modified 61 | final @property long lastmodified() const { return(Msecs(modtime)); } 62 | 63 | // Byte input converted to header as string 64 | final @property string header() const { return(fullheader(inbuffer.data)); } 65 | 66 | // Byte input converted to body as string 67 | final @property string body() const { 68 | if (bodyStart < 0 || bodyStart > inbuffer.data.length) return(""); 69 | return(to!string(inbuffer.data[bodyStart() .. $])); 70 | } 71 | 72 | // Where does the HTML request header end ? 73 | final @property ptrdiff_t endOfHeader() const { return(endofheader(inbuffer.data)); } 74 | 75 | // Where does the HTML request body begin ? 76 | final @property ptrdiff_t bodyStart() const { return(bodystart(inbuffer.data)); } 77 | 78 | // Do we have a header separator ? "\r\n\r\n" or "\n\n" 79 | final @property bool hasHeader() const { 80 | if(endOfHeader <= 0) return(false); 81 | return(true); 82 | } 83 | } 84 | 85 | class StringDriver : DriverInterface { 86 | this(string input) { 87 | this.socket = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP); 88 | this.systime = Clock.currTime(); // Time in ms since this process came alive 89 | this.modtime = Clock.currTime(); // Time in ms since this process was modified 90 | inbuffer ~= input; 91 | } 92 | override bool openConnection() { return(true); } 93 | override void closeConnection() nothrow { } 94 | override bool isAlive() { return(true); } 95 | @nogc override bool isSecure() const nothrow { return(false); } 96 | override ptrdiff_t receive(Socket socket, ptrdiff_t maxsize = 4096) { return(inbuffer.data.length); } 97 | override void send(ref Response response, Socket socket, ptrdiff_t maxsize = 4096) { 98 | response.header(); 99 | response.completed = true; 100 | } 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /danode/log.d: -------------------------------------------------------------------------------- 1 | module danode.log; 2 | 3 | import danode.imports; 4 | import danode.interfaces : ClientInterface; 5 | import danode.request : Request; 6 | import danode.response : Response; 7 | import danode.functions; 8 | import danode.statuscode : StatusCode; 9 | 10 | extern(C) __gshared int cverbose; // Verbose level of C-Code 11 | 12 | immutable int NOTSET = -1, NORMAL = 0, INFO = 1, TRACE = 2, DEBUG = 3; 13 | 14 | /* Verbose level control of stdout */ 15 | void write(T)(const T fmt) { if(cverbose > 0) stdout.write(fmt); } 16 | 17 | /* Write an warning string to stdout */ 18 | void warning(A...)(const string fmt, auto ref A args) { if(cverbose >= 0) writefln("[WARN] " ~ fmt, args); } 19 | 20 | /* Informational level of debug to stdout */ 21 | void info(A...)(const string fmt, auto ref A args) { if(cverbose >= 1) stdout.writefln("[INFO] " ~ fmt, args); } 22 | 23 | /* Informational level of debug to stdout */ 24 | void custom(A...)(const int lvl, const string pre, const string fmt, auto ref A args) { 25 | if(cverbose >= lvl) { 26 | string sep = " "; 27 | size_t i = 1; 28 | while(i < (7 - pre.length)) { sep ~= " "; i++; } 29 | stdout.writefln("[" ~ pre ~ "]" ~ sep ~ fmt, args); 30 | } 31 | } 32 | 33 | /* Trace level debug to stdout */ 34 | void trace(A...)(const string fmt, auto ref A args) { if(cverbose >= 2) stdout.writefln("[TRACE] " ~ fmt, args); } 35 | 36 | /* Write an error string to stderr */ 37 | void error(A...)(const string fmt, auto ref A args) { stderr.writefln("[ERROR] " ~ fmt, args); } 38 | 39 | /* Abort with error code, default: -1 */ 40 | void abort(in string s, int exitcode = -1){ 41 | error(s); 42 | exit(exitcode); 43 | } 44 | 45 | /* Expect condition cond, otherwise abort the process */ 46 | void expect(A...)(bool cond, string msg, auto ref A args) { if (!cond) abort(format(msg, args), -1); } 47 | 48 | struct Info { 49 | long[StatusCode] responses; 50 | Appender!(long[]) starttimes; 51 | Appender!(long[]) timings; 52 | Appender!(bool[]) keepalives; 53 | long[string] useragents; 54 | long[string] ips; 55 | 56 | void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const { 57 | sink(format("%s %d %.2f", responses, timings.data[($-1)], mean(timings.data))); 58 | } 59 | } 60 | 61 | class Log { 62 | private: 63 | File RequestLogFp; 64 | File PerformanceLogFp; 65 | Info[string] statistics; 66 | 67 | public: 68 | this(int verbose = NORMAL, string requestLog = "request.log", string perfLog = "perf.log", bool overwrite = false) { 69 | cverbose = verbose; 70 | 71 | // Initialize the request log 72 | if (exists(requestLog) && overwrite) { 73 | warning("overwriting log: %s", requestLog); 74 | remove(requestLog); 75 | } 76 | RequestLogFp = File(requestLog, "a"); 77 | 78 | // Initialize the performance log 79 | if (exists(perfLog) && overwrite) { 80 | warning("overwriting log: %s", perfLog); 81 | remove(perfLog); 82 | } 83 | PerformanceLogFp = File(perfLog, "a"); 84 | } 85 | 86 | // Set verbose level of the application 87 | @property @nogc int verbose(int verbose = NOTSET) const nothrow { 88 | if (verbose != NOTSET) { 89 | printf("[INFO] changing verbose level from %d to %d\n", cverbose, verbose); 90 | cverbose = verbose; 91 | } 92 | return(cverbose); 93 | } 94 | 95 | // Update the performance statistics 96 | void updatePerformanceStatistics(in ClientInterface cl, in Request rq, in Response rs) { 97 | string key = format("%s%s", rq.shorthost, rq.uripath); 98 | if(!statistics.has(key)) statistics[key] = Info(); // Unknown key, create new Info statistics object 99 | // Fill run-time statistics 100 | statistics[key].responses[rs.statuscode]++; 101 | statistics[key].starttimes.put(rq.starttime.toUnixTime()); 102 | statistics[key].timings.put(Msecs(rq.starttime)); 103 | statistics[key].keepalives.put(rs.keepalive); 104 | statistics[key].ips[((rq.track)? cl.ip : "DNT")]++; 105 | if (cverbose == TRACE) { 106 | PerformanceLogFp.writefln("%s = [%s] %s", key, rs.statuscode, statistics[key]); 107 | PerformanceLogFp.flush(); 108 | } 109 | } 110 | 111 | // Log the responses to the request 112 | void logRequest(in ClientInterface cl, in Request rq, in Response rs) { 113 | if (cverbose >= NOTSET) { 114 | string s = format("[%d] %s %s:%s %s%s %s %s", rs.statuscode, htmltime(), cl.ip, cl.port, rq.shorthost, decodeComponent(rq.uri), Msecs(rq.starttime), rs.payload.length); 115 | RequestLogFp.writeln(s); 116 | custom(-1, "REQ", s); 117 | RequestLogFp.flush(); 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /danode/mimetypes.d: -------------------------------------------------------------------------------- 1 | module danode.mimetypes; 2 | 3 | import danode.imports; 4 | 5 | immutable string UNSUPPORTED_FILE = "file/unknown"; /// Unsupported file mime 6 | immutable string CGI_FILE = "executable/"; /// CGI mime prefix 7 | 8 | pure string mime(string i) { 9 | switch(extension(i).toLower()){ 10 | case ".htx", ".htm", ".html", ".htmls": return "text/html"; 11 | case ".map", ".gitignore", ".txt", ".md", ".log", ".list" : return "text/plain"; 12 | case ".xml" : return "text/xml"; 13 | case ".css" : return "text/css"; 14 | case ".csv" : return "text/csv"; 15 | case ".ics" : return "text/calendar"; 16 | case ".rtx" : return "text/richtext"; 17 | case ".vcard" : return "text/vcard"; 18 | 19 | case ".eml", "mime" : return "message/rfc822"; 20 | 21 | case ".bmp" : return "image/bmp"; 22 | case ".gif" : return "image/gif"; 23 | case ".ico" : return "image/x-icon"; 24 | case ".jpg", ".jpeg" : return "image/jpeg"; 25 | case ".png" : return "image/png"; 26 | case ".tif", ".tiff" : return "image/tiff"; 27 | case ".rgb" : return "image/x-rgb"; 28 | case ".sgi" : return "image/sgi"; 29 | case ".svg", ".svgz" : return "image/svg+xml"; 30 | case ".psd" : return "image/vnd.adobe.photoshop"; 31 | 32 | case ".3ds" : return "image/x-3ds"; 33 | case ".mid", ".midi" : return "audio/midi"; 34 | case ".mp2", ".mp3" : return "audio/mpeg"; 35 | case ".ogg" : return "audio/ogg"; 36 | case ".wav" : return "audio/wav"; 37 | case ".aac" : return "audio/aac"; 38 | 39 | case ".mpg", ".mpe", ".mpeg", "m1v", "m2v" : return "video/mpeg"; 40 | case ".qt", ".mov" : return "video/quicktime"; 41 | case ".avi" : return "video/x-msvideo"; 42 | case ".mp4", "mp4v", "mpg4" : return "video/mp4"; 43 | case ".movie": return "video/x-sgi-movie"; 44 | case ".webm": return "video/webm"; 45 | 46 | case ".bin", ".class", ".dll", ".exe",".rdata" : return "application/octet-stream"; 47 | case ".apk" : return "application/vnd.android.package-archive"; 48 | case ".ecma" : return "application/ecmascript"; 49 | case ".epub" : return "application/epub+zip"; 50 | case ".azw" : return "application/vnd.amazon.ebook"; 51 | case ".gz" : return "application/x-gzip"; 52 | case ".js" : return "application/x-javascript"; 53 | case ".pdf" : return "application/pdf"; 54 | case ".rar", ".tgz" : return "application/x-compressed"; 55 | case ".tar" : return "application/x-tar"; 56 | case ".z" : return "application/x-compress"; 57 | case ".zip" : return "application/x-zip-compressed"; 58 | case ".bz" : return "application/x-bzip"; 59 | case ".bz2" : return "application/x-bzip2"; 60 | case ".jar" : return "application/java-archive"; 61 | 62 | case ".bib", ".bibtex" : return "application/x-bibtex"; 63 | case ".doc", ".dot" : return "application/msword"; 64 | case ".rtf" : return "application/rtf"; 65 | case ".docx" : return "applications/vnd.openxmlformats-officedocument.wordprocessingml.document"; 66 | case ".dotx" : return "applications/vnd.openxmlformats-officedocument.wordprocessingml.template"; 67 | 68 | case ".ppt" : return "application/vnd.ms-powerpoint"; 69 | case ".pptx" : return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; 70 | 71 | case ".xls", ".xlt", ".xla" : return "application/vnd.ms-excel"; 72 | 73 | case ".xlsx" : return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; 74 | case ".xltx" : return "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; 75 | 76 | case ".comp" : return "x-shader/x-compute"; 77 | case ".vert" : return "x-shader/x-vertex"; 78 | case ".frag" : return "x-shader/x-fragment"; 79 | case ".geom" : return "x-shader/x-geometry"; 80 | 81 | case ".eot" : return "application/vnd.ms-fontobject"; 82 | case ".ttf" : return "font/ttf"; 83 | case ".woff" : return "font/woff"; 84 | case ".woff2" : return "font/woff2"; 85 | 86 | case ".pem-certificate-chain" : return "application/pem-certificate-chain"; 87 | case ".pgp-encrypted" : return "application/pgp-encrypted"; 88 | case ".pgp-signature" : return "application/pgp-signature"; 89 | 90 | case ".x-x509-ca-cert" : return "application/x-x509-ca-cert"; 91 | case ".x-x509-ca-ra-cert" : return "application/x-x509-ca-ra-cert"; 92 | case ".x-x509-next-ca-cert" : return "application/x-x509-next-ca-cert"; 93 | 94 | case ".scss" : return CGI_FILE ~ "sass -t compact"; //nested (default), compact, compressed, or expanded 95 | case ".cgi" : return CGI_FILE ~ "perl"; 96 | case ".d" : return CGI_FILE ~ "rdmd"; 97 | case ".pl" : return CGI_FILE ~ "perl -X"; 98 | case ".php", ".fphp" : return CGI_FILE ~ "php-cgi -C"; 99 | case ".py" : return CGI_FILE ~ "pyton"; 100 | case ".r" : return CGI_FILE ~ "Rscript --vanilla"; 101 | case ".bf" : return CGI_FILE ~ "bf"; 102 | case ".ada" : return CGI_FILE ~ "gnatmake"; 103 | default : return UNSUPPORTED_FILE; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /danode/payload.d: -------------------------------------------------------------------------------- 1 | module danode.payload; 2 | 3 | import danode.imports; 4 | import danode.statuscode : StatusCode; 5 | import danode.mimetypes : mime, UNSUPPORTED_FILE; 6 | import danode.log : info, warning, trace, cverbose, DEBUG; 7 | import danode.functions : isCGI; 8 | 9 | enum PayloadType { Message, Script, File } 10 | enum HeaderType { None, FastCGI, HTTP10, HTTP11 } 11 | 12 | /* Payload interface, Payload is carried by the Response structure, not the Request structure */ 13 | interface Payload { 14 | public: 15 | @property long ready(); 16 | @property StatusCode statuscode() const; 17 | @property PayloadType type() const; 18 | @property ptrdiff_t length() const; 19 | @property SysTime mtime(); 20 | @property string mimetype() const; 21 | 22 | const(char)[] bytes(ptrdiff_t from, ptrdiff_t maxsize = 1024); 23 | } 24 | 25 | /* Implementation of the Payload interface, by using an empty string message */ 26 | class Empty : Message { 27 | public: 28 | this(StatusCode status, string mime = UNSUPPORTED_FILE) { 29 | super(status, "", mime); 30 | } 31 | } 32 | 33 | /* Implementation of the Payload interface, by using an underlying string buffer */ 34 | class Message : Payload { 35 | private: 36 | StatusCode status; 37 | string message; 38 | string mime; 39 | 40 | public: 41 | this(StatusCode status, string message, string mime = "text/plain") { 42 | this.status = status; 43 | this.message = message; 44 | this.mime = mime; 45 | } 46 | 47 | final @property PayloadType type() const { return(PayloadType.Message); } 48 | final @property long ready() { return(true); } 49 | final @property ptrdiff_t length() const { return(message.length); } 50 | final @property SysTime mtime() { return Clock.currTime(); } 51 | final @property string mimetype() const { return mime; } 52 | final @property StatusCode statuscode() const { return status; } 53 | char[] bytes(ptrdiff_t from, ptrdiff_t maxsize = 1024) { 54 | return( message[from .. to!ptrdiff_t(min(from+maxsize, $))].dup ); 55 | } 56 | } 57 | 58 | /* Implementation of the Payload interface, by using an underlying file (static / deflate / cgi) */ 59 | class FilePayload : Payload { 60 | public: 61 | bool deflate = false; // Is a deflate version of the file available ? 62 | private: 63 | string path; // Path of the file 64 | SysTime btime; // Time buffered 65 | bool buffered = false; // Is buffered ? 66 | size_t buffermaxsize; // Maximum size of the buffer 67 | char[] buf = null; // Byte buffer of the file 68 | char[] encbuf = null; // Encoded buffer for the file 69 | File* fp = null; // Pointer to the file 70 | 71 | public: 72 | this(string path, size_t buffermaxsize) { 73 | this.path = path; 74 | this.buffermaxsize = buffermaxsize; 75 | } 76 | 77 | /* Does the file require to be updated before sending ? */ 78 | final bool needsupdate() { 79 | if (!isStaticFile()) return false; // CGI files are never buffered, since they are executed 80 | if (fileSize() > 0 && fileSize() < buffermaxsize) { // 81 | if (!buffered) { 82 | info("need to buffer file record: %s", path); 83 | return true; 84 | } 85 | if (mtime > btime) { 86 | info("re-buffer stale file record: %s", path); 87 | return true; 88 | } 89 | }else{ 90 | info("file %s does not fit into the buffer (%d)", path, buffermaxsize); 91 | } 92 | return false; 93 | } 94 | 95 | /* Reads the file into the internal buffer, and compress the buffer to the enc buffer 96 | Updates the buffer time and status. 97 | */ 98 | final void buffer() { synchronized { 99 | if(buf is null) buf = new char[](fileSize()); 100 | buf.length = fileSize(); 101 | try { 102 | if(fp is null) fp = new File(path, "rb"); 103 | fp.open(path, "rb"); 104 | fp.rawRead(buf); 105 | fp.close(); 106 | } catch (Exception e) { 107 | warning("exception during buffering '%s': %s", path, e.msg); 108 | return; 109 | } 110 | try { 111 | encbuf = cast(char[])( compress(buf, 9) ); 112 | } catch (Exception e) { 113 | warning("exception during compressing '%s': %s", path, e.msg); 114 | } 115 | btime = Clock.currTime(); 116 | trace("buffered %s: %d|%d bytes", path, fileSize(), encbuf.length); 117 | buffered = true; 118 | } } 119 | 120 | /* Whole file content served via the bytes function */ 121 | final @property string content(){ return( to!string(bytes(0, length)) ); } 122 | /* Is the file a real file (i.e. does it exist on disk) */ 123 | final @property bool realfile() const { return(path.exists()); } 124 | /* Do we have a deflate encoded version */ 125 | final @property bool hasEncodedVersion() const { return(encbuf !is null); } 126 | /* Is the file defined as static in mimetypes.d ? */ 127 | final @property bool isStaticFile() { return(!path.isCGI()); } 128 | /* Time the file was last modified ? 129 | TODO: Is there a BUG here related to encbuf update ? */ 130 | final @property SysTime mtime() const { if(!realfile){ return btime; } return path.timeLastModified(); } 131 | /* Files are always assumed ready to be handled (unlike Common Gate Way threads) */ 132 | final @property long ready() { return(true); } 133 | /* Payload type delivered to the client */ 134 | final @property PayloadType type() const { return(PayloadType.Message); } 135 | /* Size of the file, -1 if it does not exist */ 136 | final @property ptrdiff_t fileSize() const { if(!realfile){ return -1; } return to!ptrdiff_t(path.getSize()); } 137 | /* Length of the buffer */ 138 | final @property long buffersize() const { return cast(long)(buf.length); } 139 | /* Mimetype of the file */ 140 | final @property string mimetype() const { return mime(path); } 141 | /* Status code for file is StatusCode.Ok ? 142 | TODO: Shouldn't this be based on realfile ? */ 143 | final @property StatusCode statuscode() const { return StatusCode.Ok; } 144 | /* Get the number of bytes that the client response has, based on encoding */ 145 | final @property ptrdiff_t length() const { 146 | if(hasEncodedVersion && deflate) return(encbuf.length); 147 | return(fileSize()); 148 | } 149 | 150 | /* Send the file from the underlying raw byte source stream using fseek, fp are closed */ 151 | final char[] asStream(ptrdiff_t from, ptrdiff_t maxsize = 1024) { 152 | if(buf is null) buf = new char[](maxsize); 153 | char[] slice = []; 154 | if (cverbose >= DEBUG && from == 0) write("[STREAM] ."); 155 | if (from >= fileSize()) { 156 | trace("from >= filesize, are we still trying to send?"); 157 | return([]); 158 | } 159 | try { 160 | if(fp is null) fp = new File(path, "rb"); 161 | fp.open(path, "rb"); 162 | if(fp.isOpen()) { 163 | fp.seek(from); 164 | slice = fp.rawRead!char(buf); 165 | fp.close(); 166 | if (cverbose >= DEBUG) write("."); 167 | if (cverbose >= DEBUG && (from + slice.length) >= fileSize()) write("\n"); 168 | } 169 | } catch(Exception e) { 170 | warning("exception %s while streaming file: %s", e.msg, path); 171 | } 172 | return(slice); 173 | } 174 | 175 | /* Get bytes in a lockfree manner from the correct underlying buffer */ 176 | final char[] bytes(ptrdiff_t from, ptrdiff_t maxsize = 1024){ synchronized { 177 | if (!realfile) { return []; } 178 | trace("file provided is a real file"); 179 | if (needsupdate) { buffer(); } 180 | if (!buffered) { 181 | return(asStream(from, maxsize)); 182 | } else { 183 | if(hasEncodedVersion && deflate) { 184 | if(from < encbuf.length) return( encbuf[from .. to!ptrdiff_t(min(from+maxsize, $))] ); 185 | } else { 186 | if(from < buf.length) return( buf[from .. to!ptrdiff_t(min(from+maxsize, $))] ); 187 | } 188 | } 189 | return([]); 190 | } } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /danode/post.d: -------------------------------------------------------------------------------- 1 | module danode.post; 2 | 3 | import danode.imports; 4 | import danode.cgi : CGI; 5 | import danode.statuscode : StatusCode; 6 | import danode.request : Request; 7 | import danode.response : SERVERINFO, Response, redirect, create, notmodified; 8 | import danode.webconfig : WebConfig; 9 | import danode.payload : Message; 10 | import danode.mimetypes : mime; 11 | import danode.filesystem : FileSystem; 12 | import danode.functions : from, has, isCGI, isFILE, isDIR, writeinfile; 13 | import danode.log : info, custom, trace, warning; 14 | 15 | immutable string MPHEADER = "multipart/form-data"; /// Multipart header id 16 | immutable string XFORMHEADER = "application/x-www-form-urlencoded"; /// X-form header id 17 | immutable string JSON = "application/json"; /// json input 18 | enum PostType { Input, File }; 19 | 20 | struct PostItem { 21 | PostType type; 22 | string name; 23 | string filename; 24 | string value; 25 | string mime = "post/input"; 26 | long size = 0; 27 | } 28 | 29 | // Parse the POST request data from the client, or waits (returning false) for more data 30 | // when the entire request body is not yet available. POST data supplied in Multipart 31 | // and X-form post formats are currently supported 32 | final bool parsePost (ref Request request, ref Response response, in FileSystem filesystem) { 33 | if (response.havepost || request.method != "POST") { 34 | response.havepost = true; 35 | return(true); 36 | } 37 | long expectedlength = to!long(from(request.headers, "Content-Length", "0")); 38 | string content = request.body; 39 | if (expectedlength == 0) { 40 | custom(2, "POST", "Content-Length was not specified or 0: real length: %s", content.length); 41 | response.havepost = true; 42 | return(true); // When we don't receive any post data it is meaningless to scan for any content 43 | } 44 | custom(2, "POST", "received %s of %s", content.length, expectedlength); 45 | if(content.length < expectedlength) return(false); 46 | 47 | string contenttype = from(request.headers, "Content-Type"); 48 | custom(2, "POST", "content type: %s", contenttype); 49 | 50 | if (contenttype.indexOf(XFORMHEADER) >= 0) { 51 | custom(0, "XFORM", "parsing %d bytes", expectedlength); 52 | request.parseXform(content); 53 | custom(1, "XFORM", "# of items: %s", request.postinfo.length); 54 | } else if (contenttype.indexOf(MPHEADER) >= 0) { 55 | string mpid = split(contenttype, "boundary=")[1]; 56 | custom(1, "MPART", "header: %s, parsing %d bytes", mpid, expectedlength); 57 | request.parseMultipart(filesystem, content, mpid); 58 | custom(1, "MPART", "# of items: %s", request.postinfo.length); 59 | } else if (contenttype.indexOf(JSON) >= 0) { 60 | custom(0, "JSONP", "parsing %d bytes", expectedlength); 61 | //request.postinfo["php://input"] = PostItem(PostType.File, "stdin", "php://input", content, JSON, content.length); 62 | } else { 63 | warning("unsupported POST content type: %s [%s] -> %s", contenttype, expectedlength, content); 64 | request.parseXform(content); 65 | } 66 | response.havepost = true; 67 | return(response.havepost); 68 | } 69 | 70 | // Parse X-form content in the body of the request 71 | final void parseXform(ref Request request, const string content) { 72 | foreach (s; content.split("&")) { 73 | string[] elem = strip(s).split("="); 74 | request.postinfo[ elem[0] ] = PostItem( PostType.Input, elem[0], "", elem[1] ); 75 | } 76 | } 77 | 78 | // Parse Multipart content in the body of the request 79 | final void parseMultipart(ref Request request, in FileSystem filesystem, const string content, const string mpid) { 80 | //writeinfile("multipart.txt", content); 81 | int[string] keys; 82 | bool isarraykey; 83 | foreach (size_t i, part; chomp(content).split(mpid)) { 84 | string[] elem = strip(part).split("\r\n"); 85 | if (elem[0] != "--") { 86 | string[] mphdr = elem[0].split("; "); 87 | string key = mphdr[1][6 .. ($-1)]; 88 | if (mphdr.length == 2) { 89 | request.postinfo[key] = PostItem(PostType.Input, key, "", join(elem[2 .. ($-1)])); 90 | } else if (mphdr.length == 3) { 91 | string fname = mphdr[2][10 .. ($-1)]; 92 | custom(1, "MPART", "found on key %s file %s", key, fname); 93 | if (key.length > 2) { 94 | isarraykey = (key[($-2) .. $] == "[]")? true : false; 95 | } 96 | keys[key] = keys.has(key)? keys[key] + 1: 0; 97 | custom(1, "MPART", "found on key %s #%d file %s", key, keys[key], fname); 98 | if (fname != "") { 99 | string fkey = isarraykey? key ~ to!string(keys[key]) : key; 100 | string skey = isarraykey? key[0 .. $-2] : key; 101 | string localpath = request.uploadfile(filesystem, fkey); 102 | string mpcontent = join(elem[3 .. ($-1)], "\r\n"); 103 | request.postinfo[fkey] = PostItem(PostType.File, skey, mphdr[2][10 .. ($-1)], localpath, split(elem[1],": ")[1], mpcontent.length); 104 | writeinfile(localpath, mpcontent); 105 | custom(1, "MPART", "wrote %d bytes to file %s", mpcontent.length, localpath); 106 | } else { 107 | request.postinfo[key] = PostItem(PostType.Input, key, ""); 108 | } 109 | } 110 | }else{ 111 | custom(1, "MPART", "ID element: %s", elem[0]); 112 | } 113 | } 114 | } 115 | 116 | /* The serverAPI functions prepares and writes out the input file for external process execution 117 | The inputfile contains the SERVER, COOKIES, POST, and FILES information that can be used by the external script 118 | This data is picked-up by the different CGI APIs, and presented to the client in the regular way */ 119 | final void serverAPI(in FileSystem filesystem, in WebConfig config, in Request request, in Response response) { 120 | Appender!(string) content; 121 | content.put(format("S=REDIRECT_STATUS=%d\n", response.payload.statuscode)); 122 | content.put(format("S=HTTP_HOST=%s:%s\n", request.host, request.serverport)); 123 | content.put(format("S=HTTP_USER_AGENT=%s\n", request.headers.from("User-Agent"))); 124 | content.put(format("S=HTTP_ACCEPT=%s\n", request.headers.from("Accept"))); 125 | content.put(format("S=HTTP_ACCEPT_LANGUAGE=%s\n", request.headers.from("Accept-Language"))); 126 | content.put(format("S=HTTP_ACCEPT_ENCODING=%s\n", request.headers.from("Accept-Encoding"))); 127 | content.put(format("S=HTTP_REFERER=%s\n", request.headers.from("HTTP_REFERER"))); 128 | content.put(format("S=HTTP_CONNECTION=%s\n", (response.keepalive)? "Keep-Alive" : "Close" )); 129 | // Give HTTP_COOKIES to CGI 130 | foreach (c; request.cookies.split("; ")) { 131 | content.put(format("C=%s\n", chomp(c)) ); 132 | } 133 | // TODO: Add content.put(format("S=HTTP_UPGRADE_INSECURE_REQUESTS=%s\n", SSL )); 134 | // TODO: Add content.put(format("S=HTTP_CACHE_CONTROL=%s\n", Filesystem )); 135 | // TODO: Add content.put(format("S=PATH=%s\n", CGI import path )); 136 | // TODO: Add content.put(format("S=SERVER_SIGNATURE=
%s
\n", Server Signature )); 137 | content.put(format("S=SERVER_SOFTWARE=%s\n", SERVERINFO)); 138 | try{ 139 | content.put(format("S=SERVER_NAME=%s\n", (response.address)? response.address.toHostNameString() : "localhost")); 140 | }catch(Exception e){ 141 | warning("Exception while trying to call: toHostNameString()"); 142 | content.put("S=SERVER_NAME=localhost\n"); 143 | } 144 | content.put(format("S=SERVER_ADDR=%s\n", (response.address)? response.address.toAddrString() : "127.0.0.1")); 145 | content.put(format("S=SERVER_PORT=%s\n", (response.address)? response.address.toPortString() : "80")); 146 | content.put(format("S=REMOTE_ADDR=%s\n", request.ip)); 147 | content.put(format("S=DOCUMENT_ROOT=%s\n", filesystem.localroot(request.shorthost()))); 148 | // TODO: Add content.put(format("S=REQUEST_SCHEME=%s\n", )); 149 | // TODO: Add content.put(format("S=CONTEXT_PREFIX=%s\n", )); 150 | // TODO: Add content.put(format("S=CONTEXT_DOCUMENT_ROOT=%s\n", )); 151 | // TODO: Add content.put(format("S=SERVER_ADMIN=%s\n", )); 152 | content.put(format("S=SCRIPT_FILENAME=%s\n", config.localpath(filesystem.localroot(request.shorthost()), request.path))); 153 | content.put(format("S=REMOTE_PORT=%s\n", request.port)); 154 | // TODO: Add content.put(format("S=REDIRECT_URL=%s\n", )); 155 | content.put(format("S=GATEWAY_INTERFACE=%s\n", "CGI/1.1")); 156 | content.put(format("S=SERVER_PROTOCOL=%s\n", request.protocol)); 157 | content.put(format("S=REQUEST_METHOD=%s\n", request.method)); 158 | content.put(format("S=QUERY_STRING=%s\n", request.query)); 159 | content.put(format("S=REQUEST_URI=%s\n", request.uripath)); 160 | content.put(format("S=SCRIPT_NAME=%s\n", request.path)); 161 | content.put(format("S=PHP_SELF=%s\n", request.path)); 162 | // TODO: Add content.put(format("S=REQUEST_TIME_FLOAT=%s\n", )); 163 | content.put(format("S=REQUEST_TIME=%s\n", request.starttime.toUnixTime)); 164 | 165 | // Were the following invented / made up by me ? or mistaken/old ones ? 166 | content.put(format("S=HTTPS=%s\n", (request.isSecure)? "1" : "0" )); 167 | content.put(format("S=REMOTE_PAGE=%s\n", request.page)); 168 | content.put(format("S=REQUEST_DIR=%s\n", request.dir)); 169 | content.put(format("S=HTTP_ACCEPT_CHARSET=%s\n", request.headers.from("Accept-Charset"))); 170 | 171 | // Write the post information we received 172 | foreach (p; request.postinfo) { 173 | if(p.type == PostType.Input) content.put(format("P=%s=%s\n", p.name, p.value)); 174 | if(p.type == PostType.File) content.put(format("F=%s=%s=%s=%s\n", p.name, p.filename, p.mime, p.value)); 175 | } 176 | 177 | string filename = request.inputfile(filesystem); 178 | trace("[IN %s]\n%s[/IN %s]", filename, content.data, filename); 179 | writeinfile(filename, content.data); 180 | } 181 | 182 | -------------------------------------------------------------------------------- /danode/process.d: -------------------------------------------------------------------------------- 1 | module danode.process; 2 | 3 | import danode.imports; 4 | import danode.functions : Msecs; 5 | import danode.log : custom, warning, trace; 6 | version(Posix) { 7 | import core.sys.posix.fcntl : fcntl, F_SETFL, O_NONBLOCK; 8 | } 9 | 10 | struct WaitResult { 11 | bool terminated; /// Is the process terminated 12 | int status; /// Exit status when terminated 13 | } 14 | 15 | /* Set a filestream to nonblocking mode, if not Posix, use winbase.h */ 16 | bool nonblocking(ref File file) { 17 | version(Posix) { 18 | return(fcntl(fileno(file.getFP()), F_SETFL, O_NONBLOCK) != -1); 19 | }else{ 20 | import core.sys.windows.winbase; 21 | auto x = PIPE_NOWAIT; 22 | auto res = SetNamedPipeHandleState(file.windowsHandle(), &x, null, null); 23 | return(res != 0); 24 | } 25 | } 26 | 27 | version(Posix) { 28 | alias kill killProcess; 29 | }else{ 30 | /* Windows hack: Spawn a new process to kill the still running process */ 31 | void killProcess(Pid pid, uint signal) { executeShell(format("taskkill /F /T /PID %d", pid.processID)); } 32 | } 33 | 34 | /* The Process class provides external process communication via pipes, to the web language interpreter 35 | process runs as a thread inside the web server. Output of the running process should be queried via 36 | the output() function. When there is any output on the stderr of the process (stored in errbuffer), 37 | the error buffer will be served. only if the error buffer is empty, will outbuffer be served. */ 38 | class Process : Thread { 39 | private: 40 | string command; /// Command to execute 41 | string inputfile; /// Path of input file 42 | bool completed = false; 43 | bool removeInput = true; 44 | 45 | File fStdIn; /// Input file stream 46 | File fStdOut; /// Output file stream 47 | File fStdErr; /// Error file stream 48 | 49 | Pipe pStdOut; /// Output pipe 50 | Pipe pStdErr; /// Error pipe 51 | 52 | WaitResult process; /// Process try/wait results 53 | SysTime starttime; /// Time in ms since this process came alive 54 | SysTime modified; /// Time in ms since this process was modified 55 | long maxtime; /// Maximum time in ms before we kill the process 56 | 57 | Appender!(char[]) outbuffer; /// Output appender buffer 58 | Appender!(char[]) errbuffer; /// Error appender buffer 59 | 60 | public: 61 | this(string command, string inputfile, bool removeInput = true, long maxtime = 4500) { 62 | this.command = command; 63 | this.inputfile = inputfile; 64 | this.removeInput = removeInput; 65 | this.maxtime = maxtime; 66 | this.starttime = Clock.currTime(); 67 | this.modified = Clock.currTime(); 68 | this.outbuffer = appender!(char[])(); 69 | this.errbuffer = appender!(char[])(); 70 | super(&run); 71 | } 72 | 73 | // Query Output/Errors from 'from' to the end, if the outbuffer contains any output this will be served 74 | // from is checked to be in-range of the outbuffer/errbuffer, if not an empty array is returned 75 | final @property const(char)[] output(ptrdiff_t from) const { 76 | synchronized { 77 | if (outbuffer.data.length > 0 && from >= 0 && from <= outbuffer.data.length) { 78 | return outbuffer.data[from .. $]; 79 | } 80 | if (from >= 0 && from <= errbuffer.data.length) { 81 | return errbuffer.data[from .. $]; 82 | } 83 | return []; 84 | } 85 | } 86 | 87 | // Runtime of the thread in mseconds 88 | final @property long time() const { 89 | synchronized { return(Msecs(starttime)); } 90 | } 91 | 92 | // Last time the process was modified (e.g. data on stdout/stderr) 93 | final @property long lastmodified() const { 94 | synchronized { return(Msecs(modified)); } 95 | } 96 | 97 | // Is the external process still running ? 98 | final @property bool running() const { 99 | synchronized { return(!process.terminated); } 100 | } 101 | 102 | // Did our internal thread finish processing the external process, etc ? 103 | final @property bool finished() const { 104 | synchronized { return(this.completed); } 105 | } 106 | 107 | // Returns the 'flattened' exit status of the external process 108 | // ( -1 = non-0 exit code, 0 = succes, 1 = still running ) 109 | final @property int status() const { 110 | synchronized { 111 | if (running) return 1; 112 | if (process.status == 0) return 0; 113 | return -1; 114 | } 115 | } 116 | 117 | // Length of output, if the outbuffer contains any data, the outbuffer will be prefered (errors are silenced) 118 | final @property long length() const { synchronized { 119 | if (outbuffer.data.length > 0) { return(outbuffer.data.length); } 120 | return errbuffer.data.length; 121 | } } 122 | 123 | // Read a character from a filestream and append it to buffer 124 | // TODO: Use another function an read more bytes at the same time 125 | void readpipe (ref File file, ref Appender!(char[]) buffer) { 126 | try { 127 | int ch; 128 | auto fp = file.getFP(); 129 | while ((ch = fgetc(fp)) != EOF && lastmodified < maxtime) { 130 | modified = Clock.currTime(); 131 | buffer.put(cast(char) ch); 132 | } 133 | } catch (Exception e) { 134 | warning("exception during readpipe command: %s", e.msg); 135 | file.close(); 136 | } 137 | } 138 | 139 | @property void notifyovertime() { maxtime = -1; } 140 | 141 | 142 | // Execute the process 143 | // check the input path, and create a pipe:StdIn to the input file 144 | // create 2 pipes for the external process stdout & stderr 145 | // execute the process and wait until maxtime has finished or the process returns 146 | // inputfile is removed when the run() returns succesfully, on error, it is kept 147 | final void run() { 148 | try { 149 | int ch; 150 | if( !exists(inputfile) ) { 151 | warning("no input path: %s", inputfile); 152 | this.process.terminated = true; 153 | this.completed = true; 154 | return; 155 | } 156 | fStdIn = File(inputfile, "r"); 157 | pStdOut = pipe(); pStdErr = pipe(); 158 | custom(1, "PROC", "command: %s < %s", command, inputfile); 159 | auto cpid = spawnShell(command, fStdIn, pStdOut.writeEnd, pStdErr.writeEnd, null); 160 | 161 | fStdOut = pStdOut.readEnd; 162 | if(!nonblocking(fStdOut) && fStdOut.isOpen()) custom(2, "WARN", "unable to create nonblocking stdout pipe for command"); 163 | 164 | fStdErr = pStdErr.readEnd; 165 | if(!nonblocking(fStdErr) && fStdErr.isOpen()) custom(2, "WARN", "unable to create nonblocking error pipe for command"); 166 | 167 | while (running && lastmodified < maxtime) { 168 | this.readpipe(fStdOut, outbuffer); // Non blocking slurp of stdout 169 | this.readpipe(fStdErr, errbuffer); // Non blocking slurp of stderr 170 | process = cast(WaitResult) tryWait(cpid); 171 | Thread.sleep(msecs(1)); 172 | } 173 | if (!process.terminated) { 174 | warning("command: %s < %s did not finish in time [%s msecs]", command, inputfile, time()); 175 | killProcess(cpid, 9); 176 | process = WaitResult(true, wait(cpid)); 177 | } 178 | trace("command finished %d after %s msecs", status(), time()); 179 | 180 | this.readpipe(fStdOut, outbuffer); // Non blocking slurp of stdout 181 | this.readpipe(fStdErr, errbuffer); // Non blocking slurp of stderr 182 | trace("Output %d & %d processed after %s msecs", outbuffer.data.length, errbuffer.data.length, time()); 183 | 184 | // Close the file handles 185 | fStdIn.close(); fStdOut.close(); fStdErr.close(); 186 | 187 | trace("removing process input file %s ? %s", inputfile, removeInput); 188 | if(removeInput) remove(inputfile); 189 | 190 | this.completed = true; 191 | } catch(Exception e) { 192 | warning("process.d, exception: '%s'", e.msg); 193 | } 194 | } 195 | } 196 | 197 | unittest { 198 | custom(0, "FILE", "%s", __FILE__); 199 | auto p = new Process("rdmd www/localhost/dmd.d", "test/dmd.in", false); 200 | p.start(); 201 | while(!p.finished){ Thread.sleep(msecs(5)); } 202 | custom(0, "TEST", "status of output: %s", p.status()); 203 | custom(0, "TEST", "length of output: %s", p.length()); 204 | custom(0, "TEST", "time of output: %s", p.time()); 205 | } 206 | 207 | -------------------------------------------------------------------------------- /danode/request.d: -------------------------------------------------------------------------------- 1 | module danode.request; 2 | 3 | import danode.imports; 4 | import danode.filesystem : FileSystem; 5 | import danode.interfaces : ClientInterface, DriverInterface; 6 | import danode.functions : interpreter, from, parseHtmlDate; 7 | import danode.webconfig : WebConfig; 8 | import danode.http : HTTP; 9 | import danode.post : PostItem, PostType; 10 | import danode.log : custom, info, trace, warning; 11 | 12 | // The Request-Method indicates which method is to be performed on the specified resource 13 | enum RequestMethod : string { 14 | GET = "GET", HEAD = "HEAD", POST = "POST", PUT = "PUT", DELETE = "DELETE", 15 | CONNECT = "CONNECT", OPTIONS = "OPTIONS", TRACE = "TRACE" 16 | } 17 | 18 | // The HTTP-Version indicates which protocol version is requested to obtain the specified resource 19 | enum HTTPVersion : string { 20 | v09 = "HTTP/0.9", v10 = "HTTP/1.0", v11 = "HTTP/1.1", v20 = "HTTP/2", v30 = "HTTP/3" 21 | } 22 | 23 | // Parse the HTTP-Version, throw an error if it cannot be parsed 24 | pure HTTPVersion parseHTTPVersion(const string line) { 25 | foreach (immutable v; EnumMembers!HTTPVersion) { 26 | if (v == line) return(v); 27 | } 28 | throw new Exception(format("invalid HTTP-Version requested: %s", line)); 29 | } 30 | 31 | // Parse the Request-Line: "method uri protocol" 32 | pure bool parseRequestLine(ref Request request, const string line) { 33 | auto parts = line.split(" "); 34 | if (parts.length < 3) 35 | throw new Exception(format("malformed Request-Line: '%s'", line)); 36 | 37 | request.method = to!RequestMethod(strip(parts[0])); 38 | request.uri = request.url = strip(join(parts[1 .. ($-1)], " ")); 39 | request.protocol = parseHTTPVersion(strip(parts[($-1)])); 40 | return(true); 41 | } 42 | 43 | struct Request { 44 | string ip; /// IP location of the client 45 | long port; /// Port at which the client is connected 46 | string body; /// the body of the HTMLrequest 47 | bool isSecure; /// was a secure request made 48 | bool isValid; /// Is the header valid ? 49 | UUID id; /// md5UUID for this request 50 | RequestMethod method; /// requested HTTP method 51 | string uri = "/"; /// uri requested 52 | string url = "/"; /// url requested 53 | string page; /// page is used when performing a canonical redirect 54 | string dir; /// dir is used in directory redirection 55 | HTTPVersion protocol; /// protocol requested 56 | string[string] headers; /// Associative array holding the header values 57 | SysTime starttime; /// start time of the Request 58 | PostItem[string] postinfo; /// Associative array holding the post parameters and values 59 | long maxtime; /// Maximum time in ms before the request is discarded 60 | 61 | // Start a new Request, and parseHeader on the DriverInterface 62 | final void initialize(const DriverInterface driver, long maxtime = 4500) { 63 | this.ip = driver.ip; 64 | this.port = driver.port; 65 | this.body = driver.body; 66 | this.isSecure = driver.isSecure; 67 | this.starttime = Clock.currTime(); 68 | this.maxtime = maxtime; 69 | this.id = md5UUID(format("%s:%d-%s", driver.ip, driver.port, starttime)); 70 | this.isValid = this.parseHeader(driver.header); 71 | info("request: %s to %s from %s:%d - %s", method, uri, this.ip, this.port, this.id); 72 | trace("request header: %s", driver.header); 73 | } 74 | 75 | // Parse the HTML request header (method, uri, protocol) as well as the supplemental headers 76 | final bool parseHeader(const string header) { 77 | try { 78 | foreach (i, line; header.split("\n")) { 79 | if (i == 0) { 80 | this.parseRequestLine(line); 81 | } else { // next lines: header-param: attribute 82 | auto parts = line.split(":"); 83 | if (parts.length > 1) this.headers[strip(parts[0])] = strip(join(parts[1 .. $], ":")); 84 | } 85 | } 86 | } catch (Exception e) { 87 | warning("parseHeader exception: %s", e.msg); 88 | return(false); 89 | } 90 | trace("parseHeader %s %s %s, nParams: %d", this.method, this.uri, this.protocol, this.headers.length); 91 | return(true); 92 | } 93 | 94 | // New input was obtained and / or the driver has been changed, update the driver 95 | final void update(string body) { this.body = body; } 96 | 97 | // The Host header requested in the request 98 | final @property string host() const { 99 | ptrdiff_t i = headers.from("Host").indexOf(":"); 100 | if (i > 0) { 101 | return(headers.from("Host")[0 .. i]); 102 | } 103 | return(headers.from("Host")); 104 | } 105 | 106 | // The Post from the Host header in the request 107 | final @property ushort serverport() const { 108 | ptrdiff_t i = headers.from("Host").indexOf(":"); 109 | if (i > 0) { 110 | return( to!ushort(headers.from("Host")[(i+1) .. $])); 111 | } 112 | return(isSecure ? to!ushort(443) : to!ushort(80)); // return the default ports 113 | } 114 | 115 | // Input file generated storing the headers of the request 116 | final @property string inputfile(in FileSystem filesystem) const { 117 | return format("%s/%s.in", filesystem.localroot(shorthost()), this.id); 118 | } 119 | 120 | // Location of a file with name, uploaded by POST request 121 | final @property string uploadfile(in FileSystem filesystem, in string name) const { 122 | return format("%s/%s.up", filesystem.localroot(shorthost()), md5UUID(format("%s-%s", this.id, name))); 123 | } 124 | 125 | // Get parameters as associative array 126 | final string[string] get() const { 127 | string[string] params; 128 | foreach(param; query[1 .. $].split("&")){ string[] elems = param.split("="); if(elems.length == 1){ elems ~= "TRUE"; } params[elems[0]] = elems[1]; } 129 | return params; 130 | } 131 | 132 | // List of filenames uploaded by the user 133 | final @property string[] postfiles() const { 134 | string[] files; 135 | foreach (p; postinfo) { 136 | if(p.type == PostType.File && p.size > 0) files ~= p.value; 137 | } 138 | return(files); 139 | } 140 | 141 | final @property string path() const { ptrdiff_t i = url.indexOf("?"); if(i > 0){ return(url[0 .. i]); }else{ return(url); } } 142 | final @property string query() const { ptrdiff_t i = uri.indexOf("?"); if(i > 0){ return(uri[i .. $]); }else{ return("?"); } } 143 | final @property string uripath() const { ptrdiff_t i = uri.indexOf("?"); if(i > 0){ return(uri[0 .. i]); }else{ return(uri); } } 144 | final @property bool keepalive() const { return( toLower(headers.from("Connection")) == "keep-alive"); } 145 | final @property SysTime ifModified() const { return(parseHtmlDate(headers.from("If-Modified-Since"))); } 146 | final @property bool acceptsEncoding(string encoding = "deflate") const { return(headers.from("Accept-Encoding").canFind(encoding)); } 147 | final @property bool track() const { return( headers.from("DNT","0") == "0"); } 148 | final @property string params() const { Appender!string str; foreach(k; get.byKey()){ str.put(format(" \"%s=%s\"", k, get[k])); } return(str.data); } 149 | final @property string cookies() const { return(headers.from("Cookie")); } 150 | final @property string useragent() const { return(headers.from("User-Agent", "Unknown")); } 151 | final string shorthost() const { return( (host.indexOf("www.") >= 0)? host[4 .. $] : host ); } 152 | final string command(string localpath) const { return(format("%s %s%s", localpath.interpreter(), localpath, params())); } 153 | 154 | // Canonical redirect of the Request for a directory to the index page specified in the WebConfig 155 | final void redirectdir(in WebConfig config) { 156 | if(config.redirectdir() && config.redirect){ 157 | this.dir = this.path()[1..$]; // We need to redirect, so save the path to this.dir 158 | this.url = config.index; 159 | } 160 | } 161 | 162 | // Clear all files uploaded by the user after the Request is done 163 | final void clearUploadFiles() const { 164 | foreach(f; postfiles) { if(exists(f)) { 165 | trace("removing uploaded file at %s", f); 166 | remove(f); 167 | } } 168 | } 169 | } 170 | 171 | unittest { 172 | custom(0, "FILE", "%s", __FILE__); 173 | } 174 | -------------------------------------------------------------------------------- /danode/response.d: -------------------------------------------------------------------------------- 1 | module danode.response; 2 | 3 | import danode.imports; 4 | import danode.cgi : CGI; 5 | import danode.interfaces : DriverInterface, StringDriver; 6 | import danode.process : Process, WaitResult; 7 | import danode.functions : htmltime; 8 | import danode.statuscode : StatusCode; 9 | import danode.request : Request; 10 | import danode.router : Router; 11 | import danode.mimetypes : UNSUPPORTED_FILE; 12 | import danode.payload : Payload, FilePayload, PayloadType, HeaderType, Empty, Message; 13 | import danode.log; 14 | import danode.webconfig; 15 | import danode.filesystem : FileSystem; 16 | import danode.post : serverAPI; 17 | import danode.functions : browseDir; 18 | 19 | immutable string SERVERINFO = "DaNode/0.0.3"; 20 | 21 | struct Response { 22 | string protocol = "HTTP/1.1"; 23 | string connection = "Keep-Alive"; 24 | string charset = "UTF-8"; 25 | Address address; 26 | long maxage = 0; 27 | string[string] headers; 28 | Payload payload; 29 | bool created = false; 30 | bool havepost = false; 31 | bool routed = false; 32 | bool completed = false; 33 | bool cgiheader = false; 34 | Appender!(char[]) hdr; 35 | ptrdiff_t index = 0; 36 | 37 | final void customheader(string key, string value) nothrow { headers[key] = value; } 38 | 39 | // Generate a HTML header for the response 40 | @property final char[] header() { 41 | if (hdr.data) { 42 | return(hdr.data); // Header was constructed 43 | } 44 | // Scripts are allowed to have their own header 45 | if (payload.type == PayloadType.Script) { 46 | CGI script = to!CGI(payload); 47 | string scriptheader = script.fullHeader(); 48 | auto status = script.statuscode(); 49 | custom(1, "INFO", "script '%s', status (%s)", script.command, status); 50 | connection = script.getHeader("Connection", "No Request"); 51 | long clength = script.getHeader("Content-Length", -1); // Is the content length provided ? 52 | foreach (line; scriptheader.split("\n")) { 53 | auto v = line.split(": "); 54 | if(v.length == 2) this.headers[v[0]] = chomp(v[1]); 55 | } 56 | if (status.code != 500) { 57 | hdr.put(format("%s %d %s\r\n", protocol, payload.statuscode, payload.statuscode.reason)); 58 | hdr.put(scriptheader); 59 | if(clength == -1) connection = "Close"; 60 | return(hdr.data); // The script can communicate 61 | } 62 | if (connection != "No Request" && clength > -1) { 63 | custom(0, "KEEP", "script '%s' in keepalive mode connection '%s' (%s, %d)", script.command, connection, script.headerType(), clength); 64 | hdr.put(scriptheader); 65 | return(hdr.data); // The script can communicate 66 | } 67 | if (connection != "Close") { 68 | custom(0, "WARN", "script '%s', failed (%s, %d)\n%s", script.command, script.headerType(), clength, scriptheader); 69 | }else{ 70 | custom(1, "INFO", "script '%s', header generation (%s, %d)", script.command, script.headerType(), clength); 71 | } 72 | connection = "Close"; 73 | } 74 | // Construct the header for all other requests (and scripts that failed to provide a valid one 75 | hdr.put(format("%s %d %s\r\n", protocol, payload.statuscode, payload.statuscode.reason)); 76 | foreach (key, value; headers) { 77 | hdr.put(format("%s: %s\r\n", key, value)); 78 | } 79 | hdr.put(format("Date: %s\r\n", htmltime())); 80 | if (payload.type != PayloadType.Script && payload.length >= 0) { // If we have any payload 81 | hdr.put(format("Content-Length: %d\r\n", payload.length)); // We can send the expected size 82 | hdr.put(format("Last-Modified: %s\r\n", htmltime(payload.mtime))); // It could be modified long ago, lets inform the client 83 | if (maxage > 0) { // Perhaps we can have the client cache it (when very old) 84 | hdr.put(format("Cache-Control: max-age=%d, public\r\n", maxage)); 85 | } 86 | } 87 | hdr.put(format("Content-Type: %s\r\n", payload.mimetype)); // We just send our mime and an encoding 88 | //hdr.put(format("Content-Type: %s; charset=%s\r\n", payload.mimetype, charset)); // We just send our mime and an encoding 89 | hdr.put(format("Connection: %s\r\n\r\n", connection)); // Client can choose to keep-alive 90 | return(hdr.data); 91 | } 92 | 93 | @property final StatusCode statuscode() const { return payload.statuscode; } 94 | @property final bool keepalive() const { return( toLower(connection) == "keep-alive"); } 95 | @property final long length() { return header.length + payload.length; } 96 | @property final const(char)[] bytes(in ptrdiff_t maxsize = 1024) { // Stream of bytes (header + stream of bytes) 97 | ptrdiff_t hsize = header.length; 98 | if(index <= hsize) { // We haven't completed the header yet 99 | return(header[index .. hsize] ~ payload.bytes(0, maxsize-hsize)); 100 | } 101 | return(payload.bytes(index-hsize)); // Header completed, just stream bytes from the payload 102 | } 103 | 104 | @property final bool ready(bool r = false){ if(r){ routed = r; } return(routed && payload.ready()); } 105 | } 106 | 107 | // parse a HTTPresponse header from an external script 108 | char[] parseHTTPResponseHeader(ref Response response, CGI script, HeaderType type, long clength) { 109 | if (type == HeaderType.FastCGI) { 110 | // FastCGI type header, create response line on Status: indicator 111 | string status = script.getHeader("Status", "500 Internal Server Error"); 112 | response.hdr.put(format("%s %s\n", "HTTP/1.1", status)); 113 | } 114 | 115 | /* foreach (line; script.fullHeader().split("\n")) { 116 | auto v = line.split(": "); 117 | response.headers[v[0]] = v[1]; 118 | } */ 119 | response.hdr.put(script.fullHeader()); 120 | info("script: status: %d, eoh: %d, content: %d", script.statuscode, script.endOfHeader(), clength); 121 | response.connection = strip(script.getHeader("Connection", "Close")); 122 | info("connection: %s -> %s, to %s in %d bytes", strip(script.getHeader("Connection", "Close")), response.connection, type, response.hdr.data.length); 123 | response.cgiheader = true; 124 | return(response.hdr.data); 125 | } 126 | 127 | // create a standard response 128 | Response create(in Request request, Address address, in StatusCode statuscode = StatusCode.Ok, in string mimetype = UNSUPPORTED_FILE){ 129 | Response response = Response(request.protocol); 130 | response.address = address; 131 | response.customheader("Server", SERVERINFO); 132 | response.customheader("X-Powered-By", format("%s %s.%s", name, version_major, version_minor)); 133 | response.payload = new Empty(statuscode, mimetype); 134 | if (request.keepalive) response.connection = "Keep-Alive"; 135 | response.created = true; 136 | return(response); 137 | } 138 | 139 | // send a redirect permanently response 140 | void redirect(ref Response response, in Request request, in string fqdn, bool isSecure = false) { 141 | trace("redirecting request to %s", fqdn); 142 | response.payload = new Empty(StatusCode.MovedPermanently); 143 | response.customheader("Location", format("http%s://%s%s%s", isSecure? "s": "", fqdn, request.path, request.query)); 144 | response.connection = "Close"; 145 | response.ready = true; 146 | } 147 | 148 | // serve a not modified response 149 | void notmodified(ref Response response, in Request request, in string mimetype = UNSUPPORTED_FILE) { 150 | response.payload = new Empty(StatusCode.NotModified, mimetype); 151 | response.ready = true; 152 | } 153 | 154 | // serve a 404 domain not found page 155 | void domainNotFound(ref Response response, in Request request) { 156 | warning("requested domain '%s', was not found", request.shorthost()); 157 | response.payload = new Message(StatusCode.NotFound, format("404 - No such domain is available\n")); 158 | response.ready = true; 159 | } 160 | 161 | // serve a 408 connection timed out page 162 | void setTimedOut(ref DriverInterface driver, ref Response response) { 163 | if(response.payload && response.payload.type == PayloadType.Script){ 164 | CGI cgi = to!CGI(response.payload); 165 | cgi.notifyovertime(); 166 | } 167 | response.payload = new Message(StatusCode.TimedOut, format("408 - Connection Timed Out\n")); 168 | response.ready = true; 169 | driver.send(response, driver.socket); // Send the response, hit multiple times, send what you can and return 170 | } 171 | 172 | // serve a the output of an external script 173 | void serveCGI(ref Response response, in Request request, in WebConfig config, in FileSystem fs, bool removeInput = true) { 174 | trace("requested a cgi file, execution allowed"); 175 | string localroot = fs.localroot(request.shorthost()); 176 | string localpath = config.localpath(localroot, request.path); 177 | if (!response.routed) { // Store POST data (could fail multiple times) 178 | trace("writing server variables"); 179 | fs.serverAPI(config, request, response); 180 | trace("creating CGI payload"); 181 | response.payload = new CGI(request.command(localpath), request.inputfile(fs), removeInput, request.maxtime-5); 182 | response.ready = true; 183 | } 184 | } 185 | 186 | // serve a static file from the disc, send encrypted when requested and available 187 | void serveStaticFile(ref Response response, in Request request, FileSystem fs) { 188 | trace("serving a static file"); 189 | string localroot = fs.localroot(request.shorthost()); 190 | FilePayload reqFile = fs.file(localroot, request.path); 191 | if (request.acceptsEncoding("deflate") && reqFile.hasEncodedVersion) { 192 | info("will serve %s with deflate encoding", request.path); 193 | reqFile.deflate = true; 194 | response.customheader("Content-Encoding","deflate"); 195 | } 196 | response.payload = reqFile; 197 | if (request.ifModified >= response.payload.mtime()) { // Non modified static content 198 | trace("static file has not changed, sending notmodified"); 199 | response.notmodified(request, response.payload.mimetype); 200 | } 201 | 202 | response.ready = true; 203 | } 204 | 205 | // serve a directory browsing request, via a message 206 | void serveDirectory(ref Response response, ref Request request, in WebConfig config, in FileSystem fs) { 207 | trace("sending browse directory"); 208 | string localroot = fs.localroot(request.shorthost()); 209 | string localpath = config.localpath(localroot, request.path); 210 | response.payload = new Message(StatusCode.Ok, browseDir(localroot, localpath), "text/html"); 211 | response.ready = true; 212 | } 213 | 214 | // serve a forbidden page 215 | void serveForbidden(ref Response response, in Request request) { 216 | trace("resource is restricted from being accessed"); 217 | response.payload = new Message(StatusCode.Forbidden, format("403 - Access to this resource has been restricted\n")); 218 | response.ready = true; 219 | } 220 | 221 | // serve a 400 bad request 222 | void serveBadRequest(ref Response response, in Request request) { 223 | trace("Request was malformed"); 224 | response.payload = new Message(StatusCode.BadRequest, format("400 - Bad Request\n")); 225 | response.ready = true; 226 | } 227 | 228 | // serve a 404 not found page 229 | void notFound(ref Response response) { 230 | trace("resource not found"); 231 | response.payload = new Message(StatusCode.NotFound, format("404 - The requested path does not exists on disk\n")); 232 | response.ready = true; 233 | } 234 | 235 | unittest { 236 | custom(0, "FILE", "%s", __FILE__); 237 | } 238 | 239 | -------------------------------------------------------------------------------- /danode/router.d: -------------------------------------------------------------------------------- 1 | module danode.router; 2 | 3 | import danode.imports; 4 | import danode.cgi : CGI; 5 | import danode.client : Client; 6 | import danode.interfaces : ClientInterface, DriverInterface, StringDriver; 7 | import danode.statuscode : StatusCode; 8 | import danode.request : Request; 9 | import danode.response; 10 | import danode.webconfig : WebConfig; 11 | import danode.payload : Message, FilePayload; 12 | import danode.mimetypes : mime; 13 | import danode.functions : from, has, isCGI, isFILE, isDIR, Msecs, htmltime, isAllowed; 14 | import danode.filesystem : FileSystem; 15 | import danode.post : parsePost, PostType; 16 | import danode.log : custom, trace, info, Log, NOTSET, NORMAL; 17 | version(SSL) { 18 | import danode.ssl : hasCertificate; 19 | } 20 | 21 | class Router { 22 | private: 23 | FileSystem filesystem; 24 | Log logger; 25 | WebConfig config; 26 | Address address; 27 | 28 | public: 29 | this(string wwwRoot = "./www/", Address address = Address.init, int verbose = NORMAL){ 30 | this.logger = new Log(verbose); 31 | this.address = address; 32 | this.filesystem = new FileSystem(logger, wwwRoot); 33 | } 34 | 35 | // Update the performance statistics and log the finished request 36 | void logRequest(in ClientInterface client, in Request request, in Response response) { 37 | logger.updatePerformanceStatistics(client, request, response); 38 | logger.logRequest(client, request, response); 39 | } 40 | 41 | // Parse the header of a request, or receive additional post data when the user is uploading 42 | final bool parse(in DriverInterface driver, ref Request request, ref Response response, long maxtime = 4500) { 43 | if (!driver.hasHeader()) return(false); 44 | if (!response.created) { 45 | request.initialize(driver, maxtime); 46 | response = request.create(this.address); 47 | } else { 48 | request.update(driver.body); 49 | } 50 | return(true); 51 | } 52 | 53 | // Route a request based on the request header 54 | final void route(DriverInterface driver, ref Request request, ref Response response, long maxtime = 4500) { 55 | if ( !response.routed && parse(driver, request, response, maxtime + 10)) { 56 | if ( parsePost(request, response, filesystem) ) { // We have stored all the post data, and can deliver a response 57 | deliver(request, response); 58 | } 59 | } 60 | } 61 | 62 | // Deliver a response to the request 63 | final void deliver(ref Request request, ref Response response, bool finalrewrite = false) { 64 | if (!request.isValid) return response.serveBadRequest(request); 65 | 66 | string localroot = filesystem.localroot(request.shorthost()); 67 | 68 | trace("%s:%s %s client (%s)", request.ip, request.port, (finalrewrite? "redirecting" : "routing"), request.id); 69 | trace("shorthost -> localroot: %s -> %s", request.shorthost(), localroot); 70 | 71 | if (request.shorthost() == "" || !exists(localroot)) // No domain requested, or we are not hosting it 72 | return response.domainNotFound(request); 73 | 74 | config = WebConfig(filesystem.file(localroot, "/web.config")); 75 | string fqdn = config.domain(request.shorthost()); 76 | string localpath = config.localpath(localroot, decodeComponent(request.path)); 77 | 78 | trace("configfile at: %s%s", localroot, "/web.config"); 79 | trace("request.host: %s, fqdn: %s", request.host, fqdn); 80 | trace("localpath: %s, exists ? %s", localpath, localpath.exists()); 81 | 82 | version (SSL) { 83 | // Check if teh security requested can be provided, by checking SSL status 84 | // against a certificate availability, and/or fix the requested the wrong 85 | // shortdomain requested by the client (domain.com or www.domain.com) 86 | if (request.isSecure != hasCertificate(fqdn) || request.host != fqdn) { 87 | trace("SSL redirect %s != %s for %s to fqdn: %s", request.isSecure, hasCertificate(fqdn), request.host, fqdn); 88 | return response.redirect(request, fqdn, hasCertificate(fqdn)); 89 | } 90 | } else { 91 | // No SSL, just check if the client requested the 'wrong' fully qualified 92 | // domain (domain.com or www.domain.com), and redirect them 93 | if (request.host != fqdn) { 94 | return response.redirect(request, fqdn, false); 95 | } 96 | } 97 | 98 | if (localpath.exists()) { 99 | trace("localpath %s exists", localpath); 100 | // A path that can be responded to has been detected, it is an existing resource 101 | if (localpath.isCGI() && config.allowcgi) { 102 | trace("localpath %s is a CGI file", localpath); 103 | return response.serveCGI(request, config, filesystem); // Serve CGI script 104 | } 105 | if (localpath.isFILE() && !localpath.isCGI() && localpath.isAllowed()) { 106 | trace("localpath %s is a normal file", localpath); 107 | return response.serveStaticFile(request, filesystem); 108 | } 109 | if (localpath.isDIR() && config.dirAllowed(localroot, localpath)) { 110 | trace("localpath %s is a directory [%s,%s]", localpath, config.redirectdir(), config.index()); 111 | if (config.redirectdir() && !finalrewrite) // Route this directory request to the index page 112 | return this.redirectDirectory(request, response); // Redirect the directory 113 | 114 | if (config.redirect() && exists(localpath ~ "/" ~ config.index()) && !finalrewrite) // Route this directory request to the index page 115 | return this.redirectCanonical(request, response); // Redirect the directory 116 | 117 | return response.serveDirectory(request, config, filesystem); 118 | } 119 | return response.serveForbidden(request); 120 | } 121 | trace("redirect: %s %d", config.redirect, finalrewrite); 122 | if(config.redirect && !finalrewrite) // Route this request as canonical request the index page 123 | return this.redirectCanonical(request, response); 124 | 125 | return response.notFound(); // Request is not hosted on this server 126 | } 127 | 128 | // Redirect a directory browsing request to the index script 129 | void redirectDirectory(ref Request request, ref Response response){ 130 | trace("redirecting directory request to index page"); 131 | request.redirectdir(config); 132 | return deliver(request, response, true); 133 | } 134 | 135 | // Perform a canonical redirect of a non-existing page to the index script 136 | void redirectCanonical(ref Request request, ref Response response){ 137 | trace("redirecting non-existing page (canonical url) to the index page"); 138 | request.page = request.uripath(); // Save the URL path 139 | request.url = format("%s?%s", config.index, request.query); 140 | return deliver(request, response, true); 141 | } 142 | 143 | // Set the verbose level by string value 144 | final @property int verbose(string verbose = "") { 145 | string[] sp = verbose.split(" "); 146 | int nval = NOTSET; 147 | if(sp.length == 1) nval = to!int(sp[0]); 148 | if(sp.length >= 2) nval = to!int(sp[1]); 149 | return(logger.verbose(nval)); 150 | } 151 | } 152 | 153 | // Helper function used to make calls during a unittest, setup a driver, a client and run the request 154 | void runRequest(Router router, string request = "GET /dmd.d HTTP/1.1\nHost: localhost\n\n") { 155 | auto driver = new StringDriver(request); 156 | auto client = new Client(router, driver, 250); 157 | custom(0, "TEST", "%s:%s %s", client.ip(), client.port(), split(request, "\n")[0]); 158 | client.start(); 159 | while (client.running()) { 160 | Thread.sleep(dur!"msecs"(2)); 161 | } 162 | } 163 | 164 | unittest { 165 | custom(0, "FILE", "%s", __FILE__); 166 | 167 | auto router = new Router("./www/", Address.init, NORMAL); 168 | router.runRequest("GET /dmd.d HTTP/1.1\nHost: localhost\n\n"); 169 | router.runRequest("POST /dmd.d HTTP/1.1\nHost: localhost\n\n"); 170 | 171 | router.runRequest("GET /keepalive.d HTTP/1.1\nHost: localhost\n\n"); 172 | router.runRequest("POST /keepalive.d HTTP/1.1\nHost: localhost\n\n"); 173 | 174 | router.runRequest("GET /notfound.txt HTTP/1.1\nHost: localhost\n\n"); 175 | router.runRequest("POST /notfound.txt HTTP/1.1\nHost: localhost\n\n"); 176 | 177 | router.runRequest("GET /data.ill HTTP/1.1\nHost: localhost\n\n"); 178 | router.runRequest("POST /data.ill HTTP/1.1\nHost: localhost\n\n"); 179 | 180 | router.runRequest("GET /ISE1.d HTTP/1.1\nHost: localhost\n\n"); 181 | router.runRequest("POST /ISE1.d HTTP/1.1\nHost: localhost\n\n"); 182 | 183 | router.runRequest("GET /ISE2.d HTTP/1.1\nHost: localhost\n\n"); 184 | router.runRequest("POST /ISE2.d HTTP/1.1\nHost: localhost\n\n"); 185 | 186 | router.runRequest("GET /ISE3.d HTTP/1.1\nHost: localhost\nConnection: keep-alive\n\n"); 187 | router.runRequest("POST /ISE3.d HTTP/1.1\nHost: localhost\nConnection: keep-alive\n\n"); 188 | 189 | router.runRequest("GET /test.txt HTTP/1.1\nHost: localhost\n\n"); 190 | router.runRequest("POST /test.txt HTTP/1.1\nHost: localhost\n\n"); 191 | 192 | router.runRequest("GET /test HTTP/1.1\nHost: localhost\n\n"); 193 | router.runRequest("POST /test HTTP/1.1\nHost: localhost\n\n"); 194 | 195 | router.runRequest("GET /test/1.txt HTTP/1.1\nHost: localhost\n\n"); 196 | router.runRequest("POST /test/1.txt HTTP/1.1\nHost: localhost\n\n"); 197 | 198 | router.runRequest("GET /test/notfound.txt HTTP/1.1\nHost: localhost\n\n"); 199 | router.runRequest("POST /test/notfound.txt HTTP/1.1\nHost: localhost\n\n"); 200 | } 201 | 202 | -------------------------------------------------------------------------------- /danode/server.d: -------------------------------------------------------------------------------- 1 | module danode.server; 2 | 3 | import danode.imports; 4 | import danode.functions : Msecs, sISelect; 5 | import danode.client : Client; 6 | import danode.interfaces : DriverInterface; 7 | import danode.http : HTTP; 8 | import danode.router : Router; 9 | import danode.log; 10 | import danode.serverconfig : ServerConfig; 11 | 12 | version(SSL) { 13 | import danode.ssl : initSSL, closeSSL; 14 | import danode.https : HTTPS; 15 | } 16 | 17 | class Server : Thread { 18 | private: 19 | Socket socket; // The server socket 20 | SocketSet set; // SocketSet for server socket and client listeners 21 | Client[] clients; // List of clients 22 | bool terminated; // Server running 23 | SysTime starttime; // Start time of the server 24 | Router router; // Router to route requests 25 | version(SSL) { 26 | Socket sslsocket; // SSL / HTTPs socket 27 | } 28 | 29 | public: 30 | this(ushort port = 80, int backlog = 100, string wwwRoot = "./www/", int verbose = NORMAL) { 31 | this.starttime = Clock.currTime(); // Start the timer 32 | this.socket = initialize(port, backlog); // Create the HTTP socket 33 | this.router = new Router(wwwRoot, this.socket.localAddress(), verbose); // Start the router 34 | version(SSL) { 35 | this.sslsocket = initialize(443, backlog); // Create the SSL / HTTPs socket 36 | } 37 | set = new SocketSet(1); // Create a server socket set 38 | custom(0, "SERVER", "server '%s' created backlog: %d", this.hostname(), backlog); 39 | super(&run); 40 | } 41 | 42 | // Initialize the listening socket to a certain port and backlog 43 | Socket initialize(ushort port = 80, int backlog = 100) { 44 | Socket socket; 45 | try { 46 | socket = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP); 47 | socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 48 | socket.blocking = false; 49 | socket.bind(new InternetAddress(port)); 50 | socket.listen(backlog); 51 | custom(0, "SERVER", "socket listening on port %s", port); 52 | } catch(Exception e) { 53 | abort(format("unable to bind socket on port %s\n%s", port, e.msg), -1); 54 | } 55 | return socket; 56 | } 57 | 58 | // Accept an incoming connection and create a client object 59 | final Client accept(Socket socket, bool secure = false) { 60 | if (set.isSet(socket)) { 61 | try { 62 | DriverInterface driver = null; 63 | if(!secure) driver = new HTTP(socket.accept(), false); 64 | version(SSL) { 65 | if(secure) driver = new HTTPS(socket.accept(), false); 66 | } 67 | if(driver is null) return(null); 68 | Client client = new Client(router, driver); 69 | client.start(); 70 | //Thread.sleep(dur!"msecs"(1)); 71 | return(client); 72 | } catch(Exception e) { 73 | error("unable to accept connection: %s", e.msg); 74 | } 75 | } else { 76 | error("socket is not in the socketset"); 77 | } 78 | return(null); 79 | } 80 | 81 | // is the server still running ? 82 | final @property bool running(){ synchronized { 83 | version(SSL) { 84 | return(socket.isAlive() && sslsocket.isAlive() && !terminated); 85 | } else { 86 | return(socket.isAlive() && !terminated); 87 | } 88 | } } 89 | 90 | // Stop all clients and shutdown the server 91 | final void stop(){ synchronized { 92 | foreach(ref Client client; clients){ client.stop(); } terminated = true; 93 | } } 94 | 95 | // Returns a Duration object holding the server uptime 96 | final @property Duration uptime() const { return(Clock.currTime() - starttime); } 97 | 98 | // Print some server information 99 | final @property void info() { 100 | custom(0, "SERVER", "uptime %s\n[INFO] # of connections: %d / %d", uptime(), nAlive(), clients.length); 101 | } 102 | 103 | // Hostanme of the server 104 | final @property string hostname() { return(this.socket.hostName()); } 105 | 106 | // Number of alive connections 107 | final @property long nAlive() { 108 | long sum = 0; foreach(Client client; clients){ if(client.running){ sum++; } } return sum; 109 | } 110 | 111 | final @property int verbose(string verbose = "") { return(router.verbose(verbose)); } // Verbose level 112 | 113 | final void run() { 114 | int select; 115 | Appender!(Client[]) persistent; 116 | while(running) { 117 | try { 118 | persistent.clear(); 119 | if ((select = set.sISelect(socket)) > 0) { 120 | custom(3, "SERVER", "accepting HTTP request"); 121 | Client client = this.accept(socket); 122 | if(client !is null) persistent.put(client); 123 | } 124 | version (SSL) { 125 | if ((select = set.sISelect(sslsocket)) > 0) { 126 | custom(3, "SERVER", "accepting HTTPs request"); 127 | Client client = this.accept(sslsocket, true); 128 | if(client !is null) persistent.put(client); 129 | } 130 | } 131 | foreach(Client client; clients){ if(client.running){ persistent.put(client); } } // Add the backlog of persistent clients 132 | clients = persistent.data; 133 | } catch(Exception e) { 134 | error("Unspecified top level server error: %s", e.msg); 135 | } 136 | } 137 | custom(0, "SERVER", "Server socket closed, running: %s", running); 138 | socket.close(); 139 | version (SSL) { 140 | sslsocket.closeSSL(); 141 | } 142 | } 143 | } 144 | 145 | void parseKeyInput(ref Server server){ 146 | string line = chomp(stdin.readln()); 147 | if (line.startsWith("quit")) server.stop(); 148 | if (line.startsWith("info")) server.info(); 149 | if (line.startsWith("verbose")) server.verbose(line); 150 | } 151 | 152 | void main(string[] args) { 153 | version(unittest){ ushort port = 8080; }else{ ushort port = 80; } 154 | int backlog = 100; 155 | int verbose = NORMAL; 156 | bool keyoff = false; 157 | string certDir = ".ssl/"; 158 | string keyFile = ".ssl/server.key"; 159 | string wwwRoot = "./www/"; 160 | getopt(args, "port|p", &port, // Port to listen on 161 | "backlog|b", &backlog, // Backlog of clients supported 162 | "keyoff|k", &keyoff, // Keyboard on or off 163 | "certDir", &certDir, // Location of SSL certificates 164 | "keyFile", &keyFile, // Server private key 165 | "wwwRoot", &wwwRoot, // Server www root folder 166 | "verbose|v", &verbose); // Verbose level (via commandline) 167 | version (unittest) { 168 | // Do nothing, unittests will run 169 | } else { 170 | version (Posix) { 171 | import core.sys.posix.signal : signal, SIGPIPE; 172 | import danode.signals : handle_signal; 173 | signal(SIGPIPE, &handle_signal); 174 | } 175 | version (Windows) { 176 | warning("-k has been set to true, we cannot handle keyboard input under windows at the moment"); 177 | keyoff = true; 178 | } 179 | 180 | auto server = new Server(port, backlog, wwwRoot, verbose); 181 | version (SSL) { 182 | server.initSSL(certDir, keyFile); // Load SSL certificates, using the server key 183 | } 184 | server.start(); 185 | while (server.running) { 186 | if (!keyoff) { 187 | server.parseKeyInput(); 188 | } 189 | stdout.flush(); 190 | Thread.sleep(dur!"msecs"(250)); 191 | } 192 | info("server shutting down: %d", server.running); 193 | server.info(); 194 | } 195 | } 196 | 197 | unittest { 198 | custom(0, "FILE", "%s", __FILE__); 199 | version(SSL) { 200 | custom(0, "TEST", "SSL support"); 201 | }else{ 202 | custom(0, "TEST", "No SSL support"); 203 | } 204 | } 205 | 206 | -------------------------------------------------------------------------------- /danode/serverconfig.d: -------------------------------------------------------------------------------- 1 | module danode.serverconfig; 2 | 3 | import danode.imports; 4 | import danode.payload : FilePayload; 5 | import danode.log : custom; 6 | 7 | struct ServerConfig { 8 | string[string] data; 9 | 10 | this(FilePayload file, string def = "no") { 11 | string[] elements; 12 | foreach(line; split(file.content, "\n")){ 13 | if(chomp(strip(line)) != "" && line[0] != '#'){ 14 | elements = split(line, "="); 15 | string key = toLower(chomp(strip(elements[0]))); 16 | if(elements.length == 1){ 17 | data[key] = def; 18 | }else if(elements.length >= 2){ 19 | data[key] = toLower(chomp(strip(join(elements[1 .. $], "=")))); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | unittest { 27 | custom(0, "FILE", "%s", __FILE__); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /danode/signals.d: -------------------------------------------------------------------------------- 1 | module danode.signals; 2 | 3 | version(Posix) { 4 | import core.sys.posix.unistd : write; 5 | import core.sys.posix.signal : SIGPIPE; 6 | 7 | import danode.log : cverbose; 8 | 9 | extern(C) @nogc nothrow void handle_signal(int signal) { 10 | switch (signal) { 11 | case SIGPIPE: 12 | if(cverbose > 1) write(2, cast(const(void*)) "[SIG] Broken pipe caught, and ignored\n\0".ptr, 41); 13 | break; 14 | default: 15 | if(cverbose > 1) write(2, cast(const(void*)) "[SIG] Caught\n\0".ptr, 17); 16 | break; 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /danode/ssl.d: -------------------------------------------------------------------------------- 1 | module danode.ssl; 2 | 3 | import danode.log : custom, warning, info; 4 | 5 | version(SSL) { 6 | import deimos.openssl.ssl; 7 | import deimos.openssl.err; 8 | 9 | import danode.imports; 10 | import danode.client; 11 | import danode.server : Server; 12 | import danode.response : Response; 13 | 14 | //--- Add shims for OpenSSL 1.1, from: https://github.com/CyberShadow/ae 15 | alias SSLv3_server_method = TLSv1_server_method; 16 | void SSL_load_error_strings() {} 17 | struct OPENSSL_INIT_SETTINGS; 18 | extern(C) void OPENSSL_init_ssl(ulong opts, const OPENSSL_INIT_SETTINGS *settings) nothrow; 19 | void SSL_library_init() { OPENSSL_init_ssl(0, null); } 20 | void OpenSSL_add_all_algorithms() { SSL_library_init(); } 21 | // --- // 22 | 23 | // SSL context structure, stored relation between hostname 24 | // and the SSL context, should be allocated only once available to C, and deallocated at exit 25 | struct SSLcontext { 26 | char[256] hostname; 27 | SSL_CTX* context; 28 | } 29 | 30 | alias size_t VERSION; 31 | immutable VERSION SSL23 = 0, SSL3 = 1, TLS1 = 2, DTLS1 = 3; 32 | 33 | alias ExternC(T) = SetFunctionAttributes!(T, "C", functionAttributes!T); 34 | 35 | extern (C) 36 | { 37 | __gshared int ncontext; // How many contexts are available 38 | __gshared SSLcontext* contexts; // SSL / HTTPs contexts (allocated globally from C) 39 | 40 | // C callback function to switch SSL contexts after hostname lookup 41 | static void switchContext(SSL* ssl, int *ad, void *arg) { 42 | string hostname = to!(string)(cast(const(char*)) SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name)); 43 | custom(1, "HTTPS", "looking for hostname: %s", hostname); 44 | if(hostname is null) { 45 | custom(1, "WARN", "client does not support Server Name Indication (SNI), using default: contexts[0]"); 46 | return; 47 | } 48 | string s; 49 | for(int x = 0; x < ncontext; x++) { 50 | s = to!string(contexts[x].hostname.ptr); 51 | custom(1, "HTTPS", "context: %s %s", hostname, s); 52 | if(hostname.endsWith(s)) { 53 | custom(1, "HTTPS", "switching SSL context to %s", hostname); 54 | SSL_set_SSL_CTX(ssl, contexts[x].context); 55 | return; 56 | } 57 | } 58 | custom(1, "WARN", "callback failed to find certificate for %s", hostname); 59 | return; 60 | } 61 | } 62 | 63 | // Create a new SSL context pointer using a certificate, chain and privateKey file 64 | SSL_CTX* createCTX(string certFile, string keyFile, string chainFile) { 65 | SSL_CTX* ctx = SSL_CTX_new(SSLv3_server_method()); 66 | sslAssert(!(ctx is null)); 67 | sslAssert(SSL_CTX_use_certificate_file(ctx, cast(const char*) toStringz(certFile), SSL_FILETYPE_PEM) > 0); 68 | if (exists(chainFile) && isFile(chainFile)) { 69 | custom(1, "HTTPS", "loading certificate chain from file: %s", chainFile); 70 | sslAssert(SSL_CTX_use_certificate_chain_file(ctx, cast(const char*) toStringz(chainFile)) > 0); 71 | } 72 | sslAssert(SSL_CTX_use_PrivateKey_file(ctx, cast(const char*) toStringz(keyFile), SSL_FILETYPE_PEM) > 0); 73 | sslAssert(SSL_CTX_check_private_key(ctx) > 0); 74 | return ctx; 75 | } 76 | 77 | // Does the hostname requested have a certificate ? 78 | bool hasCertificate(string hostname) { 79 | custom(1, "HTTPS", "'%s' certificate?", hostname); 80 | string s; 81 | for(size_t x = 0; x < ncontext; x++) { 82 | s = to!string(contexts[x].hostname.ptr); 83 | if(hostname.endsWith(s)) { 84 | custom(1, "HTTPS", "'%s' certificate found", hostname); 85 | return true; 86 | } 87 | } 88 | return false; 89 | } 90 | 91 | // Should be used after SSL_connect(), SSL_accept(), SSL_do_handshake(), 92 | // SSL_read_ex(), SSL_read(), SSL_peek_ex(), SSL_peek(), SSL_write_ex() 93 | // or SSL_write() on the ssl 94 | int checkForError(SSL* ssl, Socket socket, int retcode) { 95 | int err = SSL_get_error(ssl, retcode); 96 | switch (err) { 97 | case SSL_ERROR_NONE: 98 | /* warning("SSL_ERROR_NONE"); */ break; 99 | case SSL_ERROR_SSL: 100 | /* warning("SSL_ERROR_SSL"); */ break; 101 | case SSL_ERROR_ZERO_RETURN: 102 | /* warning("SSL_ERROR_ZERO_RETURN"); */ break; 103 | case SSL_ERROR_WANT_READ: 104 | /* warning("SSL_ERROR_WANT_READ"); */ break; 105 | case SSL_ERROR_WANT_WRITE: 106 | /* warning("SSL_ERROR_WANT_WRITE"); */ break; 107 | case SSL_ERROR_WANT_CONNECT: 108 | /* warning("SSL_ERROR_WANT_CONNECT"); */ break; 109 | case SSL_ERROR_WANT_ACCEPT: 110 | /* warning("SSL_ERROR_WANT_ACCEPT"); */ break; 111 | case SSL_ERROR_WANT_X509_LOOKUP: 112 | /* warning("SSL_ERROR_WANT_X509_LOOKUP"); */ break; 113 | case SSL_ERROR_SYSCALL: 114 | /* warning("[ERROR] SSL_ERROR_SYSCALL: RETURN: %d", retcode); */ break; 115 | default: /* warning("[ERROR] SSL_ERROR Error %d %d", err, retcode); */ break; 116 | } 117 | return(err); 118 | } 119 | 120 | // loads an SSL context for hostname from the .crt file at path; 121 | SSLcontext loadContext(string path, string hostname, string keyFile, string chainFile) { 122 | SSLcontext ctx; 123 | size_t certNameEnd = (path.length - 4); 124 | for(size_t x = 0; x < hostname.length; x++) { 125 | ctx.hostname[x] = hostname[x]; 126 | } 127 | ctx.hostname[hostname.length] = '\0'; 128 | ctx.context = createCTX(path, keyFile, chainFile); 129 | custom(1, "HTTPS", "context created for certificate: %s", to!string(ctx.hostname.ptr)); 130 | SSL_CTX_callback_ctrl(ctx.context,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cast(ExternC!(void function())) &switchContext); 131 | return(ctx); 132 | } 133 | 134 | // loads all crt files in the certDir, using keyfile: server.key 135 | SSLcontext* initSSL(Server server, string certDir = ".ssl/", string keyFile = ".ssl/server.key", VERSION v = SSL23) { 136 | custom(0, "HTTPS", "loading Deimos.openSSL, certDir: %s, keyFile: %s, SSL:%s", certDir, keyFile, v); 137 | SSL_library_init(); 138 | OpenSSL_add_all_algorithms(); 139 | SSL_load_error_strings(); 140 | contexts = cast(SSLcontext*) malloc(0 * SSLcontext.sizeof); 141 | custom(0, "HTTPS", "certificate folder: %d", exists(certDir)); 142 | if (!exists(certDir)) { 143 | warning("SSL certificate folder '%s' not found", certDir); 144 | return contexts; 145 | } 146 | if (!isDir(certDir)) { 147 | warning("SSL certificate folder '%s' not a folder", certDir); 148 | return contexts; 149 | } 150 | if (!exists(keyFile)) { 151 | warning("SSL private key file: '%s' not found", certDir); 152 | return contexts; 153 | } 154 | if (!isFile(keyFile)) { 155 | warning("SSL private key file: '%s' not a file", certDir); 156 | return contexts; 157 | } 158 | 159 | foreach (DirEntry d; dirEntries(certDir, SpanMode.shallow)) { 160 | if (d.name.endsWith(".crt")) { 161 | string hostname = baseName(d.name, ".crt"); 162 | if (hostname.length < 255) { 163 | string chainFile = baseName(d.name, ".crt") ~ ".chain"; 164 | custom(1, "HTTPS", "loading certificate from file: %s", d.name); 165 | contexts = cast(SSLcontext*) realloc(contexts, (ncontext+1) * SSLcontext.sizeof); 166 | contexts[ncontext] = loadContext(d.name, hostname, keyFile, chainFile); 167 | custom(1, "HTTPS", "stored certificate: %s in context: %d", to!string(contexts[ncontext].hostname.ptr), ncontext); 168 | ncontext++; 169 | } 170 | } 171 | } 172 | custom(0, "HTTPS", "loaded %s SSL certificates", ncontext); 173 | return contexts; 174 | } 175 | 176 | // Close the server SSL socket, and clean up the different contexts 177 | void closeSSL(Socket socket) { 178 | custom(1, "HTTPS", "closing server SSL socket"); 179 | socket.close(); 180 | custom(1, "HTTPS", "cleaning up %d HTTPS contexts", ncontext); 181 | for(int x = 0; x < ncontext; x++) { 182 | // Free the different SSL contexts 183 | SSL_CTX_free(contexts[x].context); 184 | } 185 | free(contexts); 186 | } 187 | 188 | void sslAssert(bool ret) { 189 | if (!ret) { 190 | ERR_print_errors_fp(stderr.getFP()); 191 | throw new Exception("SSL_ERROR"); 192 | } 193 | } 194 | 195 | unittest { 196 | custom(0, "FILE", "%s", __FILE__); 197 | } 198 | } else {// End version SSL 199 | unittest { 200 | custom(0, "WARN", "Skipping unittest for: '%s'", __FILE__); 201 | } 202 | } 203 | 204 | -------------------------------------------------------------------------------- /danode/statuscode.d: -------------------------------------------------------------------------------- 1 | module danode.statuscode; 2 | 3 | import danode.imports; 4 | 5 | struct StatusCodeT { 6 | size_t code; 7 | string reason; 8 | alias code this; 9 | } 10 | 11 | enum StatusCode : StatusCodeT { 12 | Continue = StatusCodeT(100, "Continue"), 13 | SwitchingProtocols = StatusCodeT(101, "Switching Protocols"), 14 | 15 | Ok = StatusCodeT(200, "Ok"), 16 | Created = StatusCodeT(201, "Created"), 17 | Accepted = StatusCodeT(202, "Accepted"), 18 | NonAuthoritative = StatusCodeT(203, "Non-Authoritative Information"), 19 | NoContent = StatusCodeT(204, "No Content"), 20 | ResetContent = StatusCodeT(205, "Reset Content"), 21 | PartialContent = StatusCodeT(206, "Partial Content"), 22 | 23 | MultipleChoices = StatusCodeT(300, "Multiple Choices"), 24 | MovedPermanently = StatusCodeT(301, "Moved Permanently"), 25 | Found = StatusCodeT(302, "Found"), 26 | SeeOther = StatusCodeT(303, "See Other"), 27 | NotModified = StatusCodeT(304, "Not Modified"), 28 | TemporaryRedirect = StatusCodeT(307, "Temporary Redirect"), 29 | PermanentRedirect = StatusCodeT(308, "Permanent Redirect"), 30 | 31 | BadRequest = StatusCodeT(400, "Bad Request"), 32 | Unauthorized = StatusCodeT(401, "Unauthorized"), 33 | Forbidden = StatusCodeT(403, "Forbidden"), 34 | NotFound = StatusCodeT(404, "Not Found"), 35 | MethodNotAllowed = StatusCodeT(405, "Method Not Allowed"), 36 | NotAcceptable = StatusCodeT(406, "Not Acceptable"), 37 | ProxyAuthentication = StatusCodeT(407, "Proxy Authentication Required"), 38 | TimedOut = StatusCodeT(408, "Connection Timed Out"), 39 | Conflict = StatusCodeT(409, "Conflict"), 40 | Gone = StatusCodeT(410, "Gone"), 41 | LengthRequired = StatusCodeT(411, "Length Required"), 42 | PreconditionFailed = StatusCodeT(412, "Precondition Failed"), 43 | PayloadTooLarge = StatusCodeT(413, "Payload Too Large"), 44 | UriTooLong = StatusCodeT(414, "URI Too Long"), 45 | UnsupportedMediaType = StatusCodeT(415, "Unsupported Media Type"), 46 | RangeNotSatisfiable = StatusCodeT(416, "Range Not Satisfiable"), 47 | ExpectationFailed = StatusCodeT(417, "Expectation Failed"), 48 | Teapot = StatusCodeT(418, "I'm a teapot"), 49 | UnprocessableEntity = StatusCodeT(422, "Unprocessable Entity"), 50 | TooEarly = StatusCodeT(425, "Too Early"), 51 | UpgradeRequired = StatusCodeT(426, "Upgrade Required"), 52 | PreconditionRequired = StatusCodeT(428, "Precondition Required"), 53 | TooManyRequests = StatusCodeT(429, "Too Many Requests"), 54 | HeaderFieldsTooLarge = StatusCodeT(431, "Request Header Fields Too Large"), 55 | LegalReasons = StatusCodeT(451, "Unavailable For Legal Reasons"), 56 | 57 | ISE = StatusCodeT(500, "Internal Server Error"), 58 | NotImplemented = StatusCodeT(501, "Not Implemented"), 59 | BadGateway = StatusCodeT(502, "Bad Gateway"), 60 | ServiceUnavailable = StatusCodeT(503, "Service Unavailable"), 61 | GatewayTimeout = StatusCodeT(504, "Gateway Timeout"), 62 | VersionUnsupported = StatusCodeT(505, "HTTP Version Not Supported"), 63 | NetworkAuthenticationRequired = StatusCodeT(511, "Network Authentication Required") 64 | }; 65 | 66 | unittest { 67 | import danode.log : custom; 68 | custom(0, "FILE", "%s", __FILE__); 69 | custom(0, "TEST", "statuscodes: %s", EnumMembers!StatusCode.length); 70 | /*foreach (immutable v; EnumMembers!StatusCode) { 71 | custom(2, "TEST", "[%s] %s: \"%s\"", v.code, v, v.reason); 72 | }*/ 73 | } 74 | 75 | -------------------------------------------------------------------------------- /danode/webconfig.d: -------------------------------------------------------------------------------- 1 | module danode.webconfig; 2 | 3 | import danode.imports; 4 | import danode.functions : has, from; 5 | import danode.payload : FilePayload; 6 | import danode.request : Request; 7 | import danode.log : trace; 8 | 9 | struct WebConfig { 10 | string[string] data; 11 | 12 | this(FilePayload file, string def = "no") { 13 | string[] elements; 14 | foreach (line; split(file.content, "\n")) { 15 | if (chomp(strip(line)) != "" && line[0] != '#') { 16 | elements = split(line, "="); 17 | string key = toLower(chomp(strip(elements[0]))); 18 | if (elements.length == 1) { 19 | data[key] = def; 20 | }else if (elements.length >= 2) { 21 | data[key] = toLower(chomp(strip(join(elements[1 .. $], "=")))); 22 | } 23 | } 24 | } 25 | } 26 | 27 | // Which domain is the prefered domain to be used: http://www.xxx.nl or http://xxx.nl 28 | @property string domain(string shorthost) const { 29 | if (data.from("shorturl", "yes") == "yes") return(shorthost); 30 | return(format("www.%s", shorthost)); 31 | } 32 | 33 | // Is CGI allowed ? 34 | @property @nogc bool allowcgi() const nothrow { 35 | if (data.from("allowcgi", "no") == "yes") return(true); 36 | return(false); 37 | } 38 | 39 | // concats localroot with the path specified 40 | @property string localpath(in string localroot, in string path) const { 41 | return(format("%s%s", localroot, path)); 42 | } 43 | 44 | // Should redirection be performed ? 45 | @property @nogc bool redirect() const nothrow { 46 | return(data.from("redirect", "/") != "/"); 47 | } 48 | 49 | // Should directories be redirected to the index page ? 50 | @property @nogc bool redirectdir() const nothrow { 51 | return(data.from("redirectdir", "no") != "no"); 52 | } 53 | 54 | // What index page is specified in the 'redirect' option in the web.config file 55 | @property string index() const { 56 | string to = data.from("redirect", "/"); 57 | if (to[0] != '/') return(format("/%s", to)); 58 | return(to); 59 | } 60 | 61 | // All directories listed in the allowdirs option in the web.config file 62 | @property string[] allowdirs() const nothrow { 63 | return(data.from("allowdirs", "/").split(",")); 64 | } 65 | 66 | // Is the directory allowed to be viewed ? 67 | @property bool dirAllowed(in string localroot, in string path) const { 68 | trace("dirAllowed: %s %s", localroot, path); 69 | string npath = path[(localroot.length + 1) .. $]; 70 | trace("npath: %s", npath); 71 | if (npath == "") // path / is always allowed 72 | return(true); 73 | 74 | foreach (d; allowdirs) { 75 | trace("%s in allowdirs: %s %s", npath, d, npath.indexOf(d)); 76 | if(indexOf(strip(d), strip(npath)) == 0) return(true); 77 | } 78 | return(false); 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danode", 3 | "description": "Small and flexible HTTP server for the D language.", 4 | "authors": ["Danny Arends"], 5 | "homepage": "http://www.dannyarends.nl", 6 | "importPaths": ["danode"], 7 | "sourcePaths": ["danode"], 8 | "mainSourceFile": "danode/server.d", 9 | "targetPath": "danode", 10 | "targetName": "server", 11 | "configurations": [ 12 | { 13 | "name": "default", 14 | "targetType": "executable", 15 | }, 16 | { 17 | "name": "ssl", 18 | "targetType": "executable", 19 | "versions": ["SSL"], 20 | "lflags-windows-x86_64": ["/LIBPATH:C:/OpenSSL-Win64/lib"], 21 | "libs-windows-x86_64": ["libssl", "libcrypto"], 22 | "dependencies": { 23 | "openssl": "==1.1.6+1.0.1g" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /sh/compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | UI1="$(echo $1 | tr '[:upper:]' '[:lower:]')" 4 | CFLAGS="" 5 | 6 | if [ "$UI1" == "ssl" ]; then 7 | echo "Compiling: DaNode openSSL version" 8 | CFLAGS="--config=ssl" 9 | fi 10 | 11 | dub build --build=release $CFLAGS 12 | 13 | -------------------------------------------------------------------------------- /sh/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | authbind danode/server -k $* 4 | 5 | -------------------------------------------------------------------------------- /sh/debug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | UI1="$(echo $1 | tr '[:upper:]' '[:lower:]')" 4 | CFLAGS="" 5 | 6 | if [ "$UI1" == "ssl" ]; then 7 | echo "Compiling: DaNode openSSL version" 8 | CFLAGS="-version=SSL -I../openssl -L-lssl -L-lcrypto" 9 | fi 10 | 11 | rdmd --force --build-only -O -gc -debug $CFLAGS -w danode/server.d 12 | 13 | -------------------------------------------------------------------------------- /sh/doc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | UI1="$(echo $1 | tr '[:upper:]' '[:lower:]')" 4 | CFLAGS="" 5 | 6 | if [ "$UI1" == "ssl" ]; then 7 | echo "Documentation: DaNode openSSL version" 8 | CFLAGS="-version=SSL -I../openssl -L-lssl -L-lcrypto" 9 | fi 10 | 11 | rdmd --build-only $CFLAGS -D -Ddwww/localhost/ddoc danode/server.d 12 | -------------------------------------------------------------------------------- /sh/letsEncrypt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #### ! Only once ! Setup dependancies #### 4 | 5 | # Setup some dependancies for the certbot-auto executable 6 | # [command] $ sudo apt-get install python-pip python-dev build-essential libffi-dev 7 | # [command] $ sudo pip install virtualenv 8 | 9 | # Download the certbot from eff.org 10 | wget https://dl.eff.org/certbot-auto 11 | 12 | # Make the script executable 13 | chmod a+x certbot-auto 14 | 15 | # Create a folder holding all the .ssl related files 16 | mkdir .ssl 17 | 18 | #### ! Only once ! Generate a server private key, and domain signing request #### 19 | 20 | # Generate a Server Private key (ONLY ONCE, Backup your key !!) 21 | # [command] $ openssl genrsa 4096 > .ssl/server.key 22 | # Create a backup of your key ! 23 | # [command] $ cp .ssl/server.key /server.key.backup 24 | 25 | # Secure via SSL the domain: mydomain.com using Let's Encrypt 26 | # - Generate certificate signing request for domain (? only once ?) 27 | # [command] $ openssl req -new -sha256 -key .ssl/server.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:mydomain.com,DNS:www.mydomain.com")) > .ssl/mydomain.nl.csr 28 | 29 | #### Each time when a certificate is to be renewed #### 30 | 31 | # - Run the script in stand-alone mode, and request certificate for mydomain.com 32 | ~/downloads/certbot-auto certonly --no-bootstrap --csr .ssl/mydomain.com.csr --standalone --email MyEmail@EMail.com --agree-tos 33 | # - Copy the received certificate to the correct location and use the name: mydomain.com.crt 34 | cp 0000_cert.pem .ssl/mydomain.com.crt 35 | # - Copy the received certificate chain to the correct location and use the name: mydomain.com.chain 36 | cp 0000_chain.pem .ssl/mydomain.com.chain 37 | # - Create folder, and move all the received files (cert, chain and parent) to a safe location for backup 38 | mkdir .ssl/mydomain.com/ 39 | mv *.pem .ssl/mydomain.com/ 40 | 41 | -------------------------------------------------------------------------------- /sh/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf server.log 4 | nohup authbind danode/server -k -b 100 -v 2 > server.log 2>&1 & 5 | 6 | -------------------------------------------------------------------------------- /sh/rundebug: -------------------------------------------------------------------------------- 1 | 2 | # start the server 3 | nohup authbind danode/server -v 2 4 | 5 | # In another terminal: 6 | # 1) Make sure that we are allowed to attach to our own processes 7 | echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 8 | 9 | # 2) Get the PID of the server 10 | pgrep server 11 | 12 | # 3) start GDB and attach to our PID 13 | gdb attach 14 | 15 | # 4) in GDB set the following options to not be pausin on normal events (threads, start/stop and pagination of output) 16 | handle SIGUSR1 nostop 17 | handle SIGUSR2 nostop 18 | set pagination off 19 | c 20 | 21 | 22 | -------------------------------------------------------------------------------- /sh/selfSignedKey: -------------------------------------------------------------------------------- 1 | # Generate a private Key 2 | openssl genrsa -des3 -out server.key 1024 3 | 4 | # Certificate Signing Request, use our config 5 | openssl req -new -key server.key -out localhost.csr -config ./test/ssl.conf/localhost.cnf 6 | 7 | # Sign the key yourself get a 3650 day certificate 8 | openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt -extfile openssl.cnf -extensions v3_req 9 | 10 | # Set the passphrase into the key, so the start of the webserver doesn't ask for it 11 | cp server.key server.key.org 12 | openssl rsa -in server.key.org -out server.key 13 | 14 | -------------------------------------------------------------------------------- /sh/stop: -------------------------------------------------------------------------------- 1 | kill -9 `ps -ef | grep danode/server | grep -v grep | awk '{print $2}'` 2 | -------------------------------------------------------------------------------- /test/dmd.in: -------------------------------------------------------------------------------- 1 | S=PHP_SELF=/dmd.d 2 | S=GATEWAY_INTERFACE=CGI/1.1 3 | S=SERVER_ADDR=127.0.0.1 4 | S=SERVER_NAME=laptop.danny 5 | S=SERVER_SOFTWARE=DaNode/0.0.2 (Universal) 6 | S=SERVER_PROTOCOL=HTTP/1.1 7 | S=REQUEST_METHOD=GET 8 | S=REQUEST_TIME=1546447990 9 | S=DOCUMENT_ROOT=./www/localhost 10 | S=QUERY_STRING=? 11 | S=HTTP_CONNECTION=Keep-Alive 12 | S=HTTP_HOST=localhost:80 13 | S=HTTPS= 14 | S=REMOTE_ADDR=127.0.0.1 15 | S=REMOTE_PORT=59641 16 | S=REMOTE_PAGE=/favicon.ico 17 | S=REQUEST_DIR= 18 | S=SCRIPT_FILENAME=./www/localhost/dmd.d 19 | S=SERVER_PORT=80 20 | S=REQUEST_URI=/favicon.ico 21 | S=HTTP_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0 22 | S=HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 23 | S=HTTP_ACCEPT_CHARSET= 24 | S=HTTP_ACCEPT_ENCODING=gzip, deflate 25 | S=HTTP_ACCEPT_LANGUAGE=en-US,en;q=0.5 26 | -------------------------------------------------------------------------------- /test/empty.req: -------------------------------------------------------------------------------- 1 | echo "open $1 $2" 2 | sleep 0.3 3 | echo "GET $4 HTTP/1.1" 4 | echo "Host: $3" 5 | echo 6 | echo 7 | sleep 0.3 8 | -------------------------------------------------------------------------------- /test/gdb.in: -------------------------------------------------------------------------------- 1 | handle SIGUSR1 nostop 2 | handle SIGUSR2 nostop 3 | set pagination off 4 | run 5 | -------------------------------------------------------------------------------- /test/ldc2compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PATH=~/ldc2/bin:$PATH 4 | dub build --compiler=ldc2 --config=ssl --force 5 | 6 | -------------------------------------------------------------------------------- /test/malformed.sh: -------------------------------------------------------------------------------- 1 | ./test/empty.req localhost 80 www.dannyarends.nl / | telnet 2 | ./test/empty.req localhost 80 127.0.0.1 / | telnet 3 | ./test/empty.req localhost 80 www.xyz.nl/ /index?ii7%20_/../php | telnet 4 | -------------------------------------------------------------------------------- /test/server.files/server.conf: -------------------------------------------------------------------------------- 1 | # Web server configuration 2 | 3 | wwwroot = ./www 4 | working = /tmp 5 | watched = /var/www 6 | -------------------------------------------------------------------------------- /test/ssl.conf/README.md: -------------------------------------------------------------------------------- 1 | # Generate a private key 2 | openssl genrsa -des3 -out .ssl/server.key 1024 3 | 4 | # Set the passphrase into the key, so the start of the webserver doesn't ask for it 5 | cp .ssl/server.key .ssl/server.key.org 6 | openssl rsa -in .ssl/server.key.org -out .ssl/server.key 7 | 8 | # Certificate Signing Request, use our config 9 | openssl req -new -key .ssl/server.key -out .ssl/localhost.csr -config ./test/ssl.conf/localhost.cnf 10 | openssl req -new -key .ssl/server.key -out .ssl/dannyarends.nl.csr -config ./test/ssl.conf/dannyarends.nl.cnf 11 | openssl req -new -key .ssl/server.key -out .ssl/wordpress.test.csr -config ./test/ssl.conf/wordpress.test.cnf 12 | 13 | # View what is requested: 14 | # openssl req -text -noout -in localhost.csr 15 | 16 | # Sign the key yourself get a 3650 day certificate 17 | openssl x509 -req -days 3650 -in .ssl/localhost.csr -signkey .ssl/server.key -out .ssl/localhost.crt -extfile ./test/ssl.conf/localhost.cnf -extensions v3_req 18 | openssl x509 -req -days 3650 -in .ssl/dannyarends.nl.csr -signkey .ssl/server.key -out .ssl/dannyarends.nl.crt -extfile ./test/ssl.conf/dannyarends.nl.cnf -extensions v3_req 19 | openssl x509 -req -days 3650 -in .ssl/wordpress.test.csr -signkey .ssl/server.key -out .ssl/wordpress.test.crt -extfile ./test/ssl.conf/wordpress.test.cnf -extensions v3_req 20 | -------------------------------------------------------------------------------- /test/ssl.conf/localhost.cnf: -------------------------------------------------------------------------------- 1 | # OpenSSL configuration file. 2 | # Establish working directory. 3 | 4 | dir=. 5 | 6 | [ ca ] 7 | default_ca = CA_default 8 | 9 | [ CA_default ] 10 | default_days = 365 11 | default_md = md5 12 | 13 | [ policy_match ] 14 | countryName = match 15 | stateOrProvinceName = match 16 | organizationName = match 17 | organizationalUnitName = optional 18 | commonName = supplied 19 | emailAddress = optional 20 | 21 | [ req ] 22 | default_bits = 2048 # Size of keys 23 | default_keyfile = server.key # name of generated keys 24 | default_md = md5 # message digest algorithm 25 | string_mask = nombstr # permitted characters 26 | distinguished_name = req_distinguished_name 27 | req_extensions = v3_req 28 | 29 | [ req_distinguished_name ] 30 | # Variable name Prompt string 31 | #------------------------- ---------------------------------- 32 | 0.organizationName = Organization Name (Danny Arends) 33 | organizationalUnitName = Organizational Unit Name (department, division) 34 | emailAddress = Email Address 35 | emailAddress_max = 40 36 | localityName = Locality Name (Groningen, Groningen) 37 | stateOrProvinceName = State or Province Name (Groningen) 38 | countryName = Country Name (DE) 39 | countryName_min = 2 40 | countryName_max = 2 41 | commonName = Common Name (localhost) 42 | commonName_max = 64 43 | 44 | # Default values for the above, for consistency and less typing. 45 | # Variable name Value 46 | #------------------------ ------------------------------ 47 | 0.organizationName_default = Danny Arends 48 | localityName_default = Berlin, Berlin 49 | stateOrProvinceName_default = Berlin 50 | countryName_default = DE 51 | commonName_default = localhost 52 | 53 | [v3_ca] 54 | basicConstraints = CA:TRUE 55 | subjectKeyIdentifier = hash 56 | authorityKeyIdentifier = keyid:always,issuer:always 57 | 58 | # Extensions to add to a certificate request 59 | [v3_req] 60 | basicConstraints = CA:FALSE 61 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 62 | subjectAltName = @alt_names 63 | 64 | [alt_names] 65 | DNS.1 = localhost 66 | IP.1 = 127.0.0.1 # Loopback 67 | 68 | -------------------------------------------------------------------------------- /test/ssl.conf/wordpress.test.cnf: -------------------------------------------------------------------------------- 1 | # OpenSSL configuration file. 2 | # Establish working directory. 3 | 4 | dir=. 5 | 6 | [ ca ] 7 | default_ca = CA_default 8 | 9 | [ CA_default ] 10 | default_days = 365 11 | default_md = md5 12 | 13 | [ policy_match ] 14 | countryName = match 15 | stateOrProvinceName = match 16 | organizationName = match 17 | organizationalUnitName = optional 18 | commonName = supplied 19 | emailAddress = optional 20 | 21 | [ req ] 22 | default_bits = 2048 # Size of keys 23 | default_keyfile = server.key # name of generated keys 24 | default_md = md5 # message digest algorithm 25 | string_mask = nombstr # permitted characters 26 | distinguished_name = req_distinguished_name 27 | req_extensions = v3_req 28 | 29 | [ req_distinguished_name ] 30 | # Variable name Prompt string 31 | #------------------------- ---------------------------------- 32 | 0.organizationName = Organization Name (Danny Arends) 33 | organizationalUnitName = Organizational Unit Name (department, division) 34 | emailAddress = Email Address 35 | emailAddress_max = 40 36 | localityName = Locality Name (Groningen, Groningen) 37 | stateOrProvinceName = State or Province Name (Groningen) 38 | countryName = Country Name (DE) 39 | countryName_min = 2 40 | countryName_max = 2 41 | commonName = Common Name (wordpress.test) 42 | commonName_max = 64 43 | 44 | # Default values for the above, for consistency and less typing. 45 | # Variable name Value 46 | #------------------------ ------------------------------ 47 | 0.organizationName_default = Danny Arends 48 | localityName_default = Berlin, Berlin 49 | stateOrProvinceName_default = Berlin 50 | countryName_default = DE 51 | commonName_default = wordpress.test 52 | 53 | [v3_ca] 54 | basicConstraints = CA:TRUE 55 | subjectKeyIdentifier = hash 56 | authorityKeyIdentifier = keyid:always,issuer:always 57 | 58 | # Extensions to add to a certificate request 59 | [v3_req] 60 | basicConstraints = CA:FALSE 61 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 62 | subjectAltName = @alt_names 63 | 64 | [alt_names] 65 | DNS.1 = wordpress.test 66 | IP.1 = 127.0.0.1 # Loopback 67 | 68 | -------------------------------------------------------------------------------- /test/stress.d: -------------------------------------------------------------------------------- 1 | module test.stress; 2 | import std.stdio, std.string, std.socket, std.file, std.path, std.conv, std.getopt, std.array; 3 | import std.datetime, std.uri, std.random, core.thread, std.concurrency, std.math, core.memory; 4 | import danode.helper, danode.client, danode.structs, danode.math; 5 | 6 | alias std.array.join j; 7 | 8 | class HTTPTester : core.thread.Thread { 9 | this(uint tid, string[] urls, uint req, ushort port = 3000){ 10 | this.tid = tid; 11 | this.urls = urls; 12 | this.req = req; 13 | this.port = port; 14 | super(&run); 15 | } 16 | 17 | void run(){ 18 | string htmlGET, retCode; 19 | string[] spliturl; 20 | auto st = now(); 21 | long[][string] times; 22 | auto rnd = Random(tid); 23 | TcpSocket handle; 24 | foreach(url;randomCover(urls,rnd)){ 25 | spliturl = url.split("/"); 26 | for(uint r = 0; r < req; r++){ 27 | handle = new TcpSocket(); 28 | try{ 29 | auto stt = now(); 30 | handle.connect(new InternetAddress(spliturl[0], port)); 31 | if(spliturl.length==1) spliturl ~= ""; 32 | htmlGET = format("GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n",encode(j(spliturl[1..$],"/")), spliturl[0]); 33 | writefln("[ASK] T-%s %s%s", tid, spliturl[0], strsplit(htmlGET," ")[1]); 34 | handle.send(htmlGET); 35 | while((ret = handle.receive(buf)) > 0) 36 | data ~= buf[0..ret]; 37 | times[url] ~= (now() - stt).total!"msecs"; 38 | if(data && indexOf(to!string(data)," ") > 0) retCode = strsplit(to!string(data)," ")[1]; 39 | writefln("[%s] T-%s %s%s", retCode, tid, spliturl[0], strsplit(htmlGET," ")[1]); 40 | data = null; 41 | }catch(SocketException ex){ 42 | writefln("[500] T-%s Failed to connect to server (%s:%d)",tid, url, port); 43 | } 44 | if(handle) handle.close(); 45 | delete handle; 46 | GC.collect(); 47 | GC.minimize(); 48 | } 49 | } 50 | writefln("Thread %s:%s finished after %s requests to %s urls [%s secs]",getpid(), tid, req, urls.length, (now() - st).total!"seconds"); 51 | foreach(key, time; times) 52 | writefln("%s %s %s msecs", al(key,30), time, mean(time)); 53 | } 54 | 55 | private: 56 | string[] urls; 57 | size_t ret; 58 | uint req, tid; 59 | ushort port; 60 | char buf[1024]; 61 | char[] data; 62 | } 63 | 64 | void main(string[] args){ 65 | uint req = 5; 66 | uint work = 1; 67 | ushort port = 80; 68 | getopt(args, "req|r", &req, "work|w", &work, "port|p", &port); 69 | string[] urls = ["127.0.0.1"]; 70 | foreach(loc; dirEntries("www", SpanMode.breadth)) 71 | urls ~= "www." ~ std.array.replace(loc[4..$], "\\", "/"); 72 | writefln("Parsed %s urls, checking using %s workers", urls.length, work); 73 | for(uint w = 0; w < work; w++) 74 | (new HTTPTester(w, urls, req, port)).start(); 75 | } 76 | -------------------------------------------------------------------------------- /www/bludit.test/web.config: -------------------------------------------------------------------------------- 1 | # Web server & site parsed settings 2 | 3 | shorturl = yes 4 | allowcgi = yes 5 | redirect = index.php 6 | -------------------------------------------------------------------------------- /www/localhost/ISE1.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | int main(string[] args){ 4 | return(1/0); 5 | } 6 | -------------------------------------------------------------------------------- /www/localhost/ISE2.d: -------------------------------------------------------------------------------- 1 | import std.conv; 2 | 3 | void main() { 4 | const int value = to!int("hello"); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /www/localhost/ISE3.d: -------------------------------------------------------------------------------- 1 | import std.datetime : dur, msecs; 2 | import core.thread : Thread; 3 | 4 | void main() { 5 | while (true) { 6 | Thread.sleep(dur!"msecs"(10)); 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /www/localhost/data.ill: -------------------------------------------------------------------------------- 1 | This is confidential data 2 | Trying to access this file should trigger a 403 response 3 | -------------------------------------------------------------------------------- /www/localhost/dmd.d: -------------------------------------------------------------------------------- 1 | #!rdmd -O 2 | import std.stdio, std.compiler; 3 | import std.string : format, indexOf, split, strip, toLower; 4 | import api.danode; 5 | 6 | void main(string[] args){ 7 | setGET(args); 8 | if(SERVER["REQUEST_URI"] == "/dmd.d" || SERVER["REQUEST_URI"] == "/") { 9 | writeln("HTTP/1.1 200 OK"); 10 | writeln("Content-Type: text/html; charset=utf-8"); 11 | writeln("Connection: Keep-Alive"); // This is wrong, (Keep-Alive MUST be specified with a Content-Length) to test if the server handles it correctly 12 | writefln("Server: %s", SERVER["SERVER_SOFTWARE"]); 13 | writefln("X-Powered-By: %s %s.%s\n", std.compiler.name, version_major, version_minor); 14 | 15 | writeln(""); 16 | writeln(" "); 17 | writeln(" DaNode 'user defined' CGI (D) test script"); 18 | writeln(" "); 19 | writeln(" "); 20 | writeln(" "); 21 | writeln(" DaNode 'user defined' CGI (D) test script
"); 22 | writefln(" Server: %s
", SERVER); 23 | writefln(" Config: %s
", CONFIG); 24 | writeln("
"); 25 | writeln(" "); 26 | writefln(" ", GET); 27 | writefln(" ", POST); 28 | writefln(" ", FILES); 29 | writeln(" "); 30 | writeln(" "); 31 | writeln(" "); 32 | writeln("
GET: %s
POST: %s
FILES: %s
Test:
File:
 
"); 33 | writeln("
"); 34 | 35 | foreach(file; FILES){ // Handle any files that being uploaded 36 | string to = format("%s/%s", SERVER["DOCUMENT_ROOT"], file.name); // Choose a folder (here: root of the web folder) to save the uploads 37 | move_upload_file(file.loc, to); // Move the uploaded file to somewhere 38 | writefln("Uploaded: %s to %s", file.loc, to); // Add a message to the HTML 39 | } 40 | 41 | writeln(" "); 42 | writeln(""); 43 | } else { 44 | writeln("HTTP/1.1 404 Page Not Found"); 45 | writeln("Content-Type: text/html; charset=utf-8"); 46 | writeln("Connection: Close"); 47 | writefln("Server: %s", SERVER["SERVER_SOFTWARE"]); 48 | writefln("X-Powered-By: %s %s.%s\n", std.compiler.name, version_major, version_minor); 49 | 50 | writeln(""); 51 | writeln(" "); 52 | writeln(" 404 Page Not Found"); 53 | writeln(" "); 54 | writeln(" "); 55 | writeln(" "); 56 | writeln(" 404 - Page Not Found !"); 57 | writeln(" "); 58 | writeln(""); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /www/localhost/etc/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font-family: Georgia, Times, serif; 3 | } 4 | 5 | h1, h2, h3, h4, h5, h6 { 6 | text-shadow: 1px 1px 1px #ccc; 7 | } 8 | -------------------------------------------------------------------------------- /www/localhost/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | DaNode 'user defined' test page 4 | 5 | 6 | 7 | 8 |

DaNode 'user defined' test page

9 |
10 | Item: 11 |
16 | Quantity: 17 |
18 | File: 19 |

20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /www/localhost/keepalive.d: -------------------------------------------------------------------------------- 1 | #!rdmd -O 2 | import std.stdio, std.compiler; 3 | import std.file : copy; 4 | import std.array : Appender, appender; 5 | import std.string : format, indexOf, split, strip, toLower; 6 | import api.danode; 7 | 8 | void main(string[] args){ 9 | setGET(args); 10 | 11 | Appender!(char[]) htmlpage; // Build the output before writing the headers 12 | 13 | htmlpage.put(""); 14 | htmlpage.put(" "); 15 | htmlpage.put(" DaNode 'user defined' CGI (D) test script"); 16 | htmlpage.put(" "); 17 | htmlpage.put(" "); 18 | htmlpage.put(" "); 19 | htmlpage.put(" DaNode 'user defined' CGI (D) test script
"); 20 | htmlpage.put(format(" Server: %s
", SERVER)); 21 | htmlpage.put(format(" Config: %s
", CONFIG)); 22 | htmlpage.put("
"); 23 | htmlpage.put(" "); 24 | htmlpage.put(format(" ", GET)); 25 | htmlpage.put(format(" ", POST)); 26 | htmlpage.put(format(" ", FILES)); 27 | htmlpage.put(" "); 28 | htmlpage.put(" "); 29 | htmlpage.put(" "); 30 | htmlpage.put("
GET: %s
POST: %s
FILES: %s
Test:
File:
 
"); 31 | htmlpage.put("
"); 32 | 33 | foreach(file; FILES){ // Handle any files that being uploaded 34 | string to = format("%s/%s", SERVER["DOCUMENT_ROOT"], file.name); // Choose a folder (here: root of the web folder) to save the uploads 35 | move_upload_file(file.loc, to); // Move the uploaded file to somewhere 36 | htmlpage.put(format("Uploaded: %s to %s", file.loc, to)); // Add a message to the HTML 37 | } 38 | 39 | htmlpage.put(" "); 40 | htmlpage.put("\n"); 41 | 42 | // Write headers 43 | writeln("HTTP/1.1 200 OK"); 44 | writeln("Content-Type: text/html; charset=utf-8"); 45 | writeln("Connection: Keep-Alive"); // If Keep-Alive 46 | writefln("Content-Length: %s", htmlpage.data.length); // Content.Length == Required, and should be correct ! 47 | writefln("Server: %s", SERVER["SERVER_SOFTWARE"]); 48 | writefln("X-Powered-By: %s %s.%s\n", std.compiler.name, version_major, version_minor); 49 | 50 | // Write html output 51 | writeln(htmlpage.data); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /www/localhost/long.d: -------------------------------------------------------------------------------- 1 | #!rdmd -O 2 | import std.stdio, std.compiler, std.datetime; 3 | import core.thread : Thread; 4 | import api.danode; 5 | 6 | void main(string[] args){ setGET(args); 7 | Thread.sleep(dur!"seconds"(12)); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /www/localhost/noheader.d: -------------------------------------------------------------------------------- 1 | #!rdmd -O 2 | import std.stdio, std.compiler; 3 | import std.string : format, indexOf, split, strip, toLower; 4 | import api.danode; 5 | 6 | void main(string[] args){ 7 | setGET(args); 8 | 9 | writeln(""); 10 | writeln(" "); 11 | writeln(" DaNode 'user defined' CGI (D) test script"); 12 | writeln(" "); 13 | writeln(" "); 14 | writeln(" "); 15 | writeln(" DaNode 'user defined' CGI (D) test script
"); 16 | writefln(" Server: %s
", SERVER); 17 | writefln(" Config: %s
", CONFIG); 18 | writeln("
"); 19 | writeln(" "); 20 | writefln(" ", GET); 21 | writefln(" ", POST); 22 | writefln(" ", FILES); 23 | writeln(" "); 24 | writeln(" "); 25 | writeln(" "); 26 | writeln("
GET: %s
POST: %s
FILES: %s
Test:
File:
 
"); 27 | writeln("
"); 28 | 29 | foreach(file; FILES){ // Handle any files that being uploaded 30 | string to = format("%s/%s", SERVER["DOCUMENT_ROOT"], file.name); // Choose a folder (here: root of the web folder) to save the uploads 31 | move_upload_file(file.loc, to); // Move the uploaded file to somewhere 32 | writefln("Uploaded: %s to %s", file.loc, to); // Add a message to the HTML 33 | } 34 | 35 | writeln(" "); 36 | writeln(""); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /www/localhost/perl.pl: -------------------------------------------------------------------------------- 1 | #!perl -w 2 | use strict; 3 | use api::danode; 4 | 5 | print "HTTP/1.1 200 OK\n"; 6 | print "Content-Type: text/html; charset=utf-8\n\n"; 7 | print ""; 8 | print " "; 9 | print " DaNode 'user defined' CGI (perl) test script"; 10 | print " "; 11 | print " "; 12 | print " "; 13 | print " DaNode 'user defined' CGI (perl) test script
"; 14 | print "
"; 15 | print " "; 16 | print " "; 17 | print " "; 18 | print " "; 19 | print " "; 20 | print " "; 21 | print "
GET: ".toS($_GET)."
POST:".toS($_POST)."
Test:
File:
 
"; 22 | print "
"; 23 | print " "; 24 | print ""; 25 | -------------------------------------------------------------------------------- /www/localhost/perlinfo.pl: -------------------------------------------------------------------------------- 1 | #!perl -w 2 | use strict; 3 | use api::danode; 4 | use HTML::Perlinfo; 5 | 6 | print "HTTP/1.1 200 OK\n"; 7 | print "Content-Type: text/html; charset=utf-8\n\n"; 8 | perlinfo(); 9 | -------------------------------------------------------------------------------- /www/localhost/php-cgi.fphp: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | DaNode 'user defined' CGI (PHP) test script 13 | 14 | 15 | 16 | DaNode 'user defined' CGI (PHP) test script
17 | Config:
18 | Server:
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Get:
Post:
Files:
Cookie:
Test:
File:
 
29 |
30 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /www/localhost/php.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | DaNode 'user defined' CGI (PHP) test script 13 | 14 | 15 | 16 | DaNode 'user defined' CGI (PHP) test script
17 | Config:
18 | Server:
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Get:
Post:
Files:
Cookie:
Test:
File:
 
29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /www/localhost/phpinfo.fphp: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /www/localhost/rscript.r: -------------------------------------------------------------------------------- 1 | source("api/danode.r") 2 | 3 | cat("HTTP/1.1 200 OK\n") 4 | cat("Content-Type: text/html; charset=utf-8\n\n") 5 | cat("") 6 | cat(" ") 7 | cat(" DaNode 'user defined' CGI (R) test script") 8 | cat(" ") 9 | cat(" ") 10 | cat(" ") 11 | cat(" DaNode 'user defined' CGI (R) test script
") 12 | cat("
") 13 | cat(" ") 14 | cat(" ") 15 | cat(" ") 16 | cat(" ") 17 | cat(" ") 18 | cat(" ") 19 | cat(" ") 20 | cat("
Server: ",toS(SERVER),"
GET: ",toS(GET),"
POST: ",toS(POST),"
Test:
File:
 
") 21 | cat("
") 22 | cat(" ") 23 | cat("") 24 | -------------------------------------------------------------------------------- /www/localhost/test.ada: -------------------------------------------------------------------------------- 1 | -- 2 | -- Hello World - https://en.wikipedia.org/wiki/Ada 3 | -- GNAT Ada REPL (gnatmake) should be installed and on the path. 4 | -- > sudo apt-get install gnat 5 | 6 | with Ada.Text_IO; use Ada.Text_IO; 7 | procedure Hello is 8 | begin 9 | Put_Line ("Hello, world!"); 10 | end Hello; 11 | 12 | -------------------------------------------------------------------------------- /www/localhost/test.bf: -------------------------------------------------------------------------------- 1 | [ 2 | # Hello World - https://en.wikipedia.org/wiki/Brainfuck 3 | # Brainfuck compiler (bf) should be installed and on the path. 4 | # > sudo apt-get install bf 5 | ] 6 | 7 | ++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++. 8 | 9 | -------------------------------------------------------------------------------- /www/localhost/test.txt: -------------------------------------------------------------------------------- 1 | Just a random text file on your file system 2 | -------------------------------------------------------------------------------- /www/localhost/test/1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DannyArends/DaNode/79ffbd53cf2429c275cfb7ef829550f02e6bf6af/www/localhost/test/1.txt -------------------------------------------------------------------------------- /www/localhost/test/2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DannyArends/DaNode/79ffbd53cf2429c275cfb7ef829550f02e6bf6af/www/localhost/test/2.txt -------------------------------------------------------------------------------- /www/localhost/web.config: -------------------------------------------------------------------------------- 1 | # Web server & site parsed settings 2 | 3 | shorturl = yes 4 | allowcgi = yes 5 | redirect = dmd.d 6 | allowdirs = ddoc/,test/ 7 | 8 | # Website only settings 9 | 10 | baseurl = http://localhost 11 | databaseloc = 12 | secret_pass = 13 | 14 | -------------------------------------------------------------------------------- /www/wordpress.test/sql/createWordpress.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE wordpress; 2 | CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'wordpress'; 3 | GRANT ALL PRIVILEGES ON wordpress.* to wordpress@localhost; 4 | quit 5 | -------------------------------------------------------------------------------- /www/wordpress.test/sql/deleteWordpress.sql: -------------------------------------------------------------------------------- 1 | drop DATABASE wordpress; 2 | CREATE DATABASE wordpress; 3 | CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'wordpress'; 4 | GRANT ALL PRIVILEGES ON wordpress.* to wordpress@localhost; 5 | quit 6 | -------------------------------------------------------------------------------- /www/wordpress.test/web.config: -------------------------------------------------------------------------------- 1 | # Web server & site parsed settings 2 | 3 | shorturl = yes 4 | allowcgi = yes 5 | redirect = index.php 6 | allowdirs = wp-admin/ 7 | 8 | # Website only settings 9 | 10 | --------------------------------------------------------------------------------