[Tkabber-dev] r727 - in trunk/tkabber: . plugins/filetransfer plugins/si

tkabber-svn at jabber.ru tkabber-svn at jabber.ru
Sat Sep 23 23:48:56 MSD 2006


Author: sergei
Date: 2006-09-23 23:48:37 +0400 (Sat, 23 Sep 2006)
New Revision: 727

Modified:
   trunk/tkabber/ChangeLog
   trunk/tkabber/plugins/filetransfer/si.tcl
   trunk/tkabber/plugins/si/ibb.tcl
   trunk/tkabber/plugins/si/iqibb.tcl
   trunk/tkabber/plugins/si/socks5.tcl
   trunk/tkabber/si.tcl
Log:
	* plugins/filetransfer/si.tcl, plugins/si/ibb.tcl,
	  plugins/si/iqibb.tcl, plugins/si/socks5.tcl, si.tcl:
	  Index bytestreams by direction (in, out), connection ID, JID,
	  and SID instead of SID only.

	* plugins/si/socks5.tcl: Added mediated SOCKS5 connection
	  support.


Modified: trunk/tkabber/ChangeLog
===================================================================
--- trunk/tkabber/ChangeLog	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/ChangeLog	2006-09-23 19:48:37 UTC (rev 727)
@@ -1,13 +1,26 @@
+2006-09-23  Sergei Golovan  <sgolovan at nes.ru>
+
+	* plugins/filetransfer/si.tcl, plugins/si/ibb.tcl,
+	  plugins/si/iqibb.tcl, plugins/si/socks5.tcl, si.tcl:
+	  Index bytestreams by direction (in, out), connection ID, JID,
+	  and SID instead of SID only.
+
+	* plugins/si/socks5.tcl: Added mediated SOCKS5 connection
+	  support.
+	  
 2006-09-21  Sergei Golovan  <sgolovan at nes.ru>
 
-	* filetransfer.tcl, plugins/filetransfer/si.tcl,
-	  plugins/si/ibb.tcl, plugins/si/iqibb.tcl, plugins/si/socks5.tcl,
-	  si.tcl: Slightly redesigned filetransfer via SI (JEP-0095,
-	  JEP-0065, JEP-0047). Replaced all unnecessary vwaits by
-	  callbacks. Added new (undocumented yet) IQ-based IBB transport.
-	  Made IBB transport usable (now it does not throw all data
-	  immediately).
+	* plugins/filetransfer/si.tcl, plugins/si/ibb.tcl,
+	  plugins/si/iqibb.tcl, plugins/si/socks5.tcl, si.tcl:
+	  Slightly redesigned filetransfer via SI (JEP-0095, JEP-0065,
+	  JEP-0047). Replaced all unnecessary vwaits by callbacks.
+	  Added new (undocumented yet) IQ-based IBB transport. Made IBB
+	  transport usable (now it does not throw all data immediately).
 
+	* filetransfer.tcl, plugins/filetransfer/si.tcl: Use errors returned
+	  when file is being opened instead of simple file exisence check
+	  (thanks to Konstantin Khomoutov).
+
 	* jabberlib-tclxml/jabberlib.tcl: Added jlib::socket_ip function.
 
 	* userinfo.tcl: Made all userinfo fields flat.

Modified: trunk/tkabber/plugins/filetransfer/si.tcl
===================================================================
--- trunk/tkabber/plugins/filetransfer/si.tcl	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/plugins/filetransfer/si.tcl	2006-09-23 19:48:37 UTC (rev 727)
@@ -1,6 +1,6 @@
 # $Id$
 
-# File transfer via Stream Initiation (JEP-0095)
+# File transfer via Stream Initiation (JEP-0096)
 
 ###############################################################################
 
@@ -9,15 +9,14 @@
     set chunk_size 1024
 
     variable options
-    variable state
 
-    custom::defgroup SI \
+    custom::defgroup {Stream Initiation} \
 	[::msgcat::mc "Stream initiation options."] \
 	-group FileTransfer
 
     custom::defvar options(enable) 1 \
 	[::msgcat::mc "Enable SI transport for outgoing file transfers."] \
-	-group SI -type boolean
+	-group {Stream Initiation} -type boolean
 }
 
 set ::NS(file-transfer) http://jabber.org/protocol/si/profile/file-transfer
@@ -29,7 +28,6 @@
 
 proc si::send_file_dialog {jid args} {
     variable winid
-    variable state
 
     foreach {opt val} $args {
 	switch -- $opt {
@@ -42,8 +40,7 @@
     }
 
     set token [namespace current]::[incr winid]
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     set w .sfd$winid
     set state(w) $w
@@ -103,8 +100,7 @@
 ###############################################################################
 
 proc si::send_file_negotiate {token} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
     variable chunk_size
 
     $state(w) itemconfigure 0 -state disabled
@@ -132,27 +128,24 @@
 	$state(pb) configure -maximum $size
     }
 
-    set id [random 1000000000]
-    set state(id) $id
+    set state(stream) [si::newout $state(connid) $state(jid)]
 
     set profile [jlib::wrapper:createtag file \
 		     -vars [list xmlns $::NS(file-transfer) \
-				 id $id \
 				 name $name \
 				 size $size] \
 		     -subtags [list [jlib::wrapper:createtag desc \
 					 -chdata $desc]]]
 
-    si::connect $state(connid) $state(jid) $id $chunk_size \
-		application/octet-stream $::NS(file-transfer) $profile \
+    si::connect $state(stream) $chunk_size application/octet-stream \
+		$::NS(file-transfer) $profile \
 		[list [namespace current]::send_file $token]
 }
 
 ###############################################################################
 
 proc si::send_file {token res} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     if {![lindex $res 0]} {
 	MessageDlg .auth_err -aspect 50000 -icon error \
@@ -170,13 +163,12 @@
 }
 
 proc si::send_chunk {token} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
     variable chunk_size
 
     set chunk [read $state(fd) $chunk_size]
     if {$chunk != ""} {
-	si::send_data $state(id) $chunk \
+	si::send_data $state(stream) $chunk \
 		      [list [namespace current]::send_chunk_response $token]
     } else {
 	destroy $state(w)
@@ -184,10 +176,9 @@
 }
 
 proc si::send_chunk_response {token res} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
