PageRenderTime 29ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/jabberlib/https.tcl

http://tkabbur.googlecode.com/
TCL | 677 lines | 314 code | 109 blank | 254 comment | 47 complexity | 43686f0fb1f861c7057b2b42a6ca8ef6 MD5 | raw file
Possible License(s): AGPL-3.0
  1. # https.tcl --
  2. #
  3. # Package for using the HTTP CONNECT (it is a common method for
  4. # tunnelling HTTPS traffic, so the name is https) method for
  5. # connecting TCP sockets. Only client side.
  6. #
  7. # Copyright (c) 2007 Sergei Golovan <sgolovan@nes.ru>
  8. #
  9. # This source file is distributed under the BSD license.
  10. #
  11. # $Id: https.tcl 1282 2007-10-26 17:40:59Z sergei $
  12. package require base64
  13. package require ntlm 1.0
  14. package require autoconnect 0.2
  15. package provide autoconnect::https 1.0
  16. namespace eval https {
  17. namespace export connect
  18. variable debug 0
  19. autoconnect::register https [namespace current]::connect
  20. }
  21. # https::connect --
  22. #
  23. # Negotiates with a HTTPS proxy server.
  24. #
  25. # Arguments:
  26. # sock: an open socket token to the proxy server
  27. # addr: the peer address, not the proxy server
  28. # port: the peer port number
  29. # args:
  30. # -command tclProc {status socket}
  31. # -username userid
  32. # -password password
  33. # -useragent useragent
  34. # -timeout millisecs (default 60000)
  35. #
  36. # Results:
  37. # The connect socket or error if no -command, else empty string.
  38. #
  39. # Side effects:
  40. # Socket is prepared for data transfer.
  41. # If -command specified, the callback tclProc is called with
  42. # status OK and socket or ERROR and error message.
  43. proc https::connect {sock addr port args} {
  44. variable auth
  45. set token [namespace current]::$sock
  46. variable $token
  47. upvar 0 $token state
  48. Debug 2 "https::connect token=$token, sock=$sock, addr=$addr,\
  49. port=$port, args=$args"
  50. array set state {
  51. -command ""
  52. -timeout 60000
  53. -username ""
  54. -password ""
  55. -useragent ""
  56. async 0
  57. status ""
  58. }
  59. array set state [list \
  60. addr $addr \
  61. port $port \
  62. sock $sock]
  63. array set state $args
  64. if {[string length $state(-command)]} {
  65. set state(async) 1
  66. }
  67. if {[catch {set state(peer) [fconfigure $sock -peername]}]} {
  68. catch {close $sock}
  69. if {$state(async)} {
  70. after idle [list $state(-command) ERROR network-failure]
  71. Free $token
  72. return
  73. } else {
  74. Free $token
  75. return -code error network-failure
  76. }
  77. }
  78. PutsConnectQuery $token
  79. fileevent $sock readable \
  80. [list [namespace current]::Readable $token]
  81. # Setup timeout timer.
  82. set state(timeoutid) \
  83. [after $state(-timeout) [namespace current]::Timeout $token]
  84. if {$state(async)} {
  85. return
  86. } else {
  87. # We should not return from this proc until finished!
  88. vwait $token\(status)
  89. set status $state(status)
  90. set sock $state(sock)
  91. Free $token
  92. if {[string equal $status OK]} {
  93. return $sock
  94. } else {
  95. catch {close $sock}
  96. return -code error $sock
  97. }
  98. }
  99. }
  100. # https::Readable --
  101. #
  102. # Receive the first reply from a proxy and either finish the
  103. # negotiations or prepare to autorization process at the proxy.
  104. #
  105. # Arguments:
  106. # token A connection token.
  107. #
  108. # Result:
  109. # An empty string.
  110. #
  111. # Side effects:
  112. # The negotiation is finished or the next turn is started.
  113. proc https::Readable {token} {
  114. variable $token
  115. upvar 0 $token state
  116. Debug 2 "https::Readable token=$token"
  117. fileevent $state(sock) readable {}
  118. set code [ReadProxyAnswer $token]
  119. if {$code >= 200 && $code < 300} {
  120. # Success
  121. while {[string length [gets $state(sock)]]} {}
  122. Finish $token
  123. } elseif {$code != 407} {
  124. # Failure
  125. Finish $token $state(result)
  126. } else {
  127. # Authorization required
  128. set content_length -1
  129. set method basic
  130. while {[string length [set header [gets $state(sock)]]]} {
  131. switch -- [HttpHeaderName $header] {
  132. proxy-authenticate {
  133. if {[string equal -length 4 [HttpHeaderBody $header] "NTLM"]} {
  134. set method ntlm
  135. }
  136. }
  137. content-length {
  138. set content_length [HttpHeaderBody $header]
  139. }
  140. }
  141. }
  142. ReadProxyJunk $token $content_length
  143. close $state(sock)
  144. set state(sock) \
  145. [socket -async [lindex $state(peer) 0] [lindex $state(peer) 2]]
  146. fileevent $state(sock) writable \
  147. [list [namespace current]::Authorize $token $method]
  148. }
  149. return
  150. }
  151. # https::Authorize --
  152. #
  153. # Start the authorization procedure.
  154. #
  155. # Arguments:
  156. # token A connection token.
  157. # method (basic or ntlm) authorization method.
  158. #
  159. # Result:
  160. # Empty string.
  161. #
  162. # Side effects:
  163. # Authorization is started.
  164. proc https::Authorize {token method} {
  165. variable $token
  166. upvar 0 $token state
  167. Debug 2 "https::Authorize token=$token, method=$method"
  168. fileevent $state(sock) writable {}
  169. switch -- $method {
  170. ntlm {
  171. AuthorizeNtlmStep1 $token
  172. }
  173. default {
  174. AuthorizeBasicStep1 $token
  175. }
  176. }
  177. return
  178. }
  179. # https::AuthorizeBasicStep1 --
  180. #
  181. # The first step of basic authorization procedure: send authorization
  182. # credentials to a socket.
  183. #
  184. # Arguments:
  185. # token A connection token.
  186. #
  187. # Result:
  188. # Empty string.
  189. #
  190. # Side effects:
  191. # Authorization info is sent to a socket.
  192. proc https::AuthorizeBasicStep1 {token} {
  193. variable $token
  194. upvar 0 $token state
  195. Debug 2 "https::AuthorizeBasicStep1 token=$token"
  196. set auth \
  197. [string map {\n {}} \
  198. [base64::encode \
  199. [encoding convertto "$state(-username):$state(-$password)"]]]
  200. PutsConnectQuery $token "Basic $auth"
  201. fileevent $state(sock) readable \
  202. [list [namespace current]::AuthorizeBasicStep2 $token]
  203. return
  204. }
  205. # https::AuthorizeBasicStep2 --
  206. #
  207. # The second step of basic authorization procedure: receive and
  208. # analyze server reply.
  209. #
  210. # Arguments:
  211. # token A connection token.
  212. #
  213. # Result:
  214. # Empty string.
  215. #
  216. # Side effects:
  217. # Server reply is received from a socket.
  218. proc https::AuthorizeBasicStep2 {token} {
  219. variable $token
  220. upvar 0 $token state
  221. Debug 2 "https::AuthorizeBasicStep2 token=$token"
  222. fileevent $state(sock) readable {}
  223. set code [ReadProxyAnswer $token]
  224. if {$code >= 200 && $code < 300} {
  225. # Success
  226. while {[string length [gets $sock]]} { }
  227. Finish $token
  228. } else {
  229. # Failure
  230. Finish $token $state(result)
  231. }
  232. return
  233. }
  234. # https::AuthorizeNtlmStep1 --
  235. #
  236. # The first step of NTLM authorization procedure: send NTLM
  237. # message 1 to a socket.
  238. #
  239. # Arguments:
  240. # token A connection token.
  241. #
  242. # Result:
  243. # Empty string.
  244. #
  245. # Side effects:
  246. # Authorization info is sent to a socket.
  247. proc https::AuthorizeNtlmStep1 {token} {
  248. variable $token
  249. upvar 0 $token state
  250. Debug 2 "https::AuthorizeNtlmStep1 token=$token"
  251. set domain ""
  252. set host [info hostname]
  253. # if username is domain/username or domain\username
  254. # then set domain and username
  255. set username $state(-username)
  256. regexp {(\w+)[\\/](.*)} $username -> domain username
  257. set ntlmtok [NTLM::new -domain $domain \
  258. -host $host \
  259. -username $username \
  260. -password $state(-password)]
  261. set message1 [$ntlmtok type1Message]
  262. set state(ntlmtok) $ntlmtok
  263. PutsConnectQuery $token "NTLM $message1"
  264. fileevent $state(sock) readable \
  265. [list [namespace current]::AuthorizeNtlmStep2 $token]
  266. return
  267. }
  268. # https::AuthorizeNtlmStep2 --
  269. #
  270. # The first step of basic authorization procedure: send authorization
  271. # credentials to a socket.
  272. #
  273. # Arguments:
  274. # token A connection token.
  275. #
  276. # Result:
  277. # Empty string.
  278. #
  279. # Side effects:
  280. # Authorization info is sent to a socket.
  281. proc https::AuthorizeNtlmStep2 {token} {
  282. variable $token
  283. upvar 0 $token state
  284. Debug 2 "https::AuthorizeNtlmStep2 token=$token"
  285. fileevent $state(sock) readable {}
  286. set code [ReadProxyAnswer $token]
  287. if {$code >= 200 && $code < 300} {
  288. # Success
  289. while {[string length [gets $state(sock)]]} { }
  290. Finish $token
  291. return
  292. } elseif {$code != 407} {
  293. # Failure
  294. Finish $token $state(result)
  295. return
  296. }
  297. set content_length -1
  298. set message2 ""
  299. while {![string equal [set header [gets $state(sock)]] ""]} {
  300. switch -- [HttpHeaderName $header] {
  301. proxy-authenticate {
  302. set body [HttpHeaderBody $header]
  303. if {[string equal -length 5 $body "NTLM "]} {
  304. set message2 [string trim [string range $body 5 end]]
  305. }
  306. }
  307. content-length {
  308. set content_length [HttpHeaderBody $header]
  309. }
  310. }
  311. }
  312. ReadProxyJunk $token $content_length
  313. $state(ntlmtok) parseType2Message -message $message2
  314. set message3 [$state(ntlmtok) type3Message]
  315. $state(ntlmtok) free
  316. PutsConnectQuery $token "NTLM $message3"
  317. fileevent $state(sock) readable \
  318. [list [namespace current]::AuthorizeNtlmStep3 $token]
  319. return
  320. }
  321. # https::AuthorizeNtlmStep3 --
  322. #
  323. # The third step of NTLM authorization procedure: receive and
  324. # analyze server reply.
  325. #
  326. # Arguments:
  327. # token A connection token.
  328. #
  329. # Result:
  330. # Empty string.
  331. #
  332. # Side effects:
  333. # Server reply is received from a socket.
  334. proc https::AuthorizeNtlmStep3 {token} {
  335. variable $token
  336. upvar 0 $token state
  337. Debug 2 "https::AuthorizeNtlmStep3 token=$token"
  338. fileevent $state(sock) readable {}
  339. set code [ReadProxyAnswer $token]
  340. if {$code >= 200 && $code < 300} {
  341. # Success
  342. while {[string length [gets $state(sock)]]} { }
  343. Finish $token
  344. } else {
  345. # Failure
  346. Finish $token $state(result)
  347. }
  348. return
  349. }
  350. # https::PutsConnectQuery --
  351. #
  352. # Sends CONNECT query to a proxy server.
  353. #
  354. # Arguments:
  355. # token A connection token.
  356. # auth (optional) A proxy authorization string.
  357. #
  358. # Result:
  359. # Empty string.
  360. #
  361. # Side effects:
  362. # Some info is sent to a proxy.
  363. proc https::PutsConnectQuery {token {auth ""}} {
  364. variable $token
  365. upvar 0 $token state
  366. Debug 2 "https::PutsConnectQuery token=$token auth=$auth"
  367. fconfigure $state(sock) -buffering line -translation auto
  368. puts $state(sock) "CONNECT $state(addr):$state(port) HTTP/1.1"
  369. puts $state(sock) "Proxy-Connection: keep-alive"
  370. if {[string length $state(-useragent)]} {
  371. puts $state(sock) "User-Agent: $state(-useragent)"
  372. }
  373. if {[string length $auth]} {
  374. puts $state(sock) "Proxy-Authorization: $auth"
  375. }
  376. puts $state(sock) ""
  377. return
  378. }
  379. # https::ReadProxyAnswer --
  380. #
  381. # Reads the first line of a proxy answer with a result code.
  382. #
  383. # Arguments:
  384. # token A connection token.
  385. #
  386. # Result:
  387. # The HTTP result code.
  388. #
  389. # Side effects:
  390. # Status line is read form a socket.
  391. # Variable state(result) is set to a just read line.
  392. proc https::ReadProxyAnswer {token} {
  393. variable $token
  394. upvar 0 $token state
  395. Debug 2 "https::ReadProxyAnswer token=$token"
  396. fconfigure $state(sock) -buffering line -translation auto
  397. set state(result) [gets $state(sock)]
  398. set code [lindex [split $state(result) { }] 1]
  399. if {[string is integer -strict $code]} {
  400. return $code
  401. } else {
  402. # Invalid code
  403. return 0
  404. }
  405. }
  406. # https::ReadProxyJunk --
  407. #
  408. # Reads the body part of a proxy answer.
  409. #
  410. # Arguments:
  411. # token A connection token.
  412. #
  413. # Result:
  414. # Empty string.
  415. #
  416. # Side effects:
  417. # Some info is read from a socket and discarded.
  418. proc https::ReadProxyJunk {token length} {
  419. variable $token
  420. upvar 0 $token state
  421. Debug 2 "https::ReadProxyJunk token=$token, length=$length"
  422. fconfigure $state(sock) -buffering none -translation binary
  423. if {$length != -1} {
  424. read $state(sock) $length
  425. } else {
  426. read $state(sock)
  427. }
  428. return
  429. }
  430. # https::HttpHeaderName --
  431. #
  432. # Returns HTTP header name (converted to lowercase).
  433. #
  434. # Arguments:
  435. # header A HTTP header.
  436. #
  437. # Result:
  438. # A header name.
  439. #
  440. # Side effects
  441. # None.
  442. proc https::HttpHeaderName {header} {
  443. set hlist [split $header ":"]
  444. return [string tolower [lindex $hlist 0]]
  445. }
  446. # https::HttpHeaderBody --
  447. #
  448. # Returns HTTP header body.
  449. #
  450. # Arguments:
  451. # header A HTTP header.
  452. #
  453. # Result:
  454. # A header body.
  455. #
  456. # Side effects
  457. # None.
  458. proc https::HttpHeaderBody {header} {
  459. set hlist [split $header ":"]
  460. set body [join [lrange $hlist 1 end] ":"]
  461. return [string trim $body]
  462. }
  463. # https::Timeout --
  464. #
  465. # This proc is called in case of timeout.
  466. #
  467. # Arguments:
  468. # token A connection token.
  469. #
  470. # Result:
  471. # An empty string.
  472. #
  473. # Side effects:
  474. # A proxy negotiation is finished with error.
  475. proc https::Timeout {token} {
  476. Finish $token timeout
  477. return
  478. }
  479. # https::Free --
  480. #
  481. # Frees a connection token.
  482. #
  483. # Arguments:
  484. # token A connection token.
  485. #
  486. # Result:
  487. # An empty string.
  488. #
  489. # Side effects:
  490. # A connection token and its state informationa are destroyed.
  491. proc https::Free {token} {
  492. variable $token
  493. upvar 0 $token state
  494. catch {after cancel $state(timeoutid)}
  495. catch {unset state}
  496. return
  497. }
  498. # https::Finish --
  499. #
  500. # Finishes a negotiation process.
  501. #
  502. # Arguments:
  503. # token A connection token.
  504. # errormsg (optional) error message.
  505. #
  506. # Result:
  507. # An empty string.
  508. #
  509. # Side effects:
  510. # If connection is asynchronous then a callback is executed.
  511. # Otherwise state(status) is set to allow https::connect to return
  512. # with either success or error.
  513. proc https::Finish {token {errormsg ""}} {
  514. variable $token
  515. upvar 0 $token state
  516. Debug 2 "https::Finish token=$token, errormsg=$errormsg"
  517. catch {after cancel $state(timeoutid)}
  518. if {$state(async)} {
  519. if {[string length $errormsg]} {
  520. catch {close $state(sock)}
  521. uplevel #0 $state(-command) [list ERROR $errormsg]
  522. } else {
  523. uplevel #0 $state(-command) [list OK $state(sock)]
  524. }
  525. Free $token
  526. } else {
  527. if {[string length $errormsg]} {
  528. catch {close $state(sock)}
  529. set state(sock) $errormsg
  530. set state(status) ERROR
  531. } else {
  532. set state(status) OK
  533. }
  534. }
  535. return
  536. }
  537. # https::Debug --
  538. #
  539. # Prints debug information.
  540. #
  541. # Arguments:
  542. # num A debug level.
  543. # str A debug message.
  544. #
  545. # Result:
  546. # An empty string.
  547. #
  548. # Side effects:
  549. # A debug message is printed to the console if the value of
  550. # https::debug variable is not less than num.
  551. proc https::Debug {num str} {
  552. variable debug
  553. if {$num <= $debug} {
  554. puts $str
  555. }
  556. return
  557. }
  558. # Test
  559. if {0} {
  560. set s [socket 192.168.0.1 3128]
  561. set t [https::connect $s google.com 443]
  562. puts $t
  563. close $t
  564. set s [socket 192.168.0.1 3128]
  565. set t [https::connect $s google.com 80]
  566. puts $t
  567. close $t
  568. }