1 # Copyright (c) 2017 D. Richard Hipp
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 #---------------------------------------------------------------------------
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 {...}
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
35 # set fd [open content.html rb]
36 # wapp-unsafe [read $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
44 # Though occasionally necessary, the use of this interface should be minimized.
46 proc wapp-unsafe
{txt
} {
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:
69 # In other words, use "%(...)%" instead of "(...)" to include the TCL string
72 # The %unsafe substitution should be avoided whenever possible, obviously.
73 # In addition to the substitutions above, the text also does backslash
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
} {
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
} {
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\""]
98 proc wapp-subst
{txt
} {
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
} {
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
{& &
; < <
; > >
; \" "
; \\ \} $txt]
136 proc wappInt-enc-unsafe
{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]
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]
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]
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
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.
191 dict
set wapp .reply
{}
194 # Change the mime-type of the result document.
196 proc wapp-mimetype
{x
} {
198 dict
set wapp .mimetype
$x
201 # Change the reply code.
203 proc wapp-reply-code
{x
} {
205 dict
set wapp .reply-code
$x
210 proc wapp-set-cookie
{name value
} {
212 dict
lappend wapp .new-cookies
$name $value
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
} {
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:
232 # max-age=N (for some integer number of seconds, 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
{}}} {
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
} {
258 return [dict exists
$wapp $name]
261 # Set the value of a wapp parameter
263 proc wapp-set-param
{name value
} {
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 {*}}} {
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
{} {
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
} {
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
{} {
327 foreach p
[info procs
] {
329 foreach x
[split [info body
$p] \n] {
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"
345 # Return a string that descripts the current environment. Applications
346 # might find this useful for debugging.
348 proc wapp-debug-env
{} {
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"
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
375 proc wappInt-start-listener
{port wappmode fromip
} {
376 if {[string match
*scgi
$wappmode]} {
378 set server
[list wappInt-new-connection
\
379 wappInt-scgi-readable
$wappmode $fromip]
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]
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"
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
} {
405 if {$tcl_platform(platform
)=="windows"} {
406 exec cmd
/c start
$url &
407 } elseif
{$tcl_platform(os
)=="Darwin"} {
409 } elseif
{[catch {exec xdg-open
$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]} {
427 set W
[dict create REMOTE_ADDR
$ip REMOTE_PORT
$port WAPP_MODE
$wappmode \
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
440 unset ::wappInt-$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
458 set line
[string trimright
[gets $chan]]
459 set n
[string length
$line]
461 if {[dict get
$W .header
]=="" ||
[regexp {^
\s
+} $line]} {
462 dict
append W .header
$line
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"
470 # We have reached the blank line that terminates the header.
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]} {
480 if {[dict exists
$W CONTENT_LENGTH
]} {
481 set len
[dict get
$W CONTENT_LENGTH
]
484 # Still need to read the query content
485 dict
set W .toread
$len
487 # There is no query content, so handle the request immediately
489 wappInt-handle-request
$chan 0
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
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
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]
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
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
{} {
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-z
0-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
610 proc wappInt-handle-request
{chan useCgi
} {
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
]
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
]
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 /]
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
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-z
0-9_
]*$} $nm]} {
662 dict
set wapp
$nm [wappInt-decode-url
[lindex $qsplit 1]]
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]} {
674 dict
set wapp SAME_ORIGIN
$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
685 set mname
[dict get
$wapp PATH_HEAD
]
687 if {$mname!="" && [llength [info proc wapp-page-
$mname]]>0} {
693 if {[wapp-param WAPP_MODE
]=="local" ||
[wapp-param WAPP_MODE
]=="server"} {
694 puts "ERROR: $::errorInfo"
697 wapp-reply-code
"500 Internal Server Error"
698 wapp-mimetype
text/html
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"
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-z
0-9_
]*$} $nm]} {
729 puts $chan "Set-Cookie: $nm=; HttpOnly; Path=/; Max-Age=1\r"
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
]]} {
741 set x
[zlib gzip
$reply]
743 puts $chan "Content-Encoding: gzip\r"
747 set reply
[dict get
$wapp .reply
]
749 puts $chan "Content-Length: [string length $reply]\r"
751 puts -nonewline $chan $reply
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
{} {
788 if {[info exists env
($key)]} {
789 dict
set wapp
$key $env($key)
793 if {[dict exists
$wapp CONTENT_LENGTH
]} {
794 set len
[dict get
$wapp CONTENT_LENGTH
]
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
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] {
833 if {[dict exists
$W CONTENT_LENGTH
]} {
834 set len
[dict get
$W CONTENT_LENGTH
]
837 # Still need to read the query content
838 dict
set W .toread
$len
840 # There is no query content, so handle the request immediately
841 dict
set W SERVER_ADDR
[dict get
$W .remove_addr
]
843 wappInt-handle-request
$chan 0
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
]
855 wappInt-handle-request
$chan 0
860 # Start up the wapp framework. Parameters are a list passed as the
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
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
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
} {
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 {
911 set port
[lindex $arglist $i]
917 set port
[lindex $arglist $i]
923 set port
[lindex $arglist $i]
927 set mode
"remote-scgi"
928 set port
[lindex $arglist $i]
935 set fromip
[lindex $arglist $i]
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}
949 set res
[wapp-safety-check
]
951 puts "Potential problems in this code:"
959 if {[regexp {^.D
([^
=]+)=(.
*)$} $term all var val
]} {
964 error "unknown option: $term"
969 if {[info exists env
(GATEWAY_INTERFACE
)]
970 && [string match CGI
/1.
* $env(GATEWAY_INTERFACE
)]} {
977 wappInt-handle-cgi-request
979 wappInt-start-listener
$port $mode $fromip
986 # Call this version 1.0
987 package provide wapp
1.0