# HG changeset patch # User Matthew Wild # Date 1273138467 -3600 # Node ID 014bdb4154e91f1e3646b921df47890f053360e3 # Parent 163beb198646bff4d04eaa6ed349a2a5ac1db46d verse.plugins.proxy65: XEP-0065 plugin for file transfer through a proxy diff -r 163beb198646 -r 014bdb4154e9 plugins/proxy65.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/proxy65.lua Thu May 06 10:34:27 2010 +0100 @@ -0,0 +1,163 @@ +local events = require "util.events"; +local uuid = require "util.uuid"; +local sha1 = require "util.sha1"; + +local proxy65_mt = {}; +proxy65_mt.__index = proxy65_mt; + +local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; + +local negotiate_socks5; + +function verse.plugins.proxy65(stream) + stream.proxy65 = setmetatable({ stream = stream }, proxy65_mt); + stream:hook("disco-result", function (result) + -- Fill list with available proxies + end); + stream:hook("iq/"..xmlns_bytestreams, function (request) + local conn = verse.new(nil, { + initiator_jid = request.attr.from, + streamhosts = {}, + current_host = 0; + }); + + -- Parse hosts from request + for tag in request.tags[1]:childtags() do + if tag.name == "streamhost" then + table.insert(conn.streamhosts, tag.attr); + end + end + + --Attempt to connect to the next host + local function attempt_next_streamhost() + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + negotiate_socks5(stream, conn, request.tags[1].attr.sid, request.attr.from, stream.jid); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + stream:send(verse.error_reply(request, "cancel", "item-not-found")); + -- Let disconnected event fall through to user handlers... + end + + function conn:accept() + 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); + -- Send XMPP success notification + local reply = verse.reply(request) + :tag("query", request.tags[1].attr) + :tag("streamhost-used", { jid = conn.streamhosts[conn.current_host].jid }); + stream:send(reply); + end, 100); + attempt_next_streamhost(); + end + function conn:refuse() + -- FIXME: XMPP refused reply + end + stream:event("proxy65/request", conn); + end); +end + +function proxy65_mt:new(target_jid, proxies) + local conn = verse.new(nil, { + target_jid = target_jid; + bytestream_sid = uuid.generate(); + }); + + local request = verse.iq{type="set", to = target_jid} + :tag("query", { xmlns = xmlns_bytestreams, mode = "tcp", sid = conn.bytestream_sid }); + for _, proxy in ipairs(proxies or self.proxies) do + request:tag("streamhost", proxy):up(); + end + + + self.stream:send_iq(request, function (reply) + if reply.attr.type == "error" then + local type, condition, text = reply:get_error(); + conn:event("connection-failed", { conn = conn, type = type, condition = condition, text = text }); + else + -- Target connected to streamhost, connect ourselves + local streamhost_used = reply.tags[1]:get_child("streamhost-used"); + if not streamhost_used then + --FIXME: Emit error + end + conn.streamhost_jid = streamhost_used.attr.jid; + local host, port; + for _, proxy in ipairs(proxies or self.proxies) do + if proxy.jid == conn.streamhost_jid then + host, port = proxy.host, proxy.port; + break; + end + end + if not (host and port) then + --FIXME: Emit error + end + + conn:connect(host, port); + + local function handle_proxy_connected() + conn:unhook("connected", handle_proxy_connected); + -- Both of us connected, tell proxy to activate connection + local request = verse.iq{to = conn.streamhost_jid, type="set"} + :tag("query", { xmlns = xmlns_bytestreams, sid = conn.bytestream_sid }) + :tag("activate"):text(target_jid); + self.stream:send_iq(request, function (reply) + if reply.attr.type == "result" then + -- Connection activated, ready to use + conn:event("connected", conn); + else + --FIXME: Emit error + end + end); + return true; + end + conn:hook("connected", handle_proxy_connected, 100); + + negotiate_socks5(self.stream, conn, conn.bytestream_sid, self.stream.jid, target_jid); + end + end); + return conn; +end + +function negotiate_socks5(stream, conn, sid, requester_jid, target_jid) + local hash = sha1.sha1(sid..requester_jid..target_jid); + 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