farmbot_os/farmbot_os/platform/target/ssh_console.ex

122 lines
2.8 KiB
Elixir

defmodule FarmbotOS.Platform.Target.SSHConsole do
@moduledoc """
SSH IEx console.
"""
use GenServer
require Logger
require FarmbotCore.Logger
alias FarmbotCore.Asset.PublicKey
@behaviour FarmbotCore.Asset.PublicKey
def ready? do
is_pid(GenServer.whereis(__MODULE__))
end
def add_key(%PublicKey{} = public_key) do
GenServer.cast(__MODULE__, {:add_key, public_key})
end
def restart_ssh do
GenServer.cast(__MODULE__, :restart)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
port = 22
case start_ssh(port, []) do
{:ok, ssh} ->
{:ok, %{ssh: ssh, port: port, public_keys: []}}
error ->
Logger.warn("Could not start SSH: #{inspect(error)}")
:ignore
end
end
def terminate(_, %{ssh: ssh}) do
stop_ssh(ssh)
end
def handle_cast(:restart, %{ssh: ssh} = state) do
_ = stop_ssh(ssh)
case start_ssh(state.port, state.public_keys) do
{:ok, ssh} ->
{:noreply, %{state | ssh: ssh}}
error ->
FarmbotCore.Logger.warn(3, "Could not start SSH: #{inspect(error)}")
{:noreply, %{state | ssh: nil}}
end
end
def handle_cast(
{:add_key, %PublicKey{public_key: authorized_key}},
%{ssh: ssh} = state
) do
_ = stop_ssh(ssh)
decoded_authorized_key = do_decode(authorized_key)
case start_ssh(state.port, decoded_authorized_key) do
{:ok, ssh} ->
{:noreply,
%{
state
| ssh: ssh,
public_keys: [
List.first(decoded_authorized_key) | state.public_keys
]
}}
error ->
Logger.warn("Could not start SSH: #{inspect(error)}")
{:noreply, %{state | ssh: nil}}
end
end
defp stop_ssh(ssh) do
ssh && :ssh.stop_daemon(ssh)
end
defp start_ssh(port, decoded_authorized_keys)
when is_list(decoded_authorized_keys) do
# Reuse keys from `nerves_firmware_ssh` so that the user only needs one
# config.exs entry.
nerves_keys =
Application.get_env(:nerves_firmware_ssh, :authorized_keys, [])
|> Enum.join("\n")
decoded_nerves_keys = do_decode(nerves_keys)
cb_opts = [authorized_keys: decoded_nerves_keys ++ decoded_authorized_keys]
# Reuse the system_dir as well to allow for auth to work with the shared
# keys.
:ssh.daemon(port, [
{:id_string, :random},
{:key_cb, {Nerves.Firmware.SSH.Keys, cb_opts}},
{:system_dir, Nerves.Firmware.SSH.Application.system_dir()},
{:shell, {Elixir.IEx, :start, []}}
])
end
defp do_decode(nil), do: []
defp do_decode(<<>>), do: []
defp do_decode(authorized_key) do
try do
:public_key.ssh_decode(authorized_key, :auth_keys)
rescue
_err ->
Logger.warn("Could not decode ssh keys.")
[]
end
end
end