aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2023-10-01 20:41:48 -0600
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2023-10-01 20:41:48 -0600
commitffa3d553f693494c218a0244c946ba83dfbf7cd1 (patch)
tree092a824dad118c37960ab67da98cdbc445a35d2b
downloadtronglessh-ffa3d553f693494c218a0244c946ba83dfbf7cd1.tar.gz
tronglessh-ffa3d553f693494c218a0244c946ba83dfbf7cd1.zip
-rw-r--r--.formatter.exs4
-rw-r--r--.gitignore26
-rw-r--r--.tool-versions1
-rw-r--r--README.md21
-rw-r--r--config/config.exs8
-rw-r--r--config/dev.exs1
-rw-r--r--config/prod.exs1
-rw-r--r--lib/ssh/conn_cli_manager.ex143
-rw-r--r--lib/ssh/daemon.ex43
-rw-r--r--lib/ssh/screen.ex18
-rw-r--r--lib/ssh/session.ex3
-rw-r--r--lib/ssh/tui.ex25
-rw-r--r--lib/trongle_ssh.ex11
-rw-r--r--mix.exs26
-rw-r--r--priv/.gitignore1
-rw-r--r--priv/make_keys.sh12
-rw-r--r--test/test_helper.exs1
-rw-r--r--test/trongle_ssh_test.exs8
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
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..0686586
--- /dev/null
+++ b/mix.exs
@@ -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