plugins/jingle.lua

1
local verse = require "verse";
2
local timer = require "prosody.util.timer";
3
local new_id = require "prosody.util.id".short;
4
 
5
local xmlns_jingle = "urn:xmpp:jingle:1";
6
local xmlns_jingle_errors = "urn:xmpp:jingle:errors:1";
7
 
8
local jingle_mt = {};
9
jingle_mt.__index = jingle_mt;
10
 
11
local registered_transports = {};
12
local registered_content_types = {};
13
 
14
function verse.plugins.jingle(stream)
15
	stream:hook("ready", function ()
16
		stream:add_disco_feature(xmlns_jingle);
17
	end, 10);
18
 
19
	function stream:jingle(to)
20
		return verse.eventable(setmetatable(base or {
21
			role = "initiator";
22
			peer = to;
23
			sid = new_id();
24
			stream = stream;
25
		}, jingle_mt));
26
	end
27
 
28
	function stream:register_jingle_transport(transport)
29
		-- transport is a function that receives a
30
		-- <transport> element, and returns a connection
31
		-- We wait for 'connected' on that connection,
32
		-- and use :send() and 'incoming-raw'.
33
	end
34
 
35
	function stream:register_jingle_content_type(content)
36
		-- Call content() for every 'incoming-raw'?
37
		-- I think content() returns the object we return
38
		-- on jingle:accept()
39
	end
40
 
41
	local function handle_incoming_jingle(stanza)
42
		local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
43
		local sid = jingle_tag.attr.sid;
44
		local action = jingle_tag.attr.action;
45
		local result = stream:event("jingle/"..sid, stanza);
46
		if result == true then
47
			-- Ack
48
			stream:send(verse.reply(stanza));
49
			return true;
50
		end
51
		-- No existing Jingle object handled this action, our turn...
52
		if action ~= "session-initiate" then
53
			-- Trying to send a command to a session we don't know
54
			local reply = verse.error_reply(stanza, "cancel", "item-not-found")
55
				:tag("unknown-session", { xmlns = xmlns_jingle_errors }):up();
56
			stream:send(reply);
57
			return;
58
		end
59
 
60
		-- Ok, session-initiate, new session
61
 
62
		-- Create new Jingle object
63
		local sid = jingle_tag.attr.sid;
64
 
65
		local jingle = verse.eventable{
66
			role = "receiver";
67
			peer = stanza.attr.from;
68
			sid = sid;
69
			stream = stream;
70
		};
71
 
72
		setmetatable(jingle, jingle_mt);
73
 
74
		local content_tag;
75
		local content, transport;
76
		for tag in jingle_tag:childtags() do
77
			if tag.name == "content" and tag.attr.xmlns == xmlns_jingle then
78
			 	local description_tag = tag:child_with_name("description");
79
			 	local description_xmlns = description_tag.attr.xmlns;
80
			 	if description_xmlns then
81
			 		local desc_handler = stream:event("jingle/content/"..description_xmlns, jingle, description_tag);
82
			 		if desc_handler then
83
			 			content = desc_handler;
84
			 		end
85
			 	end
86
 
87
				local transport_tag = tag:child_with_name("transport");
88
				local transport_xmlns = transport_tag.attr.xmlns;
89
 
90
				transport = stream:event("jingle/transport/"..transport_xmlns, jingle, transport_tag);
91
				if content and transport then
92
					content_tag = tag;
93
					break;
94
				end
95
			end
96
		end
97
		if not content then
98
			-- FIXME: Fail, no content
99
			stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified content is not supported"));
100
			return true;
101
		end
102
 
103
		if not transport then
104
			-- FIXME: Refuse session, no transport
105
			stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified transport is not supported"));
106
			return true;
107
		end
108
 
109
		stream:send(verse.reply(stanza));
110
 
111
		jingle.content_tag = content_tag;
112
		jingle.creator, jingle.name = content_tag.attr.creator, content_tag.attr.name;
113
		jingle.content, jingle.transport = content, transport;
114
 
115
		function jingle:decline()
116
			-- FIXME: Decline session
117
		end
118
 
119
		stream:hook("jingle/"..sid, function (stanza)
120
			if stanza.attr.from ~= jingle.peer then
121
				return false;
122
			end
123
			local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
124
			return jingle:handle_command(jingle_tag);
125
		end);
126
 
127
		stream:event("jingle", jingle);
128
		return true;
129
	end
130
 
131
	function jingle_mt:handle_command(jingle_tag)
132
		local action = jingle_tag.attr.action;
133
		stream:debug("Handling Jingle command: %s", action);
134
		if action == "session-terminate" then
135
			self:destroy();
136
		elseif action == "session-accept" then
137
			-- Yay!
138
			self:handle_accepted(jingle_tag);
139
		elseif action == "transport-info" then
140
			stream:debug("Handling transport-info");
141
			self.transport:info_received(jingle_tag);
142
		elseif action == "transport-replace" then
143
			-- FIXME: Used for IBB fallback
144
			stream:error("Peer wanted to swap transport, not implemented");
145
		else
146
			-- FIXME: Reply unhandled command
147
			stream:warn("Unhandled Jingle command: %s", action);
148
			return nil;
149
		end
150
		return true;
151
	end
152
 
153
	function jingle_mt:send_command(command, element, callback)
154
		local stanza = verse.iq({ to = self.peer, type = "set" })
