Mercurial > prosody-modules
comparison mod_captcha_registration/modules/mod_register.lua @ 1373:985bfc6e8cad
mod_captcha_registration: initial commit
author | mrDoctorWho <mrdoctorwho@gmail.com> |
---|---|
date | Sat, 29 Mar 2014 22:56:24 +0700 |
parents | |
children | 11fdfd73a527 |
comparison
equal
deleted
inserted
replaced
1372:a573d64968e9 | 1373:985bfc6e8cad |
---|---|
1 -- Prosody IM | |
2 -- Copyright (C) 2008-2010 Matthew Wild | |
3 -- Copyright (C) 2008-2010 Waqas Hussain | |
4 -- Modifications copyright (C) 2014 mrDoctorWho | |
5 -- | |
6 -- This project is MIT/X11 licensed. Please see the | |
7 -- COPYING file in the source package for more information. | |
8 -- | |
9 | |
10 | |
11 local st = require "util.stanza"; | |
12 local dataform_new = require "util.dataforms".new; | |
13 local usermanager_user_exists = require "core.usermanager".user_exists; | |
14 local usermanager_create_user = require "core.usermanager".create_user; | |
15 local usermanager_set_password = require "core.usermanager".set_password; | |
16 local usermanager_delete_user = require "core.usermanager".delete_user; | |
17 local os_time = os.time; | |
18 local nodeprep = require "util.encodings".stringprep.nodeprep; | |
19 local jid_bare = require "util.jid".bare; | |
20 local timer = require "util.timer"; | |
21 | |
22 | |
23 local math = require "math"; | |
24 local captcha = require "captcha"; | |
25 | |
26 | |
27 | |
28 local base64 = require "util.encodings".base64.encode; | |
29 local sha1 = require "util.hashes".sha1; | |
30 | |
31 | |
32 local captcha_ids = {}; | |
33 | |
34 local config = module:get_option("captcha_config") or {}; | |
35 | |
36 | |
37 local compat = module:get_option_boolean("registration_compat", true); | |
38 local allow_registration = module:get_option_boolean("allow_registration", false); | |
39 local additional_fields = module:get_option("additional_registration_fields", {}); | |
40 | |
41 local account_details = module:open_store("account_details"); | |
42 | |
43 local field_map = { | |
44 username = { name = "username", type = "text-single", label = "Username", required = true }; | |
45 password = { name = "password", type = "text-private", label = "Password", required = true }; | |
46 nick = { name = "nick", type = "text-single", label = "Nickname" }; | |
47 name = { name = "name", type = "text-single", label = "Full Name" }; | |
48 first = { name = "first", type = "text-single", label = "Given Name" }; | |
49 last = { name = "last", type = "text-single", label = "Family Name" }; | |
50 email = { name = "email", type = "text-single", label = "Email" }; | |
51 address = { name = "address", type = "text-single", label = "Street" }; | |
52 city = { name = "city", type = "text-single", label = "City" }; | |
53 state = { name = "state", type = "text-single", label = "State" }; | |
54 zip = { name = "zip", type = "text-single", label = "Postal code" }; | |
55 phone = { name = "phone", type = "text-single", label = "Telephone number" }; | |
56 url = { name = "url", type = "text-single", label = "Webpage" }; | |
57 date = { name = "date", type = "text-single", label = "Birth date" }; | |
58 | |
59 -- something new | |
60 formtype = { name = "FORM_TYPE", type = "hidden"}; | |
61 captcha_text = { name = "captcha_text", type = "fixed", label = "Warning: "}; | |
62 captcha_psi = { name = "captchahidden", type = "hidden" }; -- Don't know exactly why, but it exists in ejabberd register form | |
63 captcha_url = { name = "url", type = "text-single", label = "Captcha url"}; | |
64 from = { name = "from", type = "hidden" }; | |
65 captcha_challenge = { name = "challenge", type = "hidden" }; | |
66 sid = { name = "sid", type = "hidden" }; | |
67 ocr = { name = "ocr", label = "Enter shown text", required = true, type = "media" } | |
68 }; | |
69 | |
70 local registration_form = dataform_new{ | |
71 | |
72 field_map.formtype; | |
73 field_map.username; | |
74 field_map.password; | |
75 field_map.captcha_text; | |
76 -- field_map.captcha_psi; -- Maybe later, i really have no idea why it used in ejabberd reg form | |
77 field_map.captcha_url; | |
78 field_map.from; | |
79 field_map.captcha_challenge; | |
80 field_map.sid; | |
81 field_map.ocr; | |
82 }; | |
83 | |
84 | |
85 function delete_captcha(cid) | |
86 os.remove(string.format("%s/%s.png", config.dir, cid)) | |
87 captcha_ids[cid] = nil; | |
88 end | |
89 | |
90 for _, field in ipairs(additional_fields) do | |
91 if type(field) == "table" then | |
92 registration_form[#registration_form + 1] = field; | |
93 else | |
94 if field:match("%+$") then | |
95 field = field:sub(1, #field - 1); | |
96 field_map[field].required = true; | |
97 end | |
98 | |
99 registration_form[#registration_form + 1] = field_map[field]; | |
100 registration_query:tag(field):up(); | |
101 end | |
102 end | |
103 | |
104 module:add_feature("jabber:iq:register"); | |
105 | |
106 local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); | |
107 module:hook("stream-features", function(event) | |
108 local session, features = event.origin, event.features; | |
109 | |
110 -- Advertise registration to unauthorized clients only. | |
111 if not(allow_registration) or session.type ~= "c2s_unauthed" then | |
112 return | |
113 end | |
114 | |
115 features:add_child(register_stream_feature); | |
116 end); | |
117 | |
118 local function handle_registration_stanza(event) | |
119 local session, stanza = event.origin, event.stanza; | |
120 | |
121 local query = stanza.tags[1]; | |
122 if stanza.attr.type == "get" then | |
123 local reply = st.reply(stanza); | |
124 reply:tag("query", {xmlns = "jabber:iq:register"}) | |
125 :tag("registered"):up() | |
126 :tag("username"):text(session.username):up() | |
127 :tag("password"):up(); | |
128 session.send(reply); | |
129 else -- stanza.attr.type == "set" | |
130 if query.tags[1] and query.tags[1].name == "remove" then | |
131 local username, host = session.username, session.host; | |
132 | |
133 local old_session_close = session.close; | |
134 session.close = function(session, ...) | |
135 session.send(st.reply(stanza)); | |
136 return old_session_close(session, ...); | |
137 end | |
138 | |
139 local ok, err = usermanager_delete_user(username, host); | |
140 | |
141 if not ok then | |
142 module:log("debug", "Removing user account %s@%s failed: %s", username, host, err); | |
143 session.close = old_session_close; | |
144 session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); | |
145 return true; | |
146 end | |
147 | |
148 module:log("info", "User removed their account: %s@%s", username, host); | |
149 module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); | |
150 else | |
151 local username = nodeprep(query:get_child("username"):get_text()); | |
152 local password = query:get_child("password"):get_text(); | |
153 if username and password then | |
154 if username == session.username then | |
155 if usermanager_set_password(username, password, session.host) then | |
156 session.send(st.reply(stanza)); | |
157 else | |
158 -- TODO unable to write file, file may be locked, etc, what's the correct error? | |
159 session.send(st.error_reply(stanza, "wait", "internal-server-error")); | |
160 end | |
161 else | |
162 session.send(st.error_reply(stanza, "modify", "bad-request")); | |
163 end | |
164 else | |
165 session.send(st.error_reply(stanza, "modify", "bad-request")); | |
166 end | |
167 end | |
168 end | |
169 return true; | |
170 end | |
171 | |
172 module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); | |
173 if compat then | |
174 module:hook("iq/host/jabber:iq:register:query", function (event) | |
175 local session, stanza = event.origin, event.stanza; | |
176 if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then | |
177 return handle_registration_stanza(event); | |
178 end | |
179 end); | |
180 end | |
181 | |
182 local function parse_response(query) | |
183 local form = query:get_child("x", "jabber:x:data"); | |
184 if form then | |
185 return registration_form:data(form); | |
186 else | |
187 local data = {}; | |
188 local errors = {}; | |
189 for _, field in ipairs(registration_form) do | |
190 local name, required = field.name, field.required; | |
191 if field_map[name] then | |
192 data[name] = query:get_child_text(name); | |
193 if (not data[name] or #data[name] == 0) and required then | |
194 errors[name] = "Required value missing"; | |
195 end | |
196 end | |
197 end | |
198 if next(errors) then | |
199 return data, errors; | |
200 end | |
201 return data; | |
202 end | |
203 end | |
204 | |
205 local recent_ips = {}; | |
206 local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); | |
207 local whitelist_only = module:get_option("whitelist_registration_only"); | |
208 local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" }; | |
209 local blacklisted_ips = module:get_option("registration_blacklist") or {}; | |
210 | |
211 for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end | |
212 for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end | |
213 | |
214 | |
215 local function get_file(name) | |
216 local file = io.open(name, "rb") | |
217 local data = file:read("*all") | |
218 file:close() | |
219 return data | |
220 end | |
221 | |
222 | |
223 local function get_captcha() | |
224 local cap = captcha.new(); | |
225 math.randomseed(os_time()); | |
226 local cid = tostring(math.random(1000, 90000)); -- random cid used for cap name | |
227 cap:font(config.font); | |
228 cap:scribble(); | |
229 captcha_ids[cid] = cap:write(string.format("%s/%s.png", config.dir, cid)):lower(); | |
230 timer.add_task(config.timeout, function() delete_captcha(cid) end); -- Add new function to use arguments. Is there any other way in lua? Or it even works? | |
231 return cid | |
232 end | |
233 | |
234 | |
235 | |
236 module:hook("stanza/iq/jabber:iq:register:query", function(event) | |
237 local session, stanza = event.origin, event.stanza; | |
238 | |
239 if not(allow_registration) or session.type ~= "c2s_unauthed" then | |
240 session.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
241 else | |
242 local query = stanza.tags[1]; | |
243 if stanza.attr.type == "get" then | |
244 local reply = st.reply(stanza):query("jabber:iq:register"); | |
245 -- TODO: Move this in standalone function | |
246 local challenge = get_captcha() | |
247 local captcha_data = get_file(config.dir.."/"..challenge..".png") | |
248 local captcha_sha = sha1(captcha_data, true) -- omg | |
249 local captcha_base64 = base64(captcha_data) -- lol wut | |
250 xml = registration_form:form(({FORM_TYPE = "urn:xmpp:captcha", | |
251 from = session.host, | |
252 ocr = {{ | |
253 type = "image/png", | |
254 uri = string.format("cid:sha1+%s@bob.xmpp.org", captcha_sha) | |
255 }}; | |
256 url = string.format("http://%s:5280/%s/%s", session.host, config.web_path, challenge); | |
257 captcha_text = "If you can't see an image, follow link below"; | |
258 challenge = challenge; | |
259 sid = "1"; | |
260 })); | |
261 | |
262 data = st.stanza("data", | |
263 {xmlns = "urn:xmpp:bob", | |
264 cid = string.format("sha1+%s@bob.xmpp.org", captcha_sha), | |
265 type = "image/png", | |
266 ["max-age"] = config.timeout}) | |
267 :text(captcha_base64); | |
268 | |
269 reply = reply:add_child(xml); | |
270 reply = reply:add_child(data); | |
271 session.send(reply); | |
272 | |
273 elseif stanza.attr.type == "set" then | |
274 if query.tags[1] and query.tags[1].name == "remove" then | |
275 session.send(st.error_reply(stanza, "auth", "registration-required")); | |
276 else | |
277 local data, errors = parse_response(query); | |
278 if errors then | |
279 session.send(st.error_reply(stanza, "modify", "not-acceptable")); | |
280 else | |
281 -- Check that the user is not blacklisted or registering too often | |
282 if not session.ip then | |
283 module:log("debug", "User's IP not known; can't apply blacklist/whitelist"); | |
284 elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then | |
285 session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account.")); | |
286 return true; | |
287 elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then | |
288 if not recent_ips[session.ip] then | |
289 recent_ips[session.ip] = { time = os_time(), count = 1 }; | |
290 else | |
291 local ip = recent_ips[session.ip]; | |
292 ip.count = ip.count + 1; | |
293 | |
294 if os_time() - ip.time < min_seconds_between_registrations then | |
295 ip.time = os_time(); | |
296 session.send(st.error_reply(stanza, "wait", "not-acceptable")); | |
297 return true; | |
298 end | |
299 ip.time = os_time(); | |
300 end | |
301 end | |
302 local host = module.host; | |
303 local ocr = data.ocr:lower(); | |
304 local challenge = data.challenge; | |
305 local username, password = nodeprep(data.username), data.password; | |
306 data.username, data.password = nil, nil; | |
307 | |
308 if challenge == nil or captcha_ids[challenge] == nil then | |
309 session.send(st.error_reply(stanza, "modify", "not-acceptable", "Captcha id is invalid or it has expired")); | |
310 delete_captcha(challenge); | |
311 return true; | |
312 elseif ocr ~= captcha_ids[challenge] then | |
313 session.send(st.error_reply(stanza, "modify", "not-acceptable", "Invalid captcha text")); | |
314 delete_captcha(challenge); | |
315 return true; | |
316 end | |
317 if not username or username == "" then | |
318 session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); | |
319 delete_captcha(challenge); | |
320 return true; | |
321 end | |
322 local user = { username = username , host = host, allowed = true } | |
323 module:fire_event("user-registering", user); | |
324 if not user.allowed then | |
325 delete_captcha(challenge); | |
326 session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden.")); | |
327 elseif usermanager_user_exists(username, host) then | |
328 delete_captcha(challenge) | |
329 session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); | |
330 else | |
331 -- TODO unable to write file, file may be locked, etc, what's the correct error? | |
332 local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk."); | |
333 if usermanager_create_user(username, password, host) then | |
334 if next(data) and not account_details:set(username, data) then | |
335 delete_captcha(challenge); | |
336 usermanager_delete_user(username, host); | |
337 session.send(error_reply); | |
338 return true; | |
339 end | |
340 session.send(st.reply(stanza)); -- user created! | |
341 module:log("info", "User account created: %s@%s", username, host); | |
342 module:fire_event("user-registered", { | |
343 username = username, host = host, source = "mod_register", | |
344 session = session }); | |
345 else | |
346 delete_captcha(challenge); | |
347 session.send(error_reply); | |
348 end | |
349 end | |
350 end | |
351 end | |
352 end | |
353 end | |
354 return true; | |
355 end); | |
356 | |
357 function string:split(sep) | |
358 local sep, fields = sep or ":", {} | |
359 local pattern = string.format("([^%s]+)", sep) | |
360 self:gsub(pattern, function(c) fields[#fields+1] = c end) | |
361 return fields | |
362 end | |
363 | |
364 | |
365 function handle_http_request(event) | |
366 local request = event.request; | |
367 local path = request.path; | |
368 local cid = path:split("/")[2]; | |
369 if cid == nil or captcha_ids[cid] == nil then | |
370 return nil; | |
371 end | |
372 request.response = { | |
373 status_code = 200; | |
374 headers = { | |
375 content_type = "image/png" | |
376 }; | |
377 body = get_file(string.format("%s/%s.png", config.dir, cid)); | |
378 }; | |
379 return request.response; | |
380 | |
381 | |
382 end; | |
383 | |
384 module:provides("http", { | |
385 default_path = "/"..config.web_path; | |
386 route = { | |
387 ["GET /*"] = handle_http_request; | |
388 }; | |
389 }); |