plugins/jingle.lua

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

mercurial