25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-26 16:26:24 +01:00

Added mod_register_web: web page for account registration (EJAB-471)

This commit is contained in:
Badlop 2010-11-04 22:38:48 +01:00
parent 659d546897
commit d456578b3c
4 changed files with 656 additions and 0 deletions

View File

@ -84,6 +84,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{\modservicelog}{\module{mod\_service\_log}}
\newcommand{\modsharedroster}{\module{mod\_shared\_roster}}
@ -2583,6 +2584,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{modservicelog}{\modservicelog{}} & Copy user messages to logger service & \\
\hline \ahrefloc{modsharedroster}{\modsharedroster{}} & Shared roster management & \modroster{} \\
@ -3879,6 +3881,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}

View File

@ -172,6 +172,7 @@
captcha,
http_bind,
http_poll,
register,
web_admin
]}
@ -570,6 +571,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,[]},

View File

@ -115,6 +115,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
@ -125,6 +126,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 -> []

View File

@ -0,0 +1,598 @@
%%%-------------------------------------------------------------------
%%% File : mod_register_web.erl
%%% Author : Badlop <badlop@process-one.net>
%%% Purpose : Web page to register account and related tasks
%%% Created : 4 May 2008 by Badlop <badlop@process-one.net>
%%%
%%%
%%% 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_lib("exmpp/include/exmpp.hrl").
-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 = exmpp_jid:make(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
%%%----------------------------------------------------------------------
make_xa_link_css() ->
?XA('link', [?XMLATTR('href', <<"/register/register.css">>),
?XMLATTR('type', <<"text/css">>),
?XMLATTR('rel', <<"stylesheet">>)]).
make_h1_title(TextString, Lang) ->
?XACT('h1',
[?XMLATTR('class', <<"title">>), ?XMLATTR('style', <<"text-align:center;">>)],
TextString).
index_page(Lang) ->
HeadEls = [
?XCT('title', "Jabber Account Registration"),
make_xa_link_css()
],
Els=[
make_h1_title("Jabber Account Registration", Lang),
?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"),
make_xa_link_css()
],
Els=[
make_h1_title("Register a Jabber account", Lang),
?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."),
%% <!-- JID's take the form of 'username@server.com'. For example, my JID is 'kirjava@jabber.org'.
%% The maximum length for a JID is 255 characters. -->
?XAE('form', [?XMLATTR('action', <<"">>), ?XMLATTR('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</b> (opcional)<b>:</b> <input type="text" size="20" name="name" maxlength="255"> <br /> <br /> -->
%%
%% Direcci&oacute;n de correo</b> (opcional)<b>:</b> <input type="text" size="20" name="email" maxlength="255"> <br /> <br /> -->
?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 = exmpp_jid:prep_domain_as_list(UJID),
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(), exmpp_jid:to_list(UJID),
ip_to_string(Source), node(), ?MODULE])),
lists:foreach(
fun(S) ->
JID = exmpp_jid:parse(S),
ejabberd_router:route(
exmpp_jid:make("", Host, ""),
JID,
exmpp_message:chat(Body))
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, IdS, KeyS] ->
form_new_post(Username, Host, Password, {IdS, KeyS});
[_Username, _Password, _Password2, false, false] ->
{error, passwords_not_identical};
[_Username, _Password, _Password2, IdS, KeyS] ->
ejabberd_captcha:check_captcha(IdS, KeyS), %% 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) ->
SID = <<"">>,
From = exmpp_jid:make("", "test", ""),
To = exmpp_jid:make("", "test", ""),
Args = [],
{ok, Id, [_Body, _OOB, _Captcha, _Data]} = ejabberd_captcha:create_captcha(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"),
make_xa_link_css()
],
Els=[
make_h1_title("Change Password", Lang),
?XAE('form', [?XMLATTR('action', <<"">>), ?XMLATTR('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"),
make_xa_link_css()
],
Els=[
make_h1_title("Unregister a Jabber account", Lang),
?XCT('p',
"This page allows to unregister a Jabber account in this Jabber server."),
?XAE('form', [?XMLATTR('action', <<"">>), ?XMLATTR('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".