Skip to content

Commit

Permalink
NervesSSH.SystemShell
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed Jul 13, 2022
1 parent 0c28934 commit a78dcea
Show file tree
Hide file tree
Showing 6 changed files with 496 additions and 1 deletion.
311 changes: 311 additions & 0 deletions lib/nerves_ssh/system_shell.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
defmodule NervesSSH.SystemShellUtils do
@moduledoc false

def get_shell_command() do
cond do
shell = System.get_env("SHELL") ->
[shell, "-i"]

shell = System.find_executable("sh") ->
[shell, "-i"]

true ->
raise "SHELL environment variable not set and sh not available"
end
end

def get_term(nil) do
if term = System.get_env("TERM") do
[{"TERM", term}]
else
[{"TERM", "xterm"}]
end
end

def get_term({term, _, _, _, _, _}) when is_list(term),
do: [{"TERM", List.to_string(term)}]
end

defmodule NervesSSH.SystemShell do
@moduledoc """
A `:ssh_server_channel` that uses `:erlexec` to provide an interactive system shell.
"""

@behaviour :ssh_server_channel

require Logger

import NervesSSH.SystemShellUtils

defp exec_command(cmd, %{pty_opts: pty_opts, env: env}) do
base_opts = [
:stdin,
:stdout,
:monitor,
env: [:clear] ++ env ++ get_term(pty_opts)
]

opts =
case pty_opts do
nil ->
base_opts ++ [:stderr]

{_term, _cols, _rows, _, _, opts} ->
# https://www.erlang.org/doc/man/ssh_connection.html#type-pty_ch_msg
# erlexec understands the format of the erlang ssh pty_ch_msg
base_opts ++ [{:stderr, :stdout}, {:pty, opts}]
# not yet released
# ++ [{:winsz, {rows, cols}}]
end

:exec.run(cmd, opts)
end

@impl true
def init(_opts) do
{:ok,
%{
port_pid: nil,
os_pid: nil,
pty_opts: nil,
env: [],
cid: nil,
cm: nil
}}
end

@impl true
def handle_msg({:ssh_channel_up, channel_id, connection_manager}, state) do
{:ok, %{state | cid: channel_id, cm: connection_manager}}
end

# port closed
def handle_msg(
{:DOWN, os_pid, :process, port_pid, reason},
%{os_pid: os_pid, port_pid: port_pid, cm: cm, cid: cid} = state
) do
case reason do
:normal ->
:ssh_connection.exit_status(cm, cid, 0)

{:exit_status, status} ->
:ssh_connection.exit_status(cm, cid, status)
end

:ssh_connection.send_eof(cm, cid)
{:stop, cid, state}
end

def handle_msg({:stdout, os_pid, data} = _msg, %{cm: cm, cid: cid, os_pid: os_pid} = state) do
:ssh_connection.send(cm, cid, data)
{:ok, state}
end

def handle_msg({:stderr, os_pid, data} = _msg, %{cm: cm, cid: cid, os_pid: os_pid} = state) do
:ssh_connection.send(cm, cid, 1, data)
{:ok, state}
end

def handle_msg(msg, state) do
Logger.error("[NervesSSH.SystemShell] unhandled message: #{inspect(msg)}")
{:ok, state}
end

@impl true
# client sent a pty request
def handle_ssh_msg({:ssh_cm, cm, {:pty, cid, want_reply, pty_opts} = _msg}, state = %{cm: cm}) do
:ssh_connection.reply_request(cm, want_reply, :success, cid)

{:ok, %{state | pty_opts: pty_opts}}
end

# client wants to set an environment variable
def handle_ssh_msg(
{:ssh_cm, cm, {:env, cid, want_reply, key, value}},
state = %{cm: cm, cid: cid}
) do
:ssh_connection.reply_request(cm, want_reply, :success, cid)

{:ok, update_in(state, [:env], fn vars -> [{key, value} | vars] end)}
end

# client wants to execute a command
def handle_ssh_msg(
{:ssh_cm, cm, {:exec, cid, want_reply, command} = _msg},
state = %{cm: cm, cid: cid}
)
when is_list(command) do
{:ok, pid, os_pid} = exec_command(List.to_string(command), state)
:ssh_connection.reply_request(cm, want_reply, :success, cid)
{:ok, %{state | os_pid: os_pid, port_pid: pid}}
end

# client requested a shell
def handle_ssh_msg(
{:ssh_cm, cm, {:shell, cid, want_reply} = _msg},
state = %{cm: cm, cid: cid}
) do
{:ok, pid, os_pid} = exec_command(get_shell_command(), state)
:ssh_connection.reply_request(cm, want_reply, :success, cid)
{:ok, %{state | os_pid: os_pid, port_pid: pid}}
end

def handle_ssh_msg(
{:ssh_cm, _cm, {:data, channel_id, 0, data}},
state = %{os_pid: os_pid, cid: channel_id}
) do
:exec.send(os_pid, data)

