diff --git a/mod_email/README.markdown b/mod_email/README.markdown new file mode 100644 index 0000000..c566f6b --- /dev/null +++ b/mod_email/README.markdown @@ -0,0 +1,54 @@ +--- +labels: +- 'Stage-Stable' +... + +Introduction +------------ + +Gives server owners and module developers the ability to send emails. + +Usage +----- + +For users and developers, add the following to your configuration file and +edit depending on your SMTP setup: + +smtp = { + origin = "username@hostname.domainname"; + + -- Use sendmail and have sendmail handle SMTP + exec = "/usr/sbin/sendmail"; + + -- Use SMTP directly + --user = "username"; + --password = "password"; + --server = "localhost"; + --port = 25; + --domain = "hostname.domainname"; +} + +For developers you can do something like this to send emails from your module: + +local moduleapi = require "core.moduleapi"; +module:depends("email"); +local ok, err = moduleapi:send_email({to = mail_address, subject = mail_subject, body = mail_body}); +if not ok then + module:log("error", "Failed to deliver to %s: %s", tostring(mail_address), tostring(err)); +end + +Todo +---- + +- Loading socket.smtp causes a stack trace on Prosody start up. + Everything still works fine, but this should probably be fixed. + +- No SSL/STARTTLS support. This will require implementing something like LuaSec. + If needed, I would recommend to just set up OpenSMTPD and use sendmail. + +Compatibility +------------- + + ----- -------------- + 0.12 Works + ----- -------------- diff --git a/mod_email_pass/README.markdown b/mod_email_pass/README.markdown index 7cda2ab..a99e8fe 100644 --- a/mod_email_pass/README.markdown +++ b/mod_email_pass/README.markdown @@ -6,44 +6,42 @@ labels: Introduction ============ -This module aims to help in the procedure of user password restoration. +This module aims to help 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. +The module will generate a token valid for 1h and send an email with a +specially crafted URL to the email address set in account details (not vCard). +If the user goes to this URL, the user 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. +Add "email\_pass" to your modules\_enabled list and copy the mod\_email\_pass +folder to the Prosody modules folder. -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. +This module also requires the "email" module which you can find here: +https://modules.prosody.im/mod\_email.html + +This module only sends emails to the user email address set in Prosody account +details. +The user must set this email address in order to be capable of do the +restoration by going to: /email\_pass/changemail.html 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/ - --------------- ------------------------------------------------------------ + --------------- ------------------------------------------------------------ --------------------- + token\_expire Time in seconds before token expires 3600 + attempt\_limit Maximum attempts for 'update email'/'reset with token' page 10 + attempt\_wait Time in seconds to block IP from going beyond limit 1800 + email\_pass\_url URL prefix used for sending the token https://hostname:5281 + --------------- ------------------------------------------------------------ --------------------- Compatibility ============= ----- ------- - 0.9 Works + 0.12 Works + 0.9 Works ----- ------- diff --git a/mod_email_pass/mod_email_pass.lua b/mod_email_pass/mod_email_pass.lua index a188736..a8a9c8d 100644 --- a/mod_email_pass/mod_email_pass.lua +++ b/mod_email_pass/mod_email_pass.lua @@ -3,23 +3,13 @@ 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"; +local moduleapi = require "core.moduleapi"; --- 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"); +local token_expire = module:get_option_number("token_expire", 3600); -- This table has the tokens submited by the server @@ -27,30 +17,18 @@ 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 email_pass_url = module:get_option_string("email_pass_url", "https://" .. module.host .. ":5281"); 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 +local account_details = module:open_store("account_details"); + +local attempt_limit = module:get_option_number("attempt_limit", 10); +local attempt_wait = module:get_option_number("attempt_wait", 1800); +attempt_wait_list = {}; +attempt_wait_list_time = {}; + +module:depends("email"); function template(data) -- Like util.template, but deals with plain text @@ -68,91 +46,139 @@ 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 changemail_tpl = get_template("changemail",".html"); local changepass_tpl = get_template("changepass",".html"); local sendmail_success_tpl = get_template("sendmailok",".html"); local reset_success_tpl = get_template("resetok",".html"); +local update_success_tpl = get_template("updateok",".html"); local token_tpl = get_template("token",".html"); -function generate_page(event, display_options) +function generate_page(event, lang, display_options) local request = event.request; + -- begin translation + if lang == "Español" then + s_title = "Reseteo de la contraseña de tu cuenta Jabber"; + s_username = "Nombre de Usuario"; + s_usernamemessage = "Introduce tu nombre de usuario"; + s_email = "Email"; + s_emailmessage = "Introduce tu email"; + s_send = "¡Enviar!"; + s_text = "Al pulsar sobre el botón, se enviará a la dirección de correo que figura en tu cuenta un enlace en el que deberás entrar."; + else + s_title = "Reset your Jabber account password"; + s_username = "Username"; + s_usernamemessage = "Enter your username"; + s_email = "Email"; + s_emailmessage = "Enter your email"; + s_send = "Send!"; + s_text = "When you click the button, a link will be sent to the email address in your account."; + end + -- end translation + return render(changepass_tpl, { - path = request.path; hostname = module.host; + path = request.path; + hostname = module.host; notice = display_options and display_options.register_error or ""; - }) + s_title = s_title; + s_username = s_username; + s_usernamemessage = s_usernamemessage; + s_email = s_email; + s_emailmessage = s_emailmessage; + s_send = s_send; + s_text = s_text; + s_lang = lang; + }); end -function generate_token_page(event, display_options) - local request = event.request; +function generate_token_page(event, lang, 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 ""; - }) + -- begin translation + if lang == "Español" then + s_title = "Reseto de la contraseña de tu cuenta Jabber"; + s_token = "Token"; + s_password = "Contraseña"; + s_passwordconfirm = "Contraseña (Confirmació)"; + s_change = "¡Cambiar!"; + else + s_title = "Reset the password for your Jabber account"; + s_token = "Token"; + s_password = "Password"; + s_passwordconfirm = "Password (Confirm)"; + s_change = "Change!"; + end + -- end translation + + return render(token_tpl, { + path = request.path; + hostname = module.host; + token = request.url.query; + notice = display_options and display_options.register_error or ""; + s_title = s_title; + s_token = s_token; + s_password = s_password; + s_passwordconfirm = s_passwordconfirm; + s_change = s_change; + s_lang = lang; + }); +end + +function generate_mail_page(event, lang, display_options) + local request = event.request; + + -- begin translation + if lang == "Español" then + s_title = "Cambiar la eMail de tu cuenta Jabber"; + s_username = "Nombre de Usuario"; + s_usernamemessage = "Introduce tu nombre de usuario"; + s_password = "Contraseña"; + s_passwordmessage = "Introduce tu contraseña"; + s_email = "Email"; + s_emailmessage = "Introduce tu email"; + s_change = "¡Cambiar!"; + else + s_title = "Change the eMail of your Jabber account"; + s_username = "Username"; + s_usernamemessage = "Enter your username"; + s_password = "Password"; + s_passwordmessage = "Enter your password"; + s_email = "Email"; + s_emailmessage = "Enter your email"; + s_change = "Change!"; + end + -- end translation + + return render(changemail_tpl, { + path = request.path; + hostname = module.host; + notice = display_options and display_options.register_error or ""; + s_title = s_title; + s_username = s_username; + s_usernamemessage = s_usernamemessage; + s_password = s_password; + s_passwordmessage = s_passwordmessage; + s_email = s_email; + s_emailmessage = s_emailmessage; + s_change = s_change; + s_lang = lang; + }); 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)) + + 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 + end + local token = table.concat(array); if not tokens_mails[token] then - tokens_mails[token] = address; tokens_expiration[token] = os.time(); return token @@ -166,7 +192,7 @@ 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 + if os.difftime(os.time(), tokens_expiration[token]) < token_expire then -- token is valid yet return nil; else @@ -179,7 +205,7 @@ end 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]); + module:log("info", "Expiring password reset request from user '%s', not used.", tokens_mails[token]); tokens_mails[token] = nil; tokens_expiration[token] = nil; end @@ -197,121 +223,427 @@ function hasTokenActive(address) return nil; end -function generateUrl(token) +function generateUrl(token, path) 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; + url = email_pass_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); + 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); + if form.langchange == "true" then + return nil; + end + + local lang = form.lang; + local prepped_username = nodeprep(form.username, true); 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"; + if not prepped_username then + -- begin translation + if lang == "Español" then + return nil, "El nombre de usuario contiene caracteres incorrectos"; + else + return nil, "The username contains invalid characters"; end - - email = string.lower(vcarduser.EMAIL[1]); - - if email ~= string.lower(prepped_mail) then - return nil, "Dirección eMail incorrecta"; + -- end translation + end + if #prepped_username == 0 then + -- begin translation + if lang == "Español" then + return nil, "El campo texto de nombre de usuario está vacío"; + else + return nil, "The username text field is empty"; 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 translation + end + if not usermanager.user_exists(prepped_username, module.host) then + -- begin translation + if lang == "Español" then + return nil, "El usuario no existe"; + else + return nil, "The user does not exist"; 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 translation end + if #prepped_mail == 0 then + -- begin translation + if lang == "Español" then + return nil, "El campo texto de email está vacío"; + else + return nil, "The email text field is empty"; + end + -- end translation + end + + local account_detail_table = account_details:get(prepped_username); + if account_detail_table == nil then + account_detail_table = {["email"] = ""}; + end + local account_detail_email = account_detail_table["email"]; + if not account_detail_email then + -- begin translation + if lang == "Español" then + return nil, "El cuente no tiene ningún email configurado"; + else + return nil, "The account has no email configured"; + end + -- end translation + end + email = string.lower( account_detail_email ); + + if #email == 0 then + -- begin translation + if lang == "Español" then + return nil, "El cuente no tiene ningún email configurado"; + else + return nil, "The account has no email configured"; + end + -- end translation + end + + if email ~= string.lower(prepped_mail) then + -- begin translation + if lang == "Español" then + return nil, "eMail incorrecta"; + else + return nil, "Incorrect eMail"; + end + -- end translation + end + + -- Check if has already a valid token, not used yet. + if hasTokenActive(jid) then + local valid_until = tokens_expiration[hasTokenActive(jid)] + token_expire; + -- begin translation + if lang == "Español" then + return nil, "Ya tienes una petición de reseteada de contraseña, válida hasta: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); + else + return nil, "You already have a password reset request, valid until: " .. datetime.date(valid_until) .. " " .. datetime.time(valid_until); + end + -- end translation + end + + local url_path = origin.path; + local url_token = generateToken(jid); + local url = generateUrl(url_token, url_path); + + local mail_subject = nil; + local mail_body = nil; + -- begin translation + if lang == "Español" then + mail_subject = "Jabber/XMPP - Reseto de la Contraseña"; + mail_body = render( get_template("sendtoken", ".mail"), {jid = jid, url = url, time = ((token_expire / 60) / 60), host = module.host} ); + else + mail_subject = "Jabber/XMPP - Password Reset"; + mail_body = render( get_template("sendtoken-en", ".mail"), {jid = jid, url = url, time = ((token_expire / 60) / 60), host = module.host} ); + end + -- end translation + + module:log("info", "Sending password reset mail to user %s", jid); + local ok, err = moduleapi:send_email({to = email, subject = mail_subject, body = mail_body}); + if not ok then + module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); + end + + -- begin translation + if lang == "Español" then + mail_subject = "Gestión de Cuentas"; + mail_body = "Token para reseto su contraseña ha sido enviado a su email por el sistema de reseto de contraseña."; + else + mail_subject = "Account Management"; + mail_body = "Token to reset your password has been sent to your email by password reset system."; + end + -- end translation + + sendMessage(jid, mail_subject, mail_body); + + return "ok"; end function reset_password_with_token(form, origin) + local lang = form.lang; + + if form.langchange == "true" then + return nil; + end + if attempt_wait_list[origin.ip] == nil then attempt_wait_list[origin.ip] = 0; end + if attempt_wait_list[origin.ip] >= attempt_limit then + if os.difftime(os.time(), attempt_wait_list_time[origin.ip]) > attempt_wait then + attempt_wait_list[origin.ip] = 0; + attempt_wait_list_time[origin.ip] = 0; + else + module:log("info", "Too many attempts at guessing token from IP %s", origin.ip); + -- begin translation + if lang == "Español" then + return nil, "Demasiados intentos. Inténtalo otra vez en "..(attempt_wait / 60).."m."; + else + return nil, "Too many attempts. Try again in "..(attempt_wait / 60).."m."; + end + -- end translation + end + end + local token = form.token; local password = form.newpassword; + local passwordconfirmation = form.newpasswordconfirmation; + form.newpassword, form.newpasswordconfirmation = nil, nil; if not token then - return nil, "El Token es inválido"; + -- begin translation + if lang == "Español" then + return nil, "El Token es inválido"; + else + return nil, "The token is invalid"; + end + -- end translation end if not tokens_mails[token] then - return nil, "El Token no existe o ya fué usado"; + attempt_wait_list[origin.ip] = attempt_wait_list[origin.ip]+1; + attempt_wait_list_time[origin.ip] = os.time(); + + -- begin translation + if lang == "Español" then + return nil, "El Token no existe o ya fué usado"; + else + return nil, "The token does not exist or has already been used"; + end + -- end translation end if not password then - return nil, "La campo clave no puede estar vacio"; + -- begin translation + if lang == "Español" then + return nil, "El campo texto de contraseña está vacío"; + else + return nil, "The password text field is empty"; + end + -- end translation end if #password < 5 then - return nil, "La clave debe tener una longitud de al menos 5 caracteres"; + -- begin translation + if lang == "Español" then + return nil, "El contraseña debe tener una longitud de al menos cinco caracteres"; + else + return nil, "The password must be at least five characters long"; + end + -- end translation + end + if password ~= passwordconfirmation then + -- begin translation + if lang == "Español" then + return nil, "El confirmació de contraseña es inválido"; + else + return nil, "The password confirmation is invalid"; + end + -- end translation end local jid = tokens_mails[token]; local user, host, resource = jidutil.split(jid); + local mail_subject = nil; + local mail_body = nil; + + -- begin translation + if lang == "Español" then + mail_subject = "Gestión de Cuentas"; + mail_body = "La contraseña se ha cambiado con el sistema de reseto de contraseña."; + else + mail_subject = "Account Management"; + mail_body = "Password has been changed with password reset system."; + end + -- end translation + 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); + + -- begin translation + if lang == "Español" then + mail_subject = "Jabber/XMPP - Contraseña Cambiado"; + mail_body = render( get_template("updatepass", ".mail"), {jid = jid, host = module.host} ); + else + mail_subject = "Jabber/XMPP - Password Changed"; + mail_body = render( get_template("updatepass-en", ".mail"), {jid = jid, host = module.host} ); + end + -- end translation + + module:log("info", "Sending password update mail to user %s", jid); + local ok, err = moduleapi:send_email({to = email, subject = mail_subject, body = mail_body}); + if not ok then + module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); + end + return "ok"; end -function generate_success(event, form) - return render(sendmail_success_tpl, { jid = nodeprep(form.username).."@"..module.host }); +function change_mail_with_password(form, origin) + local lang = form.lang; + + if form.langchange == "true" then + return nil; + end + if attempt_wait_list[origin.ip] == nil then attempt_wait_list[origin.ip] = 0; end + if attempt_wait_list[origin.ip] >= attempt_limit then + if os.difftime(os.time(), attempt_wait_list_time[origin.ip]) > attempt_wait then + attempt_wait_list[origin.ip] = 0; + attempt_wait_list_time[origin.ip] = 0; + else + module:log("info", "Too many attempts at guessing password from IP %s", origin.ip); + -- begin translation + if lang == "Español" then + return nil, "Demasiados intentos. Inténtalo otra vez en "..(attempt_wait / 60).."m"; + else + return nil, "Too many attempts. Try again in "..(attempt_wait / 60).."m"; + end + -- end translation + end + end + + local prepped_username = nodeprep(form.username, true); + local prepped_mail = form.email; + local password = form.password; + local jid = prepped_username .. "@" .. module.host; + form.password = nil; + + if not prepped_username then + -- begin translation + if lang == "Español" then + return nil, "El nombre de usuario contiene caracteres incorrectos"; + else + return nil, "The username contains invalid characters"; + end + -- end translation + end + if #prepped_username == 0 then + -- begin translation + if lang == "Español" then + return nil, "El campo texto de nombre de usuario está vacio"; + else + return nil, "The username text field is empty"; + end + -- end translation + end + if not usermanager.user_exists(prepped_username, module.host) then + -- begin translation + if lang == "Español" then + return nil, "El usuario no existe"; + else + return nil, "The user does not exist"; + end + -- end translation + end + + if not password then + -- begin translation + if lang == "Español" then + return nil, "El campo texto de contraseña está vacío"; + else + return nil, "The password text field is empty"; + end + -- end translation + end + + + if usermanager.test_password(prepped_username, module.host, password) then + local account_detail_table = account_details:get(prepped_username); + if account_detail_table == nil then + account_detail_table = {["email"] = ""}; + end + local remove_email; + if prepped_mail == "" then + remove_email = true; + else + remove_email = false; + end + email = string.lower(prepped_mail); + account_detail_table["email"] = email; + account_details:set(prepped_username, account_detail_table); + module:log("info", "Email changed with password for user %s", jid); + + local mail_subject = nil; + local mail_body = nil; + + -- begin translation + if lang == "Español" then + mail_subject = "Gestión de Cuentas"; + mail_body = "El email se ha actualizado a <"..email.."> con el sistema de reseto de contraseña."; + else + mail_subject = "Account Management"; + mail_body = "Email has been updated to <"..email.."> with password reset system."; + end + -- end translation + + sendMessage(jid, mail_subject, mail_body); + + if not remove_email then + -- begin translation + if lang == "Español" then + mail_subject = "Jabber/XMPP - Email Cambiado"; + mail_body = render( get_template("updatemail", ".mail"), {jid = jid, email = email, host = module.host} ); + else + mail_subject = "Jabber/XMPP - Email Changed"; + mail_body = render( get_template("updatemail-en", ".mail"), {jid = jid, email = email, host = module.host} ); + end + -- end translation + + module:log("info", "Sending email update mail to user %s", jid); + local ok, err = moduleapi:send_email({to = email, subject = mail_subject, body = mail_body}); + if not ok then + module:log("error", "Failed to deliver to %s: %s", tostring(address), tostring(err)); + end + end + + return "ok"; + else + attempt_wait_list[origin.ip] = attempt_wait_list[origin.ip]+1; + attempt_wait_list_time[origin.ip] = os.time(); + + -- begin translation + if lang == "Español" then + return nil, "Contraseña incorrecta"; + else + return nil, "Incorrect password"; + end + -- end translation + end +end + +function generate_success(event, lang, username) + -- begin translation + if lang == "Español" then + s_title = "¡Enlace enviado!"; + s_text = "Acabamos de enviarte un email con un enlace que tendrás que visitar."; + else + s_title = "Link sent!"; + s_text = "We just sent you an email with a link you’ll need to visit."; + end + -- end translation + + return render(sendmail_success_tpl, { + jid = nodeprep(username, true).."@"..module.host; + s_title = s_title; + s_text = s_text; + }); end function generate_register_response(event, form, ok, err) local message; if ok then - return generate_success(event, form); + return generate_success(event, form.lang, form.username); else - return generate_page(event, { register_error = err }); + return generate_page(event, form.lang, { register_error = err }); end end @@ -320,43 +652,100 @@ function handle_form_token(event) 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)); + 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, { }); +function generate_reset_success(event, lang) + -- begin translation + if lang == "Español" then + s_title = "¡Contraseña reseteada!"; + s_p1 = "Tu contraseña ha sido cambiada."; + s_p2 = "Ya puedes iniciar sesión de Jabber."; + else + s_title = "Password reset!"; + s_p1 = "Your password has been changed."; + s_p2 = "You can now log into Jabber."; + end + -- end translation + + return render(reset_success_tpl, { + s_title = s_title; + s_p1 = s_p1; + s_p2 = s_p2; + }); +end + +function generate_update_success(event, lang, email) + -- begin translation + if lang == "Español" then + s_title = "¡Email cambiada!"; + s_p1 = "Tu email ha sido cambiada."; + s_p2 = "Nuevo email"; + else + s_title = "Email changed!"; + s_p1 = "Your email has been changed."; + s_p2 = "New email"; + end + -- end translation + return render(update_success_tpl, { + s_title = s_title; + s_p1 = s_p1; + s_p2 = s_p2; + email = email; + }); 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 + local message; + if ok then + return generate_reset_success(event, form.lang); + else + return generate_token_page(event, form.lang, { register_error = err }); + end +end + +function generate_update_response(event, form, ok, err) + local message; + if ok then + return generate_update_success(event, form.lang, form.email); + else + return generate_mail_page(event, form.lang, { register_error = err }); + end end function handle_form_reset(event) local request, response = event.request, event.response; - local form = http.formdecode(request.body); + 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)); + 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 + return true; -- Leave connection open until we respond above +end +function handle_form_mailchange(event) + local request, response = event.request, event.response; + local form = http.formdecode(request.body); + + local change_ok, change_err = change_mail_with_password(form, request); + response:send(generate_update_response(event, form, change_ok, change_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 /style.css"] = render(get_template("style",".css"), {}); + ["GET /changepass.html"] = generate_page; + ["GET /changemail.html"] = generate_mail_page; ["GET /token.html"] = generate_token_page; ["GET /"] = generate_page; + ["POST /changepass.html"] = handle_form_token; + ["POST /changemail.html"] = handle_form_mailchange; ["POST /token.html"] = handle_form_reset; ["POST /"] = handle_form_token; }; diff --git a/mod_email_pass/templates/changemail.html b/mod_email_pass/templates/changemail.html new file mode 100644 index 0000000..07910df --- /dev/null +++ b/mod_email_pass/templates/changemail.html @@ -0,0 +1,52 @@ + + + + + + + {s_title} + + +
+

