|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package require Tcl 8.6- |
|
|
|
|
|
package provide http 2.9.0 |
|
|
|
namespace eval http { |
|
|
|
|
|
variable http |
|
if {![info exists http]} { |
|
array set http { |
|
-accept */* |
|
-pipeline 1 |
|
-postfresh 0 |
|
-proxyhost {} |
|
-proxyport {} |
|
-proxyfilter http::ProxyRequired |
|
-repost 0 |
|
-urlencoding utf-8 |
|
-zip 1 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
if {[interp issafe]} { |
|
set http(-useragent) "Mozilla/5.0\ |
|
(Windows; U;\ |
|
Windows NT 10.0)\ |
|
http/[package provide http] Tcl/[package provide Tcl]" |
|
} else { |
|
set http(-useragent) "Mozilla/5.0\ |
|
([string totitle $::tcl_platform(platform)]; U;\ |
|
$::tcl_platform(os) $::tcl_platform(osVersion))\ |
|
http/[package provide http] Tcl/[package provide Tcl]" |
|
} |
|
} |
|
|
|
proc init {} { |
|
|
|
|
|
|
|
|
|
|
|
for {set i 0} {$i <= 256} {incr i} { |
|
set c [format %c $i] |
|
if {![string match {[-._~a-zA-Z0-9]} $c]} { |
|
set map($c) %[format %.2X $i] |
|
} |
|
} |
|
|
|
set map(\n) %0D%0A |
|
variable formMap [array get map] |
|
|
|
|
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
if {[info exists socketMapping]} { |
|
|
|
foreach {url sock} [array get socketMapping] { |
|
unset -nocomplain socketClosing($url) |
|
unset -nocomplain socketPlayCmd($url) |
|
CloseSocket $sock |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
array unset socketMapping |
|
array unset socketRdState |
|
array unset socketWrState |
|
array unset socketRdQueue |
|
array unset socketWrQueue |
|
array unset socketClosing |
|
array unset socketPlayCmd |
|
array set socketMapping {} |
|
array set socketRdState {} |
|
array set socketWrState {} |
|
array set socketRdQueue {} |
|
array set socketWrQueue {} |
|
array set socketClosing {} |
|
array set socketPlayCmd {} |
|
} |
|
init |
|
|
|
variable urlTypes |
|
if {![info exists urlTypes]} { |
|
set urlTypes(http) [list 80 ::socket] |
|
} |
|
|
|
variable encodings [string tolower [encoding names]] |
|
|
|
variable defaultCharset |
|
if {![info exists defaultCharset]} { |
|
set defaultCharset "iso8859-1" |
|
} |
|
|
|
|
|
variable strict |
|
if {![info exists strict]} { |
|
set strict 1 |
|
} |
|
|
|
|
|
variable defaultKeepalive |
|
if {![info exists defaultKeepalive]} { |
|
set defaultKeepalive 0 |
|
} |
|
|
|
namespace export geturl config reset wait formatQuery quoteString |
|
namespace export register unregister registerError |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if {[info command http::Log] eq {}} {proc http::Log {args} {}} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::register {proto port command} { |
|
variable urlTypes |
|
set urlTypes([string tolower $proto]) [list $port $command] |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::unregister {proto} { |
|
variable urlTypes |
|
set lower [string tolower $proto] |
|
if {![info exists urlTypes($lower)]} { |
|
return -code error "unsupported url type \"$proto\"" |
|
} |
|
set old $urlTypes($lower) |
|
unset urlTypes($lower) |
|
return $old |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::config {args} { |
|
variable http |
|
set options [lsort [array names http -*]] |
|
set usage [join $options ", "] |
|
if {[llength $args] == 0} { |
|
set result {} |
|
foreach name $options { |
|
lappend result $name $http($name) |
|
} |
|
return $result |
|
} |
|
set options [string map {- ""} $options] |
|
set pat ^-(?:[join $options |])$ |
|
if {[llength $args] == 1} { |
|
set flag [lindex $args 0] |
|
if {![regexp -- $pat $flag]} { |
|
return -code error "Unknown option $flag, must be: $usage" |
|
} |
|
return $http($flag) |
|
} else { |
|
foreach {flag value} $args { |
|
if {![regexp -- $pat $flag]} { |
|
return -code error "Unknown option $flag, must be: $usage" |
|
} |
|
set http($flag) $value |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Finish {token {errormsg ""} {skipCB 0}} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
global errorInfo errorCode |
|
set closeQueue 0 |
|
if {$errormsg ne ""} { |
|
set state(error) [list $errormsg $errorInfo $errorCode] |
|
set state(status) "error" |
|
} |
|
if {[info commands ${token}EventCoroutine] ne {}} { |
|
rename ${token}EventCoroutine {} |
|
} |
|
if { ($state(status) eq "timeout") |
|
|| ($state(status) eq "error") |
|
|| ($state(status) eq "eof") |
|
|| ([info exists state(-keepalive)] && !$state(-keepalive)) |
|
|| ([info exists state(connection)] && ($state(connection) eq "close")) |
|
} { |
|
set closeQueue 1 |
|
set connId $state(socketinfo) |
|
set sock $state(sock) |
|
CloseSocket $state(sock) $token |
|
} elseif { |
|
([info exists state(-keepalive)] && $state(-keepalive)) |
|
&& ([info exists state(connection)] && ($state(connection) ne "close")) |
|
} { |
|
KeepSocket $token |
|
} |
|
if {[info exists state(after)]} { |
|
after cancel $state(after) |
|
unset state(after) |
|
} |
|
if {[info exists state(-command)] && (!$skipCB) |
|
&& (![info exists state(done-command-cb)])} { |
|
set state(done-command-cb) yes |
|
if {[catch {eval $state(-command) {$token}} err] && $errormsg eq ""} { |
|
set state(error) [list $err $errorInfo $errorCode] |
|
set state(status) error |
|
} |
|
} |
|
|
|
if { $closeQueue |
|
&& [info exists socketMapping($connId)] |
|
&& ($socketMapping($connId) eq $sock) |
|
} { |
|
http::CloseQueuedQueries $connId $token |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::KeepSocket {token} { |
|
variable http |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
|
|
|
|
|
|
|
|
catch {fileevent $state(sock) readable [list http::CheckEof $state(sock)]} |
|
|
|
|
|
|
|
set TEST_EOF 0 |
|
if {$TEST_EOF} { |
|
|
|
|
|
catch {fileevent $state(sock) readable {}} |
|
} |
|
|
|
if { [info exists state(socketinfo)] |
|
&& [info exists socketMapping($state(socketinfo))] |
|
} { |
|
set connId $state(socketinfo) |
|
|
|
set socketRdState($connId) Rready |
|
|
|
if { $state(-pipeline) |
|
&& [info exists socketRdQueue($connId)] |
|
&& [llength $socketRdQueue($connId)] |
|
} { |
|
|
|
|
|
set token3 [lindex $socketRdQueue($connId) 0] |
|
set socketRdQueue($connId) [lrange $socketRdQueue($connId) 1 end] |
|
variable $token3 |
|
upvar 0 $token3 state3 |
|
set tk2 [namespace tail $token3] |
|
|
|
|
|
set socketRdState($connId) $token3 |
|
ReceiveResponse $token3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
} elseif { |
|
$state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "peNding") |
|
|
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& (![set token3 [lindex $socketWrQueue($connId) 0] |
|
set ${token3}(-pipeline) |
|
] |
|
) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
variable $token3 |
|
set conn [set ${token3}(tmpConnArgs)] |
|
|
|
set socketRdState($connId) $token3 |
|
set socketWrState($connId) $token3 |
|
set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] |
|
|
|
fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] |
|
|
|
|
|
} elseif { |
|
$state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "peNding") |
|
|
|
} { |
|
|
|
|
|
|
|
|
|
|
|
Log ^X$tk <<<<< Error in queueing of requests >>>>> - token $token |
|
|
|
} elseif { |
|
$state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "Wready") |
|
|
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& (![set token3 [lindex $socketWrQueue($connId) 0] |
|
set ${token3}(-pipeline) |
|
] |
|
) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
variable $token3 |
|
set conn [set ${token3}(tmpConnArgs)] |
|
|
|
set socketRdState($connId) $token3 |
|
set socketWrState($connId) $token3 |
|
set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] |
|
|
|
fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] |
|
|
|
|
|
} elseif { |
|
(!$state(-pipeline)) |
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& ($state(connection) ne "close") |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
set token3 [lindex $socketWrQueue($connId) 0] |
|
variable $token3 |
|
set conn [set ${token3}(tmpConnArgs)] |
|
|
|
set socketRdState($connId) $token3 |
|
set socketWrState($connId) $token3 |
|
set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] |
|
|
|
fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] |
|
|
|
|
|
} elseif {(!$state(-pipeline))} { |
|
set socketWrState($connId) Wready |
|
|
|
} |
|
|
|
} else { |
|
CloseSocket $state(sock) $token |
|
|
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CheckEof {sock} { |
|
set junk [read $sock] |
|
set n [string length $junk] |
|
if {$n} { |
|
Log "WARNING: $n bytes received but no HTTP request sent" |
|
} |
|
|
|
if {[catch {eof $sock} res] || $res} { |
|
|
|
|
|
|
|
CloseSocket $sock |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CloseSocket {s {token {}}} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
set tk [namespace tail $token] |
|
|
|
catch {fileevent $s readable {}} |
|
set connId {} |
|
if {$token ne ""} { |
|
variable $token |
|
upvar 0 $token state |
|
if {[info exists state(socketinfo)]} { |
|
set connId $state(socketinfo) |
|
} |
|
} else { |
|
set map [array get socketMapping] |
|
set ndx [lsearch -exact $map $s] |
|
if {$ndx != -1} { |
|
incr ndx -1 |
|
set connId [lindex $map $ndx] |
|
} |
|
} |
|
if { ($connId ne {}) |
|
&& [info exists socketMapping($connId)] |
|
&& ($socketMapping($connId) eq $s) |
|
} { |
|
Log "Closing connection $connId (sock $socketMapping($connId))" |
|
if {[catch {close $socketMapping($connId)} err]} { |
|
Log "Error closing connection: $err" |
|
} |
|
if {$token eq {}} { |
|
|
|
|
|
http::CloseQueuedQueries $connId |
|
} |
|
} else { |
|
Log "Closing socket $s (no connection info)" |
|
if {[catch {close $s} err]} { |
|
Log "Error closing socket: $err" |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CloseQueuedQueries {connId {token {}}} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
if {![info exists socketMapping($connId)]} { |
|
|
|
|
|
return |
|
} |
|
|
|
|
|
if {$token eq {}} { |
|
set tk {} |
|
} else { |
|
set tk [namespace tail $token] |
|
} |
|
|
|
if { [info exists socketPlayCmd($connId)] |
|
&& ($socketPlayCmd($connId) ne {ReplayIfClose Wready {} {}}) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set unfinished $socketPlayCmd($connId) |
|
set socketRdQueue($connId) {} |
|
set socketWrQueue($connId) {} |
|
} else { |
|
set unfinished {} |
|
} |
|
|
|
Unset $connId |
|
|
|
if {$unfinished ne {}} { |
|
Log ^R$tk Any unfinished transactions (excluding $token) failed \ |
|
- token $token |
|
{*}$unfinished |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Unset {connId} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
unset socketMapping($connId) |
|
unset socketRdState($connId) |
|
unset socketWrState($connId) |
|
unset -nocomplain socketRdQueue($connId) |
|
unset -nocomplain socketWrQueue($connId) |
|
unset -nocomplain socketClosing($connId) |
|
unset -nocomplain socketPlayCmd($connId) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::reset {token {why reset}} { |
|
variable $token |
|
upvar 0 $token state |
|
set state(status) $why |
|
catch {fileevent $state(sock) readable {}} |
|
catch {fileevent $state(sock) writable {}} |
|
Finish $token |
|
if {[info exists state(error)]} { |
|
set errorlist $state(error) |
|
unset state |
|
eval ::error $errorlist |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::geturl {url args} { |
|
variable http |
|
variable urlTypes |
|
variable defaultCharset |
|
variable defaultKeepalive |
|
variable strict |
|
|
|
|
|
|
|
|
|
if {![info exists http(uid)]} { |
|
set http(uid) 0 |
|
} |
|
set token [namespace current]::[incr http(uid)] |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
reset $token |
|
Log ^A$tk URL $url - token $token |
|
|
|
|
|
|
|
array set state { |
|
-binary false |
|
-blocksize 8192 |
|
-queryblocksize 8192 |
|
-validate 0 |
|
-headers {} |
|
-timeout 0 |
|
-type application/x-www-form-urlencoded |
|
-queryprogress {} |
|
-protocol 1.1 |
|
binary 0 |
|
state created |
|
meta {} |
|
method {} |
|
coding {} |
|
currentsize 0 |
|
totalsize 0 |
|
querylength 0 |
|
queryoffset 0 |
|
type text/html |
|
body {} |
|
status "" |
|
http "" |
|
connection close |
|
} |
|
set state(-keepalive) $defaultKeepalive |
|
set state(-strict) $strict |
|
|
|
array set type { |
|
-binary boolean |
|
-blocksize integer |
|
-queryblocksize integer |
|
-strict boolean |
|
-timeout integer |
|
-validate boolean |
|
} |
|
set state(charset) $defaultCharset |
|
set options { |
|
-binary -blocksize -channel -command -handler -headers -keepalive |
|
-method -myaddr -progress -protocol -query -queryblocksize |
|
-querychannel -queryprogress -strict -timeout -type -validate |
|
} |
|
set usage [join [lsort $options] ", "] |
|
set options [string map {- ""} $options] |
|
set pat ^-(?:[join $options |])$ |
|
foreach {flag value} $args { |
|
if {[regexp -- $pat $flag]} { |
|
|
|
if { |
|
[info exists type($flag)] && |
|
![string is $type($flag) -strict $value] |
|
} { |
|
unset $token |
|
return -code error \ |
|
"Bad value for $flag ($value), must be $type($flag)" |
|
} |
|
set state($flag) $value |
|
} else { |
|
unset $token |
|
return -code error "Unknown option $flag, can be: $usage" |
|
} |
|
} |
|
|
|
|
|
|
|
set isQueryChannel [info exists state(-querychannel)] |
|
set isQuery [info exists state(-query)] |
|
if {$isQuery && $isQueryChannel} { |
|
unset $token |
|
return -code error "Can't combine -query and -querychannel options!" |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set URLmatcher {(?x) # this is _expanded_ syntax |
|
^ |
|
(?: (\w+) : ) ? # <protocol scheme> |
|
(?: // |
|
(?: |
|
( |
|
[^@/\#?]+ # <userinfo part of authority> |
|
) @ |
|
)? |
|
( # <host part of authority> |
|
[^/:\#?]+ | # host name or IPv4 address |
|
\[ [^/\#?]+ \] # IPv6 address in square brackets |
|
) |
|
(?: : (\d+) )? # <port part of authority> |
|
)? |
|
( [/\?] [^\#]*)? # <path> (including query) |
|
(?: \# (.*) )? # <fragment> |
|
$ |
|
} |
|
|
|
|
|
if {![regexp -- $URLmatcher $url -> proto user host port srvurl]} { |
|
unset $token |
|
return -code error "Unsupported URL: $url" |
|
} |
|
|
|
set host [string trim $host {[]}] |
|
if {$host eq ""} { |
|
|
|
|
|
unset $token |
|
return -code error "Missing host part: $url" |
|
|
|
|
|
} |
|
if {$port ne "" && $port > 65535} { |
|
unset $token |
|
return -code error "Invalid port number: $port" |
|
} |
|
|
|
|
|
if {$user ne ""} { |
|
|
|
set validityRE {(?xi) |
|
^ |
|
(?: [-\w.~!$&'()*+,;=:] | %[0-9a-f][0-9a-f] )+ |
|
$ |
|
} |
|
if {$state(-strict) && ![regexp -- $validityRE $user]} { |
|
unset $token |
|
|
|
if {[regexp {(?i)%(?![0-9a-f][0-9a-f]).?.?} $user bad]} { |
|
return -code error \ |
|
"Illegal encoding character usage \"$bad\" in URL user" |
|
} |
|
return -code error "Illegal characters in URL user" |
|
} |
|
} |
|
if {$srvurl ne ""} { |
|
|
|
|
|
|
|
if {[string index $srvurl 0] ne "/"} { |
|
set srvurl /$srvurl |
|
} |
|
|
|
set validityRE {(?xi) |
|
^ |
|
|
|
(?: [-\w.~!$&'()*+,;=:@/] | %[0-9a-f][0-9a-f] )* |
|
|
|
(?: \? (?: [-\w.~!$&'()*+,;=:@/?] | %[0-9a-f][0-9a-f] )* )? |
|
$ |
|
} |
|
if {$state(-strict) && ![regexp -- $validityRE $srvurl]} { |
|
unset $token |
|
|
|
if {[regexp {(?i)%(?![0-9a-f][0-9a-f])..} $srvurl bad]} { |
|
return -code error \ |
|
"Illegal encoding character usage \"$bad\" in URL path" |
|
} |
|
return -code error "Illegal characters in URL path" |
|
} |
|
} else { |
|
set srvurl / |
|
} |
|
if {$proto eq ""} { |
|
set proto http |
|
} |
|
set lower [string tolower $proto] |
|
if {![info exists urlTypes($lower)]} { |
|
unset $token |
|
return -code error "Unsupported URL type \"$proto\"" |
|
} |
|
set defport [lindex $urlTypes($lower) 0] |
|
set defcmd [lindex $urlTypes($lower) 1] |
|
|
|
if {$port eq ""} { |
|
set port $defport |
|
} |
|
if {![catch {$http(-proxyfilter) $host} proxy]} { |
|
set phost [lindex $proxy 0] |
|
set pport [lindex $proxy 1] |
|
} |
|
|
|
|
|
set url ${proto}:// |
|
if {$user ne ""} { |
|
append url $user |
|
append url @ |
|
} |
|
append url $host |
|
if {$port != $defport} { |
|
append url : $port |
|
} |
|
append url $srvurl |
|
|
|
set state(url) $url |
|
|
|
set sockopts [list -async] |
|
|
|
|
|
|
|
|
|
if {[info exists phost] && ($phost ne "")} { |
|
set srvurl $url |
|
set targetAddr [list $phost $pport] |
|
} else { |
|
set targetAddr [list $host $port] |
|
} |
|
|
|
set state(socketinfo) $host:$port |
|
|
|
|
|
|
|
set state(accept-types) $http(-accept) |
|
|
|
if {$isQuery || $isQueryChannel} { |
|
|
|
|
|
|
|
|
|
if {$http(-postfresh)} { |
|
|
|
|
|
set state(-keepalive) 0 |
|
} else { |
|
|
|
|
|
|
|
set state(-pipeline) 0 |
|
} |
|
} else { |
|
|
|
set state(-pipeline) $http(-pipeline) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set reusing 0 |
|
set alreadyQueued 0 |
|
if {$state(-keepalive)} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
if {[info exists socketMapping($state(socketinfo))]} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if { [info exists socketClosing($state(socketinfo))] |
|
&& $socketClosing($state(socketinfo)) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set reusing 1 |
|
set sock $socketMapping($state(socketinfo)) |
|
Log "reusing socket $sock for $state(socketinfo) - token $token" |
|
|
|
set alreadyQueued 1 |
|
lassign $socketPlayCmd($state(socketinfo)) com0 com1 com2 com3 |
|
lappend com3 $token |
|
set socketPlayCmd($state(socketinfo)) [list $com0 $com1 $com2 $com3] |
|
lappend socketWrQueue($state(socketinfo)) $token |
|
} elseif {[catch {fconfigure $socketMapping($state(socketinfo))}]} { |
|
|
|
|
|
|
|
Log "WARNING: socket for $state(socketinfo) was closed\ |
|
- token $token" |
|
Log "WARNING - if testing, pay special attention to this\ |
|
case (GH) which is seldom executed - token $token" |
|
|
|
|
|
|
|
Unset $state(socketinfo) |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
set reusing 1 |
|
set sock $socketMapping($state(socketinfo)) |
|
Log "reusing socket $sock for $state(socketinfo) - token $token" |
|
|
|
} |
|
|
|
set state(connection) {} |
|
} |
|
} |
|
|
|
if {$reusing} { |
|
|
|
|
|
set state(tmpState) [array get state] |
|
|
|
|
|
if {[info exists state(-myaddr)]} { |
|
lappend sockopts -myaddr $state(-myaddr) |
|
} |
|
|
|
set state(tmpOpenCmd) [list {*}$defcmd {*}$sockopts {*}$targetAddr] |
|
} |
|
|
|
set state(reusing) $reusing |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if {(!$state(reusing)) && ($state(-timeout) > 0)} { |
|
set state(after) [after $state(-timeout) \ |
|
[list http::reset $token timeout]] |
|
} |
|
|
|
if {![info exists sock]} { |
|
|
|
if {[info exists state(-myaddr)]} { |
|
lappend sockopts -myaddr $state(-myaddr) |
|
} |
|
set pre [clock milliseconds] |
|
|
|
|
|
if {[catch {eval $defcmd $sockopts $targetAddr} sock errdict]} { |
|
|
|
|
|
|
|
|
|
|
|
set state(sock) NONE |
|
Finish $token $sock 1 |
|
cleanup $token |
|
dict unset errdict -level |
|
return -options $errdict $sock |
|
} else { |
|
|
|
|
|
|
|
set delay [expr {[clock milliseconds] - $pre}] |
|
if {$delay > 3000} { |
|
Log socket delay $delay - token $token |
|
} |
|
fconfigure $sock -translation {auto crlf} \ |
|
-buffersize $state(-blocksize) |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
set state(sock) $sock |
|
Log "Using $sock for $state(socketinfo) - token $token" \ |
|
[expr {$state(-keepalive)?"keepalive":""}] |
|
|
|
if { $state(-keepalive) |
|
&& (![info exists socketMapping($state(socketinfo))]) |
|
} { |
|
|
|
set socketMapping($state(socketinfo)) $sock |
|
|
|
if {![info exists socketRdState($state(socketinfo))]} { |
|
set socketRdState($state(socketinfo)) {} |
|
set varName ::http::socketRdState($state(socketinfo)) |
|
trace add variable $varName unset ::http::CancelReadPipeline |
|
} |
|
if {![info exists socketWrState($state(socketinfo))]} { |
|
set socketWrState($state(socketinfo)) {} |
|
set varName ::http::socketWrState($state(socketinfo)) |
|
trace add variable $varName unset ::http::CancelWritePipeline |
|
} |
|
|
|
if {$state(-pipeline)} { |
|
|
|
|
|
set socketRdState($state(socketinfo)) $token |
|
set socketWrState($state(socketinfo)) $token |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
set socketRdState($state(socketinfo)) $token |
|
set socketWrState($state(socketinfo)) $token |
|
} |
|
|
|
set socketRdQueue($state(socketinfo)) {} |
|
set socketWrQueue($state(socketinfo)) {} |
|
set socketClosing($state(socketinfo)) 0 |
|
set socketPlayCmd($state(socketinfo)) {ReplayIfClose Wready {} {}} |
|
} |
|
|
|
if {![info exists phost]} { |
|
set phost "" |
|
} |
|
if {$reusing} { |
|
|
|
|
|
set state(tmpConnArgs) [list $proto $phost $srvurl] |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if {$alreadyQueued} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} elseif { $reusing |
|
&& $state(-pipeline) |
|
&& ($socketWrState($state(socketinfo)) ne "Wready") |
|
} { |
|
|
|
lappend socketWrQueue($state(socketinfo)) $token |
|
|
|
} elseif { $reusing |
|
&& (!$state(-pipeline)) |
|
&& ($socketWrState($state(socketinfo)) ne "Wready") |
|
} { |
|
|
|
|
|
lappend socketWrQueue($state(socketinfo)) $token |
|
|
|
} elseif { $reusing |
|
&& (!$state(-pipeline)) |
|
&& ($socketWrState($state(socketinfo)) eq "Wready") |
|
&& ($socketRdState($state(socketinfo)) ne "Rready") |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
set socketWrState($state(socketinfo)) peNding |
|
lappend socketWrQueue($state(socketinfo)) $token |
|
|
|
} else { |
|
if {$reusing && $state(-pipeline)} { |
|
|
|
set socketWrState($state(socketinfo)) $token |
|
|
|
} elseif {$reusing} { |
|
|
|
|
|
set socketRdState($state(socketinfo)) $token |
|
set socketWrState($state(socketinfo)) $token |
|
} |
|
|
|
|
|
|
|
|
|
|
|
fileevent $sock writable \ |
|
[list http::Connect $token $proto $phost $srvurl] |
|
} |
|
|
|
|
|
if {![info exists state(-command)]} { |
|
|
|
|
|
http::wait $token |
|
|
|
if {![info exists state]} { |
|
|
|
|
|
|
|
return $token |
|
} elseif {$state(status) eq "error"} { |
|
|
|
|
|
|
|
|
|
set err [lindex $state(error) 0] |
|
cleanup $token |
|
return -code error $err |
|
} |
|
} |
|
|
|
return $token |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Connected {token proto phost srvurl} { |
|
variable http |
|
variable urlTypes |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
|
|
if {$state(reusing) && (!$state(-pipeline)) && ($state(-timeout) > 0)} { |
|
set state(after) [after $state(-timeout) \ |
|
[list http::reset $token timeout]] |
|
} |
|
|
|
|
|
set sock $state(sock) |
|
set isQueryChannel [info exists state(-querychannel)] |
|
set isQuery [info exists state(-query)] |
|
set host [lindex [split $state(socketinfo) :] 0] |
|
set port [lindex [split $state(socketinfo) :] 1] |
|
|
|
set lower [string tolower $proto] |
|
set defport [lindex $urlTypes($lower) 0] |
|
|
|
|
|
|
|
|
|
lassign [fconfigure $sock -translation] trRead trWrite |
|
fconfigure $sock -translation [list $trRead crlf] \ |
|
-buffersize $state(-blocksize) |
|
|
|
|
|
|
|
|
|
catch {fconfigure $sock -blocking off} |
|
set how GET |
|
if {$isQuery} { |
|
set state(querylength) [string length $state(-query)] |
|
if {$state(querylength) > 0} { |
|
set how POST |
|
set contDone 0 |
|
} else { |
|
|
|
unset state(-query) |
|
set isQuery 0 |
|
} |
|
} elseif {$state(-validate)} { |
|
set how HEAD |
|
} elseif {$isQueryChannel} { |
|
set how POST |
|
|
|
|
|
lassign [fconfigure $sock -translation] trRead trWrite |
|
fconfigure $state(-querychannel) -blocking 1 \ |
|
-translation [list $trRead binary] |
|
set contDone 0 |
|
} |
|
if {[info exists state(-method)] && ($state(-method) ne "")} { |
|
set how $state(-method) |
|
} |
|
|
|
|
|
if {[info exists state(-handler)]} { |
|
set state(-protocol) 1.0 |
|
} |
|
set accept_types_seen 0 |
|
|
|
Log ^B$tk begin sending request - token $token |
|
|
|
if {[catch { |
|
set state(method) $how |
|
puts $sock "$how $srvurl HTTP/$state(-protocol)" |
|
if {[dict exists $state(-headers) Host]} { |
|
|
|
puts $sock "Host: [dict get $state(-headers) Host]" |
|
} elseif {$port == $defport} { |
|
|
|
|
|
puts $sock "Host: $host" |
|
} else { |
|
puts $sock "Host: $host:$port" |
|
} |
|
puts $sock "User-Agent: $http(-useragent)" |
|
if {($state(-protocol) >= 1.0) && $state(-keepalive)} { |
|
|
|
|
|
puts $sock "Connection: keep-alive" |
|
} |
|
if {($state(-protocol) > 1.0) && !$state(-keepalive)} { |
|
puts $sock "Connection: close" |
|
} |
|
if {[info exists phost] && ($phost ne "") && $state(-keepalive)} { |
|
puts $sock "Proxy-Connection: Keep-Alive" |
|
} |
|
set accept_encoding_seen 0 |
|
set content_type_seen 0 |
|
dict for {key value} $state(-headers) { |
|
set value [string map [list \n "" \r ""] $value] |
|
set key [string map {" " -} [string trim $key]] |
|
if {[string equal -nocase $key "host"]} { |
|
continue |
|
} |
|
if {[string equal -nocase $key "accept-encoding"]} { |
|
set accept_encoding_seen 1 |
|
} |
|
if {[string equal -nocase $key "accept"]} { |
|
set accept_types_seen 1 |
|
} |
|
if {[string equal -nocase $key "content-type"]} { |
|
set content_type_seen 1 |
|
} |
|
if {[string equal -nocase $key "content-length"]} { |
|
set contDone 1 |
|
set state(querylength) $value |
|
} |
|
if {[string length $key]} { |
|
puts $sock "$key: $value" |
|
} |
|
} |
|
|
|
|
|
if {!$accept_types_seen} { |
|
puts $sock "Accept: $state(accept-types)" |
|
} |
|
if { (!$accept_encoding_seen) |
|
&& (![info exists state(-handler)]) |
|
&& $http(-zip) |
|
} { |
|
puts $sock "Accept-Encoding: gzip,deflate,compress" |
|
} |
|
if {$isQueryChannel && ($state(querylength) == 0)} { |
|
|
|
|
|
|
|
set start [tell $state(-querychannel)] |
|
seek $state(-querychannel) 0 end |
|
set state(querylength) \ |
|
[expr {[tell $state(-querychannel)] - $start}] |
|
seek $state(-querychannel) $start |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if {$isQuery || $isQueryChannel} { |
|
|
|
if {!$content_type_seen} { |
|
puts $sock "Content-Type: $state(-type)" |
|
} |
|
if {!$contDone} { |
|
puts $sock "Content-Length: $state(querylength)" |
|
} |
|
puts $sock "" |
|
flush $sock |
|
|
|
|
|
|
|
|
|
lassign [fconfigure $sock -translation] trRead trWrite |
|
fconfigure $sock -translation [list $trRead binary] |
|
fileevent $sock writable [list http::Write $token] |
|
|
|
|
|
} else { |
|
|
|
if { (![catch {fileevent $sock readable} binding]) |
|
&& ($binding eq [list http::CheckEof $sock]) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
fileevent $sock readable {} |
|
} |
|
puts $sock "" |
|
flush $sock |
|
Log ^C$tk end sending request - token $token |
|
|
|
|
|
DoneRequest $token |
|
} |
|
|
|
} err]} { |
|
|
|
|
|
|
|
Log "WARNING - if testing, pay special attention to this\ |
|
case (GI) which is seldom executed - token $token" |
|
if {[info exists state(reusing)] && $state(reusing)} { |
|
|
|
|
|
if {[TestForReplay $token write $err a]} { |
|
return |
|
} else { |
|
Finish $token {failed to re-use socket} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
} elseif {$state(status) eq ""} { |
|
|
|
set msg [registerError $sock] |
|
registerError $sock {} |
|
if {$msg eq {}} { |
|
set msg {failed to use socket} |
|
} |
|
Finish $token $msg |
|
} elseif {$state(status) ne "error"} { |
|
Finish $token $err |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::registerError {sock args} { |
|
variable registeredErrors |
|
|
|
if { ([llength $args] == 0) |
|
&& (![info exists registeredErrors($sock)]) |
|
} { |
|
return |
|
} elseif { ([llength $args] == 1) |
|
&& ([lindex $args 0] eq {}) |
|
} { |
|
unset -nocomplain registeredErrors($sock) |
|
return |
|
} |
|
set registeredErrors($sock) {*}$args |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::DoneRequest {token} { |
|
variable http |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
set sock $state(sock) |
|
|
|
|
|
if {$state(reusing) && $state(-pipeline)} { |
|
|
|
|
|
|
|
|
|
|
|
set socketWrState($state(socketinfo)) Wready |
|
|
|
|
|
http::NextPipelinedWrite $token |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if { $state(-keepalive) |
|
&& $state(-pipeline) |
|
&& [info exists socketRdState($state(socketinfo))] |
|
&& ($socketRdState($state(socketinfo)) eq "Rready") |
|
} { |
|
|
|
set socketRdState($state(socketinfo)) $token |
|
} |
|
|
|
if { $state(-keepalive) |
|
&& $state(-pipeline) |
|
&& [info exists socketRdState($state(socketinfo))] |
|
&& ($socketRdState($state(socketinfo)) ne $token) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lappend socketRdQueue($state(socketinfo)) $token |
|
} else { |
|
|
|
|
|
|
|
ReceiveResponse $token |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
proc http::ReceiveResponse {token} { |
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
set sock $state(sock) |
|
|
|
|
|
lassign [fconfigure $sock -translation] trRead trWrite |
|
fconfigure $sock -translation [list auto $trWrite] \ |
|
-buffersize $state(-blocksize) |
|
Log ^D$tk begin receiving response - token $token |
|
|
|
coroutine ${token}EventCoroutine http::Event $sock $token |
|
fileevent $sock readable ${token}EventCoroutine |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::NextPipelinedWrite {token} { |
|
variable http |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable $token |
|
upvar 0 $token state |
|
set connId $state(socketinfo) |
|
|
|
if { [info exists socketClosing($connId)] |
|
&& $socketClosing($connId) |
|
} { |
|
|
|
|
|
|
|
} elseif { $state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "Wready") |
|
|
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& ([set token2 [lindex $socketWrQueue($connId) 0] |
|
set ${token2}(-pipeline) |
|
] |
|
) |
|
} { |
|
|
|
|
|
set conn [set ${token2}(tmpConnArgs)] |
|
set socketWrState($connId) $token2 |
|
set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] |
|
|
|
fileevent $state(sock) writable [list http::Connect $token2 {*}$conn] |
|
|
|
|
|
|
|
} elseif { $state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "Wready") |
|
|
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& (![ set token3 [lindex $socketWrQueue($connId) 0] |
|
set ${token3}(-pipeline) |
|
] |
|
) |
|
|
|
&& [info exists socketRdState($connId)] |
|
&& ($socketRdState($connId) eq "Rready") |
|
} { |
|
|
|
|
|
|
|
variable $token3 |
|
upvar 0 $token3 state3 |
|
set conn [set ${token3}(tmpConnArgs)] |
|
|
|
set socketRdState($connId) $token3 |
|
set socketWrState($connId) $token3 |
|
set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] |
|
|
|
fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] |
|
|
|
|
|
} elseif { $state(-pipeline) |
|
&& [info exists socketWrState($connId)] |
|
&& ($socketWrState($connId) eq "Wready") |
|
|
|
&& [info exists socketWrQueue($connId)] |
|
&& [llength $socketWrQueue($connId)] |
|
&& (![set token2 [lindex $socketWrQueue($connId) 0] |
|
set ${token2}(-pipeline) |
|
] |
|
) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set socketWrState($connId) peNding |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CancelReadPipeline {name1 connId op} { |
|
variable socketRdQueue |
|
|
|
if {[info exists socketRdQueue($connId)]} { |
|
set msg {the connection was closed by CancelReadPipeline} |
|
foreach token $socketRdQueue($connId) { |
|
set tk [namespace tail $token] |
|
Log ^X$tk end of response "($msg)" - token $token |
|
set ${token}(status) eof |
|
Finish $token |
|
} |
|
set socketRdQueue($connId) {} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CancelWritePipeline {name1 connId op} { |
|
variable socketWrQueue |
|
|
|
|
|
if {[info exists socketWrQueue($connId)]} { |
|
set msg {the connection was closed by CancelWritePipeline} |
|
foreach token $socketWrQueue($connId) { |
|
set tk [namespace tail $token] |
|
Log ^X$tk end of response "($msg)" - token $token |
|
set ${token}(status) eof |
|
Finish $token |
|
} |
|
set socketWrQueue($connId) {} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::ReplayIfDead {tokenArg doing} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $tokenArg |
|
upvar 0 $tokenArg stateArg |
|
|
|
Log running http::ReplayIfDead for $tokenArg $doing |
|
|
|
|
|
|
|
|
|
set InFlightR {} |
|
set InFlightW {} |
|
|
|
|
|
if {$stateArg(-pipeline)} { |
|
|
|
|
|
|
|
|
|
|
|
|
|
if { [info exists socketRdState($stateArg(socketinfo))] |
|
&& ($socketRdState($stateArg(socketinfo)) ne "Rready") |
|
} { |
|
lappend InFlightR $socketRdState($stateArg(socketinfo)) |
|
} elseif {($doing eq "read")} { |
|
lappend InFlightR $tokenArg |
|
} |
|
|
|
if { [info exists socketWrState($stateArg(socketinfo))] |
|
&& $socketWrState($stateArg(socketinfo)) ni {Wready peNding} |
|
} { |
|
lappend InFlightW $socketWrState($stateArg(socketinfo)) |
|
} elseif {($doing eq "write")} { |
|
lappend InFlightW $tokenArg |
|
} |
|
|
|
|
|
if { ($doing eq "read") |
|
&& [info exists socketRdState($stateArg(socketinfo))] |
|
&& ($tokenArg ne $socketRdState($stateArg(socketinfo))) |
|
} { |
|
Log WARNING - ReplayIfDead pipelined tokenArg $tokenArg $doing \ |
|
ne socketRdState($stateArg(socketinfo)) \ |
|
$socketRdState($stateArg(socketinfo)) |
|
|
|
} elseif { |
|
($doing eq "write") |
|
&& [info exists socketWrState($stateArg(socketinfo))] |
|
&& ($tokenArg ne $socketWrState($stateArg(socketinfo))) |
|
} { |
|
Log WARNING - ReplayIfDead pipelined tokenArg $tokenArg $doing \ |
|
ne socketWrState($stateArg(socketinfo)) \ |
|
$socketWrState($stateArg(socketinfo)) |
|
} |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
if {$tokenArg ne $socketRdState($stateArg(socketinfo))} { |
|
Log WARNING - ReplayIfDead nonpipeline tokenArg $tokenArg $doing \ |
|
ne socketRdState($stateArg(socketinfo)) \ |
|
$socketRdState($stateArg(socketinfo)) |
|
} |
|
|
|
|
|
if { [info exists socketRdQueue($stateArg(socketinfo))] |
|
&& ($socketRdQueue($stateArg(socketinfo)) ne {}) |
|
} { |
|
Log WARNING - ReplayIfDead nonpipeline tokenArg $tokenArg $doing \ |
|
has read queue socketRdQueue($stateArg(socketinfo)) \ |
|
$socketRdQueue($stateArg(socketinfo)) ne {} |
|
} |
|
|
|
lappend InFlightW $socketRdState($stateArg(socketinfo)) |
|
set socketRdQueue($stateArg(socketinfo)) {} |
|
} |
|
|
|
set newQueue {} |
|
lappend newQueue {*}$InFlightR |
|
lappend newQueue {*}$socketRdQueue($stateArg(socketinfo)) |
|
lappend newQueue {*}$InFlightW |
|
lappend newQueue {*}$socketWrQueue($stateArg(socketinfo)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
catch {close $stateArg(sock)} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ReplayCore $newQueue |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::ReplayIfClose {Wstate Rqueue Wqueue} { |
|
Log running http::ReplayIfClose for $Wstate $Rqueue $Wqueue |
|
|
|
if {$Wstate in $Rqueue || $Wstate in $Wqueue} { |
|
Log WARNING duplicate token in http::ReplayIfClose - token $Wstate |
|
set Wstate Wready |
|
} |
|
|
|
|
|
set InFlightW {} |
|
if {$Wstate ni {Wready peNding}} { |
|
lappend InFlightW $Wstate |
|
} |
|
|
|
set newQueue {} |
|
lappend newQueue {*}$Rqueue |
|
lappend newQueue {*}$InFlightW |
|
lappend newQueue {*}$Wqueue |
|
|
|
|
|
|
|
ReplayCore $newQueue |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::ReInit {token} { |
|
variable $token |
|
upvar 0 $token state |
|
|
|
if {!( |
|
[info exists state(tmpState)] |
|
&& [info exists state(tmpOpenCmd)] |
|
&& [info exists state(tmpConnArgs)] |
|
) |
|
} { |
|
Log FAILED in http::ReInit via ReplayCore - NO tmp vars for $token |
|
return 0 |
|
} |
|
|
|
if {[info exists state(after)]} { |
|
after cancel $state(after) |
|
unset state(after) |
|
} |
|
|
|
|
|
set tmpState $state(tmpState) |
|
set tmpOpenCmd $state(tmpOpenCmd) |
|
set tmpConnArgs $state(tmpConnArgs) |
|
foreach name [array names state] { |
|
if {$name ne "status"} { |
|
unset state($name) |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
dict unset tmpState status |
|
array set state $tmpState |
|
set state(tmpState) $tmpState |
|
set state(tmpOpenCmd) $tmpOpenCmd |
|
set state(tmpConnArgs) $tmpConnArgs |
|
|
|
return 1 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::ReplayCore {newQueue} { |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
if {[llength $newQueue] == 0} { |
|
|
|
return |
|
} |
|
|
|
|
|
set newToken [lindex $newQueue 0] |
|
set newQueue [lrange $newQueue 1 end] |
|
|
|
|
|
|
|
|
|
set token $newToken |
|
variable $token |
|
upvar 0 $token state |
|
|
|
if {![ReInit $token]} { |
|
Log FAILED in http::ReplayCore - NO tmp vars |
|
Finish $token {cannot send this request again} |
|
return |
|
} |
|
|
|
set tmpState $state(tmpState) |
|
set tmpOpenCmd $state(tmpOpenCmd) |
|
set tmpConnArgs $state(tmpConnArgs) |
|
unset state(tmpState) |
|
unset state(tmpOpenCmd) |
|
unset state(tmpConnArgs) |
|
|
|
set state(reusing) 0 |
|
|
|
if {$state(-timeout) > 0} { |
|
set resetCmd [list http::reset $token timeout] |
|
set state(after) [after $state(-timeout) $resetCmd] |
|
} |
|
|
|
set pre [clock milliseconds] |
|
|
|
|
|
|
|
if {[catch {eval $tmpOpenCmd} sock]} { |
|
|
|
Log FAILED - $sock |
|
set state(sock) NONE |
|
Finish $token $sock |
|
return |
|
} |
|
|
|
set delay [expr {[clock milliseconds] - $pre}] |
|
if {$delay > 3000} { |
|
Log socket delay $delay - token $token |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if {$state(-keepalive)} { |
|
set socketMapping($state(socketinfo)) $sock |
|
|
|
if {![info exists socketRdState($state(socketinfo))]} { |
|
set socketRdState($state(socketinfo)) {} |
|
set varName ::http::socketRdState($state(socketinfo)) |
|
trace add variable $varName unset ::http::CancelReadPipeline |
|
} |
|
|
|
if {![info exists socketWrState($state(socketinfo))]} { |
|
set socketWrState($state(socketinfo)) {} |
|
set varName ::http::socketWrState($state(socketinfo)) |
|
trace add variable $varName unset ::http::CancelWritePipeline |
|
} |
|
|
|
if {$state(-pipeline)} { |
|
|
|
set socketRdState($state(socketinfo)) $token |
|
set socketWrState($state(socketinfo)) $token |
|
} else { |
|
|
|
set socketRdState($state(socketinfo)) $token |
|
set socketWrState($state(socketinfo)) $token |
|
} |
|
|
|
set socketRdQueue($state(socketinfo)) {} |
|
set socketWrQueue($state(socketinfo)) $newQueue |
|
set socketClosing($state(socketinfo)) 0 |
|
set socketPlayCmd($state(socketinfo)) {ReplayIfClose Wready {} {}} |
|
} |
|
|
|
|
|
|
|
foreach tok $newQueue { |
|
if {[ReInit $tok]} { |
|
set ${tok}(reusing) 1 |
|
set ${tok}(sock) $sock |
|
} else { |
|
set ${tok}(reusing) 1 |
|
set ${tok}(sock) NONE |
|
Finish $token {cannot send this request again} |
|
} |
|
} |
|
|
|
|
|
set state(sock) $sock |
|
Log "Using $sock for $state(socketinfo) - token $token" \ |
|
[expr {$state(-keepalive)?"keepalive":""}] |
|
|
|
|
|
|
|
fconfigure $sock -translation {auto crlf} -buffersize $state(-blocksize) |
|
|
|
|
|
|
|
fileevent $sock writable [list http::Connect $token {*}$tmpConnArgs] |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::data {token} { |
|
variable $token |
|
upvar 0 $token state |
|
return $state(body) |
|
} |
|
proc http::status {token} { |
|
if {![info exists $token]} { |
|
return "error" |
|
} |
|
variable $token |
|
upvar 0 $token state |
|
return $state(status) |
|
} |
|
proc http::code {token} { |
|
variable $token |
|
upvar 0 $token state |
|
return $state(http) |
|
} |
|
proc http::ncode {token} { |
|
variable $token |
|
upvar 0 $token state |
|
if {[regexp {[0-9]{3}} $state(http) numeric_code]} { |
|
return $numeric_code |
|
} else { |
|
return $state(http) |
|
} |
|
} |
|
proc http::size {token} { |
|
variable $token |
|
upvar 0 $token state |
|
return $state(currentsize) |
|
} |
|
proc http::meta {token} { |
|
variable $token |
|
upvar 0 $token state |
|
return $state(meta) |
|
} |
|
proc http::error {token} { |
|
variable $token |
|
upvar 0 $token state |
|
if {[info exists state(error)]} { |
|
return $state(error) |
|
} |
|
return "" |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::cleanup {token} { |
|
variable $token |
|
upvar 0 $token state |
|
if {[info commands ${token}EventCoroutine] ne {}} { |
|
rename ${token}EventCoroutine {} |
|
} |
|
if {[info exists state(after)]} { |
|
after cancel $state(after) |
|
unset state(after) |
|
} |
|
if {[info exists state]} { |
|
unset state |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Connect {token proto phost srvurl} { |
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
set err "due to unexpected EOF" |
|
if { |
|
[eof $state(sock)] || |
|
[set err [fconfigure $state(sock) -error]] ne "" |
|
} { |
|
Log "WARNING - if testing, pay special attention to this\ |
|
case (GJ) which is seldom executed - token $token" |
|
if {[info exists state(reusing)] && $state(reusing)} { |
|
|
|
|
|
if {[TestForReplay $token write $err b]} { |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
Finish $token "connect failed $err" |
|
} else { |
|
set state(state) connecting |
|
fileevent $state(sock) writable {} |
|
::http::Connected $token $proto $phost $srvurl |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Write {token} { |
|
variable http |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
set sock $state(sock) |
|
|
|
|
|
set done 0 |
|
if {[catch { |
|
|
|
|
|
if {[info exists state(-query)]} { |
|
|
|
|
|
if { $state(queryoffset) + $state(-queryblocksize) |
|
>= $state(querylength) |
|
} { |
|
|
|
if { (![catch {fileevent $sock readable} binding]) |
|
&& ($binding eq [list http::CheckEof $sock]) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
fileevent $sock readable {} |
|
} |
|
} |
|
puts -nonewline $sock \ |
|
[string range $state(-query) $state(queryoffset) \ |
|
[expr {$state(queryoffset) + $state(-queryblocksize) - 1}]] |
|
incr state(queryoffset) $state(-queryblocksize) |
|
if {$state(queryoffset) >= $state(querylength)} { |
|
set state(queryoffset) $state(querylength) |
|
set done 1 |
|
} |
|
} else { |
|
|
|
|
|
set outStr [read $state(-querychannel) $state(-queryblocksize)] |
|
if {[eof $state(-querychannel)]} { |
|
|
|
if { (![catch {fileevent $sock readable} binding]) |
|
&& ($binding eq [list http::CheckEof $sock]) |
|
} { |
|
|
|
|
|
|
|
|
|
|
|
fileevent $sock readable {} |
|
} |
|
} |
|
puts -nonewline $sock $outStr |
|
incr state(queryoffset) [string length $outStr] |
|
if {[eof $state(-querychannel)]} { |
|
set done 1 |
|
} |
|
} |
|
} err]} { |
|
|
|
|
|
|
|
set state(posterror) $err |
|
set done 1 |
|
} |
|
|
|
if {$done} { |
|
catch {flush $sock} |
|
fileevent $sock writable {} |
|
Log ^C$tk end sending request - token $token |
|
|
|
|
|
DoneRequest $token |
|
} |
|
|
|
|
|
|
|
if {[string length $state(-queryprogress)]} { |
|
eval $state(-queryprogress) \ |
|
[list $token $state(querylength) $state(queryoffset)] |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::Event {sock token} { |
|
variable http |
|
variable socketMapping |
|
variable socketRdState |
|
variable socketWrState |
|
variable socketRdQueue |
|
variable socketWrQueue |
|
variable socketClosing |
|
variable socketPlayCmd |
|
|
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
while 1 { |
|
yield |
|
|
|
|
|
if {![info exists state]} { |
|
Log "Event $sock with invalid token '$token' - remote close?" |
|
if {![eof $sock]} { |
|
if {[set d [read $sock]] ne ""} { |
|
Log "WARNING: additional data left on closed socket\ |
|
- token $token" |
|
} |
|
} |
|
Log ^X$tk end of response (token error) - token $token |
|
CloseSocket $sock |
|
return |
|
} |
|
if {$state(state) eq "connecting"} { |
|
|
|
if { $state(reusing) |
|
&& $state(-pipeline) |
|
&& ($state(-timeout) > 0) |
|
&& (![info exists state(after)]) |
|
} { |
|
set state(after) [after $state(-timeout) \ |
|
[list http::reset $token timeout]] |
|
} |
|
|
|
if {[catch {gets $sock state(http)} nsl]} { |
|
Log "WARNING - if testing, pay special attention to this\ |
|
case (GK) which is seldom executed - token $token" |
|
if {[info exists state(reusing)] && $state(reusing)} { |
|
|
|
|
|
|
|
if {[TestForReplay $token read $nsl c]} { |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
} else { |
|
Log ^X$tk end of response (error) - token $token |
|
Finish $token $nsl |
|
return |
|
} |
|
} elseif {$nsl >= 0} { |
|
|
|
set state(state) "header" |
|
} elseif { [eof $sock] |
|
&& [info exists state(reusing)] |
|
&& $state(reusing) |
|
} { |
|
|
|
|
|
|
|
|
|
if {[TestForReplay $token read {} d]} { |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
} elseif {$state(state) eq "header"} { |
|
if {[catch {gets $sock line} nhl]} { |
|
|
|
Log ^X$tk end of response (error) - token $token |
|
Finish $token $nhl |
|
return |
|
} elseif {$nhl == 0} { |
|
|
|
Log ^E$tk end of response headers - token $token |
|
|
|
|
|
if { ($state(http) == "") |
|
|| ([regexp {^\S+\s(\d+)} $state(http) {} x] && $x == 100) |
|
} { |
|
set state(state) "connecting" |
|
continue |
|
|
|
} |
|
|
|
if { ([info exists state(connection)]) |
|
&& ([info exists socketMapping($state(socketinfo))]) |
|
&& ($state(connection) eq "keep-alive") |
|
&& ($state(-keepalive)) |
|
&& (!$state(reusing)) |
|
&& ($state(-pipeline)) |
|
} { |
|
|
|
|
|
|
|
|
|
set socketWrState($state(socketinfo)) Wready |
|
http::NextPipelinedWrite $token |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if { ([info exists state(connection)]) |
|
&& ([info exists socketMapping($state(socketinfo))]) |
|
&& ($state(connection) eq "close") |
|
&& ($state(-keepalive)) |
|
} { |
|
|
|
|
|
|
|
|
|
if { ($socketRdQueue($state(socketinfo)) ne {}) |
|
|| ($socketWrQueue($state(socketinfo)) ne {}) |
|
|| ($socketWrState($state(socketinfo)) ni |
|
[list Wready peNding $token]) |
|
} { |
|
set InFlightW $socketWrState($state(socketinfo)) |
|
if {$InFlightW in [list Wready peNding $token]} { |
|
set InFlightW Wready |
|
} else { |
|
set msg "token ${InFlightW} is InFlightW" |
|
|
|
} |
|
|
|
set socketPlayCmd($state(socketinfo)) \ |
|
[list ReplayIfClose $InFlightW \ |
|
$socketRdQueue($state(socketinfo)) \ |
|
$socketWrQueue($state(socketinfo))] |
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach tokenVal $socketRdQueue($state(socketinfo)) { |
|
if {[info exists ${tokenVal}(after)]} { |
|
after cancel [set ${tokenVal}(after)] |
|
unset ${tokenVal}(after) |
|
} |
|
} |
|
|
|
} else { |
|
set socketPlayCmd($state(socketinfo)) \ |
|
{ReplayIfClose Wready {} {}} |
|
} |
|
|
|
|
|
set socketClosing($state(socketinfo)) 1 |
|
} |
|
|
|
set state(state) body |
|
|
|
|
|
if {$state(-validate)} { |
|
Log ^F$tk end of response for HEAD request - token $token |
|
set state(state) complete |
|
Eot $token |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if { (!( [info exists state(connection)] |
|
&& ($state(connection) eq "close") |
|
) |
|
) |
|
&& (![info exists state(transfer)]) |
|
&& ($state(totalsize) == 0) |
|
} { |
|
set msg {body size is 0 and no events likely - complete} |
|
Log "$msg - token $token" |
|
set msg {(length unknown, set to 0)} |
|
Log ^F$tk end of response body {*}$msg - token $token |
|
set state(state) complete |
|
Eot $token |
|
return |
|
} |
|
|
|
|
|
lassign [fconfigure $sock -translation] trRead trWrite |
|
fconfigure $sock -translation [list binary $trWrite] |
|
|
|
if { |
|
$state(-binary) || [IsBinaryContentType $state(type)] |
|
} { |
|
|
|
set state(binary) 1 |
|
} |
|
if {[info exists state(-channel)]} { |
|
if {$state(binary) || [llength [ContentEncoding $token]]} { |
|
fconfigure $state(-channel) -translation binary |
|
} |
|
if {![info exists state(-handler)]} { |
|
|
|
fileevent $sock readable {} |
|
rename ${token}EventCoroutine {} |
|
CopyStart $sock $token |
|
return |
|
} |
|
} |
|
} elseif {$nhl > 0} { |
|
|
|
|
|
if {[regexp -nocase {^([^:]+):(.+)$} $line x key value]} { |
|
switch -- [string tolower $key] { |
|
content-type { |
|
set state(type) [string trim [string tolower $value]] |
|
|
|
if {[regexp -nocase \ |
|
{charset\s*=\s*\"((?:[^""]|\\\")*)\"} \ |
|
$state(type) -> cs]} { |
|
set state(charset) [string map {{\"} \"} $cs] |
|
} else { |
|
regexp -nocase {charset\s*=\s*(\S+?);?} \ |
|
$state(type) -> state(charset) |
|
} |
|
} |
|
content-length { |
|
set state(totalsize) [string trim $value] |
|
} |
|
content-encoding { |
|
set state(coding) [string trim $value] |
|
} |
|
transfer-encoding { |
|
set state(transfer) \ |
|
[string trim [string tolower $value]] |
|
} |
|
proxy-connection - |
|
connection { |
|
set state(connection) \ |
|
[string trim [string tolower $value]] |
|
} |
|
} |
|
lappend state(meta) $key [string trim $value] |
|
} |
|
} |
|
} else { |
|
# Now reading body |
|
##Log body - token $token |
|
if {[catch { |
|
if {[info exists state(-handler)]} { |
|
set n [eval $state(-handler) [list $sock $token]] |
|
##Log handler $n - token $token |
|
# N.B. the protocol has been set to 1.0 because the -handler |
|
# logic is not expected to handle chunked encoding. |
|
# FIXME Allow -handler with 1.1 on dechunked stacked chan. |
|
if {$state(totalsize) == 0} { |
|
# We know the transfer is complete only when the server |
|
# closes the connection - i.e. eof is not an error. |
|
set state(state) complete |
|
} |
|
if {![string is integer -strict $n]} { |
|
if 1 { |
|
# Do not tolerate bad -handler - fail with error |
|
# status. |
|
set msg {the -handler command for http::geturl must\ |
|
return an integer (the number of bytes\ |
|
read)} |
|
Log ^X$tk end of response (handler error) -\ |
|
token $token |
|
Eot $token $msg |
|
} else { |
|
# Tolerate the bad -handler, and continue. The |
|
# penalty: |
|
# (a) Because the handler returns nonsense, we know |
|
# the transfer is complete only when the server |
|
# closes the connection - i.e. eof is not an |
|
# error. |
|
# (b) http::size will not be accurate. |
|
# (c) The transaction is already downgraded to 1.0 |
|
# to avoid chunked transfer encoding. It MUST |
|
# also be forced to "Connection: close" or the |
|
# HTTP/1.0 equivalent; or it MUST fail (as |
|
# above) if the server sends |
|
# "Connection: keep-alive" or the HTTP/1.0 |
|
# equivalent. |
|
set n 0 |
|
set state(state) complete |
|
} |
|
} |
|
} elseif {[info exists state(transfer_final)]} { |
|
# This code forgives EOF in place of the final CRLF. |
|
set line [getTextLine $sock] |
|
set n [string length $line] |
|
set state(state) complete |
|
if {$n > 0} { |
|
# - HTTP trailers (late response headers) are permitted |
|
# by Chunked Transfer-Encoding, and can be safely |
|
# ignored. |
|
# - Do not count these bytes in the total received for |
|
# the response body. |
|
Log "trailer of $n bytes after final chunk -\ |
|
token $token" |
|
append state(transfer_final) $line |
|
set n 0 |
|
} else { |
|
Log ^F$tk end of response body (chunked) - token $token |
|
Log "final chunk part - token $token" |
|
Eot $token |
|
} |
|
} elseif { [info exists state(transfer)] |
|
&& ($state(transfer) eq "chunked") |
|
} { |
|
##Log chunked - token $token |
|
set size 0 |
|
set hexLenChunk [getTextLine $sock] |
|
#set ntl [string length $hexLenChunk] |
|
if {[string trim $hexLenChunk] ne ""} { |
|
scan $hexLenChunk %x size |
|
if {$size != 0} { |
|
##Log chunk-measure $size - token $token |
|
set chunk [BlockingRead $sock $size] |
|
set n [string length $chunk] |
|
if {$n >= 0} { |
|
append state(body) $chunk |
|
incr state(log_size) [string length $chunk] |
|
##Log chunk $n cumul $state(log_size) -\ |
|
token $token |
|
} |
|
if {$size != [string length $chunk]} { |
|
Log "WARNING: mis-sized chunk:\ |
|
was [string length $chunk], should be\ |
|
$size - token $token" |
|
set n 0 |
|
set state(connection) close |
|
Log ^X$tk end of response (chunk error) \ |
|
- token $token |
|
set msg {error in chunked encoding - fetch\ |
|
terminated} |
|
Eot $token $msg |
|
} |
|
# CRLF that follows chunk. |
|
# If eof, this is handled at the end of this proc. |
|
getTextLine $sock |
|
} else { |
|
set n 0 |
|
set state(transfer_final) {} |
|
} |
|
} else { |
|
# Line expected to hold chunk length is empty, or eof. |
|
##Log bad-chunk-measure - token $token |
|
set n 0 |
|
set state(connection) close |
|
Log ^X$tk end of response (chunk error) - token $token |
|
Eot $token {error in chunked encoding -\ |
|
fetch terminated} |
|
} |
|
} else { |
|
##Log unchunked - token $token |
|
if {$state(totalsize) == 0} { |
|
# We know the transfer is complete only when the server |
|
# closes the connection. |
|
set state(state) complete |
|
set reqSize $state(-blocksize) |
|
} else { |
|
# Ask for the whole of the unserved response-body. |
|
# This works around a problem with a tls::socket - for |
|
# https in keep-alive mode, and a request for |
|
# $state(-blocksize) bytes, the last part of the |
|
# resource does not get read until the server times out. |
|
set reqSize [expr { $state(totalsize) |
|
- $state(currentsize)}] |
|
|
|
# The workaround fails if reqSize is |
|
# capped at $state(-blocksize). |
|
# set reqSize [expr {min($reqSize, $state(-blocksize))}] |
|
} |
|
set c $state(currentsize) |
|
set t $state(totalsize) |
|
##Log non-chunk currentsize $c of totalsize $t -\ |
|
token $token |
|
set block [read $sock $reqSize] |
|
set n [string length $block] |
|
if {$n >= 0} { |
|
append state(body) $block |
|
##Log non-chunk [string length $state(body)] -\ |
|
token $token |
|
} |
|
} |
|
# This calculation uses n from the -handler, chunked, or |
|
# unchunked case as appropriate. |
|
if {[info exists state]} { |
|
if {$n >= 0} { |
|
incr state(currentsize) $n |
|
set c $state(currentsize) |
|
set t $state(totalsize) |
|
##Log another $n currentsize $c totalsize $t -\ |
|
token $token |
|
} |
|
# If Content-Length - check for end of data. |
|
if { |
|
($state(totalsize) > 0) |
|
&& ($state(currentsize) >= $state(totalsize)) |
|
} { |
|
Log ^F$tk end of response body (unchunked) -\ |
|
token $token |
|
set state(state) complete |
|
Eot $token |
|
} |
|
} |
|
} err]} { |
|
Log ^X$tk end of response (error ${err}) - token $token |
|
Finish $token $err |
|
return |
|
} else { |
|
if {[info exists state(-progress)]} { |
|
eval $state(-progress) \ |
|
[list $token $state(totalsize) $state(currentsize)] |
|
} |
|
} |
|
} |
|
|
|
# catch as an Eot above may have closed the socket already |
|
# $state(state) may be connecting, header, body, or complete |
|
if {![set cc [catch {eof $sock} eof]] && $eof} { |
|
##Log eof - token $token |
|
if {[info exists $token]} { |
|
set state(connection) close |
|
if {$state(state) eq "complete"} { |
|
# This includes all cases in which the transaction |
|
# can be completed by eof. |
|
# The value "complete" is set only in http::Event, and it is |
|
# used only in the test above. |
|
Log ^F$tk end of response body (unchunked, eof) -\ |
|
token $token |
|
Eot $token |
|
} else { |
|
# Premature eof. |
|
Log ^X$tk end of response (unexpected eof) - token $token |
|
Eot $token eof |
|
} |
|
} else { |
|
# open connection closed on a token that has been cleaned up. |
|
Log ^X$tk end of response (token error) - token $token |
|
CloseSocket $sock |
|
} |
|
} elseif {$cc} { |
|
return |
|
} |
|
} |
|
} |
|
|
|
# http::TestForReplay |
|
# |
|
# Command called if eof is discovered when a socket is first used for a |
|
# new transaction. Typically this occurs if a persistent socket is used |
|
# after a period of idleness and the server has half-closed the socket. |
|
# |
|
# token - the connection token returned by http::geturl |
|
# doing - "read" or "write" |
|
# err - error message, if any |
|
# caller - code to identify the caller - used only in logging |
|
# |
|
# Return Value: boolean, true iff the command calls http::ReplayIfDead. |
|
|
|
proc http::TestForReplay {token doing err caller} { |
|
variable http |
|
variable $token |
|
upvar 0 $token state |
|
set tk [namespace tail $token] |
|
if {$doing eq "read"} { |
|
set code Q |
|
set action response |
|
set ing reading |
|
} else { |
|
set code P |
|
set action request |
|
set ing writing |
|
} |
|
|
|
if {$err eq {}} { |
|
set err "detect eof when $ing (server timed out?)" |
|
} |
|
|
|
if {$state(method) eq "POST" && !$http(-repost)} { |
|
# No Replay. |
|
# The present transaction will end when Finish is called. |
|
# That call to Finish will abort any other transactions |
|
# currently in the write queue. |
|
# For calls from http::Event this occurs when execution |
|
# reaches the code block at the end of that proc. |
|
set msg {no retry for POST with http::config -repost 0} |
|
Log reusing socket failed "($caller)" - $msg - token $token |
|
Log error - $err - token $token |
|
Log ^X$tk end of $action (error) - token $token |
|
return 0 |
|
} else { |
|
# Replay. |
|
set msg {try a new socket} |
|
Log reusing socket failed "($caller)" - $msg - token $token |
|
Log error - $err - token $token |
|
Log ^$code$tk Any unfinished (incl this one) failed - token $token |
|
ReplayIfDead $token $doing |
|
return 1 |
|
} |
|
} |
|
|
|
# http::IsBinaryContentType -- |
|
# |
|
# Determine if the content-type means that we should definitely transfer |
|
# the data as binary. [Bug 838e99a76d] |
|
# |
|
# Arguments |
|
# type The content-type of the data. |
|
# |
|
# Results: |
|
# Boolean, true if we definitely should be binary. |
|
|
|
proc http::IsBinaryContentType {type} { |
|
lassign [split [string tolower $type] "/;"] major minor |
|
if {$major eq "text"} { |
|
return false |
|
} |
|
# There's a bunch of XML-as-application-format things about. See RFC 3023 |
|
# and so on. |
|
if {$major eq "application"} { |
|
set minor [string trimright $minor] |
|
if {$minor in {"xml" "xml-external-parsed-entity" "xml-dtd"}} { |
|
return false |
|
} |
|
} |
|
# Not just application/foobar+xml but also image/svg+xml, so let us not |
|
# restrict things for now... |
|
if {[string match "*+xml" $minor]} { |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
# http::getTextLine -- |
|
# |
|
# Get one line with the stream in crlf mode. |
|
# Used if Transfer-Encoding is chunked. |
|
# Empty line is not distinguished from eof. The caller must |
|
# be able to handle this. |
|
# |
|
# Arguments |
|
# sock The socket receiving input. |
|
# |
|
# Results: |
|
# The line of text, without trailing newline |
|
|
|
proc http::getTextLine {sock} { |
|
set tr [fconfigure $sock -translation] |
|
lassign $tr trRead trWrite |
|
fconfigure $sock -translation [list crlf $trWrite] |
|
set r [BlockingGets $sock] |
|
fconfigure $sock -translation $tr |
|
return $r |
|
} |
|
|
|
# http::BlockingRead |
|
# |
|
# Replacement for a blocking read. |
|
# The caller must be a coroutine. |
|
|
|
proc http::BlockingRead {sock size} { |
|
if {$size < 1} { |
|
return |
|
} |
|
set result {} |
|
while 1 { |
|
set need [expr {$size - [string length $result]}] |
|
set block [read $sock $need] |
|
set eof [eof $sock] |
|
append result $block |
|
if {[string length $result] >= $size || $eof} { |
|
return $result |
|
} else { |
|
yield |
|
} |
|
} |
|
} |
|
|
|
# http::BlockingGets |
|
# |
|
# Replacement for a blocking gets. |
|
# The caller must be a coroutine. |
|
# Empty line is not distinguished from eof. The caller must |
|
# be able to handle this. |
|
|
|
proc http::BlockingGets {sock} { |
|
while 1 { |
|
set count [gets $sock line] |
|
set eof [eof $sock] |
|
if {$count > -1 || $eof} { |
|
return $line |
|
} else { |
|
yield |
|
} |
|
} |
|
} |
|
|
|
# http::CopyStart |
|
# |
|
# Error handling wrapper around fcopy |
|
# |
|
# Arguments |
|
# sock The socket to copy from |
|
# token The token returned from http::geturl |
|
# |
|
# Side Effects |
|
# This closes the connection upon error |
|
|
|
proc http::CopyStart {sock token {initial 1}} { |
|
upvar #0 $token state |
|
if {[info exists state(transfer)] && $state(transfer) eq "chunked"} { |
|
foreach coding [ContentEncoding $token] { |
|
lappend state(zlib) [zlib stream $coding] |
|
} |
|
make-transformation-chunked $sock [namespace code [list CopyChunk $token]] |
|
} else { |
|
if {$initial} { |
|
foreach coding [ContentEncoding $token] { |
|
zlib push $coding $sock |
|
} |
|
} |
|
if {[catch { |
|
# FIXME Keep-Alive on https tls::socket with unchunked transfer |
|
# hangs until the server times out. A workaround is possible, as for |
|
# the case without -channel, but it does not use the neat "fcopy" |
|
# solution. |
|
fcopy $sock $state(-channel) -size $state(-blocksize) -command \ |
|
[list http::CopyDone $token] |
|
} err]} { |
|
Finish $token $err |
|
} |
|
} |
|
} |
|
|
|
proc http::CopyChunk {token chunk} { |
|
upvar 0 $token state |
|
if {[set count [string length $chunk]]} { |
|
incr state(currentsize) $count |
|
if {[info exists state(zlib)]} { |
|
foreach stream $state(zlib) { |
|
set chunk [$stream add $chunk] |
|
} |
|
} |
|
puts -nonewline $state(-channel) $chunk |
|
if {[info exists state(-progress)]} { |
|
eval [linsert $state(-progress) end \ |
|
$token $state(totalsize) $state(currentsize)] |
|
} |
|
} else { |
|
Log "CopyChunk Finish - token $token" |
|
if {[info exists state(zlib)]} { |
|
set excess "" |
|
foreach stream $state(zlib) { |
|
catch {set excess [$stream add -finalize $excess]} |
|
} |
|
puts -nonewline $state(-channel) $excess |
|
foreach stream $state(zlib) { $stream close } |
|
unset state(zlib) |
|
} |
|
Eot $token ;# FIX ME: pipelining. |
|
} |
|
} |
|
|
|
# http::CopyDone |
|
# |
|
# fcopy completion callback |
|
# |
|
# Arguments |
|
# token The token returned from http::geturl |
|
# count The amount transfered |
|
# |
|
# Side Effects |
|
# Invokes callbacks |
|
|
|
proc http::CopyDone {token count {error {}}} { |
|
variable $token |
|
upvar 0 $token state |
|
set sock $state(sock) |
|
incr state(currentsize) $count |
|
if {[info exists state(-progress)]} { |
|
eval $state(-progress) \ |
|
[list $token $state(totalsize) $state(currentsize)] |
|
} |
|
# At this point the token may have been reset. |
|
if {[string length $error]} { |
|
Finish $token $error |
|
} elseif {[catch {eof $sock} iseof] || $iseof} { |
|
Eot $token |
|
} else { |
|
CopyStart $sock $token 0 |
|
} |
|
} |
|
|
|
# http::Eot |
|
# |
|
# Called when either: |
|
# a. An eof condition is detected on the socket. |
|
# b. The client decides that the response is complete. |
|
# c. The client detects an inconsistency and aborts the transaction. |
|
# |
|
# Does: |
|
# 1. Set state(status) |
|
# 2. Reverse any Content-Encoding |
|
# 3. Convert charset encoding and line ends if necessary |
|
# 4. Call http::Finish |
|
# |
|
# Arguments |
|
# token The token returned from http::geturl |
|
# force (previously) optional, has no effect |
|
# reason - "eof" means premature EOF (not EOF as the natural end of |
|
# the response) |
|
# - "" means completion of response, with or without EOF |
|
# - anything else describes an error confition other than |
|
# premature EOF. |
|
# |
|
# Side Effects |
|
# Clean up the socket |
|
|
|
proc http::Eot {token {reason {}}} { |
|
variable $token |
|
upvar 0 $token state |
|
if {$reason eq "eof"} { |
|
# Premature eof. |
|
set state(status) eof |
|
set reason {} |
|
} elseif {$reason ne ""} { |
|
# Abort the transaction. |
|
set state(status) $reason |
|
} else { |
|
# The response is complete. |
|
set state(status) ok |
|
} |
|
|
|
if {[string length $state(body)] > 0} { |
|
if {[catch { |
|
foreach coding [ContentEncoding $token] { |
|
set state(body) [zlib $coding $state(body)] |
|
} |
|
} err]} { |
|
Log "error doing decompression for token $token: $err" |
|
Finish $token $err |
|
return |
|
} |
|
|
|
if {!$state(binary)} { |
|
# If we are getting text, set the incoming channel's encoding |
|
# correctly. iso8859-1 is the RFC default, but this could be any |
|
# IANA charset. However, we only know how to convert what we have |
|
# encodings for. |
|
|
|
set enc [CharsetToEncoding $state(charset)] |
|
if {$enc ne "binary"} { |
|
set state(body) [encoding convertfrom $enc $state(body)] |
|
} |
|
|
|
# Translate text line endings. |
|
set state(body) [string map {\r\n \n \r \n} $state(body)] |
|
} |
|
} |
|
Finish $token $reason |
|
} |
|
|
|
# http::wait -- |
|
# |
|
# See documentation for details. |
|
# |
|
# Arguments: |
|
# token Connection token. |
|
# |
|
# Results: |
|
# The status after the wait. |
|
|
|
proc http::wait {token} { |
|
variable $token |
|
upvar 0 $token state |
|
|
|
if {![info exists state(status)] || $state(status) eq ""} { |
|
# We must wait on the original variable name, not the upvar alias |
|
vwait ${token}(status) |
|
} |
|
|
|
return [status $token] |
|
} |
|
|
|
# http::formatQuery -- |
|
# |
|
# See documentation for details. Call http::formatQuery with an even |
|
# number of arguments, where the first is a name, the second is a value, |
|
# the third is another name, and so on. |
|
# |
|
# Arguments: |
|
# args A list of name-value pairs. |
|
# |
|
# Results: |
|
# TODO |
|
|
|
proc http::formatQuery {args} { |
|
if {[llength $args] % 2} { |
|
return \ |
|
-code error \ |
|
-errorcode [list HTTP BADARGCNT $args] \ |
|
{Incorrect number of arguments, must be an even number.} |
|
} |
|
set result "" |
|
set sep "" |
|
foreach i $args { |
|
append result $sep [mapReply $i] |
|
if {$sep eq "="} { |
|
set sep & |
|
} else { |
|
set sep = |
|
} |
|
} |
|
return $result |
|
} |
|
|
|
# http::mapReply -- |
|
# |
|
# Do x-www-urlencoded character mapping |
|
# |
|
# Arguments: |
|
# string The string the needs to be encoded |
|
# |
|
# Results: |
|
# The encoded string |
|
|
|
proc http::mapReply {string} { |
|
variable http |
|
variable formMap |
|
|
|
# The spec says: "non-alphanumeric characters are replaced by '%HH'". Use |
|
# a pre-computed map and [string map] to do the conversion (much faster |
|
# than [regsub]/[subst]). [Bug 1020491] |
|
|
|
if {$http(-urlencoding) ne ""} { |
|
set string [encoding convertto $http(-urlencoding) $string] |
|
return [string map $formMap $string] |
|
} |
|
set converted [string map $formMap $string] |
|
if {[string match "*\[\u0100-\uffff\]*" $converted]} { |
|
regexp "\[\u0100-\uffff\]" $converted badChar |
|
# Return this error message for maximum compatibility... :^/ |
|
return -code error \ |
|
"can't read \"formMap($badChar)\": no such element in array" |
|
} |
|
return $converted |
|
} |
|
interp alias {} http::quoteString {} http::mapReply |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::ProxyRequired {host} { |
|
variable http |
|
if {[info exists http(-proxyhost)] && [string length $http(-proxyhost)]} { |
|
if { |
|
![info exists http(-proxyport)] || |
|
![string length $http(-proxyport)] |
|
} { |
|
set http(-proxyport) 8080 |
|
} |
|
return [list $http(-proxyhost) $http(-proxyport)] |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc http::CharsetToEncoding {charset} { |
|
variable encodings |
|
|
|
set charset [string tolower $charset] |
|
if {[regexp {iso-?8859-([0-9]+)} $charset -> num]} { |
|
set encoding "iso8859-$num" |
|
} elseif {[regexp {iso-?2022-(jp|kr)} $charset -> ext]} { |
|
set encoding "iso2022-$ext" |
|
} elseif {[regexp {shift[-_]?js} $charset]} { |
|
set encoding "shiftjis" |
|
} elseif {[regexp {(?:windows|cp)-?([0-9]+)} $charset -> num]} { |
|
set encoding "cp$num" |
|
} elseif {$charset eq "us-ascii"} { |
|
set encoding "ascii" |
|
} elseif {[regexp {(?:iso-?)?lat(?:in)?-?([0-9]+)} $charset -> num]} { |
|
switch -- $num { |
|
5 {set encoding "iso8859-9"} |
|
1 - 2 - 3 { |
|
set encoding "iso8859-$num" |
|
} |
|
} |
|
} else { |
|
|
|
set encoding $charset |
|
} |
|
set idx [lsearch -exact $encodings $encoding] |
|
if {$idx >= 0} { |
|
return $encoding |
|
} else { |
|
return "binary" |
|
} |
|
} |
|
|
|
|
|
proc http::ContentEncoding {token} { |
|
upvar 0 $token state |
|
set r {} |
|
if {[info exists state(coding)]} { |
|
foreach coding [split $state(coding) ,] { |
|
switch -exact -- $coding { |
|
deflate { lappend r inflate } |
|
gzip - x-gzip { lappend r gunzip } |
|
compress - x-compress { lappend r decompress } |
|
identity {} |
|
default { |
|
return -code error "unsupported content-encoding \"$coding\"" |
|
} |
|
} |
|
} |
|
} |
|
return $r |
|
} |
|
|
|
proc http::ReceiveChunked {chan command} { |
|
set data "" |
|
set size -1 |
|
yield |
|
while {1} { |
|
chan configure $chan -translation {crlf binary} |
|
while {[gets $chan line] < 1} { yield } |
|
chan configure $chan -translation {binary binary} |
|
if {[scan $line %x size] != 1} { |
|
return -code error "invalid size: \"$line\"" |
|
} |
|
set chunk "" |
|
while {$size && ![chan eof $chan]} { |
|
set part [chan read $chan $size] |
|
incr size -[string length $part] |
|
append chunk $part |
|
} |
|
if {[catch { |
|
uplevel #0 [linsert $command end $chunk] |
|
}]} { |
|
http::Log "Error in callback: $::errorInfo" |
|
} |
|
if {[string length $chunk] == 0} { |
|
|
|
catch {chan event $chan readable {}} |
|
return |
|
} |
|
} |
|
} |
|
|
|
proc http::make-transformation-chunked {chan command} { |
|
coroutine [namespace current]::dechunk$chan ::http::ReceiveChunked $chan $command |
|
chan event $chan readable [namespace current]::dechunk$chan |
|
} |
|
|
|
|
|
|
|
|
|
|