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)
or (123 =< C) and (C =< 127) ->
true;
(_) -> false
end,
Name),
case Name of
[H | _] when H /= $- -> true = lists:last(Name) /= $-
end,
ASCIIName = case lists:any(fun (C) -> C > 127 end, Name)
of
true ->
true = case Name of
"xn--" ++ _ -> false;
_ -> true
end,
"xn--" ++ punycode_encode(Name);
false -> Name
end,
L = length(ASCIIName),
true = (1 =< L) and (L =< 63),
ASCIIName.
%%% PUNYCODE (RFC3492)
-define(BASE, 36).
-define(TMIN, 1).
-define(TMAX, 26).
-define(SKEW, 38).
-define(DAMP, 700).
-define(INITIAL_BIAS, 72).
-define(INITIAL_N, 128).
punycode_encode(Input) ->
N = (?INITIAL_N),
Delta = 0,
Bias = (?INITIAL_BIAS),
Basic = lists:filter(fun (C) -> C =< 127 end, Input),
NonBasic = lists:filter(fun (C) -> C > 127 end, Input),
L = length(Input),
B = length(Basic),
SNonBasic = lists:usort(NonBasic),
Output1 = if B > 0 -> Basic ++ "-";
true -> ""
end,
Output2 = punycode_encode1(Input, SNonBasic, B, B, L, N,
Delta, Bias, ""),
Output1 ++ Output2.
punycode_encode1(Input, [M | SNonBasic], B, H, L, N,
Delta, Bias, Out)
when H < L ->
Delta1 = Delta + (M - N) * (H + 1),
% let n = m
{NewDelta, NewBias, NewH, NewOut} = lists:foldl(fun (C,
{ADelta, ABias, AH,
AOut}) ->
if C < M ->
{ADelta + 1,
ABias, AH,
AOut};
C == M ->
NewOut =
punycode_encode_delta(ADelta,
ABias,
AOut),
NewBias =
adapt(ADelta,
H +
1,
H
==
B),
{0, NewBias,
AH + 1,
NewOut};
true ->
{ADelta,
ABias, AH,
AOut}
end
end,
{Delta1, Bias, H, Out},
Input),
punycode_encode1(Input, SNonBasic, B, NewH, L, M + 1,
NewDelta + 1, NewBias, NewOut);
punycode_encode1(_Input, _SNonBasic, _B, _H, _L, _N,
_Delta, _Bias, Out) ->
lists:reverse(Out).
punycode_encode_delta(Delta, Bias, Out) ->
punycode_encode_delta(Delta, Bias, Out, ?BASE).
punycode_encode_delta(Delta, Bias, Out, K) ->
T = if K =< Bias -> ?TMIN;
K >= Bias + (?TMAX) -> ?TMAX;
true -> K - Bias
end,
if Delta < T -> [codepoint(Delta) | Out];
true ->
C = T + (Delta - T) rem ((?BASE) - T),
punycode_encode_delta((Delta - T) div ((?BASE) - T),
Bias, [codepoint(C) | Out], K + (?BASE))
end.
adapt(Delta, NumPoints, FirstTime) ->
Delta1 = if FirstTime -> Delta div (?DAMP);
true -> Delta div 2
end,
Delta2 = Delta1 + Delta1 div NumPoints,
adapt1(Delta2, 0).
adapt1(Delta, K) ->
if Delta > ((?BASE) - (?TMIN)) * (?TMAX) div 2 ->
adapt1(Delta div ((?BASE) - (?TMIN)), K + (?BASE));
true ->
K +
((?BASE) - (?TMIN) + 1) * Delta div (Delta + (?SKEW))
end.
codepoint(C) ->
if (0 =< C) and (C =< 25) -> C + 97;
(26 =< C) and (C =< 35) -> C + 22
end.
%%%===================================================================
%%% Unit tests
%%%===================================================================
-ifdef(TEST).
acsii_test() ->
?assertEqual(<<"test.org">>, domain_utf8_to_ascii(<<"test.org">>)).
utf8_test() ->
?assertEqual(
<<"xn--d1acufc.xn--p1ai">>,
domain_utf8_to_ascii(
<<208,180,208,190,208,188,208,181,208,189,46,209,128,209,132>>)).
-endif.

View File

