client.lua

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