Merge pull request #1 from Simponic/erlang_ssh_server
This commit is contained in:
commit
c143bb549c
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
NODE_ID=aUniqueString
|
2
.github/workflows/elixir.yml
vendored
2
.github/workflows/elixir.yml
vendored
@ -10,9 +10,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ] # adapt branch for project
|
branches: [ "main" ] # adapt branch for project
|
||||||
|
|
||||||
# Sets the ENV `MIX_ENV` to `test` for running tests
|
|
||||||
env:
|
env:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
NODE_ID: aUniqueString
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
# CheSSH
|
# CheSSH
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- [X] SSH Key & Password authentication
|
||||||
|
- [ ] Rate limiting
|
@ -2,8 +2,17 @@ import Config
|
|||||||
|
|
||||||
config :chessh,
|
config :chessh,
|
||||||
ecto_repos: [Chessh.Repo],
|
ecto_repos: [Chessh.Repo],
|
||||||
priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
|
key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
|
||||||
port: 42069,
|
port: 42_069,
|
||||||
max_sessions: 255
|
max_sessions: 255
|
||||||
|
|
||||||
|
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]}
|
||||||
|
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
config :chessh, RateLimits,
|
||||||
|
jail_timeout_ms: 5000,
|
||||||
|
jail_attempt_threshold: 3
|
||||||
|
|
||||||
config :chessh, Chessh.Repo,
|
config :chessh, Chessh.Repo,
|
||||||
database: "chessh-test",
|
database: "chessh-test",
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
@ -8,4 +12,4 @@ config :chessh, Chessh.Repo,
|
|||||||
pool: Ecto.Adapters.SQL.Sandbox
|
pool: Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
config :chessh,
|
config :chessh,
|
||||||
priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys")
|
key_dir: Path.join(Path.dirname(__DIR__), "priv/test_keys")
|
||||||
|
@ -1,9 +1,23 @@
|
|||||||
defmodule Chessh.Application do
|
defmodule Chessh.Application do
|
||||||
|
alias Chessh.{PlayerSession, Node}
|
||||||
use Application
|
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
|
def start(_, _) do
|
||||||
children = [Chessh.Repo]
|
children = [Chessh.Repo, Chessh.SSH.Daemon]
|
||||||
opts = [strategy: :one_for_one, name: Chessh.Supervisor]
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
defmodule Chessh.Auth.PasswordAuthenticator do
|
defmodule Chessh.Auth.PasswordAuthenticator do
|
||||||
alias Chessh.Player
|
alias Chessh.{Player, Repo}
|
||||||
alias Chessh.Repo
|
|
||||||
|
|
||||||
def authenticate(username, password) do
|
def authenticate(username, password) do
|
||||||
case Repo.get_by(Player, username: String.Chars.to_string(username)) do
|
case Repo.get_by(Player, username: username) do
|
||||||
x -> Player.valid_password?(x, String.Chars.to_string(password))
|
x -> Player.valid_password?(x, password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -16,7 +16,7 @@ defmodule Chessh.Key do
|
|||||||
|> cast(update_encode_key(attrs, :key), [:key])
|
|> cast(update_encode_key(attrs, :key), [:key])
|
||||||
|> cast(attrs, [:name])
|
|> cast(attrs, [:name])
|
||||||
|> validate_required([:key, :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")
|
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ defmodule Chessh.Key do
|
|||||||
end
|
end
|
||||||
# Remove comment at end of key
|
# Remove comment at end of key
|
||||||
|> String.replace(~r/ [^ ]+\@[^ ]+$/, "")
|
|> String.replace(~r/ [^ ]+\@[^ ]+$/, "")
|
||||||
# Remove potential spaces / newline
|
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
24
lib/chessh/schema/node.ex
Normal file
24
lib/chessh/schema/node.ex
Normal file
@ -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
|
34
lib/chessh/schema/player_session.ex
Normal file
34
lib/chessh/schema/player_session.ex
Normal file
@ -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
|
77
lib/chessh/ssh/daemon.ex
Normal file
77
lib/chessh/ssh/daemon.ex
Normal file
@ -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
|
12
lib/chessh/ssh/server_key.ex
Normal file
12
lib/chessh/ssh/server_key.ex
Normal file
@ -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
|
3
mix.exs
3
mix.exs
@ -31,7 +31,8 @@ defmodule Chessh.MixProject do
|
|||||||
{:ecto, "~> 3.9"},
|
{:ecto, "~> 3.9"},
|
||||||
{:ecto_sql, "~> 3.9"},
|
{:ecto_sql, "~> 3.9"},
|
||||||
{:postgrex, "~> 0.16.5"},
|
{:postgrex, "~> 0.16.5"},
|
||||||
{:bcrypt_elixir, "~> 3.0"}
|
{:bcrypt_elixir, "~> 3.0"},
|
||||||
|
{:hammer, "~> 6.1"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
3
mix.lock
3
mix.lock
@ -8,7 +8,10 @@
|
|||||||
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
|
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"},
|
"ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"},
|
"elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"},
|
||||||
|
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||||
"esshd": {:hex, :esshd, "0.2.1", "cded6a329c32bc3b3c15828bcd34203227bbef310db3c39a6f3c55cf5b29cd34", [:mix], [], "hexpm", "b058b56af53aba1c23522d72a3c39ab7f302e509af1c0ba1a748f00d93053c4d"},
|
"esshd": {:hex, :esshd, "0.2.1", "cded6a329c32bc3b3c15828bcd34203227bbef310db3c39a6f3c55cf5b29cd34", [:mix], [], "hexpm", "b058b56af53aba1c23522d72a3c39ab7f302e509af1c0ba1a748f00d93053c4d"},
|
||||||
|
"hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"},
|
||||||
|
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||||
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||||
}
|
}
|
||||||
|
10
priv/repo/migrations/20221229225556_add_node.exs
Normal file
10
priv/repo/migrations/20221229225556_add_node.exs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
defmodule Chessh.Repo.Migrations.AddNode do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:nodes, primary_key: false) do
|
||||||
|
add(:id, :string, primary_key: true)
|
||||||
|
add(:last_start, :utc_datetime)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
priv/repo/migrations/20221229225559_add_user_session.exs
Normal file
11
priv/repo/migrations/20221229225559_add_user_session.exs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
defmodule Chessh.Repo.Migrations.AddUserSession do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:player_sessions) do
|
||||||
|
add(:login, :utc_datetime)
|
||||||
|
add(:player_id, references(:players))
|
||||||
|
add(:node_id, references(:nodes, type: :string))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
1
priv/test_keys/client_keys/.gitignore
vendored
Normal file
1
priv/test_keys/client_keys/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
known_hosts
|
7
priv/test_keys/client_keys/id_ed25519
Normal file
7
priv/test_keys/client_keys/id_ed25519
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACDyhmROJ9PPZsBpG46n+FCLn+mP0nncwSPgXO9xRRsKPQAAAJAJnUS4CZ1E
|
||||||
|
uAAAAAtzc2gtZWQyNTUxOQAAACDyhmROJ9PPZsBpG46n+FCLn+mP0nncwSPgXO9xRRsKPQ
|
||||||
|
AAAEBjR5Cy8SHUtrIf6aHJGXA/kgesZzxxjH15E4wj1DESh/KGZE4n089mwGkbjqf4UIuf
|
||||||
|
6Y/SedzBI+Bc73FFGwo9AAAABm5vbmFtZQECAwQFBgc=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
priv/test_keys/client_keys/id_ed25519.pub
Normal file
1
priv/test_keys/client_keys/id_ed25519.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKGZE4n089mwGkbjqf4UIuf6Y/SedzBI+Bc73FFGwo9
|
@ -1,27 +1,27 @@
|
|||||||
defmodule Chessh.Auth.PasswordAuthenticatorTest do
|
defmodule Chessh.Auth.PasswordAuthenticatorTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
alias Chessh.Player
|
alias Chessh.{Player, Repo}
|
||||||
alias Chessh.Repo
|
|
||||||
|
|
||||||
@valid_user %{username: "logan", password: "password"}
|
@valid_user %{username: "logan", password: "password"}
|
||||||
|
|
||||||
setup do
|
setup_all do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
|
Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||||
|
|
||||||
{:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
{:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "User can sign in with their password" do
|
test "Password can authenticate a hashed password" do
|
||||||
assert Chessh.Auth.PasswordAuthenticator.authenticate(
|
assert Chessh.Auth.PasswordAuthenticator.authenticate(
|
||||||
String.to_charlist(@valid_user.username),
|
@valid_user.username,
|
||||||
String.to_charlist(@valid_user.password)
|
@valid_user.password
|
||||||
)
|
)
|
||||||
|
|
||||||
refute Chessh.Auth.PasswordAuthenticator.authenticate(
|
refute Chessh.Auth.PasswordAuthenticator.authenticate(
|
||||||
String.to_charlist(@valid_user.username),
|
@valid_user.username,
|
||||||
String.to_charlist("a_bad_password")
|
"a_bad_password"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
|
defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
alias Chessh.Key
|
alias Chessh.{Key, Repo, Player}
|
||||||
alias Chessh.Repo
|
|
||||||
alias Chessh.Player
|
|
||||||
|
|
||||||
@valid_user %{username: "logan", password: "password"}
|
@valid_user %{username: "logan", password: "password"}
|
||||||
@valid_key %{
|
@valid_key %{
|
||||||
@ -10,8 +8,9 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
|
|||||||
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"
|
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup do
|
setup_all do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
|
Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||||
|
|
||||||
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
defmodule Chessh.Auth.UserRegistrationTest do
|
defmodule Chessh.Auth.UserRegistrationTest do
|
||||||
use Chessh.RepoCase
|
use Chessh.RepoCase
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
alias Chessh.Player
|
alias Chessh.{Player, Repo}
|
||||||
alias Chessh.Repo
|
|
||||||
|
|
||||||
@valid_user %{username: "logan", password: "password"}
|
@valid_user %{username: "logan", password: "password"}
|
||||||
@invalid_username %{username: "a", password: "password"}
|
@invalid_username %{username: "a", password: "password"}
|
||||||
|
83
test/ssh/ssh_auth_test.exs
Normal file
83
test/ssh/ssh_auth_test.exs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
defmodule Chessh.SSH.AuthTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
alias Chessh.{Player, Repo, Key}
|
||||||
|
|
||||||
|
@localhost '127.0.0.1'
|
||||||
|
@localhost_inet {{127, 0, 0, 1}, 1}
|
||||||
|
@key_name "The Gamer Machine"
|
||||||
|
@valid_user %{username: "logan", password: "password"}
|
||||||
|
@client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
|
||||||
|
@client_pub_key 'id_ed25519.pub'
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||||
|
|
||||||
|
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
||||||
|
|
||||||
|
{:ok, key_text} = File.read(Path.join(@client_test_keys_dir, @client_pub_key))
|
||||||
|
|
||||||
|
{:ok, _key} =
|
||||||
|
Repo.insert(
|
||||||
|
Key.changeset(%Key{}, %{key: key_text, name: @key_name})
|
||||||
|
|> Ecto.Changeset.put_assoc(:player, player)
|
||||||
|
)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Password attempts are rate limited" do
|
||||||
|
jail_attempt_threshold =
|
||||||
|
Application.get_env(:chessh, RateLimits)
|
||||||
|
|> Keyword.get(:jail_attempt_threshold)
|
||||||
|
|
||||||
|
assert :disconnect ==
|
||||||
|
Enum.reduce(
|
||||||
|
0..(jail_attempt_threshold + 1),
|
||||||
|
fn _, _ ->
|
||||||
|
Chessh.SSH.Daemon.pwd_authenticate(
|
||||||
|
@valid_user.username,
|
||||||
|
"wrong_password",
|
||||||
|
@localhost_inet
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "INTEGRATION - Can ssh into daemon with password or public key" do
|
||||||
|
{:ok, sup} = Task.Supervisor.start_link()
|
||||||
|
test_pid = self()
|
||||||
|
|
||||||
|
Task.Supervisor.start_child(sup, fn ->
|
||||||
|
{:ok, _pid} =
|
||||||
|
:ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
|
||||||
|
user: String.to_charlist(@valid_user.username),
|
||||||
|
password: String.to_charlist(@valid_user.password),
|
||||||
|
auth_methods: 'password',
|
||||||
|
silently_accept_hosts: true
|
||||||
|
)
|
||||||
|
|
||||||
|
send(test_pid, :connected_via_password)
|
||||||
|
end)
|
||||||
|
|
||||||
|
Task.Supervisor.start_child(sup, fn ->
|
||||||
|
{:ok, _pid} =
|
||||||
|
:ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
|
||||||
|
user: String.to_charlist(@valid_user.username),
|
||||||
|
auth_methods: 'publickey',
|
||||||
|
silently_accept_hosts: true,
|
||||||
|
user_dir: String.to_charlist(@client_test_keys_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
send(test_pid, :connected_via_public_key)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert_receive(:connected_via_password, 500)
|
||||||
|
assert_receive(:connected_via_public_key, 500)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# test "INTEGRATION - User cannot have more than specified concurrent sessions" do
|
||||||
|
# :ok
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user