plugins/jingle_s5b.lua

1
local verse = require "verse";
2
 
3
local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1";
4
local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams";
5
local sha1 = require "prosody.util.hashes".sha1;
6
local new_id = require "prosody.util.id".short;
7
 
8
local function negotiate_socks5(conn, hash)
9
	local function suppress_connected()
10
		conn:unhook("connected", suppress_connected);
11
		return true;
12
	end
13
	local function receive_connection_response(data)
14
		conn:unhook("incoming-raw", receive_connection_response);
15
 
16
		if data:sub(1, 2) ~= "\005\000" then
17
			return conn:event("error", "connection-failure");
18
		end
19
		conn:event("connected");
20
		return true;
21
	end
22
	local function receive_auth_response(data)
23
		conn:unhook("incoming-raw", receive_auth_response);
24
		if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION"
25
			-- Server is not SOCKSv5, or does not allow no auth
26
			local err = "version-mismatch";
27
			if data:sub(1,1) == "\005" then
28
				err = "authentication-failure";
29
			end
30
			return conn:event("error", err);
31
		end
32
		-- Request SOCKS5 connection
33
		conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"?
34
		conn:hook("incoming-raw", receive_connection_response, 100);
35
		return true;
36
	end
37
	conn:hook("connected", suppress_connected, 200);
38
	conn:hook("incoming-raw", receive_auth_response, 100);
39
	conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION"
40
end
41
 
42
local function connect_to_usable_streamhost(callback, streamhosts, auth_token)
43
	local conn = verse.new(nil, {
44
		streamhosts = streamhosts,
45
		current_host = 0;
46
	});
47
	--Attempt to connect to the next host
48
	local function attempt_next_streamhost(event)
49
		if event then
50
			return callback(nil, event.reason);
51
		end
52
		-- First connect, or the last connect failed
53
		if conn.current_host < #conn.streamhosts then
54
			conn.current_host = conn.current_host + 1;
55
			conn:debug("Attempting to connect to "..conn.streamhosts[conn.current_host].host..":"..conn.streamhosts[conn.current_host].port.."...");
56
			local ok, err = conn:connect(
57
				conn.streamhosts[conn.current_host].host,
58
				conn.streamhosts[conn.current_host].port
59
			);
60
			if not ok then
61
				conn:debug("Error connecting to proxy (%s:%s): %s",
62
					conn.streamhosts[conn.current_host].host,
63
					conn.streamhosts[conn.current_host].port,
64
					err
65
				);
66
			else
67
				conn:debug("Connecting...");
68
			end
69
			negotiate_socks5(conn, auth_token);
70
			return true; -- Halt processing of disconnected event
71
		end
72
		-- All streamhosts tried, none successful
73
		conn:unhook("disconnected", attempt_next_streamhost);
74
		return callback(nil);
75
		-- Let disconnected event fall through to user handlers...
76
	end
77
	conn:hook("disconnected", attempt_next_streamhost, 100);
78
	-- When this event fires, we're connected to a streamhost
79
	conn:hook("connected", function ()
80
		conn:unhook("disconnected", attempt_next_streamhost);
81
		callback(conn.streamhosts[conn.current_host], conn);
82
	end, 100);
83
	attempt_next_streamhost(); -- Set it in motion
84
	return conn;
85
end
86
 
87
function verse.plugins.jingle_s5b(stream)
88
	stream:hook("ready", function ()
89
		stream:add_disco_feature(xmlns_s5b);
90
	end, 10);
91
 
92
	local s5b = {};
93
 
94
	function s5b:generate_initiate()
95
		self.s5b_sid = new_id();
96
		local transport = verse.stanza("transport", { xmlns = xmlns_s5b,
97
			mode = "tcp", sid = self.s5b_sid });
98
		local p = 0;
99
		for jid, streamhost in pairs(stream.proxy65.available_streamhosts) do
100
			p = p + 1;
101
			transport:tag("candidate", { jid = jid, host = streamhost.host,
102
				port = streamhost.port, cid=jid, priority = p, type = "proxy" }):up();
103
		end
104
		stream:debug("Have %d proxies", p)
105
		return transport;
106
	end
107
 
108
	function s5b:generate_accept(initiate_transport)
109
		local candidates = {};
110
		self.s5b_peer_candidates = candidates;
111
		self.s5b_mode = initiate_transport.attr.mode or "tcp";
112
		self.s5b_sid = initiate_transport.attr.sid or self.jingle.sid;
