Software / code / prosody
Comparison
plugins/mod_register.lua @ 8484:f591855f060d
mod_register: Split into mod_register_ibr and mod_user_account_management (#723)
- mod_register_ibr handles in-band registration
- mod_user_account_management handles password change and user deletion
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Sat, 07 Oct 2017 22:00:50 +0200 |
| parent | 8464:1a0b76b07b7a |
| child | 8485:0e02c6de5c02 |
comparison
equal
deleted
inserted
replaced
| 8483:6d47b74926dd | 8484:f591855f060d |
|---|---|
| 5 -- This project is MIT/X11 licensed. Please see the | 5 -- This project is MIT/X11 licensed. Please see the |
| 6 -- COPYING file in the source package for more information. | 6 -- COPYING file in the source package for more information. |
| 7 -- | 7 -- |
| 8 | 8 |
| 9 | 9 |
| 10 local st = require "util.stanza"; | 10 local allow_registration = module:get_option_boolean("allow_registration", false); |
| 11 local dataform_new = require "util.dataforms".new; | |
| 12 local usermanager_user_exists = require "core.usermanager".user_exists; | |
| 13 local usermanager_create_user = require "core.usermanager".create_user; | |
| 14 local usermanager_set_password = require "core.usermanager".set_password; | |
| 15 local usermanager_delete_user = require "core.usermanager".delete_user; | |
| 16 local nodeprep = require "util.encodings".stringprep.nodeprep; | |
| 17 local jid_bare = require "util.jid".bare; | |
| 18 local create_throttle = require "util.throttle".create; | |
| 19 local new_cache = require "util.cache".new; | |
| 20 local ip_util = require "util.ip"; | |
| 21 local new_ip = ip_util.new_ip; | |
| 22 local match_ip = ip_util.match; | |
| 23 local parse_cidr = ip_util.parse_cidr; | |
| 24 | 11 |
| 25 local compat = module:get_option_boolean("registration_compat", true); | 12 if allow_registration then |
| 26 local allow_registration = module:get_option_boolean("allow_registration", false); | 13 module:depends("register_ibr"); |
| 27 local additional_fields = module:get_option("additional_registration_fields", {}); | |
| 28 local require_encryption = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); | |
| 29 | |
| 30 local account_details = module:open_store("account_details"); | |
| 31 | |
| 32 local field_map = { | |
| 33 username = { name = "username", type = "text-single", label = "Username", required = true }; | |
| 34 password = { name = "password", type = "text-private", label = "Password", required = true }; | |
| 35 nick = { name = "nick", type = "text-single", label = "Nickname" }; | |
| 36 name = { name = "name", type = "text-single", label = "Full Name" }; | |
| 37 first = { name = "first", type = "text-single", label = "Given Name" }; | |
| 38 last = { name = "last", type = "text-single", label = "Family Name" }; | |
| 39 email = { name = "email", type = "text-single", label = "Email" }; | |
| 40 address = { name = "address", type = "text-single", label = "Street" }; | |
| 41 city = { name = "city", type = "text-single", label = "City" }; | |
| 42 state = { name = "state", type = "text-single", label = "State" }; | |
| 43 zip = { name = "zip", type = "text-single", label = "Postal code" }; | |
| 44 phone = { name = "phone", type = "text-single", label = "Telephone number" }; | |
| 45 url = { name = "url", type = "text-single", label = "Webpage" }; | |
| 46 date = { name = "date", type = "text-single", label = "Birth date" }; | |
| 47 }; | |
| 48 | |
| 49 local title = module:get_option_string("registration_title", | |
| 50 "Creating a new account"); | |
| 51 local instructions = module:get_option_string("registration_instructions", | |
| 52 "Choose a username and password for use with this service."); | |
| 53 | |
| 54 local registration_form = dataform_new{ | |
| 55 title = title; | |
| 56 instructions = instructions; | |
| 57 | |
| 58 field_map.username; | |
| 59 field_map.password; | |
| 60 }; | |
| 61 | |
| 62 local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"}) | |
| 63 :tag("instructions"):text(instructions):up() | |
| 64 :tag("username"):up() | |
| 65 :tag("password"):up(); | |
| 66 | |
| 67 for _, field in ipairs(additional_fields) do | |
| 68 if type(field) == "table" then | |
| 69 registration_form[#registration_form + 1] = field; | |
| 70 elseif field_map[field] or field_map[field:sub(1, -2)] then | |
| 71 if field:match("%+$") then | |
| 72 field = field:sub(1, -2); | |
| 73 field_map[field].required = true; | |
| 74 end | |
| 75 | |
| 76 registration_form[#registration_form + 1] = field_map[field]; | |
| 77 registration_query:tag(field):up(); | |
| 78 else | |
| 79 module:log("error", "Unknown field %q", field); | |
| 80 end | |
| 81 end | |
| 82 registration_query:add_child(registration_form:form()); | |
| 83 | |
| 84 module:add_feature("jabber:iq:register"); | |
| 85 | |
| 86 local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); | |
| 87 module:hook("stream-features", function(event) | |
| 88 local session, features = event.origin, event.features; | |
| 89 | |
| 90 -- Advertise registration to unauthorized clients only. | |
| 91 if not(allow_registration) or session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then | |
| 92 return | |
| 93 end | |
| 94 | |
| 95 features:add_child(register_stream_feature); | |
| 96 end); | |
| 97 | |
| 98 -- Password change and account deletion handler | |
| 99 local function handle_registration_stanza(event) | |
| 100 local session, stanza = event.origin, event.stanza; | |
| 101 local log = session.log or module._log; | |
| 102 | |
| 103 local query = stanza.tags[1]; | |
| 104 if stanza.attr.type == "get" then | |
| 105 local reply = st.reply(stanza); | |
| 106 reply:tag("query", {xmlns = "jabber:iq:register"}) | |
| 107 :tag("registered"):up() | |
| 108 :tag("username"):text(session.username):up() | |
| 109 :tag("password"):up(); | |
| 110 session.send(reply); | |
| 111 else -- stanza.attr.type == "set" | |
| 112 if query.tags[1] and query.tags[1].name == "remove" then | |
| 113 local username, host = session.username, session.host; | |
| 114 | |
| 115 -- This one weird trick sends a reply to this stanza before the user is deleted | |
| 116 local old_session_close = session.close; | |
| 117 session.close = function(self, ...) | |
| 118 self.send(st.reply(stanza)); | |
| 119 return old_session_close(self, ...); | |
| 120 end | |
| 121 | |
| 122 local ok, err = usermanager_delete_user(username, host); | |
| 123 | |
| 124 if not ok then | |
| 125 log("debug", "Removing user account %s@%s failed: %s", username, host, err); | |
| 126 session.close = old_session_close; | |
| 127 session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); | |
| 128 return true; | |
| 129 end | |
| 130 | |
| 131 log("info", "User removed their account: %s@%s", username, host); | |
| 132 module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); | |
| 133 else | |
| 134 local username = nodeprep(query:get_child_text("username")); | |
| 135 local password = query:get_child_text("password"); | |
| 136 if username and password then | |
| 137 if username == session.username then | |
| 138 if usermanager_set_password(username, password, session.host, session.resource) then | |
| 139 session.send(st.reply(stanza)); | |
| 140 else | |
| 141 -- TODO unable to write file, file may be locked, etc, what's the correct error? | |
| 142 session.send(st.error_reply(stanza, "wait", "internal-server-error")); | |
| 143 end | |
| 144 else | |
| 145 session.send(st.error_reply(stanza, "modify", "bad-request")); | |
| 146 end | |
| 147 else | |
| 148 session.send(st.error_reply(stanza, "modify", "bad-request")); | |
| 149 end | |
| 150 end | |
| 151 end | |
| 152 return true; | |
| 153 end | 14 end |
| 154 | 15 |
| 155 module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); | 16 module:depends("user_account_management"); |
| 156 if compat then | |
| 157 module:hook("iq/host/jabber:iq:register:query", function (event) | |
| 158 local session, stanza = event.origin, event.stanza; | |
| 159 if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then | |
| 160 return handle_registration_stanza(event); | |
| 161 end | |
| 162 end); | |
| 163 end | |
| 164 | |
| 165 local function parse_response(query) | |
| 166 local form = query:get_child("x", "jabber:x:data"); | |
| 167 if form then | |
| 168 return registration_form:data(form); | |
| 169 else | |
| 170 local data = {}; | |
| 171 local errors = {}; | |
| 172 for _, field in ipairs(registration_form) do | |
| 173 local name, required = field.name, field.required; | |
| 174 if field_map[name] then | |
| 175 data[name] = query:get_child_text(name); | |
| 176 if (not data[name] or #data[name] == 0) and required then | |
| 177 errors[name] = "Required value missing"; | |
| 178 end | |
| 179 end | |
| 180 end | |
| 181 if next(errors) then | |
| 182 return data, errors; | |
| 183 end | |
| 184 return data; | |
| 185 end | |
| 186 end | |
| 187 | |
| 188 local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations"); | |
| 189 local whitelist_only = module:get_option_boolean("whitelist_registration_only"); | |
| 190 local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1", "::1" })._items; | |
| 191 local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items; | |
| 192 | |
| 193 local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1); | |
| 194 local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations); | |
| 195 local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100); | |
| 196 local blacklist_overflow = module:get_option_boolean("blacklist_on_registration_throttle_overload", false); | |
| 197 | |
| 198 local throttle_cache = new_cache(throttle_cache_size, blacklist_overflow and function (ip, throttle) | |
| 199 if not throttle:peek() then | |
| 200 module:log("info", "Adding ip %s to registration blacklist", ip); | |
| 201 blacklisted_ips[ip] = true; | |
| 202 end | |
| 203 end or nil); | |
| 204 | |
| 205 local function check_throttle(ip) | |
| 206 if not throttle_max then return true end | |
| 207 local throttle = throttle_cache:get(ip); | |
| 208 if not throttle then | |
| 209 throttle = create_throttle(throttle_max, throttle_period); | |
| 210 end | |
| 211 throttle_cache:set(ip, throttle); | |
| 212 return throttle:poll(1); | |
| 213 end | |
| 214 | |
| 215 local function ip_in_set(set, ip) | |
| 216 if set[ip] then | |
| 217 return true; | |
| 218 end | |
| 219 ip = new_ip(ip); | |
| 220 for in_set in pairs(set) do | |
| 221 if match_ip(ip, parse_cidr(in_set)) then | |
| 222 return true; | |
| 223 end | |
| 224 end | |
| 225 return false; | |
| 226 end | |
| 227 | |
| 228 -- In-band registration | |
| 229 module:hook("stanza/iq/jabber:iq:register:query", function(event) | |
| 230 local session, stanza = event.origin, event.stanza; | |
| 231 local log = session.log or module._log; | |
| 232 | |
| 233 if not(allow_registration) or session.type ~= "c2s_unauthed" then | |
| 234 log("debug", "Attempted registration when disabled or already authenticated"); | |
| 235 session.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
| 236 elseif require_encryption and not session.secure then | |
| 237 session.send(st.error_reply(stanza, "modify", "policy-violation", "Encryption is required")); | |
| 238 else | |
| 239 local query = stanza.tags[1]; | |
| 240 if stanza.attr.type == "get" then | |
| 241 local reply = st.reply(stanza); | |
| 242 reply:add_child(registration_query); | |
| 243 session.send(reply); | |
| 244 elseif stanza.attr.type == "set" then | |
| 245 if query.tags[1] and query.tags[1].name == "remove" then | |
| 246 session.send(st.error_reply(stanza, "auth", "registration-required")); | |
| 247 else | |
| 248 local data, errors = parse_response(query); | |
| 249 if errors then | |
| 250 log("debug", "Error parsing registration form:"); | |
| 251 for field, err in pairs(errors) do | |
| 252 log("debug", "Field %q: %s", field, err); | |
| 253 end | |
| 254 session.send(st.error_reply(stanza, "modify", "not-acceptable")); | |
| 255 else | |
| 256 -- Check that the user is not blacklisted or registering too often | |
| 257 if not session.ip then | |
| 258 log("debug", "User's IP not known; can't apply blacklist/whitelist"); | |
| 259 elseif ip_in_set(blacklisted_ips, session.ip) or (whitelist_only and not ip_in_set(whitelisted_ips, session.ip)) then | |
| 260 session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account.")); | |
| 261 return true; | |
| 262 elseif throttle_max and not ip_in_set(whitelisted_ips, session.ip) then | |
| 263 if not check_throttle(session.ip) then | |
| 264 log("debug", "Registrations over limit for ip %s", session.ip or "?"); | |
| 265 session.send(st.error_reply(stanza, "wait", "not-acceptable")); | |
| 266 return true; | |
| 267 end | |
| 268 end | |
| 269 local username, password = nodeprep(data.username), data.password; | |
| 270 data.username, data.password = nil, nil; | |
| 271 local host = module.host; | |
| 272 if not username or username == "" then | |
| 273 log("debug", "The requested username is invalid."); | |
| 274 session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); | |
| 275 return true; | |
| 276 end | |
| 277 local user = { username = username , host = host, additional = data, ip = session.ip, session = session, allowed = true } | |
| 278 module:fire_event("user-registering", user); | |
| 279 if not user.allowed then | |
| 280 log("debug", "Registration disallowed by module"); | |
| 281 session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden.")); | |
| 282 elseif usermanager_user_exists(username, host) then | |
| 283 log("debug", "Attempt to register with existing username"); | |
| 284 session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); | |
| 285 else | |
| 286 -- TODO unable to write file, file may be locked, etc, what's the correct error? | |
| 287 local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk."); | |
| 288 if usermanager_create_user(username, password, host) then | |
| 289 data.registered = os.time(); | |
| 290 if not account_details:set(username, data) then | |
| 291 log("debug", "Could not store extra details"); | |
| 292 usermanager_delete_user(username, host); | |
| 293 session.send(error_reply); | |
| 294 return true; | |
| 295 end | |
| 296 session.send(st.reply(stanza)); -- user created! | |
| 297 log("info", "User account created: %s@%s", username, host); | |
| 298 module:fire_event("user-registered", { | |
| 299 username = username, host = host, source = "mod_register", | |
| 300 session = session }); | |
| 301 else | |
| 302 log("debug", "Could not create user"); | |
| 303 session.send(error_reply); | |
| 304 end | |
| 305 end | |
| 306 end | |
| 307 end | |
| 308 end | |
| 309 end | |
| 310 return true; | |
| 311 end); |