1141 lines
33 KiB
Plaintext
1141 lines
33 KiB
Plaintext
#!/usr/bin/tclsh.docsrc
|
|
#### Import of wapp.tcl
|
|
# 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"
|
|
#
|
|
|
|
# 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.
|
|
#
|
|
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]]
|
|
}
|
|
|
|
# Works like wapp-subst, but also removes whitespace from the beginning
|
|
# of lines.
|
|
#
|
|
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} $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 procs] {
|
|
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", "server", or "local".
|
|
#
|
|
proc wappInt-start-listener {port wappmode} {
|
|
if {$wappmode=="scgi"} {
|
|
set type SCGI
|
|
set server [list wappInt-new-connection wappInt-scgi-readable $wappmode]
|
|
} else {
|
|
set type HTTP
|
|
set server [list wappInt-new-connection wappInt-http-readable $wappmode]
|
|
}
|
|
if {$wappmode=="local"} {
|
|
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/
|
|
} 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.
|
|
#
|
|
proc wappInt-new-connection {callback wappmode chan ip port} {
|
|
upvar #0 wappInt-$chan W
|
|
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 $chan]
|
|
}
|
|
|
|
# Close an input channel
|
|
#
|
|
proc wappInt-close-channel {chan} {
|
|
if {$chan=="stdout"} {
|
|
# This happens after completing a CGI request
|
|
exit 0
|
|
} else {
|
|
unset ::wappInt-$chan
|
|
close $chan
|
|
}
|
|
}
|
|
|
|
# Process new text received on an inbound HTTP request
|
|
#
|
|
proc wappInt-http-readable {chan} {
|
|
if {[catch [list wappInt-http-readable-unsafe $chan] msg]} {
|
|
puts stderr "$msg\n$::errorInfo"
|
|
wappInt-close-channel $chan
|
|
}
|
|
}
|
|
proc wappInt-http-readable-unsafe {chan} {
|
|
upvar #0 wappInt-$chan W 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
|
|
set a0 [file normalize $argv0]
|
|
dict set W SCRIPT_FILENAME $a0
|
|
dict set W DOCUMENT_ROOT [file dir $a0]
|
|
if {[wappInt-parse-header $chan]} {
|
|
catch {close $chan}
|
|
return
|
|
}
|
|
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 0
|
|
}
|
|
}
|
|
} 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 0
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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 {chan} {
|
|
upvar #0 wappInt-$chan 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 0
|
|
}
|
|
|
|
# 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] &&
|
|
[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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Invoke application-supplied methods to generate a reply to
|
|
# a single HTTP request.
|
|
#
|
|
# This routine always runs within [catch], so handle exceptions by
|
|
# invoking [error].
|
|
#
|
|
proc wappInt-handle-request {chan useCgi} {
|
|
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) the 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 proc 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
|
|
}
|
|
|
|
# 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 $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}
|
|
|
|
# Process a single CGI request
|
|
#
|
|
proc wappInt-handle-cgi-request {} {
|
|
global wapp env
|
|
foreach key {
|
|
CONTENT_LENGTH
|
|
CONTENT_TYPE
|
|
DOCUMENT_ROOT
|
|
HTTP_ACCEPT_ENCODING
|
|
HTTP_COOKIE
|
|
HTTP_HOST
|
|
HTTP_REFERER
|
|
HTTP_USER_AGENT
|
|
HTTPS
|
|
PATH_INFO
|
|
QUERY_STRING
|
|
REMOTE_ADDR
|
|
REQUEST_METHOD
|
|
REQUEST_URI
|
|
REMOTE_USER
|
|
SCRIPT_FILENAME
|
|
SCRIPT_NAME
|
|
SERVER_NAME
|
|
SERVER_PORT
|
|
SERVER_PROTOCOL
|
|
} {
|
|
if {[info exists env($key)]} {
|
|
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 stdout 1
|
|
}
|
|
|
|
# Process new text received on an inbound SCGI request
|
|
#
|
|
proc wappInt-scgi-readable {chan} {
|
|
if {[catch [list wappInt-scgi-readable-unsafe $chan] msg]} {
|
|
puts stderr "$msg\n$::errorInfo"
|
|
wappInt-close-channel $chan
|
|
}
|
|
}
|
|
proc wappInt-scgi-readable-unsafe {chan} {
|
|
upvar #0 wappInt-$chan W wapp wapp
|
|
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.
|
|
#
|
|
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
|
|
set wapp $W
|
|
wappInt-handle-request $chan 0
|
|
}
|
|
} 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 0
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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 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:
|
|
#
|
|
# -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 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 port [lindex $arglist $i]
|
|
}
|
|
-scgi {
|
|
incr i;
|
|
set mode "scgi"
|
|
set port [lindex $arglist $i]
|
|
}
|
|
-cgi {
|
|
set mode "cgi"
|
|
}
|
|
-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"
|
|
&& [info exists env(GATEWAY_INTERFACE)]
|
|
&& [string match CGI/1.* $env(GATEWAY_INTERFACE)])
|
|
|| $mode=="cgi"
|
|
} {
|
|
wappInt-handle-cgi-request
|
|
return
|
|
}
|
|
if {$mode=="scgi"} {
|
|
wappInt-start-listener $port scgi
|
|
} elseif {$mode=="server"} {
|
|
wappInt-start-listener $port server
|
|
} else {
|
|
wappInt-start-listener $port local
|
|
}
|
|
vwait ::forever
|
|
}
|
|
|
|
# Call this version 1.0
|
|
package provide wapp 1.0
|
|
|
|
#### End of wapp.tcl
|
|
|
|
# Generate all header content for the output document
|
|
#
|
|
proc search_header {} {
|
|
wapp-trim {
|
|
<!DOCTYPE html>
|
|
<html><head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
<link href="../sqlite.css" rel="stylesheet">
|
|
<title>Search SQLite Stats</title>
|
|
<!-- path=../ -->
|
|
</head>
|
|
<body>
|
|
<div class=nosearch>
|
|
<a href="../index.html">
|
|
<img class="logo" src="../images/sqlite370_banner.gif" alt="SQLite" border="0">
|
|
</a>
|
|
<div><!-- IE hack to prevent disappearing logo --></div>
|
|
<div class="tagline desktoponly">
|
|
Small. Fast. Reliable.<br>Choose any three.
|
|
</div>
|
|
<div class="menu mainmenu">
|
|
<ul>
|
|
<li><a href="../index.html">Home</a>
|
|
<li class='mobileonly'><a href="javascript:void(0)" onclick='toggle_div("submenu")'>Menu</a>
|
|
<li class='wideonly'><a href='../about.html'>About</a>
|
|
<li class='desktoponly'><a href="../docs.html">Documentation</a>
|
|
<li class='desktoponly'><a href="../download.html">Download</a>
|
|
<li class='wideonly'><a href='../copyright.html'>License</a>
|
|
<li class='desktoponly'><a href="../support.html">Support</a>
|
|
<li class='desktoponly'><a href="../prosupport.html">Purchase</a>
|
|
<li class='search' id='search_menubutton'>
|
|
<a href="javascript:void(0)" onclick='toggle_search()'>Search</a>
|
|
</ul>
|
|
</div>
|
|
<div class="menu submenu" id="submenu">
|
|
<ul>
|
|
<li><a href='../about.html'>About</a>
|
|
<li><a href='../docs.html'>Documentation</a>
|
|
<li><a href='../download.html'>Download</a>
|
|
<li><a href='../support.html'>Support</a>
|
|
<li><a href='../prosupport.html'>Purchase</a>
|
|
</ul>
|
|
</div>
|
|
<div class="searchmenu" id="searchmenu">
|
|
<form method="GET" action="../search">
|
|
<select name="s" id="searchtype">
|
|
<option value="d">Search Documentation</option>
|
|
<option value="c">Search Changelog</option>
|
|
</select>
|
|
<input type="text" name="q" id="searchbox" value="">
|
|
<input type="submit" value="Go">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function toggle_div(nm) {
|
|
var w = document.getElementById(nm);
|
|
if( w.style.display=="block" ){
|
|
w.style.display = "none";
|
|
}else{
|
|
w.style.display = "block";
|
|
}
|
|
}
|
|
function toggle_search() {
|
|
var w = document.getElementById("searchmenu");
|
|
if( w.style.display=="block" ){
|
|
w.style.display = "none";
|
|
} else {
|
|
w.style.display = "block";
|
|
setTimeout(function(){
|
|
document.getElementById("searchbox").focus()
|
|
}, 30);
|
|
}
|
|
}
|
|
function div_off(nm){document.getElementById(nm).style.display="none";}
|
|
window.onbeforeunload = function(e){div_off("submenu");}
|
|
/* Disable the Search feature if we are not operating from CGI, since */
|
|
/* Search is accomplished using CGI and will not work without it. */
|
|
if( !location.origin || !location.origin.match || !location.origin.match(/http/) ){
|
|
document.getElementById("search_menubutton").style.display = "none";
|
|
}
|
|
/* Used by the Hide/Show button beside syntax diagrams, to toggle the */
|
|
function hideorshow(btn,obj){
|
|
var x = document.getElementById(obj);
|
|
var b = document.getElementById(btn);
|
|
if( x.style.display!='none' ){
|
|
x.style.display = 'none';
|
|
b.innerHTML='show';
|
|
}else{
|
|
x.style.display = '';
|
|
b.innerHTML='hide';
|
|
}
|
|
return false;
|
|
}
|
|
</script>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
proc wapp-default {} {
|
|
# When running using the built-in webserver in Wapp (in other words,
|
|
# when not running as CGI) any filename that contains a "." loads
|
|
# directly from the filesystem.
|
|
if {[wapp-param WAPP_MODE]!="cgi"]
|
|
&& [string match *.* [wapp-param PATH_INFO]]
|
|
} {
|
|
set altfile [file dir [wapp-param SCRIPT_FILENAME]]/../[wapp-param PATH_INFO]
|
|
set fd [open $altfile rb]
|
|
fconfigure $fd -translation binary
|
|
wapp-unsafe [read $fd]
|
|
close $fd
|
|
switch -glob -- $altfile {
|
|
*.html {
|
|
wapp-mimetype text/html
|
|
}
|
|
*.css {
|
|
wapp-mimetype text/css
|
|
}
|
|
*.gif {
|
|
wapp-mimetype image/gif
|
|
}
|
|
}
|
|
return
|
|
}
|
|
wapp-page-admin
|
|
}
|
|
proc wapp-page-admin {} {
|
|
wapp-allow-xorigin-params
|
|
wapp-content-security-policy off
|
|
if {[wapp-param-exists env]} {
|
|
search_header
|
|
wapp-trim {
|
|
<h1>Environment</h1>
|
|
<pre>%html([wapp-debug-env])</pre>
|
|
}
|
|
return
|
|
}
|
|
sqlite3 db2 [file dir [wapp-param SCRIPT_FILENAME]]/searchlog.db
|
|
set where ""
|
|
set res ""
|
|
|
|
set ipfilter ""
|
|
set ipaddr [wapp-param ip]
|
|
if {$ipaddr!=""} {
|
|
set where {WHERE ip = $ipaddr}
|
|
set ipfilter $ipaddr
|
|
}
|
|
|
|
set checked ""
|
|
set isUnique [expr {[wapp-param unique 0]+0}]
|
|
if {$isUnique} {
|
|
set checked "checked"
|
|
}
|
|
|
|
set limit [wapp-param limit 10]
|
|
set s10 ""
|
|
set s100 ""
|
|
set s1000 ""
|
|
if {$limit==10} {set s10 selected}
|
|
if {$limit==100} {set s100 selected}
|
|
if {$limit==1000} {set s1000 selected}
|
|
|
|
search_header
|
|
set self [wapp-param PATH_HEAD]
|
|
wapp-trim {
|
|
<div style="margin:2em">
|
|
<center>
|
|
<form action='%url($self)' method='GET'>
|
|
Results: <select name=limit onChange="this.form.submit()">
|
|
<option %html($s10) value="10">10</option>
|
|
<option %html($s100) value="100">100</option>
|
|
<option %html($s1000) value="1000">1000</option>
|
|
</select>
|
|
IP: <input type=input name=ip value="%html($ipfilter)">
|
|
Unique: <input
|
|
type=checkbox name=unique value=1
|
|
$checked
|
|
onChange="this.form.submit()">
|
|
<input type=submit>
|
|
</form>
|
|
</center>
|
|
</div>
|
|
<table border=1 cellpadding=10 align=center>
|
|
<tr><td><th>IP <th>Query <th> Results <th> Timestamp
|
|
}
|
|
|
|
set i 0
|
|
db2 eval "
|
|
SELECT rowid, ip, query, nres, timestamp FROM log $where
|
|
ORDER BY rowid DESC
|
|
" {
|
|
|
|
if {$isUnique} {
|
|
if {[info exists seen($query)]} continue
|
|
set seen($query) 1
|
|
}
|
|
|
|
wapp-trim {
|
|
<tr><td>%html($rowid)
|
|
<td><a href='%url($self)?ip=%qp($ip)'>%html($ip)</a>
|
|
<td><a href='../search?q=%qp($query)&donotlog=1'>%html($query)</a>
|
|
<td>%html($nres)<td>%html($timestamp)
|
|
}
|
|
incr i
|
|
if {$i >= $limit} break
|
|
}
|
|
wapp-subst {</table\n}
|
|
db2 close
|
|
}
|
|
wapp-start $argv
|