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