From 16281b0e8deb6b3bf86ac0b9381f3fdf89b22b58 Mon Sep 17 00:00:00 2001 From: Simponic Date: Mon, 2 Jan 2023 19:10:23 -0700 Subject: [PATCH] Now a simple logo draws in the center of the terminal, terminal size is limited, and resizing support --- README.md | 3 +- config/config.exs | 12 ++-- lib/chessh/client.ex | 0 lib/chessh/schema/key.ex | 17 +++-- lib/chessh/ssh/client.ex | 112 +++++++++++++++++++++++++++++-- lib/chessh/ssh/renderers/menu.ex | 35 ++++++++++ lib/chessh/ssh/tui.ex | 15 +++-- lib/chessh/utils.ex | 5 ++ 8 files changed, 172 insertions(+), 27 deletions(-) delete mode 100644 lib/chessh/client.ex create mode 100644 lib/chessh/ssh/renderers/menu.ex diff --git a/README.md b/README.md index ae1b5a0..2949303 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,5 @@ Features: - [X] SSH Key & Password authentication -- [ ] Rate limiting \ No newline at end of file +- [X] Session rate limiting +- [X] Multi-node support diff --git a/config/config.exs b/config/config.exs index 42339fd..a12949b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,9 @@ import Config +# This will be redis when scaled across multiple nodes +config :hammer, + backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} + config :chessh, ecto_repos: [Chessh.Repo], key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), @@ -9,10 +13,8 @@ config :chessh, config :chessh, RateLimits, jail_timeout_ms: 5 * 60 * 1000, jail_attempt_threshold: 15, - max_concurrent_user_sessions: 5 - -# This will be redis when scaled across multiple nodes -config :hammer, - backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} + max_concurrent_user_sessions: 5, + player_session_message_burst_ms: 3_000, + player_session_message_burst_rate: 15 import_config "#{config_env()}.exs" diff --git a/lib/chessh/client.ex b/lib/chessh/client.ex deleted file mode 100644 index e69de29..0000000 diff --git a/lib/chessh/schema/key.ex b/lib/chessh/schema/key.ex index adf018d..df790e2 100644 --- a/lib/chessh/schema/key.ex +++ b/lib/chessh/schema/key.ex @@ -20,14 +20,6 @@ defmodule Chessh.Key do |> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported") end - defp update_encode_key(attrs, field) do - if Map.has_key?(attrs, field) do - Map.update!(attrs, field, &encode_key/1) - else - attrs - end - end - def encode_key(key) do if is_tuple(key) do case key do @@ -39,8 +31,15 @@ defmodule Chessh.Key do else key end - # Remove comment at end of key |> String.replace(~r/ [^ ]+\@[^ ]+$/, "") |> String.trim() end + + defp update_encode_key(attrs, field) do + if Map.has_key?(attrs, field) do + Map.update!(attrs, field, &encode_key/1) + else + attrs + end + end end diff --git a/lib/chessh/ssh/client.ex b/lib/chessh/ssh/client.ex index eba188f..4eceb38 100644 --- a/lib/chessh/ssh/client.ex +++ b/lib/chessh/ssh/client.ex @@ -1,27 +1,125 @@ defmodule Chessh.SSH.Client do alias IO.ANSI + require Logger use GenServer - @default_message [ + @clear_codes [ ANSI.clear(), ANSI.reset(), - ANSI.home(), - ["Hello, world"] + ANSI.home() + ] + + @min_terminal_width 64 + @min_terminal_height 31 + @max_terminal_width 255 + @max_terminal_height 127 + + @terminal_bad_dim_msg [ + @clear_codes | "The dimensions of your terminal are not within in the valid range" ] defmodule State do defstruct tui_pid: nil, - width: nil, - height: nil, + width: 0, + height: 0, player_session: nil, - state_statck: [] + buffer: [], + state_stack: [{&Chessh.SSH.Client.Menu.render/2, []}] end @impl true def init([%State{tui_pid: tui_pid} = state]) do - send(tui_pid, {:send_data, @default_message}) + send(tui_pid, {:send_data, render(state)}) {:ok, state} end + + @impl true + def handle_info(:quit, %State{} = state) do + {:stop, :normal, state} + end + + @impl true + def handle_info(msg, state) do + [burst_ms, burst_rate] = + Application.get_env(:chessh, RateLimits) + |> Keyword.take([:player_session_message_burst_ms, :player_session_message_burst_rate]) + |> Keyword.values() + + case Hammer.check_rate_inc( + "player-session-#{state.player_session.id}-burst-message-rate", + burst_ms, + burst_rate, + 1 + ) do + {:allow, _count} -> + handle(msg, state) + + {:deny, _limit} -> + {:noreply, state} + end + end + + def handle({:data, data}, %State{tui_pid: tui_pid} = state) do + new_state = + keymap(data) + |> keypress(state) + + send(tui_pid, {:send_data, render(new_state)}) + {:noreply, new_state} + end + + def handle({:resize, {width, height}}, %State{tui_pid: tui_pid} = state) do + new_state = %State{state | width: width, height: height} + + if height <= @max_terminal_height || width <= @max_terminal_width do + send(tui_pid, {:send_data, render(new_state)}) + end + + {:noreply, new_state} + end + + def keypress(:up, state), do: state + def keypress(:right, state), do: state + def keypress(:down, state), do: state + def keypress(:left, state), do: state + + def keypress(:quit, state) do + send(self(), :quit) + state + end + + def keypress(_, state), do: state + + def keymap(key) do + case key do + # Exit keys - C-c and C-d + <<3>> -> :quit + <<4>> -> :quit + # Arrow keys + "\e[A" -> :up + "\e[B" -> :down + "\e[D" -> :left + "\e[C" -> :right + x -> x + end + end + + @spec terminal_size_allowed(any, any) :: boolean + def terminal_size_allowed(width, height) do + Enum.member?(@min_terminal_width..@max_terminal_width, width) && + Enum.member?(@min_terminal_height..@max_terminal_height, height) + end + + defp render(%{width: width, height: height, state_stack: [{render_fn, args} | _tail]} = state) do + if terminal_size_allowed(width, height) do + [ + @clear_codes ++ + render_fn.(state, args) + ] + else + @terminal_bad_dim_msg + end + end end diff --git a/lib/chessh/ssh/renderers/menu.ex b/lib/chessh/ssh/renderers/menu.ex new file mode 100644 index 0000000..c3c3646 --- /dev/null +++ b/lib/chessh/ssh/renderers/menu.ex @@ -0,0 +1,35 @@ +defmodule Chessh.SSH.Client.Menu do + alias Chessh.SSH.Client.State + alias Chessh.Utils + + alias IO.ANSI + + @logo " Simponic's + + dP MP\"\"\"\"\"\"`MM MP\"\"\"\"\"\"`MM M\"\"MMMMM\"\"MM + 88 M mmmmm..M M mmmmm..M M MMMMM MM +.d8888b. 88d888b. .d8888b. M. `YM M. `YM M `M +88' `\"\" 88' `88 88ooood8 MMMMMMM. M MMMMMMM. M M MMMMM MM +88. ... 88 88 88. ... M. .MMM' M M. .MMM' M M MMMMM MM +`88888P' dP dP `88888P' Mb. .dM Mb. .dM M MMMMM MM + MMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM" + + def render( + %State{width: width, height: height, state_stack: [_current_state | _tail]} = _state, + _args + ) do + {logo_width, logo_height} = Utils.text_dim(@logo) + + split = String.split(@logo, "\n") + + Enum.flat_map( + Enum.zip(0..(length(split) - 1), split), + fn {i, x} -> + [ + ANSI.cursor(div(height - logo_height, 2) + i, div(width - logo_width, 2)), + "#{x}\n" + ] + end + ) + end +end diff --git a/lib/chessh/ssh/tui.ex b/lib/chessh/ssh/tui.ex index c0fc910..d76986f 100644 --- a/lib/chessh/ssh/tui.ex +++ b/lib/chessh/ssh/tui.ex @@ -64,8 +64,12 @@ defmodule Chessh.SSH.Tui do end end - def handle_msg({:EXIT, client_pid, _reason}, %{client_pid: client_pid} = state) do - {:stop, state.channel, state} + def handle_msg( + {:EXIT, client_pid, _reason}, + %{client_pid: client_pid, channel_id: channel_id} = state + ) do + send(client_pid, :quit) + {:stop, channel_id, state} end def handle_msg( @@ -94,7 +98,7 @@ defmodule Chessh.SSH.Tui do state ) do Logger.debug("DATA #{inspect(data)}") - # send(state.client_pid, {:data, data}) + send(state.client_pid, {:data, data}) {:ok, state} end @@ -126,10 +130,11 @@ defmodule Chessh.SSH.Tui do def handle_ssh_msg( {:ssh_cm, _connection_handler, {:window_change, _channel_id, width, height, _pixwidth, _pixheight}}, - state + %{client_pid: client_pid} = state ) do Logger.debug("WINDOW CHANGE") - # Chessh.SSH.Client.resize(state.client_pid, width, height) + send(client_pid, {:resize, {width, height}}) + {:ok, %{ state diff --git a/lib/chessh/utils.ex b/lib/chessh/utils.ex index 1a7f8cf..3e83d5e 100644 --- a/lib/chessh/utils.ex +++ b/lib/chessh/utils.ex @@ -6,4 +6,9 @@ defmodule Chessh.Utils do |> List.delete_at(-1) |> to_string() end + + def text_dim(text) do + split = String.split(text, "\n") + {Enum.reduce(split, 0, fn x, acc -> max(acc, String.length(x)) end), length(split)} + end end