Mercurial > prosody-modules
annotate mod_http_roster_admin/mod_http_roster_admin.lua @ 5593:6d0574bfbf5d
mod_client_management: Include software version in table (when known)
Showing software versions could be useful for statistical reasons, e.g.
determining how quickly (or not) users upgrade, but most importantly for
revoking vulnerable clients versions in case of a security issue.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Thu, 13 Jul 2023 23:26:02 +0200 |
parents | 7d2400710d65 |
children |
rev | line source |
---|---|
2161 | 1 -- mod_http_roster_admin |
2 -- Description: Allow user rosters to be sourced from a remote HTTP API | |
3 -- | |
4 -- Version: 1.0 | |
5 -- Date: 2015-03-06 | |
6 -- Author: Matthew Wild <matthew@prosody.im> | |
7 -- License: MPLv2 | |
8 -- | |
9 -- Requirements: | |
10 -- Prosody config: | |
11 -- storage = { roster = "memory" } | |
12 -- modules_disabled = { "roster" } | |
13 -- Dependencies: | |
14 -- Prosody 0.9 | |
15 -- lua-cjson (Debian/Ubuntu/LuaRocks: lua-cjson) | |
16 | |
17 local http = require "net.http"; | |
18 local json = require "cjson"; | |
19 local it = require "util.iterators"; | |
20 local set = require "util.set"; | |
21 local rm = require "core.rostermanager"; | |
22 local st = require "util.stanza"; | |
23 local array = require "util.array"; | |
24 | |
3316
9d8098f4b652
mod_http_roster_admin: Explicitly set 'id' attribute on roster pushes
Matthew Wild <mwild1@gmail.com>
parents:
2631
diff
changeset
|
25 local new_id = require "util.id".short; |
9d8098f4b652
mod_http_roster_admin: Explicitly set 'id' attribute on roster pushes
Matthew Wild <mwild1@gmail.com>
parents:
2631
diff
changeset
|
26 |
2161 | 27 local host = module.host; |
28 local sessions = hosts[host].sessions; | |
29 | |
30 local roster_url = module:get_option_string("http_roster_url", "http://localhost/%s"); | |
31 | |
32 -- Send a roster push to the named user, with the given roster, for the specified | |
33 -- contact's roster entry. Used to notify clients of changes/removals. | |
34 local function roster_push(username, roster, contact_jid) | |
3316
9d8098f4b652
mod_http_roster_admin: Explicitly set 'id' attribute on roster pushes
Matthew Wild <mwild1@gmail.com>
parents:
2631
diff
changeset
|
35 local stanza = st.iq({type="set", id=new_id()}) |
2161 | 36 :tag("query", {xmlns = "jabber:iq:roster" }); |
37 local item = roster[contact_jid]; | |
38 if item then | |
39 stanza:tag("item", {jid = contact_jid, subscription = item.subscription, name = item.name, ask = item.ask}); | |
40 for group in pairs(item.groups) do | |
41 stanza:tag("group"):text(group):up(); | |
42 end | |
43 else | |
44 stanza:tag("item", {jid = contact_jid, subscription = "remove"}); | |
45 end | |
46 stanza:up():up(); -- move out from item | |
47 for _, session in pairs(hosts[host].sessions[username].sessions) do | |
48 if session.interested then | |
49 session.send(stanza); | |
50 end | |
51 end | |
52 end | |
53 | |
54 -- Send latest presence from the named local user to a contact. | |
55 local function send_presence(username, contact_jid, available) | |
56 module:log("debug", "Sending %savailable presence from %s to contact %s", (available and "" or "un"), username, contact_jid); | |
57 for resource, session in pairs(sessions[username].sessions) do | |
58 local pres; | |
59 if available then | |
60 pres = st.clone(session.presence); | |
61 pres.attr.to = contact_jid; | |
62 else | |
63 pres = st.presence({ to = contact_jid, from = session.full_jid, type = "unavailable" }); | |
64 end | |
65 module:send(pres); | |
66 end | |
67 end | |
68 | |
69 -- Converts a 'friend' object from the API to a Prosody roster item object | |
70 local function friend_to_roster_item(friend) | |
71 return { | |
72 name = friend.name; | |
73 subscription = "both"; | |
74 groups = friend.groups or {}; | |
75 }; | |
76 end | |
77 | |
78 -- Returns a handler function to consume the data returned from | |
79 -- the API, compare it to the user's current roster, and perform | |
80 -- any actions necessary (roster pushes, presence probes) to | |
81 -- synchronize them. | |
82 local function updated_friends_handler(username, cb) | |
83 return (function (ok, code, friends) | |
84 if not ok then | |
85 cb(false, code); | |
86 end | |
87 local user = sessions[username]; | |
88 local roster = user.roster; | |
89 local old_contacts = set.new(array.collect(it.keys(roster))); | |
90 local new_contacts = set.new(array.collect(it.keys(friends))); | |
91 | |
92 -- These two entries are not real contacts, ignore them | |
93 old_contacts:remove(false); | |
94 old_contacts:remove("pending"); | |
95 | |
96 module:log("debug", "New friends list of %s: %s", username, json.encode(friends)); | |
97 | |
98 -- Calculate which contacts have been added/removed since | |
99 -- the last time we fetched the roster | |
100 local added_contacts = new_contacts - old_contacts; | |
101 local removed_contacts = old_contacts - new_contacts; | |
102 | |
103 local added, removed = 0, 0; | |
104 | |
105 -- Add new contacts and notify connected clients | |
106 for contact_jid in added_contacts do | |
107 module:log("debug", "Processing new friend of %s: %s", username, contact_jid); | |
108 roster[contact_jid] = friend_to_roster_item(friends[contact_jid]); | |
109 roster_push(username, roster, contact_jid); | |
110 send_presence(username, contact_jid, true); | |
111 added = added + 1; | |
112 end | |
113 | |
114 -- Remove contacts and notify connected clients | |
115 for contact_jid in removed_contacts do | |
116 module:log("debug", "Processing removed friend of %s: %s", username, contact_jid); | |
117 roster[contact_jid] = nil; | |
118 roster_push(username, roster, contact_jid); | |
119 send_presence(username, contact_jid, false); | |
120 removed = removed + 1; | |
121 end | |
122 module:log("debug", "User %s: added %d new contacts, removed %d contacts", username, added, removed); | |
2631
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
123 if cb ~= nil then |
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
124 cb(true); |
2bfa7d476092
mod_http_roster_admin: Don't call callback if it's nil
JC Brand <jc@opkode.com>
parents:
2622
diff
changeset
|
125 end |
2161 | 126 end); |
127 end | |
128 | |
129 -- Fetch the named user's roster from the API, call callback (cb) | |
130 -- with status and result (friends list) when received. | |
131 function fetch_roster(username, cb) | |
2210
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
132 local x = {headers = {}}; |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
133 x["headers"]["ACCEPT"] = "application/json, text/plain, */*"; |
2441
68ebc52222dc
Log URL called by http_roster_admin
JC Brand <jc@opkode.com>
parents:
2210
diff
changeset
|
134 module:log("debug", "Fetching roster at URL: %s", roster_url:format(username)); |
2161 | 135 local ok, err = http.request( |
2210
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
136 roster_url:format(username), |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
137 x, |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
138 function (roster_data, code) |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
139 if code ~= 200 then |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
140 module:log("error", "Error fetching roster from %s (code %d): %s", roster_url:format(username), code, tostring(roster_data):sub(1, 40):match("^[^\r\n]+")); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
141 if code ~= 0 then |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
142 cb(nil, code, roster_data); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
143 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
144 return; |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
145 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
146 module:log("debug", "Successfully fetched roster for %s", username); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
147 module:log("debug", "The roster data is %s", roster_data); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
148 cb(true, code, json.decode(roster_data)); |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
149 end |
126d79bf079b
mod_http_roster_admin: Also log if the status error is 0 (Connection refused)
JC Brand <jcbrand@minddistrict.com>
parents:
2161
diff
changeset
|
150 ); |
2161 | 151 if not ok then |
152 module:log("error", "Failed to connect to roster API at %s: %s", roster_url:format(username), err); | |
153 cb(false, 0, err); | |
154 end | |
155 end | |
156 | |
157 -- Fetch the named user's roster from the API, synchronize it with | |
158 -- the user's current roster. Notify callback (cb) with true/false | |
159 -- depending on success or failure. | |
160 function refresh_roster(username, cb) | |
161 local user = sessions[username]; | |
162 if not (user and user.roster) then | |
163 module:log("debug", "User's (%q) roster updated, but they are not online - ignoring", username); | |
164 cb(true); | |
165 return; | |
166 end | |
167 fetch_roster(username, updated_friends_handler(username, cb)); | |
168 end | |
169 | |
170 --- Roster protocol handling --- | |
171 | |
172 -- Build a reply to a "roster get" request | |
173 local function build_roster_reply(stanza, roster_data) | |
174 local roster = st.reply(stanza) | |
175 :tag("query", { xmlns = "jabber:iq:roster" }); | |
176 | |
177 for jid, item in pairs(roster_data) do | |
178 if jid and jid ~= "pending" then | |
179 roster:tag("item", { | |
180 jid = jid, | |
181 subscription = item.subscription, | |
182 ask = item.ask, | |
183 name = item.name, | |
184 }); | |
185 for group in pairs(item.groups) do | |
186 roster:tag("group"):text(group):up(); | |
187 end | |
188 roster:up(); -- move out from item | |
189 end | |
190 end | |
191 return roster; | |
192 end | |
193 | |
194 -- Handle clients requesting their roster (generally at login) | |
195 -- This will not work if mod_roster is loaded (in 0.9). | |
196 module:hook("iq-get/self/jabber:iq:roster:query", function(event) | |
197 local session, stanza = event.origin, event.stanza; | |
198 | |
199 session.interested = true; -- resource is interested in roster updates | |
200 | |
201 local roster = session.roster; | |
202 if roster[false].downloaded then | |
203 return session.send(build_roster_reply(stanza, roster)); | |
204 end | |
205 | |
206 -- It's possible that we can call this more than once for a new roster | |
207 -- Should happen rarely (multiple clients of the same user request the | |
208 -- roster in the time it takes the API to respond). Currently we just | |
209 -- issue multiple requests, as it's harmless apart from the wasted | |
210 -- requests. | |
211 fetch_roster(session.username, function (ok, code, friends) | |
212 if not ok then | |
213 session.send(st.error_reply(stanza, "cancel", "internal-server-error")); | |
214 session:close("internal-server-error"); | |
215 return; | |
216 end | |
217 | |
218 -- Are we the first callback to handle the downloaded roster? | |
219 local first = roster[false].downloaded == nil; | |
220 if first then | |
221 -- Fill out new roster | |
222 for jid, friend in pairs(friends) do | |
223 roster[jid] = friend_to_roster_item(friend); | |
224 end | |
225 end | |
2617
7c3a1688e385
Purge the roster from RAM when the user logs off.
JC Brand <jc@opkode.com>
parents:
2441
diff
changeset
|
226 |
7c3a1688e385
Purge the roster from RAM when the user logs off.
JC Brand <jc@opkode.com>
parents:
2441
diff
changeset
|
227 roster[false].downloaded = true; |
2161 | 228 |
229 -- Send full roster to client | |
230 session.send(build_roster_reply(stanza, roster)); | |
231 | |
232 if not first then | |
233 -- We already had a roster, make sure to handle any changes... | |
234 updated_friends_handler(session.username, nil)(ok, code, friends); | |
235 end | |
236 end); | |
237 | |
238 return true; | |
239 end); | |
240 | |
241 -- Prevent client from making changes to the roster. This will not | |
242 -- work if mod_roster is loaded (in 0.9). | |
243 module:hook("iq-set/self/jabber:iq:roster:query", function(event) | |
244 local session, stanza = event.origin, event.stanza; | |
245 return session.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
246 end); | |
247 | |
248 --- HTTP endpoint to trigger roster refresh --- | |
249 | |
250 -- Handles updating for a single user: GET /roster_admin/refresh/USERNAME | |
251 function handle_refresh_single(event, username) | |
252 refresh_roster(username, function (ok, code, err) | |
253 event.response.headers["Content-Type"] = "application/json"; | |
254 event.response:send(json.encode({ | |
255 status = ok and "ok" or "error"; | |
256 message = err or "roster update complete"; | |
257 })); | |
258 end); | |
259 return true; | |
260 end | |
261 | |
262 -- Handles updating for multiple users: POST /roster_admin/refresh | |
263 -- Payload should be a JSON array of usernames, e.g. ["user1", "user2", "user3"] | |
264 function handle_refresh_multi(event) | |
265 local users = json.decode(event.request.body); | |
266 if not users then | |
267 module:log("warn", "Multi-user refresh attempted with missing/invalid payload"); | |
268 event.response:send(400); | |
269 return true; | |
270 end | |
271 | |
272 local count, count_err = 0, 0; | |
273 | |
274 local function cb(ok) | |
275 count = count + 1; | |
276 if not ok then | |
277 count_err = count_err + 1; | |
278 end | |
279 | |
280 if count == #users then | |
281 event.response.headers["Content-Type"] = "application/json"; | |
282 event.response:send(json.encode({ | |
283 status = "ok"; | |
284 message = "roster update complete"; | |
285 updated = count - count_err; | |
286 errors = count_err; | |
287 })); | |
288 end | |
289 end | |
290 | |
291 for _, username in ipairs(users) do | |
292 refresh_roster(username, cb); | |
293 end | |
294 | |
295 return true; | |
296 end | |
297 | |
3338
7d2400710d65
mod_http_roster_admin: Add explicit dependency on mod_http
Kim Alvefur <zash@zash.se>
parents:
3316
diff
changeset
|
298 module:depends("http"); |
2161 | 299 module:provides("http", { |
300 route = { | |
301 ["POST /refresh"] = handle_refresh_multi; | |
302 ["GET /refresh/*"] = handle_refresh_single; | |
303 }; | |
304 }); |