aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh
diff options
context:
space:
mode:
authorHans Nilsson <[email protected]>2015-10-29 19:51:04 +0100
committerHans Nilsson <[email protected]>2015-11-04 12:09:56 +0100
commitc505918a86fb9ac8c19e47cd751a9db4e2d9efb2 (patch)
treed0f0c0c284b906a3c55d5951f301b8287da3fc93 /lib/ssh
parent4f085471fc4e1886bd7549cf135e7038a87e6a8e (diff)
downloadotp-c505918a86fb9ac8c19e47cd751a9db4e2d9efb2.tar.gz
otp-c505918a86fb9ac8c19e47cd751a9db4e2d9efb2.tar.bz2
otp-c505918a86fb9ac8c19e47cd751a9db4e2d9efb2.zip
ssh: pwdfun/4 and simple tests
Also solves OTP-13053
Diffstat (limited to 'lib/ssh')
-rw-r--r--lib/ssh/src/ssh.erl4
-rw-r--r--lib/ssh/src/ssh.hrl1
-rw-r--r--lib/ssh/src/ssh_auth.erl49
-rw-r--r--lib/ssh/test/ssh_options_SUITE.erl161
4 files changed, 199 insertions, 16 deletions
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
index 049018b21c..6f79b48091 100644
--- a/lib/ssh/src/ssh.erl
+++ b/lib/ssh/src/ssh.erl
@@ -462,7 +462,9 @@ handle_ssh_option({password, Value} = Opt) when is_list(Value) ->
Opt;
handle_ssh_option({user_passwords, Value} = Opt) when is_list(Value)->
Opt;
-handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value) ->
+handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,2) ->
+ Opt;
+handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,4) ->
Opt;
handle_ssh_option({key_cb, Value} = Opt) when is_atom(Value) ->
Opt;
diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
index fc9d60c500..4ad936f742 100644
--- a/lib/ssh/src/ssh.hrl
+++ b/lib/ssh/src/ssh.hrl
@@ -138,6 +138,7 @@
kb_tries_left = 0, % integer(), num tries left for "keyboard-interactive"
userauth_preference,
available_host_keys,
+ pwdfun_user_state,
authenticated = false
}).
diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl
index 04749fcf8e..4272eb3c52 100644
--- a/lib/ssh/src/ssh_auth.erl
+++ b/lib/ssh/src/ssh_auth.erl
@@ -174,15 +174,15 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
#ssh{opts = Opts,
userauth_supported_methods = Methods} = Ssh) ->
Password = unicode:characters_to_list(BinPwd),
- case check_password(User, Password, Opts) of
- true ->
+ case check_password(User, Password, Opts, Ssh) of
+ {true,Ssh1} ->
{authorized, User,
- ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)};
- false ->
+ ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh1)};
+ {false,Ssh1} ->
{not_authorized, {User, {error,"Bad user or password"}},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications = Methods,
- partial_success = false}, Ssh)}
+ partial_success = false}, Ssh1)}
end;
handle_userauth_request(#ssh_msg_userauth_request{user = User,
@@ -335,16 +335,16 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{num_responses = 1,
kb_tries_left = KbTriesLeft,
user = User,
userauth_supported_methods = Methods} = Ssh) ->
- case check_password(User, unicode:characters_to_list(Password), Opts) of
- true ->
+ case check_password(User, unicode:characters_to_list(Password), Opts, Ssh) of
+ {true,Ssh1} ->
{authorized, User,
- ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)};
- false ->
+ ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh1)};
+ {false,Ssh1} ->
{not_authorized, {User, {error,"Bad user or password"}},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications = Methods,
partial_success = false},
- Ssh#ssh{kb_tries_left = max(KbTriesLeft-1, 0)}
+ Ssh1#ssh{kb_tries_left = max(KbTriesLeft-1, 0)}
)}
end;
@@ -387,13 +387,34 @@ user_name(Opts) ->
{ok, User}
end.
-check_password(User, Password, Opts) ->
+check_password(User, Password, Opts, Ssh) ->
case proplists:get_value(pwdfun, Opts) of
undefined ->
Static = get_password_option(Opts, User),
- Password == Static;
- Cheker ->
- Cheker(User, Password)
+ {Password == Static, Ssh};
+
+ Checker when is_function(Checker,2) ->
+ {Checker(User, Password), Ssh};
+
+ Checker when is_function(Checker,4) ->
+ #ssh{pwdfun_user_state = PrivateState,
+ peer = {_,PeerAddr={_,_}}
+ } = Ssh,
+ case Checker(User, Password, PeerAddr, PrivateState) of
+ true ->
+ {true,Ssh};
+ false ->
+ {false,Ssh};
+ {true,NewState} ->
+ {true, Ssh#ssh{pwdfun_user_state=NewState}};
+ {false,NewState} ->
+ {false, Ssh#ssh{pwdfun_user_state=NewState}};
+ disconnect ->
+ throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE,
+ description =
+ "Unable to connect using the available authentication methods",
+ language = ""})
+ end
end.
get_password_option(Opts, User) ->
diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
index cf15ca4253..6a201d401f 100644
--- a/lib/ssh/test/ssh_options_SUITE.erl
+++ b/lib/ssh/test/ssh_options_SUITE.erl
@@ -45,6 +45,9 @@
max_sessions_ssh_connect_sequential/1,
server_password_option/1,
server_userpassword_option/1,
+ server_pwdfun_option/1,
+ server_pwdfun_4_option/1,
+ server_pwdfun_4_option_repeat/1,
ssh_connect_arg4_timeout/1,
ssh_connect_negtimeout_parallel/1,
ssh_connect_negtimeout_sequential/1,
@@ -83,6 +86,9 @@ all() ->
connectfun_disconnectfun_client,
server_password_option,
server_userpassword_option,
+ server_pwdfun_option,
+ server_pwdfun_4_option,
+ server_pwdfun_4_option_repeat,
{group, dir_options},
ssh_connect_timeout,
ssh_connect_arg4_timeout,
@@ -188,7 +194,9 @@ init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(TestCase, Config) when TestCase == server_password_option;
- TestCase == server_userpassword_option ->
+ TestCase == server_userpassword_option;
+ TestCase == server_pwdfun_option;
+ TestCase == server_pwdfun_4_option ->
UserDir = filename:join(?config(priv_dir, Config), nopubkey),
ssh_test_lib:del_dirs(UserDir),
end_per_testcase(Config);
@@ -272,6 +280,157 @@ server_userpassword_option(Config) when is_list(Config) ->
ssh:stop_daemon(Pid).
%%--------------------------------------------------------------------
+%%% validate to server that uses the 'pwdfun' option
+server_pwdfun_option(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ CHKPWD = fun("foo",Pwd) -> Pwd=="bar";
+ (_,_) -> false
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {pwdfun,CHKPWD}]),
+ ConnectionRef =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef),
+
+ Reason = "Unable to connect using the available authentication methods",
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "vego"},
+ {password, "foo"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:stop_daemon(Pid).
+
+
+%%--------------------------------------------------------------------
+%%% validate to server that uses the 'pwdfun/4' option
+server_pwdfun_4_option(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ PWDFUN = fun("foo",Pwd,{_,_},undefined) -> Pwd=="bar";
+ ("fie",Pwd,{_,_},undefined) -> {Pwd=="bar",new_state};
+ ("bandit",_,_,_) -> disconnect;
+ (_,_,_,_) -> false
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {pwdfun,PWDFUN}]),
+ ConnectionRef1 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef1),
+
+ ConnectionRef2 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "fie"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef2),
+
+ Reason = "Unable to connect using the available authentication methods",
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "fie"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "vego"},
+ {password, "foo"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "bandit"},
+ {password, "pwd breaking"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:stop_daemon(Pid).
+
+
+%%--------------------------------------------------------------------
+server_pwdfun_4_option_repeat(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ %% Test that the state works
+ Parent = self(),
+ PWDFUN = fun("foo",P="bar",_,S) -> Parent!{P,S},true;
+ (_,P,_,S=undefined) -> Parent!{P,S},{false,1};
+ (_,P,_,S) -> Parent!{P,S}, {false,S+1}
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {auth_methods,"keyboard-interactive"},
+ {pwdfun,PWDFUN}]),
+
+ %% Try with passwords "incorrect", "Bad again" and finally "bar"
+ KIFFUN = fun(_,_,_) ->
+ K={k,self()},
+ case get(K) of
+ undefined ->
+ put(K,1),
+ ["incorrect"];
+ 2 ->
+ put(K,3),
+ ["bar"];
+ S->
+ put(K,S+1),
+ ["Bad again"]
+ end
+ end,
+
+ ConnectionRef2 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {keyboard_interact_fun, KIFFUN},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef2),
+ ssh:stop_daemon(Pid),
+
+ lists:foreach(fun(Expect) ->
+ receive
+ Expect -> ok;
+ Other -> ct:fail("Expect: ~p~nReceived ~p",[Expect,Other])
+ after
+ 2000 -> ct:fail("Timeout expecting ~p",[Expect])
+ end
+ end, [{"incorrect",undefined},
+ {"Bad again",1},
+ {"bar",2}]).
+
+%%--------------------------------------------------------------------
system_dir_option(Config) ->
DirUnread = proplists:get_value(unreadable_dir,Config),
FileRead = proplists:get_value(readable_file,Config),