Move XMPP stream and SASL processing to xmpp repo

This commit is contained in:
Evgeniy Khramtsov 2018-07-06 01:07:36 +03:00
parent 59f5a098b5
commit 0bb14d16c7
25 changed files with 69 additions and 4589 deletions

View File

@ -1,14 +0,0 @@
XmppAddr { iso(1) identified-organization(3)
dod(6) internet(1) security(5) mechanisms(5) pkix(7)
id-on(8) id-on-xmppAddr(5) }
DEFINITIONS EXPLICIT TAGS ::=
BEGIN
id-on-xmppAddr OBJECT IDENTIFIER ::= { iso(1) identified-organization(3)
dod(6) internet(1) security(5) mechanisms(5) pkix(7)
id-on(8) 5 }
XmppAddr ::= UTF8String
END

View File

@ -1,28 +0,0 @@
%%%----------------------------------------------------------------------
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-record(scram, {storedkey = <<"">> :: binary(),
serverkey = <<"">> :: binary(),
salt = <<"">> :: binary(),
iterationcount = 0 :: integer()}).
-type scram() :: #scram{}.
-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).

View File

@ -25,7 +25,7 @@
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.23"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.12"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.32"}}},
{xmpp, ".*", {git, "https://github.com/processone/xmpp", "0e2ef5d"}},
{xmpp, ".*", {git, "https://github.com/processone/xmpp", "2a5193c"}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.15"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.3"}}},
@ -100,7 +100,7 @@
{if_have_fun, {public_key, short_name_hash, 1}, {d, 'SHORT_NAME_HASH'}},
{if_var_true, new_sql_schema, {d, 'NEW_SQL_SCHEMA'}},
{if_var_true, hipe, native},
{src_dirs, [asn1, src,
{src_dirs, [src,
{if_var_true, tools, tools},
{if_var_true, elixir, include}]}]}.

View File

