Wapp

Check-in [fb5eafae32]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Improved SCGI security: (1) The --scgi option only listens on IP address 127.0.0.1. The new --remote-scgi option must be used if the webserver is on a different machine. (2) The new --fromip option can be used to restrict incoming requests to a particular IP address. (3) In SCGI mode, the new parameter "SERVER_ADDR" contains the IP address of the webserver that originated the SCGI request.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: fb5eafae32c267434df7a9a427ca9b68faf254e8cbae4e13fd40c2295893cbb7
User & Date: drh 2019-04-01 00:53:50
Context
2019-04-01
01:31
Fix error in the first example of the "intro.md" page. check-in: 83e002a08c user: drh tags: trunk
00:53
Improved SCGI security: (1) The --scgi option only listens on IP address 127.0.0.1. The new --remote-scgi option must be used if the webserver is on a different machine. (2) The new --fromip option can be used to restrict incoming requests to a particular IP address. (3) In SCGI mode, the new parameter "SERVER_ADDR" contains the IP address of the webserver that originated the SCGI request. check-in: fb5eafae32 user: drh tags: trunk
2019-03-08
00:08
Add the forgotten helloworld.md documentation file. check-in: 5f79eb875f user: drh tags: trunk
Changes
Hide Diffs Unified Diffs Show Whitespace Changes Patch

Changes to docs/intro.md.

59
60
61
62
63
64
65
66
67





68
69
70
71
72
73
74
web-browser at that script.

Run the hello-world program as SCGI like this:

>
    wapptclsh main.tcl --scgi 9000

Then configure your web-server to send SCGI requests to TCL port 9000
for some specific URI, and point your web-browser at that URI.






1.2 Using Plain Old Tclsh
-------------------------

Wapp applications are pure TCL code.  You can run them using an ordinary
"tclsh" command if desired, instead of the "wapptclsh" shown above.  We
normally use "wapptclsh" for the following reasons:







|
|
>
>
>
>
>







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
web-browser at that script.

Run the hello-world program as SCGI like this:

>
    wapptclsh main.tcl --scgi 9000

Then configure your web-server to send SCGI requests to TCP port 9000
for some specific URI, and point your web-browser at that URI.  By
default, the web-server must be on the same machine as the wapp script.
The --scgi option only accepts SCGI requests from IP address 127.0.0.1.
If your webserver is running on a different machine, use the --remote-scgi
option instead, probably with a --fromip option to specify the IP address
of the machine that is running the webserver.

1.2 Using Plain Old Tclsh
-------------------------

Wapp applications are pure TCL code.  You can run them using an ordinary
"tclsh" command if desired, instead of the "wapptclsh" shown above.  We
normally use "wapptclsh" for the following reasons:

Changes to docs/params.md.

244
245
246
247
248
249
250




251
252
253
254
255
256
257
     it should invoke the "wapp-allow-xorigin-params" interface to explicitly
     signal that cross-origin parameters are safe for that page.

  +  **SELF\_URL**  
     The URL for the current page, stripped of query parameter. This is
     useful for filling in the action= attribute of forms.





  +  **WAPP\_MODE**  
     This parameter has a value of "cgi", "local", "scgi", or "server" depending
     on how Wapp was launched.


### 3.1 URL Parsing Example








>
>
>
>







244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
     it should invoke the "wapp-allow-xorigin-params" interface to explicitly
     signal that cross-origin parameters are safe for that page.

  +  **SELF\_URL**  
     The URL for the current page, stripped of query parameter. This is
     useful for filling in the action= attribute of forms.

  +  **SERVER\_ADDR**  
     In SCGI mode only, this variable is the address of the webserver from which
     the SCGI request originates.

  +  **WAPP\_MODE**  
     This parameter has a value of "cgi", "local", "scgi", or "server" depending
     on how Wapp was launched.


### 3.1 URL Parsing Example

Changes to docs/quickref.md.

