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