import files from hg.prosody.im/prosody-modules

This commit is contained in:
edshot99 2024-03-12 22:33:03 -05:00
parent a5476269c6
commit ac47a4133b
17 changed files with 1540 additions and 0 deletions

49
mod_email/mod_email.lua Normal file
View 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

View 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
----- -------

View 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;
};
});

View 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&oacute;n, se enviar&aacute; a la direcci&oacute;n de correo que figura
en tu vCard un enlace en el que deber&aacute;s entrar.<br />
</p>
</div>
</body>
</html>

View 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&oacute;n con ella.</h1>
</div>
</body>
</html>

View 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&aacute;s que visitar.</h1>
</div>
</body>
</html>

View 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

View 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;
}

View 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&ntilde;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>

View 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;
};

View 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!

View 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;
};
});

View 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>

View 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>

View 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>

View 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>

View 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>