64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
|REMOTE\_ADDR|→|IP address of the client|
|REMOTE\_PORT|→|TCP port of the client|
|REQUEST\_METHOD|→|"GET" or "POST" or "HEAD"|
|SAME\_ORIGIN|→|True if this request is from the same origin|
|SCRIPT\_FILENAME|→|Full pathname of the Wapp application script|
|SCRIPT\_NAME|→|Prefix of PATH\_INFO that identifies the application script|
|SELF\_URL|→|URL of this request without PATH\_TAIL|

|WAPP\_MODE|→|One of "cgi", "scgi", "server", or "local"|

4.0 URL Parsing
---------------

Assuming "env.tcl" is the name of the Wapp application script:

>







>
|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|REMOTE\_ADDR|→|IP address of the client|
|REMOTE\_PORT|→|TCP port of the client|
|REQUEST\_METHOD|→|"GET" or "POST" or "HEAD"|
|SAME\_ORIGIN|→|True if this request is from the same origin|
|SCRIPT\_FILENAME|→|Full pathname of the Wapp application script|
|SCRIPT\_NAME|→|Prefix of PATH\_INFO that identifies the application script|
|SELF\_URL|→|URL of this request without PATH\_TAIL|
|SERVER\_ADDR|→|IP address of the webserver sending an SCGI request|
|WAPP\_MODE|→|One of "cgi", "scgi", "remote-scgi", "server", or "local"|

4.0 URL Parsing
---------------

Assuming "env.tcl" is the name of the Wapp application script:

>

Changes to wapp.tcl.

363
364
365
366
367
368
369
370
371



372
373

374
375

376
377
378

379
380
381
382
383
384
385
386
387
388


389
390
391
392
393
394
395
...
404
405
406
407
408
409
410
411

412
413
414




415
416
417
418
419
420
421
...
803
804
805
806
807
808
809

810
811
812
813
814
815
816
...
821
822
823
824
825
826
827

828
829
830
831
832
833
834
835
836
837
838

839
840
841
842
843
844
845
846
847
848
849
850
851
852


853
854
855
856
857
858
859
860
861
862
863




864
865
866
867
868
869
870
...
874
875
876
877
878
879
880

881
882
883
884
885
886
887
888
889
890
891
892
893

894
895
896
897
898






899
900
901
902
903




904
905
906
907
908
909
910
...
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942

943
944
945
946
947
948
949
950
951
952
953

954
955
956

# 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
#
................................................................................
  }
}

# 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
................................................................................
  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
................................................................................
      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:
#




#    -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
................................................................................
#
#
proc wapp-start {arglist} {
  global env
  set mode auto
  set port 0
  set nowait 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"
      }




      -nowait {
        set nowait 1
      }
      -trace {
        proc wappInt-trace {} {
          set q [wapp-param QUERY_STRING]
          set uri [wapp-param BASE_URL][wapp-param PATH_INFO]
................................................................................
        }
      }
      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
  }
  if {!$nowait} {
    vwait ::forever
  }
}


# Call this version 1.0
package provide wapp 1.0







|

>
>
>
|
<
>

|
>


|
>

|








>
>







 







|
>

|

>
>
>
>







 







>







 







>











>













|
>
>











>
>
>
>







 







>













>





>
>
>
>
>
>





>
>
>
>







 







|
|
|
|
|
|
<
|
>
|
|
<
<

|
<




>



363
364
365
366
367
368
369
370
371
372
373
374
375

376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
...
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
...
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
...
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
...
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
...
961
962
963
964
965
966
967
968
969
970
971
972
973

974
975
976
977


978
979

980
981
982
983
984
985
986
987

# 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
#
................................................................................
  }
}

# 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} {
  upvar #0 wappInt-$chan W
  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 $chan]
}

# Close an input channel
................................................................................
  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
................................................................................
      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 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
      dict set W SERVER_ADDR [dict get $W .remove_addr]
      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 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
................................................................................
#
#
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]
................................................................................
        }
      }
      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