summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSimponic <loganhunt@simponic.xyz>2022-12-30 05:46:35 -0700
committerSimponic <loganhunt@simponic.xyz>2022-12-30 05:46:35 -0700
commit42425b02260d279cd9c12fb3e625282979b9e308 (patch)
tree0412bf9f39d44266cff94082c499e44e5f6e60f5 /lib
parent60eea1b4ed65bc7cfce1e383dac6de9d004540eb (diff)
downloadchessh-42425b02260d279cd9c12fb3e625282979b9e308.tar.gz
chessh-42425b02260d279cd9c12fb3e625282979b9e308.zip
Add scalable session thresholds
Diffstat (limited to 'lib')
-rw-r--r--lib/chessh/auth/keys.ex11
-rw-r--r--lib/chessh/auth/password.ex7
-rw-r--r--lib/chessh/schema/node.ex4
-rw-r--r--lib/chessh/schema/player.ex7
-rw-r--r--lib/chessh/schema/player_session.ex52
-rw-r--r--lib/chessh/ssh/cli.ex26
-rw-r--r--lib/chessh/ssh/daemon.ex75
-rw-r--r--lib/chessh/ssh/server_key.ex7
-rw-r--r--lib/chessh/utils.ex9
9 files changed, 170 insertions, 28 deletions
diff --git a/lib/chessh/auth/keys.ex b/lib/chessh/auth/keys.ex
index a29d169..f0e1c78 100644
--- a/lib/chessh/auth/keys.ex
+++ b/lib/chessh/auth/keys.ex
@@ -1,7 +1,16 @@
defmodule Chessh.Auth.KeyAuthenticator do
- alias Chessh.{Key, Repo}
+ alias Chessh.{Key, Repo, Player}
import Ecto.Query
+ def authenticate(player = %Player{}, public_key) do
+ !!Repo.one(
+ from(k in Key,
+ where: k.key == ^Key.encode_key(public_key),
+ where: k.player_id == ^player.id
+ )
+ )
+ end
+
def authenticate(username, public_key) do
!!Repo.one(
from(k in Key,
diff --git a/lib/chessh/auth/password.ex b/lib/chessh/auth/password.ex
index ea2c8fc..a01291d 100644
--- a/lib/chessh/auth/password.ex
+++ b/lib/chessh/auth/password.ex
@@ -1,9 +1,14 @@
defmodule Chessh.Auth.PasswordAuthenticator do
alias Chessh.{Player, Repo}
+ def authenticate(player = %Player{}, password) do
+ Player.valid_password?(player, password)
+ end
+
def authenticate(username, password) do
case Repo.get_by(Player, username: username) do
- x -> Player.valid_password?(x, password)
+ player -> authenticate(player, password)
+ nil -> false
end
end
end
diff --git a/lib/chessh/schema/node.ex b/lib/chessh/schema/node.ex
index 867a6e2..8834aef 100644
--- a/lib/chessh/schema/node.ex
+++ b/lib/chessh/schema/node.ex
@@ -5,7 +5,7 @@ defmodule Chessh.Node do
@primary_key {:id, :string, []}
schema "nodes" do
- field(:last_start, :utc_datetime)
+ field(:last_start, :utc_datetime_usec)
end
def changeset(node, attrs) do
@@ -18,7 +18,7 @@ defmodule Chessh.Node do
nil -> %Chessh.Node{id: node_id}
node -> node
end
- |> Chessh.Node.changeset(%{last_start: DateTime.utc_now()})
+ |> changeset(%{last_start: DateTime.utc_now()})
|> Repo.insert_or_update()
end
end
diff --git a/lib/chessh/schema/player.ex b/lib/chessh/schema/player.ex
index 4b6a324..8eaffee 100644
--- a/lib/chessh/schema/player.ex
+++ b/lib/chessh/schema/player.ex
@@ -9,11 +9,18 @@ defmodule Chessh.Player do
field(:password, :string, virtual: true)
field(:hashed_password, :string)
+ field(:authentications, :integer, default: 0)
+
has_many(:keys, Chessh.Key)
timestamps()
end
+ def authentications_changeset(player, attrs) do
+ player
+ |> cast(attrs, [:authentications])
+ end
+
def registration_changeset(player, attrs, opts \\ []) do
player
|> cast(attrs, [:username, :password])
diff --git a/lib/chessh/schema/player_session.ex b/lib/chessh/schema/player_session.ex
index 84f15ee..ce3fc1f 100644
--- a/lib/chessh/schema/player_session.ex
+++ b/lib/chessh/schema/player_session.ex
@@ -1,10 +1,12 @@
defmodule Chessh.PlayerSession do
- alias Chessh.Repo
+ alias Chessh.{Repo, Player, PlayerSession, Utils}
use Ecto.Schema
import Ecto.{Query, Changeset}
+ require Logger
schema "player_sessions" do
- field(:login, :utc_datetime)
+ field(:process, :string)
+ field(:login, :utc_datetime_usec)
belongs_to(:node, Chessh.Node, type: :string)
belongs_to(:player, Chessh.Player)
@@ -17,7 +19,7 @@ defmodule Chessh.PlayerSession do
def concurrent_sessions(player) do
Repo.aggregate(
- from(p in Chessh.PlayerSession,
+ from(p in PlayerSession,
where: p.player_id == ^player.id
),
:count
@@ -31,4 +33,48 @@ defmodule Chessh.PlayerSession do
)
)
end
+
+ def player_within_concurrent_sessions_and_satisfies(username, auth_fn) do
+ max_sessions =
+ Application.get_env(:chessh, RateLimits)
+ |> Keyword.get(:max_concurrent_user_sessions)
+
+ Repo.transaction(fn ->
+ case Repo.one(
+ from(p in Player,
+ where: p.username == ^String.Chars.to_string(username),
+ lock: "FOR UPDATE"
+ )
+ ) do
+ nil ->
+ Logger.error("Player with username #{username} does not exist")
+ send(self(), {:authed, false})
+
+ player ->
+ authed =
+ 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())
+ })
+
+ player
+ |> Player.authentications_changeset(%{authentications: player.authentications + 1})
+ |> Repo.update()
+
+ send(self(), {:authed, authed})
+ end
+ end)
+
+ receive do
+ {:authed, authed} -> authed
+ after
+ 3_000 -> false
+ end
+ end
end
diff --git a/lib/chessh/ssh/cli.ex b/lib/chessh/ssh/cli.ex
new file mode 100644
index 0000000..7ab0433
--- /dev/null
+++ b/lib/chessh/ssh/cli.ex
@@ -0,0 +1,26 @@
+defmodule Chessh.SSH.Cli do
+ @behaviour :ssh_server_channel
+
+ def init() do
+ {:ok, %{}}
+ end
+
+ def handle_msg(message, state) do
+ {:ok, state}
+ end
+
+ def handle_ssh_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 terminate(reason, state) do
+ :ok
+ end
+end
diff --git a/lib/chessh/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex
index 9f17f75..9ffc538 100644
--- a/lib/chessh/ssh/daemon.ex
+++ b/lib/chessh/ssh/daemon.ex
@@ -1,6 +1,10 @@
defmodule Chessh.SSH.Daemon do
+ alias Chessh.{Repo, PlayerSession, Player, Utils}
alias Chessh.Auth.PasswordAuthenticator
use GenServer
+ import Ecto.Query
+
+ require Logger
def start_link(_) do
GenServer.start_link(__MODULE__, %{
@@ -13,33 +17,33 @@ defmodule Chessh.SSH.Daemon do
{:ok, state}
end
- def pwd_authenticate(username, password) do
- # TODO - check concurrent sessions
- PasswordAuthenticator.authenticate(
- String.Chars.to_string(username),
- String.Chars.to_string(password)
- )
- end
-
- def pwd_authenticate(username, password, inet) do
+ def pwd_authenticate(username, password, {ip, _port}) do
[jail_timeout_ms, jail_attempt_threshold] =
Application.get_env(:chessh, RateLimits)
|> Keyword.take([:jail_timeout_ms, :jail_attempt_threshold])
|> Keyword.values()
- {ip, _port} = inet
rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}"
- if pwd_authenticate(username, password) do
- true
- else
- case Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_attempt_threshold, 1) do
- {:allow, _count} ->
- false
+ case PasswordAuthenticator.authenticate(
+ String.Chars.to_string(username),
+ String.Chars.to_string(password)
+ ) do
+ false ->
+ case Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_attempt_threshold, 1) do
+ {:allow, _count} ->
+ false
+
+ {:deny, _limit} ->
+ :disconnect
+ end
- {:deny, _limit} ->
- :disconnect
- end
+ x ->
+ if PlayerSession.player_within_concurrent_sessions_and_satisfies(username, fn _player ->
+ x
+ end),
+ do: true,
+ else: :disconnect
end
end
@@ -53,10 +57,13 @@ defmodule Chessh.SSH.Daemon do
case :ssh.daemon(
port,
+ # shell: fn _username, _peer -> Process.sleep(5000) end,
system_dir: key_dir,
pwdfun: &pwd_authenticate/4,
key_cb: Chessh.SSH.ServerKey,
- # disconnectfun:
+ ssh_cli: {Chessh.SSH.Cli, []},
+ # connectfun: &on_connect/3,
+ disconnectfun: &on_disconnect/1,
id_string: :random,
subsystems: [],
parallel_login: true,
@@ -74,4 +81,32 @@ defmodule Chessh.SSH.Daemon do
end
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")
+
+ Repo.delete_all(
+ from(p in PlayerSession,
+ where: p.node_id == ^System.fetch_env!("NODE_ID"),
+ where: p.process == ^Utils.pid_to_str(self())
+ )
+ )
+ end
end
diff --git a/lib/chessh/ssh/server_key.ex b/lib/chessh/ssh/server_key.ex
index 72a4fbb..5252624 100644
--- a/lib/chessh/ssh/server_key.ex
+++ b/lib/chessh/ssh/server_key.ex
@@ -1,9 +1,14 @@
defmodule Chessh.SSH.ServerKey do
+ alias Chessh.PlayerSession
alias Chessh.Auth.KeyAuthenticator
+
@behaviour :ssh_server_key_api
def is_auth_key(key, username, _daemon_options) do
- KeyAuthenticator.authenticate(username, key)
+ PlayerSession.player_within_concurrent_sessions_and_satisfies(
+ username,
+ &KeyAuthenticator.authenticate(&1, key)
+ )
end
def host_key(algorithm, daemon_options) do
diff --git a/lib/chessh/utils.ex b/lib/chessh/utils.ex
new file mode 100644
index 0000000..1a7f8cf
--- /dev/null
+++ b/lib/chessh/utils.ex
@@ -0,0 +1,9 @@
+defmodule Chessh.Utils do
+ def pid_to_str(pid) do
+ pid
+ |> :erlang.pid_to_list()
+ |> List.delete_at(0)
+ |> List.delete_at(-1)
+ |> to_string()
+ end
+end