A simple stalling TUI! Also, ensure sessions are counted correctly. Next up, some way of pub-sub across multiple nodes
This commit is contained in:
parent
3308036c08
commit
58d0b1a89c
@ -2,10 +2,13 @@ defmodule Chessh.Application do
|
|||||||
alias Chessh.{PlayerSession, Node}
|
alias Chessh.{PlayerSession, Node}
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
def initialize_player_sessions_on_node() do
|
def initialize_node() do
|
||||||
# If we have more than one node running the ssh daemon, we'd want to ensure
|
# If we have more than one node running the ssh daemon, we'd want to ensure
|
||||||
# this is restarting after every potential crash. Otherwise the player sessions
|
# this is restarting after every potential crash. Otherwise the player sessions
|
||||||
# on the node would hang.
|
# on the node would hang.
|
||||||
|
|
||||||
|
# User session also need to be cleaned up after the node exits the pool for
|
||||||
|
# the same reason.
|
||||||
node_id = System.fetch_env!("NODE_ID")
|
node_id = System.fetch_env!("NODE_ID")
|
||||||
Node.boot(node_id)
|
Node.boot(node_id)
|
||||||
PlayerSession.delete_all_on_node(node_id)
|
PlayerSession.delete_all_on_node(node_id)
|
||||||
@ -16,7 +19,7 @@ defmodule Chessh.Application do
|
|||||||
opts = [strategy: :one_for_one, name: Chessh.Supervisor]
|
opts = [strategy: :one_for_one, name: Chessh.Supervisor]
|
||||||
|
|
||||||
with {:ok, pid} <- Supervisor.start_link(children, opts) do
|
with {:ok, pid} <- Supervisor.start_link(children, opts) do
|
||||||
initialize_player_sessions_on_node()
|
initialize_node()
|
||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
0
lib/chessh/client.ex
Normal file
0
lib/chessh/client.ex
Normal file
@ -55,17 +55,22 @@ defmodule Chessh.PlayerSession do
|
|||||||
auth_fn.(player) &&
|
auth_fn.(player) &&
|
||||||
PlayerSession.concurrent_sessions(player) < max_sessions
|
PlayerSession.concurrent_sessions(player) < max_sessions
|
||||||
|
|
||||||
|
if authed do
|
||||||
|
Logger.debug(
|
||||||
|
"Creating session for player #{username} on node #{System.fetch_env!("NODE_ID")} with process #{inspect(self())}"
|
||||||
|
)
|
||||||
|
|
||||||
Repo.insert(%PlayerSession{
|
Repo.insert(%PlayerSession{
|
||||||
login: DateTime.utc_now(),
|
login: DateTime.utc_now(),
|
||||||
node_id: System.fetch_env!("NODE_ID"),
|
node_id: System.fetch_env!("NODE_ID"),
|
||||||
player: player,
|
player: player,
|
||||||
# TODO: This PID may be wrong - need to determine if this PID is shared with disconnectfun
|
|
||||||
process: Utils.pid_to_str(self())
|
process: Utils.pid_to_str(self())
|
||||||
})
|
})
|
||||||
|
|
||||||
player
|
player
|
||||||
|> Player.authentications_changeset(%{authentications: player.authentications + 1})
|
|> Player.authentications_changeset(%{authentications: player.authentications + 1})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
send(self(), {:authed, authed})
|
send(self(), {:authed, authed})
|
||||||
end
|
end
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
defmodule Chessh.SSH.Cli do
|
|
||||||
@behaviour :ssh_server_channel
|
|
||||||
|
|
||||||
def init(_args) do
|
|
||||||
{:ok, %{}}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_msg(_message, state) do
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_ssh_msg(
|
|
||||||
{:ssh_cm, _connection_handler, {:exit_signal, channel_id, _signal, _err, _lang}},
|
|
||||||
state
|
|
||||||
) do
|
|
||||||
{:stop, channel_id, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_ssh_msg(_message, state) do
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
def terminate(_reason, _state) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
16
lib/chessh/ssh/client.ex
Normal file
16
lib/chessh/ssh/client.ex
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
defmodule Chessh.SSH.Client do
|
||||||
|
alias Chessh.SSH.Client
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
# TODO: tui_state_stack is like [:menu, :player_settings, :change_password] or [:menu, {:game, game_id}, {:game_chat, game_id}]
|
||||||
|
|
||||||
|
defstruct [:tui_pid, :width, :height, :player_id, :tui_state_stack]
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init([tui_pid, width, height] = args) do
|
||||||
|
Logger.debug("#{inspect(args)}")
|
||||||
|
{:ok, %Client{tui_pid: tui_pid, width: width, height: height}}
|
||||||
|
end
|
||||||
|
end
|
@ -61,7 +61,7 @@ defmodule Chessh.SSH.Daemon do
|
|||||||
system_dir: key_dir,
|
system_dir: key_dir,
|
||||||
pwdfun: &pwd_authenticate/4,
|
pwdfun: &pwd_authenticate/4,
|
||||||
key_cb: Chessh.SSH.ServerKey,
|
key_cb: Chessh.SSH.ServerKey,
|
||||||
ssh_cli: {Chessh.SSH.Cli, []},
|
ssh_cli: {Chessh.SSH.Tui, []},
|
||||||
# connectfun: &on_connect/3,
|
# connectfun: &on_connect/3,
|
||||||
disconnectfun: &on_disconnect/1,
|
disconnectfun: &on_disconnect/1,
|
||||||
id_string: :random,
|
id_string: :random,
|
||||||
@ -82,23 +82,6 @@ defmodule Chessh.SSH.Daemon do
|
|||||||
|
|
||||||
def handle_info(_, state), do: {:noreply, state}
|
def handle_info(_, state), do: {:noreply, state}
|
||||||
|
|
||||||
# defp on_connect(username, _inet, _method) do
|
|
||||||
# Logger.debug("#{inspect(self())} connected and is authenticated as #{username}")
|
|
||||||
#
|
|
||||||
# case Repo.get_by(Player, username: String.Chars.to_string(username)) do
|
|
||||||
# nil ->
|
|
||||||
# nil
|
|
||||||
#
|
|
||||||
# player ->
|
|
||||||
# Repo.insert(%PlayerSession{
|
|
||||||
# login: DateTime.utc_now(),
|
|
||||||
# node_id: System.fetch_env!("NODE_ID"),
|
|
||||||
# player: player,
|
|
||||||
# process: pid_to_str(self())
|
|
||||||
# })
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
defp on_disconnect(_reason) do
|
defp on_disconnect(_reason) do
|
||||||
Logger.debug("#{inspect(self())} disconnected")
|
Logger.debug("#{inspect(self())} disconnected")
|
||||||
|
|
||||||
|
163
lib/chessh/ssh/tui.ex
Normal file
163
lib/chessh/ssh/tui.ex
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
defmodule Chessh.SSH.Tui do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour :ssh_server_channel
|
||||||
|
|
||||||
|
def init(opts) do
|
||||||
|
Logger.debug("#{inspect(opts)}")
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
channel: nil,
|
||||||
|
cm: nil,
|
||||||
|
pty: %{term: nil, width: nil, height: nil, pixwidth: nil, pixheight: nil, modes: nil},
|
||||||
|
shell: false,
|
||||||
|
client_pid: nil
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec handle_msg(any, any) ::
|
||||||
|
:ok
|
||||||
|
| {:ok, atom | %{:channel => any, :cm => any, optional(any) => any}}
|
||||||
|
| {:stop, any, %{:channel => any, :client_pid => any, optional(any) => any}}
|
||||||
|
def handle_msg({:ssh_channel_up, channel_id, connection_handler}, state) do
|
||||||
|
Logger.debug(
|
||||||
|
"SSH CHANNEL UP #{inspect(connection_handler)} #{inspect(:ssh.connection_info(connection_handler))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, %{state | channel: channel_id, cm: connection_handler}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_msg({:EXIT, client_pid, _reason}, %{client_pid: client_pid} = state) do
|
||||||
|
{:stop, state.channel, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
### commands we expect from the client ###
|
||||||
|
def handle_msg({:send_data, data}, state) do
|
||||||
|
Logger.debug("DATA SENT #{inspect(data)}")
|
||||||
|
:ssh_connection.send(state.cm, state.channel, data)
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
### catch all for what we haven't seen ###
|
||||||
|
def handle_msg(msg, term) do
|
||||||
|
Logger.debug("Unknown msg #{inspect(msg)}, #{inspect(term)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler, {:data, _channel_id, _type, data}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("DATA #{inspect(data)}")
|
||||||
|
send(state.client_pid, {:data, data})
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, connection_handler,
|
||||||
|
{:pty, channel_id, want_reply?, {term, width, height, pixwidth, pixheight, modes} = _pty}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
:ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| pty: %{
|
||||||
|
term: term,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
pixwidth: pixwidth,
|
||||||
|
pixheight: pixheight,
|
||||||
|
modes: modes
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, connection_handler, {:env, channel_id, want_reply?, var, value}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
:ssh_connection.reply_request(connection_handler, want_reply?, :failure, channel_id)
|
||||||
|
Logger.debug("ENV #{var} = #{value}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler,
|
||||||
|
{:window_change, _channel_id, width, height, pixwidth, pixheight}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("WINDOW CHANGE")
|
||||||
|
# SSHnakes.Client.resize(state.client_pid, width, height)
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| pty: %{
|
||||||
|
state.pty
|
||||||
|
| width: width,
|
||||||
|
height: height,
|
||||||
|
pixwidth: pixwidth,
|
||||||
|
pixheight: pixheight
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
:ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
|
||||||
|
|
||||||
|
{:ok, client_pid} =
|
||||||
|
GenServer.start_link(Chessh.SSH.Client, [self(), state.pty.width, state.pty.height])
|
||||||
|
|
||||||
|
{:ok, %{state | client_pid: client_pid, shell: true}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, connection_handler, {:exec, channel_id, want_reply?, cmd}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
:ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
|
||||||
|
Logger.debug("EXEC #{cmd}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler, {:eof, _channel_id}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("EOF")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler, {:signal, _channel_id, signal}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("SIGNAL #{signal}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler, {:exit_signal, channel_id, signal, err, lang}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("EXIT SIGNAL #{signal} #{err} #{lang}")
|
||||||
|
{:stop, channel_id, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_ssh_msg(
|
||||||
|
{:ssh_cm, _connection_handler, {:exit_STATUS, channel_id, status}},
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("EXIT STATUS #{status}")
|
||||||
|
{:stop, channel_id, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminate(_reason, _state) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
2
mix.exs
2
mix.exs
@ -38,7 +38,7 @@ defmodule Chessh.MixProject do
|
|||||||
|
|
||||||
defp aliases do
|
defp aliases do
|
||||||
[
|
[
|
||||||
test: ["ecto.create --quiet", "ecto.migrate", "test"]
|
test: ["ecto.create --quiet", "ecto.migrate", "test --seed 0"]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
3
test/README.md
Normal file
3
test/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
TODO:
|
||||||
|
- [ ] When password changed, remove all sessions
|
||||||
|
- [ ] When session removed, ssh connection closed
|
@ -34,6 +34,20 @@ defmodule Chessh.SSH.AuthTest do
|
|||||||
Process.sleep(1_000)
|
Process.sleep(1_000)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_ssh_connection_to_pid(parent, auth_method) do
|
||||||
|
send(
|
||||||
|
parent,
|
||||||
|
{:attempted,
|
||||||
|
:ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
|
||||||
|
user: String.to_charlist(@valid_user.username),
|
||||||
|
password: String.to_charlist(@valid_user.password),
|
||||||
|
auth_methods: auth_method,
|
||||||
|
silently_accept_hosts: true,
|
||||||
|
user_dir: String.to_charlist(@client_test_keys_dir)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "Password attempts are rate limited" do
|
test "Password attempts are rate limited" do
|
||||||
jail_attempt_threshold =
|
jail_attempt_threshold =
|
||||||
Application.get_env(:chessh, RateLimits)
|
Application.get_env(:chessh, RateLimits)
|
||||||
@ -88,7 +102,7 @@ defmodule Chessh.SSH.AuthTest do
|
|||||||
cleanup()
|
cleanup()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "INTEGRATION - Player cannot have more than specified concurrent sessions" do
|
test "INTEGRATION - Player cannot have more than specified concurrent sessions which are tracked by successful authentications and disconnections" do
|
||||||
max_concurrent_user_sessions =
|
max_concurrent_user_sessions =
|
||||||
Application.get_env(:chessh, RateLimits)
|
Application.get_env(:chessh, RateLimits)
|
||||||
|> Keyword.get(:max_concurrent_user_sessions)
|
|> Keyword.get(:max_concurrent_user_sessions)
|
||||||
@ -99,28 +113,21 @@ defmodule Chessh.SSH.AuthTest do
|
|||||||
test_pid = self()
|
test_pid = self()
|
||||||
|
|
||||||
Enum.reduce(0..(max_concurrent_user_sessions + 1), fn i, _ ->
|
Enum.reduce(0..(max_concurrent_user_sessions + 1), fn i, _ ->
|
||||||
Task.Supervisor.start_child(sup, fn ->
|
Task.Supervisor.start_child(
|
||||||
case :ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
|
sup,
|
||||||
user: String.to_charlist(@valid_user.username),
|
fn ->
|
||||||
password: String.to_charlist(@valid_user.password),
|
send_ssh_connection_to_pid(
|
||||||
auth_methods: if(rem(i, 2) == 0, do: 'publickey', else: 'password'),
|
|
||||||
silently_accept_hosts: true,
|
|
||||||
user_dir: String.to_charlist(@client_test_keys_dir)
|
|
||||||
) do
|
|
||||||
{:ok, conn} ->
|
|
||||||
send(
|
|
||||||
test_pid,
|
test_pid,
|
||||||
{:attempted, {:ok, conn}}
|
if(rem(i, 2) == 0, do: 'publickey', else: 'password')
|
||||||
)
|
)
|
||||||
|
|
||||||
x ->
|
|
||||||
send(test_pid, {:attempted, x})
|
|
||||||
end
|
end
|
||||||
end)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Enum.reduce(0..max_concurrent_user_sessions, fn _, _ ->
|
conns =
|
||||||
assert_receive({:attempted, {:ok, _conn}}, 2000)
|
Enum.map(1..max_concurrent_user_sessions, fn _ ->
|
||||||
|
assert_receive({:attempted, {:ok, conn}}, 2_000)
|
||||||
|
conn
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert_receive(
|
assert_receive(
|
||||||
@ -133,6 +140,10 @@ defmodule Chessh.SSH.AuthTest do
|
|||||||
:timer.sleep(100)
|
:timer.sleep(100)
|
||||||
assert PlayerSession.concurrent_sessions(player) == max_concurrent_user_sessions
|
assert PlayerSession.concurrent_sessions(player) == max_concurrent_user_sessions
|
||||||
|
|
||||||
|
Enum.map(conns, fn conn -> :ssh.close(conn) end)
|
||||||
|
:timer.sleep(100)
|
||||||
|
assert PlayerSession.concurrent_sessions(player) == 0
|
||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user