{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:eof, _}}, state) do
{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:signal, _, _} = _msg}, state) do
{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:exit_signal, channel_id, _, _error, _}}, state) do
{:stop, channel_id, state}
end

def handle_ssh_msg({:ssh_cm, _, {:exit_status, channel_id, _status}}, state) do
{:stop, channel_id, state}
end

def handle_ssh_msg(
{:ssh_cm, cm, {:window_change, cid, width, height, _, _} = _msg},
state = %{os_pid: os_pid, cm: cm, cid: cid}
) do
:exec.winsz(os_pid, height, width)

{:ok, state}
end

def handle_ssh_msg(msg, state) do
Logger.error("[NervesSSH.SystemShell] unhandled ssh message: #{inspect(msg)}")
{:ok, state}
end

@impl true
def terminate(_reason, _state) do
:ok
end
end

defmodule NervesSSH.SystemShellSubsystem do
# TODO: maybe merge this into the SystemShell module
# but not sure yet if it's worth the effort

@moduledoc false

@behaviour :ssh_server_channel

require Logger

import NervesSSH.SystemShellUtils

@impl true
def init(opts) do
# SSH subsystems do not send :exec, :shell or :pty messages
command = Keyword.get_lazy(opts, :command, fn -> get_shell_command() end)
force_pty = Keyword.get(opts, :force_pty, true)

base_opts = [
:stdin,
:stdout,
:monitor,
env: get_term(nil)
]

opts =
if force_pty do
base_opts ++ [{:stderr, :stdout}, :pty, :pty_echo]
else
base_opts ++ [:stderr]
end

{:ok, port_pid, os_pid} = :exec.run(command, opts)

{:ok, %{os_pid: os_pid, port_pid: port_pid, cid: nil, cm: nil}}
end

@impl true
def handle_msg({:ssh_channel_up, channel_id, connection_manager}, state) do
{:ok, %{state | cid: channel_id, cm: connection_manager}}
end

# port closed
def handle_msg(
{:DOWN, os_pid, :process, port_pid, reason},
%{os_pid: os_pid, port_pid: port_pid, cm: cm, cid: cid} = state
) do
case reason do
:normal ->
:ssh_connection.exit_status(cm, cid, 0)

{:exit_status, status} ->
:ssh_connection.exit_status(cm, cid, status)
end

:ssh_connection.send_eof(cm, cid)
{:stop, cid, state}
end

def handle_msg({:stdout, os_pid, data}, %{os_pid: os_pid, cm: cm, cid: cid} = state) do
:ssh_connection.send(cm, cid, data)
{:ok, state}
end

def handle_msg({:stderr, os_pid, data}, %{os_pid: os_pid, cm: cm, cid: cid} = state) do
:ssh_connection.send(cm, cid, 1, data)
{:ok, state}
end

@impl true
def handle_ssh_msg(
{:ssh_cm, cm, {:data, cid, 0, data}},
state = %{os_pid: os_pid, cm: cm, cid: cid}
) do
:exec.send(os_pid, data)

{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:eof, _}}, state) do
{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:signal, _, _}}, state) do
{:ok, state}
end

def handle_ssh_msg({:ssh_cm, _, {:exit_signal, channel_id, _, _error, _}}, state) do
{:stop, channel_id, state}
end

def handle_ssh_msg({:ssh_cm, _, {:exit_status, channel_id, _status}}, state) do
{:stop, channel_id, state}
end

def handle_ssh_msg(
{:ssh_cm, cm, {:window_change, cid, width, height, _, _}},
state = %{os_pid: os_pid, cm: cm, cid: cid}
) do
:exec.winsz(os_pid, height, width)

{:ok, state}
end

def handle_ssh_msg(msg, state) do
Logger.error("[NervesSSH.SystemShellSubsystem] unhandled ssh message: #{inspect(msg)}")
{:ok, state}
end

@impl true
def terminate(_reason, _state) do
:ok
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defmodule NervesSSH.MixProject do
{:ex_doc, "~> 0.22", only: :docs, runtime: false},
{:ssh_subsystem_fwup, "~> 0.5"},
{:nerves_runtime, "~> 0.11"},
{:erlexec, "~> 1.21.0", optional: true},
# lfe currently requires `compile: "make"` to build and this is
# disallowed when pushing the package to hex.pm. Work around this by
# listing it as dev/test only.
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlexec": {:hex, :erlexec, "1.21.0", "748af41a9b77fde69bb82d5ec37e37384d4f849f971687de49d57e8cb59422ea", [:rebar3], [], "hexpm", "826616a344d1bc0a35c9d2c7175478166f607a4e3b14c878229fd405694d65c0"},
"ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
Expand Down
Loading

0 comments on commit a78dcea

Please sign in to comment.