From f672fd0824004d0d4ce2207b9dfb0b596a539aef Mon Sep 17 00:00:00 2001 From: Badlop Date: Thu, 4 Nov 2010 22:34:18 +0100 Subject: [PATCH] Added mod_register_web: web page for account registration (EJAB-471) --- doc/guide.tex | 45 +++ src/ejabberd.cfg.example | 8 + src/web/ejabberd_http.erl | 5 + src/web/mod_register_web.erl | 613 +++++++++++++++++++++++++++++++++++ 4 files changed, 671 insertions(+) create mode 100644 src/web/mod_register_web.erl diff --git a/doc/guide.tex b/doc/guide.tex index 51804e226..4fdb43b22 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -88,6 +88,7 @@ \newcommand{\modpubsub}{\module{mod\_pubsub}} \newcommand{\modpubsubodbc}{\module{mod\_pubsub\_odbc}} \newcommand{\modregister}{\module{mod\_register}} +\newcommand{\modregisterweb}{\module{mod\_register\_web}} \newcommand{\modroster}{\module{mod\_roster}} \newcommand{\modrosterodbc}{\module{mod\_roster\_odbc}} \newcommand{\modservicelog}{\module{mod\_service\_log}} @@ -2530,6 +2531,7 @@ The following table lists all modules included in \ejabberd{}. \hline \ahrefloc{modpubsub}{\modpubsub{}} & Pub-Sub (\xepref{0060}), PEP (\xepref{0163}) & \modcaps{} \\ \hline \ahrefloc{modpubsub}{\modpubsubodbc{}} & Pub-Sub (\xepref{0060}), PEP (\xepref{0163}) & supported DB (*) and \modcaps{} \\ \hline \ahrefloc{modregister}{\modregister{}} & In-Band Registration (\xepref{0077}) & \\ + \hline \ahrefloc{modregisterweb}{\modregisterweb{}} & Web for Account Registrations & \\ \hline \ahrefloc{modroster}{\modroster{}} & Roster management (XMPP IM) & \\ \hline \ahrefloc{modroster}{\modrosterodbc{}} & Roster management (XMPP IM) & supported DB (*) \\ \hline \ahrefloc{modservicelog}{\modservicelog{}} & Copy user messages to logger service & \\ @@ -3862,6 +3864,49 @@ Also define a registration timeout of one hour: \end{verbatim} \end{itemize} +\makesubsection{modregisterweb}{\modregisterweb{}} +\ind{modules!\modregisterweb{}} + +This module provides a web page where people can: +\begin{itemize} +\item Register a new account on the server. +\item Change the password from an existing account on the server. +\item Delete an existing account on the server. +\end{itemize} + +This module supports CAPTCHA image to register a new account. +To enable this feature, configure the options captcha\_cmd and captcha\_host. + +Options: +\begin{description} +\titem{\{registration\_watchers, [ JID, ...]\}} \ind{options!rwatchers}This option defines a + list of JIDs which will be notified each time a new account is registered. +\end{description} + +This example configuration shows how to enable the module and the web handler: +\begin{verbatim} +{listen, [ + ... + {5281, ejabberd_http, [ + tls, + {certfile, "/etc/ejabberd/certificate.pem"}, + register + ]}, + ... +]}. + +{modules, + [ + ... + {mod_register_web, []}, + ... + ]}. +\end{verbatim} + +The users can visit this page: https://localhost:5281/register/ +It is important to include the last / character in the URL, +otherwise the subpages URL will be incorrect. + \makesubsection{modroster}{\modroster{}} \ind{modules!\modroster{}}\ind{roster management}\ind{protocols!RFC 3921: XMPP IM} diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index d05ca8e67..96495d7e5 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -162,6 +162,7 @@ captcha, http_bind, http_poll, + register, web_admin ]} @@ -546,6 +547,13 @@ {access, register} ]}, + {mod_register_web, [ + %% + %% When a user registers, send a notification to + %% these XMPP accounts. + %% + %%{registration_watchers, ["admin1@example.org"]} + ]}, {mod_roster, []}, %%{mod_service_log,[]}, {mod_shared_roster,[]}, diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 2f2315017..313e0f264 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -111,6 +111,7 @@ init({SockMod, Socket}, Opts) -> %% web_admin -> {["admin"], ejabberd_web_admin} %% http_bind -> {["http-bind"], mod_http_bind} %% http_poll -> {["http-poll"], ejabberd_http_poll} + %% register -> {["register"], mod_register_web} RequestHandlers = case lists:keysearch(request_handlers, 1, Opts) of @@ -121,6 +122,10 @@ init({SockMod, Socket}, Opts) -> true -> [{["captcha"], ejabberd_captcha}]; false -> [] end ++ + case lists:member(register, Opts) of + true -> [{["register"], mod_register_web}]; + false -> [] + end ++ case lists:member(web_admin, Opts) of true -> [{["admin"], ejabberd_web_admin}]; false -> [] diff --git a/src/web/mod_register_web.erl b/src/web/mod_register_web.erl new file mode 100644 index 000000000..3da93dde8 --- /dev/null +++ b/src/web/mod_register_web.erl @@ -0,0 +1,613 @@ +%%%------------------------------------------------------------------- +%%% File : mod_register_web.erl +%%% Author : Badlop +%%% Purpose : Web page to register account and related tasks +%%% Created : 4 May 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2010 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License +%%% along with this program; if not, write to the Free Software +%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +%%% IDEAS: +%%% +%%% * Implement those options, already present in mod_register: +%%% + access +%%% + captcha_protected +%%% + password_strength +%%% + welcome_message +%%% + registration_timeout +%%% +%%% * Improve this module to allow each virtual host to have different +%%% options. See http://support.process-one.net/browse/EJAB-561 +%%% +%%% * Check that all the text is translatable. +%%% +%%% * Add option to use a custom CSS file, or custom CSS lines. +%%% +%%% * Don't hardcode the "register" path in URL. +%%% +%%% * Allow private email during register, and store in custom table. +%%% * Optionally require private email to register. +%%% * Optionally require email confirmation to register. +%%% * Allow to set a private email address anytime. +%%% * Allow to recover password using private email to confirm (mod_passrecover) +%%% * Optionally require invitation +%%% * Optionally register request is forwarded to admin, no account created. + +-module(mod_register_web). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, + stop/1, + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). + +%%%---------------------------------------------------------------------- +%%% gen_mod callbacks +%%%---------------------------------------------------------------------- + +start(_Host, _Opts) -> + %% case gen_mod:get_opt(docroot, Opts, undefined) of + ok. + +stop(_Host) -> + ok. + +%%%---------------------------------------------------------------------- +%%% HTTP handlers +%%%---------------------------------------------------------------------- + +process([], #request{method = 'GET', lang = Lang}) -> + index_page(Lang); + +process(["register.css"], #request{method = 'GET'}) -> + serve_css(); + +process(["new"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_new_get(Host, Lang); + +process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_del_get(Host, Lang); + +process(["change_password"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_changepass_get(Host, Lang); + +process(["new"], #request{method = 'POST', q = Q, ip = {Ip,_Port}, lang = Lang, host = Host}) -> + case form_new_post(Q, Host) of + {success, ok, {Username, Host, _Password}} -> + Jid = jlib:make_jid(Username, Host, ""), + send_registration_notifications(Jid, Ip), + Text = ?T("Your Jabber account was succesfully created."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error creating the account: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end; + +process(["delete"], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> + case form_del_post(Q, Host) of + {atomic, ok} -> + Text = ?T("Your Jabber account was succesfully deleted."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error deleting the account: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end; + +%% TODO: Currently only the first vhost is usable. The web request record +%% should include the host where the POST was sent. +process(["change_password"], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> + case form_changepass_post(Q, Host) of + {atomic, ok} -> + Text = ?T("The password of your Jabber account was succesfully changed."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error changing the password: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end. + +%%%---------------------------------------------------------------------- +%%% CSS +%%%---------------------------------------------------------------------- + +serve_css() -> + {200, [{"Content-Type", "text/css"}, + last_modified(), cache_control_public()], css()}. + +last_modified() -> + {"Last-Modified", "Mon, 25 Feb 2008 13:23:30 GMT"}. +cache_control_public() -> + {"Cache-Control", "public"}. + +css() -> + "html,body { +background: white; +margin: 0; +padding: 0; +height: 100%; +}". + +%%%---------------------------------------------------------------------- +%%% Index page +%%%---------------------------------------------------------------------- + +index_page(Lang) -> + HeadEls = [ + ?XCT("title", "Jabber Account Registration"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Jabber Account Registration"), + ?XE("ul", [ + ?XE("li", [?ACT("new", "Register a Jabber account")]), + ?XE("li", [?ACT("change_password", "Change Password")]), + ?XE("li", [?ACT("delete", "Unregister a Jabber account")]) + ] + ) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%%%---------------------------------------------------------------------- +%%% Formulary new account GET +%%%---------------------------------------------------------------------- + +form_new_get(Host, Lang) -> + CaptchaEls = build_captcha_li_list(Lang), + HeadEls = [ + ?XCT("title", "Register a Jabber account"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Register a Jabber account"), + ?XCT("p", + "This page allows to create a Jabber account in this Jabber server. " + "Your JID (Jabber IDentifier) will be of the form: username@server. " + "Please read carefully the instructions to fill correctly the fields."), + %% + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20"), + ?BR, + ?XE("ul", [ + ?XCT("li", "This is case insensitive: macbeth is the same that MacBeth and Macbeth."), + ?XCT("li", "Characters not allowed: @ : ' \" < > &") + ]) + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20"), + ?BR, + ?XE("ul", [ + ?XCT("li", "Don't tell your password to anybody, " + "not even the administrators of the Jabber server."), + ?XCT("li", "You can later change your password using a Jabber client."), + ?XCT("li", "Some Jabber clients can store your password in your computer. " + "Use that feature only if you trust your computer is safe."), + ?XCT("li", "Memorize your password, or write it in a paper placed in a safe place. " + "In Jabber there isn't an automated way to recover your password if you forget it.") + ]) + ]), + ?XE("li", [ + ?CT("Password Verification:"), + ?C(" "), + ?INPUTS("password", "password2", "", "20") + ])] ++ CaptchaEls ++ [ + %% Nombre (opcional):

