File

plugins/jingle.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 timer = require "prosody.util.timer";
local new_id = require "prosody.util.id".short;

local xmlns_jingle = "urn:xmpp:jingle:1";
local xmlns_jingle_errors = "urn:xmpp:jingle:errors:1";

local jingle_mt = {};
jingle_mt.__index = jingle_mt;

local registered_transports = {};
local registered_content_types = {};

function verse.plugins.jingle(stream)
	stream:hook("ready", function ()
		stream:add_disco_feature(xmlns_jingle);
	end, 10);

	function stream:jingle(to)
		return verse.eventable(setmetatable(base or {
			role = "initiator";
			peer = to;
			sid = new_id();
			stream = stream;
		}, jingle_mt));
	end

	function stream:register_jingle_transport(transport)
		-- transport is a function that receives a
		-- <transport> element, and returns a connection
		-- We wait for 'connected' on that connection,
		-- and use :send() and 'incoming-raw'.
	end

	function stream:register_jingle_content_type(content)
		-- Call content() for every 'incoming-raw'?
		-- I think content() returns the object we return
		-- on jingle:accept()
	end

	local function handle_incoming_jingle(stanza)
		local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
		local sid = jingle_tag.attr.sid;
		local action = jingle_tag.attr.action;
		local result = stream:event("jingle/"..sid, stanza);
		if result == true then
			-- Ack
			stream:send(verse.reply(stanza));
			return true;
		end
		-- No existing Jingle object handled this action, our turn...
		if action ~= "session-initiate" then
			-- Trying to send a command to a session we don't know
			local reply = verse.error_reply(stanza, "cancel", "item-not-found")
				:tag("unknown-session", { xmlns = xmlns_jingle_errors }):up();
			stream:send(reply);
			return;
		end

		-- Ok, session-initiate, new session

		-- Create new Jingle object
		local sid = jingle_tag.attr.sid;

		local jingle = verse.eventable{
			role = "receiver";
			peer = stanza.attr.from;
			sid = sid;
			stream = stream;
		};

		setmetatable(jingle, jingle_mt);

		local content_tag;
		local content, transport;
		for tag in jingle_tag:childtags() do
			if tag.name == "content" and tag.attr.xmlns == xmlns_jingle then
			 	local description_tag = tag:child_with_name("description");
			 	local description_xmlns = description_tag.attr.xmlns;
			 	if description_xmlns then
			 		local desc_handler = stream:event("jingle/content/"..description_xmlns, jingle, description_tag);
			 		if desc_handler then
			 			content = desc_handler;
			 		end
			 	end

				local transport_tag = tag:child_with_name("transport");
				local transport_xmlns = transport_tag.attr.xmlns;

				transport = stream:event("jingle/transport/"..transport_xmlns, jingle, transport_tag);
				if content and transport then
					content_tag = tag;
					break;
				end
			end
		end
		if not content then
			-- FIXME: Fail, no content
			stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified content is not supported"));
			return true;
		end

		if not transport then
			-- FIXME: Refuse session, no transport
			stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified transport is not supported"));
			return true;
		end

		stream:send(verse.reply(stanza));

		jingle.content_tag = content_tag;
		jingle.creator, jingle.name = content_tag.attr.creator, content_tag.attr.name;
		jingle.content, jingle.transport = content, transport;

		function jingle:decline()
			-- FIXME: Decline session
		end

		stream:hook("jingle/"..sid, function (stanza)
			if stanza.attr.from ~= jingle.peer then
				return false;
			end
			local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
			return jingle:handle_command(jingle_tag);
		end);

		stream:event("jingle", jingle);
		return true;
	end

	function jingle_mt:handle_command(jingle_tag)
		local action = jingle_tag.attr.action;
		stream:debug("Handling Jingle command: %s", action);
		if action == "session-terminate" then
			self:destroy();
		elseif action == "session-accept" then
			-- Yay!
			self:handle_accepted(jingle_tag);
		elseif action == "transport-info" then
			stream:debug("Handling transport-info");
			self.transport:info_received(jingle_tag);
		elseif action == "transport-replace" then
			-- FIXME: Used for IBB fallback
			stream:error("Peer wanted to swap transport, not implemented");
		else
			-- FIXME: Reply unhandled command
			stream:warn("Unhandled Jingle command: %s", action);
			return nil;
		end
		return true;
	end

	function jingle_mt:send_command(command, element, callback)
		local stanza = verse.iq({ to = self.peer, type = "set" })
			:tag("jingle", {
				xmlns = xmlns_jingle,
				sid = self.sid,
				action = command,
				initiator = self.role == "initiator" and self.stream.jid or nil,
				responder = self.role == "responder" and self.jid or nil,
			}):add_child(element);
		if not callback then
			self.stream:send(stanza);
		else
			self.stream:send_iq(stanza, callback);
		end
	end

	function jingle_mt:accept(options)
		local accept_stanza = verse.iq({ to = self.peer, type = "set" })
			:tag("jingle", {
				xmlns = xmlns_jingle,
				sid = self.sid,
				action = "session-accept",
				responder = stream.jid,
			})
				:tag("content", { creator = self.creator, name = self.name });

		local content_accept_tag = self.content:generate_accept(self.content_tag:child_with_name("description"), options);
		accept_stanza:add_child(content_accept_tag);

		local transport_accept_tag = self.transport:generate_accept(self.content_tag:child_with_name("transport"), options);
		accept_stanza:add_child(transport_accept_tag);

		local jingle = self;
		stream:send_iq(accept_stanza, function (result)
			if result.attr.type == "error" then
				local type, condition, text = result:get_error();
				stream:error("session-accept rejected: %s", condition); -- FIXME: Notify
				return false;
			end
			jingle.transport:connect(function (conn)
				stream:warn("CONNECTED (receiver)!!!");
				jingle.state = "active";
				jingle:event("connected", conn);
			end);
		end);
	end


	stream:hook("iq/"..xmlns_jingle, handle_incoming_jingle);
	return true;
