Now a simple logo draws in the center of the terminal, terminal size is limited, and resizing support

This commit is contained in:
Simponic 2023-01-02 19:10:23 -07:00
parent 2bf058d5db
commit 16281b0e8d
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
8 changed files with 172 additions and 27 deletions

View File

@ -2,4 +2,5 @@
Features: Features:
- [X] SSH Key & Password authentication - [X] SSH Key & Password authentication
- [ ] Rate limiting - [X] Session rate limiting
- [X] Multi-node support

View File

@ -1,5 +1,9 @@
import Config 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, config :chessh,
ecto_repos: [Chessh.Repo], ecto_repos: [Chessh.Repo],
key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
@ -9,10 +13,8 @@ config :chessh,
config :chessh, RateLimits, config :chessh, RateLimits,
jail_timeout_ms: 5 * 60 * 1000, jail_timeout_ms: 5 * 60 * 1000,
jail_attempt_threshold: 15, jail_attempt_threshold: 15,
max_concurrent_user_sessions: 5 max_concurrent_user_sessions: 5,
player_session_message_burst_ms: 3_000,
# This will be redis when scaled across multiple nodes player_session_message_burst_rate: 15
config :hammer,
backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

View File

@ -20,14 +20,6 @@ defmodule Chessh.Key do
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported") |> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
end 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 def encode_key(key) do
if is_tuple(key) do if is_tuple(key) do
case key do case key do
@ -39,8 +31,15 @@ defmodule Chessh.Key do
else else
key key
end end
# Remove comment at end of key
|> String.replace(~r/ [^ ]+\@[^ ]+$/, "") |> String.replace(~r/ [^ ]+\@[^ ]+$/, "")
|> String.trim() |> String.trim()
end 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 end

View File

@ -1,27 +1,125 @@
defmodule Chessh.SSH.Client do defmodule Chessh.SSH.Client do
alias IO.ANSI alias IO.ANSI
require Logger require Logger
use GenServer use GenServer
@default_message [ @clear_codes [
ANSI.clear(), ANSI.clear(),
ANSI.reset(), ANSI.reset(),
ANSI.home(), ANSI.home()
["Hello, world"] ]
@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 defmodule State do
defstruct tui_pid: nil, defstruct tui_pid: nil,
width: nil, width: 0,
height: nil, height: 0,
player_session: nil, player_session: nil,
state_statck: [] buffer: [],
state_stack: [{&Chessh.SSH.Client.Menu.render/2, []}]
end end
@impl true @impl true
def init([%State{tui_pid: tui_pid} = state]) do 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} {:ok, state}
end 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 end

View File

@ -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

View File

@ -64,8 +64,12 @@ defmodule Chessh.SSH.Tui do
end end
end end
def handle_msg({:EXIT, client_pid, _reason}, %{client_pid: client_pid} = state) do def handle_msg(
{:stop, state.channel, state} {:EXIT, client_pid, _reason},
%{client_pid: client_pid, channel_id: channel_id} = state
) do
send(client_pid, :quit)
{:stop, channel_id, state}
end end
def handle_msg( def handle_msg(
@ -94,7 +98,7 @@ defmodule Chessh.SSH.Tui do
state state
) do ) do
Logger.debug("DATA #{inspect(data)}") Logger.debug("DATA #{inspect(data)}")
# send(state.client_pid, {:data, data}) send(state.client_pid, {:data, data})
{:ok, state} {:ok, state}
end end
@ -126,10 +130,11 @@ defmodule Chessh.SSH.Tui do
def handle_ssh_msg( def handle_ssh_msg(
{:ssh_cm, _connection_handler, {:ssh_cm, _connection_handler,
{:window_change, _channel_id, width, height, _pixwidth, _pixheight}}, {:window_change, _channel_id, width, height, _pixwidth, _pixheight}},
state %{client_pid: client_pid} = state
) do ) do
Logger.debug("WINDOW CHANGE") Logger.debug("WINDOW CHANGE")
# Chessh.SSH.Client.resize(state.client_pid, width, height) send(client_pid, {:resize, {width, height}})
{:ok, {:ok,
%{ %{
state state

View File

@ -6,4 +6,9 @@ defmodule Chessh.Utils do
|> List.delete_at(-1) |> List.delete_at(-1)
|> to_string() |> to_string()
end 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 end