Posted to tcl by mjanssen at Sun Jan 24 09:12:40 GMT 2021view raw
- # Copyright (c) 2017 D. Richard Hipp
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the Simplified BSD License (also
- # known as the "2-Clause License" or "FreeBSD License".)
- #
- # This program is distributed in the hope that it will be useful,
- # but without any warranty; without even the implied warranty of
- # merchantability or fitness for a particular purpose.
- #
- #---------------------------------------------------------------------------
- #
- # Design rules:
- #
- # (1) All identifiers in the global namespace begin with "wapp"
- #
- # (2) Indentifiers intended for internal use only begin with "wappInt"
- #
- package require Tcl 8.6
- # Add text to the end of the HTTP reply. No interpretation or transformation
- # of the text is performs. The argument should be enclosed within {...}
- #
- proc wapp {txt} {
- global wapp
- dict append wapp .reply $txt
- }
- # Add text to the page under construction. Do no escaping on the text.
- #
- # Though "unsafe" in general, there are uses for this kind of thing.
- # For example, if you want to return the complete, unmodified content of
- # a file:
- #
- # set fd [open content.html rb]
- # wapp-unsafe [read $fd]
- # close $fd
- #
- # You could do the same thing using ordinary "wapp" instead of "wapp-unsafe".
- # The difference is that wapp-safety-check will complain about the misuse
- # of "wapp", but it assumes that the person who write "wapp-unsafe" understands
- # the risks.
- #
- # Though occasionally necessary, the use of this interface should be minimized.
- #
- proc wapp-unsafe {txt} {
- global wapp
- dict append wapp .reply $txt
- }
- # Add text to the end of the reply under construction. The following
- # substitutions are made:
- #
- # %html(...) Escape text for inclusion in HTML
- # %url(...) Escape text for use as a URL
- # %qp(...) Escape text for use as a URI query parameter
- # %string(...) Escape text for use within a JSON string
- # %unsafe(...) No transformations of the text
- #
- # The substitutions above terminate at the first ")" character. If the
- # text of the TCL string in ... contains ")" characters itself, use instead:
- #
- # %html%(...)%
- # %url%(...)%
- # %qp%(...)%
- # %string%(...)%
- # %unsafe%(...)%
- #
- # In other words, use "%(...)%" instead of "(...)" to include the TCL string
- # to substitute.
- #
- # The %unsafe substitution should be avoided whenever possible, obviously.
- # In addition to the substitutions above, the text also does backslash
- # escapes.
- #
- # The wapp-trim proc works the same as wapp-subst except that it also removes
- # whitespace from the left margin, so that the generated HTML/CSS/Javascript
- # does not appear to be indented when delivered to the client web browser.
- #
- if {$tcl_version>=8.7} {
- proc wapp-subst {txt} {
- global wapp
- regsub -all -command \
- {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
- dict append wapp .reply [subst -novariables -nocommand $txt]
- }
- proc wapp-trim {txt} {
- global wapp
- regsub -all {\n\s+} [string trim $txt] \n txt
- regsub -all -command \
- {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
- dict append wapp .reply [subst -novariables -nocommand $txt]
- }
- proc wappInt-enc {all mode nu1 txt} {
- return [uplevel 2 "wappInt-enc-$mode \"$txt\""]
- }
- } else {
- proc wapp-subst {txt} {
- global wapp
- regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
- {[wappInt-enc-\1 "\3"]} txt
- dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
- }
- proc wapp-trim {txt} {
- global wapp
- regsub -all {\n\s+} [string trim $txt] \n txt
- regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
- {[wappInt-enc-\1 "\3"]} txt
- dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
- }
- }
- # There must be a wappInt-enc-NAME routine for each possible substitution
- # in wapp-subst. Thus there are routines for "html", "url", "qp", and "unsafe".
- #
- # wappInt-enc-html Escape text so that it is safe to use in the
- # body of an HTML document.
- #
- # wappInt-enc-url Escape text so that it is safe to pass as an
- # argument to href= and src= attributes in HTML.
- #
- # wappInt-enc-qp Escape text so that it is safe to use as the
- # value of a query parameter in a URL or in
- # post data or in a cookie.
- #
- # wappInt-enc-string Escape ", ', \, and < for using inside of a
- # javascript string literal. The < character
- # is escaped to prevent "</script>" from causing
- # problems in embedded javascript.
- #
- # wappInt-enc-unsafe Perform no encoding at all. Unsafe.
- #
- proc wappInt-enc-html {txt} {
- return [string map {& & < < > > \" " \\ \} $txt]
- }
- proc wappInt-enc-unsafe {txt} {
- return $txt
- }
- proc wappInt-enc-url {s} {
- if {[regsub -all {[^-{}@~?=#_.:/a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
- set s [subst -novar -noback $s]
- }
- if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
- set s [subst -novar -noback $s]
- }
- return $s
- }
- proc wappInt-enc-qp {s} {
- if {[regsub -all {[^-{}_.a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
- set s [subst -novar -noback $s]
- }
- if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
- set s [subst -novar -noback $s]
- }
- return $s
- }
- proc wappInt-enc-string {s} {
- return [string map {\\ \\\\ \" \\\" ' \\' < \\u003c \n \\n \r \\r
- \f \\f \t \\t \x01 \\u0001 \x02 \\u0002 \x03 \\u0003
- \x04 \\u0004 \x05 \\u0005 \x06 \\u0006 \x07 \\u0007
- \x0b \\u000b \x0e \\u000e \x0f \\u000f \x10 \\u0010
- \x11 \\u0011 \x12 \\u0012 \x13 \\u0013 \x14 \\u0014
- \x15 \\u0015 \x16 \\u0016 \x17 \\u0017 \x18 \\u0018
- \x19 \\u0019 \x1a \\u001a \x1b \\u001b \x1c \\u001c
- \x1d \\u001d \x1e \\u001e \x1f \\u001f} $s]
- }
- # This is a helper routine for wappInt-enc-url and wappInt-enc-qp. It returns
- # an appropriate %HH encoding for the single character c. If c is a unicode
- # character, then this routine might return multiple bytes: %HH%HH%HH
- #
- proc wappInt-%HHchar {c} {
- if {$c==" "} {return +}
- return [regsub -all .. [binary encode hex [encoding convertto utf-8 $c]] {%&}]
- }
- # Undo the www-url-encoded format.
- #
- # HT: This code stolen from ncgi.tcl
- #
- proc wappInt-decode-url {str} {
- set str [string map [list + { } "\\" "\\\\" \[ \\\[ \] \\\]] $str]
- regsub -all -- \
- {%([Ee][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
- $str {[encoding convertfrom utf-8 [binary decode hex \1\2\3]]} str
- regsub -all -- \
- {%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
- $str {[encoding convertfrom utf-8 [binary decode hex \1\2]]} str
- regsub -all -- {%([0-7][A-Fa-f0-9])} $str {\\u00\1} str
- return [subst -novar $str]
- }
- # Reset the document back to an empty string.
- #
- proc wapp-reset {} {
- global wapp
- dict set wapp .reply {}
- }
- # Change the mime-type of the result document.
- #
- proc wapp-mimetype {x} {
- global wapp
- dict set wapp .mimetype $x
- }
- # Change the reply code.
- #
- proc wapp-reply-code {x} {
- global wapp
- dict set wapp .reply-code $x
- }
- # Set a cookie
- #
- proc wapp-set-cookie {name value} {
- global wapp
- dict lappend wapp .new-cookies $name $value
- }
- # Unset a cookie
- #
- proc wapp-clear-cookie {name} {
- wapp-set-cookie $name {}
- }
- # Add extra entries to the reply header
- #
- proc wapp-reply-extra {name value} {
- global wapp
- dict lappend wapp .reply-extra $name $value
- }
- # Specifies how the web-page under construction should be cached.
- # The argument should be one of:
- #
- # no-cache
- # max-age=N (for some integer number of seconds, N)
- # private,max-age=N
- #
- proc wapp-cache-control {x} {
- wapp-reply-extra Cache-Control $x
- }
- # Redirect to a different web page
- #
- proc wapp-redirect {uri} {
- wapp-reply-code {307 Redirect}
- wapp-reply-extra Location $uri
- }
- # Return the value of a wapp parameter
- #
- proc wapp-param {name {dflt {}}} {
- global wapp
- if {![dict exists $wapp $name]} {return $dflt}
- return [dict get $wapp $name]
- }
- # Return true if a and only if the wapp parameter $name exists
- #
- proc wapp-param-exists {name} {
- global wapp
- return [dict exists $wapp $name]
- }
- # Set the value of a wapp parameter
- #
- proc wapp-set-param {name value} {
- global wapp
- dict set wapp $name $value
- }
- # Return all parameter names that match the GLOB pattern, or all
- # names if the GLOB pattern is omitted.
- #
- proc wapp-param-list {{glob {*}}} {
- global wapp
- return [dict keys $wapp $glob]
- }
- # By default, Wapp does not decode query parameters and POST parameters
- # for cross-origin requests. This is a security restriction, designed to
- # help prevent cross-site request forgery (CSRF) attacks.
- #
- # As a consequence of this restriction, URLs for sites generated by Wapp
- # that contain query parameters will not work as URLs found in other
- # websites. You cannot create a link from a second website into a Wapp
- # website if the link contains query planner, by default.
- #
- # Of course, it is sometimes desirable to allow query parameters on external
- # links. For URLs for which this is safe, the application should invoke
- # wapp-allow-xorigin-params. This procedure tells Wapp that it is safe to
- # go ahead and decode the query parameters even for cross-site requests.
- #
- # In other words, for Wapp security is the default setting. Individual pages
- # need to actively disable the cross-site request security if those pages
- # are safe for cross-site access.
- #
- proc wapp-allow-xorigin-params {} {
- global wapp
- if {![dict exists $wapp .qp] && ![dict get $wapp SAME_ORIGIN]} {
- wappInt-decode-query-params
- }
- }
- # Set the content-security-policy.
- #
- # The default content-security-policy is very strict: "default-src 'self'"
- # The default policy prohibits the use of in-line javascript or CSS.
- #
- # Provide an alternative CSP as the argument. Or use "off" to disable
- # the CSP completely.
- #
- proc wapp-content-security-policy {val} {
- global wapp
- if {$val=="off"} {
- dict unset wapp .csp
- } else {
- dict set wapp .csp $val
- }
- }
- # Examine the bodys of all procedures in this program looking for
- # unsafe calls to various Wapp interfaces. Return a text string
- # containing warnings. Return an empty string if all is ok.
- #
- # This routine is advisory only. It misses some constructs that are
- # dangerous and flags others that are safe.
- #
- proc wapp-safety-check {} {
- set res {}
- foreach p [info command] {
- set ln 0
- foreach x [split [info body $p] \n] {
- incr ln
- if {[regexp {^[ \t]*wapp[ \t]+([^\n]+)} $x all tail]
- && [string index $tail 0]!="\173"
- && [regexp {[[$]} $tail]
- } {
- append res "$p:$ln: unsafe \"wapp\" call: \"[string trim $x]\"\n"
- }
- if {[regexp {^[ \t]*wapp-(subst|trim)[ \t]+[^\173]} $x all cx]} {
- append res "$p:$ln: unsafe \"wapp-$cx\" call: \"[string trim $x]\"\n"
- }
- }
- }
- return $res
- }
- # Return a string that descripts the current environment. Applications
- # might find this useful for debugging.
- #
- proc wapp-debug-env {} {
- global wapp
- set out {}
- foreach var [lsort [dict keys $wapp]] {
- if {[string index $var 0]=="."} continue
- append out "$var = [list [dict get $wapp $var]]\n"
- }
- append out "\[pwd\] = [list [pwd]]\n"
- return $out
- }
- # Tracing function for each HTTP request. This is overridden by wapp-start
- # if tracing is enabled.
- #
- proc wappInt-trace {} {}
- # Start up a listening socket. Arrange to invoke wappInt-new-connection
- # for each inbound HTTP connection.
- #
- # port Listen on this TCP port. 0 means to select a port
- # that is not currently in use
- #
- # wappmode One of "scgi", "remote-scgi", "server", or "local".
- #
- # fromip If not {}, then reject all requests from IP addresses
- # other than $fromip
- #
- proc wappInt-start-listener {port wappmode fromip} {
- if {[string match *scgi $wappmode]} {
- set type SCGI
- set server [list wappInt-new-connection \
- wappInt-scgi-readable $wappmode $fromip]
- } else {
- set type HTTP
- set server [list wappInt-new-connection \
- wappInt-http-readable $wappmode $fromip]
- }
- if {$wappmode=="local" || $wappmode=="scgi"} {
- set x [socket -server $server -myaddr 127.0.0.1 $port]
- } else {
- set x [socket -server $server $port]
- }
- set coninfo [chan configure $x -sockname]
- set port [lindex $coninfo 2]
- if {$wappmode=="local"} {
- wappInt-start-browser http://127.0.0.1:$port/
- } elseif {$fromip!=""} {
- puts "Listening for $type requests on TCP port $port from IP $fromip"
- } else {
- puts "Listening for $type requests on TCP port $port"
- }
- }
- # Start a web-browser and point it at $URL
- #
- proc wappInt-start-browser {url} {
- global tcl_platform
- if {$tcl_platform(platform)=="windows"} {
- exec cmd /c start $url &
- } elseif {$tcl_platform(os)=="Darwin"} {
- exec open $url &
- } elseif {[catch {exec xdg-open $url}]} {
- exec firefox $url &
- }
- }
- # This routine is a "socket -server" callback. The $chan, $ip, and $port
- # arguments are added by the socket command.
- #
- # Arrange to invoke $callback when content is available on the new socket.
- # The $callback will process inbound HTTP or SCGI content. Reject the
- # request if $fromip is not an empty string and does not match $ip.
- #
- proc wappInt-new-connection {callback wappmode fromip chan ip port} {
- if {$fromip!="" && ![string match $fromip $ip]} {
- close $chan
- return
- }
- set W [dict create REMOTE_ADDR $ip REMOTE_PORT $port WAPP_MODE $wappmode \
- .header {}]
- fconfigure $chan -blocking 0 -translation binary
- fileevent $chan readable [list $callback $W $chan]
- }
- # Close an input channel
- #
- proc wappInt-close-channel {chan} {
- if {$chan=="stdout"} {
- # This happens after completing a CGI request
- exit 0
- } else {
- close $chan
- }
- }
- # Process new text received on an inbound HTTP request
- #
- proc wappInt-http-readable {W chan} {
- if {[catch [list wappInt-http-readable-unsafe $W $chan] msg]} {
- puts stderr "$msg\n$::errorInfo"
- wappInt-close-channel $chan
- }
- }
- proc wappInt-http-readable-unsafe {W chan} {
- upvar #0 wapp wapp
- if {![dict exists $W .toread]} {
- # If the .toread key is not set, that means we are still reading
- # the header
- set line [string trimright [gets $chan]]
- set n [string length $line]
- if {$n>0} {
- if {[dict get $W .header]=="" || [regexp {^\s+} $line]} {
- dict append W .header $line
- } else {
- dict append W .header \n$line
- }
- if {[string length [dict get $W .header]]>100000} {
- error "HTTP request header too big - possible DOS attack"
- }
- } elseif {$n==0} {
- # We have reached the blank line that terminates the header.
- global argv0
- if {[info exists ::argv0]} {
- set a0 [file normalize $argv0]
- } else {
- set a0 /
- }
- dict set W SCRIPT_FILENAME $a0
- dict set W DOCUMENT_ROOT [file dir $a0]
- set W [wappInt-parse-header $chan]
- set len 0
- if {[dict exists $W CONTENT_LENGTH]} {
- set len [dict get $W CONTENT_LENGTH]
- }
- if {$len>0} {
- # Still need to read the query content
- dict set W .toread $len
- } else {
- # There is no query content, so handle the request immediately
- set wapp $W
- wappInt-handle-request $chan
- return
- }
- }
- } else {
- # If .toread is set, that means we are reading the query content.
- # Continue reading until .toread reaches zero.
- set got [read $chan [dict get $W .toread]]
- dict append W CONTENT $got
- dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
- if {[dict get $W .toread]<=0} {
- # Handle the request as soon as all the query content is received
- set wapp $W
- wappInt-handle-request $chan
- return
- }
- }
- fileevent $chan readable [list wappInt-http-readable $W $chan]
- }
- # Decode the HTTP request header.
- #
- # This routine is always running inside of a [catch], so if
- # any problems arise, simply raise an error.
- #
- proc wappInt-parse-header {W} {
- set hdr [split [dict get $W .header] \n]
- if {$hdr==""} {return 1}
- set req [lindex $hdr 0]
- dict set W REQUEST_METHOD [set method [lindex $req 0]]
- if {[lsearch {GET HEAD POST} $method]<0} {
- error "unsupported request method: \"[dict get $W REQUEST_METHOD]\""
- }
- set uri [lindex $req 1]
- set split_uri [split $uri ?]
- set uri0 [lindex $split_uri 0]
- if {![regexp {^/[-.a-z0-9_/]*$} $uri0]} {
- error "invalid request uri: \"$uri0\""
- }
- dict set W REQUEST_URI $uri0
- dict set W PATH_INFO $uri0
- set uri1 [lindex $split_uri 1]
- dict set W QUERY_STRING $uri1
- set n [llength $hdr]
- for {set i 1} {$i<$n} {incr i} {
- set x [lindex $hdr $i]
- if {![regexp {^(.+): +(.*)$} $x all name value]} {
- error "invalid header line: \"$x\""
- }
- set name [string toupper $name]
- switch -- $name {
- REFERER {set name HTTP_REFERER}
- USER-AGENT {set name HTTP_USER_AGENT}
- CONTENT-LENGTH {set name CONTENT_LENGTH}
- CONTENT-TYPE {set name CONTENT_TYPE}
- HOST {set name HTTP_HOST}
- COOKIE {set name HTTP_COOKIE}
- ACCEPT-ENCODING {set name HTTP_ACCEPT_ENCODING}
- default {set name .hdr:$name}
- }
- dict set W $name $value
- }
- return $W
- }
- # Decode the QUERY_STRING parameters from a GET request or the
- # application/x-www-form-urlencoded CONTENT from a POST request.
- #
- # This routine sets the ".qp" element of the ::wapp dict as a signal
- # that query parameters have already been decoded.
- #
- proc wappInt-decode-query-params {} {
- global wapp
- dict set wapp .qp 1
- if {[dict exists $wapp QUERY_STRING]} {
- foreach qterm [split [dict get $wapp QUERY_STRING] &] {
- set qsplit [split $qterm =]
- set nm [lindex $qsplit 0]
- if {[regexp {^[a-z][a-z0-9]*$} $nm]} {
- dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
- }
- }
- }
- if {[dict exists $wapp CONTENT_TYPE] && [dict exists $wapp CONTENT]} {
- set ctype [dict get $wapp CONTENT_TYPE]
- if {$ctype=="application/x-www-form-urlencoded"} {
- foreach qterm [split [string trim [dict get $wapp CONTENT]] &] {
- set qsplit [split $qterm =]
- set nm [lindex $qsplit 0]
- if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
- dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
- }
- }
- } elseif {[string match multipart/form-data* $ctype]} {
- regexp {^(.*?)\r\n(.*)$} [dict get $wapp CONTENT] all divider body
- set ndiv [string length $divider]
- while {[string length $body]} {
- set idx [string first $divider $body]
- set unit [string range $body 0 [expr {$idx-3}]]
- set body [string range $body [expr {$idx+$ndiv+2}] end]
- if {[regexp {^Content-Disposition: form-data; (.*?)\r\n\r\n(.*)$} \
- $unit unit hdr content]} {
- if {[regexp {name="(.*)"; filename="(.*)"\r\nContent-Type: (.*?)$}\
- $hdr hr name filename mimetype]} {
- dict set wapp $name.filename \
- [string map [list \\\" \" \\\\ \\] $filename]
- dict set wapp $name.mimetype $mimetype
- dict set wapp $name.content $content
- } elseif {[regexp {name="(.*)"} $hdr hr name]} {
- dict set wapp $name $content
- }
- }
- }
- }
- }
- }
- # Invoke application-supplied methods to generate a reply to
- # a single HTTP request.
- #
- # This routine uses the global variable ::wapp and so must not be nested.
- # It must run to completion before the next instance runs. If a recursive
- # instances of this routine starts while another is running, the the
- # recursive instance is added to a queue to be invoked after the current
- # instance finishes. Yes, this means that WAPP IS SINGLE THREADED. Only
- # a single page rendering instance my be running at a time. There can
- # be multiple HTTP requests inbound at once, but only one my be processed
- # at a time once the request is full read and parsed.
- #
- set wappIntPending {}
- set wappIntLock 0
- proc wappInt-handle-request {chan} {
- global wappIntPending wappIntLock
- fileevent $chan readable {}
- if {$wappIntLock} {
- # Another instance of request is already running, so defer this one
- lappend wappIntPending [list wappInt-handle-request $chan]
- return
- }
- set wappIntLock 1
- catch [list wappInt-handle-request-unsafe $chan]
- set wappIntLock 0
- if {[llength $wappIntPending]>0} {
- # If there are deferred requests, then launch the oldest one
- after idle [lindex $wappIntPending 0]
- set wappIntPending [lrange $wappIntPending 1 end]
- }
- }
- proc wappInt-handle-request-unsafe {chan} {
- global wapp
- dict set wapp .reply {}
- dict set wapp .mimetype {text/html; charset=utf-8}
- dict set wapp .reply-code {200 Ok}
- dict set wapp .csp {default-src 'self'}
- # Set up additional CGI environment values
- #
- if {![dict exists $wapp HTTP_HOST]} {
- dict set wapp BASE_URL {}
- } elseif {[dict exists $wapp HTTPS]} {
- dict set wapp BASE_URL https://[dict get $wapp HTTP_HOST]
- } else {
- dict set wapp BASE_URL http://[dict get $wapp HTTP_HOST]
- }
- if {![dict exists $wapp REQUEST_URI]} {
- dict set wapp REQUEST_URI /
- } elseif {[regsub {\?.*} [dict get $wapp REQUEST_URI] {} newR]} {
- # Some servers (ex: nginx) append the query parameters to REQUEST_URI.
- # These need to be stripped off
- dict set wapp REQUEST_URI $newR
- }
- if {[dict exists $wapp SCRIPT_NAME]} {
- dict append wapp BASE_URL [dict get $wapp SCRIPT_NAME]
- } else {
- dict set wapp SCRIPT_NAME {}
- }
- if {![dict exists $wapp PATH_INFO]} {
- # If PATH_INFO is missing (ex: nginx) then construct it
- set URI [dict get $wapp REQUEST_URI]
- set skip [string length [dict get $wapp SCRIPT_NAME]]
- dict set wapp PATH_INFO [string range $URI $skip end]
- }
- if {[regexp {^/([^/]+)(.*)$} [dict get $wapp PATH_INFO] all head tail]} {
- dict set wapp PATH_HEAD $head
- dict set wapp PATH_TAIL [string trimleft $tail /]
- } else {
- dict set wapp PATH_INFO {}
- dict set wapp PATH_HEAD {}
- dict set wapp PATH_TAIL {}
- }
- dict set wapp SELF_URL [dict get $wapp BASE_URL]/[dict get $wapp PATH_HEAD]
- # Parse query parameters from the query string, the cookies, and
- # POST data
- #
- if {[dict exists $wapp HTTP_COOKIE]} {
- foreach qterm [split [dict get $wapp HTTP_COOKIE] {;}] {
- set qsplit [split [string trim $qterm] =]
- set nm [lindex $qsplit 0]
- if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
- dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
- }
- }
- }
- set same_origin 0
- if {[dict exists $wapp HTTP_REFERER]} {
- set referer [dict get $wapp HTTP_REFERER]
- set base [dict get $wapp BASE_URL]
- if {$referer==$base || [string match $base/* $referer]} {
- set same_origin 1
- }
- }
- dict set wapp SAME_ORIGIN $same_origin
- if {$same_origin} {
- wappInt-decode-query-params
- }
- # Invoke the application-defined handler procedure for this page
- # request. If an error occurs while running that procedure, generate
- # an HTTP reply that contains the error message.
- #
- wapp-before-dispatch-hook
- wappInt-trace
- set mname [dict get $wapp PATH_HEAD]
- if {[catch {
- if {$mname!="" && [llength [info command wapp-page-$mname]]>0} {
- wapp-page-$mname
- } else {
- wapp-default
- }
- } msg]} {
- if {[wapp-param WAPP_MODE]=="local" || [wapp-param WAPP_MODE]=="server"} {
- puts "ERROR: $::errorInfo"
- }
- wapp-reset
- wapp-reply-code "500 Internal Server Error"
- wapp-mimetype text/html
- wapp-trim {
- <h1>Wapp Application Error</h1>
- <pre>%html($::errorInfo)</pre>
- }
- dict unset wapp .new-cookies
- }
- wapp-before-reply-hook
- # Transmit the HTTP reply
- #
- if {$chan=="stdout"} {
- puts $chan "Status: [dict get $wapp .reply-code]\r"
- } else {
- puts $chan "HTTP/1.1 [dict get $wapp .reply-code]\r"
- puts $chan "Server: wapp\r"
- puts $chan "Connection: close\r"
- }
- if {[dict exists $wapp .reply-extra]} {
- foreach {name value} [dict get $wapp .reply-extra] {
- puts $chan "$name: $value\r"
- }
- }
- if {[dict exists $wapp .csp]} {
- puts $chan "Content-Security-Policy: [dict get $wapp .csp]\r"
- }
- set mimetype [dict get $wapp .mimetype]
- puts $chan "Content-Type: $mimetype\r"
- if {[dict exists $wapp .new-cookies]} {
- foreach {nm val} [dict get $wapp .new-cookies] {
- if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
- if {$val==""} {
- puts $chan "Set-Cookie: $nm=; HttpOnly; Path=/; Max-Age=1\r"
- } else {
- set val [wappInt-enc-url $val]
- puts $chan "Set-Cookie: $nm=$val; HttpOnly; Path=/\r"
- }
- }
- }
- }
- if {[string match text/* $mimetype]} {
- set reply [encoding convertto utf-8 [dict get $wapp .reply]]
- if {[regexp {\ygzip\y} [wapp-param HTTP_ACCEPT_ENCODING]]} {
- catch {
- set x [zlib gzip $reply]
- set reply $x
- puts $chan "Content-Encoding: gzip\r"
- }
- }
- } else {
- set reply [dict get $wapp .reply]
- }
- puts $chan "Content-Length: [string length $reply]\r"
- puts $chan \r
- puts -nonewline $chan $reply
- flush $chan
- wappInt-close-channel $chan
- }
- # This routine runs just prior to request-handler dispatch. The
- # default implementation is a no-op, but applications can override
- # to do additional transformations or checks.
- #
- proc wapp-before-dispatch-hook {} {return}
- # This routine runs after the request-handler dispatch and just
- # before the reply is generated. The default implementation is
- # a no-op, but applications can override to do validation and security
- # checks on the reply, such as verifying that no sensitive information
- # such as an API key or password is accidentally included in the
- # reply text.
- #
- proc wapp-before-reply-hook {} {return}
- # Process a single CGI request
- #
- proc wappInt-handle-cgi-request {} {
- global wapp env
- foreach key [array names env {[A-Z]*}] {dict set wapp $key $env($key)}
- set len 0
- if {[dict exists $wapp CONTENT_LENGTH]} {
- set len [dict get $wapp CONTENT_LENGTH]
- }
- if {$len>0} {
- fconfigure stdin -translation binary
- dict set wapp CONTENT [read stdin $len]
- }
- dict set wapp WAPP_MODE cgi
- fconfigure stdout -translation binary
- wappInt-handle-request-unsafe stdout
- }
- # Process new text received on an inbound SCGI request
- #
- proc wappInt-scgi-readable {W chan} {
- if {[catch [list wappInt-scgi-readable-unsafe $W $chan] msg]} {
- puts stderr "$msg\n$::errorInfo"
- wappInt-close-channel $chan
- }
- }
- proc wappInt-scgi-readable-unsafe {W chan} {
- if {![dict exists $W .toread]} {
- # If the .toread key is not set, that means we are still reading
- # the header.
- #
- # An SGI header is short. This implementation assumes the entire
- # header is available all at once.
- #
- dict set W .remove_addr [dict get $W REMOTE_ADDR]
- set req [read $chan 15]
- set n [string length $req]
- scan $req %d:%s len hdr
- incr len [string length "$len:,"]
- append hdr [read $chan [expr {$len-15}]]
- foreach {nm val} [split $hdr \000] {
- if {$nm==","} break
- dict set W $nm $val
- }
- set len 0
- if {[dict exists $W CONTENT_LENGTH]} {
- set len [dict get $W CONTENT_LENGTH]
- }
- if {$len>0} {
- # Still need to read the query content
- dict set W .toread $len
- } else {
- # There is no query content, so handle the request immediately
- dict set W SERVER_ADDR [dict get $W .remove_addr]
- set wapp $W
- wappInt-handle-request $chan
- return
- }
- } else {
- # If .toread is set, that means we are reading the query content.
- # Continue reading until .toread reaches zero.
- set got [read $chan [dict get $W .toread]]
- dict append W CONTENT $got
- dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
- if {[dict get $W .toread]<=0} {
- # Handle the request as soon as all the query content is received
- dict set W SERVER_ADDR [dict get $W .remove_addr]
- set wapp $W
- wappInt-handle-request $chan
- return
- }
- }
- fileevent $chan readable [list wappInt-scgi-readable $W $chan]
- }
- # Start up the wapp framework. Parameters are a list passed as the
- # single argument.
- #
- # -server $PORT Listen for HTTP requests on this TCP port $PORT
- #
- # -local $PORT Listen for HTTP requests on 127.0.0.1:$PORT
- #
- # -scgi $PORT Listen for SCGI requests on 127.0.0.1:$PORT
- #
- # -remote-scgi $PORT Listen for SCGI requests on TCP port $PORT
- #
- # -cgi Handle a single CGI request
- #
- # With no arguments, the behavior is called "auto". In "auto" mode,
- # if the GATEWAY_INTERFACE environment variable indicates CGI, then run
- # as CGI. Otherwise, start an HTTP server bound to the loopback address
- # only, on an arbitrary TCP port, and automatically launch a web browser
- # on that TCP port.
- #
- # Additional options:
- #
- # -fromip GLOB Reject any incoming request where the remote
- # IP address does not match the GLOB pattern. This
- # value defaults to '127.0.0.1' for -local and -scgi.
- #
- # -nowait Do not wait in the event loop. Return immediately
- # after all event handlers are established.
- #
- # -trace "puts" each request URL as it is handled, for
- # debugging
- #
- # -lint Run wapp-safety-check on the application instead
- # of running the application itself
- #
- # -Dvar=value Set TCL global variable "var" to "value"
- #
- #
- proc wapp-start {arglist} {
- global env
- set mode auto
- set port 0
- set nowait 0
- set fromip {}
- set n [llength $arglist]
- for {set i 0} {$i<$n} {incr i} {
- set term [lindex $arglist $i]
- if {[string match --* $term]} {set term [string range $term 1 end]}
- switch -glob -- $term {
- -server {
- incr i;
- set mode "server"
- set port [lindex $arglist $i]
- }
- -local {
- incr i;
- set mode "local"
- set fromip 127.0.0.1
- set port [lindex $arglist $i]
- }
- -scgi {
- incr i;
- set mode "scgi"
- set fromip 127.0.0.1
- set port [lindex $arglist $i]
- }
- -remote-scgi {
- incr i;
- set mode "remote-scgi"
- set port [lindex $arglist $i]
- }
- -cgi {
- set mode "cgi"
- }
- -fromip {
- incr i
- set fromip [lindex $arglist $i]
- }
- -nowait {
- set nowait 1
- }
- -trace {
- proc wappInt-trace {} {
- set q [wapp-param QUERY_STRING]
- set uri [wapp-param BASE_URL][wapp-param PATH_INFO]
- if {$q!=""} {append uri ?$q}
- puts $uri
- }
- }
- -lint {
- set res [wapp-safety-check]
- if {$res!=""} {
- puts "Potential problems in this code:"
- puts $res
- exit 1
- } else {
- exit
- }
- }
- -D*=* {
- if {[regexp {^.D([^=]+)=(.*)$} $term all var val]} {
- set ::$var $val
- }
- }
- default {
- error "unknown option: $term"
- }
- }
- }
- if {$mode=="auto"} {
- if {[info exists env(GATEWAY_INTERFACE)]
- && [string match CGI/1.* $env(GATEWAY_INTERFACE)]} {
- set mode cgi
- } else {
- set mode local
- }
- }
- if {$mode=="cgi"} {
- wappInt-handle-cgi-request
- } else {
- wappInt-start-listener $port $mode $fromip
- if {!$nowait} {
- vwait ::forever
- }
- }
- }
- # Call this version 1.0
- package provide wapp 1.0