author | Kim Alvefur <zash@zash.se> |
Sat, 25 Jan 2020 01:31:49 +0100 | |
changeset 3861 | 8752e5b5dd08 |
parent 1345 | c60e9943dcb9 |
permissions | -rw-r--r-- |
1342 | 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 |
||
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
29 |
-- URL |
1342 | 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 |
||
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
165 |
function isExpired(token) |
1342 | 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; |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
175 |
end |
1342 | 176 |
end |
177 |
||
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
178 |
-- Expire tokens |
1342 | 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; |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
202 |
|
1342 | 203 |
if https_host then |
204 |
url = "https://" .. https_host; |
|
205 |
else |
|
206 |
url = "http://" .. http_host; |
|
207 |
end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
208 |
|
1342 | 209 |
if https_port then |
210 |
url = url .. ":" .. https_port[1]; |
|
211 |
else |
|
212 |
url = url .. ":" .. http_port[1]; |
|
213 |
end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
214 |
|
1342 | 215 |
url = url .. url_path .. "token.html?" .. token; |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
216 |
|
1342 | 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) |
|
1345
c60e9943dcb9
Fix problem handling form input
Luis G.F <luisgf@gmail.com>
parents:
1343
diff
changeset
|
228 |
local prepped_username = nodeprep(form.username); |
1342 | 229 |
local prepped_mail = form.email; |
1345
c60e9943dcb9
Fix problem handling form input
Luis G.F <luisgf@gmail.com>
parents:
1343
diff
changeset
|
230 |
local jid = prepped_username .. "@" .. module.host; |
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
231 |
|
1342 | 232 |
if not prepped_username then |
233 |
return nil, "El usuario contiene caracteres incorrectos"; |
|
234 |
end |
|
235 |
if #prepped_username == 0 then |
|
236 |
return nil, "El campo usuario está vacio"; |
|
237 |
end |
|
238 |
if not usermanager.user_exists(prepped_username, module.host) then |
|
239 |
return nil, "El usuario NO existe"; |
|
240 |
end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
241 |
|
1342 | 242 |
if #prepped_mail == 0 then |
243 |
return nil, "El campo email está vacio"; |
|
244 |
end |
|
245 |
||
246 |
local vcarduser = get_user_vcard(prepped_username, module.host); |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
247 |
|
1342 | 248 |
if not vcarduser then |
249 |
return nil, "User has not vCard"; |
|
250 |
else |
|
251 |
if not vcarduser.EMAIL then |
|
252 |
return nil, "Esa cuente no tiene ningún email configurado en su vCard"; |
|
253 |
end |
|
254 |
||
255 |
email = string.lower(vcarduser.EMAIL[1]); |
|
256 |
||
257 |
if email ~= string.lower(prepped_mail) then |
|
258 |
return nil, "Dirección eMail incorrecta"; |
|
259 |
end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
260 |
|
1342 | 261 |
-- Check if has already a valid token, not used yet. |
262 |
if hasTokenActive(jid) then |
|
263 |
local valid_until = tokens_expiration[hasTokenActive(jid)] + 86400; |
|
264 |
return nil, "Ya tienes una petición de restablecimiento de clave válida hasta: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); |
|
265 |
end |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
266 |
|
1342 | 267 |
local url_token = generateToken(jid); |
268 |
local url = generateUrl(url_token); |
|
269 |
local email_body = render(get_template("sendtoken",".mail"), {jid = jid, url = url} ); |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
270 |
|
1342 | 271 |
module:log("info", "Sending password reset mail to user %s", jid); |
272 |
send_email(email, smtp_address, email_body, mail_subject); |
|
273 |
return "ok"; |
|
274 |
end |
|
275 |
||
276 |
end |
|
277 |
||
278 |
function reset_password_with_token(form, origin) |
|
279 |
local token = form.token; |
|
280 |
local password = form.newpassword; |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
281 |
|
1342 | 282 |
if not token then |
283 |
return nil, "El Token es inválido"; |
|
284 |
end |
|
285 |
if not tokens_mails[token] then |
|
286 |
return nil, "El Token no existe o ya fué usado"; |
|
287 |
end |
|
288 |
if not password then |
|
289 |
return nil, "La campo clave no puede estar vacio"; |
|
290 |
end |
|
291 |
if #password < 5 then |
|
292 |
return nil, "La clave debe tener una longitud de al menos 5 caracteres"; |
|
293 |
end |
|
294 |
local jid = tokens_mails[token]; |
|
295 |
local user, host, resource = jidutil.split(jid); |
|
1343
7dbde05b48a9
all the things: Remove trailing whitespace
Florian Zeitz <florob@babelmonkeys.de>
parents:
1342
diff
changeset
|
296 |
|
1342 | 297 |
usermanager.set_password(user, password, host); |
298 |
module:log("info", "Password changed with token for user %s", jid); |
|
299 |
tokens_mails[token] = nil; |
|
300 |
tokens_expiration[token] = nil; |
|
301 |
sendMessage(jid, mail_subject, mail_body); |
|
302 |
return "ok"; |
|
303 |
end |
|
304 |
||
305 |
function generate_success(event, form) |
|
306 |
return render(sendmail_success_tpl, { jid = nodeprep(form.username).."@"..module.host }); |
|
307 |
end |
|
308 |
||
309 |
function generate_register_response(event, form, ok, err) |
|
310 |
local message; |
|
311 |
if ok then |
|
312 |
return generate_success(event, form); |
|
313 |
else |
|
314 |
return generate_page(event, { register_error = err }); |
|
315 |
end |
|
316 |
end |
|
317 |
||
318 |
function handle_form_token(event) |
|
319 |
local request, response = event.request, event.response; |
|
320 |
local form = http.formdecode(request.body); |
|
321 |
||
322 |
local token_ok, token_err = send_token_mail(form, request); |
|
323 |
response:send(generate_register_response(event, form, token_ok, token_err)); |
|
324 |
||
325 |
return true; -- Leave connection open until we respond above |
|
326 |
end |
|
327 |
||
328 |
function generate_reset_success(event, form) |
|
329 |
return render(reset_success_tpl, { }); |
|
330 |
end |
|
331 |
||
332 |
function generate_reset_response(event, form, ok, err) |
|
333 |
local message; |
|
334 |
if ok then |
|
335 |
return generate_reset_success(event, form); |
|
336 |
else |
|
337 |
return generate_token_page(event, { register_error = err }); |
|
338 |
end |
|
339 |
end |
|
340 |
||
341 |
function handle_form_reset(event) |
|
342 |
local request, response = event.request, event.response; |
|
343 |
local form = http.formdecode(request.body); |
|
344 |
||
345 |
local reset_ok, reset_err = reset_password_with_token(form, request); |
|
346 |
response:send(generate_reset_response(event, form, reset_ok, reset_err)); |
|
347 |
||
348 |
return true; -- Leave connection open until we respond above |
|
349 |
||
350 |
end |
|
351 |
||
352 |
timer.add_task(timer_repeat, expireTokens); |
|
353 |
||
354 |
module:provides("http", { |
|
355 |
default_path = url_path; |
|
356 |
route = { |
|
357 |
["GET /style.css"] = render(get_template("style",".css"), {}); |
|
358 |
["GET /token.html"] = generate_token_page; |
|
359 |
["GET /"] = generate_page; |
|
360 |
["POST /token.html"] = handle_form_reset; |
|
361 |
["POST /"] = handle_form_token; |
|
362 |
}; |
|
363 |
}); |
|
364 |
||
365 |