verse.plugins.proxy65: XEP-0065 plugin for file transfer through a proxy

Thu, 06 May 2010 10:34:27 +0100

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 06 May 2010 10:34:27 +0100
changeset 56
014bdb4154e9
parent 55
163beb198646
child 57
8e8bac82e119

verse.plugins.proxy65: XEP-0065 plugin for file transfer through a proxy

plugins/proxy65.lua file | annotate | diff | comparison | revisions
--- /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

mercurial