Software /
code /
prosody-modules
Changeset
5593:04f36a470dca
Update from upstream
author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
---|---|
date | Sun, 09 Jul 2023 01:31:29 +0700 |
parents | 5591:680fb3344357 (current diff) 5592:59acf7f540c1 (diff) |
children | 5594:14480ca9576e |
files | |
diffstat | 48 files changed, 1446 insertions(+), 690 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/lnav/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,6 @@ +% Prosody log format for lnav + +This is a format definition that allows <https://lnav.org/> to better +handle Prosody logs. + +Install it using `lnav -i ./prosody.json`
--- a/misc/lnav/prosody.json Fri May 26 02:15:45 2023 +0700 +++ b/misc/lnav/prosody.json Sun Jul 09 01:31:29 2023 +0700 @@ -14,7 +14,7 @@ "ordered-by-time" : true, "regex" : { "standard" : { - "pattern" : "^(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2})\\s+(?<loggername>\\S+)\\s+(?<loglevel>debug|info|warn|error)\\s+(?<message>.+)$" + "pattern" : "^(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}\\s+)(?<loggername>\\S+)\\s+(?<loglevel>debug|info|warn|error)\\s+(?<message>.+)$" } }, "sample" : [ @@ -23,7 +23,9 @@ } ], "timestamp-field" : "timestamp", - "timestamp-format" : "%b %d %H:%M:%S ", + "timestamp-format" : [ + "%b %d %H:%M:%S " + ], "title" : "Prosody log", "url" : "https://prosody.im/doc/logging", "value" : {
--- a/mod_auth_oauth_external/README.md Fri May 26 02:15:45 2023 +0700 +++ b/mod_auth_oauth_external/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -79,7 +79,7 @@ owner password grant. `oauth_external_scope` -: String. Defaults to `"oauth"`. Included in request for resource +: String. Defaults to `"openid"`. Included in request for resource owner password grant. # Compatibility
--- a/mod_client_management/mod_client_management.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_client_management/mod_client_management.lua Sun Jul 09 01:31:29 2023 +0700 @@ -10,8 +10,8 @@ local strict = module:get_option_boolean("enforce_client_ids", false); -module:default_permission("prosody:user", ":list-clients"); -module:default_permission("prosody:user", ":manage-clients"); +module:default_permission("prosody:registered", ":list-clients"); +module:default_permission("prosody:registered", ":manage-clients"); local tokenauth = module:depends("tokenauth"); local mod_fast = module:depends("sasl2_fast"); @@ -35,6 +35,8 @@ if not (sasl_agent or token_agent) then return; end return { software = sasl_agent and sasl_agent.software or token_agent and token_agent.name or nil; + software_id = token_agent and token_agent.id or nil; + software_version = token_agent and token_agent.version or nil; uri = token_agent and token_agent.uri or nil; device = sasl_agent and sasl_agent.device or nil; }; @@ -348,7 +350,7 @@ local user_agent = st.stanza("user-agent"); if client.user_agent then if client.user_agent.software then - user_agent:text_tag("software", client.user_agent.software); + user_agent:text_tag("software", client.user_agent.software, { id = client.user_agent.software_id; version = client.user_agent.software_version }); end if client.user_agent.device then user_agent:text_tag("device", client.user_agent.device);
--- a/mod_cloud_notify_extensions/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_cloud_notify_extensions/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -38,13 +38,10 @@ There is no configuration for this module, just add it to modules\_enabled as normal. -Compatibility -============= +# Compatibility - ----- ------- - 0.12 Works - ----- ------- - 0.11 Should work - ----- ------- - trunk Works - ----- ------- + ------- ------------- + 0.12 Works + 0.11 Should work + trunk Works + ------- -------------
--- a/mod_compat_roles/mod_compat_roles.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_compat_roles/mod_compat_roles.lua Sun Jul 09 01:31:29 2023 +0700 @@ -33,8 +33,12 @@ local role_inheritance = { ["prosody:operator"] = "prosody:admin"; - ["prosody:admin"] = "prosody:user"; - ["prosody:user"] = "prosody:restricted"; + ["prosody:admin"] = "prosody:member"; + ["prosody:member"] = "prosody:registered"; + ["prosody:registered"] = "prosody:guest"; + + -- COMPAT + ["prosody:user"] = "prosody:registered"; }; local function role_may(host, role_name, permission)
--- a/mod_firewall/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -10,6 +10,8 @@ mod_firewall.definitions: definitions.lib.lua mod_firewall.marks: marks.lib.lua mod_firewall.test: test.lib.lua + copy_directories: + - scripts --- ------------------------------------------------------------------------ @@ -253,12 +255,13 @@ ### Sender/recipient matching - Condition Matches - ------------- ------------------------------------------------------- - `FROM` The JID in the 'from' attribute matches the given JID. - `TO` The JID in the 'to' attribute matches the given JID. - `TO SELF` The stanza is sent by any of a user's resources to their own bare JID. - `TO FULL JID` The stanza is addressed to a valid full JID on the local server (full JIDs include a resource at the end, and only exist for the lifetime of a single session, therefore the recipient must be online, or this check will not match). + Condition Matches + --------------- ------------------------------------------------------- + `FROM` The JID in the 'from' attribute matches the given JID. + `TO` The JID in the 'to' attribute matches the given JID. + `TO SELF` The stanza is sent by any of a user's resources to their own bare JID. + `TO FULL JID` The stanza is addressed to a **valid** full JID on the local server (full JIDs include a resource at the end, and only exist for the lifetime of a single session, therefore the recipient **must be online**, or this check will not match). + `FROM FULL JID` The stanza is from a full JID (unlike `TO FULL JID` this check is on the format of the JID only). The TO and FROM conditions both accept wildcards in the JID when it is enclosed in angle brackets ('\<...\>'). For example:
--- a/mod_firewall/actions.lib.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/actions.lib.lua Sun Jul 09 01:31:29 2023 +0700 @@ -220,11 +220,29 @@ end function action_handlers.MARK_USER(name) - return [[if session.firewall_marks then session.firewall_marks.]]..idsafe(name)..[[ = current_timestamp; end]], { "timestamp" }; + return ([[if session.username and session.host == current_host then + fire_event("firewall/marked/user", { + username = session.username; + mark = %q; + timestamp = current_timestamp; + }); + else + log("warn", "Attempt to MARK a remote user - only local users may be marked"); + end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)), { + "current_host"; + "timestamp"; + }; end function action_handlers.UNMARK_USER(name) - return [[if session.firewall_marks then session.firewall_marks.]]..idsafe(name)..[[ = nil; end]], { "timestamp" }; + return ([[if session.username and session.host == current_host then + fire_event("firewall/unmarked/user", { + username = session.username; + mark = %q; + }); + else + log("warn", "Attempt to UNMARK a remote user - only local users may be marked"); + end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)); end function action_handlers.ADD_TO(spec)
--- a/mod_firewall/conditions.lib.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/conditions.lib.lua Sun Jul 09 01:31:29 2023 +0700 @@ -67,6 +67,10 @@ return compile_jid_match("from", from), { "split_from" }; end +function condition_handlers.FROM_FULL_JID() + return "not "..compile_jid_match_part("from_resource", nil), { "split_from" }; +end + function condition_handlers.FROM_EXACTLY(from) local metadeps = {}; return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) }; @@ -310,7 +314,9 @@ error("Error parsing mark name, see documentation for usage examples"); end if time then - return ("(current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" }; + return ([[( + current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0) + ) < %d]]):format(idsafe(name), tonumber(time)), { "timestamp" }; end return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")"); end @@ -341,7 +347,13 @@ if not (search_name) then error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST"); end - return ("scan_list(list_%s, %s)"):format(list_name, "tokens_"..search_name.."_"..pattern_name), { "scan_list", "tokens:"..search_name.."-"..pattern_name, "list:"..list_name }; + return ("scan_list(list_%s, %s)"):format( + list_name, + "tokens_"..search_name.."_"..pattern_name + ), { + "scan_list", + "tokens:"..search_name.."-"..pattern_name, "list:"..list_name + }; end -- COUNT: lines in body < 10 @@ -361,7 +373,12 @@ end local comp_op = comparator_expression:gsub("%s+", ""); assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op); - return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(search_name, pattern_name, comp_op, value), { "it_count", "search:"..search_name, "pattern:"..pattern_name }; + return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format( + search_name, pattern_name, comp_op, value + ), { + "it_count", + "search:"..search_name, "pattern:"..pattern_name + }; end return condition_handlers;
--- a/mod_firewall/marks.lib.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/marks.lib.lua Sun Jul 09 01:31:29 2023 +0700 @@ -1,23 +1,35 @@ local mark_storage = module:open_store("firewall_marks"); +local mark_map_storage = module:open_store("firewall_marks", "map"); local user_sessions = prosody.hosts[module.host].sessions; -module:hook("resource-bind", function (event) - local session = event.session; - local username = session.username; - local user = user_sessions[username]; - local marks = user.firewall_marks; - if not marks then - marks = mark_storage:get(username) or {}; - user.firewall_marks = marks; -- luacheck: ignore 122 +module:hook("firewall/marked/user", function (event) + local user = user_sessions[event.username]; + local marks = user and user.firewall_marks; + if user and not marks then + -- Load marks from storage to cache on the user object + marks = mark_storage:get(event.username) or {}; + user.firewall_marks = marks; --luacheck: ignore 122 + end + if marks then + marks[event.mark] = event.timestamp; + end + local ok, err = mark_map_storage:set(event.username, event.mark, event.timestamp); + if not ok then + module:log("error", "Failed to mark user %q with %q: %s", event.username, event.mark, err); end - session.firewall_marks = marks; -end); + return true; +end, -1); -module:hook("resource-unbind", function (event) - local session = event.session; - local username = session.username; - local marks = session.firewall_marks; - mark_storage:set(username, marks); -end); - +module:hook("firewall/unmarked/user", function (event) + local user = user_sessions[event.username]; + local marks = user and user.firewall_marks; + if marks then + marks[event.mark] = nil; + end + local ok, err = mark_map_storage:set(event.username, event.mark, nil); + if not ok then + module:log("error", "Failed to unmark user %q with %q: %s", event.username, event.mark, err); + end + return true; +end, -1);
--- a/mod_firewall/mod_firewall.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/mod_firewall.lua Sun Jul 09 01:31:29 2023 +0700 @@ -316,7 +316,7 @@ local condition_handlers = module:require("conditions"); local action_handlers = module:require("actions"); -if module:get_option_boolean("firewall_experimental_user_marks", false) then +if module:get_option_boolean("firewall_experimental_user_marks", true) then module:require"marks"; end @@ -742,3 +742,43 @@ print("end -- End of file "..filename); end end + + +-- Console + +local console_env = module:shared("/*/admin_shell/env"); + +console_env.firewall = {}; + +function console_env.firewall:mark(user_jid, mark_name) + local username, host = jid.split(user_jid); + if not username or not hosts[host] then + return nil, "Invalid JID supplied"; + elseif not idsafe(mark_name) then + return nil, "Invalid characters in mark name"; + end + if not module:context(host):fire_event("firewall/marked/user", { + username = session.username; + mark = mark_name; + timestamp = os.time(); + }) then + return nil, "Mark not set - is mod_firewall loaded on that host?"; + end + return true, "User marked"; +end + +function console_env.firewall:unmark(jid, mark_name) + local username, host = jid.split(user_jid); + if not username or not hosts[host] then + return nil, "Invalid JID supplied"; + elseif not idsafe(mark_name) then + return nil, "Invalid characters in mark name"; + end + if not module:context(host):fire_event("firewall/unmarked/user", { + username = session.username; + mark = mark_name; + }) then + return nil, "Mark not removed - is mod_firewall loaded on that host?"; + end + return true, "User unmarked"; +end
--- a/mod_firewall/scripts/spam-blocking.pfw Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/scripts/spam-blocking.pfw Sun Jul 09 01:31:29 2023 +0700 @@ -97,6 +97,12 @@ TYPE: groupchat PASS. +# Mediated MUC invitations are naturally from 'strangers' and have special +# handling. We lean towards accepting them, unless overridden by custom rules. +NOT FROM FULL JID? +INSPECT: {http://jabber.org/protocol/muc#user}x/invite +JUMP CHAIN=user/spam_check_muc_invite + # Non-chat message types often generate pop-ups in clients, # so we won't accept them from strangers NOT TYPE: chat @@ -138,6 +144,18 @@ ################################################################## +#### Rules for MUC invitations ################################### + +::user/spam_check_muc_invite + +# This chain can be used to inspect the invitation and determine +# the appropriate action. Otherwise, we proceed with the default +# action below. +JUMP CHAIN=user/spam_check_muc_invite_custom + +# Allow mediated MUC invitations by default +PASS. + #### Stanzas reaching this chain will be rejected ################ ::user/spam_reject @@ -151,7 +169,7 @@ ################################################################## -#### Stanzas that may be spam, but we're not sure either way###### +#### Stanzas that may be spam, but we're not sure either way ##### ::user/spam_handle_unknown # This chain can be used by other scripts
--- a/mod_firewall/scripts/spam-blocklists.pfw Fri May 26 02:15:45 2023 +0700 +++ b/mod_firewall/scripts/spam-blocklists.pfw Sun Jul 09 01:31:29 2023 +0700 @@ -8,3 +8,13 @@ CHECK LIST: blocklist contains $<@from|host> BOUNCE=policy-violation (Your server is blocked due to spam) + +::user/spam_check_muc_invite_custom + +# Check the server we received the invitation from +CHECK LIST: blocklist contains $<@from|host> +BOUNCE=policy-violation (Your server is blocked due to spam) + +# Check the inviter's JID against the blocklist, too +CHECK LIST: blocklist contains $<{http://jabber.org/protocol/muc#user}x/invite@from|host> +BOUNCE=policy-violation (Your server is blocked due to spam)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_groups_oidc/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,12 @@ +--- +summary: OIDC group membership in UserInfo +labels: +- Stage-Alpha +rockspec: + dependencies: + - mod_http_oauth2 >= 200 + - mod_groups_internal +--- + +This module exposes [mod_groups_internal] groups to +[OAuth 2.0][mod_http_oauth2] clients via a `groups` scope/claim.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_groups_oidc/mod_groups_oidc.lua Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,15 @@ +local array = require "util.array"; + +module:add_item("openid-claim", "groups"); + +local group_memberships = module:open_store("groups", "map"); +local function user_groups(username) + return pairs(group_memberships:get_all(username) or {}); +end + +module:hook("token/userinfo", function(event) + local userinfo = event.userinfo; + if event.claims:contains("groups") then + userinfo.groups = array(user_groups(event.username)); + end +end);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_debug/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,40 @@ +--- +summary: HTTP module returning info about requests for debugging +--- + +This module returns some info about HTTP requests as Prosody sees them +from an endpoint like `http://xmpp.example.net:5281/debug`. This can be +used to validate [reverse-proxy configuration][doc:http] and similar use +cases. + +# Example + +``` +$ curl -sSf https://xmpp.example.net:5281/debug | json_pp +{ + "body" : "", + "headers" : { + "accept" : "*/*", + "host" : "xmpp.example.net:5281", + "user_agent" : "curl/7.74.0" + }, + "httpversion" : "1.1", + "id" : "jmFROQKoduU3", + "ip" : "127.0.0.1", + "method" : "GET", + "path" : "/debug", + "secure" : true, + "url" : { + "path" : "/debug" + } +} +``` + +# Configuration + +HTTP Methods handled can be configured via the `http_debug_methods` +setting. By default, the most common methods are already enabled. + +```lua +http_debug_methods = { "GET"; "HEAD"; "DELETE"; "OPTIONS"; "PATCH"; "POST"; "PUT" }; +```
--- a/mod_http_debug/mod_http_debug.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_debug/mod_http_debug.lua Sun Jul 09 01:31:29 2023 +0700 @@ -1,26 +1,34 @@ local json = require "util.json" module:depends("http") +local function handle_request(event) + local request = event.request; + (request.log or module._log)("debug", "%s -- %s %q HTTP/%s -- %q -- %s", request.ip, request.method, request.url, request.httpversion, request.headers, request.body); + return { + status_code = 200; + headers = { content_type = "application/json" }; + host = module.host; + body = json.encode { + body = request.body; + headers = request.headers; + httpversion = request.httpversion; + id = request.id; + ip = request.ip; + method = request.method; + path = request.path; + secure = request.secure; + url = request.url; + }; + } +end + +local methods = module:get_option_set("http_debug_methods", { "GET"; "HEAD"; "DELETE"; "OPTIONS"; "PATCH"; "POST"; "PUT" }); +local route = {}; +for method in methods do + route[method] = handle_request; + route[method .. " /*"] = handle_request; +end + module:provides("http", { - route = { - GET = function(event) - local request = event.request; - return { - status_code = 200; - headers = { - content_type = "application/json", - }, - body = json.encode { - body = request.body; - headers = request.headers; - httpversion = request.httpversion; - ip = request.ip; - method = request.method; - path = request.path; - secure = request.secure; - url = request.url; - } - } - end; - } - }) + route = route; +})
--- a/mod_http_dir_listing/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_dir_listing/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -2,9 +2,9 @@ rockspec: build: copy_directories: - - mod_http_dir_listing/http_dir_listing/resources + - http_dir_listing/resources modules: - mod_http_dir_listing: mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua + mod_http_dir_listing: http_dir_listing/mod_http_dir_listing.lua summary: HTTP directory listing ...
--- a/mod_http_dir_listing2/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_dir_listing2/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -1,6 +1,10 @@ --- summary: HTTP directory listing -... +rockspec: + build: + copy_directories: + - resources +--- Introduction ============
--- a/mod_http_muc_log/mod_http_muc_log.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_muc_log/mod_http_muc_log.lua Sun Jul 09 01:31:29 2023 +0700 @@ -128,17 +128,42 @@ local presence_logged = module:get_option_boolean("muc_log_presences", false); -local function hide_presence(request) +local function show_presence(request) --> boolean|nil + -- boolean -> yes or no + -- nil -> dunno if not presence_logged then - return false; + -- No presence stored, skip + return nil; end if request.url.query then local data = httplib.formdecode(request.url.query); - if data then - return data.p == "h" + if type(data) == "table" then + if data.p == "s" or data.p == "h" then + return data.p == "s"; + end end end - return false; +end + +local function presence_with(request) + local show = show_presence(request); + if show == true then + return nil; -- no filter, everything + elseif show == false or show == nil then + -- only messages + return "message<groupchat"; + end +end + +local function presence_query(request) -- > ?p=[sh] + local show = show_presence(request); + if show == true then + return { p = "s" } + elseif show == false then + return { p = "h" } + else + return nil; + end end local function get_dates(room) --> { integer, ... } @@ -254,7 +279,8 @@ room = room_obj._data; jid = room_obj.jid; jid_node = jid_split(room_obj.jid); - hide_presence = hide_presence(request); + q = presence_query(request); + show_presence = show_presence(request); presence_available = presence_logged; dates = date_list; links = { @@ -300,7 +326,7 @@ local iter, err = archive:find(room, { ["start"] = day_start; ["end"] = day_start + 86399; - ["with"] = hide_presence(request) and "message<groupchat" or nil; + ["with"] = presence_with(request); }); if not iter then module:log("warn", "Could not search archive: %s", err or "no error"); @@ -475,7 +501,8 @@ room = room_obj._data; jid = room_obj.jid; jid_node = jid_split(room_obj.jid); - hide_presence = hide_presence(request); + q = presence_query(request); + show_presence = show_presence(request); presence_available = presence_logged; lang = room_obj.get_language and room_obj:get_language(); lines = logs; @@ -524,7 +551,8 @@ static = "./@static"; title = module:get_option_string("name", "Prosody Chatrooms"); jid = module.host; - hide_presence = hide_presence(request); + q = presence_query(request); + show_presence = show_presence(request); presence_available = presence_logged; rooms = room_list; dates = {}; -- COMPAT util.interpolation {nil|func#...} bug
--- a/mod_http_muc_log/res/http_muc_log.html Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_muc_log/res/http_muc_log.html Sun Jul 09 01:31:29 2023 +0700 @@ -19,7 +19,7 @@ <li class="button"><a href="{room.webchat_url}">Join via web</a></li> } {links# -<li><a class="{item.rel?}" href="{item.href}{hide_presence&?p=h}" rel="{item.rel?}">{item.text}</a></li>} +<li><a class="{item.rel?}" href="{item.href}{q&?{q%{idx}={item}}}" rel="{item.rel?}">{item.text}</a></li>} </ul> </nav> </header> @@ -28,7 +28,7 @@ <nav> <dl class="room-list"> {rooms# -<dt {item.lang&lang="{item.lang}"} class="name"><a href="{item.href}{hide_presence&?p=h}">{item.name}</a></dt> +<dt {item.lang&lang="{item.lang}"} class="name"><a href="{item.href}{q&?{q%{idx}={item}}}">{item.name}</a></dt> <dd {item.lang&lang="{item.lang}"} class="description">{item.description?}</dd>} </dl> {dates|calendarize# @@ -38,7 +38,7 @@ <caption>{item.month}</caption> <thead><tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr></thead> <tbody>{item.weeks# -<tr>{item.days#<td>{item.href&<a href="{item.href}{hide_presence&?p=h}">}<span>{item.day? }</span>{item.href&</a>}</td>}</tr>} +<tr>{item.days#<td>{item.href&<a href="{item.href}{q&?{q%{idx}={item}}}">}<span>{item.day? }</span>{item.href&</a>}</td>}</tr>} </tbody> </table> } @@ -48,8 +48,8 @@ <div> {presence_available&<form> <label> -<input name="p" value="h" type="checkbox"{hide_presence& checked}> -<span>Hide joins and parts</span> + <input name="p" value="s" type="checkbox"{show_presence& checked}> +<span>show joins and parts</span> </label> <noscript> <button type="submit">Apply</button> @@ -72,7 +72,7 @@ <footer> <nav> <ul>{links# -<li><a class="{item.rel?}" href="{item.href}{hide_presence&?p=h}" rel="{item.rel?}">{item.text}</a></li>} +<li><a class="{item.rel?}" href="{item.href}{q&?{q%{idx}={item}}}" rel="{item.rel?}">{item.text}</a></li>} </ul> </nav> <br>
--- a/mod_http_oauth2/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -1,12 +1,12 @@ --- labels: - Stage-Alpha -summary: 'OAuth2 API' rockspec: build: copy_directories: - html -... +summary: OAuth 2.0 Authorization Server API +--- ## Introduction @@ -18,9 +18,10 @@ third-party applications limited access to your account, without sharing your password with them. -With this module deployed, software that supports OAuth can obtain "access -tokens" from Prosody which can then be used to connect to XMPP accounts using -the 'OAUTHBEARER' SASL mechanism or via non-XMPP interfaces such as [mod_rest]. +With this module deployed, software that supports OAuth can obtain +"access tokens" from Prosody which can then be used to connect to XMPP +accounts using the [OAUTHBEARER SASL mechanism][rfc7628] or via non-XMPP +interfaces such as [mod_rest]. Although this module has been around for some time, it has recently been significantly extended and largely rewritten to support OAuth/OIDC more fully. @@ -36,9 +37,10 @@ - [example shell script for mod_rest](https://hg.prosody.im/prosody-modules/file/tip/mod_rest/example/rest.sh) - *(we need you!)* -Support for OAUTHBEARER has been added to the Lua XMPP library, [verse](https://code.matthewwild.co.uk/verse). -If you know of additional implementations, or are motivated to work on one, -please let us know! We'd be happy to help (e.g. by providing a test server). +Support for [OAUTHBEARER][rfc7628] has been added to the Lua XMPP +library, [verse](https://code.matthewwild.co.uk/verse). If you know of +additional implementations, or are motivated to work on one, please let +us know! We'd be happy to help (e.g. by providing a test server). ## Standards support @@ -49,6 +51,7 @@ - [RFC 7591: OAuth 2.0 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591.html) - [RFC 7628: A Set of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth](https://www.rfc-editor.org/rfc/rfc7628) - [RFC 7636: Proof Key for Code Exchange by OAuth Public Clients](https://www.rfc-editor.org/rfc/rfc7636) +- [RFC 9207: OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9207.html) - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) - [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html) (_partial, e.g. missing JWKS_) - [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) @@ -61,7 +64,7 @@ a client requests access. Built-in pages are provided, but you may also theme or entirely override them. -This module honours the 'site_name' configuration option that is also used by +This module honours the `site_name` configuration option that is also used by a number of other modules: ```lua @@ -75,7 +78,7 @@ ``` Some templates support additional variables, that can be provided by the -'oauth2_template_style' option: +`oauth2_template_style` option: ```lua oauth2_template_style = { @@ -83,6 +86,13 @@ } ``` +If you know what features your templates use use you can adjust the +`Content-Security-Policy` header to only allow what is needed: + +```lua +oauth2_security_policy = "default-src 'self'" -- this is the default +``` + ### Token parameters The following options configure the lifetime of tokens issued by the module. @@ -107,12 +117,109 @@ oauth2_registration_ttl = nil -- unlimited by default ``` +Registering a client is described in +[RFC7591](https://www.rfc-editor.org/rfc/rfc7591.html). + +In addition to the requirements in the RFC, the following requirements +are enforced: + +`client_name` +: **MUST** be present, is shown to users in consent screen. + +`client_uri` +: **MUST** be present and **MUST** be a `https://` URL. + +`redirect_uris` + +: **MUST** contain at least one valid URI. Different rules apply + depending on the value of `application_type`, see below. + +`application_type` + +: Optional, defaults to `web`. Determines further restrictions for + `redirect_uris`. The following values are supported: + + `web` *(default)* + : For web clients. With this, `redirect_uris` **MUST** be + `https://` URIs and **MUST** use the same hostname part as the + `client_uri`. + + `native` + + `native` + + : For native e.g. desktop clients etc. `redirect_uris` **MUST** + match one of: + + - Loopback HTTP URI, e.g. `http://127.0.0.1/` or + `http://[::1]` + - Application-specific scheme, e.g. `com.example.app:/` + - The special OOB URI `urn:ietf:wg:oauth:2.0:oob` + +`tos_uri`, `policy_uri` +: Informative URLs pointing to Terms of Service and Service Policy + document **MUST** use the same scheme (i.e. `https://`) and hostname + as the `client_uri`. + +#### Registration Examples + +In short registration works by POST-ing a JSON structure describing your +client to an endpoint: + +``` bash +curl -sSf https://xmpp.example.net/oauth2/register \ + -H Content-Type:application/json \ + -H Accept:application/json \ + --data ' +{ + "client_name" : "My Application", + "client_uri" : "https://app.example.com/", + "redirect_uris" : [ + "https://app.example.com/redirect" + ] +} +' +``` + +Another example with more fields: + +``` bash +curl -sSf https://xmpp.example.net/oauth2/register \ + -H Content-Type:application/json \ + -H Accept:application/json \ + --data ' +{ + "application_type" : "native", + "client_name" : "Desktop Chat App", + "client_uri" : "https://app.example.org/", + "contacts" : [ + "support@example.org" + ], + "policy_uri" : "https://app.example.org/about/privacy", + "redirect_uris" : [ + "http://localhost:8080/redirect", + "org.example.app:/redirect" + ], + "scope" : "xmpp", + "software_id" : "32a0a8f3-4016-5478-905a-c373156eca73", + "software_version" : "3.4.1", + "tos_uri" : "https://app.example.org/about/terms" +} +' +``` + ### Supported flows +- Authorization Code grant, optionally with Proof Key for Code Exchange +- Resource owner password grant +- Implicit flow *(disabled by default)* +- Refresh Token grants + Various flows can be disabled and enabled with `allowed_oauth2_grant_types` and `allowed_oauth2_response_types`: ```lua +-- These examples reflect the defaults allowed_oauth2_grant_types = { "authorization_code"; -- authorization code grant "password"; -- resource owner password grant @@ -124,16 +231,17 @@ } ``` -The [Proof Key for Code Exchange][RFC 7636] mitigation method can be -made required: +The [Proof Key for Code Exchange][RFC 7636] mitigation method is +optional by default but can be made required: ```lua -oauth2_require_code_challenge = true +oauth2_require_code_challenge = true -- default is false ``` Further, individual challenge methods can be enabled or disabled: ```lua +-- These reflects the default allowed_oauth2_code_challenge_methods = { "plain"; -- the insecure one "S256"; @@ -148,6 +256,7 @@ ```lua oauth2_terms_url = "https://example.com/terms-of-service.html" oauth2_policy_url = "https://example.com/service-policy.pdf" +-- These are unset by default ``` ## Deployment notes @@ -157,7 +266,7 @@ This module does not provide an interface for users to manage what they have granted access to their account! (e.g. to view and revoke clients they have previously authorized). It is recommended to join this module with -mod_client_management to provide such access. However, at the time of writing, +[mod_client_management] to provide such access. However, at the time of writing, no XMPP clients currently support the protocol used by that module. We plan to work on additional interfaces in the future.
--- a/mod_http_oauth2/html/consent.html Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/html/consent.html Sun Jul 09 01:31:29 2023 +0700 @@ -14,6 +14,7 @@ <h1>{site_name}</h1> <fieldset> + <form method="post"> <legend>Authorize new application</legend> <p>A new application wants to connect to your account.</p> <dl> @@ -29,6 +30,11 @@ {client.policy_uri& <dt>Policy</dt> <dd><a href="{client.policy_uri}">View policy</a></dd>} + + <dt>Requested permissions</dt> + <dd>{scopes# + <input class="scope" type="checkbox" id="scope_{idx}" name="scope" value="{item}" checked><label class="scope" for="scope_{idx}">{item}</label>} + </dd> </dl> <p>To allow <em>{client.client_name}</em> to access your account @@ -36,10 +42,6 @@ select 'Allow'. Otherwise, select 'Deny'. </p> - <form method="post"> - <details><summary>Requested permissions</summary>{scopes# - <input class="scope" type="checkbox" id="scope_{idx}" name="scope" value="{item}" checked><label class="scope" for="scope_{idx}">{item}</label>} - </details> <input type="hidden" name="user_token" value="{state.user.token}"> <button type="submit" name="consent" value="denied">Deny</button> <button type="submit" name="consent" value="granted">Allow</button>
--- a/mod_http_oauth2/html/error.html Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/html/error.html Sun Jul 09 01:31:29 2023 +0700 @@ -8,7 +8,7 @@ </head> <body> <main> - <h1>{site_name}<h1> + <h1>{site_name}</h1> <h2>Authentication error</h2> <p>There was a problem with the authentication request. If you were trying to sign in to a third-party application, you may want to report this issue to the developers.</p>
--- a/mod_http_oauth2/html/login.html Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/html/login.html Sun Jul 09 01:31:29 2023 +0700 @@ -16,7 +16,7 @@ <p>{state.error}</p> </div>} <form method="post"> - <input type="text" name="username" placeholder="Username" aria-label="Username" required {extra.no_username_hint&autofocus}{extra.username_hint& value="{extra.username_hint?}"}><br/> + <input type="text" name="username" placeholder="Username" aria-label="Username" required {extra.username_hint~autofocus}{extra.username_hint& value="{extra.username_hint?}"}><br/> <input type="password" name="password" placeholder="Password" aria-label="Password" autocomplete="current-password" required {extra.username_hint&autofocus}><br/> <input type="submit" value="Sign in"> </form>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_oauth2/html/oob.html Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>{site_name} - Authorization Code</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + <main> + <h1>{site_name}</h1> + <h2>Your Authorization Code</h2> + <p>Here’s your authorization code, copy and paste it into {client.client_name}</p> + <div class="oob"> + <p><input readonly name="authorization_code" value="{authorization_code}"></p> + </div> + </main> +</body> +</html>
--- a/mod_http_oauth2/html/style.css Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/html/style.css Sun Jul 09 01:31:29 2023 +0700 @@ -27,6 +27,22 @@ border: solid 1px #f5c2c7; } +.oob +{ + background-color: #d7daf8; + border: solid 1px #c2c7f5; + color: #202984; + margin: 0.75em; +} +.oob input { + font-size: xx-large; + font-family: monospace; + background-color: inherit; + color: inherit; + border: none; + padding: 1ex 2em; +} + input { margin: 0.3rem; padding: 0.2rem; @@ -71,6 +87,10 @@ color: #f8d7da; background-color: #842029; } + .oob { + color: #d7daf8; + background-color: #202984; + } :link
--- a/mod_http_oauth2/mod_http_oauth2.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_http_oauth2/mod_http_oauth2.lua Sun Jul 09 01:31:29 2023 +0700 @@ -1,22 +1,23 @@ -local hashes = require "util.hashes"; +local usermanager = require "core.usermanager"; +local url = require "socket.url"; +local array = require "util.array"; local cache = require "util.cache"; +local encodings = require "util.encodings"; +local errors = require "util.error"; +local hashes = require "util.hashes"; local http = require "util.http"; +local id = require "util.id"; +local it = require "util.iterators"; local jid = require "util.jid"; local json = require "util.json"; -local usermanager = require "core.usermanager"; -local errors = require "util.error"; -local url = require "socket.url"; -local id = require "util.id"; -local encodings = require "util.encodings"; -local base64 = encodings.base64; +local schema = require "util.jsonschema"; +local jwt = require "util.jwt"; local random = require "util.random"; -local schema = require "util.jsonschema"; local set = require "util.set"; -local jwt = require"util.jwt"; -local it = require "util.iterators"; -local array = require "util.array"; local st = require "util.stanza"; +local base64 = encodings.base64; + local function b64url(s) return (base64.encode(s):gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) end @@ -27,6 +28,24 @@ end end +local function strict_formdecode(query) + if not query then + return nil; + end + local params = http.formdecode(query); + if type(params) ~= "table" then + return nil, "no-pairs"; + end + local dups = {}; + for _, pair in ipairs(params) do + if dups[pair.name] then + return nil, "duplicate"; + end + dups[pair.name] = true; + end + return params; +end + local function read_file(base_path, fn, required) local f, err = io.open(base_path .. "/" .. fn); if not f then @@ -41,10 +60,14 @@ return data; end +local allowed_locales = module:get_option_array("allowed_oauth2_locales", {}); +-- TODO Allow translations or per-locale templates somehow. + local template_path = module:get_option_path("oauth2_template_path", "html"); local templates = { login = read_file(template_path, "login.html", true); consent = read_file(template_path, "consent.html", true); + oob = read_file(template_path, "oob.html", true); error = read_file(template_path, "error.html", true); css = read_file(template_path, "style.css"); js = read_file(template_path, "script.js"); @@ -52,7 +75,9 @@ local site_name = module:get_option_string("site_name", module.host); -local _render_html = require"util.interpolation".new("%b{}", st.xml_escape); +local security_policy = module:get_option_string("oauth2_security_policy", "default-src 'self'"); + +local render_html = require"util.interpolation".new("%b{}", st.xml_escape); local function render_page(template, data, sensitive) data = data or {}; data.site_name = site_name; @@ -60,16 +85,19 @@ status_code = data.error and data.error.code or 200; headers = { ["Content-Type"] = "text/html; charset=utf-8"; - ["Content-Security-Policy"] = "default-src 'self'"; + ["Content-Security-Policy"] = security_policy; ["Referrer-Policy"] = "no-referrer"; ["X-Frame-Options"] = "DENY"; ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private"; + ["Pragma"] = "no-cache"; }; - body = _render_html(template, data); + body = render_html(template, data); }; return resp; end +local authorization_server_metadata = nil; + local tokens = module:depends("tokenauth"); local default_access_ttl = module:get_option_number("oauth2_access_token_ttl", 86400); @@ -92,6 +120,21 @@ sign_client, verify_client = jwt.init(registration_algo, registration_key, registration_key, registration_options); end +-- verify and prepare client structure +local function check_client(client_id) + if not verify_client then + return nil, "client-registration-not-enabled"; + end + + local ok, client = verify_client(client_id); + if not ok then + return ok, client; + end + + client.client_hash = b64url(hashes.sha256(client_id)); + return client; +end + -- scope : string | array | set -- -- at each step, allow the same or a subset of scopes @@ -103,7 +146,16 @@ return array(scope_string:gmatch("%S+")); end -local openid_claims = set.new({ "openid"; "profile"; "email"; "address"; "phone" }); +local openid_claims = set.new(); +module:add_item("openid-claim", "openid"); + +module:handle_items("openid-claim", function(event) + authorization_server_metadata = nil; + openid_claims:add(event.item); +end, function() + authorization_server_metadata = nil; + openid_claims = set.new(module:get_host_items("openid-claim")); +end, true); -- array -> array, array, array local function split_scopes(scope_list) @@ -196,7 +248,13 @@ -- properties that are deemed useful e.g. in case tokens issued to a certain -- client needs to be revoked local function client_subset(client) - return { name = client.client_name; uri = client.client_uri; id = client.software_id; version = client.software_version }; + return { + name = client.client_name; + uri = client.client_uri; + id = client.software_id; + version = client.software_version; + hash = client.client_hash; + }; end local function new_access_token(token_jid, role, scope_string, client, id_token, refresh_token_info) @@ -328,24 +386,14 @@ local redirect_uri = get_redirect_uri(client, params.redirect_uri); if redirect_uri == oob_uri then - -- TODO some nicer template page - -- mod_http_errors will set content-type to text/html if it catches this - -- event, if not text/plain is kept for the fallback text. - local response = { status_code = 200; headers = { content_type = "text/plain" } } - response.body = module:context("*"):fire_event("http-message", { - response = response; - title = "Your authorization code"; - message = "Here's your authorization code, copy and paste it into " .. (client.client_name or "your client"); - extra = code; - }) or ("Here's your authorization code:\n%s\n"):format(code); - return response; + return render_page(templates.oob, { client = client; authorization_code = code }, true); elseif not redirect_uri then return oauth_error("invalid_redirect_uri"); end local redirect = url.parse(redirect_uri); - local query = http.formdecode(redirect.query or ""); + local query = strict_formdecode(redirect.query); if type(query) ~= "table" then query = {}; end table.insert(query, { name = "code", value = code }); table.insert(query, { name = "iss", value = get_issuer() }); @@ -357,6 +405,8 @@ return { status_code = 303; headers = { + cache_control = "no-store"; + pragma = "no-cache"; location = url.build(redirect); }; } @@ -379,6 +429,8 @@ return { status_code = 303; headers = { + cache_control = "no-store"; + pragma = "no-cache"; location = url.build(redirect); }; } @@ -401,8 +453,8 @@ return oauth_error("invalid_scope", "unknown scope requested"); end - local client_ok, client = verify_client(params.client_id); - if not client_ok then + local client = check_client(params.client_id); + if not client then return oauth_error("invalid_client", "incorrect credentials"); end @@ -414,6 +466,7 @@ if err then error(err); end -- MUST NOT use the authorization code more than once, so remove it to -- prevent a second attempted use + -- TODO if a second attempt *is* made, revoke any tokens issued codes:set(params.client_id .. "#" .. params.code, nil); if not code or type(code) ~= "table" or code_expired(code) then module:log("debug", "authorization_code invalid or expired: %q", code); @@ -436,8 +489,8 @@ if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end - local client_ok, client = verify_client(params.client_id); - if not client_ok then + local client = check_client(params.client_id); + if not client then return oauth_error("invalid_client", "incorrect credentials"); end @@ -451,6 +504,13 @@ return oauth_error("invalid_grant", "invalid refresh token"); end + local refresh_token_client = refresh_token_info.grant.data.oauth2_client; + if not refresh_token_client.hash or refresh_token_client.hash ~= client.client_hash then + module:log("warn", "OAuth client %q (%s) tried to use refresh token belonging to %q (%s)", client.client_name, client.client_hash, + refresh_token_client.name, refresh_token_client.hash); + return oauth_error("unauthorized_client", "incorrect credentials"); + end + local refresh_scopes = refresh_token_info.grant.data.oauth2_scopes; if params.scope then @@ -514,7 +574,7 @@ user = { username = username; host = module.host; - token = new_user_token({ username = username, host = module.host }); + token = new_user_token({ username = username; host = module.host; auth_time = os.time() }); }; }; elseif form.user_token and form.consent then @@ -607,7 +667,7 @@ if not redirect_uri or redirect_uri == oob_uri then return render_error(err); end - local q = request.url.query and http.formdecode(request.url.query); + local q = strict_formdecode(request.url.query); local redirect_query = url.parse(redirect_uri); local sep = redirect_query.query and "&" or "?"; redirect_uri = redirect_uri @@ -617,12 +677,18 @@ return { status_code = 303; headers = { + cache_control = "no-store"; + pragma = "no-cache"; location = redirect_uri; }; }; end -local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password", "refresh_token"}) +local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", { + "authorization_code"; + "password"; -- TODO Disable. The resource owner password credentials grant [RFC6749] MUST NOT be used. + "refresh_token"; +}) for handler_type in pairs(grant_type_handlers) do if not allowed_grant_type_handlers:contains(handler_type) then module:log("debug", "Grant type %q disabled", handler_type); @@ -657,7 +723,9 @@ local credentials = get_request_credentials(event.request); event.response.headers.content_type = "application/json"; - local params = http.formdecode(event.request.body); + event.response.headers.cache_control = "no-store"; + event.response.headers.pragma = "no-cache"; + local params = strict_formdecode(event.request.body); if not params then return oauth_error("invalid_request"); end @@ -683,7 +751,7 @@ if not request.url.query then return render_error(oauth_error("invalid_request", "Missing query parameters")); end - local params = http.formdecode(request.url.query); + local params = strict_formdecode(request.url.query); if not params then return render_error(oauth_error("invalid_request", "Invalid query parameters")); end @@ -692,9 +760,9 @@ return render_error(oauth_error("invalid_request", "Missing 'client_id' parameter")); end - local ok, client = verify_client(params.client_id); + local client = check_client(params.client_id); - if not ok then + if not client then return render_error(oauth_error("invalid_request", "Invalid 'client_id' parameter")); end @@ -718,13 +786,31 @@ end); end + -- The 'prompt' parameter from OpenID Core + local prompt = set.new(parse_scopes(params.prompt or "select_account login consent")); + if prompt:contains("none") then + -- Client wants no interaction, only confirmation of prior login and + -- consent, but this is not implemented. + return error_response(request, redirect_uri, oauth_error("interaction_required")); + elseif not prompt:contains("select_account") then + -- TODO If the login page is split into account selection followed by login + -- (e.g. password), and then the account selection could be skipped iff the + -- 'login_hint' parameter is present. + return error_response(request, redirect_uri, oauth_error("account_selection_required")); + elseif not prompt:contains("login") then + -- Currently no cookies or such are used, so login is required every time. + return error_response(request, redirect_uri, oauth_error("login_required")); + elseif not prompt:contains("consent") then + -- Are there any circumstances when consent would be implied or assumed? + return error_response(request, redirect_uri, oauth_error("consent_required")); + end + local auth_state = get_auth_state(request); if not auth_state.user then -- Render login page local extra = {}; if params.login_hint then extra.username_hint = (jid.prepped_split(params.login_hint)); - extra.no_username_hint = not extra.username_hint; end return render_page(templates.login, { state = auth_state; client = client; extra = extra }); elseif auth_state.consent == nil then @@ -755,6 +841,7 @@ iss = get_issuer(); sub = url.build({ scheme = "xmpp"; path = user_jid }); aud = params.client_id; + auth_time = auth_state.user.auth_time; nonce = params.nonce; }); local response_type = params.response_type; @@ -771,6 +858,8 @@ local function handle_revocation_request(event) local request, response = event.request, event.response; + response.headers.cache_control = "no-store"; + response.headers.pragma = "no-cache"; if request.headers.authorization then local credentials = get_request_credentials(request); if not credentials or credentials.type ~= "basic" then @@ -783,7 +872,7 @@ end end - local form_data = http.formdecode(event.request.body or ""); + local form_data = strict_formdecode(event.request.body); if not form_data or not form_data.token then response.headers.accept = "application/x-www-form-urlencoded"; return 415; @@ -839,24 +928,31 @@ default = { "code" }; }; client_name = { type = "string" }; - client_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; - logo_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; + client_uri = { type = "string"; format = "uri"; pattern = "^https:" }; + logo_uri = { type = "string"; format = "uri"; pattern = "^https:" }; scope = { type = "string" }; contacts = { type = "array"; minItems = 1; items = { type = "string"; format = "email" } }; - tos_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; - policy_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; - jwks_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; - jwks = { type = "object"; description = "JSON Web Key Set, RFC 7517" }; + tos_uri = { type = "string"; format = "uri"; pattern = "^https:" }; + policy_uri = { type = "string"; format = "uri"; pattern = "^https:" }; software_id = { type = "string"; format = "uuid" }; software_version = { type = "string" }; }; - luaPatternProperties = { - -- Localized versions of descriptive properties and URIs - ["^client_name#"] = { description = "Localized version of 'client_name'"; type = "string" }; - ["^[a-z_]+_uri#"] = { type = "string"; format = "uri"; luaPattern = "^https:" }; - }; } +-- Limit per-locale fields to allowed locales, partly to keep size of client_id +-- down, partly because we don't yet use them for anything. +-- Only relevant for user-visible strings and URIs. +if allowed_locales[1] then + local props = registration_schema.properties; + for _, locale in ipairs(allowed_locales) do + props["client_name#" .. locale] = props["client_name"]; + props["client_uri#" .. locale] = props["client_uri"]; + props["logo_uri#" .. locale] = props["logo_uri"]; + props["tos_uri#" .. locale] = props["tos_uri"]; + props["policy_uri#" .. locale] = props["policy_uri"]; + end +end + local function redirect_uri_allowed(redirect_uri, client_uri, app_type) local uri = url.parse(redirect_uri); if not uri.scheme then @@ -881,6 +977,13 @@ end end + -- MUST ignore any metadata that it does not understand + for propname in pairs(client_metadata) do + if not registration_schema.properties[propname] then + client_metadata[propname] = nil; + end + end + local client_uri = url.parse(client_metadata.client_uri); if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri"); @@ -900,19 +1003,6 @@ end end - for k, v in pairs(client_metadata) do - local base_k = k:match"^([^#]+)#" or k; - if not registration_schema.properties[base_k] or k:find"^client_uri#" then - -- Ignore and strip unknown extra properties - client_metadata[k] = nil; - elseif k:find"_uri#" then - -- Localized URIs should be secure too - if not redirect_uri_allowed(v, client_uri, "web") then - return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI"); - end - end - end - local grant_types = set.new(client_metadata.grant_types); local response_types = set.new(client_metadata.response_types); @@ -928,10 +1018,6 @@ return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified"); end - -- Ensure each signed client_id JWT is unique, short ID and issued at - -- timestamp should be sufficient to rule out brute force attacks - client_metadata.nonce = id.short(); - -- Do we want to keep everything? local client_id = sign_client(client_metadata); @@ -939,7 +1025,14 @@ client_metadata.client_id_issued_at = os.time(); if client_metadata.token_endpoint_auth_method ~= "none" then - local client_secret = make_client_secret(client_id); + -- Ensure that each client_id JWT with a client_secret is unique. + -- A short ID along with the issued at timestamp should be sufficient to + -- rule out brute force attacks. + -- Not needed for public clients without a secret, but those are expected + -- to be uncommon since they can only do the insecure implicit flow. + client_metadata.nonce = id.short(); + + local client_secret = make_client_secret(client_id, client_metadata); client_metadata.client_secret = client_secret; client_metadata.client_secret_expires_at = 0; @@ -963,7 +1056,11 @@ return { status_code = 201; - headers = { content_type = "application/json" }; + headers = { + cache_control = "no-store"; + pragma = "no-cache"; + content_type = "application/json"; + }; body = json.encode(response); }; end @@ -1036,6 +1133,7 @@ -- Step 2. User-facing login and consent view ["GET /authorize"] = handle_authorization_request; ["POST /authorize"] = handle_authorization_request; + ["OPTIONS /authorize"] = { status_code = 403; body = "" }; -- Step 3. User is redirected to the 'redirect_uri' along with an -- authorization code. In the insecure 'implicit' flow, the access token @@ -1057,7 +1155,7 @@ headers = { ["Content-Type"] = "text/css"; }; - body = _render_html(templates.css, module:get_option("oauth2_template_style")); + body = render_html(templates.css, module:get_option("oauth2_template_style")); } or nil; ["GET /script.js"] = templates.js and { headers = { @@ -1087,39 +1185,53 @@ -- OIDC Discovery +function get_authorization_server_metadata() + if authorization_server_metadata then + return authorization_server_metadata; + end + authorization_server_metadata = { + -- RFC 8414: OAuth 2.0 Authorization Server Metadata + issuer = get_issuer(); + authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; + token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; + registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; + scopes_supported = usermanager.get_all_roles + and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items())); + response_types_supported = array(it.keys(response_type_handlers)); + token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); + op_policy_uri = module:get_option_string("oauth2_policy_url", nil); + op_tos_uri = module:get_option_string("oauth2_terms_url", nil); + revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; + revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); + code_challenge_methods_supported = array(it.keys(verifier_transforms)); + grant_types_supported = array(it.keys(response_type_handlers)):map(tmap { + token = "implicit"; + code = "authorization_code"; + }); + response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); + authorization_response_iss_parameter_supported = true; + service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); + ui_locales_supported = allowed_locales[1] and allowed_locales; + + -- OpenID + userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; + jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata + id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key. + } + return authorization_server_metadata; +end + module:provides("http", { name = "oauth2-discovery"; default_path = "/.well-known/oauth-authorization-server"; cors = { enabled = true }; route = { - ["GET"] = { - headers = { content_type = "application/json" }; - body = json.encode { - -- RFC 8414: OAuth 2.0 Authorization Server Metadata - issuer = get_issuer(); - authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; - token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; - registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; - scopes_supported = usermanager.get_all_roles - and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items())); - response_types_supported = array(it.keys(response_type_handlers)); - token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); - op_policy_uri = module:get_option_string("oauth2_policy_url", nil); - op_tos_uri = module:get_option_string("oauth2_terms_url", nil); - revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; - revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); - code_challenge_methods_supported = array(it.keys(verifier_transforms)); - grant_types_supported = array(it.keys(response_type_handlers)):map(tmap { token = "implicit"; code = "authorization_code" }); - response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); - authorization_response_iss_parameter_supported = true; - service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); - - -- OpenID - userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; - jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata - id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key. - }; - }; + ["GET"] = function() + return { + headers = { content_type = "application/json" }; + body = json.encode(get_authorization_server_metadata()); + } + end }; });
--- a/mod_invites_adhoc/mod_invites_adhoc.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_invites_adhoc/mod_invites_adhoc.lua Sun Jul 09 01:31:29 2023 +0700 @@ -19,7 +19,11 @@ if module.may then if allow_user_invites then - module:default_permission("prosody:user", ":invite-new-users"); + if require "core.features".available:contains("split-user-roles") then + module:default_permission("prosody:registered", ":invite-new-users"); + else -- COMPAT + module:default_permission("prosody:user", ":invite-new-users"); + end end if not allow_user_invite_roles:empty() or not deny_user_invite_roles:empty() then return error("allow_user_invites_by_roles and deny_user_invites_by_roles are deprecated options");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_measure_lua/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,19 @@ +This module provides two [metrics][doc:statistics]: + +`lua_heap_bytes` +: Bytes of memory as reported by `collectgarbage("count")`{.lua} + +`lua_info` +: Provides the current Lua version as a label + +``` openmetrics +# HELP lua_info Lua runtime version +# UNIT lua_info +# TYPE lua_info gauge +lua_info{version="Lua 5.4"} 1 +# HELP lua_heap_bytes Memory used by objects under control of the Lua +garbage collector +# UNIT lua_heap_bytes bytes +# TYPE lua_heap_bytes gauge +lua_heap_bytes 8613218 +```
--- a/mod_muc_defaults/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_muc_defaults/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -4,7 +4,7 @@ ## Configuration -Under your MUC component, add a `muc_defaults` option with the relevant settings. +Under your MUC component, add a `default_mucs` option with the relevant settings. ``` Component "conference.example.org" "muc" @@ -12,7 +12,7 @@ "muc_defaults"; } - muc_defaults = { + default_mucs = { { jid_node = "trollbox", affiliations = {
--- a/mod_muc_limits/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_muc_limits/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -35,11 +35,13 @@ You can define (globally or per-MUC component) the following options: - Name Default value Description - ------------------------ --------------- ---------------------------------------------- - muc\_event\_rate 0.5 The maximum number of events per second. - muc\_burst\_factor 6 Allow temporary bursts of this multiple. - muc\_max\_nick\_length 23 The maximum allowed length of user nicknames + Name Default value Description + --------------------- --------------- -------------------------------------------------- + muc_event_rate 0.5 The maximum number of events per second. + muc_burst_factor 6 Allow temporary bursts of this multiple. + muc_max_nick_length 23 The maximum allowed length of user nicknames + muc_max_char_count 5664 The maximum allowed number of bytes in a message + muc_max_line_count 23 The maximum allowed number of lines in a message For more understanding of how these values are used, see the algorithm section below.
--- a/mod_muc_limits/mod_muc_limits.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_muc_limits/mod_muc_limits.lua Sun Jul 09 01:31:29 2023 +0700 @@ -13,6 +13,9 @@ local burst = math.max(module:get_option_number("muc_burst_factor", 6), 1); local max_nick_length = module:get_option_number("muc_max_nick_length", 23); -- Default chosen through scientific methods +local max_line_count = module:get_option_number("muc_max_line_count", 23); -- Default chosen through s/scientific methods/copy and paste/ +local max_char_count = module:get_option_number("muc_max_char_count", 5664); -- Default chosen by multiplying a number by 23 + local join_only = module:get_option_boolean("muc_limit_joins_only", false); local dropped_count = 0; local dropped_jids; @@ -46,7 +49,25 @@ throttle = new_throttle(period*burst, burst); room.throttle = throttle; end - if not throttle:poll(1) then + local cost = 1; + local body = stanza:get_child_text("body"); + if body then + -- TODO calculate a text diagonal cross-section or some mathemagical + -- number, maybe some cost multipliers + if #body > max_char_count then + origin.send(st.error_reply(stanza, "modify", "policy-violation", "Your message is too long, please write a shorter one") + :up():tag("x", { xmlns = xmlns_muc })); + return true; + end + local body_lines = select(2, body:gsub("\n[^\n]*", "")); + if body_lines > max_line_count then + origin.send(st.error_reply(stanza, "modify", "policy-violation", "Your message is too long, please write a shorter one"):up() + :tag("x", { xmlns = xmlns_muc; })); + return true; + end + cost = cost + body_lines; + end + if not throttle:poll(cost) then module:log("debug", "Dropping stanza for %s@%s from %s, over rate limit", dest_room, dest_host, from_jid); if not dropped_jids then dropped_jids = { [from_jid] = true, from_jid }; @@ -60,7 +81,6 @@ return true; end local reply = st.error_reply(stanza, "wait", "policy-violation", "The room is currently overactive, please try again later"); - local body = stanza:get_child_text("body"); if body then reply:up():tag("body"):text(body):up(); end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_members_json/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,81 @@ +--- +labels: +- 'Stage-Beta' +summary: 'Import MUC membership info from a JSON file' +... + +Introduction +============ + +This module allows you to import MUC membership information from an external +URL in JSON format. + +Details +======= + +If you have an organization or community and lots of members and/or channels, +it can be frustrating to manage MUC affiliations manually. This module will +fetch a JSON file from a configured URL, and use that to automatically set the +MUC affiliations. + +It also supports hats/badges. + +Configuration +============= + +Add the module to the MUC host (not the global modules\_enabled): + + Component "conference.example.com" "muc" + modules_enabled = { "muc_members_json" } + +You can define (globally or per-MUC component) the following options: + + Name Description + --------------------- -------------------------------------------------- + muc_members_json_url The URL to the JSON file describing memberships + muc_members_json_mucs The MUCs to manage, and their associated configuration + +The `muc_members_json_mucs` setting determines which rooms will be managed by +the plugin, and how to map roles to hats (if desired). + +``` +muc_members_json_mucs = { + myroom = { + member_hat = { + id = "urn:uuid:6a1b143a-1c5c-11ee-80aa-4ff1ce4867dc"; + title = "Cool Member"; + }; + }; +} +``` + +JSON format +=========== + +``` +{ + "members": [ + { + "jids": ["user@example.com"] + }, + { + "jids": ["user2@example.com"] + }, + { + "jids": ["user3@example.com"], + roles: ["janitor"] + } + ] +} +``` + +Each member must have a `jids` field, and optionally a `roles` field. + +Compatibility +============= + + ------- ------------------ + trunk Works + 0.12 Works + ------- ------------------ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_members_json/mod_muc_members_json.lua Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,93 @@ +local http = require "net.http"; +local json = require "util.json"; + +local json_url = assert(module:get_option_string("muc_members_json_url"), "muc_members_json_url required"); +local managed_mucs = module:get_option("muc_members_json_mucs"); + +local mod_muc = module:depends("muc"); + +--[[ +{ + xsf = { + team_hats = { + board = { + id = "xmpp:xmpp.org/hats/board"; + title = "Board"; + }; + }; + member_hat = { + id = "xmpp:xmpp.org/hats/member"; + title = "XSF member"; + }; + }; + iteam = { + team_hats = { + iteam = { + id = "xmpp:xmpp.org/hats/iteam"; + title = "Infra team"; + }; + }; + }; +} +--]] + +local function get_hats(member_info, muc_config) + local hats = {}; + if muc_config.member_hat then + hats[muc_config.member_hat.id] = { + title = muc_config.member_hat.title; + active = true; + }; + end + if muc_config.team_hats and member_info.roles then + for _, role in ipairs(member_info.roles) do + local hat = muc_config.team_hats[role]; + if hat then + hats[hat.id] = { + title = hat.title; + active = true; + }; + end + end + end + return hats; +end + +function module.load() + http.request(json_url) + :next(function (result) + return json.decode(result.body); + end) + :next(function (data) + module:log("debug", "DATA: %s", require "util.serialization".serialize(data, "debug")); + + for name, muc_config in pairs(managed_mucs) do + local muc_jid = name.."@"..module.host; + local muc = mod_muc.get_room_from_jid(muc_jid); + module:log("warn", "%s -> %s -> %s", name, muc_jid, muc); + if muc then + local jids = {}; + for _, member_info in ipairs(data.members) do + for _, member_jid in ipairs(member_info.jids) do + jids[member_jid] = true; + local affiliation = muc:get_affiliation(member_jid); + if not affiliation then + muc:set_affiliation(true, member_jid, "member", "imported membership"); + muc:set_affiliation_data(member_jid, "source", module.name); + end + muc:set_affiliation_data(member_jid, "hats", get_hats(member_info, muc_config)); + end + end + -- Remove affiliation from folk who weren't in the source data but previously were + for jid, aff, data in muc:each_affiliation() do + if not jids[jid] and data.source == module.name then + muc:set_affiliation(true, jid, "none", "imported membership lost"); + end + end + end + end + + end):catch(function (err) + module:log("error", "FAILED: %s", err); + end); +end
--- a/mod_oidc_userinfo_vcard4/README.md Fri May 26 02:15:45 2023 +0700 +++ b/mod_oidc_userinfo_vcard4/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -4,7 +4,7 @@ - Stage-Alpha rockspec: dependencies: - - mod_http_oauth2 + - mod_http_oauth2 >= 200 --- This module extracts profile details from the user's [vcard4][XEP-0292]
--- a/mod_oidc_userinfo_vcard4/mod_oidc_userinfo_vcard4.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_oidc_userinfo_vcard4/mod_oidc_userinfo_vcard4.lua Sun Jul 09 01:31:29 2023 +0700 @@ -1,11 +1,14 @@ -- Provide OpenID UserInfo data to mod_http_oauth2 -- Alternatively, separate module for the whole HTTP endpoint? -- -local nodeprep = require "util.encodings".stringprep.nodeprep; +module:add_item("openid-claim", "address"); +module:add_item("openid-claim", "email"); +module:add_item("openid-claim", "phone"); +module:add_item("openid-claim", "profile"); local mod_pep = module:depends "pep"; -local gender_map = { M = "male"; F = "female"; O = "other"; N = "nnot applicable"; U = "unknown" } +local gender_map = { M = "male"; F = "female"; O = "other"; N = "not applicable"; U = "unknown" } module:hook("token/userinfo", function(event) local pep_service = mod_pep.get_pep_service(event.username);
--- a/mod_pubsub_alertmanager/README.md Fri May 26 02:15:45 2023 +0700 +++ b/mod_pubsub_alertmanager/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -93,3 +93,21 @@ `alertmanager_node_template` : Template for the pubsub node name, defaults to `"{path?alerts}"` + +`alertmanager_path_configs` +: Per-path configuration variables (see below). + +### Per-path configuration + +It's possible to override configuration options based on the path suffix. For +example, if a request is made to `http://prosody/pubsub_alertmanager/foo` the +path suffix is `foo`. You can then supply the following configuration: + +``` lua +alertmanager_path_configs = { + foo = { + node_template = "alerts/{alert.labels.severity}"; + publisher = "user@example.net"; + }; +} +```
--- a/mod_pubsub_alertmanager/mod_pubsub_alertmanager.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_pubsub_alertmanager/mod_pubsub_alertmanager.lua Sun Jul 09 01:31:29 2023 +0700 @@ -29,11 +29,16 @@ return 202; end -local node_template = module:get_option_string("alertmanager_node_template", "{path?alerts}"); +local global_node_template = module:get_option_string("alertmanager_node_template", "{path?alerts}"); +local path_configs = module:get_option("alertmanager_path_configs", {}); function handle_POST(event, path) local request = event.request; + local config = path_configs[path] or {}; + local node_template = config.node_template or global_node_template; + local publisher = config.publisher or request.ip; + local payload = json.decode(event.request.body); if type(payload) ~= "table" then return 400; end if payload.version ~= "4" then return 501; end @@ -55,7 +60,7 @@ end local node = render(node_template, {alert = alert, path = path, payload = payload, request = request}); - local ret = publish_payload(node, request.ip, uuid_generate(), item); + local ret = publish_payload(node, publisher, uuid_generate(), item); if ret ~= 202 then return ret end
--- a/mod_pubsub_feeds/README.markdown Fri May 26 02:15:45 2023 +0700 +++ b/mod_pubsub_feeds/README.markdown Sun Jul 09 01:31:29 2023 +0700 @@ -35,27 +35,27 @@ [XEP-0060](http://xmpp.org/extensions/xep-0060.html). Results are in [ATOM 1.0 format](http://atomenabled.org/) for easy consumption. -# PubSubHubbub +# WebSub {#pubsubhubbub} -This module also implements a -[PubSubHubbub](http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html) -subscriber. This allows feeds that have an associated "hub" to push -updates when they are published. +This module also implements [WebSub](https://www.w3.org/TR/websub/), +formerly known as +[PubSubHubbub](http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html). +This allows "feed hubs" to instantly push feed updates to subscribers. -Not all feeds support this. - -It needs to expose a HTTP callback endpoint to work. +This may be removed in the future since it does not seem to be oft used +anymore. # Option summary - Option Description - ---------------------- ------------------------------------------------------------------------- - `feeds` A list of virtual nodes to create and their associated Atom or RSS URL. - `feed_pull_interval` Number of minutes between polling for new results (default 15) - `use_pubsubhubub` Set to `false` to disable PubSubHubbub + Option Description + ------------------------------ -------------------------------------------------------------------------- + `feeds` A list of virtual nodes to create and their associated Atom or RSS URL. + `feed_pull_interval_seconds` Number of seconds between polling for new results (default 15 *minutes*) + `use_pubsubhubub` Set to `true` to enable WebSub # Compatibility - ----- ------- - 0.9 Works - ----- ------- + ------ ------- + 0.12 Works + 0.11 Works + ------ -------
--- a/mod_pubsub_feeds/mod_pubsub_feeds.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_pubsub_feeds/mod_pubsub_feeds.lua Sun Jul 09 01:31:29 2023 +0700 @@ -1,17 +1,4 @@ -- Fetches Atom feeds and publishes to PubSub nodes --- --- Config: --- Component "pubsub.example.com" "pubsub" --- modules_enabled = { --- "pubsub_feeds"; --- } --- feeds = { -- node -> url --- prosody_blog = "http://blog.prosody.im/feed/atom.xml"; --- } --- feed_pull_interval = 20 -- minutes --- --- Reference --- http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.4.html local pubsub = module:depends"pubsub"; @@ -36,7 +23,7 @@ return nil, "unsupported-format"; end -local use_pubsubhubub = module:get_option_boolean("use_pubsubhubub", true); +local use_pubsubhubub = module:get_option_boolean("use_pubsubhubub", false); if use_pubsubhubub then module:depends"http"; end @@ -46,7 +33,8 @@ local formencode = http.formencode; local feed_list = module:shared("feed_list"); -local refresh_interval = module:get_option_number("feed_pull_interval", 15) * 60; +local legacy_refresh_interval = module:get_option_number("feed_pull_interval", 15); +local refresh_interval = module:get_option_number("feed_pull_interval_seconds", legacy_refresh_interval*60); local lease_length = tostring(math.floor(module:get_option_number("feed_lease_length", 86400))); function module.load() @@ -60,7 +48,12 @@ end new_feed_list[node] = true; if not feed_list[node] then - feed_list[node] = { url = url; node = node; last_update = 0 }; + local ok, err = pubsub.service:create(node, true); + if ok or err == "conflict" then + feed_list[node] = { url = url; node = node; last_update = 0 }; + else + module:log("error", "Could not create node %s: %s", node, err); + end else feed_list[node].url = url; end @@ -75,58 +68,68 @@ end end -function update_entry(item) +function update_entry(item, data) local node = item.node; - module:log("debug", "parsing %d bytes of data in node %s", #item.data or 0, node) - local feed, err = parse_feed(item.data); + module:log("debug", "parsing %d bytes of data in node %s", #data or 0, node) + local feed, err = parse_feed(data); if not feed then module:log("error", "Could not parse feed %q: %s", item.url, err); - module:log("debug", "Feed data:\n%s\n.", item.data); + module:log("debug", "Feed data:\n%s\n.", data); return; end local entries = {}; for entry in feed:childtags("entry") do table.insert(entries, entry); end - local ok, items = pubsub.service:get_items(node, true); + local ok, last_id = pubsub.service:get_last_item(node, true); if not ok then - local ok, err = pubsub.service:create(node, true); - if not ok then - module:log("error", "Could not create node %s: %s", node, err); - return; + module:log("error", "PubSub node %q missing: %s", node, last_id); + return + end + + local start_from = #entries; + for i, entry in ipairs(entries) do + local id = entry:get_child_text("id"); + if not id then + local link = entry:get_child("link"); + if link then + module:log("debug", "Feed %q item %s is missing an id, using <link> instead", item.url, entry:top_tag()); + id = link and link.attr.href; + else + module:log("error", "Feed %q item %s is missing both id and link, this feed is unusable", item.url, entry:top_tag()); + return; + end + entry:text_tag("id", id); end - items = {}; + + if last_id == id then + -- This should be the first item that we already have. + start_from = i-1; + break + end end - for i = #entries, 1, -1 do -- Feeds are usually in reverse order + + for i = start_from, 1, -1 do -- Feeds are usually in reverse order local entry = entries[i]; entry.attr.xmlns = xmlns_atom; - local e_published = entry:get_child_text("published"); - e_published = e_published and dt_parse(e_published); - local e_updated = entry:get_child_text("updated"); - e_updated = e_updated and dt_parse(e_updated); + local id = entry:get_child_text("id"); - local timestamp = e_updated or e_published or nil; - --module:log("debug", "timestamp is %s, item.last_update is %s", tostring(timestamp), tostring(item.last_update)); + local timestamp = dt_parse(entry:get_child_text("published")); + if not timestamp then + timestamp = time(); + entry:text_tag("published", dt_datetime(timestamp)); + end + if not timestamp or not item.last_update or timestamp > item.last_update then - local id = entry:get_child_text("id"); - if not id then - local link = entry:get_child("link"); - id = link and link.attr.href; - end - if not id then - -- Sigh, no link? - id = feed.url .. "#" .. hmac_sha1(feed.url, tostring(entry), true) .. "@" .. dt_datetime(timestamp); - end - if not items[id] then - local xitem = st.stanza("item", { id = id, xmlns = "http://jabber.org/protocol/pubsub" }):add_child(entry); - -- TODO Put data from /feed into item/source + local xitem = st.stanza("item", { id = id, xmlns = "http://jabber.org/protocol/pubsub" }):add_child(entry); + -- TODO Put data from /feed into item/source - --module:log("debug", "publishing to %s, id %s", node, id); - local ok, err = pubsub.service:publish(node, true, id, xitem); - if not ok then - module:log("error", "Publishing to node %s failed: %s", node, err); - end + local ok, err = pubsub.service:publish(node, true, id, xitem); + if not ok then + module:log("error", "Publishing to node %s failed: %s", node, err); + elseif timestamp then + item.last_update = timestamp; end end end @@ -148,20 +151,18 @@ end function fetch(item, callback) -- HTTP Pull - local headers = { }; - if item.data and item.etag then - headers["If-None-Match"] = item.etag; - end + local headers = { + ["If-None-Match"] = item.etag; + ["Accept"] = "application/atom+xml, application/x-rss+xml, application/xml"; + }; http.request(item.url, { headers = headers }, function(data, code, resp) if code == 200 then - item.data = data; - if callback then callback(item) end - item.last_update = time(); + if callback then callback(item, data) end if resp.headers then item.etag = resp.headers.etag end elseif code == 304 then - item.last_update = time(); + module:log("debug", "No updates to %q", item.url); elseif code == 301 and resp.headers.location then module:log("info", "Feed %q has moved to %q", item.url, resp.headers.location); elseif code <= 100 then @@ -268,9 +269,7 @@ end module:log("debug", "Valid signature"); end - feed.data = body; - update_entry(feed); - feed.last_update = time(); + update_entry(feed, body); return 202; end return 400;
--- a/mod_rest/example/prosody_oauth.py Fri May 26 02:15:45 2023 +0700 +++ b/mod_rest/example/prosody_oauth.py Sun Jul 09 01:31:29 2023 +0700 @@ -16,6 +16,9 @@ "client_name": client_name, "client_uri": client_uri, "redirect_uris": [redirect_uri], + "application_type": redirect_uri[:8] == "https://" + and "web" + or "native", }, ).json()
--- a/mod_rest/mod_rest.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_rest/mod_rest.lua Sun Jul 09 01:31:29 2023 +0700 @@ -294,6 +294,7 @@ local function handle_request(event, path) local request, response = event.request, event.response; + local log = request.log or module._log; local from; local origin; local echo = path == "echo"; @@ -308,8 +309,9 @@ return post_errors.new("unauthz"); end from = jid.join(origin.username, origin.host, origin.resource); + origin.full_jid = from; origin.type = "c2s"; - origin.log = module._log; + origin.log = log; end local payload, err = parse_request(request, path); if not payload then @@ -352,7 +354,7 @@ ["xml:lang"] = payload.attr["xml:lang"], }; - module:log("debug", "Received[rest]: %s", payload:top_tag()); + log("debug", "Received[rest]: %s", payload:top_tag()); local send_type = decide_type((request.headers.accept or "") ..",".. (request.headers.content_type or ""), supported_outputs) if echo then @@ -395,7 +397,7 @@ local p = module:send_iq(payload, origin, iq_timeout):next( function (result) - module:log("debug", "Sending[rest]: %s", result.stanza:top_tag()); + log("debug", "Sending[rest]: %s", result.stanza:top_tag()); response.headers.content_type = send_type; if responses[1] then local tail = responses[#responses]; @@ -410,11 +412,11 @@ end, function (error) if not errors.is_err(error) then - module:log("error", "Uncaught native error: %s", error); + log("error", "Uncaught native error: %s", error); return select(2, errors.coerce(nil, error)); elseif error.context and error.context.stanza then response.headers.content_type = send_type; - module:log("debug", "Sending[rest]: %s", error.context.stanza:top_tag()); + log("debug", "Sending[rest]: %s", error.context.stanza:top_tag()); return encode(send_type, error.context.stanza); else return error; @@ -430,7 +432,7 @@ return p; else function origin.send(stanza) - module:log("debug", "Sending[rest]: %s", stanza:top_tag()); + log("debug", "Sending[rest]: %s", stanza:top_tag()); response.headers.content_type = send_type; response:send(encode(send_type, stanza)); return true;
--- a/mod_rest/res/openapi.yaml Fri May 26 02:15:45 2023 +0700 +++ b/mod_rest/res/openapi.yaml Sun Jul 09 01:31:29 2023 +0700 @@ -1,6 +1,5 @@ --- openapi: 3.0.1 - info: title: mod_rest API version: 0.3.2 @@ -10,14 +9,12 @@ and a simplified JSON mapping. license: name: MIT - paths: - /rest: post: summary: Send stanzas and receive responses. Webhooks work the same way. tags: - - generic + - generic security: - basic: [] - token: [] @@ -25,35 +22,33 @@ requestBody: $ref: '#/components/requestBodies/common' responses: - '200': + "200": $ref: '#/components/responses/success' - '202': + "202": $ref: '#/components/responses/sent' - /rest/{kind}/{type}/{to}: post: summary: Even more RESTful mapping with certain components in the path. tags: - - generic + - generic security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/kind' - - $ref: '#/components/parameters/type' - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/kind' + - $ref: '#/components/parameters/type' + - $ref: '#/components/parameters/to' requestBody: $ref: '#/components/requestBodies/common' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/echo: post: summary: Build as stanza and return it for inspection. tags: - - debug + - debug security: - basic: [] - token: [] @@ -61,22 +56,21 @@ requestBody: $ref: '#/components/requestBodies/common' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/ping/{to}: get: tags: - - query + - query summary: Ping a local or remote server or other entity security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": description: Test reachability of some address content: application/json: @@ -85,21 +79,19 @@ application/xmpp+xml: schema: $ref: '#/components/schemas/iq_pong' - - /rest/version/{to}: get: tags: - - query + - query summary: Ask what software version is used. security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": description: Version query response content: application/json: @@ -108,155 +100,146 @@ application/xmpp+xml: schema: $ref: '#/components/schemas/iq_result_version' - /rest/disco/{to}: get: tags: - - query + - query summary: Query a remote entity for supported features security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/items/{to}: get: tags: - - query + - query summary: Query an entity for related services, chat rooms or other items security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/extdisco/{to}: get: tags: - - query + - query summary: Query for external services (usually STUN and TURN) security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' - - name: type - in: query - schema: - type: string - example: stun + - $ref: '#/components/parameters/to' + - name: type + in: query + schema: + type: string + example: stun responses: - '200': + "200": $ref: '#/components/responses/success' - - /rest/archive/{to}: get: tags: - - query + - query summary: Query a message archive security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' - - name: with - in: query - schema: - type: string - - name: start - in: query - schema: - type: string - - name: end - in: query - schema: - type: string - - name: before-id - in: query - schema: - type: string - - name: after-id - in: query - schema: - type: string - - name: ids - in: query - schema: - type: string - description: comma-separated list of archive ids - - name: after - in: query - schema: - type: string - - name: before - in: query - schema: - type: string - - name: max - in: query - schema: - type: integer + - $ref: '#/components/parameters/to' + - name: with + in: query + schema: + type: string + - name: start + in: query + schema: + type: string + - name: end + in: query + schema: + type: string + - name: before-id + in: query + schema: + type: string + - name: after-id + in: query + schema: + type: string + - name: ids + in: query + schema: + type: string + description: comma-separated list of archive ids + - name: after + in: query + schema: + type: string + - name: before + in: query + schema: + type: string + - name: max + in: query + schema: + type: integer responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/lastactivity/{to}: get: tags: - - query + - query summary: Query last activity of an entity. Sometimes used as "uptime" for servers. security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/stats/{to}: get: tags: - - query + - query summary: Query an entity for statistics security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": $ref: '#/components/responses/success' - /rest/upload_request/{to}: get: tags: - - query + - query summary: Lorem ipsum security: - - basic: [] - - token: [] - - oauth2: [] + - basic: [] + - token: [] + - oauth2: [] parameters: - - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/to' responses: - '200': + "200": $ref: '#/components/responses/success' - components: schemas: stanza: @@ -271,7 +254,6 @@ - $ref: '#/components/schemas/message' - $ref: '#/components/schemas/presence' - $ref: '#/components/schemas/iq' - message: type: object xml: @@ -281,18 +263,17 @@ description: Which kind of stanza type: string enum: - - message + - message type: type: string enum: - - chat - - error - - groupchat - - headline - - normal + - chat + - error + - groupchat + - headline + - normal xml: attribute: true - to: $ref: '#/components/schemas/to' from: @@ -301,7 +282,6 @@ $ref: '#/components/schemas/id' lang: $ref: '#/components/schemas/lang' - body: $ref: '#/components/schemas/body' subject: @@ -310,7 +290,6 @@ $ref: '#/components/schemas/thread' invite: $ref: '#/components/schemas/invite' - state: $ref: '#/components/schemas/state' nick: @@ -319,7 +298,6 @@ $ref: '#/components/schemas/delay' replace: $ref: '#/components/schemas/replace' - html: $ref: '#/components/schemas/html' oob: @@ -344,19 +322,14 @@ $ref: '#/components/schemas/displayed' encryption: $ref: '#/components/schemas/encryption' - archive: $ref: '#/components/schemas/archive_result' - dataform: $ref: '#/components/schemas/dataform' - forwarded: $ref: '#/components/schemas/forwarded' - error: $ref: '#/components/schemas/error' - presence: type: object properties: @@ -364,7 +337,7 @@ description: Which kind of stanza type: string enum: - - presence + - presence type: type: string enum: @@ -385,14 +358,12 @@ $ref: '#/components/schemas/id' lang: $ref: '#/components/schemas/lang' - show: $ref: '#/components/schemas/show' status: $ref: '#/components/schemas/status' priority: $ref: '#/components/schemas/priority' - caps: $ref: '#/components/schemas/caps' nick: @@ -403,13 +374,10 @@ $ref: '#/components/schemas/vcard_update' idle_since: $ref: '#/components/schemas/idle_since' - muc: $ref: '#/components/schemas/muc' - error: $ref: '#/components/schemas/error' - iq: type: object properties: @@ -417,14 +385,14 @@ description: Which kind of stanza type: string enum: - - iq + - iq type: type: string enum: - - get - - set - - result - - error + - get + - set + - result + - error xml: attribute: true to: @@ -435,7 +403,6 @@ $ref: '#/components/schemas/id' lang: $ref: '#/components/schemas/lang' - ping: $ref: '#/components/schemas/ping' version: @@ -448,7 +415,6 @@ $ref: '#/components/schemas/items' command: $ref: '#/components/schemas/command' - stats: $ref: '#/components/schemas/stats' payload: @@ -463,10 +429,8 @@ $ref: '#/components/schemas/upload_request' upload_slot: $ref: '#/components/schemas/upload_slot' - error: $ref: '#/components/schemas/error' - iq_pong: description: Test reachability of some XMPP address type: object @@ -476,10 +440,9 @@ type: type: string enum: - - result + - result xml: attribute: true - iq_result_version: description: Version query response type: object @@ -489,60 +452,56 @@ type: type: string enum: - - result + - result xml: attribute: true version: $ref: '#/components/schemas/version' - kind: description: Which kind of stanza type: string enum: - - message - - presence - - iq - + - message + - presence + - iq type: description: Stanza type type: string enum: - - chat - - normal - - headline - - groupchat - - get - - set - - result - - available - - unavailable - - subscribe - - subscribed - - unsubscribe - - unsubscribed + - chat + - normal + - headline + - groupchat + - get + - set + - result + - available + - unavailable + - subscribe + - subscribed + - unsubscribe + - unsubscribed xml: attribute: true - to: - description: recipient + description: the intended recipient for the stanza example: alice@example.com + format: xmpp-jid type: string xml: attribute: true - from: - description: the sender - example: bob@localhost.example + description: the sender of the stanza + example: bob@example.net + format: xmpp-jid type: string xml: attribute: true - id: description: Reasonably unique id. mod_rest generates one if left out. type: string xml: attribute: true - lang: description: Language code example: en @@ -550,17 +509,14 @@ prefix: xml attribute: true type: string - body: description: Human-readable chat message example: Hello, World! type: string - subject: description: Subject of message or group chat example: Talking about stuff type: string - thread: description: Message thread identifier properties: @@ -572,26 +528,22 @@ type: string xml: text: true - show: description: indicator of availability, ie away or not type: string enum: - - away - - chat - - dnd - - xa - + - away + - chat + - dnd + - xa status: description: Textual status message. type: string - priority: description: Presence priority type: integer maximum: 127 minimum: -128 - state: description: Chat state notifications, e.g. "is typing..." type: string @@ -599,30 +551,27 @@ namespace: http://jabber.org/protocol/chatstates x_name_is_value: true enum: - - active - - inactive - - gone - - composing - - paused + - active + - inactive + - gone + - composing + - paused example: composing - nick: type: string description: Nickname of the sender xml: name: nick namespace: http://jabber.org/protocol/nick - delay: type: string format: date-time - description: Timestamp of when a stanza was delayed, in ISO 8601 / XEP-0082 - format. + description: Timestamp of when a stanza was delayed, in ISO 8601 / XEP-0082 format. + title: 'XEP-0203: Delayed Delivery' xml: name: delay namespace: urn:xmpp:delay x_single_attribute: stamp - replace: type: string description: ID of message being replaced (e.g. for corrections) @@ -630,7 +579,6 @@ name: replace namespace: urn:xmpp:message-correct:0 x_single_attribute: id - muc: description: Multi-User-Chat related type: object @@ -661,14 +609,12 @@ format: date-time xml: attribute: true - - invite: description: Invite to a group chat - title: "XEP-0249: Direct MUC Invitations" + title: 'XEP-0249: Direct MUC Invitations' type: object required: - - jid + - jid xml: name: x namespace: jabber:x:conference @@ -698,21 +644,18 @@ description: Whether the group chat continues a one-to-one chat xml: attribute: true - html: description: HTML version of 'body' example: <body><p>Hello!</p></body> type: string - ping: description: A ping. type: boolean enum: - - true + - true xml: name: ping namespace: urn:xmpp:ping - version: type: object description: Software version query @@ -727,116 +670,111 @@ type: string example: Linux required: - - name - - version + - name + - version xml: name: query namespace: jabber:iq:version - disco: description: Discover supported features oneOf: - - description: A full response - type: object - properties: - features: - description: List of URIs indicating supported features - type: array - items: + - description: A full response + type: object + properties: + features: + description: List of URIs indicating supported features + type: array + items: + type: string + identities: + description: List of abstract identities or types that describe the entity + type: array + example: + - name: Prosody + type: im + category: server + items: + type: object + properties: + name: + type: string + type: + type: string + category: + type: string + node: type: string - identities: - description: List of abstract identities or types that describe the - entity - type: array - example: - - name: Prosody - type: im - category: server - items: + extensions: type: object - properties: - name: - type: string - type: - type: string - category: - type: string - node: - type: string - extensions: - type: object - - description: A query with a node, or an empty response with a node - type: string - - description: Either a query, or an empty response - type: boolean - + - description: A query with a node, or an empty response with a node + type: string + - description: Either a query, or an empty response + type: boolean items: description: List of references to other entities oneOf: - - description: List of items referenced - type: array - items: - properties: - jid: - type: string - description: Address of item - node: - type: string - name: - type: string - description: Descriptive name - required: - - jid - type: object - - type: string - description: A query with a node, or an empty reply list with a node - - description: An items query or empty list - type: boolean - enum: - - true - + - description: List of items referenced + type: array + items: + properties: + jid: + type: string + description: Address of item + node: + type: string + name: + type: string + description: Descriptive name + required: + - jid + type: object + - type: string + description: A query with a node, or an empty reply list with a node + - description: An items query or empty list + type: boolean + enum: + - true command: description: Ad-hoc commands. oneOf: - - type: object - properties: - data: - $ref: '#/components/schemas/formdata' - action: - type: string - note: - type: object - properties: - text: - type: string - type: - type: string - enum: - - info - - warn - - error - form: - $ref: '#/components/schemas/dataform' - sessionid: - type: string - status: - type: string - node: - type: string - actions: - type: object - properties: - complete: - type: boolean - prev: - type: boolean - next: - type: boolean - execute: - type: string - - type: string - description: Call a command by 'node' id, without arguments - + - type: object + properties: + data: + $ref: '#/components/schemas/formdata' + action: + type: string + note: + type: object + properties: + text: + type: string + type: + type: string + enum: + - info + - warn + - error + form: + $ref: '#/components/schemas/dataform' + sessionid: + type: string + status: + type: string + node: + type: string + actions: + type: object + properties: + complete: + type: boolean + prev: + type: boolean + next: + type: boolean + execute: + type: string + - type: string + description: Call a command by 'node' id, without arguments oob: type: object description: Reference a media file @@ -852,7 +790,6 @@ desc: description: Optional description type: string - payload: title: 'XEP-0335: JSON Containers' description: A piece of arbitrary JSON with a type field attached @@ -870,7 +807,6 @@ datatype: example: urn:example:my-json#payload type: string - rsm: title: 'XEP-0059: Result Set Management' xml: @@ -892,7 +828,6 @@ type: string first: type: string - archive_query: title: 'XEP-0313: Message Archive Management' type: object @@ -908,7 +843,6 @@ xml: name: query namespace: urn:xmpp:mam:2 - archive_result: title: 'XEP-0313: Message Archive Management' xml: @@ -922,7 +856,6 @@ attribute: true forward: $ref: '#/components/schemas/forwarded' - forwarded: title: 'XEP-0297: Stanza Forwarding' xml: @@ -934,7 +867,6 @@ $ref: '#/components/schemas/message' delay: $ref: '#/components/schemas/delay' - dataform: description: Data form type: object @@ -952,10 +884,10 @@ value: description: Field value oneOf: - - type: string - - type: array - items: - type: string + - type: string + - type: array + items: + type: string type: description: Type of form field type: string @@ -974,23 +906,21 @@ type: type: string enum: - - form - - submit - - cancel - - result + - form + - submit + - cancel + - result instructions: type: string - formdata: description: Simplified data form carrying only values type: object additionalProperties: oneOf: - - type: string - - type: array - items: - type: string - + - type: string + - type: array + items: + type: string stats: description: Statistics type: array @@ -1013,7 +943,6 @@ type: string xml: attribute: true - lastactivity: type: object xml: @@ -1029,7 +958,6 @@ type: string xml: text: true - caps: type: object xml: @@ -1052,7 +980,6 @@ type: string xml: attribute: true - vcard_update: type: object xml: @@ -1062,7 +989,6 @@ photo: type: string example: adc83b19e793491b1c6ea0fd8b46cd9f32e592fc - reactions: type: object xml: @@ -1081,35 +1007,31 @@ xml: wrapped: false name: reactions - occupant_id: type: string xml: namespace: urn:xmpp:occupant-id:0 x_single_attribute: id name: occupant-id - attach_to: type: string xml: namespace: urn:xmpp:message-attaching:1 x_single_attribute: id name: attach-to - fallback: type: boolean xml: namespace: urn:xmpp:fallback:0 x_name_is_value: true name: fallback - stanza_ids: type: array items: type: object required: - - id - - by + - id + - by xml: namespace: urn:xmpp:sid:0 name: stanza-id @@ -1123,7 +1045,6 @@ attribute: true format: xmpp-jid type: string - reference: type: object xml: @@ -1149,9 +1070,8 @@ attribute: true type: string required: - - type - - uri - + - type + - uri reply: title: 'XEP-0461: Message Replies' description: Reference a message being replied to @@ -1168,20 +1088,17 @@ type: string xml: attribute: true - markable: type: boolean xml: namespace: urn:xmpp:chat-markers:0 x_name_is_value: true - displayed: type: string description: Message ID of a message that has been displayed xml: namespace: urn:xmpp:chat-markers:0 x_single_attribute: id - idle_since: type: string xml: @@ -1189,7 +1106,6 @@ x_single_attribute: since name: idle format: date-time - gateway: type: object xml: @@ -1202,7 +1118,6 @@ type: string jid: type: string - extdisco: type: object xml: @@ -1219,8 +1134,8 @@ xml: name: service required: - - type - - host + - type + - host properties: transport: xml: @@ -1260,7 +1175,6 @@ attribute: true type: string type: array - register: type: object description: Register with a service @@ -1313,9 +1227,8 @@ name: type: string required: - - username - - password - + - username + - password upload_slot: type: object xml: @@ -1335,17 +1248,17 @@ items: type: object required: - - name - - value + - name + - value xml: name: header properties: name: type: string enum: - - Authorization - - Cookie - - Expires + - Authorization + - Cookie + - Expires xml: attribute: true value: @@ -1363,8 +1276,8 @@ upload_request: type: object required: - - filename - - size + - filename + - size xml: name: request namespace: urn:xmpp:http:upload:0 @@ -1381,7 +1294,6 @@ type: integer xml: attribute: true - encryption: title: 'XEP-0380: Explicit Message Encryption' type: string @@ -1389,7 +1301,6 @@ x_single_attribute: namespace name: encryption namespace: urn:xmpp:eme:0 - error: description: Description of something gone wrong. See the Stanza Errors section in RFC 6120. type: object @@ -1398,22 +1309,48 @@ description: General category of error type: string enum: - - auth - - cancel - - continue - - modify - - wait + - auth + - cancel + - continue + - modify + - wait condition: description: Specific error condition. type: string - # enum: [ full list available in RFC 6120 ] + enum: + - bad-request + - conflict + - feature-not-implemented + - forbidden + - gone + - internal-server-error + - item-not-found + - jid-malformed + - not-acceptable + - not-allowed + - not-authorized + - policy-violation + - recipient-unavailable + - redirect + - registration-required + - remote-server-not-found + - remote-server-timeout + - resource-constraint + - service-unavailable + - subscription-required + - undefined-condition + - unexpected-request code: description: Legacy numeric error code. Similar to HTTP status codes. type: integer text: description: Description of error intended for human eyes. type: string - + by: + description: Originator of the error, when different from the stanza @from attribute + type: string + xml: + attribute: true securitySchemes: token: description: Tokens from mod_http_oauth2. @@ -1435,7 +1372,6 @@ prosody:user: Regular user privileges prosody:admin: Administrator privileges prosody:operator: Server operator privileges - requestBodies: common: required: true @@ -1449,7 +1385,6 @@ application/x-www-form-urlencoded: schema: description: A subset of the JSON schema, only top level string fields. - responses: success: description: The stanza was sent and returned a response. @@ -1471,9 +1406,7 @@ example: Hello type: string sent: - description: The stanza was sent without problem, and without response, - so an empty reply. - + description: The stanza was sent without problem, and without response, so an empty reply. parameters: to: name: to @@ -1493,5 +1426,3 @@ required: true schema: $ref: '#/components/schemas/type' - -...
--- a/mod_rest/res/schema-xmpp.json Fri May 26 02:15:45 2023 +0700 +++ b/mod_rest/res/schema-xmpp.json Sun Jul 09 01:31:29 2023 +0700 @@ -108,6 +108,7 @@ } }, "delay" : { + "description" : "Timestamp of when a stanza was delayed, in ISO 8601 / XEP-0082 format.", "format" : "date-time", "title" : "XEP-0203: Delayed Delivery", "type" : "string", @@ -204,7 +205,7 @@ }, "to" : { "description" : "the intended recipient for the stanza", - "example" : "alice@another.example", + "example" : "alice@example.com", "format" : "xmpp-jid", "type" : "string", "xml" : { @@ -697,6 +698,12 @@ "forward" : { "$ref" : "#/properties/message/properties/forwarded" }, + "id" : { + "type" : "string", + "xml" : { + "attribute" : true + } + }, "queryid" : { "type" : "string", "xml" : {
--- a/mod_restrict_xmpp/mod_restrict_xmpp.lua Fri May 26 02:15:45 2023 +0700 +++ b/mod_restrict_xmpp/mod_restrict_xmpp.lua Sun Jul 09 01:31:29 2023 +0700 @@ -3,7 +3,18 @@ local set = require "util.set"; local st = require "util.stanza"; -module:default_permission("prosody:user", "xmpp:federate"); +local normal_user_role = "prosody:registered"; +local limited_user_role = "prosody:guest"; + +local features = require "core.features"; + +-- COMPAT +if not features.available:contains("split-user-roles") then + normal_user_role = "prosody:user"; + limited_user_role = "prosody:restricted"; +end + +module:default_permission(normal_user_role, "xmpp:federate"); module:hook("route/remote", function (event) if not module:may("xmpp:federate", event) then if event.stanza.attr.type ~= "result" and event.stanza.attr.type ~= "error" then @@ -93,12 +104,12 @@ --module:default_permission("prosody:restricted", "xmpp:account:read"); --module:default_permission("prosody:restricted", "xmpp:account:write"); -module:default_permission("prosody:restricted", "xmpp:account:messages:read"); -module:default_permission("prosody:restricted", "xmpp:account:messages:write"); +module:default_permission(limited_user_role, "xmpp:account:messages:read"); +module:default_permission(limited_user_role, "xmpp:account:messages:write"); for _, property_list in ipairs({ iq_namespaces, legacy_storage_nodes, pep_nodes }) do for account_property in set.new(array.collect(it.values(property_list))) do - module:default_permission("prosody:restricted", "xmpp:account:"..account_property..":read"); - module:default_permission("prosody:restricted", "xmpp:account:"..account_property..":write"); + module:default_permission(limited_user_role, "xmpp:account:"..account_property..":read"); + module:default_permission(limited_user_role, "xmpp:account:"..account_property..":write"); end end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_s2sout_override/README.md Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,41 @@ +--- +summary: Override s2s connection targets +--- + +This module replaces [mod_s2soutinjection] and uses more modern and +reliable methods for overriding connection targets. + +# Configuration + +Enable the module as usual, then specify a map of XMPP remote hostnames +to URIs like `"tcp://host.example:port"`, to have Prosody connect there +instead of doing normal DNS SRV resolution. + +Currently supported schemes are `tcp://` and `tls://`. A future version +could support more methods including alternate SRV lookup targets or +even UNIX sockets. + +URIs with IP addresses like `tcp://127.0.0.1:9999` will bypass A/AAAA +DNS lookups. + +```lua +-- Global section +modules_enabled = { + -- other global modules + "s2sout_override"; +} + +s2sout_override = { + ["example.com"] = "tcp://other.host.example:5299"; + ["xmpp.example.net"] = "tcp://localhost:5999"; + ["secure.example"] = = "tls://127.0.0.1:5270"; +} +``` + +# Compatibility + +Prosody version status +--------------- ---------- +0.12.4 Will work +0.12.3 Will not work +0.11 Will not work
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_s2sout_override/mod_s2sout_override.lua Sun Jul 09 01:31:29 2023 +0700 @@ -0,0 +1,19 @@ +--% requires: s2sout-pre-connect-event + +local url = require"socket.url"; +local basic_resolver = require "net.resolvers.basic"; + +local override_for = module:get_option(module.name, {}); -- map of host to "tcp://example.com:5269" + +module:hook("s2sout-pre-connect", function(event) + local override = override_for[event.session.to_host]; + if type(override) == "string" then + override = url.parse(override); + end + if type(override) == "table" and override.scheme == "tcp" and type(override.host) == "string" then + event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5269, override.scheme, {}); + elseif type(override) == "table" and override.scheme == "tls" and type(override.host) == "string" then + event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5270, "tcp", + { servername = event.session.to_host; sslctx = event.session.ssl_ctx }); + end +end);