diff --git a/mod_email/mod_email.lua b/mod_email/mod_email.lua
new file mode 100644
index 0000000..5af0259
--- /dev/null
+++ b/mod_email/mod_email.lua
@@ -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
diff --git a/mod_email_pass/README.markdown b/mod_email_pass/README.markdown
new file mode 100644
index 0000000..7cda2ab
--- /dev/null
+++ b/mod_email_pass/README.markdown
@@ -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
+ ----- -------
diff --git a/mod_email_pass/mod_email_pass.lua b/mod_email_pass/mod_email_pass.lua
new file mode 100644
index 0000000..a188736
--- /dev/null
+++ b/mod_email_pass/mod_email_pass.lua
@@ -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;
+ };
+});
+
+
diff --git a/mod_email_pass/templates/changepass.html b/mod_email_pass/templates/changepass.html
new file mode 100644
index 0000000..b3f87bb
--- /dev/null
+++ b/mod_email_pass/templates/changepass.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Reseteo de la clave de tu cuenta Jabber
+
+
+
+
Reseteo de la clave de tu cuenta Jabber
+
+
+ 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.
+
Acabamos de enviarte un email con un enlace que tendrás que visitar.
+
+
+
+
diff --git a/mod_email_pass/templates/sendtoken.mail b/mod_email_pass/templates/sendtoken.mail
new file mode 100644
index 0000000..75193b7
--- /dev/null
+++ b/mod_email_pass/templates/sendtoken.mail
@@ -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
+
+
diff --git a/mod_email_pass/templates/style.css b/mod_email_pass/templates/style.css
new file mode 100644
index 0000000..aabd0e6
--- /dev/null
+++ b/mod_email_pass/templates/style.css
@@ -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;
+}
+
diff --git a/mod_email_pass/templates/token.html b/mod_email_pass/templates/token.html
new file mode 100644
index 0000000..c1fed05
--- /dev/null
+++ b/mod_email_pass/templates/token.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ Reseto de la clave de tu cuenta Jabber
+
+
+
+
Reseteo de la clave de tu cuenta Jabber
+
+
+
+
+
diff --git a/mod_email_pass/vcard.lib.lua b/mod_email_pass/vcard.lib.lua
new file mode 100644
index 0000000..2406023
--- /dev/null
+++ b/mod_email_pass/vcard.lib.lua
@@ -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;
+};
diff --git a/mod_register_web/README.markdown b/mod_register_web/README.markdown
new file mode 100644
index 0000000..d6a9aed
--- /dev/null
+++ b/mod_register_web/README.markdown
@@ -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!
diff --git a/mod_register_web/mod_register_web.lua b/mod_register_web/mod_register_web.lua
new file mode 100644
index 0000000..df59b87
--- /dev/null
+++ b/mod_register_web/mod_register_web.lua
@@ -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;
+ };
+});
diff --git a/mod_register_web/templates/hcaptcha.html b/mod_register_web/templates/hcaptcha.html
new file mode 100644
index 0000000..b9756a4
--- /dev/null
+++ b/mod_register_web/templates/hcaptcha.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mod_register_web/templates/recaptcha.html b/mod_register_web/templates/recaptcha.html
new file mode 100644
index 0000000..7853245
--- /dev/null
+++ b/mod_register_web/templates/recaptcha.html
@@ -0,0 +1,26 @@
+