| 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 |