{s_title}

+
+
+
+
+ + + +
+
+
+
+
+

{notice}

+ + @{hostname} +
+ + +
+ + +
+ + + + +
+
+
+ + + diff --git a/mod_email_pass/templates/changepass.html b/mod_email_pass/templates/changepass.html index b3f87bb..c72e29f 100644 --- a/mod_email_pass/templates/changepass.html +++ b/mod_email_pass/templates/changepass.html @@ -1,35 +1,49 @@ - - - Reseteo de la clave de tu cuenta Jabber + + + + {s_title} +
+

{s_title}

+
+
+
+
+ + + +
+
+
-

Reseteo de la clave de tu cuenta Jabber

{notice}

@{hostname}
- + + +

- 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.
+ {s_text}

+
diff --git a/mod_email_pass/templates/resetok.html b/mod_email_pass/templates/resetok.html index ceeb8b7..586a9a2 100644 --- a/mod_email_pass/templates/resetok.html +++ b/mod_email_pass/templates/resetok.html @@ -1,14 +1,15 @@ - - - Clave reseteada! + + + {s_title}
-

Tu clave ha sido cambiada correctamente. Ya puedes iniciar sesión con ella.

+

{s_p1}

+

{s_p2}

diff --git a/mod_email_pass/templates/sendmailok.html b/mod_email_pass/templates/sendmailok.html index 2c3be4c..642b977 100644 --- a/mod_email_pass/templates/sendmailok.html +++ b/mod_email_pass/templates/sendmailok.html @@ -1,14 +1,14 @@ - - - Enlace enviado! + + + {s_title}
-