end

function jingle_mt:offer(name, content)
	local session_initiate = verse.iq({ to = self.peer, type = "set" })
		:tag("jingle", { xmlns = xmlns_jingle, action = "session-initiate",
			initiator = self.stream.jid, sid = self.sid });

	-- Content tag
	session_initiate:tag("content", { creator = self.role, name = name });

	-- Need description element from someone who can turn 'content' into XML
	local description = self.stream:event("jingle/describe/"..name, content);

	if not description then
		return false, "Unknown content type";
	end

	session_initiate:add_child(description);

	-- FIXME: Sort transports by 1) recipient caps 2) priority (SOCKS vs IBB, etc.)
	-- Fixed to s5b in the meantime
	local transport = self.stream:event("jingle/transport/".."urn:xmpp:jingle:transports:s5b:1", self);
	self.transport = transport;

	session_initiate:add_child(transport:generate_initiate());

	self.stream:debug("Hooking %s", "jingle/"..self.sid);
	self.stream:hook("jingle/"..self.sid, function (stanza)
		if stanza.attr.from ~= self.peer then
			return false;
		end
		local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
		return self:handle_command(jingle_tag)
	end);

	self.stream:send_iq(session_initiate, function (result)
		if result.attr.type == "error" then
			self.state = "terminated";
			local type, condition, text = result:get_error();
			return self:event("error", { type = type, condition = condition, text = text });
		end
	end);
	self.state = "pending";
end

function jingle_mt:terminate(reason)
	local reason_tag = verse.stanza("reason"):tag(reason or "success");
	self:send_command("session-terminate", reason_tag, function (result)
		self.state = "terminated";
		self.transport:disconnect();
		self:destroy();
	end);
end

function jingle_mt:destroy()
	self:event("terminated");
	self.stream:unhook("jingle/"..self.sid, self.handle_command);
end

function jingle_mt:handle_accepted(jingle_tag)
	local transport_tag = jingle_tag:child_with_name("transport");
	self.transport:handle_accepted(transport_tag);
	self.transport:connect(function (conn)
		self.stream:debug("CONNECTED (initiator)!")
		-- Connected, send file
		self.state = "active";
		self:event("connected", conn);
	end);
end

function jingle_mt:set_source(source, auto_close)
	local function pump()
		local chunk, err = source();
		if chunk and chunk ~= "" then
			self.transport.conn:send(chunk);
		elseif chunk == "" then
			return pump(); -- We need some data!
		elseif chunk == nil then
			if auto_close then
				self:terminate();
			end
			self.transport.conn:unhook("drained", pump);
			source = nil;
		end
	end
	self.transport.conn:hook("drained", pump);
	pump();
end

function jingle_mt:set_sink(sink)
	self.transport.conn:hook("incoming-raw", sink);
	self.transport.conn:hook("disconnected", function (event)
		self.stream:debug("Closing sink...");
		local reason = event.reason;
		if reason == "closed" then reason = nil; end
		sink(nil, reason);
	end);
end