summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimponic <loganhunt@simponic.xyz>2022-12-31 02:29:38 -0700
committerSimponic <loganhunt@simponic.xyz>2022-12-31 02:31:58 -0700
commit58d0b1a89c461467c9ea6229f9a6b3d5ed573da5 (patch)
treef0e91ce7bccb93e755357395f1ed7a7a18d6257b
parent3308036c0859dcca2d80d80396a19a3cca269e78 (diff)
downloadchessh-58d0b1a89c461467c9ea6229f9a6b3d5ed573da5.tar.gz
chessh-58d0b1a89c461467c9ea6229f9a6b3d5ed573da5.zip
A simple stalling TUI! Also, ensure sessions are counted correctly. Next up, some way of pub-sub across multiple nodes
-rw-r--r--lib/chessh/application.ex7
-rw-r--r--lib/chessh/client.ex0
-rw-r--r--lib/chessh/schema/player_session.ex25
-rw-r--r--lib/chessh/ssh/cli.ex26
-rw-r--r--lib/chessh/ssh/client.ex16
-rw-r--r--lib/chessh/ssh/daemon.ex19
-rw-r--r--lib/chessh/ssh/tui.ex163
-rw-r--r--mix.exs2
-rw-r--r--test/README.md3
-rw-r--r--test/ssh/ssh_auth_test.exs53
10 files changed, 236 insertions, 78 deletions
diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex
index 4692489..4b03169 100644
--- a/lib/chessh/application.ex
+++ b/lib/chessh/application.ex
@@ -2,10 +2,13 @@ defmodule Chessh.Application do
alias Chessh.{PlayerSession, Node}
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
# this is restarting after every potential crash. Otherwise the player sessions
# 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.boot(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]
with {:ok, pid} <- Supervisor.start_link(children, opts) do
- initialize_player_sessions_on_node()
+ initialize_node()
{:ok, pid}
end
end
diff --git a/lib/chessh/client.ex b/lib/chessh/client.ex
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/chessh/client.ex
diff --git a/lib/chessh/schema/player_session.ex b/lib/chessh/schema/player_session.ex
index ce3fc1f..57803cb 100644
--- a/lib/chessh/schema/player_session.ex
+++ b/lib/chessh/schema/player_session.ex
@@ -55,17 +55,22 @@ defmodule Chessh.PlayerSession do
auth_fn.(player) &&
PlayerSession.concurrent_sessions(player) < max_sessions
- Repo.insert(%PlayerSession{
- login: DateTime.utc_now(),
- node_id: System.fetch_env!("NODE_ID"),
- player: player,
- # TODO: This PID may be wrong - need to determine if this PID is shared with disconnectfun
- process: Utils.pid_to_str(self())
- })
+ if authed do
+ Logger.debug(
+ "Creating session for player #{username} on node #{System.fetch_env!("NODE_ID")} with process #{inspect(self())}"
+ )
- player
- |> Player.authentications_changeset(%{authentications: player.authentications + 1})
- |> Repo.update()
+ Repo.insert(%PlayerSession{
+ login: DateTime.utc_now(),
+ node_id: System.fetch_env!("NODE_ID"),
+ player: player,
+ process: Utils.pid_to_str(self())
+ })
+
+ player
+ |> Player.authentications_changeset(%{authentications: player.authentications + 1})
+ |> Repo.update()
+ end
send(self(), {:authed, authed})
end
diff --git a/lib/chessh/ssh/cli.ex b/lib/chessh/ssh/cli.ex
deleted file mode 100644
index 71d789b..0000000
--- a/lib/chessh/ssh/cli.ex
+++ /dev/null
@@ -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
diff --git a/lib/chessh/ssh/client.ex b/lib/chessh/ssh/client.ex
new file mode 100644
index 0000000..35893f1
--- /dev/null
+++ b/lib/chessh/ssh/client.ex
@@ -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
diff --git a/lib/chessh/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex
index b341833..24ad259 100644
--- a/lib/chessh/ssh/daemon.ex
+++ b/lib/chessh/ssh/daemon.ex
@@ -61,7 +61,7 @@ defmodule Chessh.SSH.Daemon do
system_dir: key_dir,
pwdfun: &pwd_authenticate/4,
key_cb: Chessh.SSH.ServerKey,
- ssh_cli: {Chessh.SSH.Cli, []},
+ ssh_cli: {Chessh.SSH.Tui, []},
# connectfun: &on_connect/3,
disconnectfun: &on_disconnect/1,
id_string: :random,
@@ -82,23 +82,6 @@ defmodule Chessh.SSH.Daemon do
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
Logger.debug("#{inspect(self())} disconnected")
diff --git a/lib/chessh/ssh/tui.ex b/lib/chessh/ssh/tui.ex
new file mode 100644
index 0000000..78360a9
--- /dev/null
+++ b/lib/chessh/ssh/tui.ex
@@ -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
diff --git a/mix.exs b/mix.exs
index 441fb98..e77428a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -38,7 +38,7 @@ defmodule Chessh.MixProject do
defp aliases do
[
- test: ["ecto.create --quiet", "ecto.migrate", "test"]
+ test: ["ecto.create --quiet", "ecto.migrate", "test --seed 0"]
]
end
end
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..13d4389
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,3 @@
+TODO:
+- [ ] When password changed, remove all sessions
+- [ ] When session removed, ssh connection closed
diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs
index 92c4b22..b6711c7 100644
--- a/test/ssh/ssh_auth_test.exs
+++ b/test/ssh/ssh_auth_test.exs
@@ -34,6 +34,20 @@ defmodule Chessh.SSH.AuthTest do
Process.sleep(1_000)
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
jail_attempt_threshold =
Application.get_env(:chessh, RateLimits)
@@ -88,7 +102,7 @@ defmodule Chessh.SSH.AuthTest do
cleanup()
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 =
Application.get_env(:chessh, RateLimits)
|> Keyword.get(:max_concurrent_user_sessions)
@@ -99,29 +113,22 @@ defmodule Chessh.SSH.AuthTest do
test_pid = self()
Enum.reduce(0..(max_concurrent_user_sessions + 1), fn i, _ ->
- Task.Supervisor.start_child(sup, fn ->
- case :ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
- user: String.to_charlist(@valid_user.username),
- password: String.to_charlist(@valid_user.password),
- 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,
- {:attempted, {:ok, conn}}
- )
-
- x ->
- send(test_pid, {:attempted, x})
+ Task.Supervisor.start_child(
+ sup,
+ fn ->
+ send_ssh_connection_to_pid(
+ test_pid,
+ if(rem(i, 2) == 0, do: 'publickey', else: 'password')
+ )
end
- end)
+ )
end)
- Enum.reduce(0..max_concurrent_user_sessions, fn _, _ ->
- assert_receive({:attempted, {:ok, _conn}}, 2000)
- end)
+ conns =
+ Enum.map(1..max_concurrent_user_sessions, fn _ ->
+ assert_receive({:attempted, {:ok, conn}}, 2_000)
+ conn
+ end)
assert_receive(
{:attempted, {:error, 'Unable to connect using the available authentication methods'}},
@@ -133,6 +140,10 @@ defmodule Chessh.SSH.AuthTest do
:timer.sleep(100)
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()
end
end