Acabamos de enviarte un email con un enlace que tendrás que visitar.

+

{s_text}

diff --git a/mod_email_pass/templates/sendtoken-en.mail b/mod_email_pass/templates/sendtoken-en.mail new file mode 100644 index 0000000..cd3336d --- /dev/null +++ b/mod_email_pass/templates/sendtoken-en.mail @@ -0,0 +1,16 @@ +Hello + +You have received this email because you have requested the +reset of your Jabber/XMPP account password. + +To proceed with the password change, use the following link: + +Account ID: {jid} +Reset Link: {url} + +If you have not requested to reset your password, ignore this message. +The link expires in {time}h. + +Sincerely, the team of {host} + + diff --git a/mod_email_pass/templates/sendtoken.mail b/mod_email_pass/templates/sendtoken.mail index 75193b7..064daa6 100644 --- a/mod_email_pass/templates/sendtoken.mail +++ b/mod_email_pass/templates/sendtoken.mail @@ -1,14 +1,16 @@ -Hola: +Hola -Si has recibido este email es porque has solicitado el reseteo de la -clave de tu cuenta Jabber/XMPP {jid} +Tú tienen recibido este email porque has solicitado el reseteo de la +contraseña de tu cuenta Jabber/XMPP. -Para proceder con el cambio de clave, haz click en el siguiente enlace: +Para proceder con el cambio de contraseña, usar el siguiente enlace: -{url} +ID de Cuenta: {jid} +Enlace de Reseteo: {url} -Si no has solicitado resetear tu clave, ignora este mensaje. +Si no has solicitado resetear tu contraseña, ignora este mensaje. +El enlace expira en {time}h. -Atentamente, el equipo de mijabber.es +Atentamente, el equipo de {host} diff --git a/mod_email_pass/templates/style.css b/mod_email_pass/templates/style.css index aabd0e6..4cf87ba 100644 --- a/mod_email_pass/templates/style.css +++ b/mod_email_pass/templates/style.css @@ -1,29 +1,59 @@ -body{ +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;} +h1 { + text-align:center; +} + +p, h1, form, button { + border:0; + margin:0; + padding:0; +} + +.spacer { + clear:both; + height:1px; +} /* ----------- My Form ----------- */ -.formulario{ +.formulario { margin:0 auto; width:500px; padding:14px; } + +.formulario2 { + margin:0 auto; + width:500px; + padding:14px; +} + /* ----------- stylized ----------- */ #estilo { border:solid 2px #b7ddf2; background:#ebf4fb; } - + +#estilo2 { + border-left:solid 2px #b7ddf2; + border-right:solid 2px #b7ddf2; + background:#ebf4fb; +} + +#estilo3 { + border:solid 2px #b7ddf2; + background:#ebf4fb; +} + #estilo h1 { font-size:14px; font-weight:bold; margin-bottom:8px; } - + #estilo p { font-size:11px; color:#666666; @@ -39,17 +69,18 @@ p, h1, form, button{border:0; margin:0; padding:0;} margin-bottom:20px; border-bottom:solid 1px #b7ddf2; padding-bottom:10px; + text-align:center; } - -#estilo label{ + +#estilo label { display:block; font-weight:bold; text-align:right; width:140px; float:left; } - -#estilo .small{ + +#estilo .small { color:#666666; display:block; font-size:11px; @@ -58,52 +89,72 @@ p, h1, form, button{border:0; margin:0; padding:0;} width:140px; } -#estilo input{ - float:left; +#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; +} + +#estilo2 div { + text-align:center; +} + +#estilo2 input { + padding:5px; +} + +#estilo3 h1 { + font-size:14px; + font-weight:bold; + margin-bottom:8px; +} + +#estilo .button { + margin:2px 150px 20px 150px; +} + +.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 index c1fed05..baa7b34 100644 --- a/mod_email_pass/templates/token.html +++ b/mod_email_pass/templates/token.html @@ -1,30 +1,50 @@ - - Reseto de la clave de tu cuenta Jabber + + + {s_title} -
-

Reseteo de la clave de tu cuenta Jabber

+
+

{s_title}

+
+
+
+
+ + + +
+
+
+

{notice}

- + + +
+ +
+ +
-
+
diff --git a/mod_email_pass/templates/updatemail-en.mail b/mod_email_pass/templates/updatemail-en.mail new file mode 100644 index 0000000..9832f01 --- /dev/null +++ b/mod_email_pass/templates/updatemail-en.mail @@ -0,0 +1,11 @@ +Hello + +You have received this email because your Jabber/XMPP +account email has been changed. + +Account ID: {jid} +Account Email: {email} + +Sincerely, the team of {host} + + diff --git a/mod_email_pass/templates/updatemail.mail b/mod_email_pass/templates/updatemail.mail new file mode 100644 index 0000000..2aa473b --- /dev/null +++ b/mod_email_pass/templates/updatemail.mail @@ -0,0 +1,11 @@ +Hola + +Tú tienen recibido este email porque has cambiado el email de tu +cuenta de Jabber/XMPP. + +ID de Cuenta: {jid} +Email de Cuenta: {email} + +Atentamente, el equipo de {host} + + diff --git a/mod_email_pass/templates/updateok.html b/mod_email_pass/templates/updateok.html new file mode 100644 index 0000000..4d32bc5 --- /dev/null +++ b/mod_email_pass/templates/updateok.html @@ -0,0 +1,17 @@ + + + + + + + {s_title} + + +
+

{s_p1}

+

{s_p2}:

+

{email}

+
+ + + diff --git a/mod_email_pass/templates/updatepass-en.mail b/mod_email_pass/templates/updatepass-en.mail new file mode 100644 index 0000000..0e5bca5 --- /dev/null +++ b/mod_email_pass/templates/updatepass-en.mail @@ -0,0 +1,10 @@ +Hello + +You have received this email because your Jabber/XMPP +account password has been reset. + +Account ID: {jid} + +Sincerely, the team of {host} + + diff --git a/mod_email_pass/templates/updatepass.mail b/mod_email_pass/templates/updatepass.mail new file mode 100644 index 0000000..6a40a01 --- /dev/null +++ b/mod_email_pass/templates/updatepass.mail @@ -0,0 +1,10 @@ +Hola + +Tú tienen recibido este email porque has reseteo la contraseña de tu +cuenta de Jabber/XMPP. + +ID de Cuenta: {jid} + +Atentamente, el equipo de {host} + + diff --git a/mod_email_pass/vcard.lib.lua b/mod_email_pass/vcard.lib.lua deleted file mode 100644 index 2406023..0000000 --- a/mod_email_pass/vcard.lib.lua +++ /dev/null @@ -1,464 +0,0 @@ --- 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 index d6a9aed..7292ab4 100644 --- a/mod_register_web/README.markdown +++ b/mod_register_web/README.markdown @@ -19,27 +19,35 @@ 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.). +for an account. It implements a CAPTCHA system 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). +be found in our [HTTP documentation](https://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"; + -- provider = "recaptcha"; + -- recaptcha_private_key = "12345"; + -- recaptcha_public_key = "78901"; + + -- provider = "hcaptcha"; + -- hcaptcha_private_key = "12345"; + -- hcaptcha_public_key = " 78901"; + + ---- Default if no reCaptcha or hCaptcha options are set + -- provider = "simplecaptcha"; } -The keys for reCAPTCHA are available in your reCAPTCHA account, visit -[reCAPTCHA](https://developers.google.com/recaptcha/) for more info. +To only allow registration through this module, enter in the following +to your configuration: -If no reCaptcha options are set, a simple built in captcha is used. +registration_web_only = true Customization ------------- @@ -52,6 +60,7 @@ Compatibility ------------- ----- -------------- + 0.12 Works 0.10 Works 0.9 Works 0.8 Doesn't work diff --git a/mod_register_web/mod_register_web.lua b/mod_register_web/mod_register_web.lua index df59b87..6698c93 100644 --- a/mod_register_web/mod_register_web.lua +++ b/mod_register_web/mod_register_web.lua @@ -25,8 +25,8 @@ function template(data) 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 function get_template(name, extension) + local fh = assert(module:load_resource(template_path..path_sep..name..extension)); local data = assert(fh:read("*a")); fh:close(); return template(data); @@ -36,15 +36,30 @@ local function render(template, data) return tostring(template.apply(data)); end -local register_tpl = get_template "register"; -local success_tpl = get_template "success"; +local register_tpl = get_template("register", ".html"); +local success_tpl = get_template("success", ".html"); + +local web_verified; +local web_only = module:get_option_boolean("registration_web_only", false); +if web_only then + -- from mod_invites_register.lua + module:hook("user-registering", function (event) + local web_verified = event.web_verified; + + if not web_verified then + event.allowed = false; + event.reason = "Registration on this server is through website only"; + return; + end + end); +end -- 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"; + local recaptcha_tpl = get_template("recaptcha", ".html"); function generate_captcha(display_options) return recaptcha_tpl.apply(setmetatable({ @@ -77,7 +92,7 @@ if next(captcha_options) ~= nil then end); end elseif provider == "hcaptcha" then - local captcha_tpl = get_template "hcaptcha"; + local captcha_tpl = get_template("hcaptcha", ".html"); function generate_captcha(display_options) return captcha_tpl.apply(setmetatable({ @@ -93,7 +108,7 @@ if next(captcha_options) ~= nil then function verify_captcha(request, form, callback) http.request("https://hcaptcha.com/siteverify", { body = http.formencode { - secret = captcha_options.captcha_private_key; + secret = captcha_options.hcaptcha_private_key; remoteip = request.ip or request.conn:ip(); response = form["h-captcha-response"]; }; @@ -116,8 +131,16 @@ else 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 captcha_tpl = get_template("simplecaptcha", ".html"); + function generate_captcha(display_options, lang) + -- begin translation + if lang == "Español" then + s_question = "¿Qué es"; + else + s_question = "What is"; + end + -- end translation + local op = ops[random(1, #ops)]; local x, y = random(1, 9) repeat @@ -135,53 +158,113 @@ else end local challenge = hmac_sha1(secret, answer, true); return captcha_tpl.apply { - op = op, x = x, y = y, challenge = challenge; + op = op, x = x, y = y, challenge = challenge, s_question = s_question; }; 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"); + -- begin translation + if form.lang == "Español" then + callback(false, "Verificación de Captcha fallida"); + else + callback(false, "Captcha verification failed"); + end + -- end translation end end end -function generate_page(event, display_options) +function generate_page(event, lang, display_options) local request, response = event.request, event.response; + -- begin translation + if lang == "Español" then + s_title = "Registro de cuenta XMPP"; + s_username = "Nombre de Usuario"; + s_password = "Contraseña"; + s_passwordconfirm = "Contraseña Confirmación"; + s_register = "¡Registro!"; + else + s_title = "XMPP Account Registration"; + s_username = "Username"; + s_password = "Password"; + s_passwordconfirm = "Confirm Password"; + s_register = "Register!"; + end + -- end translation + 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); - }) + captcha = generate_captcha(display_options, lang); + s_title = s_title; + s_username = s_username; + s_password = s_password; + s_passwordconfirm = s_passwordconfirm; + s_register = s_register; + s_lang = lang; + }); end function register_user(form, origin) + local lang = form.lang; 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; + form.password, form.confirm_password = nil, nil; local prepped_username = nodeprep(username, true); if not prepped_username then - return nil, "Username contains forbidden characters"; + -- begin translation + if lang == "Español" then + return nil, "Nombre de usuario contiene caracteres prohibidos"; + else + return nil, "Username contains forbidden characters"; + end + -- end translation end if #prepped_username == 0 then - return nil, "The username field was empty"; + -- begin translation + if lang == "Español" then + return nil, "El campo texto de nombre de usuario estaba vacío"; + else + return nil, "The username field was empty"; + end + -- end translation end if usermanager.user_exists(prepped_username, module.host) then - return nil, "Username already taken"; + -- begin translation + if lang == "Español" then + return nil, "Nombre de usuario ya ocupado"; + else + return nil, "Username already taken"; + end + -- end translation end - local registering = { username = prepped_username , host = module.host, additional = form, ip = origin.ip or origin.conn:ip(), allowed = true } + + local registering = { username = prepped_username , host = module.host, additional = form, ip = origin.ip or origin.conn:ip(), allowed = true, web_verified = true } module:fire_event("user-registering", registering); if not registering.allowed then - return nil, registering.reason or "Registration not allowed"; + -- begin translation + if lang == "Español" then + return nil, registering.reason or "Registro no permitido"; + else + return nil, registering.reason or "Registration not allowed"; + end + -- end translation end if confirm_password ~= password then - return nil, "Passwords don't match"; + -- begin translation + if lang == "Español" then + return nil, "Las contraseñas no igualar"; + else + return nil, "Passwords don't match"; + end + -- end translation end local ok, err = usermanager.create_user(prepped_username, password, module.host); if ok then @@ -202,20 +285,41 @@ function register_user(form, origin) source = module.name, ip = origin.ip or origin.conn:ip(), }); + module:log("info", "New Account Registered: %s#%s@%s", prepped_username, origin.ip, module.host); end + return jid, err; end -function generate_success(event, jid) - return render(success_tpl, { jid = jid }); +function generate_success(event, jid, lang) + local request, response = event.request, event.response; + + -- begin translation + if lang == "Español" then + s_title = "¡Registro exitoso!"; + s_message = "Tu cuenta es"; + else + s_title = "Registration succeeded!"; + s_message = "Your account is"; + end + -- end translation + + response.headers.content_type = "text/html; charset=utf-8"; + return render(success_tpl, { + path = request.path; + jid = jid; + lang = lang; + s_title = s_title; + s_message = s_message; + }); end -function generate_register_response(event, jid, err) +function generate_register_response(event, jid, lang, err) event.response.headers.content_type = "text/html; charset=utf-8"; if jid then - return generate_success(event, jid); + return generate_success(event, jid, lang); else - return generate_page(event, { register_error = err }); + return generate_page(event, lang, { register_error = err }); end end @@ -225,9 +329,9 @@ function handle_form(event) 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)); + response:send(generate_register_response(event, jid, form.lang, register_err)); else - response:send(generate_page(event, { register_error = err })); + response:send(generate_page(event, form.lang, { register_error = err })); end end); return true; -- Leave connection open until we respond above @@ -236,6 +340,7 @@ end module:provides("http", { title = module:get_option_string("register_web_title", "Account Registration"); route = { + ["GET /style.css"] = render(get_template("style", ".css"), {}); GET = generate_page; ["GET /"] = generate_page; POST = handle_form; diff --git a/mod_register_web/templates/register.html b/mod_register_web/templates/register.html index 287e392..6a42b18 100644 --- a/mod_register_web/templates/register.html +++ b/mod_register_web/templates/register.html @@ -2,32 +2,47 @@ - XMPP Account Registration + + + {s_title} -

XMPP Account Registration

-
-

{notice}

- - - - - - - - - - - - - - - {captcha} - - - - -
Username:@{hostname}
Password:
Confirm Password:
-
+
+

{s_title}

+
+
+
+
+ + +
+
+
+
+

{notice}

+
+ + + + + + + + + + + + + + + {captcha} + + + + + +
{s_username}:@{hostname}
{s_password}:
{s_passwordconfirm}:
+
+
diff --git a/mod_register_web/templates/simplecaptcha.html b/mod_register_web/templates/simplecaptcha.html index ad2122b..c50157f 100644 --- a/mod_register_web/templates/simplecaptcha.html +++ b/mod_register_web/templates/simplecaptcha.html @@ -1,5 +1,7 @@ - What is {x} {op} {y}? + {s_question} {x} {op} {y}? + - + + diff --git a/mod_register_web/templates/style.css b/mod_register_web/templates/style.css new file mode 100644 index 0000000..8cfc04f --- /dev/null +++ b/mod_register_web/templates/style.css @@ -0,0 +1,164 @@ +body { + font-family:"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif; + font-size:12px; +} + +h1 { + text-align:center; +} + +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; +} + +.formulario2 { + margin:0 auto; + width:500px; + padding:14px; +} + +/* ----------- stylized ----------- */ +#estilo { + border:solid 2px #f2ddb7; + background:#fbf4eb; +} + +#estilo2 { + border-left:solid 2px #f2ddb7; + border-right:solid 2px #f2ddb7; + background:#fbf4eb; +} + +#estilo3 { + border:solid 2px #f2ddb7; + background:#fbf4eb; +} + +#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 #f2ddb7; + padding-bottom:10px; +} + +#estilo p.error { + font-size:12px; + font-weight:bold; + color:red; + margin-bottom:20px; + border-bottom:solid 1px #f2ddb7; + padding-bottom:10px; + text-align:center; +} + +#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 #e4cfaa; + width:200px; + margin:2px 0 20px 10px; +} + +#estilo th { + padding-bottom:18px; +} + +#estilo2 div { + text-align:center; +} + +#estilo2 input { + padding:5px; +} + +#estilo3 h1 { + font-size:14px; + font-weight:bold; + margin-bottom:8px; +} + +#estilo .button { + margin:2px 150px 20px 150px; +} + +.button { + -moz-box-shadow:inset 0px 1px 0px 0px #fce3ca; + -webkit-box-shadow:inset 0px 1px 0px 0px #fce3ca; + box-shadow:inset 0px 1px 0px 0px #fce3ca; + background-color:#ffbb79; + -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 #f59d46; + 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 #ce7a28; +} + +.button:hover { + background-color:#ee9741; +} + +.button:active { + position:relative; + top:1px; +} + diff --git a/mod_register_web/templates/success.html b/mod_register_web/templates/success.html index 1f4c11e..6aa4956 100644 --- a/mod_register_web/templates/success.html +++ b/mod_register_web/templates/success.html @@ -2,12 +2,20 @@ - Registration succeeded! + + + {s_title} -

Registration succeeded!

-

Your account is

-
{jid}
-

- happy chatting!

+
+

{s_title}

+

+

{s_message}:

+

{jid}

+ +