diff options
| -rw-r--r-- | .formatter.exs | 4 | ||||
| -rw-r--r-- | .gitignore | 26 | ||||
| -rw-r--r-- | .tool-versions | 1 | ||||
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | config/config.exs | 8 | ||||
| -rw-r--r-- | config/dev.exs | 1 | ||||
| -rw-r--r-- | config/prod.exs | 1 | ||||
| -rw-r--r-- | lib/ssh/conn_cli_manager.ex | 143 | ||||
| -rw-r--r-- | lib/ssh/daemon.ex | 43 | ||||
| -rw-r--r-- | lib/ssh/screen.ex | 18 | ||||
| -rw-r--r-- | lib/ssh/session.ex | 3 | ||||
| -rw-r--r-- | lib/ssh/tui.ex | 25 | ||||
| -rw-r--r-- | lib/trongle_ssh.ex | 11 | ||||
| -rw-r--r-- | mix.exs | 26 | ||||
| -rw-r--r-- | priv/.gitignore | 1 | ||||
| -rw-r--r-- | priv/make_keys.sh | 12 | ||||
| -rw-r--r-- | test/test_helper.exs | 1 | ||||
| -rw-r--r-- | test/trongle_ssh_test.exs | 8 |
18 files changed, 353 insertions, 0 deletions
diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f8455 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +tronglessh-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..787dcad --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +elixir 1.15.6-otp-25 diff --git a/README.md b/README.md new file mode 100644 index 0000000..829b05c --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# TrongleSSH + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `tronglessh` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:tronglessh, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at <https://hexdocs.pm/tronglessh>. + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..06e657b --- /dev/null +++ b/config/config.exs @@ -0,0 +1,8 @@ +import Config
+
+config :tronglessh,
+ key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
+ ssh_port: 42069,
+ max_sessions: 255
+
+import_config "#{config_env()}.exs"
\ No newline at end of file diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..1c00233 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1 @@ +import Config
\ No newline at end of file diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..1c00233 --- /dev/null +++ b/config/prod.exs @@ -0,0 +1 @@ +import Config
\ No newline at end of file diff --git a/lib/ssh/conn_cli_manager.ex b/lib/ssh/conn_cli_manager.ex new file mode 100644 index 0000000..19360f8 --- /dev/null +++ b/lib/ssh/conn_cli_manager.ex @@ -0,0 +1,143 @@ +defmodule TrongleSSH.SSH.ConnectionCLIManager do + alias TrongleSSH.SSH.{Session, Tui} + alias IO.ANSI + require Logger + + @behaviour :ssh_server_channel + @session_closed_message [ + ANSI.clear(), + ["This session has been closed."] + ] + + defmodule State do + defstruct channel_id: nil, + width: nil, + height: nil, + client_pid: nil, + connection_ref: nil, + session: %Session{} + end + + def init([%State{} = init_state]) do + {:ok, init_state} + end + + def handle_msg({:ssh_channel_up, channel_id, connection_ref}, %State{} = state) do + connected_user = + :ssh.connection_info(connection_ref) + |> Keyword.fetch!(:user) + |> String.Chars.to_string() + + {:ok, + %State{ + state + | channel_id: channel_id, + connection_ref: connection_ref, + session: %Session{username: connected_user} + }} + end + + def handle_msg( + {:EXIT, client_pid, _reason}, + %State{client_pid: client_pid, channel_id: channel_id} = state + ) do + send(client_pid, :quit) + {:stop, channel_id, state} + end + + def handle_msg( + {:send_data, data}, + %State{connection_ref: connection_ref, channel_id: channel_id} = state + ) do + :ssh_connection.send(connection_ref, channel_id, data) + {:ok, state} + end + + def handle_msg( + :session_closed, + %State{connection_ref: connection_ref, channel_id: channel_id} = state + ) do + :ssh_connection.send(connection_ref, channel_id, @session_closed_message) + {:stop, channel_id, state} + end + + def handle_msg(msg, term) do + Logger.debug("Unknown msg #{inspect(msg)}, #{inspect(term)}") + end + + def handle_ssh_msg( + {:ssh_cm, _connection_handler, {:data, _channel_id, _type, data}}, + %State{} = state + ) do + send(state.client_pid, {:data, data}) + {:ok, state} + end + + def handle_ssh_msg( + {:ssh_cm, connection_handler, + {:pty, channel_id, want_reply?, {_term, width, height, _pixwidth, _pixheight, _opts}}}, + %State{} = state + ) do + :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id) + + {:ok, + %{ + state + | width: width, + height: height + }} + end + + def handle_ssh_msg( + {:ssh_cm, connection_handler, {:env, channel_id, want_reply?, _var, _value}}, + state + ) do + :ssh_connection.reply_request(connection_handler, want_reply?, :failure, channel_id) + {:ok, state} + end + + def handle_ssh_msg( + {:ssh_cm, _connection_handler, + {:window_change, _channel_id, width, height, _pixwidth, _pixheight}}, + %State{client_pid: client_pid} = state + ) do + send(client_pid, {:resize, {width, height}}) + + {:ok, + %State{ + state + | width: width, + height: height + }} + end + + def handle_ssh_msg( + {:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}}, + %State{width: width, height: height, session: session} = state + ) do + :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id) + + {:ok, client_pid} = + GenServer.start_link(Tui, [ + %Tui.State{ + connection_manager_pid: self(), + width: width, + height: height, + session: session + } + ]) + + send(client_pid, :refresh) + {:ok, %State{state | client_pid: client_pid}} + end + + def handle_ssh_msg( + msg, + %State{channel_id: channel_id} = state + ) do + Logger.debug("unknown ssh #{inspect(msg)}") + {:stop, channel_id, state} + end + + def terminate(_reason, _state), do: :ok +end diff --git a/lib/ssh/daemon.ex b/lib/ssh/daemon.ex new file mode 100644 index 0000000..34cd4b9 --- /dev/null +++ b/lib/ssh/daemon.ex @@ -0,0 +1,43 @@ +defmodule TrongleSSH.SSH.Daemon do + alias TrongleSSH.SSH.ConnectionCLIManager + use GenServer + require Logger + + defmodule State do + defstruct daemon_pid: nil + end + + def start_link(_) do + GenServer.start_link(__MODULE__, %State{}) + end + + def init(%State{} = state) do + send(self(), :start) + + {:ok, state} + end + + def handle_info(:start, %State{} = state) do + port = Application.fetch_env!(:tronglessh, :ssh_port) + key_dir = String.to_charlist(Application.fetch_env!(:tronglessh, :key_dir)) + + case :ssh.daemon( + port, + system_dir: key_dir, + ssh_cli: {ConnectionCLIManager, [%ConnectionCLIManager.State{}]}, + no_auth_needed: true, + id_string: :random, + parallel_login: true, + subsystems: [] + ) do + {:ok, daemon_pid} -> + Logger.info("SSH server started on port #{port}, on #{inspect(daemon_pid)}") + {:noreply, %State{state | daemon_pid: daemon_pid}} + + {:error, err} -> + raise inspect(err) + end + end + + def handle_info(_, state), do: {:ok, state} +end diff --git a/lib/ssh/screen.ex b/lib/ssh/screen.ex new file mode 100644 index 0000000..8f0c7bd --- /dev/null +++ b/lib/ssh/screen.ex @@ -0,0 +1,18 @@ +defmodule TrongleSSH.SSH.Screen do + @callback render(width :: integer(), height :: integer(), state :: any()) :: any() + @callback input(width :: integer(), height :: integer(), action :: any(), state :: any()) :: + any() + + defmacro __using__(_) do + quote do + @behaviour TrongleSSH.SSH.Screen + use GenServer + + def handle_info({:render, width, height}, state), + do: {:noreply, render(width, height, state)} + + def handle_info({:input, width, height, action}, state), + do: {:noreply, input(width, height, action, state)} + end + end +end diff --git a/lib/ssh/session.ex b/lib/ssh/session.ex new file mode 100644 index 0000000..d662e59 --- /dev/null +++ b/lib/ssh/session.ex @@ -0,0 +1,3 @@ +defmodule TrongleSSH.SSH.Session do + defstruct username: "" +end diff --git a/lib/ssh/tui.ex b/lib/ssh/tui.ex new file mode 100644 index 0000000..4fbbcf5 --- /dev/null +++ b/lib/ssh/tui.ex @@ -0,0 +1,25 @@ +defmodule TrongleSSH.SSH.Tui do + alias TrongleSSH.SSH.Session + + defmodule State do + defstruct connection_manager_pid: nil, + width: nil, + height: nil, + session: %Session{} + end + + use TrongleSSH.SSH.Screen + + def render(width, height, %State{connection_manager_pid: connection_manager_pid} = state) do + send( + connection_manager_pid, + {:send_to_ssh, render_state(width, height, state)} + ) + + state + end + + def input(width, height, action, %State{} = state) do + state + end +end diff --git a/lib/trongle_ssh.ex b/lib/trongle_ssh.ex new file mode 100644 index 0000000..a872149 --- /dev/null +++ b/lib/trongle_ssh.ex @@ -0,0 +1,11 @@ +defmodule TrongleSSH.Application do + use Application + + def start(_, _) do + children = [ + TrongleSSH.SSH.Daemon + ] + + Supervisor.start_link(children, strategy: :one_for_one, name: TrongleSSH.Supervisor) + end +end @@ -0,0 +1,26 @@ +defmodule TrongleSSH.MixProject do + use Mix.Project + + def project do + [ + app: :tronglessh, + version: "0.1.0", + elixir: "~> 1.15", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + mod: {TrongleSSH.Application, []}, + extra_applications: [:logger, :crypto, :ssh, :inets] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [] + end +end diff --git a/priv/.gitignore b/priv/.gitignore new file mode 100644 index 0000000..2744c99 --- /dev/null +++ b/priv/.gitignore @@ -0,0 +1 @@ +keys/
\ No newline at end of file diff --git a/priv/make_keys.sh b/priv/make_keys.sh new file mode 100644 index 0000000..8d8a872 --- /dev/null +++ b/priv/make_keys.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +if [ ! -d "keys" ] +then + mkdir keys + chmod 700 keys + cd keys + + ssh-keygen -N "" -b 256 -t ecdsa -f ssh_host_ecdsa_key + ssh-keygen -N "" -b 1024 -t dsa -f ssh_host_dsa_key + ssh-keygen -N "" -b 2048 -t rsa -f ssh_host_rsa_key +fi
\ No newline at end of file diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test/trongle_ssh_test.exs b/test/trongle_ssh_test.exs new file mode 100644 index 0000000..c4c2a15 --- /dev/null +++ b/test/trongle_ssh_test.exs @@ -0,0 +1,8 @@ +defmodule TrongleSSHTest do + use ExUnit.Case + doctest TrongleSSH + + test "greets the world" do + assert TrongleSSH.hello() == :world + end +end |