113
 
114
		-- Import the list of candidates the initiator offered us
115
		for candidate in initiate_transport:childtags() do
116
			--if candidate.attr.jid == "asterix4@jabber.lagaule.org/Gajim"
117
			--and candidate.attr.host == "82.246.25.239" then
118
				candidates[candidate.attr.cid] = {
119
					type = candidate.attr.type;
120
					jid = candidate.attr.jid;
121
					host = candidate.attr.host;
122
					port = tonumber(candidate.attr.port) or 0;
123
					priority = tonumber(candidate.attr.priority) or 0;
124
					cid = candidate.attr.cid;
125
				};
126
			--end
127
		end
128
 
129
		-- Import our own candidates
130
		-- TODO ^
131
		local transport = verse.stanza("transport", { xmlns = xmlns_s5b });
132
		return transport;
133
	end
134
 
135
	function s5b:connect(callback)
136
		stream:warn("Connecting!");
137
 
138
		local streamhost_array = {};
139
		for cid, streamhost in pairs(self.s5b_peer_candidates or {}) do
140
			streamhost_array[#streamhost_array+1] = streamhost;
141
		end
142
 
143
		if #streamhost_array > 0 then
144
			self.connecting_peer_candidates = true;
145
			local function onconnect(streamhost, conn)
146
				self.jingle:send_command("transport-info", verse.stanza("content", { creator = self.creator, name = self.name })
147
					:tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid })
148
						:tag("candidate-used", { cid = streamhost.cid }));
149
				self.onconnect_callback = callback;
150
				self.conn = conn;
151
			end
152
			local auth_token = sha1(self.s5b_sid..self.peer..stream.jid, true);
153
			connect_to_usable_streamhost(onconnect, streamhost_array, auth_token);
154
		else
155
			stream:warn("Actually, I'm going to wait for my peer to tell me its streamhost...");
156
			self.onconnect_callback = callback;
157
		end
158
	end
159
 
160
	function s5b:info_received(jingle_tag)
161
		stream:warn("Info received");
162
		local content_tag = jingle_tag:child_with_name("content");
163
		local transport_tag = content_tag:child_with_name("transport");
164
		if transport_tag:get_child("candidate-used") and not self.connecting_peer_candidates then
165
			local candidate_used = transport_tag:child_with_name("candidate-used");
166
			if candidate_used then
167
				-- Connect straight away to candidate used, we weren't trying any anyway
168
				local function onconnect(streamhost, conn)
169
					if self.jingle.role == "initiator" then -- More correct would be - "is this a candidate we offered?"
170
						-- Activate the stream
171
						self.jingle.stream:send_iq(verse.iq({ to = streamhost.jid, type = "set" })
172
							:tag("query", { xmlns = xmlns_bytestreams, sid = self.s5b_sid })
173
								:tag("activate"):text(self.jingle.peer), function (result)
174
 
175
							if result.attr.type == "result" then
176
								self.jingle:send_command("transport-info", verse.stanza("content", content_tag.attr)
177
									:tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid })
178
										:tag("activated", { cid = candidate_used.attr.cid }));
179
								self.conn = conn;
180
								self.onconnect_callback(conn);
181
							else
182
								self.jingle.stream:error("Failed to activate bytestream");
183
							end
184
						end);
185
					end
186
				end
187
 
188
				-- FIXME: Another assumption that cid==jid, and that it was our candidate
189
				self.jingle.stream:debug("CID: %s", self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]);
190
				local streamhost_array = {
191
					self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid];
192
				};
193
 
194
				local auth_token = sha1(self.s5b_sid..stream.jid..self.peer, true);
195
				connect_to_usable_streamhost(onconnect, streamhost_array, auth_token);
196
			end
197
		elseif transport_tag:get_child("activated") then
198
			self.onconnect_callback(self.conn);
199
		end
200
	end
201
 
202
	function s5b:disconnect()
203
		if self.conn then
204
			self.conn:close();
205
		end
206
	end
207
 
208
	function s5b:handle_accepted(jingle_tag)
209
	end
210
 
211
	local s5b_mt = { __index = s5b };
212
	stream:hook("jingle/transport/"..xmlns_s5b, function (jingle)
213
		return setmetatable({
214
			role = jingle.role,
215
			peer = jingle.peer,
216
			stream = jingle.stream,
217
			jingle = jingle,
218
		}, s5b_mt);
219
	end);
220
end