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);