Spaces:
Running
Running
| # http.tcl -- | |
| # | |
| # Client-side HTTP for GET, POST, and HEAD commands. These routines can | |
| # be used in untrusted code that uses the Safesock security policy. | |
| # These procedures use a callback interface to avoid using vwait, which | |
| # is not defined in the safe base. | |
| # | |
| # See the file "license.terms" for information on usage and redistribution of | |
| # this file, and for a DISCLAIMER OF ALL WARRANTIES. | |
| package require Tcl 8.6- | |
| # Keep this in sync with pkgIndex.tcl and with the install directories in | |
| # Makefiles | |
| package provide http 2.9.5 | |
| namespace eval http { | |
| # Allow resourcing to not clobber existing data | |
| 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 | |
| } | |
| # We need a useragent string of this style or various servers will | |
| # refuse to send us compressed content even when we ask for it. This | |
| # follows the de-facto layout of user-agent strings in current browsers. | |
| # Safe interpreters do not have ::tcl_platform(os) or | |
| # ::tcl_platform(osVersion). | |
| 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 {} { | |
| # Set up the map for quoting chars. RFC3986 Section 2.3 say percent | |
| # encode all except: "... percent-encoded octets in the ranges of | |
| # ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), period | |
| # (%2E), underscore (%5F), or tilde (%7E) should not be created by URI | |
| # producers ..." | |
| 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] | |
| } | |
| } | |
| # These are handled specially | |
| set map(\n) %0D%0A | |
| variable formMap [array get map] | |
| # Create a map for HTTP/1.1 open sockets | |
| variable socketMapping | |
| variable socketRdState | |
| variable socketWrState | |
| variable socketRdQueue | |
| variable socketWrQueue | |
| variable socketClosing | |
| variable socketPlayCmd | |
| if {[info exists socketMapping]} { | |
| # Close open sockets on re-init. Do not permit retries. | |
| foreach {url sock} [array get socketMapping] { | |
| unset -nocomplain socketClosing($url) | |
| unset -nocomplain socketPlayCmd($url) | |
| CloseSocket $sock | |
| } | |
| } | |
| # CloseSocket should have unset the socket* arrays, one element at | |
| # a time. Now unset anything that was overlooked. | |
| # Traces on "unset socketRdState(*)" will call CancelReadPipeline and | |
| # cancel any queued responses. | |
| # Traces on "unset socketWrState(*)" will call CancelWritePipeline and | |
| # cancel any queued requests. | |
| 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]] | |
| # This can be changed, but iso8859-1 is the RFC standard. | |
| variable defaultCharset | |
| if {![info exists defaultCharset]} { | |
| set defaultCharset "iso8859-1" | |
| } | |
| # Force RFC 3986 strictness in geturl url verification? | |
| variable strict | |
| if {![info exists strict]} { | |
| set strict 1 | |
| } | |
| # Let user control default keepalive for compatibility | |
| variable defaultKeepalive | |
| if {![info exists defaultKeepalive]} { | |
| set defaultKeepalive 0 | |
| } | |
| namespace export geturl config reset wait formatQuery quoteString | |
| namespace export register unregister registerError | |
| # - Useful, but not exported: data, size, status, code, cleanup, error, | |
| # meta, ncode, mapReply, init. Comments suggest that "init" can be used | |
| # for re-initialisation, although the command is undocumented. | |
| # - Not exported, probably should be upper-case initial letter as part | |
| # of the internals: getTextLine, make-transformation-chunked. | |
| } | |
| # http::Log -- | |
| # | |
| # Debugging output -- define this to observe HTTP/1.1 socket usage. | |
| # Should echo any args received. | |
| # | |
| # Arguments: | |
| # msg Message to output | |
| # | |
| if {[info command http::Log] eq {}} {proc http::Log {args} {}} | |
| # http::register -- | |
| # | |
| # See documentation for details. | |
| # | |
| # Arguments: | |
| # proto URL protocol prefix, e.g. https | |
| # port Default port for protocol | |
| # command Command to use to create socket | |
| # Results: | |
| # list of port and command that was registered. | |
| proc http::register {proto port command} { | |
| variable urlTypes | |
| set urlTypes([string tolower $proto]) [list $port $command] | |
| } | |
| # http::unregister -- | |
| # | |
| # Unregisters URL protocol handler | |
| # | |
| # Arguments: | |
| # proto URL protocol prefix, e.g. https | |
| # Results: | |
| # list of port and command that was unregistered. | |
| 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 | |
| } | |
| # http::config -- | |
| # | |
| # See documentation for details. | |
| # | |
| # Arguments: | |
| # args Options parsed by the procedure. | |
| # Results: | |
| # TODO | |
| 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 | |
| } | |
| } | |
| } | |
| # http::Finish -- | |
| # | |
| # Clean up the socket and eval close time callbacks | |
| # | |
| # Arguments: | |
| # token Connection token. | |
| # errormsg (optional) If set, forces status to error. | |
| # skipCB (optional) If set, don't call the -command callback. This | |
| # is useful when geturl wants to throw an exception instead | |
| # of calling the callback. That way, the same error isn't | |
| # reported to two places. | |
| # | |
| # Side Effects: | |
| # May close the socket. | |
| 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 | |
| } | |
| } | |
| # http::KeepSocket - | |
| # | |
| # Keep a socket in the persistent sockets table and connect it to its next | |
| # queued task if possible. Otherwise leave it idle and ready for its next | |
| # use. | |
| # | |
| # If $socketClosing(*), then ($state(connection) eq "close") and therefore | |
| # this command will not be called by Finish. | |
| # | |
| # Arguments: | |
| # token Connection 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] | |
| # Keep this socket open for another request ("Keep-Alive"). | |
| # React if the server half-closes the socket. | |
| # Discussion is in http::geturl. | |
| catch {fileevent $state(sock) readable [list http::CheckEof $state(sock)]} | |
| # The line below should not be changed in production code. | |
| # It is edited by the test suite. | |
| set TEST_EOF 0 | |
| if {$TEST_EOF} { | |
| # ONLY for testing reaction to server eof. | |
| # No server timeouts will be caught. | |
| catch {fileevent $state(sock) readable {}} | |
| } | |
| if { [info exists state(socketinfo)] | |
| && [info exists socketMapping($state(socketinfo))] | |
| } { | |
| set connId $state(socketinfo) | |
| # The value "Rready" is set only here. | |
| set socketRdState($connId) Rready | |
| if { $state(-pipeline) | |
| && [info exists socketRdQueue($connId)] | |
| && [llength $socketRdQueue($connId)] | |
| } { | |
| # The usual case for pipelined responses - if another response is | |
| # queued, arrange to read it. | |
| 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] | |
| #Log pipelined, GRANT read access to $token3 in KeepSocket | |
| set socketRdState($connId) $token3 | |
| ReceiveResponse $token3 | |
| # Other pipelined cases. | |
| # - The test above ensures that, for the pipelined cases in the two | |
| # tests below, the read queue is empty. | |
| # - In those two tests, check whether the next write will be | |
| # nonpipeline. | |
| } 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) | |
| ] | |
| ) | |
| } { | |
| # This case: | |
| # - Now it the time to run the "pending" request. | |
| # - The next token in the write queue is nonpipeline, and | |
| # socketWrState has been marked "pending" (in | |
| # http::NextPipelinedWrite or http::geturl) so a new pipelined | |
| # request cannot jump the queue. | |
| # | |
| # Tests: | |
| # - In this case the read queue (tested above) is empty and this | |
| # "pending" write token is in front of the rest of the write | |
| # queue. | |
| # - The write state is not Wready and therefore appears to be busy, | |
| # but because it is "pending" we know that it is reserved for the | |
| # first item in the write queue, a non-pipelined request that is | |
| # waiting for the read queue to empty. That has now happened: so | |
| # give that request read and write access. | |
| variable $token3 | |
| set conn [set ${token3}(tmpConnArgs)] | |
| #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket | |
| set socketRdState($connId) $token3 | |
| set socketWrState($connId) $token3 | |
| set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] | |
| # Connect does its own fconfigure. | |
| fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] | |
| #Log ---- $state(sock) << conn to $token3 for HTTP request (c) | |
| } elseif { | |
| $state(-pipeline) | |
| && [info exists socketWrState($connId)] | |
| && ($socketWrState($connId) eq "peNding") | |
| } { | |
| # Should not come here. The second block in the previous "elseif" | |
| # test should be tautologous (but was needed in an earlier | |
| # implementation) and will be removed after testing. | |
| # If we get here, the value "pending" was assigned in error. | |
| # This error would block the queue for ever. | |
| 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) | |
| ] | |
| ) | |
| } { | |
| # This case: | |
| # - The next token in the write queue is nonpipeline, and | |
| # socketWrState is Wready. Get the next event from socketWrQueue. | |
| # Tests: | |
| # - In this case the read state (tested above) is Rready and the | |
| # write state (tested here) is Wready - there is no "pending" | |
| # request. | |
| # Code: | |
| # - The code is the same as the code below for the nonpipelined | |
| # case with a queued request. | |
| variable $token3 | |
| set conn [set ${token3}(tmpConnArgs)] | |
| #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket | |
| set socketRdState($connId) $token3 | |
| set socketWrState($connId) $token3 | |
| set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] | |
| # Connect does its own fconfigure. | |
| fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] | |
| #Log ---- $state(sock) << conn to $token3 for HTTP request (c) | |
| } elseif { | |
| (!$state(-pipeline)) | |
| && [info exists socketWrQueue($connId)] | |
| && [llength $socketWrQueue($connId)] | |
| && ($state(connection) ne "close") | |
| } { | |
| # If not pipelined, (socketRdState eq Rready) tells us that we are | |
| # ready for the next write - there is no need to check | |
| # socketWrState. Write the next request, if one is waiting. | |
| # If the next request is pipelined, it receives premature read | |
| # access to the socket. This is not a problem. | |
| set token3 [lindex $socketWrQueue($connId) 0] | |
| variable $token3 | |
| set conn [set ${token3}(tmpConnArgs)] | |
| #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket | |
| set socketRdState($connId) $token3 | |
| set socketWrState($connId) $token3 | |
| set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] | |
| # Connect does its own fconfigure. | |
| fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] | |
| #Log ---- $state(sock) << conn to $token3 for HTTP request (d) | |
| } elseif {(!$state(-pipeline))} { | |
| set socketWrState($connId) Wready | |
| # Rready and Wready and idle: nothing to do. | |
| } | |
| } else { | |
| CloseSocket $state(sock) $token | |
| # There is no socketMapping($state(socketinfo)), so it does not matter | |
| # that CloseQueuedQueries is not called. | |
| } | |
| } | |
| # http::CheckEof - | |
| # | |
| # Read from a socket and close it if eof. | |
| # The command is bound to "fileevent readable" on an idle socket, and | |
| # "eof" is the only event that should trigger the binding, occurring when | |
| # the server times out and half-closes the socket. | |
| # | |
| # A read is necessary so that [eof] gives a meaningful result. | |
| # Any bytes sent are junk (or a bug). | |
| 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} { | |
| # The server has half-closed the socket. | |
| # If a new write has started, its transaction will fail and | |
| # will then be error-handled. | |
| CloseSocket $sock | |
| } | |
| } | |
| # http::CloseSocket - | |
| # | |
| # Close a socket and remove it from the persistent sockets table. If | |
| # possible an http token is included here but when we are called from a | |
| # fileevent on remote closure we need to find the correct entry - hence | |
| # the "else" block of the first "if" command. | |
| 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 >= 0} { | |
| 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 {}} { | |
| # Cases with a non-empty token are handled by Finish, so the tokens | |
| # are finished in connection order. | |
| http::CloseQueuedQueries $connId | |
| } | |
| } else { | |
| Log "Closing socket $s (no connection info)" | |
| if {[catch {close $s} err]} { | |
| Log "Error closing socket: $err" | |
| } | |
| } | |
| } | |
| # http::CloseQueuedQueries | |
| # | |
| # connId - identifier "domain:port" for the connection | |
| # token - (optional) used only for logging | |
| # | |
| # Called from http::CloseSocket and http::Finish, after a connection is closed, | |
| # to clear the read and write queues if this has not already been done. | |
| proc http::CloseQueuedQueries {connId {token {}}} { | |
| variable socketMapping | |
| variable socketRdState | |
| variable socketWrState | |
| variable socketRdQueue | |
| variable socketWrQueue | |
| variable socketClosing | |
| variable socketPlayCmd | |
| if {![info exists socketMapping($connId)]} { | |
| # Command has already been called. | |
| # Don't come here again - especially recursively. | |
| return | |
| } | |
| # Used only for logging. | |
| if {$token eq {}} { | |
| set tk {} | |
| } else { | |
| set tk [namespace tail $token] | |
| } | |
| if { [info exists socketPlayCmd($connId)] | |
| && ($socketPlayCmd($connId) ne {ReplayIfClose Wready {} {}}) | |
| } { | |
| # Before unsetting, there is some unfinished business. | |
| # - If the server sent "Connection: close", we have stored the command | |
| # for retrying any queued requests in socketPlayCmd, so copy that | |
| # value for execution below. socketClosing(*) was also set. | |
| # - Also clear the queues to prevent calls to Finish that would set the | |
| # state for the requests that will be retried to "finished with error | |
| # status". | |
| 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 | |
| } | |
| } | |
| # http::Unset | |
| # | |
| # The trace on "unset socketRdState(*)" will call CancelReadPipeline | |
| # and cancel any queued responses. | |
| # The trace on "unset socketWrState(*)" will call CancelWritePipeline | |
| # and cancel any queued requests. | |
| 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) | |
| } | |
| # http::reset -- | |
| # | |
| # See documentation for details. | |
| # | |
| # Arguments: | |
| # token Connection token. | |
| # why Status info. | |
| # | |
| # Side Effects: | |
| # See Finish | |
| 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 | |
| } | |
| } | |
| # http::geturl -- | |
| # | |
| # Establishes a connection to a remote url via http. | |
| # | |
| # Arguments: | |
| # url The http URL to goget. | |
| # args Option value pairs. Valid options include: | |
| # -blocksize, -validate, -headers, -timeout | |
| # Results: | |
| # Returns a token for this connection. This token is the name of an | |
| # array that the caller should unset to garbage collect the state. | |
| proc http::geturl {url args} { | |
| variable http | |
| variable urlTypes | |
| variable defaultCharset | |
| variable defaultKeepalive | |
| variable strict | |
| # Initialize the state variable, an array. We'll return the name of this | |
| # array as the token for the transaction. | |
| if {![info exists http(uid)]} { | |
| set http(uid) 0 | |
| } | |
| set token [namespace current]::[incr http(uid)] | |
| ##Log Starting http::geturl - token $token | |
| variable $token | |
| upvar 0 $token state | |
| set tk [namespace tail $token] | |
| reset $token | |
| Log ^A$tk URL $url - token $token | |
| # Process command options. | |
| 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 keep-alive | |
| } | |
| set state(-keepalive) $defaultKeepalive | |
| set state(-strict) $strict | |
| # These flags have their types verified [Bug 811170] | |
| array set type { | |
| -binary boolean | |
| -blocksize integer | |
| -queryblocksize integer | |
| -strict boolean | |
| -timeout integer | |
| -validate boolean | |
| -headers dict | |
| } | |
| 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]} { | |
| # Validate numbers | |
| if {($flag eq "-headers") ? [catch {dict size $value}] : | |
| ([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" | |
| } | |
| } | |
| # Make sure -query and -querychannel aren't both specified | |
| 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!" | |
| } | |
| # Validate URL, determine the server host and port, and check proxy case | |
| # Recognize user:pass@host URLs also, although we do not do anything with | |
| # that info yet. | |
| # URLs have basically four parts. | |
| # First, before the colon, is the protocol scheme (e.g. http) | |
| # Second, for HTTP-like protocols, is the authority | |
| # The authority is preceded by // and lasts up to (but not including) | |
| # the following / or ? and it identifies up to four parts, of which | |
| # only one, the host, is required (if an authority is present at all). | |
| # All other parts of the authority (user name, password, port number) | |
| # are optional. | |
| # Third is the resource name, which is split into two parts at a ? | |
| # The first part (from the single "/" up to "?") is the path, and the | |
| # second part (from that "?" up to "#") is the query. *HOWEVER*, we do | |
| # not need to separate them; we send the whole lot to the server. | |
| # Both, path and query are allowed to be missing, including their | |
| # delimiting character. | |
| # Fourth is the fragment identifier, which is everything after the first | |
| # "#" in the URL. The fragment identifier MUST NOT be sent to the server | |
| # and indeed, we don't bother to validate it (it could be an error to | |
| # pass it in here, but it's cheap to strip). | |
| # | |
| # An example of a URL that has all the parts: | |
| # | |
| # http://jschmoe:[email protected]:8000/foo/bar.tml?q=foo#changes | |
| # | |
| # The "http" is the protocol, the user is "jschmoe", the password is | |
| # "xyzzy", the host is "www.bogus.net", the port is "8000", the path is | |
| # "/foo/bar.tml", the query is "q=foo", and the fragment is "changes". | |
| # | |
| # Note that the RE actually combines the user and password parts, as | |
| # recommended in RFC 3986. Indeed, that RFC states that putting passwords | |
| # in URLs is a Really Bad Idea, something with which I would agree utterly. | |
| # | |
| # From a validation perspective, we need to ensure that the parts of the | |
| # URL that are going to the server are correctly encoded. This is only | |
| # done if $state(-strict) is true (inherited from $::http::strict). | |
| 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> | |
| $ | |
| } | |
| # Phase one: parse | |
| if {![regexp -- $URLmatcher $url -> proto user host port srvurl]} { | |
| unset $token | |
| return -code error "Unsupported URL: $url" | |
| } | |
| # Phase two: validate | |
| set host [string trim $host {[]}]; # strip square brackets from IPv6 address | |
| if {$host eq ""} { | |
| # Caller has to provide a host name; we do not have a "default host" | |
| # that would enable us to handle relative URLs. | |
| unset $token | |
| return -code error "Missing host part: $url" | |
| # Note that we don't check the hostname for validity here; if it's | |
| # invalid, we'll simply fail to resolve it later on. | |
| } | |
| if {$port ne "" && $port > 65535} { | |
| unset $token | |
| return -code error "Invalid port number: $port" | |
| } | |
| # The user identification and resource identification parts of the URL can | |
| # have encoded characters in them; take care! | |
| if {$user ne ""} { | |
| # Check for validity according to RFC 3986, Appendix A | |
| set validityRE {(?xi) | |
| ^ | |
| (?: [-\w.~!$&'()*+,;=:] | %[0-9a-f][0-9a-f] )+ | |
| $ | |
| } | |
| if {$state(-strict) && ![regexp -- $validityRE $user]} { | |
| unset $token | |
| # Provide a better error message in this error case | |
| 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 ""} { | |
| # RFC 3986 allows empty paths (not even a /), but servers | |
| # return 400 if the path in the HTTP request doesn't start | |
| # with / , so add it here if needed. | |
| if {[string index $srvurl 0] ne "/"} { | |
| set srvurl /$srvurl | |
| } | |
| # Check for validity according to RFC 3986, Appendix A | |
| set validityRE {(?xi) | |
| ^ | |
| # Path part (already must start with / character) | |
| (?: [-\w.~!$&'()*+,;=:@/] | %[0-9a-f][0-9a-f] )* | |
| # Query part (optional, permits ? characters) | |
| (?: \? (?: [-\w.~!$&'()*+,;=:@/?] | %[0-9a-f][0-9a-f] )* )? | |
| $ | |
| } | |
| if {$state(-strict) && ![regexp -- $validityRE $srvurl]} { | |
| unset $token | |
| # Provide a better error message in this error case | |
| 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] | |
| } | |
| # OK, now reassemble into a full URL | |
| set url ${proto}:// | |
| if {$user ne ""} { | |
| append url $user | |
| append url @ | |
| } | |
| append url $host | |
| if {$port != $defport} { | |
| append url : $port | |
| } | |
| append url $srvurl | |
| # Don't append the fragment! | |
| set state(url) $url | |
| set sockopts [list -async] | |
| # If we are using the proxy, we must pass in the full URL that includes | |
| # the server name. | |
| if {[info exists phost] && ($phost ne "")} { | |
| set srvurl $url | |
| set targetAddr [list $phost $pport] | |
| } else { | |
| set targetAddr [list $host $port] | |
| } | |
| # Proxy connections aren't shared among different hosts. | |
| set state(socketinfo) $host:$port | |
| # Save the accept types at this point to prevent a race condition. [Bug | |
| # c11a51c482] | |
| set state(accept-types) $http(-accept) | |
| if {$isQuery || $isQueryChannel} { | |
| # It's a POST. | |
| # A client wishing to send a non-idempotent request SHOULD wait to send | |
| # that request until it has received the response status for the | |
| # previous request. | |
| if {$http(-postfresh)} { | |
| # Override -keepalive for a POST. Use a new connection, and thus | |
| # avoid the small risk of a race against server timeout. | |
| set state(-keepalive) 0 | |
| } else { | |
| # Allow -keepalive but do not -pipeline - wait for the previous | |
| # transaction to finish. | |
| # There is a small risk of a race against server timeout. | |
| set state(-pipeline) 0 | |
| } | |
| } else { | |
| # It's a GET or HEAD. | |
| set state(-pipeline) $http(-pipeline) | |
| } | |
| # We cannot handle chunked encodings with -handler, so force HTTP/1.0 | |
| # until we can manage this. | |
| if {[info exists state(-handler)]} { | |
| set state(-protocol) 1.0 | |
| } | |
| # RFC 7320 A.1 - HTTP/1.0 Keep-Alive is problematic. We do not support it. | |
| if {$state(-protocol) eq "1.0"} { | |
| set state(connection) close | |
| set state(-keepalive) 0 | |
| } | |
| # See if we are supposed to use a previously opened channel. | |
| # - In principle, ANY call to http::geturl could use a previously opened | |
| # channel if it is available - the "Connection: keep-alive" header is a | |
| # request to leave the channel open AFTER completion of this call. | |
| # - In fact, we try to use an existing channel only if -keepalive 1 -- this | |
| # means that at most one channel is left open for each value of | |
| # $state(socketinfo). This property simplifies the mapping of open | |
| # channels. | |
| 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 the connection is idle, it has a "fileevent readable" binding | |
| # to http::CheckEof, in case the server times out and half-closes | |
| # the socket (http::CheckEof closes the other half). | |
| # - We leave this binding in place until just before the last | |
| # puts+flush in http::Connected (GET/HEAD) or http::Write (POST), | |
| # after which the HTTP response might be generated. | |
| if { [info exists socketClosing($state(socketinfo))] | |
| && $socketClosing($state(socketinfo)) | |
| } { | |
| # socketClosing(*) is set because the server has sent a | |
| # "Connection: close" header. | |
| # Do not use the persistent socket again. | |
| # Since we have only one persistent socket per server, and the | |
| # old socket is not yet dead, add the request to the write queue | |
| # of the dying socket, which will be replayed by ReplayIfClose. | |
| # Also add it to socketWrQueue(*) which is used only if an error | |
| # causes a call to Finish. | |
| 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))}]} { | |
| # FIXME Is it still possible for this code to be executed? If | |
| # so, this could be another place to call TestForReplay, | |
| # rather than discarding the queued transactions. | |
| 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" | |
| # This will call CancelReadPipeline, CancelWritePipeline, and | |
| # cancel any queued requests, responses. | |
| Unset $state(socketinfo) | |
| } else { | |
| # Use the persistent socket. | |
| # The socket may not be ready to write: an earlier request might | |
| # still be still writing (in the pipelined case) or | |
| # writing/reading (in the nonpipeline case). This possibility | |
| # is handled by socketWrQueue later in this command. | |
| set reusing 1 | |
| set sock $socketMapping($state(socketinfo)) | |
| Log "reusing socket $sock for $state(socketinfo) - token $token" | |
| } | |
| # Do not automatically close the connection socket. | |
| set state(connection) keep-alive | |
| } | |
| } | |
| if {$reusing} { | |
| # Define state(tmpState) and state(tmpOpenCmd) for use | |
| # by http::ReplayIfDead if the persistent connection has died. | |
| set state(tmpState) [array get state] | |
| # Pass -myaddr directly to the socket command | |
| if {[info exists state(-myaddr)]} { | |
| lappend sockopts -myaddr $state(-myaddr) | |
| } | |
| set state(tmpOpenCmd) [list {*}$defcmd {*}$sockopts {*}$targetAddr] | |
| } | |
| set state(reusing) $reusing | |
| # Excluding ReplayIfDead and the decision whether to call it, there are four | |
| # places outside http::geturl where state(reusing) is used: | |
| # - Connected - if reusing and not pipelined, start the state(-timeout) | |
| # timeout (when writing). | |
| # - DoneRequest - if reusing and pipelined, send the next pipelined write | |
| # - Event - if reusing and pipelined, start the state(-timeout) | |
| # timeout (when reading). | |
| # - Event - if (not reusing) and pipelined, send the next pipelined | |
| # write | |
| # See comments above re the start of this timeout in other cases. | |
| if {(!$state(reusing)) && ($state(-timeout) > 0)} { | |
| set state(after) [after $state(-timeout) \ | |
| [list http::reset $token timeout]] | |
| } | |
| if {![info exists sock]} { | |
| # Pass -myaddr directly to the socket command | |
| if {[info exists state(-myaddr)]} { | |
| lappend sockopts -myaddr $state(-myaddr) | |
| } | |
| set pre [clock milliseconds] | |
| ##Log pre socket opened, - token $token | |
| ##Log [concat $defcmd $sockopts $targetAddr] - token $token | |
| if {[catch {eval $defcmd $sockopts $targetAddr} sock errdict]} { | |
| # Something went wrong while trying to establish the connection. | |
| # Clean up after events and such, but DON'T call the command | |
| # callback (if available) because we're going to throw an | |
| # exception from here instead. | |
| set state(sock) NONE | |
| Finish $token $sock 1 | |
| cleanup $token | |
| dict unset errdict -level | |
| return -options $errdict $sock | |
| } else { | |
| # Initialisation of a new socket. | |
| ##Log post socket opened, - token $token | |
| ##Log socket opened, now fconfigure - token $token | |
| set delay [expr {[clock milliseconds] - $pre}] | |
| if {$delay > 3000} { | |
| Log socket delay $delay - token $token | |
| } | |
| fconfigure $sock -translation {auto crlf} \ | |
| -buffersize $state(-blocksize) | |
| ##Log socket opened, DONE fconfigure - token $token | |
| } | |
| } | |
| # Command [socket] is called with -async, but takes 5s to 5.1s to return, | |
| # with probability of order 1 in 10,000. This may be a bizarre scheduling | |
| # issue with my (KJN's) system (Fedora Linux). | |
| # This does not cause a problem (unless the request times out when this | |
| # command returns). | |
| set state(sock) $sock | |
| Log "Using $sock for $state(socketinfo) - token $token" \ | |
| [expr {$state(-keepalive)?"keepalive":""}] | |
| if { $state(-keepalive) | |
| && (![info exists socketMapping($state(socketinfo))]) | |
| } { | |
| # Freshly-opened socket that we would like to become persistent. | |
| 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)} { | |
| #Log new, init for pipelined, GRANT write access to $token in geturl | |
| # Also grant premature read access to the socket. This is OK. | |
| set socketRdState($state(socketinfo)) $token | |
| set socketWrState($state(socketinfo)) $token | |
| } else { | |
| # socketWrState is not used by this non-pipelined transaction. | |
| # We cannot leave it as "Wready" because the next call to | |
| # http::geturl with a pipelined transaction would conclude that the | |
| # socket is available for writing. | |
| #Log new, init for nonpipeline, GRANT r/w access to $token in geturl | |
| 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} { | |
| # For use by http::ReplayIfDead if the persistent connection has died. | |
| # Also used by NextPipelinedWrite. | |
| set state(tmpConnArgs) [list $proto $phost $srvurl] | |
| } | |
| # The element socketWrState($connId) has a value which is either the name of | |
| # the token that is permitted to write to the socket, or "Wready" if no | |
| # token is permitted to write. | |
| # | |
| # The code that sets the value to Wready immediately calls | |
| # http::NextPipelinedWrite, which examines socketWrQueue($connId) and | |
| # processes the next request in the queue, if there is one. The value | |
| # Wready is not found when the interpreter is in the event loop unless the | |
| # socket is idle. | |
| # | |
| # The element socketRdState($connId) has a value which is either the name of | |
| # the token that is permitted to read from the socket, or "Rready" if no | |
| # token is permitted to read. | |
| # | |
| # The code that sets the value to Rready then examines | |
| # socketRdQueue($connId) and processes the next request in the queue, if | |
| # there is one. The value Rready is not found when the interpreter is in | |
| # the event loop unless the socket is idle. | |
| if {$alreadyQueued} { | |
| # A write may or may not be in progress. There is no need to set | |
| # socketWrState to prevent another call stealing write access - all | |
| # subsequent calls on this socket will come here because the socket | |
| # will close after the current read, and its | |
| # socketClosing($connId) is 1. | |
| ##Log "HTTP request for token $token is queued" | |
| } elseif { $reusing | |
| && $state(-pipeline) | |
| && ($socketWrState($state(socketinfo)) ne "Wready") | |
| } { | |
| ##Log "HTTP request for token $token is queued for pipelined use" | |
| lappend socketWrQueue($state(socketinfo)) $token | |
| } elseif { $reusing | |
| && (!$state(-pipeline)) | |
| && ($socketWrState($state(socketinfo)) ne "Wready") | |
| } { | |
| # A write is queued or in progress. Lappend to the write queue. | |
| ##Log "HTTP request for token $token is queued for nonpipeline use" | |
| lappend socketWrQueue($state(socketinfo)) $token | |
| } elseif { $reusing | |
| && (!$state(-pipeline)) | |
| && ($socketWrState($state(socketinfo)) eq "Wready") | |
| && ($socketRdState($state(socketinfo)) ne "Rready") | |
| } { | |
| # A read is queued or in progress, but not a write. Cannot start the | |
| # nonpipeline transaction, but must set socketWrState to prevent a | |
| # pipelined request jumping the queue. | |
| ##Log "HTTP request for token $token is queued for nonpipeline use" | |
| #Log re-use nonpipeline, GRANT delayed write access to $token in geturl | |
| set socketWrState($state(socketinfo)) peNding | |
| lappend socketWrQueue($state(socketinfo)) $token | |
| } else { | |
| if {$reusing && $state(-pipeline)} { | |
| #Log re-use pipelined, GRANT write access to $token in geturl | |
| set socketWrState($state(socketinfo)) $token | |
| } elseif {$reusing} { | |
| # Cf tests above - both are ready. | |
| #Log re-use nonpipeline, GRANT r/w access to $token in geturl | |
| set socketRdState($state(socketinfo)) $token | |
| set socketWrState($state(socketinfo)) $token | |
| } | |
| # All (!$reusing) cases come here, and also some $reusing cases if the | |
| # connection is ready. | |
| #Log ---- $state(socketinfo) << conn to $token for HTTP request (a) | |
| # Connect does its own fconfigure. | |
| fileevent $sock writable \ | |
| [list http::Connect $token $proto $phost $srvurl] | |
| } | |
| # Wait for the connection to complete. | |
| if {![info exists state(-command)]} { | |
| # geturl does EVERYTHING asynchronously, so if the user | |
| # calls it synchronously, we just do a wait here. | |
| http::wait $token | |
| if {![info exists state]} { | |
| # If we timed out then Finish has been called and the users | |
| # command callback may have cleaned up the token. If so we end up | |
| # here with nothing left to do. | |
| return $token | |
| } elseif {$state(status) eq "error"} { | |
| # Something went wrong while trying to establish the connection. | |
| # Clean up after events and such, but DON'T call the command | |
| # callback (if available) because we're going to throw an | |
| # exception from here instead. | |
| set err [lindex $state(error) 0] | |
| cleanup $token | |
| return -code error $err | |
| } | |
| } | |
| ##Log Leaving http::geturl - token $token | |
| return $token | |
| } | |
| # http::Connected -- | |
| # | |
| # Callback used when the connection to the HTTP server is actually | |
| # established. | |
| # | |
| # Arguments: | |
| # token State token. | |
| # proto What protocol (http, https, etc.) was used to connect. | |
| # phost Are we using keep-alive? Non-empty if yes. | |
| # srvurl Service-local URL that we're requesting | |
| # Results: | |
| # None. | |
| 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 back the variables needed here. | |
| 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] | |
| # Send data in cr-lf format, but accept any line terminators. | |
| # Initialisation to {auto *} now done in geturl, KeepSocket and DoneRequest. | |
| # We are concerned here with the request (write) not the response (read). | |
| lassign [fconfigure $sock -translation] trRead trWrite | |
| fconfigure $sock -translation [list $trRead crlf] \ | |
| -buffersize $state(-blocksize) | |
| # The following is disallowed in safe interpreters, but the socket is | |
| # already in non-blocking mode in that case. | |
| 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 { | |
| # There's no query data. | |
| unset state(-query) | |
| set isQuery 0 | |
| } | |
| } elseif {$state(-validate)} { | |
| set how HEAD | |
| } elseif {$isQueryChannel} { | |
| set how POST | |
| # The query channel must be blocking for the async Write to | |
| # work properly. | |
| fconfigure $state(-querychannel) -blocking 1 -translation binary | |
| set contDone 0 | |
| } | |
| if {[info exists state(-method)] && ($state(-method) ne "")} { | |
| set how $state(-method) | |
| } | |
| 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]} { | |
| # Allow Host spoofing. [Bug 928154] | |
| puts $sock "Host: [dict get $state(-headers) Host]" | |
| } elseif {$port == $defport} { | |
| # Don't add port in this case, to handle broken servers. [Bug | |
| # #504508] | |
| puts $sock "Host: $host" | |
| } else { | |
| puts $sock "Host: $host:$port" | |
| } | |
| puts $sock "User-Agent: $http(-useragent)" | |
| if {($state(-protocol) > 1.0) && $state(-keepalive)} { | |
| # Send this header, because a 1.1 server is not compelled to treat | |
| # this as the default. | |
| puts $sock "Connection: keep-alive" | |
| } | |
| if {($state(-protocol) > 1.0) && !$state(-keepalive)} { | |
| puts $sock "Connection: close" ;# RFC2616 sec 8.1.2.1 | |
| } | |
| if {($state(-protocol) < 1.1)} { | |
| # RFC7230 A.1 | |
| # Some server implementations of HTTP/1.0 have a faulty | |
| # implementation of RFC 2068 Keep-Alive. | |
| # Don't leave this to chance. | |
| # For HTTP/1.0 we have already "set state(connection) close" | |
| # and "state(-keepalive) 0". | |
| puts $sock "Connection: close" | |
| } | |
| # RFC7230 A.1 - "clients are encouraged not to send the | |
| # Proxy-Connection header field in any requests" | |
| 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" | |
| } | |
| } | |
| # Allow overriding the Accept header on a per-connection basis. Useful | |
| # for working with REST services. [Bug c11a51c482] | |
| 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)} { | |
| # Try to determine size of data in channel. If we cannot seek, the | |
| # surrounding catch will trap us | |
| set start [tell $state(-querychannel)] | |
| seek $state(-querychannel) 0 end | |
| set state(querylength) \ | |
| [expr {[tell $state(-querychannel)] - $start}] | |
| seek $state(-querychannel) $start | |
| } | |
| # Flush the request header and set up the fileevent that will either | |
| # push the POST data or read the response. | |
| # | |
| # fileevent note: | |
| # | |
| # It is possible to have both the read and write fileevents active at | |
| # this point. The only scenario it seems to affect is a server that | |
| # closes the connection without reading the POST data. (e.g., early | |
| # versions TclHttpd in various error cases). Depending on the | |
| # platform, the client may or may not be able to get the response from | |
| # the server because of the error it will get trying to write the post | |
| # data. Having both fileevents active changes the timing and the | |
| # behavior, but no two platforms (among Solaris, Linux, and NT) behave | |
| # the same, and none behave all that well in any case. Servers should | |
| # always read their POST data if they expect the client to read their | |
| # response. | |
| if {$isQuery || $isQueryChannel} { | |
| # POST method. | |
| if {!$content_type_seen} { | |
| puts $sock "Content-Type: $state(-type)" | |
| } | |
| if {!$contDone} { | |
| puts $sock "Content-Length: $state(querylength)" | |
| } | |
| puts $sock "" | |
| flush $sock | |
| # Flush flushes the error in the https case with a bad handshake: | |
| # else the socket never becomes writable again, and hangs until | |
| # timeout (if any). | |
| lassign [fconfigure $sock -translation] trRead trWrite | |
| fconfigure $sock -translation [list $trRead binary] | |
| fileevent $sock writable [list http::Write $token] | |
| # The http::Write command decides when to make the socket readable, | |
| # using the same test as the GET/HEAD case below. | |
| } else { | |
| # GET or HEAD method. | |
| if { (![catch {fileevent $sock readable} binding]) | |
| && ($binding eq [list http::CheckEof $sock]) | |
| } { | |
| # Remove the "fileevent readable" binding of an idle persistent | |
| # socket to http::CheckEof. We can no longer treat bytes | |
| # received as junk. The server might still time out and | |
| # half-close the socket if it has not yet received the first | |
| # "puts". | |
| fileevent $sock readable {} | |
| } | |
| puts $sock "" | |
| flush $sock | |
| Log ^C$tk end sending request - token $token | |
| # End of writing (GET/HEAD methods). The request has been sent. | |
| DoneRequest $token | |
| } | |
| } err]} { | |
| # The socket probably was never connected, OR the connection dropped | |
| # later, OR https handshake error, which may be discovered as late as | |
| # the "flush" command above... | |
| Log "WARNING - if testing, pay special attention to this\ | |
| case (GI) which is seldom executed - token $token" | |
| if {[info exists state(reusing)] && $state(reusing)} { | |
| # The socket was closed at the server end, and closed at | |
| # this end by http::CheckEof. | |
| if {[TestForReplay $token write $err a]} { | |
| return | |
| } else { | |
| Finish $token {failed to re-use socket} | |
| } | |
| # else: | |
| # This is NOT a persistent socket that has been closed since its | |
| # last use. | |
| # If any other requests are in flight or pipelined/queued, they will | |
| # be discarded. | |
| } elseif {$state(status) eq ""} { | |
| # ...https handshake errors come here. | |
| 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 | |
| } | |
| } | |
| } | |
| # http::registerError | |
| # | |
| # Called (for example when processing TclTLS activity) to register | |
| # an error for a connection on a specific socket. This helps | |
| # http::Connected to deliver meaningful error messages, e.g. when a TLS | |
| # certificate fails verification. | |
| # | |
| # Usage: http::registerError socket ?newValue? | |
| # | |
| # "set" semantics, except that a "get" (a call without a new value) for a | |
| # non-existent socket returns {}, not an error. | |
| 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 | |
| } | |
| # http::DoneRequest -- | |
| # | |
| # Command called when a request has been sent. It will arrange the | |
| # next request and/or response as appropriate. | |
| # | |
| # If this command is called when $socketClosing(*), the request $token | |
| # that calls it must be pipelined and destined to fail. | |
| 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 pipelined, connect the next HTTP request to the socket. | |
| if {$state(reusing) && $state(-pipeline)} { | |
| # Enable next token (if any) to write. | |
| # The value "Wready" is set only here, and | |
| # in http::Event after reading the response-headers of a | |
| # non-reusing transaction. | |
| # Previous value is $token. It cannot be pending. | |
| set socketWrState($state(socketinfo)) Wready | |
| # Now ready to write the next pipelined request (if any). | |
| http::NextPipelinedWrite $token | |
| } else { | |
| # If pipelined, this is the first transaction on this socket. We wait | |
| # for the response headers to discover whether the connection is | |
| # persistent. (If this is not done and the connection is not | |
| # persistent, we SHOULD retry and then MUST NOT pipeline before knowing | |
| # that we have a persistent connection | |
| # (rfc2616 8.1.2.2)). | |
| } | |
| # Connect to receive the response, unless the socket is pipelined | |
| # and another response is being sent. | |
| # This code block is separate from the code below because there are | |
| # cases where socketRdState already has the value $token. | |
| if { $state(-keepalive) | |
| && $state(-pipeline) | |
| && [info exists socketRdState($state(socketinfo))] | |
| && ($socketRdState($state(socketinfo)) eq "Rready") | |
| } { | |
| #Log pipelined, GRANT read access to $token in Connected | |
| set socketRdState($state(socketinfo)) $token | |
| } | |
| if { $state(-keepalive) | |
| && $state(-pipeline) | |
| && [info exists socketRdState($state(socketinfo))] | |
| && ($socketRdState($state(socketinfo)) ne $token) | |
| } { | |
| # Do not read from the socket until it is ready. | |
| ##Log "HTTP response for token $token is queued for pipelined use" | |
| # If $socketClosing(*), then the caller will be a pipelined write and | |
| # execution will come here. | |
| # This token has already been recorded as "in flight" for writing. | |
| # When the socket is closed, the read queue will be cleared in | |
| # CloseQueuedQueries and so the "lappend" here has no effect. | |
| lappend socketRdQueue($state(socketinfo)) $token | |
| } else { | |
| # In the pipelined case, connection for reading depends on the | |
| # value of socketRdState. | |
| # In the nonpipeline case, connection for reading always occurs. | |
| ReceiveResponse $token | |
| } | |
| } | |
| # http::ReceiveResponse | |
| # | |
| # Connects token to its socket for reading. | |
| proc http::ReceiveResponse {token} { | |
| variable $token | |
| upvar 0 $token state | |
| set tk [namespace tail $token] | |
| set sock $state(sock) | |
| #Log ---- $state(socketinfo) >> conn to $token for HTTP response | |
| 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 | |
| if {[info exists state(-handler)] || [info exists state(-progress)]} { | |
| fileevent $sock readable [list http::EventGateway $sock $token] | |
| } else { | |
| fileevent $sock readable ${token}EventCoroutine | |
| } | |
| return | |
| } | |
| # http::EventGateway | |
| # | |
| # Bug [c2dc1da315]. | |
| # - Recursive launch of the coroutine can occur if a -handler or -progress | |
| # callback is used, and the callback command enters the event loop. | |
| # - To prevent this, the fileevent "binding" is disabled while the | |
| # coroutine is in flight. | |
| # - If a recursive call occurs despite these precautions, it is not | |
| # trapped and discarded here, because it is better to report it as a | |
| # bug. | |
| # - Although this solution is believed to be sufficiently general, it is | |
| # used only if -handler or -progress is specified. In other cases, | |
| # the coroutine is called directly. | |
| proc http::EventGateway {sock token} { | |
| variable $token | |
| upvar 0 $token state | |
| fileevent $sock readable {} | |
| catch {${token}EventCoroutine} res opts | |
| if {[info commands ${token}EventCoroutine] ne {}} { | |
| # The coroutine can be deleted by completion (a non-yield return), by | |
| # http::Finish (when there is a premature end to the transaction), by | |
| # http::reset or http::cleanup, or if the caller set option -channel | |
| # but not option -handler: in the last case reading from the socket is | |
| # now managed by commands ::http::Copy*, http::ReceiveChunked, and | |
| # http::make-transformation-chunked. | |
| # | |
| # Catch in case the coroutine has closed the socket. | |
| catch {fileevent $sock readable [list http::EventGateway $sock $token]} | |
| } | |
| # If there was an error, re-throw it. | |
| return -options $opts $res | |
| } | |
| # http::NextPipelinedWrite | |
| # | |
| # - Connecting a socket to a token for writing is done by this command and by | |
| # command KeepSocket. | |
| # - If another request has a pipelined write scheduled for $token's socket, | |
| # and if the socket is ready to accept it, connect the write and update | |
| # the queue accordingly. | |
| # - This command is called from http::DoneRequest and http::Event, | |
| # IF $state(-pipeline) AND (the current transfer has reached the point at | |
| # which the socket is ready for the next request to be written). | |
| # - This command is called when a token has write access and is pipelined and | |
| # keep-alive, and sets socketWrState to Wready. | |
| # - The command need not consider the case where socketWrState is set to a token | |
| # that does not yet have write access. Such a token is waiting for Rready, | |
| # and the assignment of the connection to the token will be done elsewhere (in | |
| # http::KeepSocket). | |
| # - This command cannot be called after socketWrState has been set to a | |
| # "pending" token value (that is then overwritten by the caller), because that | |
| # value is set by this command when it is called by an earlier token when it | |
| # relinquishes its write access, and the pending token is always the next in | |
| # line to write. | |
| 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) | |
| } { | |
| # socketClosing(*) is set because the server has sent a | |
| # "Connection: close" header. | |
| # Behave as if the queues are empty - so do nothing. | |
| } 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) | |
| ] | |
| ) | |
| } { | |
| # - The usual case for a pipelined connection, ready for a new request. | |
| #Log pipelined, GRANT write access to $token2 in NextPipelinedWrite | |
| set conn [set ${token2}(tmpConnArgs)] | |
| set socketWrState($connId) $token2 | |
| set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] | |
| # Connect does its own fconfigure. | |
| fileevent $state(sock) writable [list http::Connect $token2 {*}$conn] | |
| #Log ---- $connId << conn to $token2 for HTTP request (b) | |
| # In the tests below, the next request will be nonpipeline. | |
| } 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") | |
| } { | |
| # The case in which the next request will be non-pipelined, and the read | |
| # and write queues is ready: which is the condition for a non-pipelined | |
| # write. | |
| variable $token3 | |
| upvar 0 $token3 state3 | |
| set conn [set ${token3}(tmpConnArgs)] | |
| #Log nonpipeline, GRANT r/w access to $token3 in NextPipelinedWrite | |
| set socketRdState($connId) $token3 | |
| set socketWrState($connId) $token3 | |
| set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end] | |
| # Connect does its own fconfigure. | |
| fileevent $state(sock) writable [list http::Connect $token3 {*}$conn] | |
| #Log ---- $state(sock) << conn to $token3 for HTTP request (c) | |
| } 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) | |
| ] | |
| ) | |
| } { | |
| # - The case in which the next request will be non-pipelined, but the | |
| # read queue is NOT ready. | |
| # - A read is queued or in progress, but not a write. Cannot start the | |
| # nonpipeline transaction, but must set socketWrState to prevent a new | |
| # pipelined request (in http::geturl) jumping the queue. | |
| # - Because socketWrState($connId) is not set to Wready, the assignment | |
| # of the connection to $token2 will be done elsewhere - by command | |
| # http::KeepSocket when $socketRdState($connId) is set to "Rready". | |
| #Log re-use nonpipeline, GRANT delayed write access to $token in NextP.. | |
| set socketWrState($connId) peNding | |
| } | |
| } | |
| # http::CancelReadPipeline | |
| # | |
| # Cancel pipelined responses on a closing "Keep-Alive" socket. | |
| # | |
| # - Called by a variable trace on "unset socketRdState($connId)". | |
| # - The variable relates to a Keep-Alive socket, which has been closed. | |
| # - Cancels all pipelined responses. The requests have been sent, | |
| # the responses have not yet been received. | |
| # - This is a hard cancel that ends each transaction with error status, | |
| # and closes the connection. Do not use it if you want to replay failed | |
| # transactions. | |
| # - N.B. Always delete ::http::socketRdState($connId) before deleting | |
| # ::http::socketRdQueue($connId), or this command will do nothing. | |
| # | |
| # Arguments | |
| # As for a trace command on a variable. | |
| proc http::CancelReadPipeline {name1 connId op} { | |
| variable socketRdQueue | |
| ##Log CancelReadPipeline $name1 $connId $op | |
| 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 ;#$msg | |
| } | |
| set socketRdQueue($connId) {} | |
| } | |
| } | |
| # http::CancelWritePipeline | |
| # | |
| # Cancel queued events on a closing "Keep-Alive" socket. | |
| # | |
| # - Called by a variable trace on "unset socketWrState($connId)". | |
| # - The variable relates to a Keep-Alive socket, which has been closed. | |
| # - In pipelined or nonpipeline case: cancels all queued requests. The | |
| # requests have not yet been sent, the responses are not due. | |
| # - This is a hard cancel that ends each transaction with error status, | |
| # and closes the connection. Do not use it if you want to replay failed | |
| # transactions. | |
| # - N.B. Always delete ::http::socketWrState($connId) before deleting | |
| # ::http::socketWrQueue($connId), or this command will do nothing. | |
| # | |
| # Arguments | |
| # As for a trace command on a variable. | |
| proc http::CancelWritePipeline {name1 connId op} { | |
| variable socketWrQueue | |
| ##Log CancelWritePipeline $name1 $connId $op | |
| 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 ;#$msg | |
| } | |
| set socketWrQueue($connId) {} | |
| } | |
| } | |
| # http::ReplayIfDead -- | |
| # | |
| # - A query on a re-used persistent socket failed at the earliest opportunity, | |
| # because the socket had been closed by the server. Keep the token, tidy up, | |
| # and try to connect on a fresh socket. | |
| # - The connection is monitored for eof by the command http::CheckEof. Thus | |
| # http::ReplayIfDead is needed only when a server event (half-closing an | |
| # apparently idle connection), and a client event (sending a request) occur at | |
| # almost the same time, and neither client nor server detects the other's | |
| # action before performing its own (an "asynchronous close event"). | |
| # - To simplify testing of http::ReplayIfDead, set TEST_EOF 1 in | |
| # http::KeepSocket, and then http::ReplayIfDead will be called if http::geturl | |
| # is called at any time after the server timeout. | |
| # | |
| # Arguments: | |
| # token Connection token. | |
| # | |
| # Side Effects: | |
| # Use the same token, but try to open a new socket. | |
| 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 | |
| # 1. Merge the tokens for transactions in flight, the read (response) queue, | |
| # and the write (request) queue. | |
| set InFlightR {} | |
| set InFlightW {} | |
| # Obtain the tokens for transactions in flight. | |
| if {$stateArg(-pipeline)} { | |
| # Two transactions may be in flight. The "read" transaction was first. | |
| # It is unlikely that the server would close the socket if a response | |
| # was pending; however, an earlier request (as well as the present | |
| # request) may have been sent and ignored if the socket was half-closed | |
| # by the server. | |
| 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 | |
| } | |
| # Report any inconsistency of $tokenArg with socket*state. | |
| 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 { | |
| # One transaction should be in flight. | |
| # socketRdState, socketWrQueue are used. | |
| # socketRdQueue should be empty. | |
| # Report any inconsistency of $tokenArg with socket*state. | |
| if {$tokenArg ne $socketRdState($stateArg(socketinfo))} { | |
| Log WARNING - ReplayIfDead nonpipeline tokenArg $tokenArg $doing \ | |
| ne socketRdState($stateArg(socketinfo)) \ | |
| $socketRdState($stateArg(socketinfo)) | |
| } | |
| # Report the inconsistency that socketRdQueue is non-empty. | |
| 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)) | |
| # 2. Tidy up tokenArg. This is a cut-down form of Finish/CloseSocket. | |
| # Do not change state(status). | |
| # No need to after cancel stateArg(after) - either this is done in | |
| # ReplayCore/ReInit, or Finish is called. | |
| catch {close $stateArg(sock)} | |
| # 2a. Tidy the tokens in the queues - this is done in ReplayCore/ReInit. | |
| # - Transactions, if any, that are awaiting responses cannot be completed. | |
| # They are listed for re-sending in newQueue. | |
| # - All tokens are preserved for re-use by ReplayCore, and their variables | |
| # will be re-initialised by calls to ReInit. | |
| # - The relevant element of socketMapping, socketRdState, socketWrState, | |
| # socketRdQueue, socketWrQueue, socketClosing, socketPlayCmd will be set | |
| # to new values in ReplayCore. | |
| ReplayCore $newQueue | |
| } | |
| # http::ReplayIfClose -- | |
| # | |
| # A request on a socket that was previously "Connection: keep-alive" has | |
| # received a "Connection: close" response header. The server supplies | |
| # that response correctly, but any later requests already queued on this | |
| # connection will be lost when the socket closes. | |
| # | |
| # This command takes arguments that represent the socketWrState, | |
| # socketRdQueue and socketWrQueue for this connection. The socketRdState | |
| # is not needed because the server responds in full to the request that | |
| # received the "Connection: close" response header. | |
| # | |
| # Existing request tokens $token (::http::$n) are preserved. The caller | |
| # will be unaware that the request was processed this way. | |
| 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 | |
| } | |
| # 1. Create newQueue | |
| set InFlightW {} | |
| if {$Wstate ni {Wready peNding}} { | |
| lappend InFlightW $Wstate | |
| } | |
| set newQueue {} | |
| lappend newQueue {*}$Rqueue | |
| lappend newQueue {*}$InFlightW | |
| lappend newQueue {*}$Wqueue | |
| # 2. Cleanup - none needed, done by the caller. | |
| ReplayCore $newQueue | |
| } | |
| # http::ReInit -- | |
| # | |
| # Command to restore a token's state to a condition that | |
| # makes it ready to replay a request. | |
| # | |
| # Command http::geturl stores extra state in state(tmp*) so | |
| # we don't need to do the argument processing again. | |
| # | |
| # The caller must: | |
| # - Set state(reusing) and state(sock) to their new values after calling | |
| # this command. | |
| # - Unset state(tmpState), state(tmpOpenCmd) if future calls to ReplayCore | |
| # or ReInit are inappropriate for this token. Typically only one retry | |
| # is allowed. | |
| # The caller may also unset state(tmpConnArgs) if this value (and the | |
| # token) will be used immediately. The value is needed by tokens that | |
| # will be stored in a queue. | |
| # | |
| # Arguments: | |
| # token Connection token. | |
| # | |
| # Return Value: (boolean) true iff the re-initialisation was successful. | |
| 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) | |
| } | |
| # Don't alter state(status) - this would trigger http::wait if it is in use. | |
| set tmpState $state(tmpState) | |
| set tmpOpenCmd $state(tmpOpenCmd) | |
| set tmpConnArgs $state(tmpConnArgs) | |
| foreach name [array names state] { | |
| if {$name ne "status"} { | |
| unset state($name) | |
| } | |
| } | |
| # Don't alter state(status). | |
| # Restore state(tmp*) - the caller may decide to unset them. | |
| # Restore state(tmpConnArgs) which is needed for connection. | |
| # state(tmpState), state(tmpOpenCmd) are needed only for retries. | |
| dict unset tmpState status | |
| array set state $tmpState | |
| set state(tmpState) $tmpState | |
| set state(tmpOpenCmd) $tmpOpenCmd | |
| set state(tmpConnArgs) $tmpConnArgs | |
| return 1 | |
| } | |
| # http::ReplayCore -- | |
| # | |
| # Command to replay a list of requests, using existing connection tokens. | |
| # | |
| # Abstracted from http::geturl which stores extra state in state(tmp*) so | |
| # we don't need to do the argument processing again. | |
| # | |
| # Arguments: | |
| # newQueue List of connection tokens. | |
| # | |
| # Side Effects: | |
| # Use existing tokens, but try to open a new socket. | |
| proc http::ReplayCore {newQueue} { | |
| variable socketMapping | |
| variable socketRdState | |
| variable socketWrState | |
| variable socketRdQueue | |
| variable socketWrQueue | |
| variable socketClosing | |
| variable socketPlayCmd | |
| if {[llength $newQueue] == 0} { | |
| # Nothing to do. | |
| return | |
| } | |
| ##Log running ReplayCore for {*}$newQueue | |
| set newToken [lindex $newQueue 0] | |
| set newQueue [lrange $newQueue 1 end] | |
| # 3. Use newToken, and restore its values of state(*). Do not restore | |
| # elements tmp* - we try again only once. | |
| 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] | |
| ##Log pre socket opened, - token $token | |
| ##Log $tmpOpenCmd - token $token | |
| # 4. Open a socket. | |
| if {[catch {eval $tmpOpenCmd} sock]} { | |
| # Something went wrong while trying to establish the connection. | |
| Log FAILED - $sock | |
| set state(sock) NONE | |
| Finish $token $sock | |
| return | |
| } | |
| ##Log post socket opened, - token $token | |
| set delay [expr {[clock milliseconds] - $pre}] | |
| if {$delay > 3000} { | |
| Log socket delay $delay - token $token | |
| } | |
| # Command [socket] is called with -async, but takes 5s to 5.1s to return, | |
| # with probability of order 1 in 10,000. This may be a bizarre scheduling | |
| # issue with my (KJN's) system (Fedora Linux). | |
| # This does not cause a problem (unless the request times out when this | |
| # command returns). | |
| # 5. Configure the persistent socket data. | |
| 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)} { | |
| #Log new, init for pipelined, GRANT write acc to $token ReplayCore | |
| set socketRdState($state(socketinfo)) $token | |
| set socketWrState($state(socketinfo)) $token | |
| } else { | |
| #Log new, init for nonpipeline, GRANT r/w acc to $token ReplayCore | |
| 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 {} {}} | |
| } | |
| ##Log pre newQueue ReInit, - token $token | |
| # 6. Configure sockets in the queue. | |
| 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} | |
| } | |
| } | |
| # 7. Configure the socket for newToken to send a request. | |
| set state(sock) $sock | |
| Log "Using $sock for $state(socketinfo) - token $token" \ | |
| [expr {$state(-keepalive)?"keepalive":""}] | |
| # Initialisation of a new socket. | |
| ##Log socket opened, now fconfigure - token $token | |
| fconfigure $sock -translation {auto crlf} -buffersize $state(-blocksize) | |
| ##Log socket opened, DONE fconfigure - token $token | |
| # Connect does its own fconfigure. | |
| fileevent $sock writable [list http::Connect $token {*}$tmpConnArgs] | |
| #Log ---- $sock << conn to $token for HTTP request (e) | |
| } | |
| # Data access functions: | |
| # Data - the URL data | |
| # Status - the transaction status: ok, reset, eof, timeout, error | |
| # Code - the HTTP transaction code, e.g., 200 | |
| # Size - the size of the URL data | |
| 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 "" | |
| } | |
| # http::cleanup | |
| # | |
| # Garbage collect the state associated with a transaction | |
| # | |
| # Arguments | |
| # token The token returned from http::geturl | |
| # | |
| # Side Effects | |
| # unsets the state array | |
| 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 | |
| } | |
| } | |
| # http::Connect | |
| # | |
| # This callback is made when an asyncronous connection completes. | |
| # | |
| # Arguments | |
| # token The token returned from http::geturl | |
| # | |
| # Side Effects | |
| # Sets the status of the connection, which unblocks | |
| # the waiting geturl call | |
| 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)} { | |
| # The socket was closed at the server end, and closed at | |
| # this end by http::CheckEof. | |
| if {[TestForReplay $token write $err b]} { | |
| return | |
| } | |
| # else: | |
| # This is NOT a persistent socket that has been closed since its | |
| # last use. | |
| # If any other requests are in flight or pipelined/queued, they will | |
| # be discarded. | |
| } | |
| Finish $token "connect failed $err" | |
| } else { | |
| set state(state) connecting | |
| fileevent $state(sock) writable {} | |
| ::http::Connected $token $proto $phost $srvurl | |
| } | |
| } | |
| # http::Write | |
| # | |
| # Write POST query data to the socket | |
| # | |
| # Arguments | |
| # token The token for the connection | |
| # | |
| # Side Effects | |
| # Write the socket and handle callbacks. | |
| 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) | |
| # Output a block. Tcl will buffer this if the socket blocks | |
| set done 0 | |
| if {[catch { | |
| # Catch I/O errors on dead sockets | |
| if {[info exists state(-query)]} { | |
| # Chop up large query strings so queryprogress callback can give | |
| # smooth feedback. | |
| if { $state(queryoffset) + $state(-queryblocksize) | |
| >= $state(querylength) | |
| } { | |
| # This will be the last puts for the request-body. | |
| if { (![catch {fileevent $sock readable} binding]) | |
| && ($binding eq [list http::CheckEof $sock]) | |
| } { | |
| # Remove the "fileevent readable" binding of an idle | |
| # persistent socket to http::CheckEof. We can no longer | |
| # treat bytes received as junk. The server might still time | |
| # out and half-close the socket if it has not yet received | |
| # the first "puts". | |
| 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 { | |
| # Copy blocks from the query channel | |
| set outStr [read $state(-querychannel) $state(-queryblocksize)] | |
| if {[eof $state(-querychannel)]} { | |
| # This will be the last puts for the request-body. | |
| if { (![catch {fileevent $sock readable} binding]) | |
| && ($binding eq [list http::CheckEof $sock]) | |
| } { | |
| # Remove the "fileevent readable" binding of an idle | |
| # persistent socket to http::CheckEof. We can no longer | |
| # treat bytes received as junk. The server might still time | |
| # out and half-close the socket if it has not yet received | |
| # the first "puts". | |
| fileevent $sock readable {} | |
| } | |
| } | |
| puts -nonewline $sock $outStr | |
| incr state(queryoffset) [string length $outStr] | |
| if {[eof $state(-querychannel)]} { | |
| set done 1 | |
| } | |
| } | |
| } err]} { | |
| # Do not call Finish here, but instead let the read half of the socket | |
| # process whatever server reply there is to get. | |
| set state(posterror) $err | |
| set done 1 | |
| } | |
| if {$done} { | |
| catch {flush $sock} | |
| fileevent $sock writable {} | |
| Log ^C$tk end sending request - token $token | |
| # End of writing (POST method). The request has been sent. | |
| DoneRequest $token | |
| } | |
| # Callback to the client after we've completely handled everything. | |
| if {[string length $state(-queryprogress)]} { | |
| eval $state(-queryprogress) \ | |
| [list $token $state(querylength) $state(queryoffset)] | |
| } | |
| } | |
| # http::Event | |
| # | |
| # Handle input on the socket. This command is the core of | |
| # the coroutine commands ${token}EventCoroutine that are | |
| # bound to "fileevent $sock readable" and process input. | |
| # | |
| # Arguments | |
| # sock The socket receiving input. | |
| # token The token returned from http::geturl | |
| # | |
| # Side Effects | |
| # Read the socket and handle callbacks. | |
| 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 | |
| ##Log Event call - token $token | |
| 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"} { | |
| ##Log - connecting - token $token | |
| 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)} { | |
| # The socket was closed at the server end, and closed at | |
| # this end by http::CheckEof. | |
| if {[TestForReplay $token read $nsl c]} { | |
| return | |
| } | |
| # else: | |
| # This is NOT a persistent socket that has been closed since | |
| # its last use. | |
| # If any other requests are in flight or pipelined/queued, | |
| # they will be discarded. | |
| } else { | |
| Log ^X$tk end of response (error) - token $token | |
| Finish $token $nsl | |
| return | |
| } | |
| } elseif {$nsl >= 0} { | |
| ##Log - connecting 1 - token $token | |
| set state(state) "header" | |
| } elseif { [eof $sock] | |
| && [info exists state(reusing)] | |
| && $state(reusing) | |
| } { | |
| # The socket was closed at the server end, and we didn't notice. | |
| # This is the first read - where the closure is usually first | |
| # detected. | |
| if {[TestForReplay $token read {} d]} { | |
| return | |
| } | |
| # else: | |
| # This is NOT a persistent socket that has been closed since its | |
| # last use. | |
| # If any other requests are in flight or pipelined/queued, they | |
| # will be discarded. | |
| } | |
| } elseif {$state(state) eq "header"} { | |
| if {[catch {gets $sock line} nhl]} { | |
| ##Log header failed - token $token | |
| Log ^X$tk end of response (error) - token $token | |
| Finish $token $nhl | |
| return | |
| } elseif {$nhl == 0} { | |
| ##Log header done - token $token | |
| Log ^E$tk end of response headers - token $token | |
| # We have now read all headers | |
| # We ignore HTTP/1.1 100 Continue returns. RFC2616 sec 8.2.3 | |
| if { ($state(http) == "") | |
| || ([regexp {^\S+\s(\d+)} $state(http) {} x] && $x == 100) | |
| } { | |
| set state(state) "connecting" | |
| continue | |
| # This was a "return" in the pre-coroutine code. | |
| } | |
| if { ([info exists state(connection)]) | |
| && ([info exists socketMapping($state(socketinfo))]) | |
| && ($state(connection) eq "keep-alive") | |
| && ($state(-keepalive)) | |
| && (!$state(reusing)) | |
| && ($state(-pipeline)) | |
| } { | |
| # Response headers received for first request on a | |
| # persistent socket. Now ready for pipelined writes (if | |
| # any). | |
| # Previous value is $token. It cannot be "pending". | |
| set socketWrState($state(socketinfo)) Wready | |
| http::NextPipelinedWrite $token | |
| } | |
| # Once a "close" has been signaled, the client MUST NOT send any | |
| # more requests on that connection. | |
| # | |
| # If either the client or the server sends the "close" token in | |
| # the Connection header, that request becomes the last one for | |
| # the connection. | |
| if { ([info exists state(connection)]) | |
| && ([info exists socketMapping($state(socketinfo))]) | |
| && ($state(connection) eq "close") | |
| && ($state(-keepalive)) | |
| } { | |
| # The server warns that it will close the socket after this | |
| # response. | |
| ##Log WARNING - socket will close after response for $token | |
| # Prepare data for a call to ReplayIfClose. | |
| 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" | |
| ##Log $msg - token $token | |
| } | |
| set socketPlayCmd($state(socketinfo)) \ | |
| [list ReplayIfClose $InFlightW \ | |
| $socketRdQueue($state(socketinfo)) \ | |
| $socketWrQueue($state(socketinfo))] | |
| # - All tokens are preserved for re-use by ReplayCore. | |
| # - Queues are preserved in case of Finish with error, | |
| # but are not used for anything else because | |
| # socketClosing(*) is set below. | |
| # - Cancel the state(after) timeout events. | |
| 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 {} {}} | |
| } | |
| # Do not allow further connections on this socket. | |
| set socketClosing($state(socketinfo)) 1 | |
| } | |
| set state(state) body | |
| # If doing a HEAD, then we won't get any body | |
| if {$state(-validate)} { | |
| Log ^F$tk end of response for HEAD request - token $token | |
| set state(state) complete | |
| Eot $token | |
| return | |
| } | |
| # - For non-chunked transfer we may have no body - in this case | |
| # we may get no further file event if the connection doesn't | |
| # close and no more data is sent. We can tell and must finish | |
| # up now - not later - the alternative would be to wait until | |
| # the server times out. | |
| # - In this case, the server has NOT told the client it will | |
| # close the connection, AND it has NOT indicated the resource | |
| # length EITHER by setting the Content-Length (totalsize) OR | |
| # by using chunked Transfer-Encoding. | |
| # - Do not worry here about the case (Connection: close) because | |
| # the server should close the connection. | |
| # - IF (NOT Connection: close) AND (NOT chunked encoding) AND | |
| # (totalsize == 0). | |
| 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 | |
| } | |
| # We have to use binary translation to count bytes properly. | |
| lassign [fconfigure $sock -translation] trRead trWrite | |
| fconfigure $sock -translation [list binary $trWrite] | |
| if { | |
| $state(-binary) || [IsBinaryContentType $state(type)] | |
| } { | |
| # Turn off conversions for non-text data. | |
| 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)]} { | |
| # Initiate a sequence of background fcopies. | |
| fileevent $sock readable {} | |
| rename ${token}EventCoroutine {} | |
| CopyStart $sock $token | |
| return | |
| } | |
| } | |
| } elseif {$nhl > 0} { | |
| # Process header lines. | |
| ##Log header - token $token - $line | |
| if {[regexp -nocase {^([^:]+):(.+)$} $line x key value]} { | |
| switch -- [string tolower $key] { | |
| content-type { | |
| set state(type) [string trim [string tolower $value]] | |
| # Grab the optional charset information. | |
| 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 tmpHeader [string trim [string tolower $value]] | |
| # RFC 7230 Section 6.1 states that a comma-separated | |
| # list is an acceptable value. According to | |
| # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection | |
| # any comma-separated list implies keep-alive, but I | |
| # don't see this in the RFC so we'll play safe and | |
| # scan any list for "close". | |
| if {$tmpHeader in {close keep-alive}} { | |
| # The common cases, continue. | |
| } elseif {[string first , $tmpHeader] < 0} { | |
| # Not a comma-separated list, not "close", | |
| # therefore "keep-alive". | |
| set tmpHeader keep-alive | |
| } else { | |
| set tmpResult keep-alive | |
| set tmpCsl [split $tmpHeader ,] | |
| # Optional whitespace either side of separator. | |
| foreach el $tmpCsl { | |
| if {[string trim $el] eq {close}} { | |
| set tmpResult close | |
| break | |
| } | |
| } | |
| set tmpHeader $tmpResult | |
| } | |
| set state(connection) $tmpHeader | |
| } | |
| } | |
| 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 {"json" "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 | |
| # http::ProxyRequired -- | |
| # Default proxy filter. | |
| # | |
| # Arguments: | |
| # host The destination host | |
| # | |
| # Results: | |
| # The current proxy settings | |
| 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)] | |
| } | |
| } | |
| # http::CharsetToEncoding -- | |
| # | |
| # Tries to map a given IANA charset to a tcl encoding. If no encoding | |
| # can be found, returns binary. | |
| # | |
| 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 { | |
| # other charset, like euc-xx, utf-8,... may directly map to encoding | |
| set encoding $charset | |
| } | |
| set idx [lsearch -exact $encodings $encoding] | |
| if {$idx >= 0} { | |
| return $encoding | |
| } else { | |
| return "binary" | |
| } | |
| } | |
| # Return the list of content-encoding transformations we need to do in order. | |
| 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} { | |
| # channel might have been closed in the callback | |
| 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 | |
| } | |
| # Local variables: | |
| # indent-tabs-mode: t | |
| # End: | |