├── .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: [](https://github.com/DannyArends/DaNode/actions/workflows/d.yml)
4 | licence: [](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(" ");
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 |