--> + %% + %% Dirección de correo (opcional):

--> + ?XE("li", [ + ?INPUTT("submit", "register", "Register") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%% Copied from mod_register.erl +send_registration_notifications(UJID, Source) -> + Host = UJID#jid.lserver, + case gen_mod:get_module_opt(Host, ?MODULE, registration_watchers, []) of + [] -> ok; + JIDs when is_list(JIDs) -> + Body = lists:flatten( + io_lib:format( + "[~s] The account ~s was registered from IP address ~s " + "on node ~w using ~p.", + [get_time_string(), jlib:jid_to_string(UJID), + ip_to_string(Source), node(), ?MODULE])), + lists:foreach( + fun(S) -> + case jlib:string_to_jid(S) of + error -> ok; + JID -> + ejabberd_router:route( + jlib:make_jid("", Host, ""), + JID, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], + [{xmlcdata, Body}]}]}) + end + end, JIDs); + _ -> + ok + end. +ip_to_string(Source) when is_tuple(Source) -> inet_parse:ntoa(Source); +ip_to_string(undefined) -> "undefined"; +ip_to_string(_) -> "unknown". +get_time_string() -> write_time(erlang:localtime()). +%% Function copied from ejabberd_logger_h.erl and customized +write_time({{Y,Mo,D},{H,Mi,S}}) -> + io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Y, Mo, D, H, Mi, S]). + +%%%---------------------------------------------------------------------- +%%% Formulary new POST +%%%---------------------------------------------------------------------- + +form_new_post(Q, Host) -> + case catch get_register_parameters(Q) of + [Username, Password, Password, Id, Key] -> + form_new_post(Username, Host, Password, {Id, Key}); + [_Username, _Password, _Password2, false, false] -> + {error, passwords_not_identical}; + [_Username, _Password, _Password2, Id, Key] -> + ejabberd_captcha:check_captcha(Id, Key), %% This deletes the captcha + {error, passwords_not_identical}; + _ -> + {error, wrong_parameters} + end. + +get_register_parameters(Q) -> + lists:map( + fun(Key) -> + case lists:keysearch(Key, 1, Q) of + {value, {_Key, Value}} -> Value; + false -> false + end + end, + ["username", "password", "password2", "id", "key"]). + +form_new_post(Username, Host, Password, {false, false}) -> + register_account(Username, Host, Password); +form_new_post(Username, Host, Password, {Id, Key}) -> + case ejabberd_captcha:check_captcha(Id, Key) of + captcha_valid -> + register_account(Username, Host, Password); + captcha_non_valid -> + {error, captcha_non_valid}; + captcha_not_found -> + {error, captcha_non_valid} + end. + +%%%---------------------------------------------------------------------- +%%% Formulary Captcha support for new GET/POST +%%%---------------------------------------------------------------------- + +build_captcha_li_list(Lang) -> + case ejabberd_captcha:is_feature_available() of + true -> build_captcha_li_list2(Lang); + false -> [] + end. + +build_captcha_li_list2(Lang) -> + Id = randoms:get_string(), + SID = "", + From = #jid{user = "", server = "test", resource = ""}, + To = #jid{user = "", server = "test", resource = ""}, + Args = [], + ejabberd_captcha:create_captcha(Id, SID, From, To, Lang, Args), + {_, {CImg,CText,CId,CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang), + [?XE("li", [CText, + ?C(" "), + CId, + CKey, + ?BR, + CImg] + )]. + +%%%---------------------------------------------------------------------- +%%% Formulary change password GET +%%%---------------------------------------------------------------------- + +form_changepass_get(Host, Lang) -> + HeadEls = [ + ?XCT("title", "Change Password"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Change Password"), + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20") + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Old Password:"), + ?C(" "), + ?INPUTS("password", "passwordold", "", "20") + ]), + ?XE("li", [ + ?CT("New Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20") + ]), + ?XE("li", [ + ?CT("Password Verification:"), + ?C(" "), + ?INPUTS("password", "password2", "", "20") + ]), + ?XE("li", [ + ?INPUTT("submit", "changepass", "Change Password") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%%%---------------------------------------------------------------------- +%%% Formulary change password POST +%%%---------------------------------------------------------------------- + +form_changepass_post(Q, Host) -> + case catch get_changepass_parameters(Q) of + [Username, PasswordOld, Password, Password] -> + try_change_password(Username, Host, PasswordOld, Password); + [_Username, _PasswordOld, _Password, _Password2] -> + {error, passwords_not_identical}; + _ -> + {error, wrong_parameters} + end. + +get_changepass_parameters(Q) -> + lists:map( + fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + ["username", "passwordold", "password", "password2"]). + +%% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | +%% {error, account_doesnt_exist} | +%% {error, password_not_changed} | +%% {error, password_incorrect} +try_change_password(Username, Host, PasswordOld, Password) -> + try change_password(Username, Host, PasswordOld, Password) of + {atomic, ok} -> + {atomic, ok} + catch + error:{badmatch, Error} -> + {error, Error} + end. + +change_password(Username, Host, PasswordOld, Password) -> + %% Check the account exists + account_exists = check_account_exists(Username, Host), + + %% Check the old password is correct + password_correct = check_password(Username, Host, PasswordOld), + + %% This function always returns: ok + %% Change the password + ok = ejabberd_auth:set_password(Username, Host, Password), + + %% Check the new password is correct + case check_password(Username, Host, Password) of + password_correct -> + {atomic, ok}; + password_incorrect -> + {error, password_not_changed} + end. + +check_account_exists(Username, Host) -> + case ejabberd_auth:is_user_exists(Username, Host) of + true -> account_exists; + false -> account_doesnt_exist + end. + +check_password(Username, Host, Password) -> + case ejabberd_auth:check_password(Username, Host, Password) of + true -> password_correct; + false -> password_incorrect + end. + +%%%---------------------------------------------------------------------- +%%% Formulary delete account GET +%%%---------------------------------------------------------------------- + +form_del_get(Host, Lang) -> + HeadEls = [ + ?XCT("title", "Unregister a Jabber account"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Unregister a Jabber account"), + ?XCT("p", + "This page allows to unregister a Jabber account in this Jabber server."), + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20") + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20") + ]), + ?XE("li", [ + ?INPUTT("submit", "unregister", "Unregister") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} | +%% {success, exists, {Username, Host, Password}} | +%% {error, not_allowed} | +%% {error, invalid_jid} +register_account(Username, Host, Password) -> + case ejabberd_auth:try_register(Username, Host, Password) of + {atomic, Res} -> + {success, Res, {Username, Host, Password}}; + Other -> + Other + end. + +%%%---------------------------------------------------------------------- +%%% Formulary delete POST +%%%---------------------------------------------------------------------- + +form_del_post(Q, Host) -> + case catch get_unregister_parameters(Q) of + [Username, Password] -> + try_unregister_account(Username, Host, Password); + _ -> + {error, wrong_parameters} + end. + +get_unregister_parameters(Q) -> + lists:map( + fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + ["username", "password"]). + +%% @spec(Username, Host, Password) -> {atomic, ok} | +%% {error, account_doesnt_exist} | +%% {error, account_exists} | +%% {error, password_incorrect} +try_unregister_account(Username, Host, Password) -> + try unregister_account(Username, Host, Password) of + {atomic, ok} -> + {atomic, ok} + catch + error:{badmatch, Error} -> + {error, Error} + end. + +unregister_account(Username, Host, Password) -> + %% Check the account exists + account_exists = check_account_exists(Username, Host), + + %% Check the password is correct + password_correct = check_password(Username, Host, Password), + + %% This function always returns: ok + ok = ejabberd_auth:remove_user(Username, Host, Password), + + %% Check the account does not exist anymore + account_doesnt_exist = check_account_exists(Username, Host), + + %% If we reached this point, return success + {atomic, ok}. + +%%%---------------------------------------------------------------------- +%%% Error texts +%%%---------------------------------------------------------------------- + +get_error_text({error, captcha_non_valid}) -> + "The captcha you entered is wrong"; +get_error_text({atomic, exists}) -> + "The account already exists"; +get_error_text({error, password_incorrect}) -> + "Incorrect password"; +get_error_text({error, invalid_jid}) -> + "The username is not valid"; +get_error_text({error, not_allowed}) -> + "Not allowed"; +get_error_text({error, account_doesnt_exist}) -> + "Account doesn't exist"; +get_error_text({error, account_exists}) -> + "The account was not deleted"; +get_error_text({error, password_not_changed}) -> + "The password was not changed"; +get_error_text({error, passwords_not_identical}) -> + "The passwords are different"; +get_error_text({error, wrong_parameters}) -> + "Wrong parameters in the web formulary".