%%%------------------------------------------------------------------- %%% 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-2020 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., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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, reload/3, process/2, mod_options/1, depends/2]). -export([mod_doc/0]). -include("logger.hrl"). -include("xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("translate.hrl"). %%%---------------------------------------------------------------------- %%% gen_mod callbacks %%%---------------------------------------------------------------------- start(_Host, _Opts) -> %% case mod_register_web_opt:docroot(Opts, fun(A) -> A end, undefined) of ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_register, hard}]. %%%---------------------------------------------------------------------- %%% 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, ip = IP}) -> case ejabberd_router:is_my_host(Host) of true -> {Addr, _Port} = IP, form_new_get(Host, Lang, Addr); false -> {400, [], <<"Host not served">>} end; process([<<"delete">>], #request{method = 'GET', lang = Lang, host = Host}) -> case ejabberd_router:is_my_host(Host) of true -> form_del_get(Host, Lang); false -> {400, [], <<"Host not served">>} end; process([<<"change_password">>], #request{method = 'GET', lang = Lang, host = Host}) -> case ejabberd_router:is_my_host(Host) of true -> form_changepass_get(Host, Lang); false -> {400, [], <<"Host not served">>} end; process([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, lang = Lang, host = _HTTPHost}) -> case form_new_post(Q) of {success, ok, {Username, Host, _Password}} -> Jid = jid:make(Username, Host), mod_register:send_registration_notifications(?MODULE, Jid, Ip), Text = translate:translate(Lang, ?T("Your Jabber account was successfully created.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error creating the account: ")), translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; process([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, host = _HTTPHost}) -> case form_del_post(Q) of {atomic, ok} -> Text = translate:translate(Lang, ?T("Your Jabber account was successfully deleted.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error deleting the account: ")), translate:translate(Lang, 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 = _HTTPHost}) -> case form_changepass_post(Q) of {atomic, ok} -> Text = translate:translate(Lang, ?T("The password of your Jabber account was successfully changed.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error changing the password: ")), translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; process(_Path, _Request) -> {404, [], "Not Found"}. %%%---------------------------------------------------------------------- %%% CSS %%%---------------------------------------------------------------------- serve_css() -> case css() of {ok, CSS} -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], CSS}; error -> {404, [], "CSS not found"} end. last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. -spec css() -> {ok, binary()} | error. css() -> Dir = misc:css_dir(), File = filename:join(Dir, "register.css"), case file:read_file(File) of {ok, Data} -> {ok, Data}; {error, Why} -> ?ERROR_MSG("Failed to read ~ts: ~ts", [File, file:format_error(Why)]), error end. meta() -> ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, {<<"content">>, <<"width=device-width, initial-scale=1">>}]). %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- index_page(Lang) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Jabber Account Registration")), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Jabber Account Registration")), ?XE(<<"ul">>, [?XE(<<"li">>, [?ACT(<<"new">>, ?T("Register a Jabber account"))]), ?XE(<<"li">>, [?ACT(<<"change_password">>, ?T("Change Password"))]), ?XE(<<"li">>, [?ACT(<<"delete">>, ?T("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, IP) -> try build_captcha_li_list(Lang, IP) of CaptchaEls -> form_new_get2(Host, Lang, CaptchaEls) catch throw:Result -> ?DEBUG("Unexpected result when creating a captcha: ~p", [Result]), ejabberd_web:error(not_allowed) end. form_new_get2(Host, Lang, CaptchaEls) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Register a Jabber account")), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Register a Jabber account")), ?XCT(<<"p">>, ?T("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(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, ?T("This is case insensitive: macbeth is " "the same that MacBeth and Macbeth.")), ?XC(<<"li">>, <<(translate:translate(Lang, ?T("Characters not allowed:")))/binary, " \" & ' / : < > @ ">>)])]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, ?T("Don't tell your password to anybody, " "not even the administrators of the Jabber " "server.")), ?XCT(<<"li">>, ?T("You can later change your password using " "a Jabber client.")), ?XCT(<<"li">>, ?T("Some Jabber clients can store your password " "in the computer, but you should do this only " "in your personal computer for safety reasons.")), ?XCT(<<"li">>, ?T("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(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)])] ++ CaptchaEls ++ [?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"register">>, ?T("Register"))])]))])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %% Copied from mod_register.erl %% Function copied from ejabberd_logger_h.erl and customized %%%---------------------------------------------------------------------- %%% Formulary new POST %%%---------------------------------------------------------------------- form_new_post(Q) -> case catch get_register_parameters(Q) of [Username, Host, Password, Password, Id, Key] -> form_new_post(Username, Host, Password, {Id, Key}); [_Username, _Host, _Password, _Password2, false, false] -> {error, passwords_not_identical}; [_Username, _Host, _Password, _Password2, Id, Key] -> ejabberd_captcha:check_captcha(Id, Key), {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">>, <<"host">>, <<"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, IP) -> case ejabberd_captcha:is_feature_available() of true -> build_captcha_li_list2(Lang, IP); false -> [] end. build_captcha_li_list2(Lang, IP) -> SID = <<"">>, From = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, To = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, Args = [], case ejabberd_captcha:create_captcha( SID, From, To, Lang, IP, Args) of {ok, Id, _, _} -> case ejabberd_captcha:build_captcha_html(Id, Lang) of {_, {CImg, CText, CId, CKey}} -> [?XE(<<"li">>, [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; Error -> throw(Error) end; Error -> throw(Error) end. %%%---------------------------------------------------------------------- %%% Formulary change password GET %%%---------------------------------------------------------------------- form_changepass_get(Host, Lang) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Change Password")), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Change Password")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Old Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("New Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"changepass">>, ?T("Change Password"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %%%---------------------------------------------------------------------- %%% Formulary change password POST %%%---------------------------------------------------------------------- form_changepass_post(Q) -> case catch get_changepass_parameters(Q) of [Username, Host, PasswordOld, Password, Password] -> try_change_password(Username, Host, PasswordOld, Password); [_Username, _Host, _PasswordOld, _Password, _Password2] -> {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. get_changepass_parameters(Q) -> %% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, password_not_changed} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"host">>, <<"passwordold">>, <<"password">>, <<"password2">>]). 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) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, PasswordOld), ok = ejabberd_auth:set_password(Username, Host, Password), 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: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 = [meta(), ?XCT(<<"title">>, ?T("Unregister a Jabber account")), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Unregister a Jabber account")), ?XCT(<<"p">>, ?T("This page allows to unregister a Jabber " "account in this Jabber server.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"unregister">>, ?T("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) -> Access = mod_register_opt:access(Host), case jid:make(Username, Host) of error -> {error, invalid_jid}; JID -> case acl:match_rule(Host, Access, JID) of deny -> {error, not_allowed}; allow -> register_account2(Username, Host, Password) end end. register_account2(Username, Host, Password) -> case ejabberd_auth:try_register(Username, Host, Password) of ok -> {success, ok, {Username, Host, Password}}; Other -> Other end. %%%---------------------------------------------------------------------- %%% Formulary delete POST %%%---------------------------------------------------------------------- form_del_post(Q) -> case catch get_unregister_parameters(Q) of [Username, Host, Password] -> try_unregister_account(Username, Host, Password); _ -> {error, wrong_parameters} end. get_unregister_parameters(Q) -> %% @spec(Username, Host, Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, account_exists} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"host">>, <<"password">>]). 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) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, Password), ok = ejabberd_auth:remove_user(Username, Host, Password), account_doesnt_exist = check_account_exists(Username, Host), {atomic, ok}. %%%---------------------------------------------------------------------- %%% Error texts %%%---------------------------------------------------------------------- get_error_text({error, captcha_non_valid}) -> ?T("The captcha you entered is wrong"); get_error_text({error, exists}) -> ?T("The account already exists"); get_error_text({error, password_incorrect}) -> ?T("Incorrect password"); get_error_text({error, invalid_jid}) -> ?T("The username is not valid"); get_error_text({error, not_allowed}) -> ?T("Not allowed"); get_error_text({error, account_doesnt_exist}) -> ?T("Account doesn't exist"); get_error_text({error, account_exists}) -> ?T("The account was not deleted"); get_error_text({error, password_not_changed}) -> ?T("The password was not changed"); get_error_text({error, passwords_not_identical}) -> ?T("The passwords are different"); get_error_text({error, wrong_parameters}) -> ?T("Wrong parameters in the web formulary"). mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides a web page where users can:"), "", ?T("- Register a new account on the server."), "", ?T("- Change the password from an existing account on the server."), "", ?T("- Delete an existing account on the server."), "", ?T("The module depends on 'mod_register' where all the configuration " "is performed.")]}.