%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_http.erl %%% Author : Piotr Nosek %%% Purpose : Authentication via HTTP request %%% Created : 23 Sep 2013 by Piotr Nosek %%%---------------------------------------------------------------------- -module(ejabberd_auth_http). -author('piotr.nosek@erlang-solutions.com'). -behaviour(ejabberd_auth). -behaviour(ejabberd_config). %% External exports -export([start/1, set_password/3, check_password/4, check_password/6, try_register/3, get_password/2, get_password_s/2, user_exists/2, remove_user/2, remove_user/3, plain_password_required/1, store_type/1, login/2, get_password/3, opt_type/1, stop/1]). -include("ejabberd.hrl"). -include("logger.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- opt_type(auth_opts) -> fun (V) -> V end; opt_type(_) -> [auth_opts]. -spec start(binary()) -> ok. start(Host) -> AuthOpts = ejabberd_config:get_option({auth_opts, Host}, fun(V) -> V end), {_, AuthHost} = lists:keyfind(host, 1, AuthOpts), PoolSize = proplists:get_value(connection_pool_size, AuthOpts, 10), Opts = proplists:get_value(connection_opts, AuthOpts, []), ChildMods = [fusco], ChildMFA = {fusco, start_link, [binary_to_list(AuthHost), Opts]}, Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = {Proc, {cuesport, start_link, [pool_name(Host), PoolSize, ChildMods, ChildMFA]}, transient, 2000, supervisor, [cuesport | ChildMods]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec). -spec plain_password_required(binary()) -> false. plain_password_required(Server) -> store_type(Server) == scram. -spec store_type(binary()) -> external. store_type(_) -> external. -spec check_password(ejabberd:luser(), binary(), ejabberd:lserver(), binary()) -> boolean(). check_password(LUser, _AuthzId, LServer, Password) -> case scram2:enabled(LServer) of false -> case make_req(get, <<"check_password">>, LUser, LServer, Password) of {ok, <<"true">>} -> true; _ -> false end; true -> {ok, true} =:= verify_scram_password(LUser, LServer, Password) end. -spec check_password(ejabberd:luser(), binary(), ejabberd:lserver(), binary(), binary(), fun()) -> boolean(). check_password(LUser, _AuthzId, LServer, Password, Digest, DigestGen) -> case make_req(get, <<"get_password">>, LUser, LServer, <<"">>) of {error, _} -> false; {ok, GotPasswd} -> case scram2:enabled(LServer) of true -> case scram2:deserialize(GotPasswd) of {ok, #scram{} = Scram} -> scram2:check_digest(Scram, Digest, DigestGen, Password); _ -> false end; false -> check_digest(Digest, DigestGen, Password, GotPasswd) end end. -spec check_digest(binary(), fun(), binary(), binary()) -> boolean(). check_digest(Digest, DigestGen, Password, Passwd) -> DigRes = if Digest /= <<>> -> Digest == DigestGen(Passwd); true -> false end, if DigRes -> true; true -> (Passwd == Password) and (Password /= <<>>) end. -spec set_password(ejabberd:luser(), ejabberd:lserver(), binary()) -> ok | {error, term()}. set_password(LUser, LServer, Password) -> PasswordFinal = case scram2:enabled(LServer) of true -> scram2:serialize(scram2:password_to_scram( Password, scram2:iterations(LServer))); false -> Password end, case make_req(post, <<"set_password">>, LUser, LServer, PasswordFinal) of {error, _} = Err -> Err; _ -> ok end. -spec try_register(ejabberd:luser(), ejabberd:lserver(), binary()) -> ok | {error, atom()}. try_register(LUser, LServer, Password) -> PasswordFinal = case scram2:enabled(LServer) of true -> scram2:serialize(scram2:password_to_scram( Password, scram2:iterations(LServer))); false -> Password end, case make_req(post, <<"register">>, LUser, LServer, PasswordFinal) of {ok, created} -> ok; {error, conflict} -> {error, exists}; Error -> Error end. -spec get_password(ejabberd:luser(), ejabberd:lserver()) -> error. get_password(_, _) -> error. -spec get_password_s(ejabberd:luser(), ejabberd:lserver()) -> binary(). get_password_s(User, Server) -> case get_password(User, Server) of Pass when is_binary(Pass) -> Pass; _ -> <<>> end. -spec user_exists(ejabberd:luser(), ejabberd:lserver()) -> boolean(). user_exists(LUser, LServer) -> case make_req(get, <<"user_exists">>, LUser, LServer, <<"">>) of {ok, <<"true">>} -> true; _ -> false end. -spec remove_user(ejabberd:luser(), ejabberd:lserver()) -> ok | not_exists | not_allowed | bad_request. remove_user(LUser, LServer) -> remove_user_req(LUser, LServer, <<"">>, <<"remove_user">>). -spec remove_user(ejabberd:luser(), ejabberd:lserver(), binary()) -> ok | not_exists | not_allowed | bad_request. remove_user(LUser, LServer, Password) -> case scram2:enabled(LServer) of false -> remove_user_req(LUser, LServer, Password, <<"remove_user_validate">>); true -> case verify_scram_password(LUser, LServer, Password) of {ok, false} -> not_allowed; {ok, true} -> remove_user_req(LUser, LServer, <<"">>, <<"remove_user">>); {error, Error} -> Error end end. -spec remove_user_req(binary(), binary(), binary(), binary()) -> ok | not_exists | not_allowed | bad_request. remove_user_req(LUser, LServer, Password, Method) -> case make_req(post, Method, LUser, LServer, Password) of {error, not_allowed} -> not_allowed; {error, not_found} -> not_exists; {error, _} -> bad_request; _ -> ok end. %%%---------------------------------------------------------------------- %%% Request maker %%%---------------------------------------------------------------------- -spec make_req(post | get, binary(), binary(), binary(), binary()) -> {ok, Body :: binary()} | {error, term()}. make_req(_, _, LUser, LServer, _) when LUser == error orelse LServer == error -> {error, {prep_failed, LUser, LServer}}; make_req(Method, Path, LUser, LServer, Password) -> AuthOpts = ejabberd_config:get_option({auth_opts, LServer}, fun(V) -> V end), BasicAuth = case lists:keyfind(basic_auth, 1, AuthOpts) of {_, BasicAuth0} -> BasicAuth0; _ -> "" end, PathPrefix = case lists:keyfind(path_prefix, 1, AuthOpts) of {_, Prefix} -> Prefix; false -> <<"/">> end, BasicAuth64 = base64:encode(BasicAuth), LUserE = list_to_binary(http_uri:encode(binary_to_list(LUser))), LServerE = list_to_binary(http_uri:encode(binary_to_list(LServer))), PasswordE = list_to_binary(http_uri:encode(binary_to_list(Password))), Query = <<"user=", LUserE/binary, "&server=", LServerE/binary, "&pass=", PasswordE/binary>>, Header = [{<<"Authorization">>, <<"Basic ", BasicAuth64/binary>>}], Connection = cuesport:get_worker(existing_pool_name(LServer)), ?DEBUG("Making request '~s' for user ~s@~s...", [Path, LUser, LServer]), {ok, {{Code, _Reason}, _RespHeaders, RespBody, _, _}} = case Method of get -> fusco:request(Connection, <>, "GET", Header, "", 2, 5000); post -> fusco:request(Connection, <>, "POST", Header, Query, 2, 5000) end, ?DEBUG("Request result: ~s: ~p", [Code, RespBody]), case Code of <<"409">> -> {error, conflict}; <<"404">> -> {error, not_found}; <<"401">> -> {error, not_authorized}; <<"403">> -> {error, not_allowed}; <<"400">> -> {error, RespBody}; <<"204">> -> {ok, <<"">>}; <<"201">> -> {ok, created}; <<"200">> -> {ok, RespBody} end. %%%---------------------------------------------------------------------- %%% Other internal functions %%%---------------------------------------------------------------------- -spec pool_name(binary()) -> atom(). pool_name(Host) -> list_to_atom("ejabberd_auth_http_" ++ binary_to_list(Host)). -spec existing_pool_name(binary()) -> atom(). existing_pool_name(Host) -> list_to_existing_atom("ejabberd_auth_http_" ++ binary_to_list(Host)). -spec verify_scram_password(binary(), binary(), binary()) -> {ok, boolean()} | {error, bad_request | not_exists}. verify_scram_password(LUser, LServer, Password) -> case make_req(get, <<"get_password">>, LUser, LServer, <<"">>) of {ok, RawPassword} -> case scram2:deserialize(RawPassword) of {ok, #scram{} = ScramRecord} -> {ok, scram2:check_password(Password, ScramRecord)}; _ -> {error, bad_request} end; _ -> {error, not_exists} end. login(_User, _Server) -> erlang:error(not_implemented). get_password(_User, _Server, _DefaultValue) -> erlang:error(not_implemented). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), supervisor:terminate_child(ejabberd_backend_sup, Proc), supervisor:delete_child(ejabberd_backend_sup, Proc).