%%%-------------------------------------------------------------------
{deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.0.2"}}},
- {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.3"}}},
+ {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.4"}}},
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.2"}}},
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.1"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.3"}}},
CREATE TABLE [dbo].[users] (\r
[username] [varchar] (250) NOT NULL,\r
[password] [text] NOT NULL,\r
- [serverkey] [text] NOT NULL,\r
- [salt] [text] NOT NULL,\r
+ [serverkey] [text] NOT NULL DEFAULT '',\r
+ [salt] [text] NOT NULL DEFAULT '',\r
[iterationcount] [smallint] NOT NULL DEFAULT 0,\r
[created_at] [datetime] NOT NULL DEFAULT GETDATE(),\r
CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED \r
CREATE TABLE users (
username varchar(191) PRIMARY KEY,
password text NOT NULL,
- serverkey text NOT NULL DEFAULT '',
- salt text NOT NULL DEFAULT '',
+ serverkey varchar(64) NOT NULL DEFAULT '',
+ salt varchar(64) NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Add support for SCRAM auth to a database created before ejabberd 16.03:
--- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT '';
--- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT '';
+-- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT '';
+-- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT '';
-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0;
CREATE TABLE last (
mech_step(#state{server = Server} = S, ClientIn) ->
User = iolist_to_binary([randoms:get_string(),
- randoms:get_string(),
- randoms:get_string()]),
+ jlib:integer_to_binary(p1_time_compat:unique_integer([positive]))]),
case ejabberd_auth:is_user_exists(User, Server) of
true -> mech_step(S, ClientIn);
- false -> {ok, [{username, User}, {auth_module, ejabberd_auth_anonymous}]}
+ false -> {ok, [{username, User}, {authzid, User}, {auth_module, ejabberd_auth_anonymous}]}
end.
%% Create the anonymous table if at least one virtual host has anonymous features enabled
%% Register to login / logout events
-record(anonymous, {us = {<<"">>, <<"">>} :: {binary(), binary()},
- sid = {p1_time_compat:timestamp(), self()} :: ejabberd_sm:sid()}).
+ sid = ejabberd_sm:make_sid() :: ejabberd_sm:sid()}).
start(Host) ->
%% TODO: Check cluster mode
xml_socket = XMLSocket, zlib = Zlib, tls = TLS,
tls_required = StartTLSRequired,
tls_enabled = TLSEnabled, tls_options = TLSOpts,
- sid = {p1_time_compat:timestamp(), self()}, streamid = new_id(),
+ sid = ejabberd_sm:make_sid(), streamid = new_id(),
access = Access, shaper = Shaper, ip = IP,
mgmt_state = StreamMgmtState,
mgmt_max_queue = MaxAckQueue,
send_element(StateData,
?POLICY_VIOLATION_ERR(Lang,
<<"Use of STARTTLS required">>)),
- send_trailer(StateData),
{stop, normal, StateData};
true ->
fsm_next_state(wait_for_auth,
[jlib:ip_to_list(IP), LogReason]),
send_header(StateData, Server, <<"">>, DefaultLang),
send_element(StateData, ?POLICY_VIOLATION_ERR(Lang, ReasonT)),
- send_trailer(StateData),
{stop, normal, StateData};
_ ->
send_header(StateData, ?MYNAME, <<"">>, DefaultLang),
send_element(StateData, ?HOST_UNKNOWN_ERR),
- send_trailer(StateData),
{stop, normal, StateData}
end;
_ ->
send_header(StateData, ?MYNAME, <<"">>, DefaultLang),
send_element(StateData, ?INVALID_NS_ERR),
- send_trailer(StateData),
{stop, normal, StateData}
end;
wait_for_stream(timeout, StateData) ->
{stop, normal, StateData};
wait_for_stream({xmlstreamelement, _}, StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_stream({xmlstreamend, _}, StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_stream({xmlstreamerror, _}, StateData) ->
send_header(StateData, ?MYNAME, <<"1.0">>, <<"">>),
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_stream(closed, StateData) ->
{stop, normal, StateData};
wait_for_auth(timeout, StateData) ->
{stop, normal, StateData};
wait_for_auth({xmlstreamend, _Name}, StateData) ->
- send_trailer(StateData), {stop, normal, StateData};
+ {stop, normal, StateData};
wait_for_auth({xmlstreamerror, _}, StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_auth(closed, StateData) ->
{stop, normal, StateData};
send_element(StateData,
?POLICY_VIOLATION_ERR(Lang,
<<"Use of STARTTLS required">>)),
- send_trailer(StateData),
{stop, normal, StateData};
true ->
process_unauthenticated_stanza(StateData, El),
{stop, normal, StateData};
wait_for_feature_request({xmlstreamend, _Name},
StateData) ->
- send_trailer(StateData), {stop, normal, StateData};
+ {stop, normal, StateData};
wait_for_feature_request({xmlstreamerror, _},
StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_feature_request(closed, StateData) ->
{stop, normal, StateData};
{stop, normal, StateData};
wait_for_sasl_response({xmlstreamend, _Name},
StateData) ->
- send_trailer(StateData), {stop, normal, StateData};
+ {stop, normal, StateData};
wait_for_sasl_response({xmlstreamerror, _},
StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_sasl_response(closed, StateData) ->
{stop, normal, StateData};
wait_for_bind(timeout, StateData) ->
{stop, normal, StateData};
wait_for_bind({xmlstreamend, _Name}, StateData) ->
- send_trailer(StateData), {stop, normal, StateData};
+ {stop, normal, StateData};
wait_for_bind({xmlstreamerror, _}, StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
wait_for_bind(closed, StateData) ->
{stop, normal, StateData};
case check_from(El, FromJID) of
'invalid-from' ->
send_element(StateData, ?INVALID_FROM),
- send_trailer(StateData),
{stop, normal, StateData};
_NewEl ->
session_established2(El, StateData)
[?MODULE, Options, session_established, StateData]),
fsm_next_state(session_established, StateData);
session_established({xmlstreamend, _Name}, StateData) ->
- send_trailer(StateData), {stop, normal, StateData};
+ {stop, normal, StateData};
session_established({xmlstreamerror,
<<"XML stanza is too big">> = E},
StateData) ->
send_element(StateData,
?POLICY_VIOLATION_ERR((StateData#state.lang), E)),
- send_trailer(StateData),
{stop, normal, StateData};
session_established({xmlstreamerror, _}, StateData) ->
send_element(StateData, ?INVALID_XML_ERR),
- send_trailer(StateData),
{stop, normal, StateData};
session_established(closed, #state{mgmt_state = active} = StateData) ->
catch (StateData#state.sockmod):close(StateData#state.socket),
handle_info({kick, kicked_by_admin, Xmlelement}, StateName, StateData);
handle_info({kick, Reason, Xmlelement}, _StateName, StateData) ->
send_element(StateData, Xmlelement),
- send_trailer(StateData),
{stop, normal,
StateData#state{authenticated = Reason}};
handle_info({route, _From, _To, {broadcast, Data}},
{exit, Reason} ->
Lang = StateData#state.lang,
send_element(StateData, ?SERRT_CONFLICT(Lang, Reason)),
- catch send_trailer(StateData),
{stop, normal, StateData};
{privacy_list, PrivList, PrivListName} ->
case ejabberd_hooks:run_fold(privacy_updated_list,
wait_for_stream ->
send_header(StateData, ?MYNAME, <<"1.0">>, <<"en">>),
send_element(StateData, ?SERR_SYSTEM_SHUTDOWN),
- send_trailer(StateData),
ok;
_ ->
send_element(StateData, ?SERR_SYSTEM_SHUTDOWN),
- send_trailer(StateData),
ok
end,
{stop, normal, StateData};
ok
end
end,
+ catch send_trailer(StateData),
(StateData#state.sockmod):close(StateData#state.socket),
ok.
Err = ?SERRT_POLICY_VIOLATION(StateData#state.lang,
<<"Too many unacked stanzas">>),
send_element(StateData, Err),
- send_trailer(StateData),
{stop, normal, StateData#state{mgmt_resend = false}};
fsm_next_state(session_established, #state{mgmt_state = pending} = StateData) ->
fsm_next_state(wait_for_resume, StateData);
-behaviour(ejabberd_config).
-author('alexey@process-one.net').
--export([start/0, init/0, process/1,
+-export([start/0, init/0, process/1, process2/2,
register_commands/3, unregister_commands/3,
opt_type/1]).
Code.
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
+process2(Args, AccessCommands) ->
+ process2(Args, AccessCommands, ?DEFAULT_VERSION).
+
+%% @spec (Args::[string()], AccessCommands, Version) -> {String::string(), Code::integer()}
process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
list_to_binary(Pass), true}, Version);
{s2s_tls_compression, StateData#state.server},
fun(true) -> true;
(false) -> false
- end, true) of
+ end, false) of
true -> lists:delete(compression_none, TLSOpts1);
false -> [compression_none | TLSOpts1]
end,
{s2s_tls_compression, From},
fun(true) -> true;
(false) -> false
- end, true) of
+ end, false) of
false -> [compression_none | TLSOpts4];
true -> TLSOpts4
end,
get_max_user_sessions/2,
get_all_pids/0,
is_existing_resource/3,
- get_commands_spec/0
+ get_commands_spec/0,
+ make_sid/0
]).
-export([init/1, handle_call/3, handle_cast/2,
end, Resources),
length(Resources).
+make_sid() ->
+ {p1_time_compat:unique_timestamp(), self()}.
+
opt_type(sm_db_type) ->
fun (mnesia) -> mnesia;
(internal) -> mnesia;
-include("logger.hrl").
-export([start/2, stop/1, compile/1, get_cookie/0,
- remove_node/1, set_password/3,
+ remove_node/1, set_password/3, check_password/3,
check_password_hash/4, delete_old_users/1,
delete_old_users_vhost/2, ban_account/3,
num_active_users/2, num_resources/2, resource_num/3,
result_desc = "Status code: 0 on success, 1 otherwise"},
#ejabberd_commands{name = check_password, tags = [accounts],
desc = "Check if a password is correct",
- module = ejabberd_auth, function = check_password,
+ module = ?MODULE, function = check_password,
args = [{user, binary}, {host, binary}, {password, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>],
args_desc = ["User name to check", "Server to check", "Password to check"],
Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end,
user_action(User, Host, Fun, ok).
+check_password(User, Host, Password) ->
+ ejabberd_auth:check_password(User, <<>>, Host, Password).
+
%% Copied some code from ejabberd_commands.erl
check_password_hash(User, Host, PasswordHash, HashMethod) ->
AccountPass = ejabberd_auth:get_password_s(User, Host),
Txt = <<"Empty password">>,
IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]};
{error, not_allowed} ->
- Txt = <<"Chaning password is not allowed">>,
+ Txt = <<"Changing password is not allowed">>,
IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]};
{error, invalid_jid} ->
IQ#iq{type = error,
--- /dev/null
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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.
+#
+# ----------------------------------------------------------------------
+
+defmodule EjabberdCyrsaslTest do
+ @author "pawel@process-one.net"
+
+ use ExUnit.Case, async: true
+
+ setup_all do
+ :p1_sha.load_nif()
+ :mnesia.start
+ :ok = start_module(:stringprep)
+ :ok = start_module(:jid)
+ :ok = :ejabberd_config.start(["domain1"], [])
+ :ok = :cyrsasl.start
+ cyrstate = :cyrsasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1,
+ &check_password/3, &check_password_digest/5)
+ {:ok, cyrstate: cyrstate}
+ end
+
+ test "Plain text (correct user and pass)", context do
+ step1 = :cyrsasl.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">>)
+ assert step1 == {:error, "not-authorized", "user1"}, "got error response"
+ end
+
+ test "Plain text (wrong user wrong pass)", context do
+ step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"nouser1",0,"badpass">>)
+ assert step1 == {:error, "not-authorized", "nouser1"}, "got error response"
+ end
+
+ test "Anonymous", context do
+ setup_anonymous_mocks()
+ step1 = :cyrsasl.server_start(context[:cyrstate], "ANONYMOUS", "domain1")
+ assert {:ok, _} = step1
+ end
+
+ test "Digest-MD5 (correct user and pass)", context do
+ assert {:continue, init_str, state1} = :cyrsasl.server_start(context[:cyrstate], "DIGEST-MD5", "")
+ assert [_, nonce] = Regex.run(~r/nonce="(.*?)"/, init_str)
+ user = "user1"
+ domain = "domain1"
+ digest_uri = "xmpp/#{domain}"
+ pass = "pass"
+ cnonce = "abcd"
+ nc = "00000001"
+ response_hash = calc_digest_sha(user, domain, pass, nc, nonce, cnonce)
+ 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"
+ assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response)
+ assert {:ok, list} = :cyrsasl.server_step(state3, "")
+ end
+
+ defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do
+ digest_uri = "xmpp/#{domain}"
+ a0 = "#{user}:#{domain}:#{pass}"
+ a1 = "#{str_md5(a0)}:#{nonce}:#{cnonce}"
+ a2 = "AUTHENTICATE:#{digest_uri}"
+ hex_md5("#{hex_md5(a1)}:#{nonce}:#{nc}:#{cnonce}:auth:#{hex_md5(a2)}")
+ end
+
+ defp str_md5(str) do
+ :erlang.md5(str)
+ end
+
+ defp hex_md5(str) do
+ :p1_sha.to_hexlist(:erlang.md5(str))
+ end
+
+ defp setup_anonymous_mocks() do
+ :meck.unload
+ mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
+ fn (host) ->
+ true
+ end)
+ mock(:ejabberd_auth, :is_user_exists,
+ fn (user, domain) ->
+ domain == "domain1" and get_password(user) != false
+ end)
+ end
+
+ defp start_module(module) do
+ case apply(module, :start, []) do
+ :ok -> :ok
+ {:error, {:already_started, _}} -> :ok
+ other -> other
+ end
+ end
+
+ defp get_password(user) do
+ if user == "user1" or user == "user2" do
+ {"pass", :internal}
+ else
+ :false
+ end
+ end
+
+ defp check_password(user, authzid, pass) do
+ case get_password(authzid) do
+ {^pass, mod} ->
+ {true, mod}
+ _ ->
+ false
+ end
+ end
+
+ defp check_password_digest(user, authzid, pass, digest, digest_gen) do
+ case get_password(authzid) do
+ {spass, mod} ->
+ v = digest_gen.(spass)
+ if v == digest do
+ {true, mod}
+ else
+ false
+ end
+ _ ->
+ false
+ end
+ end
+
+ defp mock(module, function, fun) do
+ try do
+ :meck.new(module, [:non_strict])
+ catch
+ :error, {:already_started, _pid} -> :ok
+ end
+ :meck.expect(module, function, fun)
+ end
+end
EjabberdAuthMock.create_user @user, @domain, @password
assert :ejabberd_commands.execute_command(:check_password,
- [@user, <<"">>, @domain, @password])
+ [@user, @domain, @password])
refute :ejabberd_commands.execute_command(:check_password,
- [@user, <<"">>, @domain, "bad_password"])
+ [@user, @domain, "bad_password"])
refute :ejabberd_commands.execute_command(:check_password,
- [@user, <<"">>, "bad_domain", @password])
+ [@user, "bad_domain", @password])
refute :ejabberd_commands.execute_command(:check_password,
- ["bad_user", <<"">>, @domain, @password])
+ ["bad_user", @domain, @password])
assert :meck.validate :ejabberd_auth
assert :ejabberd_commands.execute_command(:change_password,
[@user, @domain, "new_password"])
refute :ejabberd_commands.execute_command(:check_password,
- [@user, <<"">>, @domain, @password])
+ [@user, @domain, @password])
assert :ejabberd_commands.execute_command(:check_password,
- [@user, <<"">>, @domain, "new_password"])
+ [@user, @domain, "new_password"])
assert {:not_found, 'unknown_user'} ==
catch_throw :ejabberd_commands.execute_command(:change_password,
["bad_user", @domain,