@ -214,6 +214,10 @@ set(LogLevel) when is_integer(LogLevel) ->
ok
end, gen_event:which_handlers(lager_event))
end,
case LogLevel of
5 -> xmpp:set_config([{debug, true}]);
_ -> ok
end,
{module, lager};
set({_LogLevel, _}) ->
error_logger:error_msg("custom loglevels are not supported for 'lager'"),

View File

@ -133,7 +133,7 @@ get_certfile(Domain) ->
-spec get_certfile_no_default(binary()) -> {ok, binary()} | error.
get_certfile_no_default(Domain) ->
case ejabberd_idna:domain_utf8_to_ascii(Domain) of
case xmpp_idna:domain_utf8_to_ascii(Domain) of
false ->
error;
ASCIIDomain ->

View File

@ -30,8 +30,8 @@
%% 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,
compress_methods/1,
-export([tls_options/1, tls_required/1, tls_enabled/1,
compress_methods/1, sasl_mechanisms/2,
unauthenticated_stream_features/1, authenticated_stream_features/1,
handle_stream_start/2, handle_stream_end/2,
handle_stream_established/1, handle_auth_success/4,
@ -144,12 +144,15 @@ tls_options(#{tls_options := TLSOpts, server_host := LServer}) ->
tls_required(#{server_host := LServer}) ->
ejabberd_s2s:tls_required(LServer).
tls_verify(#{server_host := LServer}) ->
ejabberd_s2s:tls_verify(LServer).
tls_enabled(#{server_host := LServer}) ->
ejabberd_s2s:tls_enabled(LServer).
sasl_mechanisms(Mechs, #{server_host := LServer}) ->
lists:filter(
fun(<<"EXTERNAL">>) -> ejabberd_s2s:tls_verify(LServer);
(_) -> false
end, Mechs).
compress_methods(#{server_host := LServer}) ->
case ejabberd_s2s:zlib_enabled(LServer) of
true -> [<<"zlib">>];
@ -344,7 +347,7 @@ set_idle_timeout(State) ->
change_shaper(#{shaper := ShaperName, server_host := ServerHost} = State,
RServer) ->
Shaper = acl:match_rule(ServerHost, ShaperName, jid:make(RServer)),
xmpp_stream_in:change_shaper(State, Shaper).
xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)).
-spec listen_opt_type(shaper) -> fun((any()) -> any());
(certfile) -> fun((binary()) -> binary());

View File

@ -101,7 +101,7 @@ init([State, Opts]) ->
end,
GlobalRoutes = proplists:get_value(global_routes, Opts, true),
Timeout = ejabberd_config:negotiation_timeout(),
State1 = xmpp_stream_in:change_shaper(State, Shaper),
State1 = xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)),
State2 = xmpp_stream_in:set_timeout(State1, Timeout),
State3 = State2#{access => Access,
xmlns => ?NS_COMPONENT,

View File

@ -39,7 +39,6 @@ init([]) ->
{ok, {{one_for_one, 10, 1},
[worker(ejabberd_hooks),
worker(ejabberd_cluster),
worker(cyrsasl),
worker(translate),
worker(ejabberd_access_permissions),
worker(ejabberd_ctl),

View File

@ -1,81 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : scram.erl
%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
%%% Purpose : SCRAM (RFC 5802)
%%% 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(scram).
-author('stephen.roettger@googlemail.com').
%% External exports
%% ejabberd doesn't implement SASLPREP, so we use the similar RESOURCEPREP instead
-export([salted_password/3, stored_key/1, server_key/1,
server_signature/2, client_signature/2, client_key/1,
client_key/2]).
-spec salted_password(binary(), binary(), non_neg_integer()) -> binary().
salted_password(Password, Salt, IterationCount) ->
hi(jid:resourceprep(Password), Salt, IterationCount).
-spec client_key(binary()) -> binary().
client_key(SaltedPassword) ->
sha_mac(SaltedPassword, <<"Client Key">>).
-spec stored_key(binary()) -> binary().
stored_key(ClientKey) -> crypto:hash(sha, ClientKey).
-spec server_key(binary()) -> binary().
server_key(SaltedPassword) ->
sha_mac(SaltedPassword, <<"Server Key">>).
-spec client_signature(binary(), binary()) -> binary().
client_signature(StoredKey, AuthMessage) ->
sha_mac(StoredKey, AuthMessage).
-spec client_key(binary(), binary()) -> binary().
client_key(ClientProof, ClientSignature) ->
crypto:exor(ClientProof, ClientSignature).
-spec server_signature(binary(), binary()) -> binary().
server_signature(ServerKey, AuthMessage) ->
sha_mac(ServerKey, AuthMessage).
hi(Password, Salt, IterationCount) ->
U1 = sha_mac(Password, <<Salt/binary, 0, 0, 0, 1>>),
crypto:exor(U1, hi_round(Password, U1, IterationCount - 1)).
hi_round(Password, UPrev, 1) ->
sha_mac(Password, UPrev);
hi_round(Password, UPrev, IterationCount) ->
U = sha_mac(Password, UPrev),
crypto:exor(U, hi_round(Password, U, IterationCount - 1)).
sha_mac(Key, Data) ->
crypto:hmac(sha, Key, Data).

View File

@ -1,393 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : xmpp_socket.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Socket with zlib and TLS support library
%%% Created : 23 Aug 2006 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(xmpp_socket).
-author('alexey@process-one.net').
%% API
-export([start/4,
connect/3,
connect/4,
connect/5,
starttls/2,
compress/1,
compress/2,
reset_stream/1,
send_element/2,
send_header/2,
send_trailer/1,
send/2,
send_xml/2,
recv/2,
activate/1,
change_shaper/2,
monitor/1,
get_sockmod/1,
get_transport/1,
get_peer_certificate/2,
get_verify_result/1,
close/1,
pp/1,
sockname/1, peername/1]).
-include("xmpp.hrl").
-include("logger.hrl").
-type sockmod() :: ejabberd_bosh |
ejabberd_http_ws |
gen_tcp | fast_tls | ezlib.
-type receiver() :: atom().
-type socket() :: pid() | inet:socket() |
fast_tls:tls_socket() |
ezlib:zlib_socket() |
ejabberd_bosh:bosh_socket() |
ejabberd_http_ws:ws_socket().
-record(socket_state, {sockmod = gen_tcp :: sockmod(),
socket :: socket(),
max_stanza_size = infinity :: timeout(),
xml_stream :: undefined | fxml_stream:xml_stream_state(),
shaper = none :: none | ejabberd_shaper:shaper(),
receiver :: receiver()}).
-type socket_state() :: #socket_state{}.
-export_type([socket/0, socket_state/0, sockmod/0]).
-callback start({module(), socket_state()},
[proplists:property()]) -> {ok, pid()} | {error, term()} | ignore.
-callback start_link({module(), socket_state()},
[proplists:property()]) -> {ok, pid()} | {error, term()} | ignore.
-callback socket_type() -> xml_stream | independent | raw.
-define(is_http_socket(S),
(S#socket_state.sockmod == ejabberd_bosh orelse
S#socket_state.sockmod == ejabberd_http_ws)).
%%====================================================================
%% API
%%====================================================================
-spec start(atom(), sockmod(), socket(), [proplists:property()])
-> {ok, pid() | independent} | {error, inet:posix() | any()} | ignore.
start(Module, SockMod, Socket, Opts) ->
try
case Module:socket_type() of
independent ->
{ok, independent};
xml_stream ->
MaxStanzaSize = proplists:get_value(max_stanza_size, Opts, infinity),
Receiver = proplists:get_value(receiver, Opts),
SocketData = #socket_state{sockmod = SockMod,
socket = Socket,
receiver = Receiver,
max_stanza_size = MaxStanzaSize},
{ok, Pid} = Module:start({?MODULE, SocketData}, Opts),
Receiver1 = if is_pid(Receiver) -> Receiver;
true -> Pid
end,
ok = controlling_process(SocketData, Receiver1),
ok = become_controller(SocketData, Pid),
{ok, Receiver1};
raw ->
{ok, Pid} = Module:start({SockMod, Socket}, Opts),
ok = SockMod:controlling_process(Socket, Pid),
{ok, Pid}
end
catch
_:{badmatch, {error, _} = Err} ->
SockMod:close(Socket),
Err;
_:{badmatch, ignore} ->
SockMod:close(Socket),
ignore
end.
connect(Addr, Port, Opts) ->
connect(Addr, Port, Opts, infinity, self()).
connect(Addr, Port, Opts, Timeout) ->
connect(Addr, Port, Opts, Timeout, self()).
connect(Addr, Port, Opts, Timeout, Owner) ->
case gen_tcp:connect(Addr, Port, Opts, Timeout) of
{ok, Socket} ->
SocketData = #socket_state{sockmod = gen_tcp, socket = Socket},
case controlling_process(SocketData, Owner) of
ok ->
activate_after(Socket, Owner, 0),
{ok, SocketData};
{error, _Reason} = Error ->
gen_tcp:close(Socket),
Error
end;
{error, _Reason} = Error ->
Error
end.
starttls(#socket_state{socket = Socket,
receiver = undefined} = SocketData, TLSOpts) ->
case fast_tls:tcp_to_tls(Socket, TLSOpts) of
{ok, TLSSocket} ->
SocketData1 = SocketData#socket_state{socket = TLSSocket,
sockmod = fast_tls},
SocketData2 = reset_stream(SocketData1),
case fast_tls:recv_data(TLSSocket, <<>>) of
{ok, TLSData} ->
parse(SocketData2, TLSData);
{error, _} = Err ->
Err
end;
{error, _} = Err ->
Err
end.
compress(SocketData) -> compress(SocketData, undefined).
compress(#socket_state{receiver = undefined,
sockmod = SockMod,
socket = Socket} = SocketData, Data) ->
{ok, ZlibSocket} = ezlib:enable_zlib(SockMod, Socket),
case Data of
undefined -> ok;
_ -> send(SocketData, Data)
end,
SocketData1 = SocketData#socket_state{socket = ZlibSocket,
sockmod = ezlib},
SocketData2 = reset_stream(SocketData1),
case ezlib:recv_data(ZlibSocket, <<"">>) of
{ok, ZlibData} ->
parse(SocketData2, ZlibData);
{error, _} = Err ->
Err
end.
reset_stream(#socket_state{xml_stream = XMLStream,
receiver = Receiver,
sockmod = SockMod, socket = Socket,
max_stanza_size = MaxStanzaSize} = SocketData) ->
XMLStream1 = try fxml_stream:reset(XMLStream)
catch error:_ ->
close_stream(XMLStream),
fxml_stream:new(self(), MaxStanzaSize)
end,
case Receiver of
undefined ->
SocketData#socket_state{xml_stream = XMLStream1};
_ ->
Socket1 = SockMod:reset_stream(Socket),
SocketData#socket_state{xml_stream = XMLStream1, socket = Socket1}
end.
-spec send_element(socket_state(), fxml:xmlel()) -> ok | {error, inet:posix()}.
send_element(SocketData, El) when ?is_http_socket(SocketData) ->
send_xml(SocketData, {xmlstreamelement, El});
send_element(SocketData, El) ->
send(SocketData, fxml:element_to_binary(El)).
-spec send_header(socket_state(), fxml:xmlel()) -> ok | {error, inet:posix()}.
send_header(SocketData, El) when ?is_http_socket(SocketData) ->
send_xml(SocketData, {xmlstreamstart, El#xmlel.name, El#xmlel.attrs});
send_header(SocketData, El) ->
send(SocketData, fxml:element_to_header(El)).
-spec send_trailer(socket_state()) -> ok | {error, inet:posix()}.
send_trailer(SocketData) when ?is_http_socket(SocketData) ->
send_xml(SocketData, {xmlstreamend, <<"stream:stream">>});
send_trailer(SocketData) ->
send(SocketData, <<"</stream:stream>">>).
-spec send(socket_state(), iodata()) -> ok | {error, closed | inet:posix()}.
send(#socket_state{sockmod = SockMod, socket = Socket} = SocketData, Data) ->
?DEBUG("(~s) Send XML on stream = ~p", [pp(SocketData), Data]),
try SockMod:send(Socket, Data) of
{error, einval} -> {error, closed};
Result -> Result
catch _:badarg ->
%% Some modules throw badarg exceptions on closed sockets
%% TODO: their code should be improved
{error, closed}
end.
-spec send_xml(socket_state(),
{xmlstreamelement, fxml:xmlel()} |
{xmlstreamstart, binary(), [{binary(), binary()}]} |
{xmlstreamend, binary()} |
{xmlstreamraw, iodata()}) -> term().
send_xml(SocketData, El) ->
(SocketData#socket_state.sockmod):send_xml(SocketData#socket_state.socket, El).
recv(#socket_state{xml_stream = undefined} = SocketData, Data) ->
XMLStream = fxml_stream:new(self(), SocketData#socket_state.max_stanza_size),
recv(SocketData#socket_state{xml_stream = XMLStream}, Data);
recv(#socket_state{sockmod = SockMod, socket = Socket} = SocketData, Data) ->
case SockMod of
fast_tls ->
case fast_tls:recv_data(Socket, Data) of
{ok, TLSData} ->
parse(SocketData, TLSData);
{error, _} = Err ->
Err
end;
ezlib ->
case ezlib:recv_data(Socket, Data) of
{ok, ZlibData} ->
parse(SocketData, ZlibData);
{error, _} = Err ->
Err
end;
_ ->
parse(SocketData, Data)
end.
change_shaper(#socket_state{receiver = undefined} = SocketData, Shaper) ->
ShaperState = ejabberd_shaper:new(Shaper),
SocketData#socket_state{shaper = ShaperState};
change_shaper(#socket_state{sockmod = SockMod,
socket = Socket} = SocketData, Shaper) ->
SockMod:change_shaper(Socket, Shaper),
SocketData.
monitor(#socket_state{receiver = undefined}) ->
make_ref();
monitor(#socket_state{sockmod = SockMod, socket = Socket}) ->
SockMod:monitor(Socket).
controlling_process(#socket_state{sockmod = SockMod,
socket = Socket}, Pid) ->
SockMod:controlling_process(Socket, Pid).
become_controller(#socket_state{receiver = Receiver,
sockmod = SockMod,
socket = Socket}, Pid) ->
if is_pid(Receiver) ->
SockMod:become_controller(Receiver, Pid);
true ->
activate_after(Socket, Pid, 0)
end.
get_sockmod(SocketData) ->
SocketData#socket_state.sockmod.
get_transport(#socket_state{sockmod = SockMod,
socket = Socket}) ->
case SockMod of
gen_tcp -> tcp;
fast_tls -> tls;
ezlib ->
case ezlib:get_sockmod(Socket) of
gen_tcp -> tcp_zlib;
fast_tls -> tls_zlib
end;
ejabberd_bosh -> http_bind;
ejabberd_http_ws -> websocket
end.
get_peer_certificate(SocketData, Type) ->
fast_tls:get_peer_certificate(SocketData#socket_state.socket, Type).
get_verify_result(SocketData) ->
fast_tls:get_verify_result(SocketData#socket_state.socket).
close(#socket_state{sockmod = SockMod, socket = Socket}) ->
SockMod:close(Socket).
sockname(#socket_state{sockmod = SockMod,
socket = Socket}) ->
case SockMod of
gen_tcp -> inet:sockname(Socket);
_ -> SockMod:sockname(Socket)
end.
peername(#socket_state{sockmod = SockMod,
socket = Socket}) ->
case SockMod of
gen_tcp -> inet:peername(Socket);
_ -> SockMod:peername(Socket)
end.
activate(#socket_state{sockmod = SockMod, socket = Socket}) ->
case SockMod of
gen_tcp -> inet:setopts(Socket, [{active, once}]);
_ -> SockMod:setopts(Socket, [{active, once}])
end.
activate_after(Socket, Pid, Pause) ->
if Pause > 0 ->
erlang:send_after(Pause, Pid, {tcp, Socket, <<>>});
true ->
Pid ! {tcp, Socket, <<>>}
end,
ok.
pp(#socket_state{receiver = Receiver} = State) ->
Transport = get_transport(State),
Receiver1 = case Receiver of
undefined -> self();
_ -> Receiver
end,
io_lib:format("~s|~w", [Transport, Receiver1]).
parse(SocketData, Data) when Data == <<>>; Data == [] ->
case activate(SocketData) of
ok ->
{ok, SocketData};
{error, _} = Err ->
Err
end;
parse(SocketData, [El | Els]) when is_record(El, xmlel) ->
self() ! {'$gen_event', {xmlstreamelement, El}},
parse(SocketData, Els);
parse(SocketData, [El | Els]) when
element(1, El) == xmlstreamstart;
element(1, El) == xmlstreamelement;
element(1, El) == xmlstreamend;
element(1, El) == xmlstreamerror ->
self() ! {'$gen_event', El},
parse(SocketData, Els);
parse(#socket_state{xml_stream = XMLStream,
socket = Socket,
shaper = ShaperState} = SocketData, Data)
when is_binary(Data) ->
?DEBUG("(~s) Received XML on stream = ~p", [pp(SocketData), Data]),
XMLStream1 = fxml_stream:parse(XMLStream, Data),
{ShaperState1, Pause} = ejabberd_shaper:update(ShaperState, byte_size(Data)),
Ret = if Pause > 0 ->
activate_after(Socket, self(), Pause);
true ->
activate(SocketData)
end,
case Ret of
ok ->
{ok, SocketData#socket_state{xml_stream = XMLStream1,
shaper = ShaperState1}};
{error, _} = Err ->
Err
end.
close_stream(undefined) ->
ok;
close_stream(XMLStream) ->
fxml_stream:close(XMLStream).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,271 +0,0 @@
%%%-------------------------------------------------------------------
%%% Created : 13 Dec 2016 by Evgeny Khramtsov <ekhramtsov@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(xmpp_stream_pkix).
%% API
-export([authenticate/1, authenticate/2, get_cert_domains/1, format_error/1]).
-include("xmpp.hrl").
-include_lib("public_key/include/public_key.hrl").
-include("XmppAddr.hrl").
-type cert() :: #'OTPCertificate'{}.
%%%===================================================================
%%% API
%%%===================================================================
-spec authenticate(xmpp_stream_in:state() | xmpp_stream_out:state())
-> {ok, binary()} | {error, atom(), binary()}.
authenticate(State) ->
authenticate(State, <<"">>).
-spec authenticate(xmpp_stream_in:state() | xmpp_stream_out:state(), binary())
-> {ok, binary()} | {error, atom(), binary()}.
authenticate(#{xmlns := ?NS_SERVER,
socket := Socket} = State, Authzid) ->
Peer = maps:get(remote_server, State, Authzid),
case verify_cert(Socket) of
{ok, Cert} ->
case ejabberd_idna:domain_utf8_to_ascii(Peer) of
false ->
{error, idna_failed, Peer};
AsciiPeer ->
case lists:any(
fun(D) -> match_domain(AsciiPeer, D) end,
get_cert_domains(Cert)) of
true ->
{ok, Peer};
false ->
{error, hostname_mismatch, Peer}
end
end;
{error, Reason} ->
{error, Reason, Peer}
end;
authenticate(#{xmlns := ?NS_CLIENT,
socket := Socket, lserver := LServer}, Authzid) ->
JID = try jid:decode(Authzid)
catch _:{bad_jid, <<>>} -> jid:make(LServer);
_:{bad_jid, _} -> {error, invalid_authzid, Authzid}
end,
case JID of
#jid{user = User} ->
case verify_cert(Socket) of
{ok, Cert} ->
JIDs = get_xmpp_addrs(Cert),
get_username(JID, JIDs, LServer);
{error, Reason} ->
{error, Reason, User}
end;
Err ->
Err
end.
format_error(idna_failed) ->
{'bad-protocol', <<"Remote domain is not an IDN hostname">>};
format_error(hostname_mismatch) ->
{'not-authorized', <<"Certificate host name mismatch">>};
format_error(jid_mismatch) ->
{'not-authorized', <<"Certificate JID mismatch">>};
format_error(get_cert_failed) ->
{'bad-protocol', <<"Failed to get peer certificate">>};
format_error(invalid_authzid) ->
{'invalid-authzid', <<"Malformed JID">>};
format_error(Other) ->
{'not-authorized', erlang:atom_to_binary(Other, utf8)}.
-spec get_cert_domains(cert()) -> [binary()].
get_cert_domains(Cert) ->
TBSCert = Cert#'OTPCertificate'.tbsCertificate,
{rdnSequence, Subject} = TBSCert#'OTPTBSCertificate'.subject,
Extensions = TBSCert#'OTPTBSCertificate'.extensions,
get_domain_from_subject(lists:flatten(Subject)) ++
get_domains_from_san(Extensions).
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec verify_cert(xmpp_socket:socket()) -> {ok, cert()} | {error, atom()}.
verify_cert(Socket) ->
case xmpp_socket:get_peer_certificate(Socket, otp) of
{ok, Cert} ->
case xmpp_socket:get_verify_result(Socket) of
0 ->
{ok, Cert};
VerifyRes ->
%% TODO: return atomic errors
%% This should be improved in fast_tls
Reason = fast_tls:get_cert_verify_string(VerifyRes, Cert),
{error, erlang:binary_to_atom(Reason, utf8)}
end;
{error, _Reason} ->
{error, get_cert_failed};
error ->
{error, get_cert_failed}
end.
-spec get_domain_from_subject([#'AttributeTypeAndValue'{}]) -> [binary()].
get_domain_from_subject(AttrVals) ->
case lists:keyfind(?'id-at-commonName',
#'AttributeTypeAndValue'.type,
AttrVals) of
#'AttributeTypeAndValue'{value = {_, S}} ->
try jid:decode(iolist_to_binary(S)) of
#jid{luser = <<"">>, lresource = <<"">>, lserver = Domain} ->
[Domain];
_ ->
[]
catch _:{bad_jid, _} ->
[]
end;
_ ->
[]
end.
-spec get_domains_from_san([#'Extension'{}] | asn1_NOVALUE) -> [binary()].
get_domains_from_san(Extensions) when is_list(Extensions) ->
case lists:keyfind(?'id-ce-subjectAltName',
#'Extension'.extnID,
Extensions) of
#'Extension'{extnValue = Vals} ->
lists:flatmap(
fun({dNSName, S}) ->
[iolist_to_binary(S)];
({otherName, AnotherName}) ->
case decode_xmpp_addr(AnotherName) of
{ok, #jid{luser = <<"">>,
lresource = <<"">>,
lserver = Domain}} ->
case ejabberd_idna:domain_utf8_to_ascii(Domain) of
false ->
[];
ASCIIDomain ->
[ASCIIDomain]
end;
_ ->
[]
end;
(_) ->
[]
end, Vals);
_ ->
[]
end;
get_domains_from_san(_) ->
[].
-spec decode_xmpp_addr(#'AnotherName'{}) -> {ok, jid()} | error.
decode_xmpp_addr(#'AnotherName'{'type-id' = ?'id-on-xmppAddr',
value = XmppAddr}) ->
try 'XmppAddr':decode('XmppAddr', XmppAddr) of
{ok, JIDStr} ->
try {ok, jid:decode(iolist_to_binary(JIDStr))}
catch _:{bad_jid, _} -> error
end;
_ ->
error
catch _:_ ->
error
end;
decode_xmpp_addr(_) ->
error.
-spec get_xmpp_addrs(cert()) -> [jid()].
get_xmpp_addrs(Cert) ->
TBSCert = Cert#'OTPCertificate'.tbsCertificate,
case TBSCert#'OTPTBSCertificate'.extensions of
Extensions when is_list(Extensions) ->
case lists:keyfind(?'id-ce-subjectAltName',
#'Extension'.extnID,
Extensions) of
#'Extension'{extnValue = Vals} ->
lists:flatmap(
fun({otherName, AnotherName}) ->
case decode_xmpp_addr(AnotherName) of
{ok, JID} -> [JID];
_ -> []
end;
(_) ->
[]
end, Vals);
_ ->
[]
end;
_ ->
[]
end.
match_domain(Domain, Domain) -> true;
match_domain(Domain, Pattern) ->
DLabels = str:tokens(Domain, <<".">>),
PLabels = str:tokens(Pattern, <<".">>),
match_labels(DLabels, PLabels).
match_labels([], []) -> true;
match_labels([], [_ | _]) -> false;
match_labels([_ | _], []) -> false;
match_labels([DL | DLabels], [PL | PLabels]) ->
case lists:all(fun (C) ->
$a =< C andalso C =< $z orelse
$0 =< C andalso C =< $9 orelse
C == $- orelse C == $*
end,
binary_to_list(PL))
of
true ->
Regexp = ejabberd_regexp:sh_to_awk(PL),
case ejabberd_regexp:run(DL, Regexp) of
match -> match_labels(DLabels, PLabels);
nomatch -> false
end;
false -> false
end.
-spec get_username(jid(), [jid()], binary()) ->
{ok, binary()} | {error, jid_mismatch, binary()}.
get_username(#jid{user = User, lserver = LS}, _, LServer) when LS /= LServer ->
%% The user provided JID from different domain
{error, jid_mismatch, User};
get_username(#jid{user = <<>>}, [#jid{user = U, lserver = LS}], LServer)
when U /= <<>> andalso LS == LServer ->
%% The user didn't provide JID or username, and there is only
%% one 'non-global' JID matching current domain
{ok, U};
get_username(#jid{user = User, luser = LUser}, JIDs, LServer) when User /= <<>> ->
%% The user provided username
lists:foldl(
fun(_, {ok, _} = OK) ->
OK;
(#jid{user = <<>>, lserver = LS}, _) when LS == LServer ->
%% Found "global" JID in the certficate
%% (i.e. in the form of 'domain.com')
%% within current domain, so we force matching
{ok, User};
(#jid{luser = LU, lserver = LS}, _) when LU == LUser, LS == LServer ->
%% Found exact JID matching
{ok, User};
(_, Err) ->
Err
end, {error, jid_mismatch, User}, JIDs);
get_username(#jid{user = User}, _, _) ->
%% Nothing from above is true
{error, jid_mismatch, User}.

View File

@ -32,32 +32,32 @@ defmodule EjabberdCyrsaslTest do
start_module(:jid)
:ejabberd_hooks.start_link
:ok = :ejabberd_config.start(["domain1"], [])
{:ok, _} = :cyrsasl.start_link
cyrstate = :cyrsasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1,
{:ok, _} = :xmpp_sasl.start_link
cyrstate = :xmpp_sasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1,
&check_password/3, &check_password_digest/5)
setup_anonymous_mocks()
{:ok, cyrstate: cyrstate}
end
test "Plain text (correct user and pass)", context do
step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"pass">>)
step1 = :xmpp_sasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"pass">>)
assert {:ok, _} = step1
{:ok, kv} = step1
assert kv[:authzid] == "user1", "got correct user"
end
test "Plain text (correct user wrong pass)", context do
step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"badpass">>)
step1 = :xmpp_sasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"badpass">>)
assert step1 == {:error, :not_authorized, "user1"}
end
test "Plain text (wrong user wrong pass)", context do
step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"nouser1",0,"badpass">>)
step1 = :xmpp_sasl.server_start(context[:cyrstate], "PLAIN", <<0,"nouser1",0,"badpass">>)
assert step1 == {:error, :not_authorized, "nouser1"}
end
test "Anonymous", context do
step1 = :cyrsasl.server_start(context[:cyrstate], "ANONYMOUS", "domain1")
step1 = :xmpp_sasl.server_start(context[:cyrstate], "ANONYMOUS", "domain1")
assert {:ok, _} = step1
end
@ -78,7 +78,7 @@ defmodule EjabberdCyrsaslTest do
end
defp process_digest_md5(cyrstate, user, domain, pass) do
assert {:continue, init_str, state1} = :cyrsasl.server_start(cyrstate, "DIGEST-MD5", "")
assert {:continue, init_str, state1} = :xmpp_sasl.server_start(cyrstate, "DIGEST-MD5", "")
assert [_, nonce] = Regex.run(~r/nonce="(.*?)"/, init_str)
digest_uri = "xmpp/#{domain}"
cnonce = "abcd"
@ -87,8 +87,8 @@ defmodule EjabberdCyrsaslTest do
response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
"nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
"charset=utf-8,algorithm=md5-sess"
case :cyrsasl.server_step(state1, response) do
{:continue, _calc_str, state2} -> :cyrsasl.server_step(state2, "")
case :xmpp_sasl.server_step(state1, response) do
{:continue, _calc_str, state2} -> :xmpp_sasl.server_step(state2, "")
other -> other
end
end

View File

@ -599,7 +599,7 @@ sasl_new(<<"ANONYMOUS">>, _) ->
sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) ->
{<<"">>,
fun (ServerIn) ->
case cyrsasl_digest:parse(ServerIn) of
case xmpp_sasl_digest:parse(ServerIn) of
bad -> {error, <<"Invalid SASL challenge">>};
KeyVals ->
Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals),
@ -625,7 +625,7 @@ sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) ->
MyResponse/binary, "\"">>,
{Resp,
fun (ServerIn2) ->
case cyrsasl_digest:parse(ServerIn2) of
case xmpp_sasl_digest:parse(ServerIn2) of
bad -> {error, <<"Invalid SASL challenge">>};
_KeyVals2 ->
{<<"">>,