From 10bc34245e8e1e3ba63fb0720d3bcfb1119db921 Mon Sep 17 00:00:00 2001 From: Simponic Date: Tue, 27 Dec 2022 23:50:22 -0700 Subject: [PATCH 1/7] Initial erlang stuff --- README.md | 3 + config/config.exs | 8 +- config/test.exs | 5 +- lib/chessh/application.ex | 2 +- lib/chessh/auth/password.ex | 3 +- lib/chessh/schema/key.ex | 3 +- lib/chessh/ssh/daemon.ex | 60 +++++++++++++++ lib/chessh/ssh/server.ex | 0 lib/chessh/ssh/server_key.ex | 11 +++ mix.exs | 5 +- mix.lock | 4 + priv/test_keys/client_keys/.gitignore | 1 + priv/test_keys/client_keys/id_ed25519 | 7 ++ priv/test_keys/client_keys/id_ed25519.pub | 1 + test/auth/password_test.exs | 7 +- test/auth/pubkey_test.exs | 6 +- test/schema/register_test.exs | 3 +- test/ssh/ssh_auth_test.exs | 94 +++++++++++++++++++++++ 18 files changed, 203 insertions(+), 20 deletions(-) create mode 100644 lib/chessh/ssh/daemon.ex delete mode 100644 lib/chessh/ssh/server.ex create mode 100644 lib/chessh/ssh/server_key.ex create mode 100644 priv/test_keys/client_keys/.gitignore create mode 100644 priv/test_keys/client_keys/id_ed25519 create mode 100644 priv/test_keys/client_keys/id_ed25519.pub create mode 100644 test/ssh/ssh_auth_test.exs diff --git a/README.md b/README.md index 967c4dc..ae1b5a0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # CheSSH +Features: +- [X] SSH Key & Password authentication +- [ ] Rate limiting \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index daffcad..2136a60 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,8 +2,12 @@ import Config config :chessh, ecto_repos: [Chessh.Repo], - priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), - port: 42069, + key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), + max_password_attempts: 3, + port: 42_069, max_sessions: 255 +config :hammer, + backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} + import_config "#{config_env()}.exs" diff --git a/config/test.exs b/config/test.exs index c1d70dd..db11bb4 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,5 +1,8 @@ import Config +config :hammer, + backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} + config :chessh, Chessh.Repo, database: "chessh-test", username: "postgres", @@ -8,4 +11,4 @@ config :chessh, Chessh.Repo, pool: Ecto.Adapters.SQL.Sandbox config :chessh, - priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys") + key_dir: Path.join(Path.dirname(__DIR__), "priv/test_keys") diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex index c760532..847dd98 100644 --- a/lib/chessh/application.ex +++ b/lib/chessh/application.ex @@ -2,7 +2,7 @@ defmodule Chessh.Application do use Application 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) end diff --git a/lib/chessh/auth/password.ex b/lib/chessh/auth/password.ex index 8a6c683..c3b03f5 100644 --- a/lib/chessh/auth/password.ex +++ b/lib/chessh/auth/password.ex @@ -1,6 +1,5 @@ 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 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/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex new file mode 100644 index 0000000..acb6bea --- /dev/null +++ b/lib/chessh/ssh/daemon.ex @@ -0,0 +1,60 @@ +defmodule Chessh.SSH.Daemon do + 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, _address, attempts) do + if Chessh.Auth.PasswordAuthenticator.authenticate(username, password) do + true + else + newAttempts = + case attempts do + :undefined -> 0 + _ -> attempts + end + + if Application.fetch_env!(:chessh, :max_password_attempts) <= newAttempts do + :disconnect + else + {false, newAttempts + 1} + end + end + end + + 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, + 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 diff --git a/lib/chessh/ssh/server_key.ex b/lib/chessh/ssh/server_key.ex new file mode 100644 index 0000000..1096e09 --- /dev/null +++ b/lib/chessh/ssh/server_key.ex @@ -0,0 +1,11 @@ +defmodule Chessh.SSH.ServerKey do + @behaviour :ssh_server_key_api + + def is_auth_key(key, username, _daemon_options) do + Chessh.Auth.KeyAuthenticator.authenticate(username, key) + end + + def host_key(algorithm, daemon_options) do + :ssh_file.host_key(algorithm, daemon_options) + end +end diff --git a/mix.exs b/mix.exs index 4d91dd2..500d0b2 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule Chessh.MixProject do def application do [ mod: {Chessh.Application, []}, - extra_applications: [:logger, :crypto, :ssh] + extra_applications: [:logger, :crypto, :ssh, :iex] ] end @@ -31,7 +31,8 @@ defmodule Chessh.MixProject do {:ecto, "~> 3.9"}, {:ecto_sql, "~> 3.9"}, {:postgrex, "~> 0.16.5"}, - {:bcrypt_elixir, "~> 3.0"} + {:bcrypt_elixir, "~> 3.0"}, + {:hammer, "~> 6.0"} ] end diff --git a/mix.lock b/mix.lock index ec97627..a956b4d 100644 --- a/mix.lock +++ b/mix.lock @@ -5,10 +5,14 @@ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "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"}, "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"}, + "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"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, } diff --git a/priv/test_keys/client_keys/.gitignore b/priv/test_keys/client_keys/.gitignore new file mode 100644 index 0000000..6599fc4 --- /dev/null +++ b/priv/test_keys/client_keys/.gitignore @@ -0,0 +1 @@ +known_hosts \ No newline at end of file diff --git a/priv/test_keys/client_keys/id_ed25519 b/priv/test_keys/client_keys/id_ed25519 new file mode 100644 index 0000000..af061d7 --- /dev/null +++ b/priv/test_keys/client_keys/id_ed25519 @@ -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----- \ No newline at end of file diff --git a/priv/test_keys/client_keys/id_ed25519.pub b/priv/test_keys/client_keys/id_ed25519.pub new file mode 100644 index 0000000..e97b08e --- /dev/null +++ b/priv/test_keys/client_keys/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKGZE4n089mwGkbjqf4UIuf6Y/SedzBI+Bc73FFGwo9 \ No newline at end of file diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs index 974f2fa..1516bdf 100644 --- a/test/auth/password_test.exs +++ b/test/auth/password_test.exs @@ -1,11 +1,10 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do use ExUnit.Case - alias Chessh.Player - alias Chessh.Repo + alias Chessh.{Player, Repo} @valid_user %{username: "logan", password: "password"} - setup do + setup_all do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) {:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) @@ -13,7 +12,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do :ok end - test "User can sign in with their password" do + test "Password can authenticate a hashed password" do assert Chessh.Auth.PasswordAuthenticator.authenticate( String.to_charlist(@valid_user.username), String.to_charlist(@valid_user.password) diff --git a/test/auth/pubkey_test.exs b/test/auth/pubkey_test.exs index 78eecfb..d8236e3 100644 --- a/test/auth/pubkey_test.exs +++ b/test/auth/pubkey_test.exs @@ -1,8 +1,6 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do use ExUnit.Case - alias Chessh.Key - alias Chessh.Repo - alias Chessh.Player + alias Chessh.{Key, Repo, Player} @valid_user %{username: "logan", password: "password"} @valid_key %{ @@ -10,7 +8,7 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44" } - setup do + setup_all do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) diff --git a/test/schema/register_test.exs b/test/schema/register_test.exs index 5705d31..0e9fdf1 100644 --- a/test/schema/register_test.exs +++ b/test/schema/register_test.exs @@ -1,8 +1,7 @@ defmodule Chessh.Auth.UserRegistrationTest do use Chessh.RepoCase use ExUnit.Case - alias Chessh.Player - alias Chessh.Repo + alias Chessh.{Player, Repo} @valid_user %{username: "logan", password: "password"} @invalid_username %{username: "a", password: "password"} diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs new file mode 100644 index 0000000..c3ced20 --- /dev/null +++ b/test/ssh/ssh_auth_test.exs @@ -0,0 +1,94 @@ +defmodule Chessh.SSH.AuthTest do + use ExUnit.Case + alias Chessh.{Player, Repo, Key} + + @localhost '127.0.0.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 + case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do + :ok -> nil + {:already, :owner} -> nil + end + + 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 "Fails to authenticate after configured max password attempt" do + assert :disconnect == + Enum.reduce( + 1..Application.fetch_env!(:chessh, :max_password_attempts), + %{attempts: 0}, + fn acc, _ -> + case Chessh.SSH.Daemon.pwd_authenticate( + @valid_user.username, + 'wrong_password', + @localhost, + acc + ) do + {false, state} -> state + x -> x + end + end + ) + end + + test "INTEGRATION TEST - 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 + + test "Hosts are rate limited via password attempts" do + :ok + end + + test "Hosts are also rate limited with public keys" do + :ok + end + + test "User cannot have more than one current session" do + :ok + end +end -- 2.45.2 From 1a2bdccf124de6207899f59538cc0ed2efc97b5a Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 17:21:20 -0700 Subject: [PATCH 2/7] Add scalable nodes and user sessions --- .env | 1 + .env.example | 1 + config/config.exs | 6 +- config/test.exs | 5 +- lib/chessh/application.ex | 16 +++- lib/chessh/auth/password.ex | 4 +- lib/chessh/schema/node.ex | 24 ++++++ lib/chessh/schema/player_session.ex | 34 ++++++++ lib/chessh/ssh/daemon.ex | 43 +++++++--- lib/chessh/ssh/server_key.ex | 3 +- mix.exs | 2 +- .../migrations/20221229225556_add_node.exs | 10 +++ .../20221229225559_add_user_session.exs | 11 +++ test/auth/password_test.exs | 8 +- .../ssh/ssh_auth_test-emacs-elixir-format.exs | 81 +++++++++++++++++++ test/ssh/ssh_auth_test.exs | 27 ++----- 16 files changed, 231 insertions(+), 45 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 lib/chessh/schema/node.ex create mode 100644 lib/chessh/schema/player_session.ex create mode 100644 priv/repo/migrations/20221229225556_add_node.exs create mode 100644 priv/repo/migrations/20221229225559_add_user_session.exs create mode 100644 test/ssh/ssh_auth_test-emacs-elixir-format.exs diff --git a/.env b/.env new file mode 100644 index 0000000..20bab31 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NODE_ID=node-one diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d87dcc8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +NODE_ID=aUniqueString \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 2136a60..c919a33 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,10 +3,14 @@ import Config config :chessh, ecto_repos: [Chessh.Repo], key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), - max_password_attempts: 3, port: 42_069, max_sessions: 255 +config :chessh, RateLimits, + jail_timeout_ms: 5 * 60 * 1000, + jail_threshold: 15, + max_concurrent_user_sessions: 5 + config :hammer, backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} diff --git a/config/test.exs b/config/test.exs index db11bb4..bdaf9f1 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,7 +1,8 @@ import Config -config :hammer, - backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} +config :chessh, RateLimits, + jail_timeout_ms: 1000, + jail_threshold: 2 config :chessh, Chessh.Repo, database: "chessh-test", diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex index 847dd98..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, 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 c3b03f5..ea2c8fc 100644 --- a/lib/chessh/auth/password.ex +++ b/lib/chessh/auth/password.ex @@ -2,8 +2,8 @@ defmodule Chessh.Auth.PasswordAuthenticator do 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/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 index acb6bea..cd0d466 100644 --- a/lib/chessh/ssh/daemon.ex +++ b/lib/chessh/ssh/daemon.ex @@ -1,4 +1,5 @@ defmodule Chessh.SSH.Daemon do + alias Chessh.Auth.PasswordAuthenticator use GenServer def start_link(_) do @@ -12,24 +13,39 @@ defmodule Chessh.SSH.Daemon do {:ok, state} end - def pwd_authenticate(username, password, _address, attempts) do - if Chessh.Auth.PasswordAuthenticator.authenticate(username, password) do - true - else - newAttempts = - case attempts do - :undefined -> 0 - _ -> attempts - end + def pwd_authenticate(username, password) do + # TODO - check concurrent sessions + PasswordAuthenticator.authenticate( + String.Chars.to_string(username), + String.Chars.to_string(password) + ) + end - if Application.fetch_env!(:chessh, :max_password_attempts) <= newAttempts do + def pwd_authenticate(username, password, inet) do + [jail_timeout_ms, jail_threshold] = + Application.get_env(:chessh, RateLimits) + |> Keyword.take([:jail_timeout_ms, :jail_threshold]) + |> Keyword.values() + + {ip, _port} = inet + rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}" + + case Hammer.check_rate(rateId, jail_timeout_ms, jail_threshold) do + {:allow, _count} -> + pwd_authenticate(username, password) || + (fn -> + Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_threshold, 1) + false + end).() + + {:deny, _limit} -> :disconnect - else - {false, newAttempts + 1} - 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)) @@ -40,6 +56,7 @@ defmodule Chessh.SSH.Daemon do system_dir: key_dir, pwdfun: &pwd_authenticate/4, key_cb: Chessh.SSH.ServerKey, + # disconnectfun: id_string: :random, subsystems: [], parallel_login: true, diff --git a/lib/chessh/ssh/server_key.ex b/lib/chessh/ssh/server_key.ex index 1096e09..72a4fbb 100644 --- a/lib/chessh/ssh/server_key.ex +++ b/lib/chessh/ssh/server_key.ex @@ -1,8 +1,9 @@ defmodule Chessh.SSH.ServerKey do + alias Chessh.Auth.KeyAuthenticator @behaviour :ssh_server_key_api def is_auth_key(key, username, _daemon_options) do - Chessh.Auth.KeyAuthenticator.authenticate(username, key) + KeyAuthenticator.authenticate(username, key) end def host_key(algorithm, daemon_options) do diff --git a/mix.exs b/mix.exs index 500d0b2..d35b519 100644 --- a/mix.exs +++ b/mix.exs @@ -32,7 +32,7 @@ defmodule Chessh.MixProject do {:ecto_sql, "~> 3.9"}, {:postgrex, "~> 0.16.5"}, {:bcrypt_elixir, "~> 3.0"}, - {:hammer, "~> 6.0"} + {:hammer, "~> 6.1"} ] end diff --git a/priv/repo/migrations/20221229225556_add_node.exs b/priv/repo/migrations/20221229225556_add_node.exs new file mode 100644 index 0000000..f8eace8 --- /dev/null +++ b/priv/repo/migrations/20221229225556_add_node.exs @@ -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 diff --git a/priv/repo/migrations/20221229225559_add_user_session.exs b/priv/repo/migrations/20221229225559_add_user_session.exs new file mode 100644 index 0000000..6f7a599 --- /dev/null +++ b/priv/repo/migrations/20221229225559_add_user_session.exs @@ -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 diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs index 1516bdf..5e1b1d8 100644 --- a/test/auth/password_test.exs +++ b/test/auth/password_test.exs @@ -14,13 +14,13 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do test "Password can authenticate a hashed password" do assert Chessh.Auth.PasswordAuthenticator.authenticate( - String.to_charlist(@valid_user.username), - String.to_charlist(@valid_user.password) + @valid_user.username, + @valid_user.password ) refute Chessh.Auth.PasswordAuthenticator.authenticate( - String.to_charlist(@valid_user.username), - String.to_charlist("a_bad_password") + @valid_user.username, + "a_bad_password" ) end end diff --git a/test/ssh/ssh_auth_test-emacs-elixir-format.exs b/test/ssh/ssh_auth_test-emacs-elixir-format.exs new file mode 100644 index 0000000..cb07259 --- /dev/null +++ b/test/ssh/ssh_auth_test-emacs-elixir-format.exs @@ -0,0 +1,81 @@ +defmodule Chessh.SSH.AuthTest do + use ExUnit.Case + alias Chessh.{Player, Repo, Key} + + @localhost '127.0.0.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 + case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do + :ok -> nil + {:already, :owner} -> nil + end + + 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 + assert :disconnect == + Enum.reduce( + 1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold), + fn _, _ -> + Chessh.SSH.Daemon.pwd_authenticate( + @valid_user.username, + 'wrong_password', + @localhost + ) do + 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 + + test "INTEGRATION - User cannot have more than specified concurrent sessions" do + :ok + end +end diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs index c3ced20..cb07259 100644 --- a/test/ssh/ssh_auth_test.exs +++ b/test/ssh/ssh_auth_test.exs @@ -29,26 +29,21 @@ defmodule Chessh.SSH.AuthTest do :ok end - test "Fails to authenticate after configured max password attempt" do + test "Password attempts are rate limited" do assert :disconnect == Enum.reduce( - 1..Application.fetch_env!(:chessh, :max_password_attempts), - %{attempts: 0}, - fn acc, _ -> - case Chessh.SSH.Daemon.pwd_authenticate( + 1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold), + fn _, _ -> + Chessh.SSH.Daemon.pwd_authenticate( @valid_user.username, 'wrong_password', - @localhost, - acc + @localhost ) do - {false, state} -> state - x -> x - end end ) end - test "INTEGRATION TEST - Can ssh into daemon with password or public key" do + test "INTEGRATION - Can ssh into daemon with password or public key" do {:ok, sup} = Task.Supervisor.start_link() test_pid = self() @@ -80,15 +75,7 @@ defmodule Chessh.SSH.AuthTest do assert_receive(:connected_via_public_key, 500) end - test "Hosts are rate limited via password attempts" do - :ok - end - - test "Hosts are also rate limited with public keys" do - :ok - end - - test "User cannot have more than one current session" do + test "INTEGRATION - User cannot have more than specified concurrent sessions" do :ok end end -- 2.45.2 From 0aab3c2027d54fc1ba834ab73fd53328952b7a1b Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 17:49:42 -0700 Subject: [PATCH 3/7] Make tests pass, rename jail threshold --- config/config.exs | 2 +- config/test.exs | 2 +- lib/chessh/ssh/daemon.ex | 22 ++--- test/auth/password_test.exs | 3 +- test/auth/pubkey_test.exs | 3 +- .../ssh/ssh_auth_test-emacs-elixir-format.exs | 81 ------------------- test/ssh/ssh_auth_test.exs | 28 ++++--- 7 files changed, 32 insertions(+), 109 deletions(-) delete mode 100644 test/ssh/ssh_auth_test-emacs-elixir-format.exs diff --git a/config/config.exs b/config/config.exs index c919a33..945f905 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,7 +8,7 @@ config :chessh, config :chessh, RateLimits, jail_timeout_ms: 5 * 60 * 1000, - jail_threshold: 15, + jail_attempt_threshold: 15, max_concurrent_user_sessions: 5 config :hammer, diff --git a/config/test.exs b/config/test.exs index bdaf9f1..69b6c93 100644 --- a/config/test.exs +++ b/config/test.exs @@ -2,7 +2,7 @@ import Config config :chessh, RateLimits, jail_timeout_ms: 1000, - jail_threshold: 2 + jail_attempt_threshold: 3 config :chessh, Chessh.Repo, database: "chessh-test", diff --git a/lib/chessh/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex index cd0d466..9f17f75 100644 --- a/lib/chessh/ssh/daemon.ex +++ b/lib/chessh/ssh/daemon.ex @@ -22,24 +22,24 @@ defmodule Chessh.SSH.Daemon do end def pwd_authenticate(username, password, inet) do - [jail_timeout_ms, jail_threshold] = + [jail_timeout_ms, jail_attempt_threshold] = Application.get_env(:chessh, RateLimits) - |> Keyword.take([:jail_timeout_ms, :jail_threshold]) + |> Keyword.take([:jail_timeout_ms, :jail_attempt_threshold]) |> Keyword.values() {ip, _port} = inet rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}" - case Hammer.check_rate(rateId, jail_timeout_ms, jail_threshold) do - {:allow, _count} -> - pwd_authenticate(username, password) || - (fn -> - Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_threshold, 1) - false - end).() + 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 + {:deny, _limit} -> + :disconnect + end end end diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs index 5e1b1d8..8c93ea9 100644 --- a/test/auth/password_test.exs +++ b/test/auth/password_test.exs @@ -5,7 +5,8 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do @valid_user %{username: "logan", password: "password"} 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)) diff --git a/test/auth/pubkey_test.exs b/test/auth/pubkey_test.exs index d8236e3..da2518b 100644 --- a/test/auth/pubkey_test.exs +++ b/test/auth/pubkey_test.exs @@ -9,7 +9,8 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest 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)) diff --git a/test/ssh/ssh_auth_test-emacs-elixir-format.exs b/test/ssh/ssh_auth_test-emacs-elixir-format.exs deleted file mode 100644 index cb07259..0000000 --- a/test/ssh/ssh_auth_test-emacs-elixir-format.exs +++ /dev/null @@ -1,81 +0,0 @@ -defmodule Chessh.SSH.AuthTest do - use ExUnit.Case - alias Chessh.{Player, Repo, Key} - - @localhost '127.0.0.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 - case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do - :ok -> nil - {:already, :owner} -> nil - end - - 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 - assert :disconnect == - Enum.reduce( - 1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold), - fn _, _ -> - Chessh.SSH.Daemon.pwd_authenticate( - @valid_user.username, - 'wrong_password', - @localhost - ) do - 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 - - test "INTEGRATION - User cannot have more than specified concurrent sessions" do - :ok - end -end diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs index cb07259..27d5e4c 100644 --- a/test/ssh/ssh_auth_test.exs +++ b/test/ssh/ssh_auth_test.exs @@ -3,17 +3,14 @@ defmodule Chessh.SSH.AuthTest do 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 - case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do - :ok -> nil - {:already, :owner} -> nil - end - + Ecto.Adapters.SQL.Sandbox.checkout(Repo) Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) @@ -30,15 +27,19 @@ defmodule Chessh.SSH.AuthTest do 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( - 1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold), + 0..(jail_attempt_threshold + 1), fn _, _ -> Chessh.SSH.Daemon.pwd_authenticate( - @valid_user.username, - 'wrong_password', - @localhost - ) do + @valid_user.username, + "wrong_password", + @localhost_inet + ) end ) end @@ -75,7 +76,8 @@ defmodule Chessh.SSH.AuthTest do assert_receive(:connected_via_public_key, 500) end - test "INTEGRATION - User cannot have more than specified concurrent sessions" do - :ok - end + # TODO + # test "INTEGRATION - User cannot have more than specified concurrent sessions" do + # :ok + # end end -- 2.45.2 From 4b9ccbf108f89236f82382a4a2e0be5965e002b1 Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 18:27:15 -0700 Subject: [PATCH 4/7] Package changes --- mix.exs | 2 +- mix.lock | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index d35b519..441fb98 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule Chessh.MixProject do def application do [ mod: {Chessh.Application, []}, - extra_applications: [:logger, :crypto, :ssh, :iex] + extra_applications: [:logger, :crypto, :ssh] ] end diff --git a/mix.lock b/mix.lock index a956b4d..e5bd298 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,6 @@ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "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"}, "elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"}, -- 2.45.2 From d1e02a3cc23e93318bcde7052f7acb6fc2473bd7 Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 18:29:40 -0700 Subject: [PATCH 5/7] Add node id in github workflow --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 5f39faa..95dc4c6 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -10,9 +10,9 @@ on: pull_request: branches: [ "main" ] # adapt branch for project -# Sets the ENV `MIX_ENV` to `test` for running tests env: MIX_ENV: test + NODE_ID: aUniqueString jobs: test: -- 2.45.2 From aaeef8158dc7d4d294a4f7083ac8c07d397c1d6d Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 18:35:24 -0700 Subject: [PATCH 6/7] increase limit ms in test --- config/test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 69b6c93..002b49f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,7 +1,7 @@ import Config config :chessh, RateLimits, - jail_timeout_ms: 1000, + jail_timeout_ms: 5000, jail_attempt_threshold: 3 config :chessh, Chessh.Repo, -- 2.45.2 From 479ca815e3a1760c71e8977674434b15f94ae833 Mon Sep 17 00:00:00 2001 From: Simponic Date: Thu, 29 Dec 2022 18:36:17 -0700 Subject: [PATCH 7/7] Add redis comment --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 945f905..42339fd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,6 +11,7 @@ config :chessh, RateLimits, 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]} -- 2.45.2