-    if {![info exists state(id)]} return
+    if {![info exists state(stream)]} return
 
     if {![lindex $res 0]} {
 	MessageDlg .auth_err -aspect 50000 -icon error \
@@ -195,7 +186,8 @@
 			  [lindex $res 1]] -type user \
 	    -buttons ok -default 0 -cancel 0
 	set state(progress) 0
-	si::close $state(id)
+	si::close $state(stream)
+	si::freeout $state(stream)
 	close $state(fd)
 	$state(w) itemconfigure 0 -state normal
 	return
@@ -208,12 +200,12 @@
 ###############################################################################
 
 proc si::send_file_close {token w1 w2} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     if {$w1 != $w2} return
 
-    catch { si::close $state(id) }
+    catch { si::close $state(stream) }
+    catch { si::freeout $state(stream) }
     catch { close $state(fd) }
     catch { unset $token }
 }
@@ -221,18 +213,18 @@
 ###############################################################################
 ###############################################################################
 
-proc si::recv_file_dialog {from id name size date hash desc} {
+proc si::recv_file_dialog {connid from id name size date hash desc} {
     variable winid
 
     set token [namespace current]::[incr winid]
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     set w .rfd$winid
     set state(w) $w
 
-    set state(id) $id
+    set state(connid) $connid
     set state(jid) $from
+    set state(id) $id
 
     Dialog $w -title [format [::msgcat::mc "Receive file from %s"] $from] \
 	-separator 1 -anchor e \
@@ -305,8 +297,7 @@
 }
 
 proc si::set_receive_file_name {token} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     set file [tk_getSaveFile -initialdir $state(dir) \
 			     -initialfile $state(name)]
@@ -318,20 +309,26 @@
 ###############################################################################
 
 proc si::recv_file_cancel {token} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     set state(result) [list error cancel not-allowed]
-
     destroy $state(w)
 }
 
 ###############################################################################
 
 proc si::recv_file_start {token} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
+    if {[catch {si::newin $state(connid) $state(jid) $state(id)} stream]} {
+	set state(result) [list error modify bad-request \
+				-text "Stream ID is in use"]
+	destroy $state(w)
+	return
+    }
+
+    set state(stream) $stream
+
     if {[catch {open $state(filename) w} fd]} {
 	ft::report_cannot_open_file $state(w) $state(filename) \
 				    [ft::get_POSIX_error_desc]
@@ -343,45 +340,44 @@
     set state(fd) $fd
     set w $state(w)
 
-    $w itemconfigure 0 -state disabled
-    bind $w <Destroy> [list [namespace current]::recv_file_close $token $w %W]
+    $state(w) itemconfigure 0 -state disabled
+    bind $state(w) <Destroy> [list [namespace current]::recv_file_close \
+				   $token $state(w) %W]
 
-    set id $state(id)
-
     si::set_readable_handler \
-	$id [list [namespace current]::recv_file_chunk $token]
+	$stream [list [namespace current]::recv_file_chunk $token]
     si::set_closed_handler \
-	$id [list [namespace current]::closed $token]
+	$stream [list [namespace current]::closed $token]
 
     set state(result) {}
 }
 
 ###############################################################################
 
