[5049] | 1 | # http.tcl -- |
---|
| 2 | # |
---|
| 3 | # Client-side HTTP for GET, POST, and HEAD commands. These routines can |
---|
| 4 | # be used in untrusted code that uses the Safesock security policy. These |
---|
| 5 | # procedures use a callback interface to avoid using vwait, which is not |
---|
| 6 | # defined in the safe base. |
---|
| 7 | # |
---|
| 8 | # See the file "license.terms" for information on usage and redistribution of |
---|
| 9 | # this file, and for a DISCLAIMER OF ALL WARRANTIES. |
---|
| 10 | # |
---|
| 11 | # RCS: @(#) $Id: http.tcl,v 1.67 2008/03/12 10:01:02 hobbs Exp $ |
---|
| 12 | |
---|
| 13 | package require Tcl 8.4 |
---|
| 14 | # Keep this in sync with pkgIndex.tcl and with the install directories |
---|
| 15 | # in Makefiles |
---|
| 16 | package provide http 2.7 |
---|
| 17 | |
---|
| 18 | namespace eval http { |
---|
| 19 | # Allow resourcing to not clobber existing data |
---|
| 20 | |
---|
| 21 | variable http |
---|
| 22 | if {![info exists http]} { |
---|
| 23 | array set http { |
---|
| 24 | -accept */* |
---|
| 25 | -proxyhost {} |
---|
| 26 | -proxyport {} |
---|
| 27 | -proxyfilter http::ProxyRequired |
---|
| 28 | -urlencoding utf-8 |
---|
| 29 | } |
---|
| 30 | set http(-useragent) "Tcl http client package [package provide http]" |
---|
| 31 | } |
---|
| 32 | |
---|
| 33 | proc init {} { |
---|
| 34 | # Set up the map for quoting chars. RFC3986 Section 2.3 say percent |
---|
| 35 | # encode all except: "... percent-encoded octets in the ranges of ALPHA |
---|
| 36 | # (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), period (%2E), |
---|
| 37 | # underscore (%5F), or tilde (%7E) should not be created by URI |
---|
| 38 | # producers ..." |
---|
| 39 | for {set i 0} {$i <= 256} {incr i} { |
---|
| 40 | set c [format %c $i] |
---|
| 41 | if {![string match {[-._~a-zA-Z0-9]} $c]} { |
---|
| 42 | set map($c) %[format %.2x $i] |
---|
| 43 | } |
---|
| 44 | } |
---|
| 45 | # These are handled specially |
---|
| 46 | set map(\n) %0d%0a |
---|
| 47 | variable formMap [array get map] |
---|
| 48 | |
---|
| 49 | # Create a map for HTTP/1.1 open sockets |
---|
| 50 | variable socketmap |
---|
| 51 | if {[info exists socketmap]} { |
---|
| 52 | # Close but don't remove open sockets on re-init |
---|
| 53 | foreach {url sock} [array get socketmap] { |
---|
| 54 | catch {close $sock} |
---|
| 55 | } |
---|
| 56 | } |
---|
| 57 | array set socketmap {} |
---|
| 58 | } |
---|
| 59 | init |
---|
| 60 | |
---|
| 61 | variable urlTypes |
---|
| 62 | if {![info exists urlTypes]} { |
---|
| 63 | set urlTypes(http) [list 80 ::socket] |
---|
| 64 | } |
---|
| 65 | |
---|
| 66 | variable encodings [string tolower [encoding names]] |
---|
| 67 | # This can be changed, but iso8859-1 is the RFC standard. |
---|
| 68 | variable defaultCharset |
---|
| 69 | if {![info exists defaultCharset]} { |
---|
| 70 | set defaultCharset "iso8859-1" |
---|
| 71 | } |
---|
| 72 | |
---|
| 73 | # Force RFC 3986 strictness in geturl url verification? |
---|
| 74 | variable strict |
---|
| 75 | if {![info exists strict]} { |
---|
| 76 | set strict 1 |
---|
| 77 | } |
---|
| 78 | |
---|
| 79 | # Let user control default keepalive for compatibility |
---|
| 80 | variable defaultKeepalive |
---|
| 81 | if {![info exists defaultKeepalive]} { |
---|
| 82 | set defaultKeepalive 0 |
---|
| 83 | } |
---|
| 84 | |
---|
| 85 | namespace export geturl config reset wait formatQuery register unregister |
---|
| 86 | # Useful, but not exported: data size status code |
---|
| 87 | } |
---|
| 88 | |
---|
| 89 | # http::Log -- |
---|
| 90 | # |
---|
| 91 | # Debugging output -- define this to observe HTTP/1.1 socket usage. |
---|
| 92 | # Should echo any args received. |
---|
| 93 | # |
---|
| 94 | # Arguments: |
---|
| 95 | # msg Message to output |
---|
| 96 | # |
---|
| 97 | proc http::Log {args} {} |
---|
| 98 | |
---|
| 99 | # http::register -- |
---|
| 100 | # |
---|
| 101 | # See documentation for details. |
---|
| 102 | # |
---|
| 103 | # Arguments: |
---|
| 104 | # proto URL protocol prefix, e.g. https |
---|
| 105 | # port Default port for protocol |
---|
| 106 | # command Command to use to create socket |
---|
| 107 | # Results: |
---|
| 108 | # list of port and command that was registered. |
---|
| 109 | |
---|
| 110 | proc http::register {proto port command} { |
---|
| 111 | variable urlTypes |
---|
| 112 | set urlTypes($proto) [list $port $command] |
---|
| 113 | } |
---|
| 114 | |
---|
| 115 | # http::unregister -- |
---|
| 116 | # |
---|
| 117 | # Unregisters URL protocol handler |
---|
| 118 | # |
---|
| 119 | # Arguments: |
---|
| 120 | # proto URL protocol prefix, e.g. https |
---|
| 121 | # Results: |
---|
| 122 | # list of port and command that was unregistered. |
---|
| 123 | |
---|
| 124 | proc http::unregister {proto} { |
---|
| 125 | variable urlTypes |
---|
| 126 | if {![info exists urlTypes($proto)]} { |
---|
| 127 | return -code error "unsupported url type \"$proto\"" |
---|
| 128 | } |
---|
| 129 | set old $urlTypes($proto) |
---|
| 130 | unset urlTypes($proto) |
---|
| 131 | return $old |
---|
| 132 | } |
---|
| 133 | |
---|
| 134 | # http::config -- |
---|
| 135 | # |
---|
| 136 | # See documentation for details. |
---|
| 137 | # |
---|
| 138 | # Arguments: |
---|
| 139 | # args Options parsed by the procedure. |
---|
| 140 | # Results: |
---|
| 141 | # TODO |
---|
| 142 | |
---|
| 143 | proc http::config {args} { |
---|
| 144 | variable http |
---|
| 145 | set options [lsort [array names http -*]] |
---|
| 146 | set usage [join $options ", "] |
---|
| 147 | if {[llength $args] == 0} { |
---|
| 148 | set result {} |
---|
| 149 | foreach name $options { |
---|
| 150 | lappend result $name $http($name) |
---|
| 151 | } |
---|
| 152 | return $result |
---|
| 153 | } |
---|
| 154 | set options [string map {- ""} $options] |
---|
| 155 | set pat ^-([join $options |])$ |
---|
| 156 | if {[llength $args] == 1} { |
---|
| 157 | set flag [lindex $args 0] |
---|
| 158 | if {[regexp -- $pat $flag]} { |
---|
| 159 | return $http($flag) |
---|
| 160 | } else { |
---|
| 161 | return -code error "Unknown option $flag, must be: $usage" |
---|
| 162 | } |
---|
| 163 | } else { |
---|
| 164 | foreach {flag value} $args { |
---|
| 165 | if {[regexp -- $pat $flag]} { |
---|
| 166 | set http($flag) $value |
---|
| 167 | } else { |
---|
| 168 | return -code error "Unknown option $flag, must be: $usage" |
---|
| 169 | } |
---|
| 170 | } |
---|
| 171 | } |
---|
| 172 | } |
---|
| 173 | |
---|
| 174 | # http::Finish -- |
---|
| 175 | # |
---|
| 176 | # Clean up the socket and eval close time callbacks |
---|
| 177 | # |
---|
| 178 | # Arguments: |
---|
| 179 | # token Connection token. |
---|
| 180 | # errormsg (optional) If set, forces status to error. |
---|
| 181 | # skipCB (optional) If set, don't call the -command callback. This |
---|
| 182 | # is useful when geturl wants to throw an exception instead |
---|
| 183 | # of calling the callback. That way, the same error isn't |
---|
| 184 | # reported to two places. |
---|
| 185 | # |
---|
| 186 | # Side Effects: |
---|
| 187 | # Closes the socket |
---|
| 188 | |
---|
| 189 | proc http::Finish { token {errormsg ""} {skipCB 0}} { |
---|
| 190 | variable $token |
---|
| 191 | upvar 0 $token state |
---|
| 192 | global errorInfo errorCode |
---|
| 193 | if {$errormsg ne ""} { |
---|
| 194 | set state(error) [list $errormsg $errorInfo $errorCode] |
---|
| 195 | set state(status) "error" |
---|
| 196 | } |
---|
| 197 | if {($state(status) eq "timeout") || ($state(status) eq "error") |
---|
| 198 | || ([info exists state(connection)] && ($state(connection) eq "close")) |
---|
| 199 | } { |
---|
| 200 | CloseSocket $state(sock) $token |
---|
| 201 | } |
---|
| 202 | if {[info exists state(after)]} { after cancel $state(after) } |
---|
| 203 | if {[info exists state(-command)] && !$skipCB} { |
---|
| 204 | if {[catch {eval $state(-command) {$token}} err]} { |
---|
| 205 | if {$errormsg eq ""} { |
---|
| 206 | set state(error) [list $err $errorInfo $errorCode] |
---|
| 207 | set state(status) error |
---|
| 208 | } |
---|
| 209 | } |
---|
| 210 | # Command callback may already have unset our state |
---|
| 211 | unset -nocomplain state(-command) |
---|
| 212 | } |
---|
| 213 | } |
---|
| 214 | |
---|
| 215 | # http::CloseSocket - |
---|
| 216 | # |
---|
| 217 | # Close a socket and remove it from the persistent sockets table. |
---|
| 218 | # If possible an http token is included here but when we are called |
---|
| 219 | # from a fileevent on remote closure we need to find the correct |
---|
| 220 | # entry - hence the second section. |
---|
| 221 | |
---|
| 222 | proc ::http::CloseSocket {s {token {}}} { |
---|
| 223 | variable socketmap |
---|
| 224 | catch {fileevent $s readable {}} |
---|
| 225 | set conn_id {} |
---|
| 226 | if {$token ne ""} { |
---|
| 227 | variable $token |
---|
| 228 | upvar 0 $token state |
---|
| 229 | if {[info exists state(socketinfo)]} { |
---|
| 230 | set conn_id $state(socketinfo) |
---|
| 231 | } |
---|
| 232 | } else { |
---|
| 233 | set map [array get socketmap] |
---|
| 234 | set ndx [lsearch -exact $map $s] |
---|
| 235 | if {$ndx != -1} { |
---|
| 236 | incr ndx -1 |
---|
| 237 | set conn_id [lindex $map $ndx] |
---|
| 238 | } |
---|
| 239 | } |
---|
| 240 | if {$conn_id eq {} || ![info exists socketmap($conn_id)]} { |
---|
| 241 | Log "Closing socket $s (no connection info)" |
---|
| 242 | if {[catch {close $s} err]} { Log "Error: $err" } |
---|
| 243 | } else { |
---|
| 244 | if {[info exists socketmap($conn_id)]} { |
---|
| 245 | Log "Closing connection $conn_id (sock $socketmap($conn_id))" |
---|
| 246 | if {[catch {close $socketmap($conn_id)} err]} { Log "Error: $err" } |
---|
| 247 | unset socketmap($conn_id) |
---|
| 248 | } else { |
---|
| 249 | Log "Cannot close connection $conn_id - no socket in socket map" |
---|
| 250 | } |
---|
| 251 | } |
---|
| 252 | } |
---|
| 253 | |
---|
| 254 | # http::reset -- |
---|
| 255 | # |
---|
| 256 | # See documentation for details. |
---|
| 257 | # |
---|
| 258 | # Arguments: |
---|
| 259 | # token Connection token. |
---|
| 260 | # why Status info. |
---|
| 261 | # |
---|
| 262 | # Side Effects: |
---|
| 263 | # See Finish |
---|
| 264 | |
---|
| 265 | proc http::reset { token {why reset} } { |
---|
| 266 | variable $token |
---|
| 267 | upvar 0 $token state |
---|
| 268 | set state(status) $why |
---|
| 269 | catch {fileevent $state(sock) readable {}} |
---|
| 270 | catch {fileevent $state(sock) writable {}} |
---|
| 271 | Finish $token |
---|
| 272 | if {[info exists state(error)]} { |
---|
| 273 | set errorlist $state(error) |
---|
| 274 | unset state |
---|
| 275 | eval ::error $errorlist |
---|
| 276 | } |
---|
| 277 | } |
---|
| 278 | |
---|
| 279 | # http::geturl -- |
---|
| 280 | # |
---|
| 281 | # Establishes a connection to a remote url via http. |
---|
| 282 | # |
---|
| 283 | # Arguments: |
---|
| 284 | # url The http URL to goget. |
---|
| 285 | # args Option value pairs. Valid options include: |
---|
| 286 | # -blocksize, -validate, -headers, -timeout |
---|
| 287 | # Results: |
---|
| 288 | # Returns a token for this connection. This token is the name of an array |
---|
| 289 | # that the caller should unset to garbage collect the state. |
---|
| 290 | |
---|
| 291 | proc http::geturl { url args } { |
---|
| 292 | variable http |
---|
| 293 | variable urlTypes |
---|
| 294 | variable defaultCharset |
---|
| 295 | variable defaultKeepalive |
---|
| 296 | variable strict |
---|
| 297 | |
---|
| 298 | # Initialize the state variable, an array. We'll return the name of this |
---|
| 299 | # array as the token for the transaction. |
---|
| 300 | |
---|
| 301 | if {![info exists http(uid)]} { |
---|
| 302 | set http(uid) 0 |
---|
| 303 | } |
---|
| 304 | set token [namespace current]::[incr http(uid)] |
---|
| 305 | variable $token |
---|
| 306 | upvar 0 $token state |
---|
| 307 | reset $token |
---|
| 308 | |
---|
| 309 | # Process command options. |
---|
| 310 | |
---|
| 311 | array set state { |
---|
| 312 | -binary false |
---|
| 313 | -blocksize 8192 |
---|
| 314 | -queryblocksize 8192 |
---|
| 315 | -validate 0 |
---|
| 316 | -headers {} |
---|
| 317 | -timeout 0 |
---|
| 318 | -type application/x-www-form-urlencoded |
---|
| 319 | -queryprogress {} |
---|
| 320 | -protocol 1.1 |
---|
| 321 | binary 0 |
---|
| 322 | state header |
---|
| 323 | meta {} |
---|
| 324 | coding {} |
---|
| 325 | currentsize 0 |
---|
| 326 | totalsize 0 |
---|
| 327 | querylength 0 |
---|
| 328 | queryoffset 0 |
---|
| 329 | type text/html |
---|
| 330 | body {} |
---|
| 331 | status "" |
---|
| 332 | http "" |
---|
| 333 | connection close |
---|
| 334 | } |
---|
| 335 | set state(-keepalive) $defaultKeepalive |
---|
| 336 | set state(-strict) $strict |
---|
| 337 | # These flags have their types verified [Bug 811170] |
---|
| 338 | array set type { |
---|
| 339 | -binary boolean |
---|
| 340 | -blocksize integer |
---|
| 341 | -queryblocksize integer |
---|
| 342 | -strict boolean |
---|
| 343 | -timeout integer |
---|
| 344 | -validate boolean |
---|
| 345 | } |
---|
| 346 | set state(charset) $defaultCharset |
---|
| 347 | set options { |
---|
| 348 | -binary -blocksize -channel -command -handler -headers -keepalive |
---|
| 349 | -method -myaddr -progress -protocol -query -queryblocksize |
---|
| 350 | -querychannel -queryprogress -strict -timeout -type -validate |
---|
| 351 | } |
---|
| 352 | set usage [join [lsort $options] ", "] |
---|
| 353 | set options [string map {- ""} $options] |
---|
| 354 | set pat ^-([join $options |])$ |
---|
| 355 | foreach {flag value} $args { |
---|
| 356 | if {[regexp -- $pat $flag]} { |
---|
| 357 | # Validate numbers |
---|
| 358 | if {[info exists type($flag)] && |
---|
| 359 | ![string is $type($flag) -strict $value]} { |
---|
| 360 | unset $token |
---|
| 361 | return -code error "Bad value for $flag ($value), must be $type($flag)" |
---|
| 362 | } |
---|
| 363 | set state($flag) $value |
---|
| 364 | } else { |
---|
| 365 | unset $token |
---|
| 366 | return -code error "Unknown option $flag, can be: $usage" |
---|
| 367 | } |
---|
| 368 | } |
---|
| 369 | |
---|
| 370 | # Make sure -query and -querychannel aren't both specified |
---|
| 371 | |
---|
| 372 | set isQueryChannel [info exists state(-querychannel)] |
---|
| 373 | set isQuery [info exists state(-query)] |
---|
| 374 | if {$isQuery && $isQueryChannel} { |
---|
| 375 | unset $token |
---|
| 376 | return -code error "Can't combine -query and -querychannel options!" |
---|
| 377 | } |
---|
| 378 | |
---|
| 379 | # Validate URL, determine the server host and port, and check proxy case |
---|
| 380 | # Recognize user:pass@host URLs also, although we do not do anything with |
---|
| 381 | # that info yet. |
---|
| 382 | |
---|
| 383 | # URLs have basically four parts. |
---|
| 384 | # First, before the colon, is the protocol scheme (e.g. http) |
---|
| 385 | # Second, for HTTP-like protocols, is the authority |
---|
| 386 | # The authority is preceded by // and lasts up to (but not including) |
---|
| 387 | # the following / and it identifies up to four parts, of which only one, |
---|
| 388 | # the host, is required (if an authority is present at all). All other |
---|
| 389 | # parts of the authority (user name, password, port number) are optional. |
---|
| 390 | # Third is the resource name, which is split into two parts at a ? |
---|
| 391 | # The first part (from the single "/" up to "?") is the path, and the |
---|
| 392 | # second part (from that "?" up to "#") is the query. *HOWEVER*, we do |
---|
| 393 | # not need to separate them; we send the whole lot to the server. |
---|
| 394 | # Fourth is the fragment identifier, which is everything after the first |
---|
| 395 | # "#" in the URL. The fragment identifier MUST NOT be sent to the server |
---|
| 396 | # and indeed, we don't bother to validate it (it could be an error to |
---|
| 397 | # pass it in here, but it's cheap to strip). |
---|
| 398 | # |
---|
| 399 | # An example of a URL that has all the parts: |
---|
| 400 | # http://jschmoe:xyzzy@www.bogus.net:8000/foo/bar.tml?q=foo#changes |
---|
| 401 | # The "http" is the protocol, the user is "jschmoe", the password is |
---|
| 402 | # "xyzzy", the host is "www.bogus.net", the port is "8000", the path is |
---|
| 403 | # "/foo/bar.tml", the query is "q=foo", and the fragment is "changes". |
---|
| 404 | # |
---|
| 405 | # Note that the RE actually combines the user and password parts, as |
---|
| 406 | # recommended in RFC 3986. Indeed, that RFC states that putting passwords |
---|
| 407 | # in URLs is a Really Bad Idea, something with which I would agree utterly. |
---|
| 408 | # Also note that we do not currently support IPv6 addresses. |
---|
| 409 | # |
---|
| 410 | # From a validation perspective, we need to ensure that the parts of the |
---|
| 411 | # URL that are going to the server are correctly encoded. |
---|
| 412 | # This is only done if $state(-strict) is true (inherited from |
---|
| 413 | # $::http::strict). |
---|
| 414 | |
---|
| 415 | set URLmatcher {(?x) # this is _expanded_ syntax |
---|
| 416 | ^ |
---|
| 417 | (?: (\w+) : ) ? # <protocol scheme> |
---|
| 418 | (?: // |
---|
| 419 | (?: |
---|
| 420 | ( |
---|
| 421 | [^@/\#?]+ # <userinfo part of authority> |
---|
| 422 | ) @ |
---|
| 423 | )? |
---|
| 424 | ( [^/:\#?]+ ) # <host part of authority> |
---|
| 425 | (?: : (\d+) )? # <port part of authority> |
---|
| 426 | )? |
---|
| 427 | ( / [^\#?]* (?: \? [^\#?]* )?)? # <path> (including query) |
---|
| 428 | (?: \# (.*) )? # <fragment> |
---|
| 429 | $ |
---|
| 430 | } |
---|
| 431 | |
---|
| 432 | # Phase one: parse |
---|
| 433 | if {![regexp -- $URLmatcher $url -> proto user host port srvurl]} { |
---|
| 434 | unset $token |
---|
| 435 | return -code error "Unsupported URL: $url" |
---|
| 436 | } |
---|
| 437 | # Phase two: validate |
---|
| 438 | if {$host eq ""} { |
---|
| 439 | # Caller has to provide a host name; we do not have a "default host" |
---|
| 440 | # that would enable us to handle relative URLs. |
---|
| 441 | unset $token |
---|
| 442 | return -code error "Missing host part: $url" |
---|
| 443 | # Note that we don't check the hostname for validity here; if it's |
---|
| 444 | # invalid, we'll simply fail to resolve it later on. |
---|
| 445 | } |
---|
| 446 | if {$port ne "" && $port > 65535} { |
---|
| 447 | unset $token |
---|
| 448 | return -code error "Invalid port number: $port" |
---|
| 449 | } |
---|
| 450 | # The user identification and resource identification parts of the URL can |
---|
| 451 | # have encoded characters in them; take care! |
---|
| 452 | if {$user ne ""} { |
---|
| 453 | # Check for validity according to RFC 3986, Appendix A |
---|
| 454 | set validityRE {(?xi) |
---|
| 455 | ^ |
---|
| 456 | (?: [-\w.~!$&'()*+,;=:] | %[0-9a-f][0-9a-f] )+ |
---|
| 457 | $ |
---|
| 458 | } |
---|
| 459 | if {$state(-strict) && ![regexp -- $validityRE $user]} { |
---|
| 460 | unset $token |
---|
| 461 | # Provide a better error message in this error case |
---|
| 462 | if {[regexp {(?i)%(?![0-9a-f][0-9a-f]).?.?} $user bad]} { |
---|
| 463 | return -code error \ |
---|
| 464 | "Illegal encoding character usage \"$bad\" in URL user" |
---|
| 465 | } |
---|
| 466 | return -code error "Illegal characters in URL user" |
---|
| 467 | } |
---|
| 468 | } |
---|
| 469 | if {$srvurl ne ""} { |
---|
| 470 | # Check for validity according to RFC 3986, Appendix A |
---|
| 471 | set validityRE {(?xi) |
---|
| 472 | ^ |
---|
| 473 | # Path part (already must start with / character) |
---|
| 474 | (?: [-\w.~!$&'()*+,;=:@/] | %[0-9a-f][0-9a-f] )* |
---|
| 475 | # Query part (optional, permits ? characters) |
---|
| 476 | (?: \? (?: [-\w.~!$&'()*+,;=:@/?] | %[0-9a-f][0-9a-f] )* )? |
---|
| 477 | $ |
---|
| 478 | } |
---|
| 479 | if {$state(-strict) && ![regexp -- $validityRE $srvurl]} { |
---|
| 480 | unset $token |
---|
| 481 | # Provide a better error message in this error case |
---|
| 482 | if {[regexp {(?i)%(?![0-9a-f][0-9a-f])..} $srvurl bad]} { |
---|
| 483 | return -code error \ |
---|
| 484 | "Illegal encoding character usage \"$bad\" in URL path" |
---|
| 485 | } |
---|
| 486 | return -code error "Illegal characters in URL path" |
---|
| 487 | } |
---|
| 488 | } else { |
---|
| 489 | set srvurl / |
---|
| 490 | } |
---|
| 491 | if {$proto eq ""} { |
---|
| 492 | set proto http |
---|
| 493 | } |
---|
| 494 | if {![info exists urlTypes($proto)]} { |
---|
| 495 | unset $token |
---|
| 496 | return -code error "Unsupported URL type \"$proto\"" |
---|
| 497 | } |
---|
| 498 | set defport [lindex $urlTypes($proto) 0] |
---|
| 499 | set defcmd [lindex $urlTypes($proto) 1] |
---|
| 500 | |
---|
| 501 | if {$port eq ""} { |
---|
| 502 | set port $defport |
---|
| 503 | } |
---|
| 504 | if {![catch {$http(-proxyfilter) $host} proxy]} { |
---|
| 505 | set phost [lindex $proxy 0] |
---|
| 506 | set pport [lindex $proxy 1] |
---|
| 507 | } |
---|
| 508 | |
---|
| 509 | # OK, now reassemble into a full URL |
---|
| 510 | set url ${proto}:// |
---|
| 511 | if {$user ne ""} { |
---|
| 512 | append url $user |
---|
| 513 | append url @ |
---|
| 514 | } |
---|
| 515 | append url $host |
---|
| 516 | if {$port != $defport} { |
---|
| 517 | append url : $port |
---|
| 518 | } |
---|
| 519 | append url $srvurl |
---|
| 520 | # Don't append the fragment! |
---|
| 521 | set state(url) $url |
---|
| 522 | |
---|
| 523 | # If a timeout is specified we set up the after event and arrange for an |
---|
| 524 | # asynchronous socket connection. |
---|
| 525 | |
---|
| 526 | set sockopts [list] |
---|
| 527 | if {$state(-timeout) > 0} { |
---|
| 528 | set state(after) [after $state(-timeout) \ |
---|
| 529 | [list http::reset $token timeout]] |
---|
| 530 | lappend sockopts -async |
---|
| 531 | } |
---|
| 532 | |
---|
| 533 | # If we are using the proxy, we must pass in the full URL that includes |
---|
| 534 | # the server name. |
---|
| 535 | |
---|
| 536 | if {[info exists phost] && ($phost ne "")} { |
---|
| 537 | set srvurl $url |
---|
| 538 | set targetAddr [list $phost $pport] |
---|
| 539 | } else { |
---|
| 540 | set targetAddr [list $host $port] |
---|
| 541 | } |
---|
| 542 | # Proxy connections aren't shared among different hosts. |
---|
| 543 | set state(socketinfo) $host:$port |
---|
| 544 | |
---|
| 545 | # See if we are supposed to use a previously opened channel. |
---|
| 546 | if {$state(-keepalive)} { |
---|
| 547 | variable socketmap |
---|
| 548 | if {[info exists socketmap($state(socketinfo))]} { |
---|
| 549 | if {[catch {fconfigure $socketmap($state(socketinfo))}]} { |
---|
| 550 | Log "WARNING: socket for $state(socketinfo) was closed" |
---|
| 551 | unset socketmap($state(socketinfo)) |
---|
| 552 | } else { |
---|
| 553 | set sock $socketmap($state(socketinfo)) |
---|
| 554 | Log "reusing socket $sock for $state(socketinfo)" |
---|
| 555 | catch {fileevent $sock writable {}} |
---|
| 556 | catch {fileevent $sock readable {}} |
---|
| 557 | } |
---|
| 558 | } |
---|
| 559 | # don't automatically close this connection socket |
---|
| 560 | set state(connection) {} |
---|
| 561 | } |
---|
| 562 | if {![info exists sock]} { |
---|
| 563 | # Pass -myaddr directly to the socket command |
---|
| 564 | if {[info exists state(-myaddr)]} { |
---|
| 565 | lappend sockopts -myaddr $state(-myaddr) |
---|
| 566 | } |
---|
| 567 | if {[catch {eval $defcmd $sockopts $targetAddr} sock]} { |
---|
| 568 | # something went wrong while trying to establish the |
---|
| 569 | # connection. Clean up after events and such, but DON'T call the |
---|
| 570 | # command callback (if available) because we're going to throw an |
---|
| 571 | # exception from here instead. |
---|
| 572 | |
---|
| 573 | set state(sock) $sock |
---|
| 574 | Finish $token "" 1 |
---|
| 575 | cleanup $token |
---|
| 576 | return -code error $sock |
---|
| 577 | } |
---|
| 578 | } |
---|
| 579 | set state(sock) $sock |
---|
| 580 | Log "Using $sock for $state(socketinfo)" \ |
---|
| 581 | [expr {$state(-keepalive)?"keepalive":""}] |
---|
| 582 | if {$state(-keepalive)} { |
---|
| 583 | set socketmap($state(socketinfo)) $sock |
---|
| 584 | } |
---|
| 585 | |
---|
| 586 | # Wait for the connection to complete. |
---|
| 587 | |
---|
| 588 | if {$state(-timeout) > 0} { |
---|
| 589 | fileevent $sock writable [list http::Connect $token] |
---|
| 590 | http::wait $token |
---|
| 591 | |
---|
| 592 | if {![info exists state]} { |
---|
| 593 | # If we timed out then Finish has been called and the users |
---|
| 594 | # command callback may have cleaned up the token. If so |
---|
| 595 | # we end up here with nothing left to do. |
---|
| 596 | return $token |
---|
| 597 | } elseif {$state(status) eq "error"} { |
---|
| 598 | # Something went wrong while trying to establish the connection. |
---|
| 599 | # Clean up after events and such, but DON'T call the command |
---|
| 600 | # callback (if available) because we're going to throw an |
---|
| 601 | # exception from here instead. |
---|
| 602 | set err [lindex $state(error) 0] |
---|
| 603 | cleanup $token |
---|
| 604 | return -code error $err |
---|
| 605 | } elseif {$state(status) ne "connect"} { |
---|
| 606 | # Likely to be connection timeout |
---|
| 607 | return $token |
---|
| 608 | } |
---|
| 609 | set state(status) "" |
---|
| 610 | } |
---|
| 611 | |
---|
| 612 | # Send data in cr-lf format, but accept any line terminators |
---|
| 613 | |
---|
| 614 | fconfigure $sock -translation {auto crlf} -buffersize $state(-blocksize) |
---|
| 615 | |
---|
| 616 | # The following is disallowed in safe interpreters, but the socket is |
---|
| 617 | # already in non-blocking mode in that case. |
---|
| 618 | |
---|
| 619 | catch {fconfigure $sock -blocking off} |
---|
| 620 | set how GET |
---|
| 621 | if {$isQuery} { |
---|
| 622 | set state(querylength) [string length $state(-query)] |
---|
| 623 | if {$state(querylength) > 0} { |
---|
| 624 | set how POST |
---|
| 625 | set contDone 0 |
---|
| 626 | } else { |
---|
| 627 | # There's no query data. |
---|
| 628 | unset state(-query) |
---|
| 629 | set isQuery 0 |
---|
| 630 | } |
---|
| 631 | } elseif {$state(-validate)} { |
---|
| 632 | set how HEAD |
---|
| 633 | } elseif {$isQueryChannel} { |
---|
| 634 | set how POST |
---|
| 635 | # The query channel must be blocking for the async Write to |
---|
| 636 | # work properly. |
---|
| 637 | fconfigure $state(-querychannel) -blocking 1 -translation binary |
---|
| 638 | set contDone 0 |
---|
| 639 | } |
---|
| 640 | if {[info exists state(-method)] && $state(-method) ne ""} { |
---|
| 641 | set how $state(-method) |
---|
| 642 | } |
---|
| 643 | |
---|
| 644 | if {[catch { |
---|
| 645 | puts $sock "$how $srvurl HTTP/$state(-protocol)" |
---|
| 646 | puts $sock "Accept: $http(-accept)" |
---|
| 647 | array set hdrs $state(-headers) |
---|
| 648 | if {[info exists hdrs(Host)]} { |
---|
| 649 | # Allow Host spoofing [Bug 928154] |
---|
| 650 | puts $sock "Host: $hdrs(Host)" |
---|
| 651 | } elseif {$port == $defport} { |
---|
| 652 | # Don't add port in this case, to handle broken servers. |
---|
| 653 | # [Bug #504508] |
---|
| 654 | puts $sock "Host: $host" |
---|
| 655 | } else { |
---|
| 656 | puts $sock "Host: $host:$port" |
---|
| 657 | } |
---|
| 658 | unset hdrs |
---|
| 659 | puts $sock "User-Agent: $http(-useragent)" |
---|
| 660 | if {$state(-protocol) == 1.0 && $state(-keepalive)} { |
---|
| 661 | puts $sock "Connection: keep-alive" |
---|
| 662 | } |
---|
| 663 | if {$state(-protocol) > 1.0 && !$state(-keepalive)} { |
---|
| 664 | puts $sock "Connection: close" ;# RFC2616 sec 8.1.2.1 |
---|
| 665 | } |
---|
| 666 | if {[info exists phost] && ($phost ne "") && $state(-keepalive)} { |
---|
| 667 | puts $sock "Proxy-Connection: Keep-Alive" |
---|
| 668 | } |
---|
| 669 | set accept_encoding_seen 0 |
---|
| 670 | foreach {key value} $state(-headers) { |
---|
| 671 | if {[string equal -nocase $key "host"]} { continue } |
---|
| 672 | if {[string equal -nocase $key "accept-encoding"]} { |
---|
| 673 | set accept_encoding_seen 1 |
---|
| 674 | } |
---|
| 675 | set value [string map [list \n "" \r ""] $value] |
---|
| 676 | set key [string trim $key] |
---|
| 677 | if {[string equal -nocase $key "content-length"]} { |
---|
| 678 | set contDone 1 |
---|
| 679 | set state(querylength) $value |
---|
| 680 | } |
---|
| 681 | if {[string length $key]} { |
---|
| 682 | puts $sock "$key: $value" |
---|
| 683 | } |
---|
| 684 | } |
---|
| 685 | # Soft zlib dependency check - no package require |
---|
| 686 | if {!$accept_encoding_seen && [llength [package provide zlib]] |
---|
| 687 | && !([info exists state(-channel)] || [info exists state(-handler)]) |
---|
| 688 | } { |
---|
| 689 | puts $sock "Accept-Encoding: gzip, identity, *;q=0.1" |
---|
| 690 | } |
---|
| 691 | if {$isQueryChannel && $state(querylength) == 0} { |
---|
| 692 | # Try to determine size of data in channel. If we cannot seek, the |
---|
| 693 | # surrounding catch will trap us |
---|
| 694 | |
---|
| 695 | set start [tell $state(-querychannel)] |
---|
| 696 | seek $state(-querychannel) 0 end |
---|
| 697 | set state(querylength) \ |
---|
| 698 | [expr {[tell $state(-querychannel)] - $start}] |
---|
| 699 | seek $state(-querychannel) $start |
---|
| 700 | } |
---|
| 701 | |
---|
| 702 | # Flush the request header and set up the fileevent that will either |
---|
| 703 | # push the POST data or read the response. |
---|
| 704 | # |
---|
| 705 | # fileevent note: |
---|
| 706 | # |
---|
| 707 | # It is possible to have both the read and write fileevents active at |
---|
| 708 | # this point. The only scenario it seems to affect is a server that |
---|
| 709 | # closes the connection without reading the POST data. (e.g., early |
---|
| 710 | # versions TclHttpd in various error cases). Depending on the platform, |
---|
| 711 | # the client may or may not be able to get the response from the server |
---|
| 712 | # because of the error it will get trying to write the post data. |
---|
| 713 | # Having both fileevents active changes the timing and the behavior, |
---|
| 714 | # but no two platforms (among Solaris, Linux, and NT) behave the same, |
---|
| 715 | # and none behave all that well in any case. Servers should always read |
---|
| 716 | # their POST data if they expect the client to read their response. |
---|
| 717 | |
---|
| 718 | if {$isQuery || $isQueryChannel} { |
---|
| 719 | puts $sock "Content-Type: $state(-type)" |
---|
| 720 | if {!$contDone} { |
---|
| 721 | puts $sock "Content-Length: $state(querylength)" |
---|
| 722 | } |
---|
| 723 | puts $sock "" |
---|
| 724 | fconfigure $sock -translation {auto binary} |
---|
| 725 | fileevent $sock writable [list http::Write $token] |
---|
| 726 | } else { |
---|
| 727 | puts $sock "" |
---|
| 728 | flush $sock |
---|
| 729 | fileevent $sock readable [list http::Event $sock $token] |
---|
| 730 | } |
---|
| 731 | |
---|
| 732 | if {! [info exists state(-command)]} { |
---|
| 733 | # geturl does EVERYTHING asynchronously, so if the user calls it |
---|
| 734 | # synchronously, we just do a wait here. |
---|
| 735 | |
---|
| 736 | wait $token |
---|
| 737 | if {$state(status) eq "error"} { |
---|
| 738 | # Something went wrong, so throw the exception, and the |
---|
| 739 | # enclosing catch will do cleanup. |
---|
| 740 | return -code error [lindex $state(error) 0] |
---|
| 741 | } |
---|
| 742 | } |
---|
| 743 | } err]} { |
---|
| 744 | # The socket probably was never connected, or the connection dropped |
---|
| 745 | # later. |
---|
| 746 | |
---|
| 747 | # Clean up after events and such, but DON'T call the command callback |
---|
| 748 | # (if available) because we're going to throw an exception from here |
---|
| 749 | # instead. |
---|
| 750 | |
---|
| 751 | # if state(status) is error, it means someone's already called Finish |
---|
| 752 | # to do the above-described clean up. |
---|
| 753 | if {$state(status) ne "error"} { |
---|
| 754 | Finish $token $err 1 |
---|
| 755 | } |
---|
| 756 | cleanup $token |
---|
| 757 | return -code error $err |
---|
| 758 | } |
---|
| 759 | |
---|
| 760 | return $token |
---|
| 761 | } |
---|
| 762 | |
---|
| 763 | # Data access functions: |
---|
| 764 | # Data - the URL data |
---|
| 765 | # Status - the transaction status: ok, reset, eof, timeout |
---|
| 766 | # Code - the HTTP transaction code, e.g., 200 |
---|
| 767 | # Size - the size of the URL data |
---|
| 768 | |
---|
| 769 | proc http::data {token} { |
---|
| 770 | variable $token |
---|
| 771 | upvar 0 $token state |
---|
| 772 | return $state(body) |
---|
| 773 | } |
---|
| 774 | proc http::status {token} { |
---|
| 775 | if {![info exists $token]} { return "error" } |
---|
| 776 | variable $token |
---|
| 777 | upvar 0 $token state |
---|
| 778 | return $state(status) |
---|
| 779 | } |
---|
| 780 | proc http::code {token} { |
---|
| 781 | variable $token |
---|
| 782 | upvar 0 $token state |
---|
| 783 | return $state(http) |
---|
| 784 | } |
---|
| 785 | proc http::ncode {token} { |
---|
| 786 | variable $token |
---|
| 787 | upvar 0 $token state |
---|
| 788 | if {[regexp {[0-9]{3}} $state(http) numeric_code]} { |
---|
| 789 | return $numeric_code |
---|
| 790 | } else { |
---|
| 791 | return $state(http) |
---|
| 792 | } |
---|
| 793 | } |
---|
| 794 | proc http::size {token} { |
---|
| 795 | variable $token |
---|
| 796 | upvar 0 $token state |
---|
| 797 | return $state(currentsize) |
---|
| 798 | } |
---|
| 799 | proc http::meta {token} { |
---|
| 800 | variable $token |
---|
| 801 | upvar 0 $token state |
---|
| 802 | return $state(meta) |
---|
| 803 | } |
---|
| 804 | proc http::error {token} { |
---|
| 805 | variable $token |
---|
| 806 | upvar 0 $token state |
---|
| 807 | if {[info exists state(error)]} { |
---|
| 808 | return $state(error) |
---|
| 809 | } |
---|
| 810 | return "" |
---|
| 811 | } |
---|
| 812 | |
---|
| 813 | # http::cleanup |
---|
| 814 | # |
---|
| 815 | # Garbage collect the state associated with a transaction |
---|
| 816 | # |
---|
| 817 | # Arguments |
---|
| 818 | # token The token returned from http::geturl |
---|
| 819 | # |
---|
| 820 | # Side Effects |
---|
| 821 | # unsets the state array |
---|
| 822 | |
---|
| 823 | proc http::cleanup {token} { |
---|
| 824 | variable $token |
---|
| 825 | upvar 0 $token state |
---|
| 826 | if {[info exists state]} { |
---|
| 827 | unset state |
---|
| 828 | } |
---|
| 829 | } |
---|
| 830 | |
---|
| 831 | # http::Connect |
---|
| 832 | # |
---|
| 833 | # This callback is made when an asyncronous connection completes. |
---|
| 834 | # |
---|
| 835 | # Arguments |
---|
| 836 | # token The token returned from http::geturl |
---|
| 837 | # |
---|
| 838 | # Side Effects |
---|
| 839 | # Sets the status of the connection, which unblocks |
---|
| 840 | # the waiting geturl call |
---|
| 841 | |
---|
| 842 | proc http::Connect {token} { |
---|
| 843 | variable $token |
---|
| 844 | upvar 0 $token state |
---|
| 845 | global errorInfo errorCode |
---|
| 846 | if {[eof $state(sock)] || |
---|
| 847 | [string length [fconfigure $state(sock) -error]]} { |
---|
| 848 | Finish $token "connect failed [fconfigure $state(sock) -error]" 1 |
---|
| 849 | } else { |
---|
| 850 | set state(status) connect |
---|
| 851 | fileevent $state(sock) writable {} |
---|
| 852 | } |
---|
| 853 | return |
---|
| 854 | } |
---|
| 855 | |
---|
| 856 | # http::Write |
---|
| 857 | # |
---|
| 858 | # Write POST query data to the socket |
---|
| 859 | # |
---|
| 860 | # Arguments |
---|
| 861 | # token The token for the connection |
---|
| 862 | # |
---|
| 863 | # Side Effects |
---|
| 864 | # Write the socket and handle callbacks. |
---|
| 865 | |
---|
| 866 | proc http::Write {token} { |
---|
| 867 | variable $token |
---|
| 868 | upvar 0 $token state |
---|
| 869 | set sock $state(sock) |
---|
| 870 | |
---|
| 871 | # Output a block. Tcl will buffer this if the socket blocks |
---|
| 872 | set done 0 |
---|
| 873 | if {[catch { |
---|
| 874 | # Catch I/O errors on dead sockets |
---|
| 875 | |
---|
| 876 | if {[info exists state(-query)]} { |
---|
| 877 | # Chop up large query strings so queryprogress callback can give |
---|
| 878 | # smooth feedback. |
---|
| 879 | |
---|
| 880 | puts -nonewline $sock \ |
---|
| 881 | [string range $state(-query) $state(queryoffset) \ |
---|
| 882 | [expr {$state(queryoffset) + $state(-queryblocksize) - 1}]] |
---|
| 883 | incr state(queryoffset) $state(-queryblocksize) |
---|
| 884 | if {$state(queryoffset) >= $state(querylength)} { |
---|
| 885 | set state(queryoffset) $state(querylength) |
---|
| 886 | puts $sock "" |
---|
| 887 | set done 1 |
---|
| 888 | } |
---|
| 889 | } else { |
---|
| 890 | # Copy blocks from the query channel |
---|
| 891 | |
---|
| 892 | set outStr [read $state(-querychannel) $state(-queryblocksize)] |
---|
| 893 | puts -nonewline $sock $outStr |
---|
| 894 | incr state(queryoffset) [string length $outStr] |
---|
| 895 | if {[eof $state(-querychannel)]} { |
---|
| 896 | set done 1 |
---|
| 897 | } |
---|
| 898 | } |
---|
| 899 | } err]} { |
---|
| 900 | # Do not call Finish here, but instead let the read half of the socket |
---|
| 901 | # process whatever server reply there is to get. |
---|
| 902 | |
---|
| 903 | set state(posterror) $err |
---|
| 904 | set done 1 |
---|
| 905 | } |
---|
| 906 | if {$done} { |
---|
| 907 | catch {flush $sock} |
---|
| 908 | fileevent $sock writable {} |
---|
| 909 | fileevent $sock readable [list http::Event $sock $token] |
---|
| 910 | } |
---|
| 911 | |
---|
| 912 | # Callback to the client after we've completely handled everything. |
---|
| 913 | |
---|
| 914 | if {[string length $state(-queryprogress)]} { |
---|
| 915 | eval $state(-queryprogress) \ |
---|
| 916 | [list $token $state(querylength) $state(queryoffset)] |
---|
| 917 | } |
---|
| 918 | } |
---|
| 919 | |
---|
| 920 | # http::Event |
---|
| 921 | # |
---|
| 922 | # Handle input on the socket |
---|
| 923 | # |
---|
| 924 | # Arguments |
---|
| 925 | # sock The socket receiving input. |
---|
| 926 | # token The token returned from http::geturl |
---|
| 927 | # |
---|
| 928 | # Side Effects |
---|
| 929 | # Read the socket and handle callbacks. |
---|
| 930 | |
---|
| 931 | proc http::Event {sock token} { |
---|
| 932 | variable $token |
---|
| 933 | upvar 0 $token state |
---|
| 934 | |
---|
| 935 | if {![info exists state]} { |
---|
| 936 | Log "Event $sock with invalid token '$token' - remote close?" |
---|
| 937 | if {! [eof $sock]} { |
---|
| 938 | if {[string length [set d [read $sock]]] != 0} { |
---|
| 939 | Log "WARNING: additional data left on closed socket" |
---|
| 940 | } |
---|
| 941 | } |
---|
| 942 | CloseSocket $sock |
---|
| 943 | return |
---|
| 944 | } |
---|
| 945 | if {$state(state) eq "header"} { |
---|
| 946 | if {[catch {gets $sock line} n]} { |
---|
| 947 | return [Finish $token $n] |
---|
| 948 | } elseif {$n == 0} { |
---|
| 949 | # We have now read all headers |
---|
| 950 | # We ignore HTTP/1.1 100 Continue returns. RFC2616 sec 8.2.3 |
---|
| 951 | if {$state(http) == "" || [lindex $state(http) 1] == 100} { return } |
---|
| 952 | |
---|
| 953 | set state(state) body |
---|
| 954 | |
---|
| 955 | # If doing a HEAD, then we won't get any body |
---|
| 956 | if {$state(-validate)} { |
---|
| 957 | Eof $token |
---|
| 958 | return |
---|
| 959 | } |
---|
| 960 | |
---|
| 961 | # For non-chunked transfer we may have no body -- in this case we |
---|
| 962 | # may get no further file event if the connection doesn't close and |
---|
| 963 | # no more data is sent. We can tell and must finish up now - not |
---|
| 964 | # later. |
---|
| 965 | if {!(([info exists state(connection)] |
---|
| 966 | && ($state(connection) eq "close")) |
---|
| 967 | || [info exists state(transfer)]) |
---|
| 968 | && $state(totalsize) == 0 |
---|
| 969 | } then { |
---|
| 970 | Log "body size is 0 and no events likely - complete." |
---|
| 971 | Eof $token |
---|
| 972 | return |
---|
| 973 | } |
---|
| 974 | |
---|
| 975 | # We have to use binary translation to count bytes properly. |
---|
| 976 | fconfigure $sock -translation binary |
---|
| 977 | |
---|
| 978 | if {$state(-binary) || ![string match -nocase text* $state(type)]} { |
---|
| 979 | # Turn off conversions for non-text data |
---|
| 980 | set state(binary) 1 |
---|
| 981 | } |
---|
| 982 | if {$state(binary) || [string match *gzip* $state(coding)] |
---|
| 983 | || [string match *compress* $state(coding)]} { |
---|
| 984 | if {[info exists state(-channel)]} { |
---|
| 985 | fconfigure $state(-channel) -translation binary |
---|
| 986 | } |
---|
| 987 | } |
---|
| 988 | if {[info exists state(-channel)] && |
---|
| 989 | ![info exists state(-handler)]} { |
---|
| 990 | # Initiate a sequence of background fcopies |
---|
| 991 | fileevent $sock readable {} |
---|
| 992 | CopyStart $sock $token |
---|
| 993 | return |
---|
| 994 | } |
---|
| 995 | } elseif {$n > 0} { |
---|
| 996 | # Process header lines |
---|
| 997 | if {[regexp -nocase {^([^:]+):(.+)$} $line x key value]} { |
---|
| 998 | switch -- [string tolower $key] { |
---|
| 999 | content-type { |
---|
| 1000 | set state(type) [string trim [string tolower $value]] |
---|
| 1001 | # grab the optional charset information |
---|
| 1002 | regexp -nocase {charset\s*=\s*(\S+?);?} \ |
---|
| 1003 | $state(type) -> state(charset) |
---|
| 1004 | } |
---|
| 1005 | content-length { |
---|
| 1006 | set state(totalsize) [string trim $value] |
---|
| 1007 | } |
---|
| 1008 | content-encoding { |
---|
| 1009 | set state(coding) [string trim $value] |
---|
| 1010 | } |
---|
| 1011 | transfer-encoding { |
---|
| 1012 | set state(transfer) \ |
---|
| 1013 | [string trim [string tolower $value]] |
---|
| 1014 | } |
---|
| 1015 | proxy-connection - |
---|
| 1016 | connection { |
---|
| 1017 | set state(connection) \ |
---|
| 1018 | [string trim [string tolower $value]] |
---|
| 1019 | } |
---|
| 1020 | } |
---|
| 1021 | lappend state(meta) $key [string trim $value] |
---|
| 1022 | } elseif {[string match HTTP* $line]} { |
---|
| 1023 | set state(http) $line |
---|
| 1024 | } |
---|
| 1025 | } |
---|
| 1026 | } else { |
---|
| 1027 | # Now reading body |
---|
| 1028 | if {[catch { |
---|
| 1029 | if {[info exists state(-handler)]} { |
---|
| 1030 | set n [eval $state(-handler) [list $sock $token]] |
---|
| 1031 | } elseif {[info exists state(transfer_final)]} { |
---|
| 1032 | set line [getTextLine $sock] |
---|
| 1033 | set n [string length $line] |
---|
| 1034 | if {$n > 0} { |
---|
| 1035 | Log "found $n bytes following final chunk" |
---|
| 1036 | append state(transfer_final) $line |
---|
| 1037 | } else { |
---|
| 1038 | Log "final chunk part" |
---|
| 1039 | Eof $token |
---|
| 1040 | } |
---|
| 1041 | } elseif {[info exists state(transfer)] |
---|
| 1042 | && $state(transfer) eq "chunked"} { |
---|
| 1043 | set size 0 |
---|
| 1044 | set chunk [getTextLine $sock] |
---|
| 1045 | set n [string length $chunk] |
---|
| 1046 | if {[string trim $chunk] ne ""} { |
---|
| 1047 | scan $chunk %x size |
---|
| 1048 | if {$size != 0} { |
---|
| 1049 | set bl [fconfigure $sock -blocking] |
---|
| 1050 | fconfigure $sock -blocking 1 |
---|
| 1051 | set chunk [read $sock $size] |
---|
| 1052 | fconfigure $sock -blocking $bl |
---|
| 1053 | set n [string length $chunk] |
---|
| 1054 | if {$n >= 0} { |
---|
| 1055 | append state(body) $chunk |
---|
| 1056 | } |
---|
| 1057 | if {$size != [string length $chunk]} { |
---|
| 1058 | Log "WARNING: mis-sized chunk:\ |
---|
| 1059 | was [string length $chunk], should be $size" |
---|
| 1060 | } |
---|
| 1061 | getTextLine $sock |
---|
| 1062 | } else { |
---|
| 1063 | set state(transfer_final) {} |
---|
| 1064 | } |
---|
| 1065 | } |
---|
| 1066 | } else { |
---|
| 1067 | #Log "read non-chunk $state(currentsize) of $state(totalsize)" |
---|
| 1068 | set block [read $sock $state(-blocksize)] |
---|
| 1069 | set n [string length $block] |
---|
| 1070 | if {$n >= 0} { |
---|
| 1071 | append state(body) $block |
---|
| 1072 | } |
---|
| 1073 | } |
---|
| 1074 | if {[info exists state]} { |
---|
| 1075 | if {$n >= 0} { |
---|
| 1076 | incr state(currentsize) $n |
---|
| 1077 | } |
---|
| 1078 | # If Content-Length - check for end of data. |
---|
| 1079 | if {($state(totalsize) > 0) |
---|
| 1080 | && ($state(currentsize) >= $state(totalsize))} { |
---|
| 1081 | Eof $token |
---|
| 1082 | } |
---|
| 1083 | } |
---|
| 1084 | } err]} { |
---|
| 1085 | return [Finish $token $err] |
---|
| 1086 | } else { |
---|
| 1087 | if {[info exists state(-progress)]} { |
---|
| 1088 | eval $state(-progress) \ |
---|
| 1089 | [list $token $state(totalsize) $state(currentsize)] |
---|
| 1090 | } |
---|
| 1091 | } |
---|
| 1092 | } |
---|
| 1093 | |
---|
| 1094 | # catch as an Eof above may have closed the socket already |
---|
| 1095 | if {![catch {eof $sock} eof] && $eof} { |
---|
| 1096 | if {[info exists $token]} { |
---|
| 1097 | set state(connection) close |
---|
| 1098 | Eof $token |
---|
| 1099 | } else { |
---|
| 1100 | # open connection closed on a token that has been cleaned up. |
---|
| 1101 | CloseSocket $sock |
---|
| 1102 | } |
---|
| 1103 | return |
---|
| 1104 | } |
---|
| 1105 | } |
---|
| 1106 | |
---|
| 1107 | # http::getTextLine -- |
---|
| 1108 | # |
---|
| 1109 | # Get one line with the stream in blocking crlf mode |
---|
| 1110 | # |
---|
| 1111 | # Arguments |
---|
| 1112 | # sock The socket receiving input. |
---|
| 1113 | # |
---|
| 1114 | # Results: |
---|
| 1115 | # The line of text, without trailing newline |
---|
| 1116 | |
---|
| 1117 | proc http::getTextLine {sock} { |
---|
| 1118 | set tr [fconfigure $sock -translation] |
---|
| 1119 | set bl [fconfigure $sock -blocking] |
---|
| 1120 | fconfigure $sock -translation crlf -blocking 1 |
---|
| 1121 | set r [gets $sock] |
---|
| 1122 | fconfigure $sock -translation $tr -blocking $bl |
---|
| 1123 | return $r |
---|
| 1124 | } |
---|
| 1125 | |
---|
| 1126 | # http::CopyStart |
---|
| 1127 | # |
---|
| 1128 | # Error handling wrapper around fcopy |
---|
| 1129 | # |
---|
| 1130 | # Arguments |
---|
| 1131 | # sock The socket to copy from |
---|
| 1132 | # token The token returned from http::geturl |
---|
| 1133 | # |
---|
| 1134 | # Side Effects |
---|
| 1135 | # This closes the connection upon error |
---|
| 1136 | |
---|
| 1137 | proc http::CopyStart {sock token} { |
---|
| 1138 | variable $token |
---|
| 1139 | upvar 0 $token state |
---|
| 1140 | if {[catch { |
---|
| 1141 | fcopy $sock $state(-channel) -size $state(-blocksize) -command \ |
---|
| 1142 | [list http::CopyDone $token] |
---|
| 1143 | } err]} { |
---|
| 1144 | Finish $token $err |
---|
| 1145 | } |
---|
| 1146 | } |
---|
| 1147 | |
---|
| 1148 | # http::CopyDone |
---|
| 1149 | # |
---|
| 1150 | # fcopy completion callback |
---|
| 1151 | # |
---|
| 1152 | # Arguments |
---|
| 1153 | # token The token returned from http::geturl |
---|
| 1154 | # count The amount transfered |
---|
| 1155 | # |
---|
| 1156 | # Side Effects |
---|
| 1157 | # Invokes callbacks |
---|
| 1158 | |
---|
| 1159 | proc http::CopyDone {token count {error {}}} { |
---|
| 1160 | variable $token |
---|
| 1161 | upvar 0 $token state |
---|
| 1162 | set sock $state(sock) |
---|
| 1163 | incr state(currentsize) $count |
---|
| 1164 | if {[info exists state(-progress)]} { |
---|
| 1165 | eval $state(-progress) \ |
---|
| 1166 | [list $token $state(totalsize) $state(currentsize)] |
---|
| 1167 | } |
---|
| 1168 | # At this point the token may have been reset |
---|
| 1169 | if {[string length $error]} { |
---|
| 1170 | Finish $token $error |
---|
| 1171 | } elseif {[catch {eof $sock} iseof] || $iseof} { |
---|
| 1172 | Eof $token |
---|
| 1173 | } else { |
---|
| 1174 | CopyStart $sock $token |
---|
| 1175 | } |
---|
| 1176 | } |
---|
| 1177 | |
---|
| 1178 | # http::Eof |
---|
| 1179 | # |
---|
| 1180 | # Handle eof on the socket |
---|
| 1181 | # |
---|
| 1182 | # Arguments |
---|
| 1183 | # token The token returned from http::geturl |
---|
| 1184 | # |
---|
| 1185 | # Side Effects |
---|
| 1186 | # Clean up the socket |
---|
| 1187 | |
---|
| 1188 | proc http::Eof {token {force 0}} { |
---|
| 1189 | variable $token |
---|
| 1190 | upvar 0 $token state |
---|
| 1191 | if {$state(state) eq "header"} { |
---|
| 1192 | # Premature eof |
---|
| 1193 | set state(status) eof |
---|
| 1194 | } else { |
---|
| 1195 | set state(status) ok |
---|
| 1196 | } |
---|
| 1197 | |
---|
| 1198 | if {($state(coding) eq "gzip") && [string length $state(body)] > 0} { |
---|
| 1199 | if {[catch { |
---|
| 1200 | set state(body) [Gunzip $state(body)] |
---|
| 1201 | } err]} { |
---|
| 1202 | return [Finish $token $err] |
---|
| 1203 | } |
---|
| 1204 | } |
---|
| 1205 | |
---|
| 1206 | if {!$state(binary)} { |
---|
| 1207 | |
---|
| 1208 | # If we are getting text, set the incoming channel's |
---|
| 1209 | # encoding correctly. iso8859-1 is the RFC default, but |
---|
| 1210 | # this could be any IANA charset. However, we only know |
---|
| 1211 | # how to convert what we have encodings for. |
---|
| 1212 | |
---|
| 1213 | set enc [CharsetToEncoding $state(charset)] |
---|
| 1214 | if {$enc ne "binary"} { |
---|
| 1215 | set state(body) [encoding convertfrom $enc $state(body)] |
---|
| 1216 | } |
---|
| 1217 | |
---|
| 1218 | # Translate text line endings. |
---|
| 1219 | set state(body) [string map {\r\n \n \r \n} $state(body)] |
---|
| 1220 | } |
---|
| 1221 | |
---|
| 1222 | Finish $token |
---|
| 1223 | } |
---|
| 1224 | |
---|
| 1225 | # http::wait -- |
---|
| 1226 | # |
---|
| 1227 | # See documentation for details. |
---|
| 1228 | # |
---|
| 1229 | # Arguments: |
---|
| 1230 | # token Connection token. |
---|
| 1231 | # |
---|
| 1232 | # Results: |
---|
| 1233 | # The status after the wait. |
---|
| 1234 | |
---|
| 1235 | proc http::wait {token} { |
---|
| 1236 | variable $token |
---|
| 1237 | upvar 0 $token state |
---|
| 1238 | |
---|
| 1239 | if {![info exists state(status)] || $state(status) eq ""} { |
---|
| 1240 | # We must wait on the original variable name, not the upvar alias |
---|
| 1241 | vwait ${token}(status) |
---|
| 1242 | } |
---|
| 1243 | |
---|
| 1244 | return [status $token] |
---|
| 1245 | } |
---|
| 1246 | |
---|
| 1247 | # http::formatQuery -- |
---|
| 1248 | # |
---|
| 1249 | # See documentation for details. Call http::formatQuery with an even |
---|
| 1250 | # number of arguments, where the first is a name, the second is a value, |
---|
| 1251 | # the third is another name, and so on. |
---|
| 1252 | # |
---|
| 1253 | # Arguments: |
---|
| 1254 | # args A list of name-value pairs. |
---|
| 1255 | # |
---|
| 1256 | # Results: |
---|
| 1257 | # TODO |
---|
| 1258 | |
---|
| 1259 | proc http::formatQuery {args} { |
---|
| 1260 | set result "" |
---|
| 1261 | set sep "" |
---|
| 1262 | foreach i $args { |
---|
| 1263 | append result $sep [mapReply $i] |
---|
| 1264 | if {$sep eq "="} { |
---|
| 1265 | set sep & |
---|
| 1266 | } else { |
---|
| 1267 | set sep = |
---|
| 1268 | } |
---|
| 1269 | } |
---|
| 1270 | return $result |
---|
| 1271 | } |
---|
| 1272 | |
---|
| 1273 | # http::mapReply -- |
---|
| 1274 | # |
---|
| 1275 | # Do x-www-urlencoded character mapping |
---|
| 1276 | # |
---|
| 1277 | # Arguments: |
---|
| 1278 | # string The string the needs to be encoded |
---|
| 1279 | # |
---|
| 1280 | # Results: |
---|
| 1281 | # The encoded string |
---|
| 1282 | |
---|
| 1283 | proc http::mapReply {string} { |
---|
| 1284 | variable http |
---|
| 1285 | variable formMap |
---|
| 1286 | |
---|
| 1287 | # The spec says: "non-alphanumeric characters are replaced by '%HH'". Use |
---|
| 1288 | # a pre-computed map and [string map] to do the conversion (much faster |
---|
| 1289 | # than [regsub]/[subst]). [Bug 1020491] |
---|
| 1290 | |
---|
| 1291 | if {$http(-urlencoding) ne ""} { |
---|
| 1292 | set string [encoding convertto $http(-urlencoding) $string] |
---|
| 1293 | return [string map $formMap $string] |
---|
| 1294 | } |
---|
| 1295 | set converted [string map $formMap $string] |
---|
| 1296 | if {[string match "*\[\u0100-\uffff\]*" $converted]} { |
---|
| 1297 | regexp {[\u0100-\uffff]} $converted badChar |
---|
| 1298 | # Return this error message for maximum compatability... :^/ |
---|
| 1299 | return -code error \ |
---|
| 1300 | "can't read \"formMap($badChar)\": no such element in array" |
---|
| 1301 | } |
---|
| 1302 | return $converted |
---|
| 1303 | } |
---|
| 1304 | |
---|
| 1305 | # http::ProxyRequired -- |
---|
| 1306 | # Default proxy filter. |
---|
| 1307 | # |
---|
| 1308 | # Arguments: |
---|
| 1309 | # host The destination host |
---|
| 1310 | # |
---|
| 1311 | # Results: |
---|
| 1312 | # The current proxy settings |
---|
| 1313 | |
---|
| 1314 | proc http::ProxyRequired {host} { |
---|
| 1315 | variable http |
---|
| 1316 | if {[info exists http(-proxyhost)] && [string length $http(-proxyhost)]} { |
---|
| 1317 | if {![info exists http(-proxyport)] || \ |
---|
| 1318 | ![string length $http(-proxyport)]} { |
---|
| 1319 | set http(-proxyport) 8080 |
---|
| 1320 | } |
---|
| 1321 | return [list $http(-proxyhost) $http(-proxyport)] |
---|
| 1322 | } |
---|
| 1323 | } |
---|
| 1324 | |
---|
| 1325 | # http::CharsetToEncoding -- |
---|
| 1326 | # |
---|
| 1327 | # Tries to map a given IANA charset to a tcl encoding. |
---|
| 1328 | # If no encoding can be found, returns binary. |
---|
| 1329 | # |
---|
| 1330 | |
---|
| 1331 | proc http::CharsetToEncoding {charset} { |
---|
| 1332 | variable encodings |
---|
| 1333 | |
---|
| 1334 | set charset [string tolower $charset] |
---|
| 1335 | if {[regexp {iso-?8859-([0-9]+)} $charset - num]} { |
---|
| 1336 | set encoding "iso8859-$num" |
---|
| 1337 | } elseif {[regexp {iso-?2022-(jp|kr)} $charset - ext]} { |
---|
| 1338 | set encoding "iso2022-$ext" |
---|
| 1339 | } elseif {[regexp {shift[-_]?js} $charset -]} { |
---|
| 1340 | set encoding "shiftjis" |
---|
| 1341 | } elseif {[regexp {(windows|cp)-?([0-9]+)} $charset - - num]} { |
---|
| 1342 | set encoding "cp$num" |
---|
| 1343 | } elseif {$charset eq "us-ascii"} { |
---|
| 1344 | set encoding "ascii" |
---|
| 1345 | } elseif {[regexp {(iso-?)?lat(in)?-?([0-9]+)} $charset - - - num]} { |
---|
| 1346 | switch -- $num { |
---|
| 1347 | 5 {set encoding "iso8859-9"} |
---|
| 1348 | 1 - |
---|
| 1349 | 2 - |
---|
| 1350 | 3 {set encoding "iso8859-$num"} |
---|
| 1351 | } |
---|
| 1352 | } else { |
---|
| 1353 | # other charset, like euc-xx, utf-8,... may directly maps to encoding |
---|
| 1354 | set encoding $charset |
---|
| 1355 | } |
---|
| 1356 | set idx [lsearch -exact $encodings $encoding] |
---|
| 1357 | if {$idx >= 0} { |
---|
| 1358 | return $encoding |
---|
| 1359 | } else { |
---|
| 1360 | return "binary" |
---|
| 1361 | } |
---|
| 1362 | } |
---|
| 1363 | |
---|
| 1364 | # http::Gunzip -- |
---|
| 1365 | # |
---|
| 1366 | # Decompress data transmitted using the gzip transfer coding. |
---|
| 1367 | # |
---|
| 1368 | |
---|
| 1369 | # FIX ME: redo using zlib sinflate |
---|
| 1370 | proc http::Gunzip {data} { |
---|
| 1371 | binary scan $data Scb5icc magic method flags time xfl os |
---|
| 1372 | set pos 10 |
---|
| 1373 | if {$magic != 0x1f8b} { |
---|
| 1374 | return -code error "invalid data: supplied data is not in gzip format" |
---|
| 1375 | } |
---|
| 1376 | if {$method != 8} { |
---|
| 1377 | return -code error "invalid compression method" |
---|
| 1378 | } |
---|
| 1379 | |
---|
| 1380 | foreach {f_text f_crc f_extra f_name f_comment} [split $flags ""] break |
---|
| 1381 | set extra "" |
---|
| 1382 | if { $f_extra } { |
---|
| 1383 | binary scan $data @${pos}S xlen |
---|
| 1384 | incr pos 2 |
---|
| 1385 | set extra [string range $data $pos $xlen] |
---|
| 1386 | set pos [incr xlen] |
---|
| 1387 | } |
---|
| 1388 | |
---|
| 1389 | set name "" |
---|
| 1390 | if { $f_name } { |
---|
| 1391 | set ndx [string first \0 $data $pos] |
---|
| 1392 | set name [string range $data $pos $ndx] |
---|
| 1393 | set pos [incr ndx] |
---|
| 1394 | } |
---|
| 1395 | |
---|
| 1396 | set comment "" |
---|
| 1397 | if { $f_comment } { |
---|
| 1398 | set ndx [string first \0 $data $pos] |
---|
| 1399 | set comment [string range $data $pos $ndx] |
---|
| 1400 | set pos [incr ndx] |
---|
| 1401 | } |
---|
| 1402 | |
---|
| 1403 | set fcrc "" |
---|
| 1404 | if { $f_crc } { |
---|
| 1405 | set fcrc [string range $data $pos [incr pos]] |
---|
| 1406 | incr pos |
---|
| 1407 | } |
---|
| 1408 | |
---|
| 1409 | binary scan [string range $data end-7 end] ii crc size |
---|
| 1410 | set inflated [zlib inflate [string range $data $pos end-8]] |
---|
| 1411 | |
---|
| 1412 | if { $crc != [set chk [zlib crc32 $inflated]] } { |
---|
| 1413 | return -code error "invalid data: checksum mismatch $crc != $chk" |
---|
| 1414 | } |
---|
| 1415 | return $inflated |
---|
| 1416 | } |
---|
| 1417 | |
---|
| 1418 | # Local variables: |
---|
| 1419 | # indent-tabs-mode: t |
---|
| 1420 | # End: |
---|