155
			:tag("jingle", {
156
				xmlns = xmlns_jingle,
157
				sid = self.sid,
158
				action = command,
159
				initiator = self.role == "initiator" and self.stream.jid or nil,
160
				responder = self.role == "responder" and self.jid or nil,
161
			}):add_child(element);
162
		if not callback then
163
			self.stream:send(stanza);
164
		else
165
			self.stream:send_iq(stanza, callback);
166
		end
167
	end
168
 
169
	function jingle_mt:accept(options)
170
		local accept_stanza = verse.iq({ to = self.peer, type = "set" })
171
			:tag("jingle", {
172
				xmlns = xmlns_jingle,
173
				sid = self.sid,
174
				action = "session-accept",
175
				responder = stream.jid,
176
			})
177
				:tag("content", { creator = self.creator, name = self.name });
178
 
179
		local content_accept_tag = self.content:generate_accept(self.content_tag:child_with_name("description"), options);
180
		accept_stanza:add_child(content_accept_tag);
181
 
182
		local transport_accept_tag = self.transport:generate_accept(self.content_tag:child_with_name("transport"), options);
183
		accept_stanza:add_child(transport_accept_tag);
184
 
185
		local jingle = self;
186
		stream:send_iq(accept_stanza, function (result)
187
			if result.attr.type == "error" then
188
				local type, condition, text = result:get_error();
189
				stream:error("session-accept rejected: %s", condition); -- FIXME: Notify
190
				return false;
191
			end
192
			jingle.transport:connect(function (conn)
193
				stream:warn("CONNECTED (receiver)!!!");
194
				jingle.state = "active";
195
				jingle:event("connected", conn);
196
			end);
197
		end);
198
	end
199
 
200
 
201
	stream:hook("iq/"..xmlns_jingle, handle_incoming_jingle);
202
	return true;
203
end
204
 
205
function jingle_mt:offer(name, content)
206
	local session_initiate = verse.iq({ to = self.peer, type = "set" })
207
		:tag("jingle", { xmlns = xmlns_jingle, action = "session-initiate",
208
			initiator = self.stream.jid, sid = self.sid });
209
 
210
	-- Content tag
211
	session_initiate:tag("content", { creator = self.role, name = name });
212
 
213
	-- Need description element from someone who can turn 'content' into XML
214
	local description = self.stream:event("jingle/describe/"..name, content);
215
 
216
	if not description then
217
		return false, "Unknown content type";
218
	end
219
 
220
	session_initiate:add_child(description);
221
 
222
	-- FIXME: Sort transports by 1) recipient caps 2) priority (SOCKS vs IBB, etc.)
223
	-- Fixed to s5b in the meantime
224
	local transport = self.stream:event("jingle/transport/".."urn:xmpp:jingle:transports:s5b:1", self);
225
	self.transport = transport;
226
 
227
	session_initiate:add_child(transport:generate_initiate());
228
 
229
	self.stream:debug("Hooking %s", "jingle/"..self.sid);
230
	self.stream:hook("jingle/"..self.sid, function (stanza)
231
		if stanza.attr.from ~= self.peer then
232
			return false;
233
		end
234
		local jingle_tag = stanza:get_child("jingle", xmlns_jingle);
235
		return self:handle_command(jingle_tag)
236
	end);
237
 
238
	self.stream:send_iq(session_initiate, function (result)
239
		if result.attr.type == "error" then
240
			self.state = "terminated";
241
			local type, condition, text = result:get_error();
242
			return self:event("error", { type = type, condition = condition, text = text });
243
		end
244
	end);
245
	self.state = "pending";
246
end
247
 
248
function jingle_mt:terminate(reason)
249
	local reason_tag = verse.stanza("reason"):tag(reason or "success");
250
	self:send_command("session-terminate", reason_tag, function (result)
251
		self.state = "terminated";
252
		self.transport:disconnect();
253
		self:destroy();
254
	end);
255
end
256
 
257
function jingle_mt:destroy()
258
	self:event("terminated");
259
	self.stream:unhook("jingle/"..self.sid, self.handle_command);
260
end
261
 
262
function jingle_mt:handle_accepted(jingle_tag)
263
	local transport_tag = jingle_tag:child_with_name("transport");
264
	self.transport:handle_accepted(transport_tag);
265
	self.transport:connect(function (conn)
266
		self.stream:debug("CONNECTED (initiator)!")
267
		-- Connected, send file
268
		self.state = "active";
269
		self:event("connected", conn);
270
	end);
271
end
272
 
273
function jingle_mt:set_source(source, auto_close)
274
	local function pump()
275
		local chunk, err = source();
276
		if chunk and chunk ~= "" then
277
			self.transport.conn:send(chunk);
278
		elseif chunk == "" then
279
			return pump(); -- We need some data!
280
		elseif chunk == nil then
281
			if auto_close then
282
				self:terminate();
283
			end
284
			self.transport.conn:unhook("drained", pump);
285
			source = nil;
286
		end
287
	end
288
	self.transport.conn:hook("drained", pump);
289
	pump();
290
end
291
 
292
function jingle_mt:set_sink(sink)
293
	self.transport.conn:hook("incoming-raw", sink);
294
	self.transport.conn:hook("disconnected", function (event)
295
		self.stream:debug("Closing sink...");
296
		local reason = event.reason;
297
		if reason == "closed" then reason = nil; end
298
		sink(nil, reason);
299
	end);
300
end