-proc si::recv_file_chunk {token id} {
-    variable $token
-    upvar 0 $token state
+proc si::recv_file_chunk {token stream} {
+    upvar #0 $token state
 
-    if {[info exists state(id)] && $state(id) == $id} {
+    if {[info exists state(stream)] && $state(stream) == $stream} {
 	set fd $state(fd)
 	set filename $state(filename)
-	set data [si::read_data $id]
+	set data [si::read_data $stream]
 
 	debugmsg filetransfer "RECV into $filename data $data"
 
 	puts -nonewline $fd $data
 	set state(progress) [tell $fd]
-
+	return 1
+    } else {
+	return 0
     }
 }
 
 ###############################################################################
 
-proc si::closed {token id} {
-    variable $token
-    upvar 0 $token state
+proc si::closed {token stream} {
+    upvar #0 $token state
 
-    if {[info exists state(id)] && $state(id) == $id} {
+    if {[info exists state(stream)] && $state(stream) == $stream} {
 	debugmsg filetransfer CLOSE
     	destroy $state(w)
     }
@@ -390,18 +386,18 @@
 ###############################################################################
 
 proc si::recv_file_close {token w1 w2} {
-    variable $token
-    upvar 0 $token state
+    upvar #0 $token state
 
     if {$w1 != $w2} return
 
-    catch { close $state(fd) }
-    catch { unset $token }
+    catch {close $state(fd)}
+    catch {si::freein $state(stream)}
+    catch {unset $token}
 }
 
 ###############################################################################
 
-proc si::si_handler {from id mimetype child} {
+proc si::si_handler {connid from id mimetype child} {
     debugmsg filetransfer "SI set: [list $from $child]"
 
     jlib::wrapper:splitxml $child tag vars isempty chdata children
@@ -416,6 +412,7 @@
 	}
 
 	recv_file_dialog \
+	    $connid \
 	    $from \
 	    $id \
 	    [jlib::wrapper:getattr $vars name] \

Modified: trunk/tkabber/plugins/si/ibb.tcl
===================================================================
--- trunk/tkabber/plugins/si/ibb.tcl	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/plugins/si/ibb.tcl	2006-09-23 19:48:37 UTC (rev 727)
@@ -9,33 +9,31 @@
 
 ###############################################################################
 
-proc ibb::connect {connid jid sid chunk_size command} {
+proc ibb::connect {stream chunk_size command} {
+    upvar #0 $stream state
+
     set_status [::msgcat::mc "Opening IBB connection"]
 
     jlib::send_iq set \
 	[jlib::wrapper:createtag open \
 	     -vars [list xmlns $::NS(ibb) \
-			 sid $sid \
+			 sid $state(id) \
 			 block-size $chunk_size]] \
-	-to $jid \
+	-to $state(jid) \
 	-command [list [namespace current]::recv_connect_response \
-		       $connid $jid $sid $command]
+		       $stream $command]
 }
 
-proc ibb::recv_connect_response {connid jid sid command res child} {
+proc ibb::recv_connect_response {stream command res child} {
+    upvar #0 $stream state
+
     if {$res != "OK"} {
 	uplevel #0 $command [list [list 0 [error_to_string $child]]]
 	return
     }
 
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
-
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
-    set state(connid) $connid
-    set state(jid) $jid
     set state(seq) 0
     uplevel #0 $command 1
 }
@@ -44,15 +42,13 @@
 
 package require base64
 
-proc ibb::send_data {sid data command} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc ibb::send_data {stream data command} {
+    upvar #0 $stream state
 
     jlib::send_msg $state(jid) \
 	-xlist [list [jlib::wrapper:createtag data \
 			  -vars [list xmlns $::NS(ibb) \
-				      sid $sid \
+				      sid $state(id) \
 				      seq $state(seq)] \
 			  -chdata [base64::encode $data]]] \
 	-connection $state(connid)
@@ -64,19 +60,15 @@
 
 ###############################################################################
 
-proc ibb::close {sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc ibb::close {stream} {
+    upvar #0 $stream state
 
     jlib::send_iq set \
 	[jlib::wrapper:createtag close \
 	     -vars [list xmlns $::NS(ibb) \
-			sid $sid]] \
+			 sid $state(id)]] \
 	-to $state(jid) \
 	-connection $state(connid)
-
-    unset $token
 }
 
 ###############################################################################
@@ -84,14 +76,21 @@
 proc ibb::iq_set_handler {connid from lang child} {
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
-    set sid [jlib::wrapper:getattr $vars sid]
+    set id [jlib::wrapper:getattr $vars sid]
+    if {[catch {si::in $connid $from $id} stream]} {
+	return [list error modify bad-request \
+		     -text [::trans::trans $lang \
+					   "Stream ID has not been negotiated"]]
+    }
+    upvar #0 $stream state
 
     switch -- $tag {
 	open {
-	    # TODO
+	    set state(block-size) [jlib::wrapper:getattr $vars block-size]
+	    set state(seq) 0
 	}
 	close {
-	    si::closed $sid
+	    si::closed $stream
 	}
     }
 
@@ -106,19 +105,33 @@
 				 err thread priority x} {
     foreach item $x {
 	jlib::wrapper:splitxml $item tag vars isempty chdata children
+
 	set xmlns [jlib::wrapper:getattr $vars xmlns]
+
 	if {[string equal $xmlns $::NS(ibb)]} {
-	    set sid [jlib::wrapper:getattr $vars sid]
-	    # TODO: seq processing
+	    set id [jlib::wrapper:getattr $vars sid]
+	    if {[catch {si::in $connid $from $id} stream]} {
+		# Unknown Stream ID
+		return stop
+	    }
+	    upvar #0 $stream state
+
 	    set seq [jlib::wrapper:getattr $vars seq]
+	    if {$seq != $state(seq)} {
+		# Incorrect sequence number
+		si::closed $stream
+		return stop
+	    } else {
+		set state(seq) [expr {($state(seq) + 1) % 65536}]
+	    }
 	    set data $chdata
 
 	    if {[catch {set decoded [base64::decode $data]}]} {
-		# TODO
 		debugmsg si "IBB: WRONG DATA"
+		si::closed $stream
 	    } else {
 		debugmsg si "IBB: RECV DATA [list $data]"
-		si::recv_data $sid $decoded
+		si::recv_data $stream $decoded
 	    }
 	    return stop
 	}

Modified: trunk/tkabber/plugins/si/iqibb.tcl
===================================================================
--- trunk/tkabber/plugins/si/iqibb.tcl	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/plugins/si/iqibb.tcl	2006-09-23 19:48:37 UTC (rev 727)
@@ -9,33 +9,31 @@
 
 ###############################################################################
 
-proc iqibb::connect {connid jid sid chunk_size command} {
+proc iqibb::connect {stream chunk_size command} {
+    upvar #0 $stream state
+
     set_status [::msgcat::mc "Opening IQ-IBB connection"]
 
     jlib::send_iq set \
 	[jlib::wrapper:createtag open \
 	     -vars [list xmlns $::NS(iqibb) \
-			 sid $sid \
+			 sid $state(id) \
 			 block-size $chunk_size]] \
-	-to $jid \
+	-to $state(jid) \
 	-command [list [namespace current]::recv_connect_response \
-		      $connid $jid $sid $command]
+		      $stream $command]
 }
 
-proc iqibb::recv_connect_response {connid jid sid command res child} {
+proc iqibb::recv_connect_response {stream command res child} {
+    upvar #0 $stream state
+
     if {$res != "OK"} {
 	uplevel #0 $command [list [list 0 [error_to_string $child]]]
 	return
     }
 
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
-
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
-    set state(connid) $connid
-    set state(jid) $jid
     set state(seq) 0
     uplevel #0 $command 1
 }
@@ -44,25 +42,23 @@
 
 package require base64
 
-proc iqibb::send_data {sid data command} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc iqibb::send_data {stream data command} {
+    upvar #0 $stream state
 
     jlib::send_iq set \
 	[jlib::wrapper:createtag data \
 	     -vars [list xmlns $::NS(iqibb) \
-			 sid $sid \
+			 sid $state(id) \
 			 seq $state(seq)] \
 	     -chdata [base64::encode $data]] \
 	-to $state(jid) \
-	-command [list [namespace current]::send_data_ack $sid $command] \
+	-command [list [namespace current]::send_data_ack $stream $command] \
 	-connection $state(connid)
 
     set state(seq) [expr {($state(seq) + 1) % 65536}]
 }
 
-proc iqibb::send_data_ack {sid command res child} {
+proc iqibb::send_data_ack {stream command res child} {
     if {$res != "OK"} {
 	uplevel #0 $command [list [list 0 [error_to_string $child]]]
     } else {
@@ -72,19 +68,15 @@
 
 ###############################################################################
 
-proc iqibb::close {sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc iqibb::close {stream} {
+    upvar #0 $stream state
 
     jlib::send_iq set \
 	[jlib::wrapper:createtag close \
 	     -vars [list xmlns $::NS(iqibb) \
-			 sid $sid]] \
+			 sid $state(id)]] \
 	-to $state(jid) \
 	-connection $state(connid)
-
-    unset $token
 }
 
 ###############################################################################
@@ -92,28 +84,48 @@
 proc iqibb::iq_set_handler {connid from lang child} {
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
-    set sid [jlib::wrapper:getattr $vars sid]
+    set id [jlib::wrapper:getattr $vars sid]
+    if {[catch {si::in $connid $from $id} stream]} {
+	return [list error modify bad-request \
+		     -text [::trans::trans $lang \
+					   "Stream ID has not been negotiated"]]
+    }
+    upvar #0 $stream state
 
     switch -- $tag {
 	open {
-	    # TODO
+	    set state(block-size) [jlib::wrapper:getattr $vars block-size]
+	    set state(seq) 0
 	}
 	close {
-	    si::closed $sid
+	    si::closed $stream
 	}
 	data {
-	    set sid [jlib::wrapper:getattr $vars sid]
-	    # TODO: seq processing
 	    set seq [jlib::wrapper:getattr $vars seq]
+	    if {$seq != $state(seq)} {
+		si::closed $stream
+		return [list error modify bad-request \
+			     -text [::trans::trans $lang \
+					"Unexpected packet sequence number"]]
+	    } else {
+		set state(seq) [expr {($state(seq) + 1) % 65536}]
+	    }
 	    set data $chdata
 
 	    if {[catch {set decoded [base64::decode $data]}]} {
-		# TODO
 		debugmsg si "IQIBB: WRONG DATA"
-		return [list error modify bad-request]
+		si::closed $stream
+		return [list error modify bad-request \
+			     -text [::trans::trans $lang \
+					"Cannot decode recieved data"]]
 	    } else {
 		debugmsg si "IQIBB: RECV DATA [list $data]"
-		si::recv_data $sid $decoded
+		if {![si::recv_data $stream $decoded]} {
+		    si::closed $stream
+		    return [list error cancel not-allowed \
+				 -text [::trans::trans $lang \
+					    "File transfer is aborted"]]
+		}
 	    }
 	}
     }
@@ -121,14 +133,15 @@
     return [list result ""]
 }
 
-iq::register_handler set "" $::NS(iqibb) [namespace current]::iqibb::iq_set_handler
+iq::register_handler set "" $::NS(iqibb) \
+		     [namespace current]::iqibb::iq_set_handler
 
 ###############################################################################
 
 si::register_transport $::NS(iqibb) $::NS(iqibb) 70 \
-    [namespace current]::iqibb::connect \
-    [namespace current]::iqibb::send_data \
-    [namespace current]::iqibb::close
+		       [namespace current]::iqibb::connect \
+		       [namespace current]::iqibb::send_data \
+		       [namespace current]::iqibb::close
 
 ###############################################################################
 

Modified: trunk/tkabber/plugins/si/socks5.tcl
===================================================================
--- trunk/tkabber/plugins/si/socks5.tcl	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/plugins/si/socks5.tcl	2006-09-23 19:48:37 UTC (rev 727)
@@ -5,32 +5,43 @@
 
 namespace eval socks5 {}
 namespace eval socks5::target {}
-namespace eval socks5::initiator {}
+namespace eval socks5::initiator {
 
+    custom::defvar options(enable_mediated_connection) 1 \
+	[::msgcat::mc "Use mediated SOCKS5 connection if proxy is available."] \
+	-group {Stream Initiation} -type boolean
+
+    custom::defvar options(proxy_servers) "proxy.jabber.org" \
+	[::msgcat::mc "List of proxy servers for SOCKS5 bytestreams (all\
+		       available servers will be tried for mediated connection)."] \
+	-group {Stream Initiation} -type string
+}
+
 set ::NS(bytestreams) http://jabber.org/protocol/bytestreams
 
 ###############################################################################
 
-proc socks5::target::sock_connect {connid jid sid hosts} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::target::sock_connect {stream hosts} {
+    upvar #0 $stream state
 
     foreach host $hosts {
 	lassign $host addr port streamhost
 	debugmsg si "CONNECTING TO $addr:$port..."
+
 	if {[catch {set sock [socket -async $addr $port]}]} continue
-	debugmsg si "CONNECTED"
+
 	fconfigure $sock -translation binary -blocking no
+
+	puts -nonewline $sock "\x05\x01\x00"
+	if {[catch {flush $sock}]} continue
+
 	set state(sock) $sock
 
-	puts -nonewline $sock "\x05\x01\x00"
-	flush $sock
 	fileevent $sock readable \
-	    [list [namespace current]::wait_for_method $sock $connid $jid $sid]
+	    [list [namespace current]::wait_for_method $sock $stream]
 
 	# Can't avoid vwait, because this procedure must return result or error
-	vwait ${token}(status)
+	vwait ${stream}(status)
 
 	if {$state(status) == 0} continue
 
@@ -50,10 +61,8 @@
 
 ###############################################################################
 
-proc socks5::target::wait_for_method {sock connid jid sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::target::wait_for_method {sock stream} {
+    upvar #0 $stream state
 
     if {[catch {set data [read $sock]}]} {
 	::close $sock
@@ -62,6 +71,7 @@
     }
 
     if {[eof $sock]} {
+	::close $sock
 	set state(status) 0
 	return
     }
@@ -74,8 +84,8 @@
 	return
     }
 
-    set myjid [jlib::connection_jid $connid]
-    set hash [::sha1::sha1 $sid$jid$myjid]
+    set myjid [jlib::connection_jid $state(connid)]
+    set hash [::sha1::sha1 $state(id)$state(jid)$myjid]
 
     set len [binary format c [string length $hash]]
 
@@ -83,14 +93,11 @@
     flush $sock
 
     fileevent $sock readable \
-	[list [namespace current]::wait_for_reply $sock $jid $sid]
-
+	[list [namespace current]::wait_for_reply $sock $stream]
 }
 
-proc socks5::target::wait_for_reply {sock jid sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::target::wait_for_reply {sock stream} {
+    upvar #0 $stream state
 
     if {[catch {set data [read $sock]}]} {
 	::close $sock
@@ -113,15 +120,13 @@
 
     set state(status) 1
     fileevent $sock readable \
-	[list [namespace parent]::readable $sid $sock]
+	[list [namespace parent]::readable $stream $sock]
 }
 
 ###############################################################################
 
-proc socks5::target::send_data {sid data} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::target::send_data {stream data} {
+    upvar #0 $stream state
 
     puts -nonewline $state(sock) $data
     flush $state(sock)
@@ -131,64 +136,273 @@
 
 ###############################################################################
 
-proc socks5::target::close {sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::target::close {stream} {
+    upvar #0 $stream state
 
     ::close $state(sock)
-
-    unset $token
 }
 
 ###############################################################################
 ###############################################################################
 
-proc socks5::initiator::connect {connid jid sid chunk_size command} {
+proc socks5::initiator::connect {stream chunk_size command} {
+    variable options
     variable hash_sid
+    upvar #0 $stream state
 
     set_status [::msgcat::mc "Opening SOCKS5 listening socket"]
 
-    set servsock [socket -server [list [namespace current]::accept $sid] 0]
+    set servsock [socket -server [list [namespace current]::accept $stream] 0]
+    set state(servsock) $servsock
     lassign [fconfigure $servsock -sockname] addr hostname port
-    set ip [jlib::socket_ip $connid]
-    set myjid [jlib::connection_jid $connid]
-    set hash [::sha1::sha1 $sid$myjid$jid]
-    set hash_sid($hash) $sid
+    set ip [jlib::socket_ip $state(connid)]
+    set myjid [jlib::connection_jid $state(connid)]
+    set hash [::sha1::sha1 $state(id)$myjid$state(jid)]
+    set hash_sid($hash) $state(id)
+::close $servsock
+    set streamhosts [list [jlib::wrapper:createtag streamhost \
+			       -vars [list jid $myjid \
+					   host $ip \
+					   port $port]]]
 
+    if {!$options(enable_mediated_connection)} {
+	request $stream $streamhosts $command
+    } else {
+	set proxies [split $options(proxy_servers) " "]
+	set proxies1 {}
+	foreach p $proxies {
+	    if {$p != ""} {
+		lappend proxies1 $p
+	    }
+	}
+	request_proxy $stream $streamhosts $proxies1 $command
+    }
+}
+
+###############################################################################
+
+proc socks5::initiator::request_proxy {stream streamhosts proxies command} {
+    upvar #0 $stream state
+
+    if {[lempty $proxies]} {
+	request $stream $streamhosts $command
+    } else {
+	jlib::send_iq get \
+	    [jlib::wrapper:createtag query \
+		 -vars [list xmlns $::NS(bytestreams)]] \
+	    -to [lindex $proxies 0] \
+	    -command [list [namespace current]::recv_request_proxy_response \
+			   $stream $streamhosts [lrange $proxies 1 end] \
+			   $command] \
+	    -connection $state(connid)
+    }
+}
+
+proc socks5::initiator::recv_request_proxy_response \
+     {stream streamhosts proxies command res child} {
+
+    if {$res != "OK"} {
+	request_proxy $stream $streamhosts $proxies $command
+	return
+    }
+
+    jlib::wrapper:splitxml $child tag vars isempty chdata children
+
+    foreach ch $children {
+	jlib::wrapper:splitxml $ch tag1 vars1 isempty1 chdata1 children1
+	if {$tag1 == "streamhost"} {
+	    lappend streamhosts $ch
+	}
+    }
+    request_proxy $stream $streamhosts $proxies $command
+}
+
+###############################################################################
+
+proc socks5::initiator::request {stream streamhosts command} {
+    upvar #0 $stream state
+
     jlib::send_iq set \
 	[jlib::wrapper:createtag query \
 	     -vars [list xmlns $::NS(bytestreams) \
-			 sid $sid] \
-	     -subtags [list \
-			   [jlib::wrapper:createtag streamhost \
-				-vars [list jid $myjid \
-					    host $ip \
-					    port $port]]]] \
+			 sid $state(id)] \
+	     -subtags $streamhosts] \
+	-to $state(jid) \
+	-command [list [namespace current]::recv_request_response \
+		       $stream $streamhosts $command] \
+	-connection $state(connid)
+}
+
+proc socks5::initiator::recv_request_response \
+     {stream streamhosts command res child} {
+    upvar #0 $stream state
+
+    if {$res != "OK"} {
+	uplevel #0 $command [list [list 0 [error_to_string $child]]]
+	return
+    }
+
+    jlib::wrapper:splitxml $child tag vars isempty chdata children
+    jlib::wrapper:splitxml [lindex $children 0] \
+			   tag1 vars1 isempty1 chdata1 children1
+    if {$tag1 != "streamhost-used"} {
+	uplevel #0 $command [list [list 0 [::msgcat::mc "Illegal result"]]]
+	return
+    }
+    
+    set jid [jlib::wrapper:getattr $vars1 jid]
+    set idx 0
+    foreach streamhost $streamhosts {
+	jlib::wrapper:splitxml $streamhost tag2 vars2 isempty2 chdata2 children2
+	if {[jlib::wrapper:getattr $vars2 jid] == $jid} {
+	    break
+	}
+	incr idx
+    }
+    
+    if {$idx == 0} {
+	# Target uses nonmediated connection
+	uplevel #0 $command 1
+    } elseif {$idx == [llength $streamhosts]} {
+	# Target has reported missing JID
+	uplevel #0 $command [list [list 0 [::msgcat::mc "Illegal result"]]]
+    } else {
+	# TODO: zeroconf support
+	set jid [jlib::wrapper:getattr $vars2 jid]
+	set host [jlib::wrapper:getattr $vars2 host]
+	set port [jlib::wrapper:getattr $vars2 port]
+	
+	# Target uses proxy, so closing server socket
+	catch {::close $state(servsock)}
+	proxy_connect $stream $jid $host $port $command
+    }
+}
+
+###############################################################################
+
+proc socks5::initiator::proxy_connect {stream jid host port command} {
+    upvar #0 $stream state
+
+    debugmsg si "CONNECTING TO PROXY $host:$port..."
+    if {[catch {socket -async $host $port} sock]} {
+	debugmsg si "CONNECTION FAILED"
+	uplevel #0 $command [list [list 0 [::msgcat::mc \
+					       "Cannot connect to proxy"]]]
+	return
+    }
+    debugmsg si "CONNECTED"
+    fconfigure $sock -translation binary -blocking no
+    set state(sock) $sock
+
+    puts -nonewline $sock "\x05\x01\x00"
+    flush $sock
+    fileevent $sock readable \
+	[list [namespace current]::proxy_wait_for_method $sock $stream]
+
+    vwait ${stream}(status)
+
+    if {$state(status) == 0} {
+	debugmsg si "SOCKS5 NEGOTIATION FAILED"
+	uplevel #0 $command \
+		[list [list 0 [::msgcat::mc \
+				   "Cannot negotiate proxy connection"]]]
+	return
+    }
+
+    # Activate mediated connection
+    jlib::send_iq set \
+	[jlib::wrapper:createtag query \
+	     -vars [list xmlns $::NS(bytestreams) \
+			 sid $state(id)] \
+	     -subtags [list [jlib::wrapper:createtag activate \
+				 -chdata $state(jid)]]] \
 	-to $jid \
-	-command [list [namespace current]::recv_connect_response \
-		       $connid $jid $sid $command] \
-	-connection $connid
+	-command [list [namespace current]::proxy_activate_response \
+		       $stream $command] \
+	-connection $state(connid)
+    
 }
 
-proc socks5::initiator::recv_connect_response {connid jid sid command res child} {
+###############################################################################
+
+proc socks5::initiator::proxy_activate_response {stream command res child} {
+    upvar #0 $stream state
+
     if {$res != "OK"} {
-	uplevel #0 $command [list 0 [error_to_string $child]]
+	uplevel #0 $command [list [list 0 [error_to_string $child]]]
 	return
     }
 
-    # TODO
     uplevel #0 $command 1
-    return
 }
 
 ###############################################################################
 
-proc socks5::initiator::send_data {sid data command} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::initiator::proxy_wait_for_method {sock stream} {
+    upvar #0 $stream state
 
+    if {[catch {set data [read $sock]}]} {
+	::close $sock
+	set state(status) 0
+	return
+    }
+
+    if {[eof $sock]} {
+	::close $sock
+	set state(status) 0
+	return
+    }
+
+    binary scan $data cc ver method
+
+    if {$ver != 5 || $method != 0} {
+	::close $sock
+	set state(status) 0
+	return
+    }
+
+    set myjid [jlib::connection_jid $state(connid)]
+    set hash [::sha1::sha1 $state(id)$myjid$state(jid)]
+
+    set len [binary format c [string length $hash]]
+
+    puts -nonewline $sock "\x05\x01\x00\x03$len$hash\x00\x00"
+    flush $sock
+
+    fileevent $sock readable \
+	[list [namespace current]::proxy_wait_for_reply $sock $stream]
+}
+
+proc socks5::initiator::proxy_wait_for_reply {sock stream} {
+    upvar #0 $stream state
+
+    if {[catch {set data [read $sock]}]} {
+	::close $sock
+	set state(status) 0
+	return
+    }
+
+    if {[eof $sock]} {
+	set state(status) 0
+	return
+    }
+
+    binary scan $data cc ver rep
+
+    if {$ver != 5 || $rep != 0} {
+	::close $sock
+	set state(status) 0
+	return
+    }
+
+    set state(status) 1
+}
+
+###############################################################################
+
+proc socks5::initiator::send_data {stream data command} {
+    upvar #0 $stream state
+
     puts -nonewline $state(sock) $data
     flush $state(sock)
 
@@ -197,22 +411,17 @@
 
 ###############################################################################
 
-proc socks5::initiator::close {sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::initiator::close {stream} {
+    upvar #0 $stream state
 
     ::close $state(sock)
-
-    unset $token
+    catch {::close $state(servsock)}
 }
 
 ###############################################################################
 
-proc socks5::initiator::accept {sid sock addr port} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::initiator::accept {stream sock addr port} {
+    upvar #0 $stream state
 
     debugmsg si "CONNECT FROM $addr:$port"
 
@@ -220,13 +429,11 @@
     fconfigure $sock -translation binary -blocking no
 
     fileevent $sock readable \
-	[list [namespace current]::wait_for_methods $sock $sid]
+	[list [namespace current]::wait_for_methods $sock $stream]
 }
 
-proc socks5::initiator::wait_for_methods {sock sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::initiator::wait_for_methods {sock stream} {
+    upvar #0 $stream state
 
     if {[catch {set data [read $sock]}]} {
 	::close $sock
@@ -252,14 +459,12 @@
     flush $sock
 
     fileevent $sock readable \
-	[list [namespace current]::wait_for_request $sock $sid]
+	[list [namespace current]::wait_for_request $sock $stream]
 }
 
-proc socks5::initiator::wait_for_request {sock sid} {
-    set token [namespace current]::$sid
-    variable $token
-    upvar 0 $token state
+proc socks5::initiator::wait_for_request {sock stream} {
     variable hash_sid
+    upvar #0 $stream state
 
     if {[catch {set data [read $sock]}]} {
 	::close $sock
@@ -287,7 +492,7 @@
     debugmsg si "RECV HASH: $hash"
 
     if {[info exists hash_sid($hash)] && \
-	    [string equal $hash_sid($hash) $sid]} {
+	    [string equal $hash_sid($hash) $state(id)]} {
 	set reply [string replace $data 1 1 \x00]
 	puts -nonewline $sock $reply
 	flush $sock
@@ -303,13 +508,13 @@
 
 ###############################################################################
 
-proc socks5::readable {sid chan} {
+proc socks5::readable {stream chan} {
     if {![eof $chan]} {
 	set buf [read $chan 4096]
-	si::recv_data $sid $buf
+	si::recv_data $stream $buf
     } else {
 	fileevent $chan readable {}
-	si::closed $sid
+	si::closed $stream
     }
 }
 
@@ -318,26 +523,31 @@
 proc socks5::iq_set_handler {connid from lang child} {
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
-    if {$tag == "query"} {
-	set sid [jlib::wrapper:getattr $vars sid]
+    if {$tag != "query"} {
+	return [list error modify bad-request]
+    }
 
-	set hosts {}
-	foreach item $children {
-	    jlib::wrapper:splitxml $item tag1 vars1 isempty1 chdata1 children1
-	    switch -- $tag1 {
-		streamhost {
-		    lappend hosts [list [jlib::wrapper:getattr $vars1 host] \
-				        [jlib::wrapper:getattr $vars1 port] \
-				        [jlib::wrapper:getattr $vars1 jid]]
-		}
+    set id [jlib::wrapper:getattr $vars sid]
+    if {[catch {si::in $connid $from $id} stream]} {
+	return [list error modify bad-request \
+		     -text [::trans::trans $lang \
+					   "Stream ID has not been negotiated"]]
+    }
+
+    set hosts {}
+    foreach item $children {
+	jlib::wrapper:splitxml $item tag1 vars1 isempty1 chdata1 children1
+	switch -- $tag1 {
+	    streamhost {
+		lappend hosts [list [jlib::wrapper:getattr $vars1 host] \
+				    [jlib::wrapper:getattr $vars1 port] \
+				    [jlib::wrapper:getattr $vars1 jid]]
 	    }
 	}
+    }
 
-	debugmsg si [list $hosts]
-	[namespace current]::target::sock_connect $connid $from $sid $hosts
-    } else {
-	return [list error modify bad-request]
-    }
+    debugmsg si [list $hosts]
+    [namespace current]::target::sock_connect $stream $hosts
 }
 
 iq::register_handler set "" $::NS(bytestreams) \

Modified: trunk/tkabber/si.tcl
===================================================================
--- trunk/tkabber/si.tcl	2006-09-21 19:44:55 UTC (rev 726)
+++ trunk/tkabber/si.tcl	2006-09-23 19:48:37 UTC (rev 727)
@@ -1,5 +1,8 @@
 # $Id$
+# Stream Initiation (JEP-0095) implementation
 
+###############################################################################
+
 namespace eval si {
     set transport(list) {}
 }
@@ -7,14 +10,95 @@
 set ::NS(si) http://jabber.org/protocol/si
 
 ###############################################################################
+###############################################################################
 
-proc si::connect {connid jid id chunk_size mimetype profile profile_el command} {
-    variable connection
+proc si::newout {connid jid} {
+    variable streams
+
+    set id [random 1000000000]
+    while {[info exists streams(out,$connid,$jid,$id)]} {
+	set id [random 1000000000]
+    }
+    set streamid 0
+    set stream [namespace current]::0
+    while {[info exists $stream]} {
+	set stream [namespace current]::[incr streamid]
+    }
+    upvar #0 $stream state
+
+    set state(connid) $connid
+    set state(jid) $jid
+    set state(id) $id
+    set streams(out,$connid,$jid,$id) $stream
+
+    return $stream
+}
+
+proc si::freeout {stream} {
+    variable streams
+    upvar #0 $stream state
+
+    catch {
+	set connid $state(connid)
+	set jid $state(jid)
+	set id $state(id)
+
+	unset state
+	unset streams(out,$connid,$jid,$id)
+    }
+}
+
+###############################################################################
+
+proc si::newin {connid jid id} {
+    variable streams
+
+    if {[info exists streams(in,$connid,$jid,$id)]} {
+	return -code error
+    }
+
+    set streamid 0
+    set stream [namespace current]::0
+    while {[info exists $stream]} {
+	set stream [namespace current]::[incr streamid]
+    }
+    upvar #0 $stream state
+
+    set state(connid) $connid
+    set state(jid) $jid
+    set state(id) $id
+    set streams(in,$connid,$jid,$id) $stream
+
+    return $stream
+}
+
+proc si::in {connid jid id} {
+    variable streams
+
+    return $streams(in,$connid,$jid,$id)
+}
+
+proc si::freein {stream} {
+    variable streams
+    upvar #0 $stream state
+
+    catch {
+	set connid $state(connid)
+	set jid $state(jid)
+	set id $state(id)
+
+	unset state
+	unset streams(in,$connid,$jid,$id)
+    }
+}
+
+###############################################################################
+###############################################################################
+
+proc si::connect {stream chunk_size mimetype profile profile_el command} {
     variable transport
+    upvar #0 $stream state
 
-    set connection(connid,$id) $connid
-    set connection(jid,$id) $jid
-
     set trans [lsort -unique -index 1 $transport(list)]
     set options {}
     foreach t $trans {
@@ -42,7 +126,7 @@
 			[list [jlib::wrapper:createtag \
 				   field \
 				   -vars [list var stream-method \
-					      type list-single] \
+					       type list-single] \
 				   -subtags $opttags]]]]]
 
 
@@ -51,21 +135,21 @@
     jlib::send_iq set \
 	[jlib::wrapper:createtag si \
 	     -vars [list xmlns $::NS(si) \
-			id $id \
+			id $state(id) \
 			mime-type $mimetype \
 			profile $profile] \
 	     -subtags [list $profile_el $feature]] \
-	-to $jid \
-	-command [list si::connect_response $connid $jid $id $chunk_size \
+	-to $state(jid) \
+	-command [list si::connect_response $stream $chunk_size \
 					    $profile $command] \
-	-connection $connid
+	-connection $state(connid)
 }
 
 ###############################################################################
 
-proc si::connect_response {connid jid id chunk_size profile command res child} {
-    variable connection
+proc si::connect_response {stream chunk_size profile command res child} {
     variable transport
+    upvar #0 $stream state
 
     if {$res != "OK"} {
 	uplevel #0 $command [list [list 0 [error_to_string $child]]]
@@ -99,8 +183,8 @@
 
     if {[llength $opts] == 1 && [lcontain $options [lindex $opts 0]]} {
 	set name [lindex $opts 0]
-	set connection(transport,$id) $name
-	eval $transport(connect,$name) [list $connid $jid $id $chunk_size $command]
+	set state(transport) $name
+	eval $transport(connect,$name) [list $stream $chunk_size $command]
 	return
     }
     uplevel #0 $command \
@@ -109,67 +193,71 @@
 
 ###############################################################################
 
-proc si::set_readable_handler {id handler} {
-    variable connection
-    set connection(readable_handler,$id) $handler
-}
+proc si::send_data {stream data command} {
+    variable transport
+    upvar #0 $stream state
 
-proc si::set_closed_handler {id handler} {
-    variable connection
-    set connection(closed_handler,$id) $handler
+    eval $transport(send,$state(transport)) [list $stream $data $command]
 }
 
 ###############################################################################
 
-proc si::send_data {id data command} {
-    variable connection
+proc si::close {stream} {
     variable transport
-    eval $transport(send,$connection(transport,$id)) [list $id $data $command]
+    upvar #0 $stream state
+
+    eval $transport(close,$state(transport)) [list $stream]
+    set_status [::msgcat::mc "SI connection closed"]
 }
 
 ###############################################################################
+###############################################################################
 
-proc si::recv_data {id data} {
-    variable connection
-    debugmsg si "RECV_DATA [list $id $data]"
+proc si::set_readable_handler {stream handler} {
+    upvar #0 $stream state
 
-    append connection(data,$id) $data
-    if {[info exists connection(readable_handler,$id)]} {
-	eval $connection(readable_handler,$id) [list $id]
-    }
+    set state(readable_handler) $handler
 }
 
+proc si::set_closed_handler {stream handler} {
+    upvar #0 $stream state
+
+    set state(closed_handler) $handler
+}
+
 ###############################################################################
 
-proc si::read_data {id} {
-    variable connection
+proc si::recv_data {stream data} {
+    upvar #0 $stream state
 
-    set data $connection(data,$id)
-    set connection(data,$id) {}
-    return $data
+    debugmsg si "RECV_DATA [list $state(id) $data]"
+
+    append state(data) $data
+    eval $state(readable_handler) [list $stream]
 }
 
 ###############################################################################
 
-proc si::close {id} {
-    variable connection
-    variable transport
-    eval $transport(close,$connection(transport,$id)) [list $id]
-    set_status [::msgcat::mc "SI connection closed"]
+proc si::read_data {stream} {
+    upvar #0 $stream state
+
+    set data $state(data)
+    set state(data) {}
+    return $data
 }
 
 ###############################################################################
 
-proc si::closed {id} {
-    variable connection
-    if {[info exists connection(closed_handler,$id)]} {
-	eval $connection(closed_handler,$id) [list $id]
+proc si::closed {stream} {
+    upvar #0 $stream state
+
+    if {[info exists state(closed_handler)]} {
+	eval $state(closed_handler) [list $stream]
     }
 }
 
 ###############################################################################
 
-
 proc si::parse_negotiation {child} {
     jlib::wrapper:splitxml $child tag vars isempty chdata children
 
@@ -224,34 +312,8 @@
     return $options
 }
 
-proc si::negotiate_handler {from type options} {
-    variable transport
+###############################################################################
 
-    set trans [lsort -unique -index 1 $transport(list)]
-    set myoptions {}
-    foreach t $trans {
-	set name [lindex $t 0]
-	if {![info exists transport(allowed,$name)] || \
-		$transport(allowed,$name)} {
-	    lappend myoptions $transport(oppos,$name)
-	}
-    }
-
-    if {$options == {}} {
-	return $myoptions
-    }
-
-    foreach opt $options {
-	if {[lcontain $myoptions $opt]} {
-	    return [list $opt]
-	}
-    }
-    return {}
-}
-
-negotiate::register_handler jabber:iq:si si::negotiate_handler
-
-
 proc si::set_handler {connid from lang child} {
     variable profiledata
     variable transport
@@ -270,7 +332,7 @@
 	    set xmlns [jlib::wrapper:getattr $vars1 xmlns]
 	    if {[string equal $xmlns $profile]} {
 		set profile_res [$profiledata($profile) \
-				     $from $id $mimetype $item]
+				     $connid $from $id $mimetype $item]
 	    } elseif {[string equal $xmlns \
 			   http://jabber.org/protocol/feature-neg]} {
 		set options [parse_negotiation $item]
@@ -331,6 +393,8 @@
 
 iq::register_handler set "" $::NS(si) si::set_handler
 
+###############################################################################
+###############################################################################
 
 proc si::register_transport {name oppos prio connect send close} {
     variable transport
@@ -342,15 +406,14 @@
     set transport(close,$name) $close
 }
 
-namespace eval si {
-    plugins::load [file join plugins si] -uplevel 1
-}
+###############################################################################
 
 proc si::register_profile {profile handler} {
     variable profiledata
     set profiledata($profile) $handler
 }
 
+###############################################################################
 
 proc si::setup_customize {} {
     variable transport
@@ -362,9 +425,17 @@
 
 	custom::defvar transport(allowed,$name) 1 \
 	[format [::msgcat::mc "Enable SI transport %s."] $name] \
-	-type boolean -group SI
+	-type boolean -group {Stream Initiation}
     }
 }
 
 hook::add finload_hook si::setup_customize 40
 
+###############################################################################
+
+namespace eval si {
+    plugins::load [file join plugins si] -uplevel 1
+}
+
+###############################################################################
+



More information about the Tkabber-dev mailing list