# $Id: socks5.tcl 1325 2007-12-06 20:32:40Z sergei $
#
# SOCKS5 Bytestreams (XEP-0065) transport for SI
#

###############################################################################

namespace eval socks5 {}
namespace eval socks5::target {}
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.netlab.cz proxy.jabber.cd.chalmers.se" \
	[::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 {stream hosts lang} {
    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

	fconfigure $sock -translation binary -blocking no

	puts -nonewline $sock "\x05\x01\x00"
	if {[catch {flush $sock}]} continue

	set state(sock) $sock

	fileevent $sock readable \
	    [list [namespace current]::wait_for_method $sock $stream]

	# Can't avoid vwait, because this procedure must return result or error
	vwait ${stream}(status)

	if {$state(status) == 0} continue

	set res [jlib::wrapper:createtag query \
		     -vars [list xmlns $::NS(bytestreams)] \
	     -subtags [list \
			   [jlib::wrapper:createtag streamhost-used \
				-vars [list jid $streamhost]]]]

	return [list result $res]
    }

    debugmsg si "FAILED"

    return [list error cancel item-not-found \
		 -text [::trans::trans \
			    $lang \
			    "Cannot connect to any of the streamhosts"]]
}

###############################################################################

proc socks5::target::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 [encoding convertto utf-8 \
		   [tolower_node_and_domain [my_jid $state(connid) $state(jid)]]]
    set hisjid [encoding convertto utf-8 [tolower_node_and_domain $state(jid)]]
    set hash [::sha1::sha1 $state(id)$hisjid$myjid]

    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]::wait_for_reply $sock $stream]
}

proc socks5::target::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
    fileevent $sock readable \
	[list [namespace parent]::readable $stream $sock]
}

###############################################################################

proc socks5::target::send_data {stream data} {
    upvar #0 $stream state

    puts -nonewline $state(sock) $data
    flush $state(sock)

    return 1
}

###############################################################################

proc socks5::target::close {stream} {
    upvar #0 $stream state

    ::close $state(sock)
}

###############################################################################
###############################################################################

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 $stream] 0]
    set state(servsock) $servsock
    lassign [fconfigure $servsock -sockname] addr hostname port
    set ip [jlib::socket_ip $state(connid)]
    set myjid [encoding convertto utf-8 \
		   [tolower_node_and_domain [my_jid $state(connid) $state(jid)]]]
    set hisjid [encoding convertto utf-8 [tolower_node_and_domain $state(jid)]]
    set hash [::sha1::sha1 $state(id)$myjid$hisjid]
    set hash_sid($hash) $state(id)

    set streamhosts [list [jlib::wrapper:createtag streamhost \
			       -vars [list jid [my_jid $state(connid) $state(jid)] \
					   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 == "DISCONNECT"} {
	uplevel #0 $command [list [list 0 [::msgcat::mc "Disconnected"]]]
	return
    }

    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 $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
	::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]::proxy_activate_response \
		       $stream $command] \
	-connection $state(connid)
    
}

###############################################################################

proc socks5::initiator::proxy_activate_response {stream command res child} {
    upvar #0 $stream state

    if {$res != "OK"} {
	uplevel #0 $command [list [list 0 [error_to_string $child]]]
	return
    }

    uplevel #0 $command 1
}

###############################################################################

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 [encoding convertto utf-8 \
		   [tolower_node_and_domain [my_jid $state(connid) $state(jid)]]]
    set hisjid [encoding convertto utf-8 [tolower_node_and_domain $state(jid)]]
    set hash [::sha1::sha1 $state(id)$myjid$hisjid]

    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)

    after idle [list uplevel #0 $command 1]
}

###############################################################################

proc socks5::initiator::close {stream} {
    upvar #0 $stream state

    ::close $state(sock)
    catch {::close $state(servsock)}
}

###############################################################################

proc socks5::initiator::accept {stream sock addr port} {
    upvar #0 $stream state

    debugmsg si "CONNECT FROM $addr:$port"

    set state(sock) $sock
    fconfigure $sock -translation binary -blocking no

    fileevent $sock readable \
	[list [namespace current]::wait_for_methods $sock $stream]
}

proc socks5::initiator::wait_for_methods {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 ccc* ver nmethods methods

    if {$ver != 5 || ![lcontain $methods 0]} {
	puts -nonewline $sock "\x05\xff"
	::close $sock
	set state(status) 0
	return
    }

    puts -nonewline $sock "\x05\x00"
    flush $sock

    fileevent $sock readable \
	[list [namespace current]::wait_for_request $sock $stream]
}

proc socks5::initiator::wait_for_request {sock stream} {
    variable hash_sid
    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 ccccc ver cmd rsv atyp len

    if {$ver != 5 || $cmd != 1 || $atyp != 3} {
	set reply [string replace $data 1 1 \x07]
	puts -nonewline $sock $reply
	::close $sock
	set state(status) 0
	return
    }

    binary scan $data @5a${len} hash

    debugmsg si "RECV HASH: $hash"

    if {[info exists hash_sid($hash)] && \
	    [string equal $hash_sid($hash) $state(id)]} {
	set reply [string replace $data 1 1 \x00]
	puts -nonewline $sock $reply
	flush $sock

	fileevent $sock readable {}
    } else {
	set reply [string replace $data 1 1 \x02]
	puts -nonewline $sock $reply
	::close $sock
	set state(status) 0
    }
}

###############################################################################

proc socks5::readable {stream chan} {
    if {![eof $chan]} {
	set buf [read $chan 4096]
	si::recv_data $stream $buf
    } else {
	fileevent $chan readable {}
	si::closed $stream
    }
}

###############################################################################

proc socks5::iq_set_handler {connid from lang child} {
    jlib::wrapper:splitxml $child tag vars isempty chdata children

    if {$tag != "query"} {
	return [list error modify bad-request]
    }

    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 $stream $hosts $lang
}

iq::register_handler set "" $::NS(bytestreams) \
    [namespace current]::socks5::iq_set_handler

###############################################################################

si::register_transport $::NS(bytestreams) $::NS(bytestreams) 50 \
    [namespace current]::socks5::initiator::connect \
    [namespace current]::socks5::initiator::send_data \
    [namespace current]::socks5::initiator::close

###############################################################################

