File

plugins/jingle_s5b.lua @ 498:50d0bd035bb7

util.sasl.oauthbearer: Don't send authzid It's not needed and not recommended in XMPP unless we want to act as someone other than who we authenticate as. We find out the JID during resource binding.
author Kim Alvefur <zash@zash.se>
date Fri, 23 Jun 2023 12:09:49 +0200
parent 490:6b2f31da9610
line wrap: on
line source

local verse = require "verse";

local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1";
local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams";
local sha1 = require "prosody.util.hashes".sha1;
local new_id = require "prosody.util.id".short;

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 = new_id();
		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("content", { creator = self.creator, name = self.name })
					:tag("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 content_tag = jingle_tag:child_with_name("content");
		local transport_tag = content_tag: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("content", content_tag.attr)
									:tag("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