Mercurial > prosody-modules
comparison mod_email_pass/mod_email_pass.lua @ 1342:0ae065453dc9
Initial commit
author | Luis G.F <luisgf@gmail.com> |
---|---|
date | Mon, 10 Mar 2014 08:22:58 +0000 |
parents | |
children | 7dbde05b48a9 |
comparison
equal
deleted
inserted
replaced
1341:f5c256a5f209 | 1342:0ae065453dc9 |
---|---|
1 local dm_load = require "util.datamanager".load; | |
2 local st = require "util.stanza"; | |
3 local nodeprep = require "util.encodings".stringprep.nodeprep; | |
4 local usermanager = require "core.usermanager"; | |
5 local http = require "net.http"; | |
6 local vcard = module:require "vcard"; | |
7 local datetime = require "util.datetime"; | |
8 local timer = require "util.timer"; | |
9 local jidutil = require "util.jid"; | |
10 | |
11 -- SMTP related params. Readed from config | |
12 local os_time = os.time; | |
13 local smtp = require "socket.smtp"; | |
14 local smtp_server = module:get_option_string("smtp_server", "localhost"); | |
15 local smtp_port = module:get_option_string("smtp_port", "25"); | |
16 local smtp_ssl = module:get_option_boolean("smtp_ssl", false); | |
17 local smtp_user = module:get_option_string("smtp_username"); | |
18 local smtp_pass = module:get_option_string("smtp_password"); | |
19 local smtp_address = module:get_option("smtp_from") or ((smtp_user or "no-responder").."@"..(smtp_server or module.host)); | |
20 local mail_subject = module:get_option_string("msg_subject") | |
21 local mail_body = module:get_option_string("msg_body"); | |
22 local url_path = module:get_option_string("url_path", "/resetpass"); | |
23 | |
24 | |
25 -- This table has the tokens submited by the server | |
26 tokens_mails = {}; | |
27 tokens_expiration = {}; | |
28 | |
29 -- URL | |
30 local https_host = module:get_option_string("https_host"); | |
31 local http_host = module:get_option_string("http_host"); | |
32 local https_port = module:get_option("https_ports", { 443 }); | |
33 local http_port = module:get_option("http_ports", { 80 }); | |
34 | |
35 local timer_repeat = 120; -- repeat after 120 secs | |
36 | |
37 function enablessl() | |
38 local sock = socket.tcp() | |
39 return setmetatable({ | |
40 connect = function(_, host, port) | |
41 local r, e = sock:connect(host, port) | |
42 if not r then return r, e end | |
43 sock = ssl.wrap(sock, {mode='client', protocol='tlsv1'}) | |
44 return sock:dohandshake() | |
45 end | |
46 }, { | |
47 __index = function(t,n) | |
48 return function(_, ...) | |
49 return sock[n](sock, ...) | |
50 end | |
51 end | |
52 }) | |
53 end | |
54 | |
55 function template(data) | |
56 -- Like util.template, but deals with plain text | |
57 return { apply = function(values) return (data:gsub("{([^}]+)}", values)); end } | |
58 end | |
59 | |
60 local function get_template(name, extension) | |
61 local fh = assert(module:load_resource("templates/"..name..extension)); | |
62 local data = assert(fh:read("*a")); | |
63 fh:close(); | |
64 return template(data); | |
65 end | |
66 | |
67 local function render(template, data) | |
68 return tostring(template.apply(data)); | |
69 end | |
70 | |
71 function send_email(address, smtp_address, message_text, subject) | |
72 local rcpt = "<"..address..">"; | |
73 | |
74 local mesgt = { | |
75 headers = { | |
76 to = address; | |
77 subject = subject or ("Jabber password reset "..jid_bare(from_address)); | |
78 }; | |
79 body = message_text; | |
80 }; | |
81 local ok, err = nil; | |
82 | |
83 if not smtp_ssl then | |
84 ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), | |
85 server = smtp_server, user = smtp_user, password = smtp_pass, port = 25 }; | |
86 else | |
87 ok, err = smtp.send{ from = smtp_address, rcpt = rcpt, source = smtp.message(mesgt), | |
88 server = smtp_server, user = smtp_user, password = smtp_pass, port = smtp_port, create = enablessl }; | |
89 end | |
90 | |
91 if not ok then | |
92 module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); | |
93 return; | |
94 end | |
95 return true; | |
96 end | |
97 | |
98 local vCard_mt = { | |
99 __index = function(t, k) | |
100 if type(k) ~= "string" then return nil end | |
101 for i=1,#t do | |
102 local t_i = rawget(t, i); | |
103 if t_i and t_i.name == k then | |
104 rawset(t, k, t_i); | |
105 return t_i; | |
106 end | |
107 end | |
108 end | |
109 }; | |
110 | |
111 local function get_user_vcard(user, host) | |
112 local vCard = dm_load(user, host or base_host, "vcard"); | |
113 if vCard then | |
114 vCard = st.deserialize(vCard); | |
115 vCard = vcard.from_xep54(vCard); | |
116 return setmetatable(vCard, vCard_mt); | |
117 end | |
118 end | |
119 | |
120 local changepass_tpl = get_template("changepass",".html"); | |
121 local sendmail_success_tpl = get_template("sendmailok",".html"); | |
122 local reset_success_tpl = get_template("resetok",".html"); | |
123 local token_tpl = get_template("token",".html"); | |
124 | |
125 function generate_page(event, display_options) | |
126 local request = event.request; | |
127 | |
128 return render(changepass_tpl, { | |
129 path = request.path; hostname = module.host; | |
130 notice = display_options and display_options.register_error or ""; | |
131 }) | |
132 end | |
133 | |
134 function generate_token_page(event, display_options) | |
135 local request = event.request; | |
136 | |
137 return render(token_tpl, { | |
138 path = request.path; hostname = module.host; | |
139 token = request.url.query; | |
140 notice = display_options and display_options.register_error or ""; | |
141 }) | |
142 end | |
143 | |
144 function generateToken(address) | |
145 math.randomseed(os.time()) | |
146 length = 16 | |
147 if length < 1 then return nil end | |
148 local array = {} | |
149 for i = 1, length, 2 do | |
150 array[i] = string.char(math.random(48,57)) | |
151 array[i+1] = string.char(math.random(97,122)) | |
152 end | |
153 local token = table.concat(array); | |
154 if not tokens_mails[token] then | |
155 | |
156 tokens_mails[token] = address; | |
157 tokens_expiration[token] = os.time(); | |
158 return token | |
159 else | |
160 module:log("error", "Reset password token collision: '%s'", token); | |
161 return generateToken(address) | |
162 end | |
163 end | |
164 | |
165 function isExpired(token) | |
166 if not tokens_expiration[token] then | |
167 return nil; | |
168 end | |
169 if os.difftime(os.time(), tokens_expiration[token]) < 86400 then -- 86400 secs == 24h | |
170 -- token is valid yet | |
171 return nil; | |
172 else | |
173 -- token invalid, we can create a fresh one. | |
174 return true; | |
175 end | |
176 end | |
177 | |
178 -- Expire tokens | |
179 expireTokens = function() | |
180 for token,value in pairs(tokens_mails) do | |
181 if isExpired(token) then | |
182 module:log("info","Expiring password reset request from user '%s', not used.", tokens_mails[token]); | |
183 tokens_mails[token] = nil; | |
184 tokens_expiration[token] = nil; | |
185 end | |
186 end | |
187 return timer_repeat; | |
188 end | |
189 | |
190 -- Check if a user has a active token not used yet. | |
191 function hasTokenActive(address) | |
192 for token,value in pairs(tokens_mails) do | |
193 if address == value and not isExpired(token) then | |
194 return token; | |
195 end | |
196 end | |
197 return nil; | |
198 end | |
199 | |
200 function generateUrl(token) | |
201 local url; | |
202 | |
203 if https_host then | |
204 url = "https://" .. https_host; | |
205 else | |
206 url = "http://" .. http_host; | |
207 end | |
208 | |
209 if https_port then | |
210 url = url .. ":" .. https_port[1]; | |
211 else | |
212 url = url .. ":" .. http_port[1]; | |
213 end | |
214 | |
215 url = url .. url_path .. "token.html?" .. token; | |
216 | |
217 return url; | |
218 end | |
219 | |
220 function sendMessage(jid, subject, message) | |
221 local msg = st.message({ from = module.host; to = jid; }): | |
222 tag("subject"):text(subject):up(): | |
223 tag("body"):text(message); | |
224 module:send(msg); | |
225 end | |
226 | |
227 function send_token_mail(form, origin) | |
228 local user, host, resource = jidutil.split(form.username); | |
229 local prepped_username = nodeprep(user); | |
230 local prepped_mail = form.email; | |
231 local jid = prepped_username .. "@" .. host; | |
232 | |
233 if not prepped_username then | |
234 return nil, "El usuario contiene caracteres incorrectos"; | |
235 end | |
236 if #prepped_username == 0 then | |
237 return nil, "El campo usuario está vacio"; | |
238 end | |
239 if not usermanager.user_exists(prepped_username, module.host) then | |
240 return nil, "El usuario NO existe"; | |
241 end | |
242 | |
243 if #prepped_mail == 0 then | |
244 return nil, "El campo email está vacio"; | |
245 end | |
246 | |
247 local vcarduser = get_user_vcard(prepped_username, module.host); | |
248 | |
249 if not vcarduser then | |
250 return nil, "User has not vCard"; | |
251 else | |
252 if not vcarduser.EMAIL then | |
253 return nil, "Esa cuente no tiene ningún email configurado en su vCard"; | |
254 end | |
255 | |
256 email = string.lower(vcarduser.EMAIL[1]); | |
257 | |
258 if email ~= string.lower(prepped_mail) then | |
259 return nil, "Dirección eMail incorrecta"; | |
260 end | |
261 | |
262 -- Check if has already a valid token, not used yet. | |
263 if hasTokenActive(jid) then | |
264 local valid_until = tokens_expiration[hasTokenActive(jid)] + 86400; | |
265 return nil, "Ya tienes una petición de restablecimiento de clave válida hasta: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); | |
266 end | |
267 | |
268 local url_token = generateToken(jid); | |
269 local url = generateUrl(url_token); | |
270 local email_body = render(get_template("sendtoken",".mail"), {jid = jid, url = url} ); | |
271 | |
272 module:log("info", "Sending password reset mail to user %s", jid); | |
273 send_email(email, smtp_address, email_body, mail_subject); | |
274 return "ok"; | |
275 end | |
276 | |
277 end | |
278 | |
279 function reset_password_with_token(form, origin) | |
280 local token = form.token; | |
281 local password = form.newpassword; | |
282 | |
283 if not token then | |
284 return nil, "El Token es inválido"; | |
285 end | |
286 if not tokens_mails[token] then | |
287 return nil, "El Token no existe o ya fué usado"; | |
288 end | |
289 if not password then | |
290 return nil, "La campo clave no puede estar vacio"; | |
291 end | |
292 if #password < 5 then | |
293 return nil, "La clave debe tener una longitud de al menos 5 caracteres"; | |
294 end | |
295 local jid = tokens_mails[token]; | |
296 local user, host, resource = jidutil.split(jid); | |
297 | |
298 usermanager.set_password(user, password, host); | |
299 module:log("info", "Password changed with token for user %s", jid); | |
300 tokens_mails[token] = nil; | |
301 tokens_expiration[token] = nil; | |
302 sendMessage(jid, mail_subject, mail_body); | |
303 return "ok"; | |
304 end | |
305 | |
306 function generate_success(event, form) | |
307 return render(sendmail_success_tpl, { jid = nodeprep(form.username).."@"..module.host }); | |
308 end | |
309 | |
310 function generate_register_response(event, form, ok, err) | |
311 local message; | |
312 if ok then | |
313 return generate_success(event, form); | |
314 else | |
315 return generate_page(event, { register_error = err }); | |
316 end | |
317 end | |
318 | |
319 function handle_form_token(event) | |
320 local request, response = event.request, event.response; | |
321 local form = http.formdecode(request.body); | |
322 | |
323 local token_ok, token_err = send_token_mail(form, request); | |
324 response:send(generate_register_response(event, form, token_ok, token_err)); | |
325 | |
326 return true; -- Leave connection open until we respond above | |
327 end | |
328 | |
329 function generate_reset_success(event, form) | |
330 return render(reset_success_tpl, { }); | |
331 end | |
332 | |
333 function generate_reset_response(event, form, ok, err) | |
334 local message; | |
335 if ok then | |
336 return generate_reset_success(event, form); | |
337 else | |
338 return generate_token_page(event, { register_error = err }); | |
339 end | |
340 end | |
341 | |
342 function handle_form_reset(event) | |
343 local request, response = event.request, event.response; | |
344 local form = http.formdecode(request.body); | |
345 | |
346 local reset_ok, reset_err = reset_password_with_token(form, request); | |
347 response:send(generate_reset_response(event, form, reset_ok, reset_err)); | |
348 | |
349 return true; -- Leave connection open until we respond above | |
350 | |
351 end | |
352 | |
353 timer.add_task(timer_repeat, expireTokens); | |
354 | |
355 module:provides("http", { | |
356 default_path = url_path; | |
357 route = { | |
358 ["GET /style.css"] = render(get_template("style",".css"), {}); | |
359 ["GET /token.html"] = generate_token_page; | |
360 ["GET /"] = generate_page; | |
361 ["POST /token.html"] = handle_form_reset; | |
362 ["POST /"] = handle_form_token; | |
363 }; | |
364 }); | |
365 | |
366 |