Software / code / prosody
Comparison
plugins/muc/register.lib.lua @ 9240:f9a83aca4421
MUC: Add support for registering with a MUC, including reserving a nickname as per XEP-0045
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Mon, 03 Sep 2018 12:26:25 +0100 |
| child | 9262:37b7cf3470f1 |
comparison
equal
deleted
inserted
replaced
| 9239:03e37f7d6c97 | 9240:f9a83aca4421 |
|---|---|
| 1 local jid_bare = require "util.jid".bare; | |
| 2 local jid_resource = require "util.jid".resource; | |
| 3 local resourceprep = require "util.encodings".stringprep.resourceprep; | |
| 4 local st = require "util.stanza"; | |
| 5 local dataforms = require "util.dataforms"; | |
| 6 | |
| 7 local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", false); | |
| 8 | |
| 9 local enforce_nick = module:get_option_boolean("enforce_registered_nickname", false); | |
| 10 | |
| 11 -- reserved_nicks[nick] = jid | |
| 12 local function get_reserved_nicks(room) | |
| 13 if room._reserved_nicks then | |
| 14 return room._reserved_nicks; | |
| 15 end | |
| 16 module:log("debug", "Refreshing reserved nicks..."); | |
| 17 local reserved_nicks = {}; | |
| 18 for jid in room:each_affiliation() do | |
| 19 local data = room._affiliation_data[jid]; | |
| 20 local nick = data and data.reserved_nickname; | |
| 21 module:log("debug", "Refreshed for %s: %s", jid, nick); | |
| 22 if nick then | |
| 23 reserved_nicks[nick] = jid; | |
| 24 end | |
| 25 end | |
| 26 room._reserved_nicks = reserved_nicks; | |
| 27 return reserved_nicks; | |
| 28 end | |
| 29 | |
| 30 -- Returns the registered nick, if any, for a JID | |
| 31 -- Note: this is just the *nick* part, i.e. the resource of the in-room JID | |
| 32 local function get_registered_nick(room, jid) | |
| 33 local registered_data = room._affiliation_data[jid]; | |
| 34 if not registered_data then | |
| 35 return; | |
| 36 end | |
| 37 return registered_data.reserved_nickname; | |
| 38 end | |
| 39 | |
| 40 -- Returns the JID, if any, that registered a nick (not in-room JID) | |
| 41 local function get_registered_jid(room, nick) | |
| 42 local reserved_nicks = get_reserved_nicks(room); | |
| 43 return reserved_nicks[nick]; | |
| 44 end | |
| 45 | |
| 46 module:hook("muc-set-affiliation", function (event) | |
| 47 -- Clear reserved nick cache | |
| 48 event.room._reserved_nicks = nil; | |
| 49 end); | |
| 50 | |
| 51 module:add_feature("jabber:iq:register"); | |
| 52 | |
| 53 module:hook("muc-disco#info", function (event) | |
| 54 event.reply:tag("feature", { var = "jabber:iq:register" }):up(); | |
| 55 end); | |
| 56 | |
| 57 local registration_form = dataforms.new { | |
| 58 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#register" }, | |
| 59 { name = "muc#register_roomnick", type = "text-single", label = "Nickname"}, | |
| 60 }; | |
| 61 | |
| 62 local function enforce_nick_policy(event) | |
| 63 local origin, stanza = event.origin, event.stanza; | |
| 64 local room = assert(event.room); -- FIXME | |
| 65 if not room then return; end | |
| 66 | |
| 67 -- Check if the chosen nickname is reserved | |
| 68 local requested_nick = jid_resource(stanza.attr.to); | |
| 69 local reserved_by = get_registered_jid(room, requested_nick); | |
| 70 if reserved_by and reserved_by ~= jid_bare(stanza.attr.from) then | |
| 71 module:log("debug", "%s attempted to use nick %s reserved by %s", stanza.attr.from, requested_nick, reserved_by); | |
| 72 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); | |
| 73 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 74 return true; | |
| 75 end | |
| 76 | |
| 77 -- Check if the occupant has a reservation they must use | |
| 78 if enforce_nick then | |
| 79 local nick = get_registered_nick(room, jid_bare(stanza.attr.from)); | |
| 80 if nick then | |
| 81 if event.occupant then | |
| 82 event.occupant.nick = jid_bare(event.occupant.nick) .. "/" .. nick; | |
| 83 elseif event.dest_occupant.nick ~= jid_bare(event.dest_occupant.nick) .. "/" .. nick then | |
| 84 module:log("debug", "Attempt by %s to join as %s, but their reserved nick is %s", stanza.attr.from, requested_nick, nick); | |
| 85 local reply = st.error_reply(stanza, "cancel", "not-acceptable"):up(); | |
| 86 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 87 return true; | |
| 88 end | |
| 89 end | |
| 90 end | |
| 91 end | |
| 92 | |
| 93 module:hook("muc-occupant-pre-join", enforce_nick_policy); | |
| 94 module:hook("muc-occupant-pre-change", enforce_nick_policy); | |
| 95 | |
| 96 -- Discovering Reserved Room Nickname | |
| 97 -- http://xmpp.org/extensions/xep-0045.html#reservednick | |
| 98 module:hook("muc-disco#info/x-roomuser-item", function (event) | |
| 99 local nick = get_registered_nick(event.room, jid_bare(event.stanza.attr.from)); | |
| 100 if nick then | |
| 101 event.reply:tag("identity", { category = "conference", type = "text", name = nick }) | |
| 102 end | |
| 103 end); | |
| 104 | |
| 105 local function handle_register_iq(room, origin, stanza) | |
| 106 local user_jid = jid_bare(stanza.attr.from) | |
| 107 local affiliation = room:get_affiliation(user_jid); | |
| 108 if affiliation == "outcast" then | |
| 109 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 110 return true; | |
| 111 elseif not (affiliation or allow_unaffiliated) then | |
| 112 origin.send(st.error_reply(stanza, "auth", "registration-required")); | |
| 113 return true; | |
| 114 end | |
| 115 local reply = st.reply(stanza); | |
| 116 local registered_nick = get_registered_nick(room, user_jid); | |
| 117 if stanza.attr.type == "get" then | |
| 118 reply:query("jabber:iq:register"); | |
| 119 if registered_nick then | |
| 120 reply:tag("registered"):up(); | |
| 121 reply:tag("username"):text(registered_nick); | |
| 122 origin.send(reply); | |
| 123 return true; | |
| 124 end | |
| 125 reply:add_child(registration_form:form()); | |
| 126 else -- type == set -- handle registration form | |
| 127 local query = stanza.tags[1]; | |
| 128 if query:get_child("remove") then | |
| 129 -- Remove "member" affiliation, but preserve if any other | |
| 130 local new_affiliation = affiliation ~= "member" and affiliation; | |
| 131 local ok, err_type, err_condition = room:set_affiliation(true, user_jid, new_affiliation, nil, false); | |
| 132 if not ok then | |
| 133 origin.send(st.error_reply(stanza, err_type, err_condition)); | |
| 134 return true; | |
| 135 end | |
| 136 origin.send(reply); | |
| 137 return true; | |
| 138 end | |
| 139 local form_tag = query:get_child("x", "jabber:x:data"); | |
| 140 local reg_data = form_tag and registration_form:data(form_tag); | |
| 141 if not reg_data then | |
| 142 origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form")); | |
| 143 return true; | |
| 144 end | |
| 145 -- Is the nickname valid? | |
| 146 local desired_nick = resourceprep(reg_data["muc#register_roomnick"]); | |
| 147 if not desired_nick then | |
| 148 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid Nickname")); | |
| 149 return true; | |
| 150 end | |
| 151 -- Is the nickname currently in use by another user? | |
| 152 local current_occupant = room:get_occupant_by_nick(room.jid.."/"..desired_nick); | |
| 153 if current_occupant and current_occupant.bare_jid ~= user_jid then | |
| 154 origin.send(st.error_reply(stanza, "cancel", "conflict")); | |
| 155 return true; | |
| 156 end | |
| 157 -- Is the nickname currently reserved by another user? | |
| 158 local reserved_by = get_registered_jid(room, desired_nick); | |
| 159 if reserved_by and reserved_by ~= user_jid then | |
| 160 origin.send(st.error_reply(stanza, "cancel", "conflict")); | |
| 161 return true; | |
| 162 end | |
| 163 | |
| 164 -- Kick any sessions that are not using this nick before we register it | |
| 165 if enforce_nick then | |
| 166 local required_room_nick = room.jid.."/"..desired_nick; | |
| 167 for room_nick, occupant in room:each_occupant() do | |
| 168 if occupant.bare_jid == user_jid and room_nick ~= required_room_nick then | |
| 169 room:set_role(true, room_nick, nil); -- Kick (TODO: would be nice to use 333 code) | |
| 170 end | |
| 171 end | |
| 172 end | |
| 173 | |
| 174 -- Checks passed, save the registration | |
| 175 if registered_nick ~= desired_nick then | |
| 176 local registration_data = { reserved_nickname = desired_nick }; | |
| 177 local ok, err_type, err_condition = room:set_affiliation(true, user_jid, "member", nil, registration_data); | |
| 178 if not ok then | |
| 179 origin.send(st.error_reply(stanza, err_type, err_condition)); | |
| 180 return true; | |
| 181 end | |
| 182 module:log("debug", "Saved nick registration for %s: %s", user_jid, desired_nick); | |
| 183 origin.send(reply); | |
| 184 return true; | |
| 185 end | |
| 186 end | |
| 187 origin.send(reply); | |
| 188 return true; | |
| 189 end | |
| 190 | |
| 191 return { | |
| 192 get_registered_nick = get_registered_nick; | |
| 193 get_registered_jid = get_registered_jid; | |
| 194 handle_register_iq = handle_register_iq; | |
| 195 } |