diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chessh/application.ex | 18 | ||||
-rw-r--r-- | lib/chessh/auth/password.ex | 7 | ||||
-rw-r--r-- | lib/chessh/schema/key.ex | 3 | ||||
-rw-r--r-- | lib/chessh/schema/node.ex | 24 | ||||
-rw-r--r-- | lib/chessh/schema/player_session.ex | 34 | ||||
-rw-r--r-- | lib/chessh/ssh/daemon.ex | 77 | ||||
-rw-r--r-- | lib/chessh/ssh/server.ex | 0 | ||||
-rw-r--r-- | lib/chessh/ssh/server_key.ex | 12 |
8 files changed, 167 insertions, 8 deletions
diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex index c760532..4692489 100644 --- a/lib/chessh/application.ex +++ b/lib/chessh/application.ex @@ -1,9 +1,23 @@ defmodule Chessh.Application do + alias Chessh.{PlayerSession, Node} use Application + def initialize_player_sessions_on_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. + node_id = System.fetch_env!("NODE_ID") + Node.boot(node_id) + PlayerSession.delete_all_on_node(node_id) + end + def start(_, _) do - children = [Chessh.Repo] + children = [Chessh.Repo, Chessh.SSH.Daemon] opts = [strategy: :one_for_one, name: Chessh.Supervisor] - Supervisor.start_link(children, opts) + + with {:ok, pid} <- Supervisor.start_link(children, opts) do + initialize_player_sessions_on_node() + {:ok, pid} + end end end diff --git a/lib/chessh/auth/password.ex b/lib/chessh/auth/password.ex index 8a6c683..ea2c8fc 100644 --- a/lib/chessh/auth/password.ex +++ b/lib/chessh/auth/password.ex @@ -1,10 +1,9 @@ defmodule Chessh.Auth.PasswordAuthenticator do - alias Chessh.Player - alias Chessh.Repo + alias Chessh.{Player, Repo} def authenticate(username, password) do - case Repo.get_by(Player, username: String.Chars.to_string(username)) do - x -> Player.valid_password?(x, String.Chars.to_string(password)) + case Repo.get_by(Player, username: username) do + x -> Player.valid_password?(x, password) end end end diff --git a/lib/chessh/schema/key.ex b/lib/chessh/schema/key.ex index 765c83b..adf018d 100644 --- a/lib/chessh/schema/key.ex +++ b/lib/chessh/schema/key.ex @@ -16,7 +16,7 @@ defmodule Chessh.Key do |> cast(update_encode_key(attrs, :key), [:key]) |> cast(attrs, [:name]) |> validate_required([:key, :name]) - |> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid ssh key") + |> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid public ssh key") |> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported") end @@ -41,7 +41,6 @@ defmodule Chessh.Key do end # Remove comment at end of key |> String.replace(~r/ [^ ]+\@[^ ]+$/, "") - # Remove potential spaces / newline |> String.trim() end end diff --git a/lib/chessh/schema/node.ex b/lib/chessh/schema/node.ex new file mode 100644 index 0000000..867a6e2 --- /dev/null +++ b/lib/chessh/schema/node.ex @@ -0,0 +1,24 @@ +defmodule Chessh.Node do + alias Chessh.Repo + import Ecto.Changeset + use Ecto.Schema + + @primary_key {:id, :string, []} + schema "nodes" do + field(:last_start, :utc_datetime) + end + + def changeset(node, attrs) do + node + |> cast(attrs, [:last_start]) + end + + def boot(node_id) do + case Repo.get(Chessh.Node, node_id) do + nil -> %Chessh.Node{id: node_id} + node -> node + end + |> Chessh.Node.changeset(%{last_start: DateTime.utc_now()}) + |> Repo.insert_or_update() + end +end diff --git a/lib/chessh/schema/player_session.ex b/lib/chessh/schema/player_session.ex new file mode 100644 index 0000000..84f15ee --- /dev/null +++ b/lib/chessh/schema/player_session.ex @@ -0,0 +1,34 @@ +defmodule Chessh.PlayerSession do + alias Chessh.Repo + use Ecto.Schema + import Ecto.{Query, Changeset} + + schema "player_sessions" do + field(:login, :utc_datetime) + + belongs_to(:node, Chessh.Node, type: :string) + belongs_to(:player, Chessh.Player) + end + + def changeset(player_session, attrs) do + player_session + |> cast(attrs, [:login]) + end + + def concurrent_sessions(player) do + Repo.aggregate( + from(p in Chessh.PlayerSession, + where: p.player_id == ^player.id + ), + :count + ) + end + + def delete_all_on_node(node_id) do + Repo.delete_all( + from(p in Chessh.PlayerSession, + where: p.node_id == ^node_id + ) + ) + end +end diff --git a/lib/chessh/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex new file mode 100644 index 0000000..9f17f75 --- /dev/null +++ b/lib/chessh/ssh/daemon.ex @@ -0,0 +1,77 @@ +defmodule Chessh.SSH.Daemon do + alias Chessh.Auth.PasswordAuthenticator + use GenServer + + def start_link(_) do + GenServer.start_link(__MODULE__, %{ + pid: nil + }) + end + + def init(state) do + GenServer.cast(self(), :start) + {: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 + [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 + + {:deny, _limit} -> + :disconnect + end + end + end + + def pwd_authenticate(username, password, inet, _address), + do: pwd_authenticate(username, password, inet) + + def handle_cast(:start, state) do + port = Application.fetch_env!(:chessh, :port) + key_dir = String.to_charlist(Application.fetch_env!(:chessh, :key_dir)) + max_sessions = Application.fetch_env!(:chessh, :max_sessions) + + case :ssh.daemon( + port, + system_dir: key_dir, + pwdfun: &pwd_authenticate/4, + key_cb: Chessh.SSH.ServerKey, + # disconnectfun: + id_string: :random, + subsystems: [], + parallel_login: true, + max_sessions: max_sessions + ) do + {:ok, pid} -> + Process.link(pid) + {:noreply, %{state | pid: pid}, :hibernate} + + {:error, err} -> + raise inspect(err) + end + + {:noreply, state} + end + + def handle_info(_, state), do: {:noreply, state} +end diff --git a/lib/chessh/ssh/server.ex b/lib/chessh/ssh/server.ex deleted file mode 100644 index e69de29..0000000 --- a/lib/chessh/ssh/server.ex +++ /dev/null diff --git a/lib/chessh/ssh/server_key.ex b/lib/chessh/ssh/server_key.ex new file mode 100644 index 0000000..72a4fbb --- /dev/null +++ b/lib/chessh/ssh/server_key.ex @@ -0,0 +1,12 @@ +defmodule Chessh.SSH.ServerKey do + alias Chessh.Auth.KeyAuthenticator + @behaviour :ssh_server_key_api + + def is_auth_key(key, username, _daemon_options) do + KeyAuthenticator.authenticate(username, key) + end + + def host_key(algorithm, daemon_options) do + :ssh_file.host_key(algorithm, daemon_options) + end +end |