12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506 |
- # 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:xyzzy@www.bogus.net: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:
|