@ -1,229 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Cyrus SASL-like library
%%% Created : 8 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl).
-author('alexey@process-one.net').
-behaviour(gen_server).
-export([start_link/0, register_mechanism/3, listmech/1,
server_new/7, server_start/3, server_step/2,
get_mech/1, format_error/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("logger.hrl").
-record(state, {}).
-record(sasl_mechanism,
{mechanism = <<"">> :: mechanism() | '$1',
module :: atom(),
password_type = plain :: password_type() | '$2'}).
-type(mechanism() :: binary()).
-type(mechanisms() :: [mechanism(),...]).
-type(password_type() :: plain | digest | scram).
-type sasl_property() :: {username, binary()} |
{authzid, binary()} |
{mechanism, binary()} |
{auth_module, atom()}.
-type sasl_return() :: {ok, [sasl_property()]} |
{ok, [sasl_property()], binary()} |
{continue, binary(), sasl_state()} |
{error, atom(), binary()}.
-type(sasl_mechanism() :: #sasl_mechanism{}).
-type error_reason() :: cyrsasl_digest:error_reason() |
cyrsasl_oauth:error_reason() |
cyrsasl_plain:error_reason() |
cyrsasl_scram:error_reason() |
unsupported_mechanism | nodeprep_failed |
empty_username | aborted.
-record(sasl_state,
{
service,
myname,
realm,
get_password,
check_password,
check_password_digest,
mech_name = <<"">>,
mech_mod,
mech_state
}).
-type sasl_state() :: #sasl_state{}.
-export_type([mechanism/0, mechanisms/0, sasl_mechanism/0, error_reason/0,
sasl_state/0, sasl_return/0, sasl_property/0]).
-callback start(list()) -> any().
-callback stop() -> any().
-callback mech_new(binary(), fun(), fun(), fun()) -> any().
-callback mech_step(any(), binary()) -> sasl_return().
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
ets:new(sasl_mechanism,
[named_table, public,
{keypos, #sasl_mechanism.mechanism}]),
cyrsasl_plain:start([]),
cyrsasl_digest:start([]),
cyrsasl_scram:start([]),
cyrsasl_anonymous:start([]),
cyrsasl_oauth:start([]),
{ok, #state{}}.
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
cyrsasl_plain:stop(),
cyrsasl_digest:stop(),
cyrsasl_scram:stop(),
cyrsasl_anonymous:stop(),
cyrsasl_oauth:stop().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec format_error(mechanism() | sasl_state(), error_reason()) -> {atom(), binary()}.
format_error(_, unsupported_mechanism) ->
{'invalid-mechanism', <<"Unsupported mechanism">>};
format_error(_, nodeprep_failed) ->
{'bad-protocol', <<"Nodeprep failed">>};
format_error(_, empty_username) ->
{'bad-protocol', <<"Empty username">>};
format_error(_, aborted) ->
{'aborted', <<"Aborted">>};
format_error(#sasl_state{mech_mod = Mod}, Reason) ->
Mod:format_error(Reason);
format_error(Mech, Reason) ->
case ets:lookup(sasl_mechanism, Mech) of
[#sasl_mechanism{module = Mod}] ->
Mod:format_error(Reason);
[] ->
{'invalid-mechanism', <<"Unsupported mechanism">>}
end.
-spec register_mechanism(Mechanim :: mechanism(), Module :: module(),
PasswordType :: password_type()) -> any().
register_mechanism(Mechanism, Module, PasswordType) ->
ets:insert(sasl_mechanism,
#sasl_mechanism{mechanism = Mechanism, module = Module,
password_type = PasswordType}).
check_credentials(_State, Props) ->
User = proplists:get_value(authzid, Props, <<>>),
case jid:nodeprep(User) of
error -> {error, nodeprep_failed};
<<"">> -> {error, empty_username};
_LUser -> ok
end.
-spec listmech(Host ::binary()) -> Mechanisms::mechanisms().
listmech(Host) ->
ets:select(sasl_mechanism,
[{#sasl_mechanism{mechanism = '$1',
password_type = '$2', _ = '_'},
case catch ejabberd_auth:store_type(Host) of
external -> [{'==', '$2', plain}];
scram -> [{'/=', '$2', digest}];
{'EXIT', {undef, [{Module, store_type, []} | _]}} ->
?WARNING_MSG("~p doesn't implement the function store_type/0",
[Module]),
[];
_Else -> []
end,
['$1']}]).
-spec server_new(binary(), binary(), binary(), term(),
fun(), fun(), fun()) -> sasl_state().
server_new(Service, ServerFQDN, UserRealm, _SecFlags,
GetPassword, CheckPassword, CheckPasswordDigest) ->
#sasl_state{service = Service, myname = ServerFQDN,
realm = UserRealm, get_password = GetPassword,
check_password = CheckPassword,
check_password_digest = CheckPasswordDigest}.
-spec server_start(sasl_state(), mechanism(), binary()) -> sasl_return().
server_start(State, Mech, ClientIn) ->
case lists:member(Mech,
listmech(State#sasl_state.myname))
of
true ->
case ets:lookup(sasl_mechanism, Mech) of
[#sasl_mechanism{module = Module}] ->
{ok, MechState} =
Module:mech_new(State#sasl_state.myname,
State#sasl_state.get_password,
State#sasl_state.check_password,
State#sasl_state.check_password_digest),
server_step(State#sasl_state{mech_mod = Module,
mech_name = Mech,
mech_state = MechState},
ClientIn);
_ -> {error, unsupported_mechanism, <<"">>}
end;
false -> {error, unsupported_mechanism, <<"">>}
end.
-spec server_step(sasl_state(), binary()) -> sasl_return().
server_step(State, ClientIn) ->
Module = State#sasl_state.mech_mod,
MechState = State#sasl_state.mech_state,
case Module:mech_step(MechState, ClientIn) of
{ok, Props} ->
case check_credentials(State, Props) of
ok -> {ok, Props};
{error, Error} -> {error, Error, <<"">>}
end;
{ok, Props, ServerOut} ->
case check_credentials(State, Props) of
ok -> {ok, Props, ServerOut};
{error, Error} -> {error, Error, <<"">>}
end;
{continue, ServerOut, NewMechState} ->
{continue, ServerOut, State#sasl_state{mech_state = NewMechState}};
{error, Error, Username} ->
{error, Error, Username};
{error, Error} ->
{error, Error, <<"">>}
end.
-spec get_mech(sasl_state()) -> binary().
get_mech(#sasl_state{mech_name = Mech}) ->
Mech.

View File

@ -1,50 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_anonymous.erl
%%% Author : Magnus Henoch <henoch@dtek.chalmers.se>
%%% Purpose : ANONYMOUS SASL mechanism
%%% See http://www.ietf.org/internet-drafts/draft-ietf-sasl-anon-05.txt
%%% Created : 23 Aug 2005 by Magnus Henoch <henoch@dtek.chalmers.se>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl_anonymous).
-protocol({xep, 175, '1.2'}).
-export([start/1, stop/0, mech_new/4, mech_step/2]).
-behaviour(cyrsasl).
-record(state, {server = <<"">> :: binary()}).
start(_Opts) ->
cyrsasl:register_mechanism(<<"ANONYMOUS">>, ?MODULE, plain).
stop() -> ok.
mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) ->
{ok, #state{server = Host}}.
mech_step(#state{}, _ClientIn) ->
User = iolist_to_binary([p1_rand:get_string(),
integer_to_binary(p1_time_compat:unique_integer([positive]))]),
{ok, [{username, User},
{authzid, User},
{auth_module, ejabberd_auth_anonymous}]}.

View File

@ -1,270 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_digest.erl
%%% Author : Alexey Shchepin <alexey@sevcom.net>
%%% Purpose : DIGEST-MD5 SASL mechanism
%%% Created : 11 Mar 2003 by Alexey Shchepin <alexey@sevcom.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl_digest).
-behaviour(ejabberd_config).
-author('alexey@sevcom.net').
-export([start/1, stop/0, mech_new/4, mech_step/2,
parse/1, format_error/1, opt_type/1]).
-include("logger.hrl").
-behaviour(cyrsasl).
-type get_password_fun() :: fun((binary()) -> {false, any()} |
{binary(), atom()}).
-type check_password_fun() :: fun((binary(), binary(), binary(), binary(),
fun((binary()) -> binary())) ->
{boolean(), any()} |
false).
-type error_reason() :: parser_failed | invalid_digest_uri |
not_authorized | unexpected_response.
-export_type([error_reason/0]).
-record(state, {step = 1 :: 1 | 3 | 5,
nonce = <<"">> :: binary(),
username = <<"">> :: binary(),
authzid = <<"">> :: binary(),
get_password :: get_password_fun(),
check_password :: check_password_fun(),
auth_module :: atom(),
host = <<"">> :: binary(),
hostfqdn = [] :: [binary()]}).
start(_Opts) ->
Fqdn = get_local_fqdn(),
?DEBUG("FQDN used to check DIGEST-MD5 SASL authentication: ~s",
[Fqdn]),
cyrsasl:register_mechanism(<<"DIGEST-MD5">>, ?MODULE,
digest).
stop() -> ok.
-spec format_error(error_reason()) -> {atom(), binary()}.
format_error(parser_failed) ->
{'bad-protocol', <<"Response decoding failed">>};
format_error(invalid_digest_uri) ->
{'bad-protocol', <<"Invalid digest URI">>};
format_error(not_authorized) ->
{'not-authorized', <<"Invalid username or password">>};
format_error(unexpected_response) ->
{'bad-protocol', <<"Unexpected response">>}.
mech_new(Host, GetPassword, _CheckPassword,
CheckPasswordDigest) ->
{ok,
#state{step = 1, nonce = p1_rand:get_string(),
host = Host, hostfqdn = get_local_fqdn(),
get_password = GetPassword,
check_password = CheckPasswordDigest}}.
mech_step(#state{step = 1, nonce = Nonce} = State, _) ->
{continue,
<<"nonce=\"", Nonce/binary,
"\",qop=\"auth\",charset=utf-8,algorithm=md5-sess">>,
State#state{step = 3}};
mech_step(#state{step = 3, nonce = Nonce} = State,
ClientIn) ->
case parse(ClientIn) of
bad -> {error, parser_failed};
KeyVals ->
DigestURI = proplists:get_value(<<"digest-uri">>, KeyVals, <<>>),
UserName = proplists:get_value(<<"username">>, KeyVals, <<>>),
case is_digesturi_valid(DigestURI, State#state.host,
State#state.hostfqdn)
of
false ->
?DEBUG("User login not authorized because digest-uri "
"seems invalid: ~p (checking for Host "
"~p, FQDN ~p)",
[DigestURI, State#state.host, State#state.hostfqdn]),
{error, invalid_digest_uri, UserName};
true ->
AuthzId = proplists:get_value(<<"authzid">>, KeyVals, <<>>),
case (State#state.get_password)(UserName) of
{false, _} -> {error, not_authorized, UserName};
{Passwd, AuthModule} ->
case (State#state.check_password)(UserName, UserName, <<"">>,
proplists:get_value(<<"response">>, KeyVals, <<>>),
fun (PW) ->
response(KeyVals,
UserName,
PW,
Nonce,
AuthzId,
<<"AUTHENTICATE">>)
end)
of
{true, _} ->
RspAuth = response(KeyVals, UserName, Passwd, Nonce,
AuthzId, <<"">>),
{continue, <<"rspauth=", RspAuth/binary>>,
State#state{step = 5, auth_module = AuthModule,
username = UserName,
authzid = AuthzId}};
false -> {error, not_authorized, UserName};
{false, _} -> {error, not_authorized, UserName}
end
end
end
end;
mech_step(#state{step = 5, auth_module = AuthModule,
username = UserName, authzid = AuthzId},
<<"">>) ->
{ok,
[{username, UserName}, {authzid, case AuthzId of
<<"">> -> UserName;
_ -> AuthzId
end
},
{auth_module, AuthModule}]};
mech_step(A, B) ->
?DEBUG("SASL DIGEST: A ~p B ~p", [A, B]),
{error, unexpected_response}.
parse(S) -> parse1(binary_to_list(S), "", []).
parse1([$= | Cs], S, Ts) ->
parse2(Cs, lists:reverse(S), "", Ts);
parse1([$, | Cs], [], Ts) -> parse1(Cs, [], Ts);
parse1([$\s | Cs], [], Ts) -> parse1(Cs, [], Ts);
parse1([C | Cs], S, Ts) -> parse1(Cs, [C | S], Ts);
parse1([], [], T) -> lists:reverse(T);
parse1([], _S, _T) -> bad.
parse2([$" | Cs], Key, Val, Ts) ->
parse3(Cs, Key, Val, Ts);
parse2([C | Cs], Key, Val, Ts) ->
parse4(Cs, Key, [C | Val], Ts);
parse2([], _, _, _) -> bad.
parse3([$" | Cs], Key, Val, Ts) ->
parse4(Cs, Key, Val, Ts);
parse3([$\\, C | Cs], Key, Val, Ts) ->
parse3(Cs, Key, [C | Val], Ts);
parse3([C | Cs], Key, Val, Ts) ->
parse3(Cs, Key, [C | Val], Ts);
parse3([], _, _, _) -> bad.
parse4([$, | Cs], Key, Val, Ts) ->
parse1(Cs, "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]);
parse4([$\s | Cs], Key, Val, Ts) ->
parse4(Cs, Key, Val, Ts);
parse4([C | Cs], Key, Val, Ts) ->
parse4(Cs, Key, [C | Val], Ts);
parse4([], Key, Val, Ts) ->
%% @doc Check if the digest-uri is valid.
%% RFC-2831 allows to provide the IP address in Host,
%% however ejabberd doesn't allow that.
%% If the service (for example jabber.example.org)
%% is provided by several hosts (being one of them server3.example.org),
%% then acceptable digest-uris would be:
%% xmpp/server3.example.org/jabber.example.org, xmpp/server3.example.org and
%% xmpp/jabber.example.org
%% The last version is not actually allowed by the RFC, but implemented by popular clients
parse1([], "", [{list_to_binary(Key), list_to_binary(lists:reverse(Val))} | Ts]).
is_digesturi_valid(DigestURICase, JabberDomain,
JabberFQDN) ->
DigestURI = stringprep:tolower(DigestURICase),
case catch str:tokens(DigestURI, <<"/">>) of
[<<"xmpp">>, Host] ->
IsHostFqdn = is_host_fqdn(Host, JabberFQDN),
(Host == JabberDomain) or IsHostFqdn;
[<<"xmpp">>, Host, ServName] ->
IsHostFqdn = is_host_fqdn(Host, JabberFQDN),
(ServName == JabberDomain) and IsHostFqdn;
_ ->
false
end.
is_host_fqdn(_Host, []) ->
false;
is_host_fqdn(Host, [Fqdn | _FqdnTail]) when Host == Fqdn ->
true;
is_host_fqdn(Host, [Fqdn | FqdnTail]) when Host /= Fqdn ->
is_host_fqdn(Host, FqdnTail).
get_local_fqdn() ->
case ejabberd_config:get_option(fqdn) of
undefined ->
{ok, Hostname} = inet:gethostname(),
{ok, {hostent, Fqdn, _, _, _, _}} = inet:gethostbyname(Hostname),
[list_to_binary(Fqdn)];
Fqdn ->
Fqdn
end.
hex(S) ->
str:to_hexlist(S).
proplists_get_bin_value(Key, Pairs, Default) ->
case proplists:get_value(Key, Pairs, Default) of
L when is_list(L) ->
list_to_binary(L);
L2 ->
L2
end.
response(KeyVals, User, Passwd, Nonce, AuthzId,
A2Prefix) ->
Realm = proplists_get_bin_value(<<"realm">>, KeyVals, <<>>),
CNonce = proplists_get_bin_value(<<"cnonce">>, KeyVals, <<>>),
DigestURI = proplists_get_bin_value(<<"digest-uri">>, KeyVals, <<>>),
NC = proplists_get_bin_value(<<"nc">>, KeyVals, <<>>),
QOP = proplists_get_bin_value(<<"qop">>, KeyVals, <<>>),
MD5Hash = erlang:md5(<<User/binary, ":", Realm/binary, ":",
Passwd/binary>>),
A1 = case AuthzId of
<<"">> ->
<<MD5Hash/binary, ":", Nonce/binary, ":", CNonce/binary>>;
_ ->
<<MD5Hash/binary, ":", Nonce/binary, ":", CNonce/binary, ":",
AuthzId/binary>>
end,
A2 = case QOP of
<<"auth">> ->
<<A2Prefix/binary, ":", DigestURI/binary>>;
_ ->
<<A2Prefix/binary, ":", DigestURI/binary,
":00000000000000000000000000000000">>
end,
T = <<(hex((erlang:md5(A1))))/binary, ":", Nonce/binary,
":", NC/binary, ":", CNonce/binary, ":", QOP/binary,
":", (hex((erlang:md5(A2))))/binary>>,
hex((erlang:md5(T))).
-spec opt_type(fqdn) -> fun((binary() | [binary()]) -> [binary()]);
(atom()) -> [atom()].
opt_type(fqdn) ->
fun(FQDN) when is_binary(FQDN) ->
[FQDN];
(FQDNs) when is_list(FQDNs) ->
[iolist_to_binary(FQDN) || FQDN <- FQDNs]
end;
opt_type(_) -> [fqdn].

View File

@ -1,104 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_oauth.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : X-OAUTH2 SASL mechanism
%%% Created : 17 Sep 2015 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl_oauth).
-author('alexey@process-one.net').
-export([start/1, stop/0, mech_new/4, mech_step/2, parse/1, format_error/1]).
-behaviour(cyrsasl).
-record(state, {host}).
-type error_reason() :: parser_failed | not_authorized.
-export_type([error_reason/0]).
start(_Opts) ->
cyrsasl:register_mechanism(<<"X-OAUTH2">>, ?MODULE, plain).
stop() -> ok.
-spec format_error(error_reason()) -> {atom(), binary()}.
format_error(parser_failed) ->
{'bad-protocol', <<"Response decoding failed">>};
format_error(not_authorized) ->
{'not-authorized', <<"Invalid token">>}.
mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) ->
{ok, #state{host = Host}}.
mech_step(State, ClientIn) ->
case prepare(ClientIn) of
[AuthzId, User, Token] ->
case ejabberd_oauth:check_token(
User, State#state.host, [<<"sasl_auth">>], Token) of
true ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]};
_ ->
{error, not_authorized, User}
end;
_ -> {error, parser_failed}
end.
prepare(ClientIn) ->
case parse(ClientIn) of
[<<"">>, UserMaybeDomain, Token] ->
case parse_domain(UserMaybeDomain) of
%% <NUL>login@domain<NUL>pwd
[User, _Domain] -> [User, User, Token];
%% <NUL>login<NUL>pwd
[User] -> [User, User, Token]
end;
%% login@domain<NUL>login<NUL>pwd
[AuthzId, User, Token] ->
case parse_domain(AuthzId) of
%% login@domain<NUL>login<NUL>pwd
[AuthzUser, _Domain] -> [AuthzUser, User, Token];
%% login<NUL>login<NUL>pwd
[AuthzUser] -> [AuthzUser, User, Token]
end;
_ -> error
end.
parse(S) -> parse1(binary_to_list(S), "", []).
parse1([0 | Cs], S, T) ->
parse1(Cs, "", [list_to_binary(lists:reverse(S)) | T]);
parse1([C | Cs], S, T) -> parse1(Cs, [C | S], T);
%parse1([], [], T) ->
% lists:reverse(T);
parse1([], S, T) ->
lists:reverse([list_to_binary(lists:reverse(S)) | T]).
parse_domain(S) -> parse_domain1(binary_to_list(S), "", []).
parse_domain1([$@ | Cs], S, T) ->
parse_domain1(Cs, "", [list_to_binary(lists:reverse(S)) | T]);
parse_domain1([C | Cs], S, T) ->
parse_domain1(Cs, [C | S], T);
parse_domain1([], S, T) ->
lists:reverse([list_to_binary(lists:reverse(S)) | T]).

View File

@ -1,94 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_plain.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : PLAIN SASL mechanism
%%% Created : 8 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl_plain).
-author('alexey@process-one.net').
-export([start/1, stop/0, mech_new/4, mech_step/2, parse/1, format_error/1]).
-behaviour(cyrsasl).
-record(state, {check_password}).
-type error_reason() :: parser_failed | not_authorized.
-export_type([error_reason/0]).
start(_Opts) ->
cyrsasl:register_mechanism(<<"PLAIN">>, ?MODULE, plain).
stop() -> ok.
-spec format_error(error_reason()) -> {atom(), binary()}.
format_error(parser_failed) ->
{'bad-protocol', <<"Response decoding failed">>};
format_error(not_authorized) ->
{'not-authorized', <<"Invalid username or password">>}.
mech_new(_Host, _GetPassword, CheckPassword, _CheckPasswordDigest) ->
{ok, #state{check_password = CheckPassword}}.
mech_step(State, ClientIn) ->
case prepare(ClientIn) of
[AuthzId, User, Password] ->
case (State#state.check_password)(User, AuthzId, Password) of
{true, AuthModule} ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, AuthModule}]};
_ -> {error, not_authorized, User}
end;
_ -> {error, parser_failed}
end.
prepare(ClientIn) ->
case parse(ClientIn) of
[<<"">>, UserMaybeDomain, Password] ->
case parse_domain(UserMaybeDomain) of
%% <NUL>login@domain<NUL>pwd
[User, _Domain] -> [User, User, Password];
%% <NUL>login<NUL>pwd
[User] -> [User, User, Password]
end;
[AuthzId, User, Password] ->
case parse_domain(AuthzId) of
%% login@domain<NUL>login<NUL>pwd
[AuthzUser, _Domain] -> [AuthzUser, User, Password];
%% login<NUL>login<NUL>pwd
[AuthzUser] -> [AuthzUser, User, Password]
end;
_ -> error
end.
parse(S) ->
binary:split(S, <<0>>, [global]).
parse_domain(S) -> parse_domain1(binary_to_list(S), "", []).
parse_domain1([$@ | Cs], S, T) ->
parse_domain1(Cs, "", [list_to_binary(lists:reverse(S)) | T]);
parse_domain1([C | Cs], S, T) ->
parse_domain1(Cs, [C | S], T);
parse_domain1([], S, T) ->
lists:reverse([list_to_binary(lists:reverse(S)) | T]).

View File

@ -1,249 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_scram.erl
%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
%%% Purpose : SASL SCRAM authentication
%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(cyrsasl_scram).
-author('stephen.roettger@googlemail.com').
-protocol({rfc, 5802}).
-export([start/1, stop/0, mech_new/4, mech_step/2, format_error/1]).
-include("scram.hrl").
-include("logger.hrl").
-behaviour(cyrsasl).
-record(state,
{step = 2 :: 2 | 4,
stored_key = <<"">> :: binary(),
server_key = <<"">> :: binary(),
username = <<"">> :: binary(),
auth_module :: module(),
get_password :: fun((binary()) ->
{false | ejabberd_auth:password(), module()}),
auth_message = <<"">> :: binary(),
client_nonce = <<"">> :: binary(),
server_nonce = <<"">> :: binary()}).
-define(SALT_LENGTH, 16).
-define(NONCE_LENGTH, 16).
-type error_reason() :: unsupported_extension | bad_username |
not_authorized | saslprep_failed |
parser_failed | bad_attribute |
nonce_mismatch | bad_channel_binding.
-export_type([error_reason/0]).
start(_Opts) ->
cyrsasl:register_mechanism(<<"SCRAM-SHA-1">>, ?MODULE,
scram).
stop() -> ok.
-spec format_error(error_reason()) -> {atom(), binary()}.
format_error(unsupported_extension) ->
{'bad-protocol', <<"Unsupported extension">>};
format_error(bad_username) ->
{'invalid-authzid', <<"Malformed username">>};
format_error(not_authorized) ->
{'not-authorized', <<"Invalid username or password">>};
format_error(saslprep_failed) ->
{'not-authorized', <<"SASLprep failed">>};
format_error(parser_failed) ->
{'bad-protocol', <<"Response decoding failed">>};
format_error(bad_attribute) ->
{'bad-protocol', <<"Malformed or unexpected attribute">>};
format_error(nonce_mismatch) ->
{'bad-protocol', <<"Nonce mismatch">>};
format_error(bad_channel_binding) ->
{'bad-protocol', <<"Invalid channel binding">>}.
mech_new(_Host, GetPassword, _CheckPassword,
_CheckPasswordDigest) ->
{ok, #state{step = 2, get_password = GetPassword}}.
mech_step(#state{step = 2} = State, ClientIn) ->
case re:split(ClientIn, <<",">>, [{return, binary}]) of
[_CBind, _AuthorizationIdentity, _UserNameAttribute, _ClientNonceAttribute, ExtensionAttribute | _]
when ExtensionAttribute /= <<"">> ->
{error, unsupported_extension};
[CBind, _AuthorizationIdentity, UserNameAttribute, ClientNonceAttribute | _]
when (CBind == <<"y">>) or (CBind == <<"n">>) ->
case parse_attribute(UserNameAttribute) of
{error, Reason} -> {error, Reason};
{_, EscapedUserName} ->
case unescape_username(EscapedUserName) of
error -> {error, bad_username};
UserName ->
case parse_attribute(ClientNonceAttribute) of
{$r, ClientNonce} ->
{Pass, AuthModule} = (State#state.get_password)(UserName),
LPass = if is_binary(Pass) -> jid:resourceprep(Pass);
true -> Pass
end,
if Pass == false ->
{error, not_authorized, UserName};
LPass == error ->
{error, saslprep_failed, UserName};
true ->
{StoredKey, ServerKey, Salt, IterationCount} =
if is_record(Pass, scram) ->
{base64:decode(Pass#scram.storedkey),
base64:decode(Pass#scram.serverkey),
base64:decode(Pass#scram.salt),
Pass#scram.iterationcount};
true ->
TempSalt =
p1_rand:bytes(?SALT_LENGTH),
SaltedPassword =
scram:salted_password(Pass,
TempSalt,
?SCRAM_DEFAULT_ITERATION_COUNT),
{scram:stored_key(scram:client_key(SaltedPassword)),
scram:server_key(SaltedPassword),
TempSalt,
?SCRAM_DEFAULT_ITERATION_COUNT}
end,
ClientFirstMessageBare =
str:substr(ClientIn,
str:str(ClientIn, <<"n=">>)),
ServerNonce =
base64:encode(p1_rand:bytes(?NONCE_LENGTH)),
ServerFirstMessage =
iolist_to_binary(
["r=",
ClientNonce,
ServerNonce,
",", "s=",
base64:encode(Salt),
",", "i=",
integer_to_list(IterationCount)]),
{continue, ServerFirstMessage,
State#state{step = 4, stored_key = StoredKey,
server_key = ServerKey,
auth_module = AuthModule,
auth_message =
<<ClientFirstMessageBare/binary,
",", ServerFirstMessage/binary>>,
client_nonce = ClientNonce,
server_nonce = ServerNonce,
username = UserName}}
end;
_ -> {error, bad_attribute}
end
end
end;
_Else -> {error, parser_failed}
end;
mech_step(#state{step = 4} = State, ClientIn) ->
case str:tokens(ClientIn, <<",">>) of
[GS2ChannelBindingAttribute, NonceAttribute,
ClientProofAttribute] ->
case parse_attribute(GS2ChannelBindingAttribute) of
{$c, CVal} ->
ChannelBindingSupport = try binary:first(base64:decode(CVal))
catch _:badarg -> 0
end,
if (ChannelBindingSupport == $n)
or (ChannelBindingSupport == $y) ->
Nonce = <<(State#state.client_nonce)/binary,
(State#state.server_nonce)/binary>>,
case parse_attribute(NonceAttribute) of
{$r, CompareNonce} when CompareNonce == Nonce ->
case parse_attribute(ClientProofAttribute) of
{$p, ClientProofB64} ->
ClientProof = try base64:decode(ClientProofB64)
catch _:badarg -> <<>>
end,
AuthMessage = iolist_to_binary(
[State#state.auth_message,
",",
str:substr(ClientIn, 1,
str:str(ClientIn, <<",p=">>)
- 1)]),
ClientSignature =
scram:client_signature(State#state.stored_key,
AuthMessage),
ClientKey = scram:client_key(ClientProof,
ClientSignature),
CompareStoredKey = scram:stored_key(ClientKey),
if CompareStoredKey == State#state.stored_key ->
ServerSignature =
scram:server_signature(State#state.server_key,
AuthMessage),
{ok, [{username, State#state.username},
{auth_module, State#state.auth_module},
{authzid, State#state.username}],
<<"v=",
(base64:encode(ServerSignature))/binary>>};
true -> {error, not_authorized, State#state.username}
end;
_ -> {error, bad_attribute}
end;
{$r, _} -> {error, nonce_mismatch};
_ -> {error, bad_attribute}
end;
true -> {error, bad_channel_binding}
end;
_ -> {error, bad_attribute}
end;
_ -> {error, parser_failed}
end.
parse_attribute(<<Name, $=, Val/binary>>) when Val /= <<>> ->
case is_alpha(Name) of
true -> {Name, Val};
false -> {error, bad_attribute}
end;
parse_attribute(_) ->
{error, bad_attribute}.
unescape_username(<<"">>) -> <<"">>;
unescape_username(EscapedUsername) ->
Pos = str:str(EscapedUsername, <<"=">>),
if Pos == 0 -> EscapedUsername;
true ->
Start = str:substr(EscapedUsername, 1, Pos - 1),
End = str:substr(EscapedUsername, Pos),
EndLen = byte_size(End),
if EndLen < 3 -> error;
true ->
case str:substr(End, 1, 3) of
<<"=2C">> ->
<<Start/binary, ",",
(unescape_username(str:substr(End, 4)))/binary>>;
<<"=3D">> ->
<<Start/binary, "=",
(unescape_username(str:substr(End, 4)))/binary>>;
_Else -> error
end
end
end.
is_alpha(Char) when Char >= $a, Char =< $z -> true;
is_alpha(Char) when Char >= $A, Char =< $Z -> true;
is_alpha(_) -> false.

View File

@ -524,9 +524,7 @@ handle_event({become_controller, C2SPid}, StateName,
{next_state, StateName, State1};
handle_event({change_shaper, Shaper}, StateName,
State) ->
NewShaperState = ejabberd_shaper:new(Shaper),
{next_state, StateName,
State#state{shaper_state = NewShaperState}};
{next_state, StateName, State#state{shaper_state = Shaper}};
handle_event(_Event, StateName, State) ->
?ERROR_MSG("unexpected event in '~s': ~p",
[StateName, _Event]),

View File

@ -33,9 +33,9 @@
%% xmpp_stream_in callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([tls_options/1, tls_required/1, tls_verify/1, tls_enabled/1,
-export([tls_options/1, tls_required/1, tls_enabled/1,
compress_methods/1, bind/2, sasl_mechanisms/2,
get_password_fun/1, check_password_fun/1, check_password_digest_fun/1,
get_password_fun/2, check_password_fun/2, check_password_digest_fun/2,
unauthenticated_stream_features/1, authenticated_stream_features/1,
handle_stream_start/2, handle_stream_end/2,
handle_unauthenticated_packet/2, handle_authenticated_packet/2,
@ -339,9 +339,6 @@ tls_options(#{lserver := LServer, tls_options := DefaultOpts,
tls_required(#{tls_required := TLSRequired}) ->
TLSRequired.
tls_verify(#{tls_verify := TLSVerify}) ->
TLSVerify.
tls_enabled(#{tls_enabled := TLSEnabled,
tls_required := TLSRequired,
tls_verify := TLSVerify}) ->
@ -358,25 +355,41 @@ unauthenticated_stream_features(#{lserver := LServer}) ->
authenticated_stream_features(#{lserver := LServer}) ->
ejabberd_hooks:run_fold(c2s_post_auth_features, LServer, [], [LServer]).
sasl_mechanisms(Mechs, #{lserver := LServer}) ->
sasl_mechanisms(Mechs, #{lserver := LServer} = State) ->
Type = ejabberd_auth:store_type(LServer),
Mechs1 = ejabberd_config:get_option({disable_sasl_mechanisms, LServer}, []),
Mechs2 = case ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer) of
true -> Mechs1;
false -> [<<"ANONYMOUS">>|Mechs1]
end,
Mechs -- Mechs2.
%% I re-created it from cyrsasl ets magic, but I think it's wrong
%% TODO: need to check before 18.09 release
lists:filter(
fun(<<"ANONYMOUS">>) ->
ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer);
(<<"DIGEST-MD5">>) -> Type == plain;
(<<"SCRAM-SHA-1">>) -> Type /= external;
(<<"PLAIN">>) -> true;
(<<"X-OAUTH2">>) -> true;
(<<"EXTERNAL">>) -> maps:get(tls_verify, State, false);
(_) -> false
end, Mechs -- Mechs1).
get_password_fun(#{lserver := LServer}) ->
get_password_fun(_Mech, #{lserver := LServer}) ->
fun(U) ->
ejabberd_auth:get_password_with_authmodule(U, LServer)
end.
check_password_fun(#{lserver := LServer}) ->
check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) ->
fun(User, _AuthzId, Token) ->
case ejabberd_oauth:check_token(
User, LServer, [<<"sasl_auth">>], Token) of
true -> {true, ejabberd_oauth};
_ -> {false, ejabberd_oauth}
end
end;
check_password_fun(_Mech, #{lserver := LServer}) ->
fun(U, AuthzId, P) ->
ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P)
end.
check_password_digest_fun(#{lserver := LServer}) ->
check_password_digest_fun(_Mech, #{lserver := LServer}) ->
fun(U, AuthzId, P, D, DG) ->
ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG)
end.
@ -920,7 +933,7 @@ change_shaper(#{shaper := ShaperName, ip := IP, lserver := LServer,
Shaper = acl:access_matches(ShaperName,
#{usr => jid:split(JID), ip => IP},
LServer),
xmpp_stream_in:change_shaper(State, Shaper).
xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)).
-spec format_reason(state(), term()) -> binary().
format_reason(#{stop_reason := Reason}, _) ->

View File

@ -782,8 +782,13 @@ set_opts(State) ->
fun(#local_config{key = Key, value = Val}) ->
{Key, Val}
end, Opts)),
set_fqdn(),
set_log_level().
set_fqdn() ->
FQDNs = get_option(fqdn, []),
xmpp:set_config([{fqdn, FQDNs}]).
set_log_level() ->
Level = get_option(loglevel, 4),
ejabberd_logger:set(Level).
@ -1452,10 +1457,16 @@ opt_type(node_start) ->
fun(I) when is_integer(I), I>=0 -> I end;
opt_type(validate_stream) ->
fun(B) when is_boolean(B) -> B end;
opt_type(fqdn) ->
fun(Domain) when is_binary(Domain) ->
[Domain];
(Domains) ->
[iolist_to_binary(Domain) || Domain <- Domains]
end;
opt_type(_) ->
[hide_sensitive_log_data, hosts, language, max_fsm_queue,
default_db, default_ram_db, queue_type, queue_dir, loglevel,
use_cache, cache_size, cache_missed, cache_life_time,
use_cache, cache_size, cache_missed, cache_life_time, fqdn,
shared_key, node_start, validate_stream, negotiation_timeout].
-spec may_hide_data(any()) -> any().

View File

@ -1,224 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : ejabberd_idna.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Support for IDNA (RFC3490)
%%% Created : 10 Apr 2004 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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.
%%%
%%%----------------------------------------------------------------------
-module(ejabberd_idna).
-author('alexey@process-one.net').
-export([domain_utf8_to_ascii/1,
domain_ucs2_to_ascii/1,
utf8_to_ucs2/1]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-spec domain_utf8_to_ascii(binary()) -> false | binary().
domain_utf8_to_ascii(Domain) ->
domain_ucs2_to_ascii(utf8_to_ucs2(Domain)).
utf8_to_ucs2(S) ->
utf8_to_ucs2(binary_to_list(S), "").
utf8_to_ucs2([], R) -> lists:reverse(R);
utf8_to_ucs2([C | S], R) when C < 128 ->
utf8_to_ucs2(S, [C | R]);
utf8_to_ucs2([C1, C2 | S], R) when C1 < 224 ->
utf8_to_ucs2(S, [C1 band 31 bsl 6 bor C2 band 63 | R]);
utf8_to_ucs2([C1, C2, C3 | S], R) when C1 < 240 ->
utf8_to_ucs2(S,
[C1 band 15 bsl 12 bor (C2 band 63 bsl 6) bor C3 band 63
| R]).
-spec domain_ucs2_to_ascii(list()) -> false | binary().
domain_ucs2_to_ascii(Domain) ->
case catch domain_ucs2_to_ascii1(Domain) of
{'EXIT', _Reason} -> false;
Res -> iolist_to_binary(Res)
end.
domain_ucs2_to_ascii1(Domain) ->
Parts = string:tokens(Domain,
[46, 12290, 65294, 65377]),
ASCIIParts = lists:map(fun (P) -> to_ascii(P) end,
Parts),
string:strip(lists:flatmap(fun (P) -> [$. | P] end,
ASCIIParts),
left, $.).
%% Domain names are already nameprep'ed in ejabberd, so we skiping this step
to_ascii(Name) ->
false = lists:any(fun (C)
when (0 =< C) and (C =< 44) or
(46 =< C) and (C =< 47)
or (58 =< C) and (C =< 64)
or (91 =< C) and (C =< 96)