import files from hg.prosody.im/prosody-modules
This commit is contained in:
parent
a5476269c6
commit
ac47a4133b
49
mod_email/mod_email.lua
Normal file
49
mod_email/mod_email.lua
Normal file
@ -0,0 +1,49 @@
|
||||
module:set_global();
|
||||
|
||||
local moduleapi = require "core.moduleapi";
|
||||
|
||||
local smtp = require"socket.smtp";
|
||||
|
||||
local config = module:get_option("smtp", { origin = "prosody", exec = "sendmail" });
|
||||
|
||||
local function send_email(to, headers, content)
|
||||
if type(headers) == "string" then -- subject
|
||||
headers = {
|
||||
Subject = headers;
|
||||
From = config.origin;
|
||||
};
|
||||
end
|
||||
headers.To = to;
|
||||
if not headers["Content-Type"] then
|
||||
headers["Content-Type"] = 'text/plain; charset="utf-8"';
|
||||
end
|
||||
local message = smtp.message{
|
||||
headers = headers;
|
||||
body = content;
|
||||
};
|
||||
|
||||
if config.exec then
|
||||
local pipe = io.popen(config.exec ..
|
||||
" '"..to:gsub("'", "'\\''").."'", "w");
|
||||
|
||||
for str in message do
|
||||
pipe:write(str);
|
||||
end
|
||||
|
||||
return pipe:close();
|
||||
end
|
||||
|
||||
return smtp.send({
|
||||
user = config.user; password = config.password;
|
||||
server = config.server; port = config.port;
|
||||
domain = config.domain;
|
||||
|
||||
from = config.origin; rcpt = to;
|
||||
source = message;
|
||||
});
|
||||
end
|
||||
|
||||
assert(not moduleapi.send_email, "another email module is already loaded");
|
||||
function moduleapi:send_email(email) --luacheck: ignore 212/self
|
||||
return send_email(email.to, email.headers or email.subject, email.body);
|
||||
end
|
49
mod_email_pass/README.markdown
Normal file
49
mod_email_pass/README.markdown
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Beta'
|
||||
...
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This module aims to help in the procedure of user password restoration.
|
||||
To start the restoration, the user must go to an URL provided by this
|
||||
module, fill the JID and email and submit the request.
|
||||
|
||||
The module will generate a token valid for 24h and send an email with a
|
||||
specially crafted url to the vCard email address. If the user goes to
|
||||
this url, will be able to change his password.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Simply add "email\_pass" to your modules\_enabled list and copy files
|
||||
"**mod\_email\_pass.lua**" and "**vcard.lib.lua**" to prosody modules
|
||||
folder. This module need to that **https\_host** or **http\_host** must
|
||||
be configured. This parameter is necessary to construct the URL that has
|
||||
been sended to the user.
|
||||
|
||||
This module only send emails to the vCard user email address, then the
|
||||
user must set this address in order to be capable of do the restoration.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
--------------- ------------------------------------------------------------
|
||||
smtp\_server The Host/ip of your SMTP server
|
||||
smtp\_port Port used by your SMTP server. Default 25
|
||||
smtp\_ssl Use of SMTP SSL legacy (No STARTTLS)
|
||||
smtp\_user Username used to do SMTP auth
|
||||
smtp\_pass Password used to do SMTP auth
|
||||
smtp\_address EMail address that will be apears as From in mails
|
||||
msg\_subject Subject used for messages/mails
|
||||
msg\_body Message send when password has been changed
|
||||
url\_path Path where the module will be visible. Default /resetpass/
|
||||
--------------- ------------------------------------------------------------
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
----- -------
|
||||
0.9 Works
|
||||
----- -------
|
365
mod_email_pass/mod_email_pass.lua
Normal file
365
mod_email_pass/mod_email_pass.lua
Normal file
@ -0,0 +1,365 @@
|
||||
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;
|
||||
};
|
||||
});
|
||||
|
||||
|
36
mod_email_pass/templates/changepass.html
Normal file
36
mod_email_pass/templates/changepass.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<title>Reseteo de la clave de tu cuenta Jabber</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="estilo" class="formulario">
|
||||
<h1>Reseteo de la clave de tu cuenta Jabber</h1>
|
||||
<form action="{path}" method="POST">
|
||||
<p class="error">{notice}</p>
|
||||
<label>
|
||||
Usuario:
|
||||
<span class="small">Introduce tu usuario</span>
|
||||
</label>
|
||||
<input type="text" name="username" required>@{hostname}
|
||||
<div class="spacer"></div>
|
||||
<label>
|
||||
Email:
|
||||
<span class="small">Introduce tu email</span>
|
||||
</label>
|
||||
<input type="text" name="email" required>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<input id="button" class="button" type="submit" value="Enviar!">
|
||||
<div class="spacer"></div>
|
||||
</form>
|
||||
<p>
|
||||
Al pulsar sobre el botón, se enviará a la dirección de correo que figura
|
||||
en tu vCard un enlace en el que deberás entrar.<br />
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
15
mod_email_pass/templates/resetok.html
Normal file
15
mod_email_pass/templates/resetok.html
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<meta charset="utf-8">
|
||||
<title>Clave reseteada!</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="estilo" class="formulario">
|
||||
<h1>Tu clave ha sido cambiada correctamente. Ya puedes iniciar sesión con ella.</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
15
mod_email_pass/templates/sendmailok.html
Normal file
15
mod_email_pass/templates/sendmailok.html
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<meta charset="utf-8">
|
||||
<title>Enlace enviado!</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="estilo" class="formulario">
|
||||
<h1>Acabamos de enviarte un email con un enlace que tendrás que visitar.</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
14
mod_email_pass/templates/sendtoken.mail
Normal file
14
mod_email_pass/templates/sendtoken.mail
Normal file
@ -0,0 +1,14 @@
|
||||
Hola:
|
||||
|
||||
Si has recibido este email es porque has solicitado el reseteo de la
|
||||
clave de tu cuenta Jabber/XMPP {jid}
|
||||
|
||||
Para proceder con el cambio de clave, haz click en el siguiente enlace:
|
||||
|
||||
{url}
|
||||
|
||||
Si no has solicitado resetear tu clave, ignora este mensaje.
|
||||
|
||||
Atentamente, el equipo de mijabber.es
|
||||
|
||||
|
109
mod_email_pass/templates/style.css
Normal file
109
mod_email_pass/templates/style.css
Normal file
@ -0,0 +1,109 @@
|
||||
body{
|
||||
font-family:"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
p, h1, form, button{border:0; margin:0; padding:0;}
|
||||
.spacer{clear:both; height:1px;}
|
||||
|
||||
/* ----------- My Form ----------- */
|
||||
.formulario{
|
||||
margin:0 auto;
|
||||
width:500px;
|
||||
padding:14px;
|
||||
}
|
||||
/* ----------- stylized ----------- */
|
||||
#estilo {
|
||||
border:solid 2px #b7ddf2;
|
||||
background:#ebf4fb;
|
||||
}
|
||||
|
||||
#estilo h1 {
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
margin-bottom:8px;
|
||||
}
|
||||
|
||||
#estilo p {
|
||||
font-size:11px;
|
||||
color:#666666;
|
||||
margin-bottom:20px;
|
||||
border-bottom:solid 1px #b7ddf2;
|
||||
padding-bottom:10px;
|
||||
}
|
||||
|
||||
#estilo p.error {
|
||||
font-size:12px;
|
||||
font-weight:bold;
|
||||
color:red;
|
||||
margin-bottom:20px;
|
||||
border-bottom:solid 1px #b7ddf2;
|
||||
padding-bottom:10px;
|
||||
}
|
||||
|
||||
#estilo label{
|
||||
display:block;
|
||||
font-weight:bold;
|
||||
text-align:right;
|
||||
width:140px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
#estilo .small{
|
||||
color:#666666;
|
||||
display:block;
|
||||
font-size:11px;
|
||||
font-weight:normal;
|
||||
text-align:right;
|
||||
width:140px;
|
||||
}
|
||||
|
||||
#estilo input{
|
||||
float:left;
|
||||
font-size:12px;
|
||||
padding:4px 2px;
|
||||
border:solid 1px #aacfe4;
|
||||
width:200px;
|
||||
margin:2px 0 20px 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
-moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
-webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
box-shadow:inset 0px 1px 0px 0px #cae3fc;
|
||||
background-color:#79bbff;
|
||||
-webkit-border-top-left-radius:18px;
|
||||
-moz-border-radius-topleft:18px;
|
||||
border-top-left-radius:18px;
|
||||
-webkit-border-top-right-radius:18px;
|
||||
-moz-border-radius-topright:18px;
|
||||
border-top-right-radius:18px;
|
||||
-webkit-border-bottom-right-radius:18px;
|
||||
-moz-border-radius-bottomright:18px;
|
||||
border-bottom-right-radius:18px;
|
||||
-webkit-border-bottom-left-radius:18px;
|
||||
-moz-border-radius-bottomleft:18px;
|
||||
border-bottom-left-radius:18px;
|
||||
text-indent:0;
|
||||
border:1px solid #469df5;
|
||||
display:inline-block;
|
||||
color:#ffffff;
|
||||
font-family:Arial;
|
||||
font-size:15px;
|
||||
font-weight:bold;
|
||||
font-style:normal;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
width:100px;
|
||||
text-decoration:none;
|
||||
text-align:center;
|
||||
text-shadow:1px 1px 0px #287ace;
|
||||
}
|
||||
.button:hover {
|
||||
background-color:#4197ee;
|
||||
}
|
||||
.button:active {
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
|
30
mod_email_pass/templates/token.html
Normal file
30
mod_email_pass/templates/token.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<meta charset="utf-8">
|
||||
<title>Reseto de la clave de tu cuenta Jabber</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="estilo" class="formulario">
|
||||
<h1>Reseteo de la clave de tu cuenta Jabber</h1>
|
||||
<form action="{path}?{token}" method="POST">
|
||||
<p class="error">{notice}</p>
|
||||
<label>
|
||||
Token:
|
||||
</label>
|
||||
<input name="token" value="{token}" required readonly>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<label>
|
||||
Contraseña:
|
||||
</label>
|
||||
<input name="newpassword" type="password" required size="35">
|
||||
<div class="spacer"></div>
|
||||
<input id="button" class="button" type="submit" value="Cambiar!">
|
||||
<div class="spacer"></div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
464
mod_email_pass/vcard.lib.lua
Normal file
464
mod_email_pass/vcard.lib.lua
Normal file
@ -0,0 +1,464 @@
|
||||
-- Copyright (C) 2011-2012 Kim Alvefur
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
-- TODO
|
||||
-- Fix folding.
|
||||
|
||||
local st = require "util.stanza";
|
||||
local t_insert, t_concat = table.insert, table.concat;
|
||||
local type = type;
|
||||
local next, pairs, ipairs = next, pairs, ipairs;
|
||||
|
||||
local from_text, to_text, from_xep54, to_xep54;
|
||||
|
||||
local line_sep = "\n";
|
||||
|
||||
local vCard_dtd; -- See end of file
|
||||
|
||||
local function fold_line()
|
||||
error "Not implemented" --TODO
|
||||
end
|
||||
local function unfold_line()
|
||||
error "Not implemented"
|
||||
-- gsub("\r?\n[ \t]([^\r\n])", "%1");
|
||||
end
|
||||
|
||||
local function vCard_esc(s)
|
||||
return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
|
||||
end
|
||||
|
||||
local function vCard_unesc(s)
|
||||
return s:gsub("\\?[\\nt:;,]", {
|
||||
["\\\\"] = "\\",
|
||||
["\\n"] = "\n",
|
||||
["\\r"] = "\r",
|
||||
["\\t"] = "\t",
|
||||
["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params
|
||||
["\\;"] = ";",
|
||||
["\\,"] = ",",
|
||||
[":"] = "\29",
|
||||
[";"] = "\30",
|
||||
[","] = "\31",
|
||||
});
|
||||
end
|
||||
|
||||
local function item_to_xep54(item)
|
||||
local t = st.stanza(item.name, { xmlns = "vcard-temp" });
|
||||
|
||||
local prop_def = vCard_dtd[item.name];
|
||||
if prop_def == "text" then
|
||||
t:text(item[1]);
|
||||
elseif type(prop_def) == "table" then
|
||||
if prop_def.types and item.TYPE then
|
||||
if type(item.TYPE) == "table" then
|
||||
for _,v in pairs(prop_def.types) do
|
||||
for _,typ in pairs(item.TYPE) do
|
||||
if typ:upper() == v then
|
||||
t:tag(v):up();
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
t:tag(item.TYPE:upper()):up();
|
||||
end
|
||||
end
|
||||
|
||||
if prop_def.props then
|
||||
for _,v in pairs(prop_def.props) do
|
||||
if item[v] then
|
||||
t:tag(v):up();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if prop_def.value then
|
||||
t:tag(prop_def.value):text(item[1]):up();
|
||||
elseif prop_def.values then
|
||||
local prop_def_values = prop_def.values;
|
||||
local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values];
|
||||
for i=1,#item do
|
||||
t:tag(prop_def.values[i] or repeat_last):text(item[i]):up();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return t;
|
||||
end
|
||||
|
||||
local function vcard_to_xep54(vCard)
|
||||
local t = st.stanza("vCard", { xmlns = "vcard-temp" });
|
||||
for i=1,#vCard do
|
||||
t:add_child(item_to_xep54(vCard[i]));
|
||||
end
|
||||
return t;
|
||||
end
|
||||
|
||||
function to_xep54(vCards)
|
||||
if not vCards[1] or vCards[1].name then
|
||||
return vcard_to_xep54(vCards)
|
||||
else
|
||||
local t = st.stanza("xCard", { xmlns = "vcard-temp" });
|
||||
for i=1,#vCards do
|
||||
t:add_child(vcard_to_xep54(vCards[i]));
|
||||
end
|
||||
return t;
|
||||
end
|
||||
end
|
||||
|
||||
function from_text(data)
|
||||
data = data -- unfold and remove empty lines
|
||||
:gsub("\r\n","\n")
|
||||
:gsub("\n ", "")
|
||||
:gsub("\n\n+","\n");
|
||||
local vCards = {};
|
||||
local c; -- current item
|
||||
for line in data:gmatch("[^\n]+") do
|
||||
local line = vCard_unesc(line);
|
||||
local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
|
||||
value = value:gsub("\29",":");
|
||||
if #params > 0 then
|
||||
local _params = {};
|
||||
for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
|
||||
k = k:upper();
|
||||
local _vt = {};
|
||||
for _p in v:gmatch("[^\31]+") do
|
||||
_vt[#_vt+1]=_p
|
||||
_vt[_p]=true;
|
||||
end
|
||||
if isval == "=" then
|
||||
_params[k]=_vt;
|
||||
else
|
||||
_params[k]=true;
|
||||
end
|
||||
end
|
||||
params = _params;
|
||||
end
|
||||
if name == "BEGIN" and value == "VCARD" then
|
||||
c = {};
|
||||
vCards[#vCards+1] = c;
|
||||
elseif name == "END" and value == "VCARD" then
|
||||
c = nil;
|
||||
elseif vCard_dtd[name] then
|
||||
local dtd = vCard_dtd[name];
|
||||
local p = { name = name };
|
||||
c[#c+1]=p;
|
||||
--c[name]=p;
|
||||
local up = c;
|
||||
c = p;
|
||||
if dtd.types then
|
||||
for _, t in ipairs(dtd.types) do
|
||||
local t = t:lower();
|
||||
if ( params.TYPE and params.TYPE[t] == true)
|
||||
or params[t] == true then
|
||||
c.TYPE=t;
|
||||
end
|
||||
end
|
||||
end
|
||||
if dtd.props then
|
||||
for _, p in ipairs(dtd.props) do
|
||||
if params[p] then
|
||||
if params[p] == true then
|
||||
c[p]=true;
|
||||
else
|
||||
for _, prop in ipairs(params[p]) do
|
||||
c[p]=prop;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if dtd == "text" or dtd.value then
|
||||
t_insert(c, value);
|
||||
elseif dtd.values then
|
||||
local value = "\30"..value;
|
||||
for p in value:gmatch("\30([^\30]*)") do
|
||||
t_insert(c, p);
|
||||
end
|
||||
end
|
||||
c = up;
|
||||
end
|
||||
end
|
||||
return vCards;
|
||||
end
|
||||
|
||||
local function item_to_text(item)
|
||||
local value = {};
|
||||
for i=1,#item do
|
||||
value[i] = vCard_esc(item[i]);
|
||||
end
|
||||
value = t_concat(value, ";");
|
||||
|
||||
local params = "";
|
||||
for k,v in pairs(item) do
|
||||
if type(k) == "string" and k ~= "name" then
|
||||
params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v);
|
||||
end
|
||||
end
|
||||
|
||||
return ("%s%s:%s"):format(item.name, params, value)
|
||||
end
|
||||
|
||||
local function vcard_to_text(vcard)
|
||||
local t={};
|
||||
t_insert(t, "BEGIN:VCARD")
|
||||
for i=1,#vcard do
|
||||
t_insert(t, item_to_text(vcard[i]));
|
||||
end
|
||||
t_insert(t, "END:VCARD")
|
||||
return t_concat(t, line_sep);
|
||||
end
|
||||
|
||||
function to_text(vCards)
|
||||
if vCards[1] and vCards[1].name then
|
||||
return vcard_to_text(vCards)
|
||||
else
|
||||
local t = {};
|
||||
for i=1,#vCards do
|
||||
t[i]=vcard_to_text(vCards[i]);
|
||||
end
|
||||
return t_concat(t, line_sep);
|
||||
end
|
||||
end
|
||||
|
||||
local function from_xep54_item(item)
|
||||
local prop_name = item.name;
|
||||
local prop_def = vCard_dtd[prop_name];
|
||||
|
||||
local prop = { name = prop_name };
|
||||
|
||||
if prop_def == "text" then
|
||||
prop[1] = item:get_text();
|
||||
elseif type(prop_def) == "table" then
|
||||
if prop_def.value then --single item
|
||||
prop[1] = item:get_child_text(prop_def.value) or "";
|
||||
elseif prop_def.values then --array
|
||||
local value_names = prop_def.values;
|
||||
if value_names.behaviour == "repeat-last" then
|
||||
for i=1,#item.tags do
|
||||
t_insert(prop, item.tags[i]:get_text() or "");
|
||||
end
|
||||
else
|
||||
for i=1,#value_names do
|
||||
t_insert(prop, item:get_child_text(value_names[i]) or "");
|
||||
end
|
||||
end
|
||||
elseif prop_def.names then
|
||||
local names = prop_def.names;
|
||||
for i=1,#names do
|
||||
if item:get_child(names[i]) then
|
||||
prop[1] = names[i];
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if prop_def.props_verbatim then
|
||||
for k,v in pairs(prop_def.props_verbatim) do
|
||||
prop[k] = v;
|
||||
end
|
||||
end
|
||||
|
||||
if prop_def.types then
|
||||
local types = prop_def.types;
|
||||
prop.TYPE = {};
|
||||
for i=1,#types do
|
||||
if item:get_child(types[i]) then
|
||||
t_insert(prop.TYPE, types[i]:lower());
|
||||
end
|
||||
end
|
||||
if #prop.TYPE == 0 then
|
||||
prop.TYPE = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- A key-value pair, within a key-value pair?
|
||||
if prop_def.props then
|
||||
local params = prop_def.props;
|
||||
for i=1,#params do
|
||||
local name = params[i]
|
||||
local data = item:get_child_text(name);
|
||||
if data then
|
||||
prop[name] = prop[name] or {};
|
||||
t_insert(prop[name], data);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
return prop;
|
||||
end
|
||||
|
||||
local function from_xep54_vCard(vCard)
|
||||
local tags = vCard.tags;
|
||||
local t = {};
|
||||
for i=1,#tags do
|
||||
t_insert(t, from_xep54_item(tags[i]));
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function from_xep54(vCard)
|
||||
if vCard.attr.xmlns ~= "vcard-temp" then
|
||||
return nil, "wrong-xmlns";
|
||||
end
|
||||
if vCard.name == "xCard" then -- A collection of vCards
|
||||
local t = {};
|
||||
local vCards = vCard.tags;
|
||||
for i=1,#vCards do
|
||||
t[i] = from_xep54_vCard(vCards[i]);
|
||||
end
|
||||
return t
|
||||
elseif vCard.name == "vCard" then -- A single vCard
|
||||
return from_xep54_vCard(vCard)
|
||||
end
|
||||
end
|
||||
|
||||
-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
|
||||
vCard_dtd = {
|
||||
VERSION = "text", --MUST be 3.0, so parsing is redundant
|
||||
FN = "text",
|
||||
N = {
|
||||
values = {
|
||||
"FAMILY",
|
||||
"GIVEN",
|
||||
"MIDDLE",
|
||||
"PREFIX",
|
||||
"SUFFIX",
|
||||
},
|
||||
},
|
||||
NICKNAME = "text",
|
||||
PHOTO = {
|
||||
props_verbatim = { ENCODING = { "b" } },
|
||||
props = { "TYPE" },
|
||||
value = "BINVAL", --{ "EXTVAL", },
|
||||
},
|
||||
BDAY = "text",
|
||||
ADR = {
|
||||
types = {
|
||||
"HOME",
|
||||
"WORK",
|
||||
"POSTAL",
|
||||
"PARCEL",
|
||||
"DOM",
|
||||
"INTL",
|
||||
"PREF",
|
||||
},
|
||||
values = {
|
||||
"POBOX",
|
||||
"EXTADD",
|
||||
"STREET",
|
||||
"LOCALITY",
|
||||
"REGION",
|
||||
"PCODE",
|
||||
"CTRY",
|
||||
}
|
||||
},
|
||||
LABEL = {
|
||||
types = {
|
||||
"HOME",
|
||||
"WORK",
|
||||
"POSTAL",
|
||||
"PARCEL",
|
||||
"DOM",
|
||||
"INTL",
|
||||
"PREF",
|
||||
},
|
||||
value = "LINE",
|
||||
},
|
||||
TEL = {
|
||||
types = {
|
||||
"HOME",
|
||||
"WORK",
|
||||
"VOICE",
|
||||
"FAX",
|
||||
"PAGER",
|
||||
"MSG",
|
||||
"CELL",
|
||||
"VIDEO",
|
||||
"BBS",
|
||||
"MODEM",
|
||||
"ISDN",
|
||||
"PCS",
|
||||
"PREF",
|
||||
},
|
||||
value = "NUMBER",
|
||||
},
|
||||
EMAIL = {
|
||||
types = {
|
||||
"HOME",
|
||||
"WORK",
|
||||
"INTERNET",
|
||||
"PREF",
|
||||
"X400",
|
||||
},
|
||||
value = "USERID",
|
||||
},
|
||||
JABBERID = "text",
|
||||
MAILER = "text",
|
||||
TZ = "text",
|
||||
GEO = {
|
||||
values = {
|
||||
"LAT",
|
||||
"LON",
|
||||
},
|
||||
},
|
||||
TITLE = "text",
|
||||
ROLE = "text",
|
||||
LOGO = "copy of PHOTO",
|
||||
AGENT = "text",
|
||||
ORG = {
|
||||
values = {
|
||||
behaviour = "repeat-last",
|
||||
"ORGNAME",
|
||||
"ORGUNIT",
|
||||
}
|
||||
},
|
||||
CATEGORIES = {
|
||||
values = "KEYWORD",
|
||||
},
|
||||
NOTE = "text",
|
||||
PRODID = "text",
|
||||
REV = "text",
|
||||
SORTSTRING = "text",
|
||||
SOUND = "copy of PHOTO",
|
||||
UID = "text",
|
||||
URL = "text",
|
||||
CLASS = {
|
||||
names = { -- The item.name is the value if it's one of these.
|
||||
"PUBLIC",
|
||||
"PRIVATE",
|
||||
"CONFIDENTIAL",
|
||||
},
|
||||
},
|
||||
KEY = {
|
||||
props = { "TYPE" },
|
||||
value = "CRED",
|
||||
},
|
||||
DESC = "text",
|
||||
};
|
||||
vCard_dtd.LOGO = vCard_dtd.PHOTO;
|
||||
vCard_dtd.SOUND = vCard_dtd.PHOTO;
|
||||
|
||||
return {
|
||||
from_text = from_text;
|
||||
to_text = to_text;
|
||||
|
||||
from_xep54 = from_xep54;
|
||||
to_xep54 = to_xep54;
|
||||
|
||||
-- COMPAT:
|
||||
lua_to_text = to_text;
|
||||
lua_to_xep54 = to_xep54;
|
||||
|
||||
text_to_lua = from_text;
|
||||
text_to_xep54 = function (...) return to_xep54(from_text(...)); end;
|
||||
|
||||
xep54_to_lua = from_xep54;
|
||||
xep54_to_text = function (...) return to_text(from_xep54(...)) end;
|
||||
};
|
67
mod_register_web/README.markdown
Normal file
67
mod_register_web/README.markdown
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
labels:
|
||||
- 'Stage-Alpha'
|
||||
summary: A web interface to register user accounts
|
||||
rockspec:
|
||||
build:
|
||||
copy_directories:
|
||||
- templates
|
||||
...
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
There are various reasons to prefer web registration instead of
|
||||
"in-band" account registration over XMPP. For example the lack of
|
||||
CAPTCHA support in clients and servers.
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
mod\_register\_web has Prosody serve a web page where users can sign up
|
||||
for an account. It implements reCAPTCHA to prevent automated sign-ups
|
||||
(from bots, etc.).
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The module is served on Prosody's default HTTP ports at the path
|
||||
`/register_web`. More details on configuring HTTP modules in Prosody can
|
||||
be found in our [HTTP documentation](http://prosody.im/doc/http).
|
||||
|
||||
To configure the CAPTCHA you need to supply a 'captcha\_options' option:
|
||||
|
||||
captcha_options = {
|
||||
recaptcha_private_key = "12345";
|
||||
recaptcha_public_key = "78901";
|
||||
}
|
||||
|
||||
The keys for reCAPTCHA are available in your reCAPTCHA account, visit
|
||||
[reCAPTCHA](https://developers.google.com/recaptcha/) for more info.
|
||||
|
||||
If no reCaptcha options are set, a simple built in captcha is used.
|
||||
|
||||
Customization
|
||||
-------------
|
||||
|
||||
Copy the files in mod_register_web/templates/ to a new directory. Edit them,
|
||||
and set `register_web_template = "/path/to/your/custom-templates"` in your
|
||||
config file.
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
----- --------------
|
||||
0.10 Works
|
||||
0.9 Works
|
||||
0.8 Doesn't work
|
||||
----- --------------
|
||||
|
||||
Todo
|
||||
----
|
||||
|
||||
Different CAPTCHA implementation support
|
||||
|
||||
Collection of additional data, such as email address
|
||||
|
||||
The module kept simple!
|
244
mod_register_web/mod_register_web.lua
Normal file
244
mod_register_web/mod_register_web.lua
Normal file
@ -0,0 +1,244 @@
|
||||
local captcha_options = module:get_option("captcha_options", {});
|
||||
local nodeprep = require "util.encodings".stringprep.nodeprep;
|
||||
local usermanager = require "core.usermanager";
|
||||
local datamanager = require "util.datamanager";
|
||||
local http = require "net.http";
|
||||
local path_sep = package.config:sub(1,1);
|
||||
local json = require "util.json".decode;
|
||||
local t_concat = table.concat;
|
||||
|
||||
pcall(function ()
|
||||
module:depends("register_limits");
|
||||
end);
|
||||
|
||||
module:depends"http";
|
||||
|
||||
local extra_fields = {
|
||||
nick = true; name = true; first = true; last = true; email = true;
|
||||
address = true; city = true; state = true; zip = true;
|
||||
phone = true; url = true; date = true;
|
||||
}
|
||||
|
||||
local template_path = module:get_option_string("register_web_template", "templates");
|
||||
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)
|
||||
local fh = assert(module:load_resource(template_path..path_sep..name..".html"));
|
||||
local data = assert(fh:read("*a"));
|
||||
fh:close();
|
||||
return template(data);
|
||||
end
|
||||
|
||||
local function render(template, data)
|
||||
return tostring(template.apply(data));
|
||||
end
|
||||
|
||||
local register_tpl = get_template "register";
|
||||
local success_tpl = get_template "success";
|
||||
|
||||
-- COMPAT `or request.conn:ip()`
|
||||
|
||||
if next(captcha_options) ~= nil then
|
||||
local provider = captcha_options.provider;
|
||||
if provider == nil or provider == "recaptcha" then
|
||||
local recaptcha_tpl = get_template "recaptcha";
|
||||
|
||||
function generate_captcha(display_options)
|
||||
return recaptcha_tpl.apply(setmetatable({
|
||||
recaptcha_display_error = display_options and display_options.recaptcha_error
|
||||
and ("&error="..display_options.recaptcha_error) or "";
|
||||
}, {
|
||||
__index = function (_, k)
|
||||
if captcha_options[k] then return captcha_options[k]; end
|
||||
module:log("error", "Missing parameter from captcha_options: %s", k);
|
||||
end
|
||||
}));
|
||||
end
|
||||
function verify_captcha(request, form, callback)
|
||||
http.request("https://www.google.com/recaptcha/api/siteverify", {
|
||||
body = http.formencode {
|
||||
secret = captcha_options.recaptcha_private_key;
|
||||
remoteip = request.ip or request.conn:ip();
|
||||
response = form["g-recaptcha-response"];
|
||||
};
|
||||
}, function (verify_result, code)
|
||||
local result = json(verify_result);
|
||||
if not result then
|
||||
module:log("warn", "Unable to decode response from recaptcha: [%d] %s", code, verify_result);
|
||||
callback(false, "Captcha API error");
|
||||
elseif result.success == true then
|
||||
callback(true);
|
||||
else
|
||||
callback(false, t_concat(result["error-codes"]));
|
||||
end
|
||||
end);
|
||||
end
|
||||
elseif provider == "hcaptcha" then
|
||||
local captcha_tpl = get_template "hcaptcha";
|
||||
|
||||
function generate_captcha(display_options)
|
||||
return captcha_tpl.apply(setmetatable({
|
||||
captcha_display_error = display_options and display_options.captcha_error
|
||||
and ("&error="..display_options.captcha_error) or "";
|
||||
}, {
|
||||
__index = function (_, k)
|
||||
if captcha_options[k] then return captcha_options[k]; end
|
||||
module:log("error", "Missing parameter from captcha_options: %s", k);
|
||||
end
|
||||
}));
|
||||
end
|
||||
function verify_captcha(request, form, callback)
|
||||
http.request("https://hcaptcha.com/siteverify", {
|
||||
body = http.formencode {
|
||||
secret = captcha_options.captcha_private_key;
|
||||
remoteip = request.ip or request.conn:ip();
|
||||
response = form["h-captcha-response"];
|
||||
};
|
||||
}, function (verify_result, code)
|
||||
local result = json(verify_result);
|
||||
if not result then
|
||||
module:log("warn", "Unable to decode response from hcaptcha: [%d] %s", code, verify_result);
|
||||
callback(false, "Captcha API error");
|
||||
elseif result.success == true then
|
||||
callback(true);
|
||||
else
|
||||
callback(false, t_concat(result["error-codes"]));
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
else
|
||||
module:log("debug", "No captcha options set, using fallback captcha")
|
||||
local random = math.random;
|
||||
local hmac_sha1 = require "util.hashes".hmac_sha1;
|
||||
local secret = require "util.uuid".generate()
|
||||
local ops = { '+', '-' };
|
||||
local captcha_tpl = get_template "simplecaptcha";
|
||||
function generate_captcha()
|
||||
local op = ops[random(1, #ops)];
|
||||
local x, y = random(1, 9)
|
||||
repeat
|
||||
y = random(1, 9);
|
||||
until x ~= y;
|
||||
local answer;
|
||||
if op == '+' then
|
||||
answer = x + y;
|
||||
elseif op == '-' then
|
||||
if x < y then
|
||||
-- Avoid negative numbers
|
||||
x, y = y, x;
|
||||
end
|
||||
answer = x - y;
|
||||
end
|
||||
local challenge = hmac_sha1(secret, answer, true);
|
||||
return captcha_tpl.apply {
|
||||
op = op, x = x, y = y, challenge = challenge;
|
||||
};
|
||||
end
|
||||
function verify_captcha(request, form, callback)
|
||||
if hmac_sha1(secret, form.captcha_reply or "", true) == form.captcha_challenge then
|
||||
callback(true);
|
||||
else
|
||||
callback(false, "Captcha verification failed");
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function generate_page(event, display_options)
|
||||
local request, response = event.request, event.response;
|
||||
|
||||
response.headers.content_type = "text/html; charset=utf-8";
|
||||
return render(register_tpl, {
|
||||
path = request.path; hostname = module.host;
|
||||
notice = display_options and display_options.register_error or "";
|
||||
captcha = generate_captcha(display_options);
|
||||
})
|
||||
end
|
||||
|
||||
function register_user(form, origin)
|
||||
local username = form.username;
|
||||
local password = form.password;
|
||||
local confirm_password = form.confirm_password;
|
||||
local jid = nil;
|
||||
form.username, form.password, form.confirm_password = nil, nil, nil;
|
||||
|
||||
local prepped_username = nodeprep(username, true);
|
||||
if not prepped_username then
|
||||
return nil, "Username contains forbidden characters";
|
||||
end
|
||||
if #prepped_username == 0 then
|
||||
return nil, "The username field was empty";
|
||||
end
|
||||
if usermanager.user_exists(prepped_username, module.host) then
|
||||
return nil, "Username already taken";
|
||||
end
|
||||
local registering = { username = prepped_username , host = module.host, additional = form, ip = origin.ip or origin.conn:ip(), allowed = true }
|
||||
module:fire_event("user-registering", registering);
|
||||
if not registering.allowed then
|
||||
return nil, registering.reason or "Registration not allowed";
|
||||
end
|
||||
if confirm_password ~= password then
|
||||
return nil, "Passwords don't match";
|
||||
end
|
||||
local ok, err = usermanager.create_user(prepped_username, password, module.host);
|
||||
if ok then
|
||||
jid = prepped_username.."@"..module.host
|
||||
local extra_data = {};
|
||||
for field in pairs(extra_fields) do
|
||||
local field_value = form[field];
|
||||
if field_value and #field_value > 0 then
|
||||
extra_data[field] = field_value;
|
||||
end
|
||||
end
|
||||
if next(extra_data) ~= nil then
|
||||
datamanager.store(prepped_username, module.host, "account_details", extra_data);
|
||||
end
|
||||
module:fire_event("user-registered", {
|
||||
username = prepped_username,
|
||||
host = module.host,
|
||||
source = module.name,
|
||||
ip = origin.ip or origin.conn:ip(),
|
||||
});
|
||||
end
|
||||
return jid, err;
|
||||
end
|
||||
|
||||
function generate_success(event, jid)
|
||||
return render(success_tpl, { jid = jid });
|
||||
end
|
||||
|
||||
function generate_register_response(event, jid, err)
|
||||
event.response.headers.content_type = "text/html; charset=utf-8";
|
||||
if jid then
|
||||
return generate_success(event, jid);
|
||||
else
|
||||
return generate_page(event, { register_error = err });
|
||||
end
|
||||
end
|
||||
|
||||
function handle_form(event)
|
||||
local request, response = event.request, event.response;
|
||||
local form = http.formdecode(request.body);
|
||||
verify_captcha(request, form, function (ok, err)
|
||||
if ok then
|
||||
local jid, register_err = register_user(form, request);
|
||||
response:send(generate_register_response(event, jid, register_err));
|
||||
else
|
||||
response:send(generate_page(event, { register_error = err }));
|
||||
end
|
||||
end);
|
||||
return true; -- Leave connection open until we respond above
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
title = module:get_option_string("register_web_title", "Account Registration");
|
||||
route = {
|
||||
GET = generate_page;
|
||||
["GET /"] = generate_page;
|
||||
POST = handle_form;
|
||||
["POST /"] = handle_form;
|
||||
};
|
||||
});
|
6
mod_register_web/templates/hcaptcha.html
Normal file
6
mod_register_web/templates/hcaptcha.html
Normal file
@ -0,0 +1,6 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<script src="https://hcaptcha.com/1/api.js" async defer></script>
|
||||
<div class="h-captcha" data-sitekey="{captcha_public_key}"></div>
|
||||
</td>
|
||||
</tr>
|
26
mod_register_web/templates/recaptcha.html
Normal file
26
mod_register_web/templates/recaptcha.html
Normal file
@ -0,0 +1,26 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
<div class="g-recaptcha" data-sitekey="{recaptcha_public_key}"></div>
|
||||
<noscript>
|
||||
<div style="width: 302px; height: 352px;">
|
||||
<div style="width: 302px; height: 352px; position: relative;">
|
||||
<div style="width: 302px; height: 352px; position: absolute;">
|
||||
<iframe src="https://www.google.com/recaptcha/api/fallback?k={recaptcha_public_key}"
|
||||
frameborder="0" scrolling="no"
|
||||
style="width: 302px; height:352px; border-style: none;">
|
||||
</iframe>
|
||||
</div>
|
||||
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
|
||||
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
|
||||
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
|
||||
class="g-recaptcha-response"
|
||||
style="width: 250px; height: 80px; border: 1px solid #c1c1c1;
|
||||
margin: 0px; padding: 0px; resize: none;" value="">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</td>
|
||||
</tr>
|
33
mod_register_web/templates/register.html
Normal file
33
mod_register_web/templates/register.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>XMPP Account Registration</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>XMPP Account Registration</h1>
|
||||
<form action="{path}" method="POST">
|
||||
<p>{notice}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Username:</th>
|
||||
<td><input name="username" required>@{hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password:</th>
|
||||
<td><input name="password" required type="password"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Confirm Password:</th>
|
||||
<td><input name="confirm_password" required type="password"></td>
|
||||
</tr>
|
||||
{captcha}
|
||||
<tr>
|
||||
<td colspan="2"><input type="submit" value="Register!"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
5
mod_register_web/templates/simplecaptcha.html
Normal file
5
mod_register_web/templates/simplecaptcha.html
Normal file
@ -0,0 +1,5 @@
|
||||
<tr>
|
||||
<th>What is {x} {op} {y}?</th><td>
|
||||
<input name="captcha_challenge" type="hidden" value="{challenge}">
|
||||
<input name="captcha_reply" pattern="[0-9]+" required type="number">
|
||||
</td></tr>
|
13
mod_register_web/templates/success.html
Normal file
13
mod_register_web/templates/success.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Registration succeeded!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Registration succeeded!</h1>
|
||||
<p>Your account is</p>
|
||||
<pre>{jid}</pre>
|
||||
<p>- happy chatting!</p>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user