plugins/proxy65.lua

1
local verse =	require "verse";
2
local new_id = require "prosody.util.id".short;
3
local sha1 = require "prosody.util.hashes".sha1;
4
 
5
local proxy65_mt = {};
6
proxy65_mt.__index = proxy65_mt;
7
 
8
local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams";
9
 
10
local negotiate_socks5;
11
 
12
function verse.plugins.proxy65(stream)
13
	stream.proxy65 = setmetatable({ stream = stream }, proxy65_mt);
14
	stream.proxy65.available_streamhosts = {};
15
	local outstanding_proxies = 0;
16
	stream:hook("disco/service-discovered/proxy", function (service)
17
		-- Fill list with available proxies
18
		if service.type == "bytestreams" then
19
			outstanding_proxies = outstanding_proxies + 1;
20
			stream:send_iq(verse.iq({ to = service.jid, type = "get" })
21
				:tag("query", { xmlns = xmlns_bytestreams }), function (result)
22
 
23
				outstanding_proxies = outstanding_proxies - 1;
24
				if result.attr.type == "result" then
25
					local streamhost = result:get_child("query", xmlns_bytestreams)
26
						:get_child("streamhost").attr;
27
 
28
					stream.proxy65.available_streamhosts[streamhost.jid] = {
29
						jid = streamhost.jid;
30
						host = streamhost.host;
31
						port = tonumber(streamhost.port);
32
					};
33
				end
34
				if outstanding_proxies == 0 then
35
					stream:event("proxy65/discovered-proxies", stream.proxy65.available_streamhosts);
36
				end
37
			end);
38
		end
39
	end);
40
	stream:hook("iq/"..xmlns_bytestreams, function (request)
41
		local conn = verse.new(nil, {
42
			initiator_jid = request.attr.from,
43
			streamhosts = {},
44
			current_host = 0;
45
		});
46
 
47
		-- Parse hosts from request
48
		for tag in request.tags[1]:childtags() do
49
			if tag.name == "streamhost" then
50
				table.insert(conn.streamhosts, tag.attr);
51
			end
52
		end
53
 
54
		--Attempt to connect to the next host
55
		local function attempt_next_streamhost()
56
			-- First connect, or the last connect failed
57
			if conn.current_host < #conn.streamhosts then
58
				conn.current_host = conn.current_host + 1;
59
				conn:connect(
60
					conn.streamhosts[conn.current_host].host,
61
					conn.streamhosts[conn.current_host].port
62
				);
63
				negotiate_socks5(stream, conn, request.tags[1].attr.sid, request.attr.from, stream.jid);
64
				return true; -- Halt processing of disconnected event
65
			end
66
			-- All streamhosts tried, none successful
67
			conn:unhook("disconnected", attempt_next_streamhost);
68
			stream:send(verse.error_reply(request, "cancel", "item-not-found"));
69
			-- Let disconnected event fall through to user handlers...
70
		end
71
 
72
		function conn:accept()
73
			conn:hook("disconnected", attempt_next_streamhost, 100);
74
			-- When this event fires, we're connected to a streamhost
75
			conn:hook("connected", function ()
76
				conn:unhook("disconnected", attempt_next_streamhost);
77
				-- Send XMPP success notification
78
				local reply = verse.reply(request)
79
					:tag("query", request.tags[1].attr)
80
					:tag("streamhost-used", { jid = conn.streamhosts[conn.current_host].jid });
81
				stream:send(reply);
82
			end, 100);
83
			attempt_next_streamhost();
84
		end
85
		function conn:refuse()
86
			-- FIXME: XMPP refused reply
87
		end
88
		stream:event("proxy65/request", conn);
89
	end);
90
end
91
 
92
function proxy65_mt:new(target_jid, proxies)
93
	local conn = verse.new(nil, {
94
		target_jid = target_jid;
95
		bytestream_sid = new_id();
96
	});
97
 
98
	local request = verse.iq{type="set", to = target_jid}
99
		:tag("query", { xmlns = xmlns_bytestreams, mode = "tcp", sid = conn.bytestream_sid });
100
	for _, proxy in ipairs(proxies or self.proxies) do
101
		request:tag("streamhost", proxy):up();
102
	end
103
 
104
 
105
	self.stream:send_iq(request, function (reply)
106
		if reply.attr.type == "error" then
107
			local type, condition, text = reply:get_error();
108
			conn:event("connection-failed", { conn = conn, type = type, condition = condition, text = text });
109
		else
110
			-- Target connected to streamhost, connect ourselves
111
			local streamhost_used = reply.tags[1]:get_child("streamhost-used");
112
			-- if not streamhost_used then
113
				--FIXME: Emit error
114
			-- end
115
			conn.streamhost_jid = streamhost_used.attr.jid;
116
			local host, port;
117
			for _, proxy in ipairs(proxies or self.proxies) do
118
				if proxy.jid == conn.streamhost_jid then
119
					host, port = proxy.host, proxy.port;
120
					break;
121
				end
122
			end
123
			-- if not (host and port) then
124
				--FIXME: Emit error
125
			-- end
126
 
127
			conn:connect(host, port);
128
 
129
			local function handle_proxy_connected()
130
				conn:unhook("connected", handle_proxy_connected);
131
				-- Both of us connected, tell proxy to activate connection
132
				local activate_request = verse.iq{to = conn.streamhost_jid, type="set"}
133
					:tag("query", { xmlns = xmlns_bytestreams, sid = conn.bytestream_sid })
134
						:tag("activate"):text(target_jid);
135
				self.stream:send_iq(activate_request, function (activated)
136
					if activated.attr.type == "result" then
137
						-- Connection activated, ready to use
138
						conn:event("connected", conn);
139
					-- else --FIXME: Emit error
140
					end
141
				end);
142
				return true;
143
			end
144
			conn:hook("connected", handle_proxy_connected, 100);
145
 
146
			negotiate_socks5(self.stream, conn, conn.bytestream_sid, self.stream.jid, target_jid);
147
		end
148
	end);
149
	return conn;
150
end
151
 
152
function negotiate_socks5(stream, conn, sid, requester_jid, target_jid)
153
	local hash = sha1(sid..requester_jid..target_jid);
154
	local function suppress_connected()
155
		conn:unhook("connected", suppress_connected);
156
		return true;
157
	end
158
	local function receive_connection_response(data)
159
		conn:unhook("incoming-raw", receive_connection_response);
160
 
161
		if data:sub(1, 2) ~= "\005\000" then
162
			return conn:event("error", "connection-failure");
163
		end
164
		conn:event("connected");
165
		return true;
166
	end
167
	local function receive_auth_response(data)
168
		conn:unhook("incoming-raw", receive_auth_response);
169
		if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION"
170
			-- Server is not SOCKSv5, or does not allow no auth
171
			local err = "version-mismatch";
172
			if data:sub(1,1) == "\005" then
173
				err = "authentication-failure";
174
			end
175
			return conn:event("error", err);
176
		end
177
		-- Request SOCKS5 connection
178
		conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"?
179
		conn:hook("incoming-raw", receive_connection_response, 100);
180
		return true;
181
	end
182
	conn:hook("connected", suppress_connected, 200);
183
	conn:hook("incoming-raw", receive_auth_response, 100);
184
	conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION"
185
end