Mercurial > prosody-modules
view mod_http_roster_admin/mod_http_roster_admin.lua @ 5401:c8d04ac200fc
mod_http_oauth2: Reject loopback URIs as client_uri
This really should be a proper website with info, https://localhost is
not good enough. Ideally we'd validate that it's got proper DNS and is
actually reachable, but triggering HTTP or even DNS lookups seems like
it would carry abuse potential that would best to avoid.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Tue, 02 May 2023 16:20:55 +0200 |
parents | 7d2400710d65 |
children |
line wrap: on
line source
-- mod_http_roster_admin -- Description: Allow user rosters to be sourced from a remote HTTP API -- -- Version: 1.0 -- Date: 2015-03-06 -- Author: Matthew Wild <matthew@prosody.im> -- License: MPLv2 -- -- Requirements: -- Prosody config: -- storage = { roster = "memory" } -- modules_disabled = { "roster" } -- Dependencies: -- Prosody 0.9 -- lua-cjson (Debian/Ubuntu/LuaRocks: lua-cjson) local http = require "net.http"; local json = require "cjson"; local it = require "util.iterators"; local set = require "util.set"; local rm = require "core.rostermanager"; local st = require "util.stanza"; local array = require "util.array"; local new_id = require "util.id".short; local host = module.host; local sessions = hosts[host].sessions; local roster_url = module:get_option_string("http_roster_url", "http://localhost/%s"); -- Send a roster push to the named user, with the given roster, for the specified -- contact's roster entry. Used to notify clients of changes/removals. local function roster_push(username, roster, contact_jid) local stanza = st.iq({type="set", id=new_id()}) :tag("query", {xmlns = "jabber:iq:roster" }); local item = roster[contact_jid]; if item then stanza:tag("item", {jid = contact_jid, subscription = item.subscription, name = item.name, ask = item.ask}); for group in pairs(item.groups) do stanza:tag("group"):text(group):up(); end else stanza:tag("item", {jid = contact_jid, subscription = "remove"}); end stanza:up():up(); -- move out from item for _, session in pairs(hosts[host].sessions[username].sessions) do if session.interested then session.send(stanza); end end end -- Send latest presence from the named local user to a contact. local function send_presence(username, contact_jid, available) module:log("debug", "Sending %savailable presence from %s to contact %s", (available and "" or "un"), username, contact_jid); for resource, session in pairs(sessions[username].sessions) do local pres; if available then pres = st.clone(session.presence); pres.attr.to = contact_jid; else pres = st.presence({ to = contact_jid, from = session.full_jid, type = "unavailable" }); end module:send(pres); end end -- Converts a 'friend' object from the API to a Prosody roster item object local function friend_to_roster_item(friend) return { name = friend.name; subscription = "both"; groups = friend.groups or {}; }; end -- Returns a handler function to consume the data returned from -- the API, compare it to the user's current roster, and perform -- any actions necessary (roster pushes, presence probes) to -- synchronize them. local function updated_friends_handler(username, cb) return (function (ok, code, friends) if not ok then cb(false, code); end local user = sessions[username]; local roster = user.roster; local old_contacts = set.new(array.collect(it.keys(roster))); local new_contacts = set.new(array.collect(it.keys(friends))); -- These two entries are not real contacts, ignore them old_contacts:remove(false); old_contacts:remove("pending"); module:log("debug", "New friends list of %s: %s", username, json.encode(friends)); -- Calculate which contacts have been added/removed since -- the last time we fetched the roster local added_contacts = new_contacts - old_contacts; local removed_contacts = old_contacts - new_contacts; local added, removed = 0, 0; -- Add new contacts and notify connected clients for contact_jid in added_contacts do module:log("debug", "Processing new friend of %s: %s", username, contact_jid); roster[contact_jid] = friend_to_roster_item(friends[contact_jid]); roster_push(username, roster, contact_jid); send_presence(username, contact_jid, true); added = added + 1; end -- Remove contacts and notify connected clients for contact_jid in removed_contacts do module:log("debug", "Processing removed friend of %s: %s", username, contact_jid); roster[contact_jid] = nil; roster_push(username, roster, contact_jid); send_presence(username, contact_jid, false); removed = removed + 1; end module:log("debug", "User %s: added %d new contacts, removed %d contacts", username, added, removed); if cb ~= nil then cb(true); end end); end -- Fetch the named user's roster from the API, call callback (cb) -- with status and result (friends list) when received. function fetch_roster(username, cb) local x = {headers = {}}; x["headers"]["ACCEPT"] = "application/json, text/plain, */*"; module:log("debug", "Fetching roster at URL: %s", roster_url:format(username)); local ok, err = http.request( roster_url:format(username), x, function (roster_data, code) if code ~= 200 then 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]+")); if code ~= 0 then cb(nil, code, roster_data); end return; end module:log("debug", "Successfully fetched roster for %s", username); module:log("debug", "The roster data is %s", roster_data); cb(true, code, json.decode(roster_data)); end ); if not ok then module:log("error", "Failed to connect to roster API at %s: %s", roster_url:format(username), err); cb(false, 0, err); end end -- Fetch the named user's roster from the API, synchronize it with -- the user's current roster. Notify callback (cb) with true/false -- depending on success or failure. function refresh_roster(username, cb) local user = sessions[username]; if not (user and user.roster) then module:log("debug", "User's (%q) roster updated, but they are not online - ignoring", username); cb(true); return; end fetch_roster(username, updated_friends_handler(username, cb)); end --- Roster protocol handling --- -- Build a reply to a "roster get" request local function build_roster_reply(stanza, roster_data) local roster = st.reply(stanza) :tag("query", { xmlns = "jabber:iq:roster" }); for jid, item in pairs(roster_data) do if jid and jid ~= "pending" then roster:tag("item", { jid = jid, subscription = item.subscription, ask = item.ask, name = item.name, }); for group in pairs(item.groups) do roster:tag("group"):text(group):up(); end roster:up(); -- move out from item end end return roster; end -- Handle clients requesting their roster (generally at login) -- This will not work if mod_roster is loaded (in 0.9). module:hook("iq-get/self/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; session.interested = true; -- resource is interested in roster updates local roster = session.roster; if roster[false].downloaded then return session.send(build_roster_reply(stanza, roster)); end -- It's possible that we can call this more than once for a new roster -- Should happen rarely (multiple clients of the same user request the -- roster in the time it takes the API to respond). Currently we just -- issue multiple requests, as it's harmless apart from the wasted -- requests. fetch_roster(session.username, function (ok, code, friends) if not ok then session.send(st.error_reply(stanza, "cancel", "internal-server-error")); session:close("internal-server-error"); return; end -- Are we the first callback to handle the downloaded roster? local first = roster[false].downloaded == nil; if first then -- Fill out new roster for jid, friend in pairs(friends) do roster[jid] = friend_to_roster_item(friend); end end roster[false].downloaded = true; -- Send full roster to client session.send(build_roster_reply(stanza, roster)); if not first then -- We already had a roster, make sure to handle any changes... updated_friends_handler(session.username, nil)(ok, code, friends); end end); return true; end); -- Prevent client from making changes to the roster. This will not -- work if mod_roster is loaded (in 0.9). module:hook("iq-set/self/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; return session.send(st.error_reply(stanza, "cancel", "service-unavailable")); end); --- HTTP endpoint to trigger roster refresh --- -- Handles updating for a single user: GET /roster_admin/refresh/USERNAME function handle_refresh_single(event, username) refresh_roster(username, function (ok, code, err) event.response.headers["Content-Type"] = "application/json"; event.response:send(json.encode({ status = ok and "ok" or "error"; message = err or "roster update complete"; })); end); return true; end -- Handles updating for multiple users: POST /roster_admin/refresh -- Payload should be a JSON array of usernames, e.g. ["user1", "user2", "user3"] function handle_refresh_multi(event) local users = json.decode(event.request.body); if not users then module:log("warn", "Multi-user refresh attempted with missing/invalid payload"); event.response:send(400); return true; end local count, count_err = 0, 0; local function cb(ok) count = count + 1; if not ok then count_err = count_err + 1; end if count == #users then event.response.headers["Content-Type"] = "application/json"; event.response:send(json.encode({ status = "ok"; message = "roster update complete"; updated = count - count_err; errors = count_err; })); end end for _, username in ipairs(users) do refresh_roster(username, cb); end return true; end module:depends("http"); module:provides("http", { route = { ["POST /refresh"] = handle_refresh_multi; ["GET /refresh/*"] = handle_refresh_single; }; });