bump version
[sqlcipher.git] / test / wapp.tcl
blob53c21e892ffe175ff6f05c8605d8fb4e99049931
1 # Copyright (c) 2017 D. Richard Hipp
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the Simplified BSD License (also
5 # known as the "2-Clause License" or "FreeBSD License".)
7 # This program is distributed in the hope that it will be useful,
8 # but without any warranty; without even the implied warranty of
9 # merchantability or fitness for a particular purpose.
11 #---------------------------------------------------------------------------
13 # Design rules:
15 # (1) All identifiers in the global namespace begin with "wapp"
17 # (2) Indentifiers intended for internal use only begin with "wappInt"
19 package require Tcl 8.6
21 # Add text to the end of the HTTP reply. No interpretation or transformation
22 # of the text is performs. The argument should be enclosed within {...}
24 proc wapp {txt} {
25 global wapp
26 dict append wapp .reply $txt
29 # Add text to the page under construction. Do no escaping on the text.
31 # Though "unsafe" in general, there are uses for this kind of thing.
32 # For example, if you want to return the complete, unmodified content of
33 # a file:
35 # set fd [open content.html rb]
36 # wapp-unsafe [read $fd]
37 # close $fd
39 # You could do the same thing using ordinary "wapp" instead of "wapp-unsafe".
40 # The difference is that wapp-safety-check will complain about the misuse
41 # of "wapp", but it assumes that the person who write "wapp-unsafe" understands
42 # the risks.
44 # Though occasionally necessary, the use of this interface should be minimized.
46 proc wapp-unsafe {txt} {
47 global wapp
48 dict append wapp .reply $txt
51 # Add text to the end of the reply under construction. The following
52 # substitutions are made:
54 # %html(...) Escape text for inclusion in HTML
55 # %url(...) Escape text for use as a URL
56 # %qp(...) Escape text for use as a URI query parameter
57 # %string(...) Escape text for use within a JSON string
58 # %unsafe(...) No transformations of the text
60 # The substitutions above terminate at the first ")" character. If the
61 # text of the TCL string in ... contains ")" characters itself, use instead:
63 # %html%(...)%
64 # %url%(...)%
65 # %qp%(...)%
66 # %string%(...)%
67 # %unsafe%(...)%
69 # In other words, use "%(...)%" instead of "(...)" to include the TCL string
70 # to substitute.
72 # The %unsafe substitution should be avoided whenever possible, obviously.
73 # In addition to the substitutions above, the text also does backslash
74 # escapes.
76 # The wapp-trim proc works the same as wapp-subst except that it also removes
77 # whitespace from the left margin, so that the generated HTML/CSS/Javascript
78 # does not appear to be indented when delivered to the client web browser.
80 if {$tcl_version>=8.7} {
81 proc wapp-subst {txt} {
82 global wapp
83 regsub -all -command \
84 {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
85 dict append wapp .reply [subst -novariables -nocommand $txt]
87 proc wapp-trim {txt} {
88 global wapp
89 regsub -all {\n\s+} [string trim $txt] \n txt
90 regsub -all -command \
91 {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
92 dict append wapp .reply [subst -novariables -nocommand $txt]
94 proc wappInt-enc {all mode nu1 txt} {
95 return [uplevel 2 "wappInt-enc-$mode \"$txt\""]
97 } else {
98 proc wapp-subst {txt} {
99 global wapp
100 regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
101 {[wappInt-enc-\1 "\3"]} txt
102 dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
104 proc wapp-trim {txt} {
105 global wapp
106 regsub -all {\n\s+} [string trim $txt] \n txt
107 regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
108 {[wappInt-enc-\1 "\3"]} txt
109 dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
113 # There must be a wappInt-enc-NAME routine for each possible substitution
114 # in wapp-subst. Thus there are routines for "html", "url", "qp", and "unsafe".
116 # wappInt-enc-html Escape text so that it is safe to use in the
117 # body of an HTML document.
119 # wappInt-enc-url Escape text so that it is safe to pass as an
120 # argument to href= and src= attributes in HTML.
122 # wappInt-enc-qp Escape text so that it is safe to use as the
123 # value of a query parameter in a URL or in
124 # post data or in a cookie.
126 # wappInt-enc-string Escape ", ', \, and < for using inside of a
127 # javascript string literal. The < character
128 # is escaped to prevent "</script>" from causing
129 # problems in embedded javascript.
131 # wappInt-enc-unsafe Perform no encoding at all. Unsafe.
133 proc wappInt-enc-html {txt} {
134 return [string map {& &amp; < &lt; > &gt; \" &quot; \\ &#92;} $txt]
136 proc wappInt-enc-unsafe {txt} {
137 return $txt
139 proc wappInt-enc-url {s} {
140 if {[regsub -all {[^-{}@~?=#_.:/a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
141 set s [subst -novar -noback $s]
143 if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
144 set s [subst -novar -noback $s]
146 return $s
148 proc wappInt-enc-qp {s} {
149 if {[regsub -all {[^-{}_.a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
150 set s [subst -novar -noback $s]
152 if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
153 set s [subst -novar -noback $s]
155 return $s
157 proc wappInt-enc-string {s} {
158 return [string map {\\ \\\\ \" \\\" ' \\' < \\u003c} $s]
161 # This is a helper routine for wappInt-enc-url and wappInt-enc-qp. It returns
162 # an appropriate %HH encoding for the single character c. If c is a unicode
163 # character, then this routine might return multiple bytes: %HH%HH%HH
165 proc wappInt-%HHchar {c} {
166 if {$c==" "} {return +}
167 return [regsub -all .. [binary encode hex [encoding convertto utf-8 $c]] {%&}]
171 # Undo the www-url-encoded format.
173 # HT: This code stolen from ncgi.tcl
175 proc wappInt-decode-url {str} {
176 set str [string map [list + { } "\\" "\\\\" \[ \\\[ \] \\\]] $str]
177 regsub -all -- \
178 {%([Ee][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
179 $str {[encoding convertfrom utf-8 [binary decode hex \1\2\3]]} str
180 regsub -all -- \
181 {%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
182 $str {[encoding convertfrom utf-8 [binary decode hex \1\2]]} str
183 regsub -all -- {%([0-7][A-Fa-f0-9])} $str {\\u00\1} str
184 return [subst -novar $str]
187 # Reset the document back to an empty string.
189 proc wapp-reset {} {
190 global wapp
191 dict set wapp .reply {}
194 # Change the mime-type of the result document.
196 proc wapp-mimetype {x} {
197 global wapp
198 dict set wapp .mimetype $x
201 # Change the reply code.
203 proc wapp-reply-code {x} {
204 global wapp
205 dict set wapp .reply-code $x
208 # Set a cookie
210 proc wapp-set-cookie {name value} {
211 global wapp
212 dict lappend wapp .new-cookies $name $value
215 # Unset a cookie
217 proc wapp-clear-cookie {name} {
218 wapp-set-cookie $name {}
221 # Add extra entries to the reply header
223 proc wapp-reply-extra {name value} {
224 global wapp
225 dict lappend wapp .reply-extra $name $value
228 # Specifies how the web-page under construction should be cached.
229 # The argument should be one of:
231 # no-cache
232 # max-age=N (for some integer number of seconds, N)
233 # private,max-age=N
235 proc wapp-cache-control {x} {
236 wapp-reply-extra Cache-Control $x
239 # Redirect to a different web page
241 proc wapp-redirect {uri} {
242 wapp-reply-code {307 Redirect}
243 wapp-reply-extra Location $uri
246 # Return the value of a wapp parameter
248 proc wapp-param {name {dflt {}}} {
249 global wapp
250 if {![dict exists $wapp $name]} {return $dflt}
251 return [dict get $wapp $name]
254 # Return true if a and only if the wapp parameter $name exists
256 proc wapp-param-exists {name} {
257 global wapp
258 return [dict exists $wapp $name]
261 # Set the value of a wapp parameter
263 proc wapp-set-param {name value} {
264 global wapp
265 dict set wapp $name $value
268 # Return all parameter names that match the GLOB pattern, or all
269 # names if the GLOB pattern is omitted.
271 proc wapp-param-list {{glob {*}}} {
272 global wapp
273 return [dict keys $wapp $glob]
276 # By default, Wapp does not decode query parameters and POST parameters
277 # for cross-origin requests. This is a security restriction, designed to
278 # help prevent cross-site request forgery (CSRF) attacks.
280 # As a consequence of this restriction, URLs for sites generated by Wapp
281 # that contain query parameters will not work as URLs found in other
282 # websites. You cannot create a link from a second website into a Wapp
283 # website if the link contains query planner, by default.
285 # Of course, it is sometimes desirable to allow query parameters on external
286 # links. For URLs for which this is safe, the application should invoke
287 # wapp-allow-xorigin-params. This procedure tells Wapp that it is safe to
288 # go ahead and decode the query parameters even for cross-site requests.
290 # In other words, for Wapp security is the default setting. Individual pages
291 # need to actively disable the cross-site request security if those pages
292 # are safe for cross-site access.
294 proc wapp-allow-xorigin-params {} {
295 global wapp
296 if {![dict exists $wapp .qp] && ![dict get $wapp SAME_ORIGIN]} {
297 wappInt-decode-query-params
301 # Set the content-security-policy.
303 # The default content-security-policy is very strict: "default-src 'self'"
304 # The default policy prohibits the use of in-line javascript or CSS.
306 # Provide an alternative CSP as the argument. Or use "off" to disable
307 # the CSP completely.
309 proc wapp-content-security-policy {val} {
310 global wapp
311 if {$val=="off"} {
312 dict unset wapp .csp
313 } else {
314 dict set wapp .csp $val
318 # Examine the bodys of all procedures in this program looking for
319 # unsafe calls to various Wapp interfaces. Return a text string
320 # containing warnings. Return an empty string if all is ok.
322 # This routine is advisory only. It misses some constructs that are
323 # dangerous and flags others that are safe.
325 proc wapp-safety-check {} {
326 set res {}
327 foreach p [info procs] {
328 set ln 0
329 foreach x [split [info body $p] \n] {
330 incr ln
331 if {[regexp {^[ \t]*wapp[ \t]+([^\n]+)} $x all tail]
332 && [string index $tail 0]!="\173"
333 && [regexp {[[$]} $tail]
335 append res "$p:$ln: unsafe \"wapp\" call: \"[string trim $x]\"\n"
337 if {[regexp {^[ \t]*wapp-(subst|trim)[ \t]+[^\173]} $x all cx]} {
338 append res "$p:$ln: unsafe \"wapp-$cx\" call: \"[string trim $x]\"\n"
342 return $res
345 # Return a string that descripts the current environment. Applications
346 # might find this useful for debugging.
348 proc wapp-debug-env {} {
349 global wapp
350 set out {}
351 foreach var [lsort [dict keys $wapp]] {
352 if {[string index $var 0]=="."} continue
353 append out "$var = [list [dict get $wapp $var]]\n"
355 append out "\[pwd\] = [list [pwd]]\n"
356 return $out
359 # Tracing function for each HTTP request. This is overridden by wapp-start
360 # if tracing is enabled.
362 proc wappInt-trace {} {}
364 # Start up a listening socket. Arrange to invoke wappInt-new-connection
365 # for each inbound HTTP connection.
367 # port Listen on this TCP port. 0 means to select a port
368 # that is not currently in use
370 # wappmode One of "scgi", "remote-scgi", "server", or "local".
372 # fromip If not {}, then reject all requests from IP addresses
373 # other than $fromip
375 proc wappInt-start-listener {port wappmode fromip} {
376 if {[string match *scgi $wappmode]} {
377 set type SCGI
378 set server [list wappInt-new-connection \
379 wappInt-scgi-readable $wappmode $fromip]
380 } else {
381 set type HTTP
382 set server [list wappInt-new-connection \
383 wappInt-http-readable $wappmode $fromip]
385 if {$wappmode=="local" || $wappmode=="scgi"} {
386 set x [socket -server $server -myaddr 127.0.0.1 $port]
387 } else {
388 set x [socket -server $server $port]
390 set coninfo [chan configure $x -sockname]
391 set port [lindex $coninfo 2]
392 if {$wappmode=="local"} {
393 wappInt-start-browser http://127.0.0.1:$port/
394 } elseif {$fromip!=""} {
395 puts "Listening for $type requests on TCP port $port from IP $fromip"
396 } else {
397 puts "Listening for $type requests on TCP port $port"
401 # Start a web-browser and point it at $URL
403 proc wappInt-start-browser {url} {
404 global tcl_platform
405 if {$tcl_platform(platform)=="windows"} {
406 exec cmd /c start $url &
407 } elseif {$tcl_platform(os)=="Darwin"} {
408 exec open $url &
409 } elseif {[catch {exec xdg-open $url}]} {
410 exec firefox $url &
414 # This routine is a "socket -server" callback. The $chan, $ip, and $port
415 # arguments are added by the socket command.
417 # Arrange to invoke $callback when content is available on the new socket.
418 # The $callback will process inbound HTTP or SCGI content. Reject the
419 # request if $fromip is not an empty string and does not match $ip.
421 proc wappInt-new-connection {callback wappmode fromip chan ip port} {
422 upvar #0 wappInt-$chan W
423 if {$fromip!="" && ![string match $fromip $ip]} {
424 close $chan
425 return
427 set W [dict create REMOTE_ADDR $ip REMOTE_PORT $port WAPP_MODE $wappmode \
428 .header {}]
429 fconfigure $chan -blocking 0 -translation binary
430 fileevent $chan readable [list $callback $chan]
433 # Close an input channel
435 proc wappInt-close-channel {chan} {
436 if {$chan=="stdout"} {
437 # This happens after completing a CGI request
438 exit 0
439 } else {
440 unset ::wappInt-$chan
441 close $chan
445 # Process new text received on an inbound HTTP request
447 proc wappInt-http-readable {chan} {
448 if {[catch [list wappInt-http-readable-unsafe $chan] msg]} {
449 puts stderr "$msg\n$::errorInfo"
450 wappInt-close-channel $chan
453 proc wappInt-http-readable-unsafe {chan} {
454 upvar #0 wappInt-$chan W wapp wapp
455 if {![dict exists $W .toread]} {
456 # If the .toread key is not set, that means we are still reading
457 # the header
458 set line [string trimright [gets $chan]]
459 set n [string length $line]
460 if {$n>0} {
461 if {[dict get $W .header]=="" || [regexp {^\s+} $line]} {
462 dict append W .header $line
463 } else {
464 dict append W .header \n$line
466 if {[string length [dict get $W .header]]>100000} {
467 error "HTTP request header too big - possible DOS attack"
469 } elseif {$n==0} {
470 # We have reached the blank line that terminates the header.
471 global argv0
472 set a0 [file normalize $argv0]
473 dict set W SCRIPT_FILENAME $a0
474 dict set W DOCUMENT_ROOT [file dir $a0]
475 if {[wappInt-parse-header $chan]} {
476 catch {close $chan}
477 return
479 set len 0
480 if {[dict exists $W CONTENT_LENGTH]} {
481 set len [dict get $W CONTENT_LENGTH]
483 if {$len>0} {
484 # Still need to read the query content
485 dict set W .toread $len
486 } else {
487 # There is no query content, so handle the request immediately
488 set wapp $W
489 wappInt-handle-request $chan 0
492 } else {
493 # If .toread is set, that means we are reading the query content.
494 # Continue reading until .toread reaches zero.
495 set got [read $chan [dict get $W .toread]]
496 dict append W CONTENT $got
497 dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
498 if {[dict get $W .toread]<=0} {
499 # Handle the request as soon as all the query content is received
500 set wapp $W
501 wappInt-handle-request $chan 0
506 # Decode the HTTP request header.
508 # This routine is always running inside of a [catch], so if
509 # any problems arise, simply raise an error.
511 proc wappInt-parse-header {chan} {
512 upvar #0 wappInt-$chan W
513 set hdr [split [dict get $W .header] \n]
514 if {$hdr==""} {return 1}
515 set req [lindex $hdr 0]
516 dict set W REQUEST_METHOD [set method [lindex $req 0]]
517 if {[lsearch {GET HEAD POST} $method]<0} {
518 error "unsupported request method: \"[dict get $W REQUEST_METHOD]\""
520 set uri [lindex $req 1]
521 set split_uri [split $uri ?]
522 set uri0 [lindex $split_uri 0]
523 if {![regexp {^/[-.a-z0-9_/]*$} $uri0]} {
524 error "invalid request uri: \"$uri0\""
526 dict set W REQUEST_URI $uri0
527 dict set W PATH_INFO $uri0
528 set uri1 [lindex $split_uri 1]
529 dict set W QUERY_STRING $uri1
530 set n [llength $hdr]
531 for {set i 1} {$i<$n} {incr i} {
532 set x [lindex $hdr $i]
533 if {![regexp {^(.+): +(.*)$} $x all name value]} {
534 error "invalid header line: \"$x\""
536 set name [string toupper $name]
537 switch -- $name {
538 REFERER {set name HTTP_REFERER}
539 USER-AGENT {set name HTTP_USER_AGENT}
540 CONTENT-LENGTH {set name CONTENT_LENGTH}
541 CONTENT-TYPE {set name CONTENT_TYPE}
542 HOST {set name HTTP_HOST}
543 COOKIE {set name HTTP_COOKIE}
544 ACCEPT-ENCODING {set name HTTP_ACCEPT_ENCODING}
545 default {set name .hdr:$name}
547 dict set W $name $value
549 return 0
552 # Decode the QUERY_STRING parameters from a GET request or the
553 # application/x-www-form-urlencoded CONTENT from a POST request.
555 # This routine sets the ".qp" element of the ::wapp dict as a signal
556 # that query parameters have already been decoded.
558 proc wappInt-decode-query-params {} {
559 global wapp
560 dict set wapp .qp 1
561 if {[dict exists $wapp QUERY_STRING]} {
562 foreach qterm [split [dict get $wapp QUERY_STRING] &] {
563 set qsplit [split $qterm =]
564 set nm [lindex $qsplit 0]
565 if {[regexp {^[a-z][a-z0-9]*$} $nm]} {
566 dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
570 if {[dict exists $wapp CONTENT_TYPE] && [dict exists $wapp CONTENT]} {
571 set ctype [dict get $wapp CONTENT_TYPE]
572 if {$ctype=="application/x-www-form-urlencoded"} {
573 foreach qterm [split [string trim [dict get $wapp CONTENT]] &] {
574 set qsplit [split $qterm =]
575 set nm [lindex $qsplit 0]
576 if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
577 dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
580 } elseif {[string match multipart/form-data* $ctype]} {
581 regexp {^(.*?)\r\n(.*)$} [dict get $wapp CONTENT] all divider body
582 set ndiv [string length $divider]
583 while {[string length $body]} {
584 set idx [string first $divider $body]
585 set unit [string range $body 0 [expr {$idx-3}]]
586 set body [string range $body [expr {$idx+$ndiv+2}] end]
587 if {[regexp {^Content-Disposition: form-data; (.*?)\r\n\r\n(.*)$} \
588 $unit unit hdr content]} {
589 if {[regexp {name="(.*)"; filename="(.*)"\r\nContent-Type: (.*?)$}\
590 $hdr hr name filename mimetype]} {
591 dict set wapp $name.filename \
592 [string map [list \\\" \" \\\\ \\] $filename]
593 dict set wapp $name.mimetype $mimetype
594 dict set wapp $name.content $content
595 } elseif {[regexp {name="(.*)"} $hdr hr name]} {
596 dict set wapp $name $content
604 # Invoke application-supplied methods to generate a reply to
605 # a single HTTP request.
607 # This routine always runs within [catch], so handle exceptions by
608 # invoking [error].
610 proc wappInt-handle-request {chan useCgi} {
611 global wapp
612 dict set wapp .reply {}
613 dict set wapp .mimetype {text/html; charset=utf-8}
614 dict set wapp .reply-code {200 Ok}
615 dict set wapp .csp {default-src 'self'}
617 # Set up additional CGI environment values
619 if {![dict exists $wapp HTTP_HOST]} {
620 dict set wapp BASE_URL {}
621 } elseif {[dict exists $wapp HTTPS]} {
622 dict set wapp BASE_URL https://[dict get $wapp HTTP_HOST]
623 } else {
624 dict set wapp BASE_URL http://[dict get $wapp HTTP_HOST]
626 if {![dict exists $wapp REQUEST_URI]} {
627 dict set wapp REQUEST_URI /
628 } elseif {[regsub {\?.*} [dict get $wapp REQUEST_URI] {} newR]} {
629 # Some servers (ex: nginx) append the query parameters to REQUEST_URI.
630 # These need to be stripped off
631 dict set wapp REQUEST_URI $newR
633 if {[dict exists $wapp SCRIPT_NAME]} {
634 dict append wapp BASE_URL [dict get $wapp SCRIPT_NAME]
635 } else {
636 dict set wapp SCRIPT_NAME {}
638 if {![dict exists $wapp PATH_INFO]} {
639 # If PATH_INFO is missing (ex: nginx) then construct it
640 set URI [dict get $wapp REQUEST_URI]
641 set skip [string length [dict get $wapp SCRIPT_NAME]]
642 dict set wapp PATH_INFO [string range $URI $skip end]
644 if {[regexp {^/([^/]+)(.*)$} [dict get $wapp PATH_INFO] all head tail]} {
645 dict set wapp PATH_HEAD $head
646 dict set wapp PATH_TAIL [string trimleft $tail /]
647 } else {
648 dict set wapp PATH_INFO {}
649 dict set wapp PATH_HEAD {}
650 dict set wapp PATH_TAIL {}
652 dict set wapp SELF_URL [dict get $wapp BASE_URL]/[dict get $wapp PATH_HEAD]
654 # Parse query parameters from the query string, the cookies, and
655 # POST data
657 if {[dict exists $wapp HTTP_COOKIE]} {
658 foreach qterm [split [dict get $wapp HTTP_COOKIE] {;}] {
659 set qsplit [split [string trim $qterm] =]
660 set nm [lindex $qsplit 0]
661 if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
662 dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
666 set same_origin 0
667 if {[dict exists $wapp HTTP_REFERER]} {
668 set referer [dict get $wapp HTTP_REFERER]
669 set base [dict get $wapp BASE_URL]
670 if {$referer==$base || [string match $base/* $referer]} {
671 set same_origin 1
674 dict set wapp SAME_ORIGIN $same_origin
675 if {$same_origin} {
676 wappInt-decode-query-params
679 # Invoke the application-defined handler procedure for this page
680 # request. If an error occurs while running that procedure, generate
681 # an HTTP reply that contains the error message.
683 wapp-before-dispatch-hook
684 wappInt-trace
685 set mname [dict get $wapp PATH_HEAD]
686 if {[catch {
687 if {$mname!="" && [llength [info proc wapp-page-$mname]]>0} {
688 wapp-page-$mname
689 } else {
690 wapp-default
692 } msg]} {
693 if {[wapp-param WAPP_MODE]=="local" || [wapp-param WAPP_MODE]=="server"} {
694 puts "ERROR: $::errorInfo"
696 wapp-reset
697 wapp-reply-code "500 Internal Server Error"
698 wapp-mimetype text/html
699 wapp-trim {
700 <h1>Wapp Application Error</h1>
701 <pre>%html($::errorInfo)</pre>
703 dict unset wapp .new-cookies
706 # Transmit the HTTP reply
708 if {$chan=="stdout"} {
709 puts $chan "Status: [dict get $wapp .reply-code]\r"
710 } else {
711 puts $chan "HTTP/1.1 [dict get $wapp .reply-code]\r"
712 puts $chan "Server: wapp\r"
713 puts $chan "Connection: close\r"
715 if {[dict exists $wapp .reply-extra]} {
716 foreach {name value} [dict get $wapp .reply-extra] {
717 puts $chan "$name: $value\r"
720 if {[dict exists $wapp .csp]} {
721 puts $chan "Content-Security-Policy: [dict get $wapp .csp]\r"
723 set mimetype [dict get $wapp .mimetype]
724 puts $chan "Content-Type: $mimetype\r"
725 if {[dict exists $wapp .new-cookies]} {
726 foreach {nm val} [dict get $wapp .new-cookies] {
727 if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
728 if {$val==""} {
729 puts $chan "Set-Cookie: $nm=; HttpOnly; Path=/; Max-Age=1\r"
730 } else {
731 set val [wappInt-enc-url $val]
732 puts $chan "Set-Cookie: $nm=$val; HttpOnly; Path=/\r"
737 if {[string match text/* $mimetype]} {
738 set reply [encoding convertto utf-8 [dict get $wapp .reply]]
739 if {[regexp {\ygzip\y} [wapp-param HTTP_ACCEPT_ENCODING]]} {
740 catch {
741 set x [zlib gzip $reply]
742 set reply $x
743 puts $chan "Content-Encoding: gzip\r"
746 } else {
747 set reply [dict get $wapp .reply]
749 puts $chan "Content-Length: [string length $reply]\r"
750 puts $chan \r
751 puts -nonewline $chan $reply
752 flush $chan
753 wappInt-close-channel $chan
756 # This routine runs just prior to request-handler dispatch. The
757 # default implementation is a no-op, but applications can override
758 # to do additional transformations or checks.
760 proc wapp-before-dispatch-hook {} {return}
762 # Process a single CGI request
764 proc wappInt-handle-cgi-request {} {
765 global wapp env
766 foreach key {
767 CONTENT_LENGTH
768 CONTENT_TYPE
769 DOCUMENT_ROOT
770 HTTP_ACCEPT_ENCODING
771 HTTP_COOKIE
772 HTTP_HOST
773 HTTP_REFERER
774 HTTP_USER_AGENT
775 HTTPS
776 PATH_INFO
777 QUERY_STRING
778 REMOTE_ADDR
779 REQUEST_METHOD
780 REQUEST_URI
781 REMOTE_USER
782 SCRIPT_FILENAME
783 SCRIPT_NAME
784 SERVER_NAME
785 SERVER_PORT
786 SERVER_PROTOCOL
788 if {[info exists env($key)]} {
789 dict set wapp $key $env($key)
792 set len 0
793 if {[dict exists $wapp CONTENT_LENGTH]} {
794 set len [dict get $wapp CONTENT_LENGTH]
796 if {$len>0} {
797 fconfigure stdin -translation binary
798 dict set wapp CONTENT [read stdin $len]
800 dict set wapp WAPP_MODE cgi
801 fconfigure stdout -translation binary
802 wappInt-handle-request stdout 1
805 # Process new text received on an inbound SCGI request
807 proc wappInt-scgi-readable {chan} {
808 if {[catch [list wappInt-scgi-readable-unsafe $chan] msg]} {
809 puts stderr "$msg\n$::errorInfo"
810 wappInt-close-channel $chan
813 proc wappInt-scgi-readable-unsafe {chan} {
814 upvar #0 wappInt-$chan W wapp wapp
815 if {![dict exists $W .toread]} {
816 # If the .toread key is not set, that means we are still reading
817 # the header.
819 # An SGI header is short. This implementation assumes the entire
820 # header is available all at once.
822 dict set W .remove_addr [dict get $W REMOTE_ADDR]
823 set req [read $chan 15]
824 set n [string length $req]
825 scan $req %d:%s len hdr
826 incr len [string length "$len:,"]
827 append hdr [read $chan [expr {$len-15}]]
828 foreach {nm val} [split $hdr \000] {
829 if {$nm==","} break
830 dict set W $nm $val
832 set len 0
833 if {[dict exists $W CONTENT_LENGTH]} {
834 set len [dict get $W CONTENT_LENGTH]
836 if {$len>0} {
837 # Still need to read the query content
838 dict set W .toread $len
839 } else {
840 # There is no query content, so handle the request immediately
841 dict set W SERVER_ADDR [dict get $W .remove_addr]
842 set wapp $W
843 wappInt-handle-request $chan 0
845 } else {
846 # If .toread is set, that means we are reading the query content.
847 # Continue reading until .toread reaches zero.
848 set got [read $chan [dict get $W .toread]]
849 dict append W CONTENT $got
850 dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
851 if {[dict get $W .toread]<=0} {
852 # Handle the request as soon as all the query content is received
853 dict set W SERVER_ADDR [dict get $W .remove_addr]
854 set wapp $W
855 wappInt-handle-request $chan 0
860 # Start up the wapp framework. Parameters are a list passed as the
861 # single argument.
863 # -server $PORT Listen for HTTP requests on this TCP port $PORT
865 # -local $PORT Listen for HTTP requests on 127.0.0.1:$PORT
867 # -scgi $PORT Listen for SCGI requests on 127.0.0.1:$PORT
869 # -remote-scgi $PORT Listen for SCGI requests on TCP port $PORT
871 # -cgi Handle a single CGI request
873 # With no arguments, the behavior is called "auto". In "auto" mode,
874 # if the GATEWAY_INTERFACE environment variable indicates CGI, then run
875 # as CGI. Otherwise, start an HTTP server bound to the loopback address
876 # only, on an arbitrary TCP port, and automatically launch a web browser
877 # on that TCP port.
879 # Additional options:
881 # -fromip GLOB Reject any incoming request where the remote
882 # IP address does not match the GLOB pattern. This
883 # value defaults to '127.0.0.1' for -local and -scgi.
885 # -nowait Do not wait in the event loop. Return immediately
886 # after all event handlers are established.
888 # -trace "puts" each request URL as it is handled, for
889 # debugging
891 # -lint Run wapp-safety-check on the application instead
892 # of running the application itself
894 # -Dvar=value Set TCL global variable "var" to "value"
897 proc wapp-start {arglist} {
898 global env
899 set mode auto
900 set port 0
901 set nowait 0
902 set fromip {}
903 set n [llength $arglist]
904 for {set i 0} {$i<$n} {incr i} {
905 set term [lindex $arglist $i]
906 if {[string match --* $term]} {set term [string range $term 1 end]}
907 switch -glob -- $term {
908 -server {
909 incr i;
910 set mode "server"
911 set port [lindex $arglist $i]
913 -local {
914 incr i;
915 set mode "local"
916 set fromip 127.0.0.1
917 set port [lindex $arglist $i]
919 -scgi {
920 incr i;
921 set mode "scgi"
922 set fromip 127.0.0.1
923 set port [lindex $arglist $i]
925 -remote-scgi {
926 incr i;
927 set mode "remote-scgi"
928 set port [lindex $arglist $i]
930 -cgi {
931 set mode "cgi"
933 -fromip {
934 incr i
935 set fromip [lindex $arglist $i]
937 -nowait {
938 set nowait 1
940 -trace {
941 proc wappInt-trace {} {
942 set q [wapp-param QUERY_STRING]
943 set uri [wapp-param BASE_URL][wapp-param PATH_INFO]
944 if {$q!=""} {append uri ?$q}
945 puts $uri
948 -lint {
949 set res [wapp-safety-check]
950 if {$res!=""} {
951 puts "Potential problems in this code:"
952 puts $res
953 exit 1
954 } else {
955 exit
958 -D*=* {
959 if {[regexp {^.D([^=]+)=(.*)$} $term all var val]} {
960 set ::$var $val
963 default {
964 error "unknown option: $term"
968 if {$mode=="auto"} {
969 if {[info exists env(GATEWAY_INTERFACE)]
970 && [string match CGI/1.* $env(GATEWAY_INTERFACE)]} {
971 set mode cgi
972 } else {
973 set mode local
976 if {$mode=="cgi"} {
977 wappInt-handle-cgi-request
978 } else {
979 wappInt-start-listener $port $mode $fromip
980 if {!$nowait} {
981 vwait ::forever
986 # Call this version 1.0
987 package provide wapp 1.0