Mercurial > prosody-modules
annotate mod_email_pass/mod_email_pass.lua @ 5497:1be6e375a7c2
misc/lnav: Fix delimiting of timestamp in pattern
The string with the timestamp format in core.loggingmanager does end
with a space, so having the exact same string here is nice, but the
pattern did not reflect this.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Wed, 31 May 2023 18:04:30 +0200 |
parents | c60e9943dcb9 |
children |
rev | line source |
---|---|
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 |