xref: /sqlite-3.40.0/test/wapp.tcl (revision a3020dcb)
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".)
6#
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.
10#
11#---------------------------------------------------------------------------
12#
13# Design rules:
14#
15#   (1)  All identifiers in the global namespace begin with "wapp"
16#
17#   (2)  Indentifiers intended for internal use only begin with "wappInt"
18#
19package require Tcl 8.6
20
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 {...}
23#
24proc wapp {txt} {
25  global wapp
26  dict append wapp .reply $txt
27}
28
29# Add text to the page under construction.  Do no escaping on the text.
30#
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:
34#
35#         set fd [open content.html rb]
36#         wapp-unsafe [read $fd]
37#         close $fd
38#
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.
43#
44# Though occasionally necessary, the use of this interface should be minimized.
45#
46proc wapp-unsafe {txt} {
47  global wapp
48  dict append wapp .reply $txt
49}
50
51# Add text to the end of the reply under construction.  The following
52# substitutions are made:
53#
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
59#
60# The substitutions above terminate at the first ")" character.  If the
61# text of the TCL string in ... contains ")" characters itself, use instead:
62#
63#     %html%(...)%
64#     %url%(...)%
65#     %qp%(...)%
66#     %string%(...)%
67#     %unsafe%(...)%
68#
69# In other words, use "%(...)%" instead of "(...)" to include the TCL string
70# to substitute.
71#
72# The %unsafe substitution should be avoided whenever possible, obviously.
73# In addition to the substitutions above, the text also does backslash
74# escapes.
75#
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.
79#
80if {$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]
86  }
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]
93  }
94  proc wappInt-enc {all mode nu1 txt} {
95    return [uplevel 2 "wappInt-enc-$mode \"$txt\""]
96  }
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]]
103  }
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]]
110  }
111}
112
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".
115#
116#    wappInt-enc-html           Escape text so that it is safe to use in the
117#                               body of an HTML document.
118#
119#    wappInt-enc-url            Escape text so that it is safe to pass as an
120#                               argument to href= and src= attributes in HTML.
121#
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.
125#
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.
130#
131#    wappInt-enc-unsafe         Perform no encoding at all.  Unsafe.
132#
133proc wappInt-enc-html {txt} {
134  return [string map {& &amp; < &lt; > &gt; \" &quot; \\ &#92;} $txt]
135}
136proc wappInt-enc-unsafe {txt} {
137  return $txt
138}
139proc wappInt-enc-url {s} {
140  if {[regsub -all {[^-{}@~?=#_.:/a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
141    set s [subst -novar -noback $s]
142  }
143  if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
144    set s [subst -novar -noback $s]
145  }
146  return $s
147}
148proc wappInt-enc-qp {s} {
149  if {[regsub -all {[^-{}_.a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
150    set s [subst -novar -noback $s]
151  }
152  if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
153    set s [subst -novar -noback $s]
154  }
155  return $s
156}
157proc wappInt-enc-string {s} {
158  return [string map {\\ \\\\ \" \\\" ' \\' < \\u003c} $s]
159}
160
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
164#
165proc wappInt-%HHchar {c} {
166  if {$c==" "} {return +}
167  return [regsub -all .. [binary encode hex [encoding convertto utf-8 $c]] {%&}]
168}
169
170
171# Undo the www-url-encoded format.
172#
173# HT: This code stolen from ncgi.tcl
174#
175proc 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]
185}
186
187# Reset the document back to an empty string.
188#
189proc wapp-reset {} {
190  global wapp
191  dict set wapp .reply {}
192}
193
194# Change the mime-type of the result document.
195#
196proc wapp-mimetype {x} {
197  global wapp
198  dict set wapp .mimetype $x
199}
200
201# Change the reply code.
202#
203proc wapp-reply-code {x} {
204  global wapp
205  dict set wapp .reply-code $x
206}
207
208# Set a cookie
209#
210proc wapp-set-cookie {name value} {
211  global wapp
212  dict lappend wapp .new-cookies $name $value
213}
214
215# Unset a cookie
216#
217proc wapp-clear-cookie {name} {
218  wapp-set-cookie $name {}
219}
220
221# Add extra entries to the reply header
222#
223proc wapp-reply-extra {name value} {
224  global wapp
225  dict lappend wapp .reply-extra $name $value
226}
227
228# Specifies how the web-page under construction should be cached.
229# The argument should be one of:
230#
231#    no-cache
232#    max-age=N             (for some integer number of seconds, N)
233#    private,max-age=N
234#
235proc wapp-cache-control {x} {
236  wapp-reply-extra Cache-Control $x
237}
238
239# Redirect to a different web page
240#
241proc wapp-redirect {uri} {
242  wapp-reply-code {307 Redirect}
243  wapp-reply-extra Location $uri
244}
245
246# Return the value of a wapp parameter
247#
248proc wapp-param {name {dflt {}}} {
249  global wapp
250  if {![dict exists $wapp $name]} {return $dflt}
251  return [dict get $wapp $name]
252}
253
254# Return true if a and only if the wapp parameter $name exists
255#
256proc wapp-param-exists {name} {
257  global wapp
258  return [dict exists $wapp $name]
259}
260
261# Set the value of a wapp parameter
262#
263proc wapp-set-param {name value} {
264  global wapp
265  dict set wapp $name $value
266}
267
268# Return all parameter names that match the GLOB pattern, or all
269# names if the GLOB pattern is omitted.
270#
271proc wapp-param-list {{glob {*}}} {
272  global wapp
273  return [dict keys $wapp $glob]
274}
275
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.
279#
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.
284#
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.
289#
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.
293#
294proc wapp-allow-xorigin-params {} {
295  global wapp
296  if {![dict exists $wapp .qp] && ![dict get $wapp SAME_ORIGIN]} {
297    wappInt-decode-query-params
298  }
299}
300
301# Set the content-security-policy.
302#
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.
305#
306# Provide an alternative CSP as the argument.  Or use "off" to disable
307# the CSP completely.
308#
309proc 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
315  }
316}
317
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.
321#
322# This routine is advisory only.  It misses some constructs that are
323# dangerous and flags others that are safe.
324#
325proc 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]
334      } {
335        append res "$p:$ln: unsafe \"wapp\" call: \"[string trim $x]\"\n"
336      }
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"
339      }
340    }
341  }
342  return $res
343}
344
345# Return a string that descripts the current environment.  Applications
346# might find this useful for debugging.
347#
348proc 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"
354  }
355  append out "\[pwd\] = [list [pwd]]\n"
356  return $out
357}
358
359# Tracing function for each HTTP request.  This is overridden by wapp-start
360# if tracing is enabled.
361#
362proc wappInt-trace {} {}
363
364# Start up a listening socket.  Arrange to invoke wappInt-new-connection
365# for each inbound HTTP connection.
366#
367#    port            Listen on this TCP port.  0 means to select a port
368#                    that is not currently in use
369#
370#    wappmode        One of "scgi", "remote-scgi", "server", or "local".
371#
372#    fromip          If not {}, then reject all requests from IP addresses
373#                    other than $fromip
374#
375proc 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]
384  }
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]
389  }
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"
398  }
399}
400
401# Start a web-browser and point it at $URL
402#
403proc 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 &
411  }
412}
413
414# This routine is a "socket -server" callback.  The $chan, $ip, and $port
415# arguments are added by the socket command.
416#
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.
420#
421proc 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
426  }
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]
431}
432
433# Close an input channel
434#
435proc 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
442  }
443}
444
445# Process new text received on an inbound HTTP request
446#
447proc 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
451  }
452}
453proc 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
465      }
466      if {[string length [dict get $W .header]]>100000} {
467        error "HTTP request header too big - possible DOS attack"
468      }
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
478      }
479      set len 0
480      if {[dict exists $W CONTENT_LENGTH]} {
481        set len [dict get $W CONTENT_LENGTH]
482      }
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
490      }
491    }
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
502    }
503  }
504}
505
506# Decode the HTTP request header.
507#
508# This routine is always running inside of a [catch], so if
509# any problems arise, simply raise an error.
510#
511proc 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]\""
519  }
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\""
525  }
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\""
535    }
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}
546    }
547    dict set W $name $value
548  }
549  return 0
550}
551
552# Decode the QUERY_STRING parameters from a GET request or the
553# application/x-www-form-urlencoded CONTENT from a POST request.
554#
555# This routine sets the ".qp" element of the ::wapp dict as a signal
556# that query parameters have already been decoded.
557#
558proc 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]]
567      }
568    }
569  }
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]]
578        }
579      }
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
597          }
598        }
599      }
600    }
601  }
602}
603
604# Invoke application-supplied methods to generate a reply to
605# a single HTTP request.
606#
607# This routine always runs within [catch], so handle exceptions by
608# invoking [error].
609#
610proc 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'}
616
617  # Set up additional CGI environment values
618  #
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]
625  }
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
632  }
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 {}
637  }
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]
643  }
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 {}
651  }
652  dict set wapp SELF_URL [dict get $wapp BASE_URL]/[dict get $wapp PATH_HEAD]
653
654  # Parse query parameters from the query string, the cookies, and
655  # POST data
656  #
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]]
663      }
664    }
665  }
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
672    }
673  }
674  dict set wapp SAME_ORIGIN $same_origin
675  if {$same_origin} {
676    wappInt-decode-query-params
677  }
678
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.
682  #
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
691    }
692  } msg]} {
693    if {[wapp-param WAPP_MODE]=="local" || [wapp-param WAPP_MODE]=="server"} {
694      puts "ERROR: $::errorInfo"
695    }
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>
702    }
703    dict unset wapp .new-cookies
704  }
705
706  # Transmit the HTTP reply
707  #
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"
714  }
715  if {[dict exists $wapp .reply-extra]} {
716    foreach {name value} [dict get $wapp .reply-extra] {
717      puts $chan "$name: $value\r"
718    }
719  }
720  if {[dict exists $wapp .csp]} {
721    puts $chan "Content-Security-Policy: [dict get $wapp .csp]\r"
722  }
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"
733        }
734      }
735    }
736  }
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"
744      }
745    }
746  } else {
747    set reply [dict get $wapp .reply]
748  }
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
754}
755
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.
759#
760proc wapp-before-dispatch-hook {} {return}
761
762# Process a single CGI request
763#
764proc 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
787  } {
788    if {[info exists env($key)]} {
789      dict set wapp $key $env($key)
790    }
791  }
792  set len 0
793  if {[dict exists $wapp CONTENT_LENGTH]} {
794    set len [dict get $wapp CONTENT_LENGTH]
795  }
796  if {$len>0} {
797    fconfigure stdin -translation binary
798    dict set wapp CONTENT [read stdin $len]
799  }
800  dict set wapp WAPP_MODE cgi
801  fconfigure stdout -translation binary
802  wappInt-handle-request stdout 1
803}
804
805# Process new text received on an inbound SCGI request
806#
807proc 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
811  }
812}
813proc 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.
818    #
819    # An SGI header is short.  This implementation assumes the entire
820    # header is available all at once.
821    #
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
831    }
832    set len 0
833    if {[dict exists $W CONTENT_LENGTH]} {
834      set len [dict get $W CONTENT_LENGTH]
835    }
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
844    }
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
856    }
857  }
858}
859
860# Start up the wapp framework.  Parameters are a list passed as the
861# single argument.
862#
863#    -server $PORT         Listen for HTTP requests on this TCP port $PORT
864#
865#    -local $PORT          Listen for HTTP requests on 127.0.0.1:$PORT
866#
867#    -scgi $PORT           Listen for SCGI requests on 127.0.0.1:$PORT
868#
869#    -remote-scgi $PORT    Listen for SCGI requests on TCP port $PORT
870#
871#    -cgi                  Handle a single CGI request
872#
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.
878#
879# Additional options:
880#
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.
884#
885#    -nowait              Do not wait in the event loop.  Return immediately
886#                         after all event handlers are established.
887#
888#    -trace               "puts" each request URL as it is handled, for
889#                         debugging
890#
891#    -lint                Run wapp-safety-check on the application instead
892#                         of running the application itself
893#
894#    -Dvar=value          Set TCL global variable "var" to "value"
895#
896#
897proc 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]
912      }
913      -local {
914        incr i;
915        set mode "local"
916        set fromip 127.0.0.1
917        set port [lindex $arglist $i]
918      }
919      -scgi {
920        incr i;
921        set mode "scgi"
922        set fromip 127.0.0.1
923        set port [lindex $arglist $i]
924      }
925      -remote-scgi {
926        incr i;
927        set mode "remote-scgi"
928        set port [lindex $arglist $i]
929      }
930      -cgi {
931        set mode "cgi"
932      }
933      -fromip {
934        incr i
935        set fromip [lindex $arglist $i]
936      }
937      -nowait {
938        set nowait 1
939      }
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
946        }
947      }
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
956        }
957      }
958      -D*=* {
959        if {[regexp {^.D([^=]+)=(.*)$} $term all var val]} {
960          set ::$var $val
961        }
962      }
963      default {
964        error "unknown option: $term"
965      }
966    }
967  }
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
974    }
975  }
976  if {$mode=="cgi"} {
977    wappInt-handle-cgi-request
978  } else {
979    wappInt-start-listener $port $mode $fromip
980    if {!$nowait} {
981      vwait ::forever
982    }
983  }
984}
985
986# Call this version 1.0
987package provide wapp 1.0
988