| 1 | local verse = require "verse"; |
| 2 | local stream = verse.stream_mt; |
| 3 | |
| 4 | local jid_split = require "prosody.util.jid".split; |
| 5 | local adns = require "prosody.net.adns"; |
| 6 | local st = require "prosody.util.stanza"; |
| 7 | local new_id = require "prosody.util.id".short; |
| 8 | |
| 9 | math.randomseed((require"socket".gettime() * 1000000) % 0x80000000); |
| 10 | |
| 11 | -- Shortcuts to save having to load util.stanza |
| 12 | verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = |
| 13 | st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; |
| 14 | |
| 15 | function verse.iq(attr) |
| 16 | if not attr.id then |
| 17 | attr.id = new_id(); |
| 18 | end |
| 19 | return st.iq(attr); |
| 20 | end |
| 21 | |
| 22 | local new_xmpp_stream = require "prosody.util.xmppstream".new; |
| 23 | |
| 24 | local xmlns_stream = "http://etherx.jabber.org/streams"; |
| 25 | |
| 26 | local function compare_srv_priorities(a, b) |
| 27 | if a.priority == b.priority then |
| 28 | if not a.weight_r then |
| 29 | a.weight_r = math.random(); |
| 30 | end |
| 31 | if not b.weight_r then |
| 32 | b.weight_r = math.random(); |
| 33 | end |
| 34 | return (1 + a.weight) * a.weight_r > (1 + b.weight) * b.weight_r; |
| 35 | end |
| 36 | return a.priority < b.priority; |
| 37 | end |
| 38 | |
| 39 | local stream_callbacks = { |
| 40 | stream_ns = xmlns_stream, |
| 41 | stream_tag = "stream", |
| 42 | default_ns = "jabber:client" }; |
| 43 | |
| 44 | function stream_callbacks.streamopened(stream, attr) |
| 45 | stream.stream_id = attr.id; |
| 46 | if not stream:event("opened", attr) then |
| 47 | stream.notopen = nil; |
| 48 | end |
| 49 | return true; |
| 50 | end |
| 51 | |
| 52 | function stream_callbacks.streamclosed(stream) |
| 53 | stream.notopen = true; |
| 54 | if not stream.closed then |
| 55 | stream:send("</stream:stream>"); |
| 56 | stream.closed = true; |
| 57 | end |
| 58 | stream:event("closed"); |
| 59 | return stream:close("stream closed") |
| 60 | end |
| 61 | |
| 62 | function stream_callbacks.handlestanza(stream, stanza) |
| 63 | if stanza.attr.xmlns == xmlns_stream then |
| 64 | return stream:event("stream-"..stanza.name, stanza); |
| 65 | elseif stanza.attr.xmlns then |
| 66 | return stream:event("stream/"..stanza.attr.xmlns, stanza); |
| 67 | end |
| 68 | |
| 69 | return stream:event("stanza", stanza); |
| 70 | end |
| 71 | |
| 72 | function stream_callbacks.error(stream, e, stanza) |
| 73 | if stream:event(e, stanza) == nil then |
| 74 | if stanza then |
| 75 | local err = stanza:get_child(nil, "urn:ietf:params:xml:ns:xmpp-streams"); |
| 76 | local text = stanza:get_child_text("text", "urn:ietf:params:xml:ns:xmpp-streams"); |
| 77 | error(err.name..(text and ": "..text or "")); |
| 78 | else |
| 79 | error(stanza and stanza.name or e or "unknown-error"); |
| 80 | end |
| 81 | end |
| 82 | end |
| 83 | |
| 84 | function stream:reset() |
| 85 | if self.stream then |
| 86 | self.stream:reset(); |
| 87 | else |
| 88 | self.stream = new_xmpp_stream(self, stream_callbacks); |
| 89 | end |
| 90 | self.notopen = true; |
| 91 | return true; |
| 92 | end |
| 93 | |
| 94 | function stream:connect_client(jid, pass, client_key, server_key) |
| 95 | self.jid, self.password = jid, pass; |
| 96 | self.client_key, self.server_key = client_key, server_key; |
| 97 | self.username, self.host, self.resource = jid_split(jid); |
| 98 | |
| 99 | -- Required XMPP features |
| 100 | self:add_plugin("tls"); |
| 101 | self:add_plugin("sasl"); |
| 102 | self:add_plugin("bind"); |
| 103 | |
| 104 | function self.data(conn, data) |
| 105 | local ok, err = self.stream:feed(data); |
| 106 | if ok then return; end |
| 107 | self:debug("Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); |
| 108 | self:close("xml-not-well-formed"); |
| 109 | end |
| 110 | |
| 111 | self:hook("connected", function () self:reopen(); end); |
| 112 | self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); |
| 113 | self:hook("read-timeout", function () self:send(" "); return true; end, -1); |
| 114 | |
| 115 | self.curr_id = 0; |
| 116 | |
| 117 | self.tracked_iqs = {}; |
| 118 | self:hook("stanza", function (stanza) |
| 119 | local id, type = stanza.attr.id, stanza.attr.type; |
| 120 | if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then |
| 121 | self.tracked_iqs[id](stanza); |
| 122 | self.tracked_iqs[id] = nil; |
| 123 | return true; |
| 124 | end |
| 125 | end); |
| 126 | |
| 127 | self:hook("stanza", function (stanza) |
| 128 | local ret; |
| 129 | if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then |
| 130 | if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then |
| 131 | local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; |
| 132 | if xmlns then |
| 133 | ret = self:event("iq/"..xmlns, stanza); |
| 134 | if not ret then |
| 135 | ret = self:event("iq", stanza); |
| 136 | end |
| 137 | end |
| 138 | if ret == nil then |
| 139 | self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); |
| 140 | return true; |
| 141 | end |
| 142 | else |
| 143 | ret = self:event(stanza.name, stanza); |
| 144 | end |
| 145 | end |
| 146 | return ret; |
| 147 | end, -1); |
| 148 | |
| 149 | self:hook("outgoing", function (data) |
| 150 | if data.name then |
| 151 | self:event("stanza-out", data); |
| 152 | end |
| 153 | end); |
| 154 | |
| 155 | self:hook("stanza-out", function (stanza) |
| 156 | if not stanza.attr.xmlns then |
| 157 | self:event(stanza.name.."-out", stanza); |
| 158 | end |
| 159 | end); |
| 160 | |
| 161 | local function stream_ready() |
| 162 | self:event("ready"); |
| 163 | end |
| 164 | self:hook("session-success", stream_ready, -1) |
| 165 | self:hook("bind-success", stream_ready, -1); |
| 166 | |
| 167 | local _base_close = self.close; |
| 168 | function self:close(reason) |
| 169 | self.close = _base_close; |
| 170 | if not self.closed then |
| 171 | self:send("</stream:stream>"); |
| 172 | self.closed = true; |
| 173 | else |
| 174 | return self:close(reason); |
| 175 | end |
| 176 | end |
| 177 | |
| 178 | local function start_connect() |
| 179 | -- Initialise connection |
| 180 | self:connect(self.connect_host or self.host, self.connect_port or 5222); |
| 181 | end |
| 182 | |
| 183 | if not (self.connect_host or self.connect_port) then |
| 184 | -- Look up SRV records |
| 185 | adns.lookup(function (answer) |
| 186 | if answer then |
| 187 | local srv_hosts = {}; |
| 188 | self.srv_hosts = srv_hosts; |
| 189 | for _, record in ipairs(answer) do |
| 190 | table.insert(srv_hosts, record.srv); |
| 191 | end |
| 192 | table.sort(srv_hosts, compare_srv_priorities); |
| 193 | |
| 194 | local srv_choice = srv_hosts[1]; |
| 195 | self.srv_choice = 1; |
| 196 | if srv_choice then |
| 197 | self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; |
| 198 | self:debug("Best record found, will connect to %s:%d", self.connect_host or self.host, self.connect_port or 5222); |
| 199 | end |
| 200 | |
| 201 | self:hook("disconnected", function () |
| 202 | if self.srv_hosts and self.srv_choice < #self.srv_hosts then |
| 203 | self.srv_choice = self.srv_choice + 1; |
| 204 | local srv_choice = srv_hosts[self.srv_choice]; |
| 205 | self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; |
| 206 | start_connect(); |
| 207 | return true; |
| 208 | end |
| 209 | end, 1000); |
| 210 | |
| 211 | self:hook("connected", function () |
| 212 | self.srv_hosts = nil; |
| 213 | end, 1000); |
| 214 | end |
| 215 | start_connect(); |
| 216 | end, "_xmpp-client._tcp."..(self.host)..".", "SRV"); |
| 217 | else |
| 218 | start_connect(); |
| 219 | end |
| 220 | end |
| 221 | |
| 222 | function stream:reopen() |
| 223 | self:reset(); |
| 224 | self:send(st.stanza("stream:stream", { to = self.host, ["xmlns:stream"]='http://etherx.jabber.org/streams', |
| 225 | xmlns = "jabber:client", version = "1.0", ["xml:lang"] = self.lang }):top_tag()); |
| 226 | end |
| 227 | |
| 228 | function stream:send_iq(iq, callback) |
| 229 | local id = iq.attr.id or uuid.generate(); |
| 230 | self.tracked_iqs[id] = callback; |
| 231 | iq.attr.id = id; |
| 232 | self:send(iq); |
| 233 | end |