︙ | | | ︙ | |
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
fconfigure $chan -blocking 0 -translation binary
fileevent $chan readable "wappInt-readable $chan"
}
# Close an input channel
#
proc wappInt-close-channel {chan} {
unset ::wappInt-$chan
close $chan
}
# Process new text received on an inbound HTTP request
#
proc wappInt-readable {chan} {
if {[catch [list wappInt-readable-unsafe $chan] msg]} {
puts stderr "$msg\n$::errorInfo"
wappInt-close-channel $chan
}
}
proc wappInt-readable-unsafe {chan} {
upvar #0 wappInt-$chan W
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} {
wappInt-parse-header $chan
set len 0
if {[dict exists $W CONTENT_LENGTH]} {
set len [dict get $W CONTENT_LENGTH]
}
if {$len>0} {
dict set W .toread $len
} else {
wappInt-handle-request $chan
}
}
} 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} {
wappInt-parse-post-data $chan
wappInt-handle-request $chan
}
}
}
# Decode the HTTP request header.
#
# This routine is always running inside of a [catch], so if
|
>
>
>
>
|
|
>
|
>
>
>
>
|
>
|
|
|
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
|
fconfigure $chan -blocking 0 -translation binary
fileevent $chan readable "wappInt-readable $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-readable {chan} {
if {[catch [list wappInt-readable-unsafe $chan] msg]} {
puts stderr "$msg\n$::errorInfo"
wappInt-close-channel $chan
}
}
proc wappInt-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.
wappInt-parse-header $chan
set len 0
if {[dict exists $W CONTENT_LENGTH]} {
set len [dict get $W CONTENT_LENGTH]
}
if {$len>0} {
# Still need to read the query content
dict set W .toread $len
} else {
# There is no query content, so handle the request immediately
set wapp $W
wappInt-handle-request $chan 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
|
︙ | | | ︙ | |
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
|
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
foreach qterm [split $uri1 &] {
set qsplit [split $qterm =]
set nm [lindex $qsplit 0]
if {[regexp {^[a-z][a-z0-9]*$} $nm]} {
dict set W $nm [wappInt-url-decode [lindex $qsplit 1]]
}
}
if {[regexp {^/([^/]+)(.*)$} $uri0 all head tail]} {
dict set W PATH_HEAD $head
dict set W PATH_TAIL $tail
} else {
dict set W PATH_HEAD {}
dict set W PATH_TAIL {}
}
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 {}
USER-AGENT {set name HTTP_USER_AGENT}
CONTENT-LENGTH {set name CONTENT_LENGTH}
CONTENT-TYPE {set name CONTENT_TYPE}
HOST {set name HTTP_HOST}
default {set name .hdr:$name}
}
dict set W $name $value
}
if {![dict exists $W HTTP_HOST]} {
dict set W BASE_URL {}
} elseif {[dict exists $W HTTPS]} {
dict set W BASE_URL https://[dict get $W HTTP_HOST]
} else {
dict set W BASE_URL http://[dict get $W HTTP_HOST]
}
dict set W SELF_URL [dict get $W BASE_URL]/[dict get $W PATH_HEAD]
if {[dict exists $W .hdr:COOKIE]} {
foreach qterm [split [dict get $W .hdr:COOKIE] {;}] {
set qsplit [split [string trim $qterm] =]
set nm [lindex $qsplit 0]
if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
dict set W $nm [wappInt-url-decode [lindex $qsplit 1]]
}
}
}
}
# 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} {
upvar #0 wappInt-$chan W wapp wapp
set wapp $W
dict set wapp .reply {}
dict set wapp .mimetype {text/html; charset=utf-8}
dict set wapp .reply-code {200 Ok}
set mname [dict get $wapp PATH_HEAD]
if {[catch {
if {$mname!="" && [llength [info commands wapp-page-$mname]]>0} {
wapp-page-$mname
} else {
wapp-default
}
} msg]} {
wapp-reset
wapp-reply-code "500 Internal Server Error"
wapp-mimetype text/html
wapp "<h1>Wapp Application Error</h1>\n"
wapp "<pre>\n"
wapp-escape-html $::errorInfo
wapp "</pre>\n"
dict unset wapp .new-cookies
}
puts $chan "HTTP/1.0 [dict get $wapp .reply-code]\r"
puts $chan "Server: wapp\r"
puts $chan "Content-Length: [string length [dict get $wapp .reply]]\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-url-encode $val]
puts $chan "Set-Cookie: $nm=$val; HttpOnly; Path=/\r"
}
}
}
puts $chan "Connection: Closed\r\n\r"
puts $chan [dict get $wapp .reply]
flush $chan
wappInt-close-channel $chan
}
# Undo the www-url-encoded format.
#
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
>
>
|
|
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
|
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}
default {set name .hdr:$name}
}
dict set W $name $value
}
}
# 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}
# Set up additional CGI environment values
#
if {![dict exists $wapp REQUEST_URI]} {
dict set wapp REQUEST_URI /
}
if {[dict exists $wapp PATH_INFO]
&& [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 {}
}
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]
}
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-url-decode [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-url-decode [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-url-decode [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.
# 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.
#
set mname [dict get $wapp PATH_HEAD]
if {[catch {
if {$mname!="" && [llength [info commands wapp-page-$mname]]>0} {
wapp-page-$mname
} else {
wapp-default
}
} msg]} {
wapp-reset
wapp-reply-code "500 Internal Server Error"
wapp-mimetype text/html
wapp "<h1>Wapp Application Error</h1>\n"
wapp "<pre>\n"
wapp-escape-html $::errorInfo
wapp "</pre>\n"
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.0 [dict get $wapp .reply-code]\r"
puts $chan "Server: wapp\r"
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-url-encode $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.
#
|
︙ | | | ︙ | |
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
403
404
405
406
407
408
409
410
|
regsub -all -- \
{%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
$str {[encoding convertfrom utf-8 [DecodeHex \1\2]]} str
regsub -all -- {%([0-7][A-Fa-f0-9])} $str {\\u00\1} str
return [subst -novar $str]
}
# Process POST data.
#
# As a defense against Cross-Site Request Forgeries, POST data is ignored
# if the REFERER is not within the BASE_URL.
#
proc wappInt-parse-post-data {chan} {
upvar #0 wappInt-$chan W
if {[dict exists $W CONTENT_TYPE]
&& [dict get $W CONTENT_TYPE]=="application/x-www-form-urlencoded"
&& [dict exists $W REFERER]
&& [string match [dict get $W BASE_URL]/* [dict get $W REFERER]]
} {
foreach qterm [split [string trim [dict get $W CONTENT]] &] {
set qsplit [split $qterm =]
set nm [lindex $qsplit 0]
if {[regexp {^[a-z][a-z0-9]*$} $nm]} {
dict set W $nm [wappInt-url-decode [lindex $qsplit 1]]
}
}
return
}
# TODO: Decode multipart/form-data
}
# Data for doing url-encoding.
#
array set wappInt-map {
\000 %00 \001 %01 \002 %02 \003 %03 \004 %04 \005 %05 \006 %06 \007 %07
\010 %08 \011 %09 \012 %0A \013 %0B \014 %0C \015 %0D \016 %0E \017 %0F
\020 %10 \021 %11 \022 %12 \023 %13 \024 %14 \025 %15 \026 %16 \027 %17
\030 %18 \031 %19 \032 %1A \033 %1B \034 %1C \035 %1D \036 %1E \037 %1F
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
432
433
434
435
436
437
438
439
440
441
442
443
444
445
|
regsub -all -- \
{%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
$str {[encoding convertfrom utf-8 [DecodeHex \1\2]]} str
regsub -all -- {%([0-7][A-Fa-f0-9])} $str {\\u00\1} str
return [subst -novar $str]
}
# Data for doing url-encoding.
#
array set wappInt-map {
\000 %00 \001 %01 \002 %02 \003 %03 \004 %04 \005 %05 \006 %06 \007 %07
\010 %08 \011 %09 \012 %0A \013 %0B \014 %0C \015 %0D \016 %0E \017 %0F
\020 %10 \021 %11 \022 %12 \023 %13 \024 %14 \025 %15 \026 %16 \027 %17
\030 %18 \031 %19 \032 %1A \033 %1B \034 %1C \035 %1D \036 %1E \037 %1F
|
︙ | | | ︙ | |
435
436
437
438
439
440
441
|
#
proc wappInt-url-encode {str} {
upvar #0 wappInt-map map
regsub -all -- \[^a-zA-Z0-9\] $str {$map(&)} str
regsub -all -- {[][{})\\]\)} $str {\\&} str
return [subst -nocommand $str]
}
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
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
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
|
#
proc wappInt-url-encode {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
#
proc wappInt-handle-cgi-request {} {
global wapp env wappInt-cgi
foreach key {
CONTENT_LENGTH
CONTENT_TYPE
HTTP_COOKIE
HTTP_HOST
HTTP_REFERER
HTTP_USER_AGENT
PATH_INFO
QUERY_STRING
REMOTE_ADDR
REQUEST_METHOD
REQUEST_URI
REMOTE_USER
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]
}
wappInt-handle-request stdout 1
}
|