# HG changeset patch # User Matthew Wild # Date 1282400888 -3600 # Node ID 9c5362d393f08d4f6ff79d90d7b001080c0c7f14 # Parent e45883a3f39a2c01b93dc7b75cb1d4095365b857 verse.plugins.jingle_s5b: Jingle SOCKS5 Bytestreams transport diff -r e45883a3f39a -r 9c5362d393f0 plugins/jingle_s5b.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/jingle_s5b.lua Sat Aug 21 15:28:08 2010 +0100 @@ -0,0 +1,215 @@ + +local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1"; +local sha1 = require "util.sha1".sha1; +local uuid_generate = require "util.uuid".generate; + +local function negotiate_socks5(conn, hash) + local function suppress_connected() + conn:unhook("connected", suppress_connected); + return true; + end + local function receive_connection_response(data) + conn:unhook("incoming-raw", receive_connection_response); + + if data:sub(1, 2) ~= "\005\000" then + return conn:event("error", "connection-failure"); + end + conn:event("connected"); + return true; + end + local function receive_auth_response(data) + conn:unhook("incoming-raw", receive_auth_response); + if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" + -- Server is not SOCKSv5, or does not allow no auth + local err = "version-mismatch"; + if data:sub(1,1) == "\005" then + err = "authentication-failure"; + end + return conn:event("error", err); + end + -- Request SOCKS5 connection + conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? + conn:hook("incoming-raw", receive_connection_response, 100); + return true; + end + conn:hook("connected", suppress_connected, 200); + conn:hook("incoming-raw", receive_auth_response, 100); + conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" +end + +local function connect_to_usable_streamhost(callback, streamhosts, auth_token) + local conn = verse.new(nil, { + streamhosts = streamhosts, + current_host = 0; + }); + --Attempt to connect to the next host + local function attempt_next_streamhost(event) + if event then + return callback(nil, event.reason); + end + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:debug("Attempting to connect to "..conn.streamhosts[conn.current_host].host..":"..conn.streamhosts[conn.current_host].port.."..."); + local ok, err = conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + if not ok then + conn:debug("Error connecting to proxy (%s:%s): %s", + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port, + err + ); + else + conn:debug("Connecting..."); + end + negotiate_socks5(conn, auth_token); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + return callback(nil); + -- Let disconnected event fall through to user handlers... + end + conn:hook("disconnected", attempt_next_streamhost, 100); + -- When this event fires, we're connected to a streamhost + conn:hook("connected", function () + conn:unhook("disconnected", attempt_next_streamhost); + callback(conn.streamhosts[conn.current_host], conn); + end, 100); + attempt_next_streamhost(); -- Set it in motion + return conn; +end + +function verse.plugins.jingle_s5b(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_s5b); + end, 10); + + local s5b = {}; + + function s5b:generate_initiate() + self.s5b_sid = uuid_generate(); + local transport = verse.stanza("transport", { xmlns = xmlns_s5b, + mode = "tcp", sid = self.s5b_sid }); + local p = 0; + for jid, streamhost in pairs(stream.proxy65.available_streamhosts) do + p = p + 1; + transport:tag("candidate", { jid = jid, host = streamhost.host, + port = streamhost.port, cid=jid, priority = p, type = "proxy" }):up(); + end + stream:debug("Have %d proxies", p) + return transport; + end + + function s5b:generate_accept(initiate_transport) + local candidates = {}; + self.s5b_peer_candidates = candidates; + self.s5b_mode = initiate_transport.attr.mode or "tcp"; + self.s5b_sid = initiate_transport.attr.sid or self.jingle.sid; + + -- Import the list of candidates the initiator offered us + for candidate in initiate_transport:childtags() do + --if candidate.attr.jid == "asterix4@jabber.lagaule.org/Gajim" + --and candidate.attr.host == "82.246.25.239" then + candidates[candidate.attr.cid] = { + type = candidate.attr.type; + jid = candidate.attr.jid; + host = candidate.attr.host; + port = tonumber(candidate.attr.port) or 0; + priority = tonumber(candidate.attr.priority) or 0; + cid = candidate.attr.cid; + }; + --end + end + + -- Import our own candidates + -- TODO ^ + local transport = verse.stanza("transport", { xmlns = xmlns_s5b }); + return transport; + end + + function s5b:connect(callback) + stream:warn("Connecting!"); + + local streamhost_array = {}; + for cid, streamhost in pairs(self.s5b_peer_candidates or {}) do + streamhost_array[#streamhost_array+1] = streamhost; + end + + if #streamhost_array > 0 then + self.connecting_peer_candidates = true; + local function onconnect(streamhost, conn) + self.jingle:send_command("transport-info", verse.stanza("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("candidate-used", { cid = streamhost.cid })); + self.onconnect_callback = callback; + self.conn = conn; + end + local auth_token = sha1(self.s5b_sid..self.peer..stream.jid, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + else + stream:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); + self.onconnect_callback = callback; + end + end + + function s5b:info_received(jingle_tag) + stream:warn("Info received"); + local transport_tag = jingle_tag:child_with_name("content"):child_with_name("transport"); + if transport_tag:get_child("candidate-used") and not self.connecting_peer_candidates then + local candidate_used = transport_tag:child_with_name("candidate-used"); + if candidate_used then + -- Connect straight away to candidate used, we weren't trying any anyway + local function onconnect(streamhost, conn) + if self.jingle.role == "initiator" then -- More correct would be - "is this a candidate we offered?" + -- Activate the stream + self.jingle.stream:send_iq(verse.iq({ to = streamhost.jid, type = "set" }) + :tag("query", { xmlns = xmlns_bytestreams, sid = self.s5b_sid }) + :tag("activate"):text(self.jingle.peer), function (result) + + if result.attr.type == "result" then + self.jingle:send_command("transport-info", verse.stanza("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("activated", { cid = candidate_used.attr.cid })); + self.conn = conn; + self.onconnect_callback(conn); + else + self.jingle.stream:error("Failed to activate bytestream"); + end + end); + end + end + + -- FIXME: Another assumption that cid==jid, and that it was our candidate + self.jingle.stream:debug("CID: %s", self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]); + local streamhost_array = { + self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]; + }; + + local auth_token = sha1(self.s5b_sid..stream.jid..self.peer, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + end + elseif transport_tag:get_child("activated") then + self.onconnect_callback(self.conn); + end + end + + function s5b:disconnect() + if self.conn then + self.conn:close(); + end + end + + function s5b:handle_accepted(jingle_tag) + end + + local s5b_mt = { __index = s5b }; + stream:hook("jingle/transport/"..xmlns_s5b, function (jingle) + return setmetatable({ + role = jingle.role, + peer = jingle.peer, + stream = jingle.stream, + jingle = jingle, + }, s5b_mt); + end); +end