Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Add the wapp-subst command, with documentation and tests. |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
b304d05bf0df51b4b0af5e26518e6ad0 |
User & Date: | drh 2017-12-16 03:10:04.902 |
Context
2017-12-16
| ||
17:57 | Improved wappInt-enc-url that works with unicode. Added %qp and %unsafe subtitutions in wapp-subst. Updated the documentation accordingly. (check-in: 908c2891cf user: drh tags: trunk) | |
03:10 | Add the wapp-subst command, with documentation and tests. (check-in: b304d05bf0 user: drh tags: trunk) | |
2017-12-15
| ||
21:44 | Add a "clean" target to the Makefile, and optimize for size. (check-in: b9b69d4d68 user: drh tags: trunk) | |
Changes
Changes to README.md.
︙ | ︙ | |||
88 89 90 91 92 93 94 | available in ::wapp. > package require wapp proc wapp-default {} { global wapp wapp "<h1>Hello, World!</h1>\n" | | > | | | | | > > | < > | < | | 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | available in ::wapp. > package require wapp proc wapp-default {} { global wapp wapp "<h1>Hello, World!</h1>\n" set B [dict get $wapp BASE_URL] wapp-subst {<p>See the <a href='%html($B)/env'>Wapp } wapp "Environment</a></p>" } proc wapp-page-env {} { global wapp wapp "<h1>Wapp Environment</h1>\n" wapp "<pre>\n" foreach var [lsort [dict keys $wapp]] { if {[string index $var 0]=="."} continue wapp-subst {%html($var) = %html([list [dict get $wapp $var]])\n} } wapp "</pre>" } wapp-start $::argv In this application, the default "Hello, World!" page has been extended with a hyperlink to the /env page. The "wapp-subst" command works like "wapp" in that it appends its argument text to the web page under construction. But "wapp-subst" also does safe substitutions of text. Within the "wapp-subst" argument, "%html(...)" is replaced by the expansion of "..." which has been escaped for safe inclusion in HTML text. Similarly, "%url(...)" is replaced by "..." after it has been expanded and escaped for use as a URL query parameter. The argument to "wapp-subst" should always be enclosed in {...}. Backslash substitutions are performed automatically. The /env page is implemented by the "wapp-page-env" proc. This proc generates HTML that describes the content of the ::wapp dict. Keys that begin with "." are for internal use by Wapp and are skipped for this display. Notice the use of "wapp-subst" to safely escape text for inclusion in an HTML document. 4.0 The ::wapp Global Dict -------------------------- To better understand how the ::wapp dict works, try running the previous sample program, but extend the /env URL with extra path elements and query parameters. For example: |
︙ | ︙ | |||
252 253 254 255 256 257 258 | though it might be some subset of $::argv if the containing application has already processed some command-line parameters for itself. + **wapp** _TEXT_ Add _TEXT_ to the web page output currently under construction. _TEXT_ must not contain any TCL variable or command substitutions. | | | | < < | < < < | < < < | > > > > > > > > > > > > > > > > > > > > | 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | though it might be some subset of $::argv if the containing application has already processed some command-line parameters for itself. + **wapp** _TEXT_ Add _TEXT_ to the web page output currently under construction. _TEXT_ must not contain any TCL variable or command substitutions. + **wapp-subst** _TEXT_ The _TEXT_ argument should be enclosed in {...} to prevent substitutions. The "wapp-subst" command itself will do all necessary backslash substitutions. Command and variable substitutions only occur within "%html(...)" and "%url(...)" and the results are safely escaped for inclusion in the body of an HTML document or as a query parameter. + **wapp-mimetype** _MIMETYPE_ Set the MIME-type for the generated web page. The default is "text/html". + **wapp-reply-code** _CODE_ Set the reply-code for the HTTP request. The default is "200 Ok". + **wapp-redirect** _TARGET-URL_ Cause an HTTP redirect to the specified URL. + **wapp-reset** Reset the web page under construction back to an empty string. + **wapp-set-cookie** \[-path _PATH_\] \[-expires _DAYS_\] _NAME_ _VALUE_ Cause the cookie _NAME_ to be set to _VALUE_. * **wapp-safety-check** Examine all TCL procedures in the application and report errors about unsafe usage of "wapp". + **wapp-unsafe** _TEXT_ Add _TEXT_ to the web page under construction even though _TEXT_ does contain TCL variable and command substitutions. The application developer must ensure that the variable and command substitutions does not allow XSS attacks. Avoid using this command. The use of "wapp-subst" is preferred in most situations. + **wapp-encode-html** _TEXT_ Add _TEXT_ to the web page under construction after first escaping any HTML markup contained with _TEXT_. This command is equivalent to "wapp-subst {%html(_TEXT_)}". + **wapp-encode-url** _TEXT_ Add _TEXT_ to the web page under construction after first escaping any characters so that the result is safe to include as the value of a query parameter on a URL. This command is equivalent to "wapp-subst {%url(_TEXT_)}". The following additional interfaces are envisioned, but are not yet implemented: + **wapp-send-hex** _HEX_ Cause the HTTP reply to be binary that is constructed from the hexadecimal text in the _HEX_ argument. Whitespace in _HEX_ is ignored. |
︙ | ︙ |
Changes to test01.tcl.
1 2 3 4 5 6 7 8 9 10 11 | # Invoke as "tclsh test01.tcl" and then surf the website that pops up # to verify the logic in wapp. # source wapp.tcl proc wapp-default {} { global wapp set B [dict get $wapp BASE_URL] set R [dict get $wapp SCRIPT_NAME] wapp "<h1>Hello, World!</h1>\n" wapp "<ol>" wapp-unsafe "<li><p><a href='$R/env'>Wapp Environment</a></p>\n" | | > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # Invoke as "tclsh test01.tcl" and then surf the website that pops up # to verify the logic in wapp. # source wapp.tcl proc wapp-default {} { global wapp set B [dict get $wapp BASE_URL] set R [dict get $wapp SCRIPT_NAME] wapp "<h1>Hello, World!</h1>\n" wapp "<ol>" wapp-unsafe "<li><p><a href='$R/env'>Wapp Environment</a></p>\n" wapp-subst {<li><p><a href='%html($B)/fullenv'>Full Environment</a>\n} set crazy [lsort [dict keys $wapp]] wapp-subst {<li><p><a href='%html($B)/env?keys=%url($crazy)'>} wapp "Environment with crazy URL\n" wapp-subst {<li><p><a href='%html($B)/lint'>Lint</a>\n} wapp-subst {<li><p><a href='%html($B)/errorout'>Deliberate error</a>\n} wapp "</ol>" if {[dict exists $wapp showenv]} { wapp-page-env } } proc wapp-page-env {} { global wapp |
︙ | ︙ | |||
54 55 56 57 58 59 60 | wapp "</form>" wapp "<pre>\n" foreach var [lsort [dict keys $wapp]] { if {$var==".reply"} continue wapp-escape-html "$var = [list [dict get $wapp $var]]\n\n" } wapp "</pre>" | | | 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | wapp "</form>" wapp "<pre>\n" foreach var [lsort [dict keys $wapp]] { if {$var==".reply"} continue wapp-escape-html "$var = [list [dict get $wapp $var]]\n\n" } wapp "</pre>" wapp-subst {<p><a href='%html([dict get $wapp BASE_URL])/'>Home</a></p>\n} } proc wapp-page-lint {} { wapp "<h1>Potental Cross-Site Injection Vulerabilities In This App</h1>\n" set res [wapp-safety-check] if {$res==""} { wapp "<p>No problems found.</p>\n" } else { |
︙ | ︙ |
Added test04.tcl.
> > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | source wapp.tcl package require wapp proc wapp-default {} { global wapp wapp "<h1>Hello, World!</h1>\n" set B [dict get $wapp BASE_URL] wapp-subst {<p>See the <a href='%html($B)/env'>Wapp } wapp "Environment</a></p>" } proc wapp-page-env {} { global wapp wapp "<h1>Wapp Environment</h1>\n" wapp "<pre>\n" foreach var [lsort [dict keys $wapp]] { if {[string index $var 0]=="."} continue wapp-subst {%html($var) = %html([list [dict get $wapp $var]])\n} } wapp "</pre>" } wapp-start $::argv |
Changes to wapp.tcl.
︙ | ︙ | |||
26 27 28 29 30 31 32 | dict append wapp .reply $txt } proc wapp-unsafe {txt} { global wapp dict append wapp .reply $txt } | | > > > > > > > > > > | > > > > > > > > > > > > > > > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | dict append wapp .reply $txt } proc wapp-unsafe {txt} { global wapp dict append wapp .reply $txt } # Append text after escaping it for HTML. # # The following commands are the same: # # wapp-escape-html TEXT # wapp-subst %html(TEXT) # proc wapp-escape-html {txt} { global wapp dict append wapp .reply [string map {& & < < > >} $txt] } # Append text after escaping it for URL query parameters. # # The following commands are the same: # # wapp-escape-url TEXT # wapp-subst %url(TEXT) # proc wapp-escape-url {txt} { global wapp dict append wapp .reply [wappInt-enc-url $txt] } # The argument should be in {...}. Substitions of %html(...) encode ... # escaped for safe insertion into HTML. %url(...) substitions encode the # argument for safe insertion into query parameters of URLs. Backslash # substitutions are also performed, but variable substitutions are not, # except within %html() and %url(). # proc wapp-subst {txt} { global wapp regsub -all -- {%(html|url)\(([^)]+)\)} $txt {[wappInt-enc-\1 "\2"]} txt dict append wapp .reply [uplevel 1 [list subst -novariables $txt]] } proc wappInt-enc-html {txt} { return [string map {& & < < > >} $txt] } # Reset the document back to an empty string. # proc wapp-reset {} { global wapp dict set wapp .reply {} |
︙ | ︙ | |||
86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 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" } } } return $res } # Start up the wapp framework. Parameters are a list passed as the # single argument. | > > > | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | 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[ \t]+[^\173]} $x]} { append res "$p:$ln: unsafe \"wapp-subst\" call: \"[string trim $x]\"\n" } } } return $res } # Start up the wapp framework. Parameters are a list passed as the # single argument. |
︙ | ︙ | |||
368 369 370 371 372 373 374 | # 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]} { | | | | | 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | # 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]] } } } 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]] } } } # POST data is only decoded if the HTTP_REFERER is from the same # application, as a defense against Cross-Site Request Forgery (CSRF) # attacks. if {[dict exists $wapp CONTENT_TYPE] && [dict get $wapp CONTENT_TYPE]=="application/x-www-form-urlencoded" && [dict exists $wapp CONTENT] && [dict exists $wapp HTTP_REFERER] && [string match [dict get $wapp BASE_URL]/* [dict get $wapp HTTP_REFERER]] } { 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]] } } } # To-Do: Perhaps add support for multipart/form-data decoding. # Alternatively, perhaps multipart/form-data decoding can be done # by application code using a separate helper function, like # "wapp_decode_multipart_formdata" or somesuch. |
︙ | ︙ | |||
439 440 441 442 443 444 445 | puts $chan "Content-Length: [string length [dict get $wapp .reply]]\r" puts $chan "Connection: Closed\r" } puts $chan "Content-Type: [dict get $wapp .mimetype]\r" if {[dict exists $wapp .new-cookies]} { foreach {nm val} [dict get $wapp .new-cookies] { if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} { | | | | 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 | puts $chan "Content-Length: [string length [dict get $wapp .reply]]\r" puts $chan "Connection: Closed\r" } puts $chan "Content-Type: [dict get $wapp .mimetype]\r" if {[dict exists $wapp .new-cookies]} { foreach {nm val} [dict get $wapp .new-cookies] { if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} { set val [wappInt-enc-url $val] puts $chan "Set-Cookie: $nm=$val; HttpOnly; Path=/\r" } } } puts $chan "\r" puts $chan [dict get $wapp .reply] flush $chan wappInt-close-channel $chan } # 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 [DecodeHex \1\2\3]]} str regsub -all -- \ {%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \ $str {[encoding convertfrom utf-8 [DecodeHex \1\2]]} str |
︙ | ︙ | |||
498 499 500 501 502 503 504 | \356 %EE \357 %EF \360 %F0 \361 %F1 \362 %F2 \363 %F3 \364 %F4 \365 %F5 \366 %F6 \367 %F7 \370 %F8 \371 %F9 \372 %FA \373 %FB \374 %FC \375 %FD \376 %FE \377 %FF } # Do URL encoding # | | | 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 | \356 %EE \357 %EF \360 %F0 \361 %F1 \362 %F2 \363 %F3 \364 %F4 \365 %F5 \366 %F6 \367 %F7 \370 %F8 \371 %F9 \372 %FA \373 %FB \374 %FC \375 %FD \376 %FE \377 %FF } # Do URL encoding # proc wappInt-enc-url {str} { upvar #0 wappInt-map map regsub -all -- \[^a-zA-Z0-9\] $str {$map(&)} str regsub -all -- {[][{})\\]\)} $str {\\&} str return [subst -nocommand $str] } # Process a single CGI request |
︙ | ︙ |