chessh/lib/chessh/ssh/daemon.ex

105 lines
2.8 KiB
Elixir

defmodule Chessh.SSH.Daemon do
alias Chessh.{Repo, PlayerSession, Utils}
alias Chessh.Auth.PasswordAuthenticator
alias Chessh.SSH.{ServerKey, Tui}
use GenServer
import Ecto.Query
require Logger
def start_link(_) do
GenServer.start_link(__MODULE__, %{
pid: nil
})
end
def init(state) do
send(self(), :start)
{:ok, state}
end
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()
rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}"
case PasswordAuthenticator.authenticate(
String.Chars.to_string(username),
String.Chars.to_string(password)
) do
false ->
Logger.debug(
"#{username} on bucket #{rateId} got their password wrong, or they don't exist! Point at them and laugh!!!!"
)
case Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_attempt_threshold, 1) do
{:allow, _count} ->
Logger.debug("Bucket #{rateId} can continue to brute force though")
false
{:deny, _limit} ->
Logger.debug("Bucket #{rateId} ran out of password attempts")
:disconnect
end
x ->
PlayerSession.update_sessions_and_player_satisfies(username, fn _player ->
x
end)
x
end
end
def pwd_authenticate(username, password, inet, _state),
do: pwd_authenticate(username, password, inet)
def handle_info(: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: ServerKey,
ssh_cli: {Tui, [%Tui.State{}]},
disconnectfun: &on_disconnect/1,
id_string: :random,
parallel_login: true,
max_sessions: max_sessions,
subsystems: []
) do
{:ok, pid} ->
Logger.info("SSH server started on port #{port}, on #{inspect(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}
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