Repository: rubencaro/sshex
Branch: master
Commit: e22f405c4485
Files: 12
Total size: 35.1 KB
Directory structure:
gitextract_8d5oz6yg/
├── .gitignore
├── .tool-versions
├── .travis.yml
├── LICENSE
├── README.md
├── lib/
│ ├── sshex/
│ │ ├── configurable_client_keys.ex
│ │ └── helpers.ex
│ └── sshex.ex
├── mix.exs
└── test/
├── configurable_client_keys_test.exs
├── sshex_test.exs
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/_build
/deps
erl_crash.dump
*.ez
*.swp
*.kate-swp
.kateproject.d
.zedstate
.directory
.elixir_ls
/config/prod.exs
/rel
/doc
================================================
FILE: .tool-versions
================================================
elixir 1.8.1-otp-21
erlang 21.2.5
================================================
FILE: .travis.yml
================================================
language: elixir
elixir:
- 1.8
otp_release:
21.0
sudo: false
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014 Rubén Caro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
----
The actively maintained version of SSHEx is now in https://github.com/witchtails/sshex
This repo has been archived.
----
# SSHEx
[](https://travis-ci.org/rubencaro/sshex)
[](https://hex.pm/packages/sshex)
[](https://hex.pm/packages/sshex)
Simple SSH helpers for Elixir.
Library to unify helpers already used on several applications. It uses low level Erlang [ssh library](http://www.erlang.org/doc/man/ssh.html).
The only purpose of these helpers is to avoid repetitive patterns seen when working with SSH from Elixir. It doesn't mean to hide anything from the venerable code underneath. If there's an ugly crash from `:ssh` it will come back as `{:error, reason}`.
## Installation
Just add `{:sshex, "2.2.1"}` to your deps on `mix.exs` and run `mix deps.get`
## Use
Then assuming `:ssh` application is already started with `:ssh.start` (hence it is listed on deps), you should acquire an SSH connection using `SSHEx.connect/1` like this:
1.You can use `ssh-copy-id myuser@123.123.123.123.123` to copy ssh key to remote host and then connect using this line:
```elixir
{:ok, conn} = SSHEx.connect ip: '123.123.123.123', user: 'myuser'
```
2.You can supply the password:
```elixir
{:ok, conn} = SSHEx.connect ip: '123.123.123.123', user: 'myuser', password: 'your-password'
```
Then you can use the acquired `conn` with the `cmd!/4` helper like this:
```elixir
SSHEx.cmd! conn, 'mkdir -p /path/to/newdir'
res = SSHEx.cmd! conn, 'ls /some/path'
```
This is meant to run commands which you don't care about the return code. `cmd!/3` will return the output of the command only, and __will raise any errors__. If you want to check the status code, and control errors too, you can use `run/3` like this:
```elixir
{:ok, _, 0} = SSHEx.run conn, 'rm -fr /something/to/delete'
{:ok, res, 0} = SSHEx.run conn, 'ls /some/path'
{:error, reason} = SSHEx.run failing_conn, 'ls /some/path'
```
You can pass the option `:separate_streams` to get separated stdout and stderr. Like this:
```elixir
{:ok, stdout, stderr, 2} = SSHEx.run conn, 'ls /nonexisting/path', separate_streams: true
```
You will be reusing the same SSH connection all over.
## Streaming
You can use `SSHEx` to run some command and create a [`Stream`](http://elixir-lang.org/docs/stable/elixir/Stream.html), so you can lazily process an arbitrarily long output as it arrives. Internally `Stream.resource/3` is used to create the `Stream`, and every response from `:ssh` is emitted so it can be easily matched with a simple `case`.
You just have to use `stream/3` like this:
```elixir
str = SSHEx.stream conn, 'somecommand'
Enum.each(str, fn(x)->
case x do
{:stdout, row} -> process_stdout(row)
{:stderr, row} -> process_stderr(row)
{:status, status} -> process_exit_status(status)
{:error, reason} -> process_error(reason)
end
end)
```
## Alternative keys
To use alternative keys you should save them somewhere on disk and then set the `:user_dir` option for `SSHEx.connect/4`. See [ssh library docs](http://www.erlang.org/doc/man/ssh.html) for more options.
## TODOs
* Add tunnelling helpers [*](http://erlang.org/pipermail/erlang-questions/2014-June/079481.html)
## Changelog
### 2.2.1
* Add new possible exit_signal message format
### 2.2.0
* Add ability to specify ssh key and known hosts
* Make it Elixir string friendly
### 2.1.2
* Remove Elixir 1.4 warnings
### 2.1.1
* Close channel after stream
### 2.1.0
* Add `connect/1` to improve testability by easier mocking
### 2.0.1
* Avoid some Elixir 1.2.0 warnings
* Adjust the SSH flow control window to handle long outputs (fixes #4).
### 2.0.0
Backwards incompatible changes:
* Remove every `raise`, get clean controlled `{:error, reason}` responses
* Put every optional parameter under a unique Keyword list
### 1.3.1
* Fix Elixir version requested. Use >= 1.0 now.
### 1.3
* Support streaming via `stream/3`
* Stop using global mocks (i.e. `:meck`)
### 1.2
* Uniform `raise` behaviour on `:ssh` errors.
* Document and test `:ssh` error handling
### 1.1
* Add support for separate stdout/stderr responses.
### 1.0
* Initial release
================================================
FILE: lib/sshex/configurable_client_keys.ex
================================================
defmodule SSHEx.ConfigurableClientKeys do
@moduledoc ~S"""
Provides public key behavior for SSH clients.
valid options:
- `key`: `IO.device` providing the ssh key (required)
- `known_hosts`: `IO.device` providing the known hosts list (required)
- `accept_hosts`: `boolean` silently accept and add new hosts to the known hosts. By default only known hosts will be accepted.
`
SSHEx.connect(
ip: to_charlist(hostname),
user: to_charlist(username),
key_cb: {SSHEx.ConfigurableClientKeys, [
key: <IO.device>,
known_hosts: <IO.device> ]}
)
`
A convenience method is provided that can take filenames instead of IO devices
`
cb_module = SSHEx.ConfigurableClientKeys.get_cb_module(key_file: "path/to/keyfile", known_hosts_file: "path_to_known_hostsFile", accept_hosts: false)
SSHEx.connect(
ip: to_charlist(hostname),
user: to_charlist(username),
key_cb: cb_module
)
`
"""
@behaviour :ssh_client_key_api
@spec add_host_key(hostname :: charlist, key :: :public_key.public_key , opts :: list) :: :ok | {:error, term}
def add_host_key(hostname, key, opts) do
case accept_hosts(opts) do
true ->
opts
|> known_hosts
|> IO.read(:all)
|> :public_key.ssh_decode(:known_hosts)
|> (fn decoded -> decoded ++ [{key, [{:hostnames, [hostname]}]}] end).()
|> :public_key.ssh_encode(:known_hosts)
|> (fn encoded -> IO.write(known_hosts(opts), encoded) end).()
_ ->
message =
"""
Error: unknown fingerprint found for #{inspect hostname} #{inspect key}.
You either need to add a known good fingerprint to your known hosts file for this host,
*or* pass the accept_hosts option to your client key callback
"""
{:error, message}
end
end
@spec is_host_key(key :: :public_key.public_key, hostname :: charlist, alg :: :ssh_client_key_api.algorithm, opts :: list) :: boolean
def is_host_key(key, hostname, _alg, opts) do
opts
|> known_hosts
|> IO.read(:all)
|> :public_key.ssh_decode(:known_hosts)
|> has_fingerprint(key, hostname)
end
@spec user_key(alg :: :ssh_client_key_api.algorithm, opts :: list) :: {:error, term} | {:ok, :public_key.private_key}
def user_key(_alg, opts) do
material =
opts
|> key
|> IO.read(:all)
|> :public_key.pem_decode
|> List.first
|> :public_key.pem_entry_decode
{:ok, material}
end
@spec get_cb_module(opts :: list) :: {atom, list}
def get_cb_module(opts) do
opts =
opts
|> Keyword.put(:key, File.open!(opts[:key_file]))
|> Keyword.put(:known_hosts, File.open!(opts[:known_hosts_file]))
{__MODULE__, opts}
end
@spec key(opts :: list) :: IO.device
defp key(opts) do
cb_opts(opts)[:key]
end
@spec accept_hosts(opts :: list) :: boolean
defp accept_hosts(opts) do
cb_opts(opts)[:accept_hosts]
end
@spec known_hosts(opts :: list) :: IO.device
defp known_hosts(opts) do
cb_opts(opts)[:known_hosts]
end
@spec cb_opts(opts :: list) :: list
defp cb_opts(opts) do
opts[:key_cb_private]
end
defp has_fingerprint(fingerprints, key, hostname) do
Enum.any?(fingerprints,
fn {k, v} -> (k == key) && (Enum.member?(v[:hostnames], hostname)) end
)
end
end
================================================
FILE: lib/sshex/helpers.ex
================================================
defmodule SSHEx.Helpers do
@moduledoc """
require SSHEx.Helpers, as: H # the cool way
"""
@doc """
Convenience to get environment bits. Avoid all that repetitive
`Application.get_env( :myapp, :blah, :blah)` noise.
"""
def env(key, default \\ nil), do: env(Mix.Project.get!.project[:app], key, default)
def env(app, key, default), do: Application.get_env(app, key, default)
@doc """
Spit to output any passed variable, with location information.
If `sample` option is given, it should be a float between 0.0 and 1.0.
Output will be produced randomly with that probability.
Given `opts` will be fed straight into `inspect`. Any option accepted by it should work.
"""
defmacro spit(obj \\ "", opts \\ []) do
quote do
opts = unquote(opts)
obj = unquote(obj)
opts = Keyword.put(opts, :env, __ENV__)
SSHEx.Helpers.maybe_spit(obj, opts, opts[:sample])
obj # chainable
end
end
@doc false
def maybe_spit(obj, opts, nil), do: do_spit(obj, opts)
def maybe_spit(obj, opts, prob) when is_float(prob) do
if :rand.uniform <= prob, do: do_spit(obj, opts)
end
defp do_spit(obj, opts) do
%{file: file, line: line} = opts[:env]
name = Process.info(self())[:registered_name]
chain = [ :bright, :red, "\n\n#{file}:#{line}", :normal, "\n #{inspect self()}", :green," #{name}"]
msg = inspect(obj, opts)
chain = chain ++ [:red, "\n\n#{msg}"]
(chain ++ ["\n\n", :reset]) |> IO.ANSI.format(true) |> IO.puts
end
@doc """
Print to stdout a _TODO_ message, with location information.
"""
defmacro todo(msg \\ "") do
quote do
%{file: file, line: line} = __ENV__
[ :yellow, "\nTODO: #{file}:#{line} #{unquote(msg)}\n", :reset]
|> IO.ANSI.format(true)
|> IO.puts
:todo
end
end
@doc """
Apply given defaults to given Keyword. Returns merged Keyword.
The inverse of `Keyword.merge`, best suited to apply some defaults in a
chainable way.
Ex:
kw = gather_data
|> transform_data
|> H.defaults(k1: 1234, k2: 5768)
|> here_i_need_defaults
Instead of:
kw1 = gather_data
|> transform_data
kw = [k1: 1234, k2: 5768]
|> Keyword.merge(kw1)
|> here_i_need_defaults
"""
def defaults(args, defs) do
defs |> Keyword.merge(args)
end
def convert_values(args) do
Enum.map(args, fn {k, v} -> {k, convert_value(v)} end)
end
def convert_value(v) when is_binary(v) do
String.to_charlist(v)
end
def convert_value(v), do: v
end
================================================
FILE: lib/sshex.ex
================================================
require SSHEx.Helpers, as: H
defmodule SSHEx do
@moduledoc """
Module to deal with SSH connections. It uses low level erlang
[ssh library](http://www.erlang.org/doc/man/ssh.html).
:ssh.start # just in case
{:ok, conn} = SSHEx.connect ip: '123.123.123.123', user: 'myuser'
"""
@doc """
Establish a connection with given options. Uses `:ssh.connect/4` for that.
Recognised options are `ip` (mandatory), `port` and `negotiation_timeout`.
Any other option is passed to `:ssh.connect/4` as is
(so be careful if you use binaries and `:ssh` expects char lists...).
See [its reference](http://erlang.org/doc/man/ssh.html#connect-4) for available options.
Default values exist for some options, which are:
* `port`: 22
* `negotiation_timeout`: 5000
* `silently_accept_hosts`: `true`
Returns `{:ok, connection}`, or `{:error, reason}`.
"""
def connect(opts) do
opts =
opts
|> H.convert_values
|> H.defaults(port: 22,
negotiation_timeout: 5000,
silently_accept_hosts: true,
ssh_module: :ssh)
own_keys = [:ip, :port, :negotiation_timeout, :ssh_module]
ssh_opts = opts |> Enum.filter(fn({k,_})-> not (k in own_keys) end)
opts[:ssh_module].connect(opts[:ip], opts[:port], ssh_opts, opts[:negotiation_timeout])
end
@doc """
Gets an open SSH connection reference (as returned by `:ssh.connect/4`),
and a command to execute.
Optionally it gets a `channel_timeout` for the underlying SSH channel opening,
and an `exec_timeout` for the execution itself. Both default to 5000ms.
Returns `{:ok,data,status}` on success. Otherwise `{:error, details}`.
If `:separate_streams` is `true` then the response on success looks like `{:ok,stdout,stderr,status}`.
Ex:
```
{:ok, _, 0} = SSHEx.run conn, 'rm -fr /something/to/delete'
{:ok, res, 0} = SSHEx.run conn, 'ls /some/path'
{:error, reason} = SSHEx.run failing_conn, 'ls /some/path'
{:ok, stdout, stderr, 2} = SSHEx.run conn, 'ls /nonexisting/path', separate_streams: true
```
"""
def run(conn, cmd, opts \\ []) do
opts =
opts
|> H.convert_values
|> H.defaults(connection_module: :ssh_connection,
channel_timeout: 5000,
exec_timeout: 5000)
cmd = H.convert_value(cmd)
case open_channel_and_exec(conn, cmd, opts) do
{:error, r} -> {:error, r}
chn -> get_response(conn, chn, opts[:exec_timeout], "", "", nil, false, opts)
end
end
@doc """
Convenience function to run `run/3` and get output string straight from it,
like `:os.cmd/1`.
See `run/3` for options.
Returns `response` only if `run/3` return value matches `{:ok, response, _}`,
or returns `{stdout, stderr}` if `run/3` returns `{:ok, stdout, stderr, _}`.
Raises any `{:error, details}` returned by `run/3`. Note return status from
`cmd` is also ignored.
Ex:
```
SSHEx.cmd! conn, 'mkdir -p /path/to/newdir'
res = SSHEx.cmd! conn, 'ls /some/path'
```
"""
def cmd!(conn, cmd, opts \\ []) do
case run(conn, cmd, opts) do
{:ok, response, _} -> response
{:ok, stdout, stderr, _} -> {stdout, stderr}
any -> raise inspect(any)
end
end
@doc """
Gets an open SSH connection reference (as returned by `:ssh.connect/4`),
and a command to execute.
See `run/3` for options.
Returns a `Stream` that you can use to lazily retrieve each line of output
for the given command.
Each iteration of the stream will read from the underlying connection and
return one of these:
* `{:stdout,row}`
* `{:stderr,row}`
* `{:status,status}`
* `{:error,reason}`
Keep in mind that rows may not be received in order.
Ex:
```
{:ok, conn} = :ssh.connect('123.123.123.123', 22,
[ {:user,'myuser'}, {:silently_accept_hosts, true} ], 5000)
str = SSHEx.stream conn, 'somecommand'
Stream.each(str, fn(x)->
case x do
{:stdout,row} -> process_stdout(row)
{:stderr,row} -> process_stderr(row)
{:status,status} -> process_exit_status(status)
{:error,reason} -> process_error(row)
end
end)
```
"""
def stream(conn, cmd, opts \\ []) do
opts =
opts
|> H.convert_values
|> H.defaults(connection_module: :ssh_connection,
channel_timeout: 5000,
exec_timeout: 5000)
cmd = H.convert_value(cmd)
start_fun = fn-> open_channel_and_exec(conn,cmd,opts) end
next_fun = fn(input)->
case input do
:halt_next -> {:halt, 'Halt requested on previous iteration'}
{:error, _} = x -> {[x], :halt_next} # emit error, then halt
chn -> do_stream_next(conn, chn, opts)
end
end
after_fun = fn(channel) ->
:ok = opts[:connection_module].close(conn, channel)
end
Stream.resource start_fun, next_fun, after_fun
end
# Actual mapping of `:ssh` responses into streamable chunks
#
defp do_stream_next(conn, channel, opts) do
case receive_and_parse_response(conn, channel, opts[:connection_module], opts[:exec_timeout]) do
{:loop, {_, _, "", "", nil, false}} -> {[], channel}
{:loop, {_, _, x, "", nil, false}} -> {[ {:stdout,x} ], channel}
{:loop, {_, _, "", x, nil, false}} -> {[ {:stderr,x} ], channel}
{:loop, {_, _, "", "", x, false}} -> {[ {:status,x} ], channel}
{:loop, {_, _, "", "", nil, true }} -> {:halt, channel}
{:error, _} = x -> {[x], :halt_next} # emit error, then halt
end
end
# Try to get the channel, and then execute the given command.
# Just a DRY to call internal `open_channel/3` and `exec/5`.
#
defp open_channel_and_exec(conn, cmd, opts) do
case open_channel(conn, opts[:channel_timeout], opts[:connection_module]) do
{:error, r} -> {:error, r}
{:ok, chn} -> exec(chn, conn, cmd, opts[:exec_timeout], opts[:connection_module])
end
end
# Try to get the channel
#
defp open_channel(conn, channel_timeout, connection_module) do
connection_module.session_channel(conn, channel_timeout)
end
# Execute the given command. Map every error to `{:error,reason}`.
#
defp exec(channel, conn, cmd, exec_timeout, connection_module) do
case connection_module.exec(conn, channel, cmd, exec_timeout) do
:success -> channel
:failure -> {:error, "Could not exec '#{cmd}'!"}
any -> any # {:error, reason}
end
end
# Loop until all data is received. Return read data and the exit_status.
#
defp get_response(conn, channel, timeout, stdout, stderr, status, closed, opts) do
# if we got status and closed, then we are done
parsed = case {status, closed} do
{st, true} when not is_nil(st) -> format_response({:ok, stdout, stderr, status}, opts)
_ -> receive_and_parse_response(conn, channel, opts[:connection_module],
timeout, stdout, stderr, status, closed)
end
# tail recursion
case parsed do
{:loop, {ch, tout, out, err, st, cl}} -> # loop again, still things missing
get_response(conn, ch, tout, out, err, st, cl, opts)
x -> x
end
end
# Parse ugly response
#
defp receive_and_parse_response(conn, chn, connection_module, tout,
stdout \\ "", stderr \\ "", status \\ nil, closed \\ false) do
response = receive do
{:ssh_cm, ^conn, res} -> res
after
tout -> {:error, "Timeout. Did not receive data for #{tout}ms."}
end
# call adjust_window to allow more data income, but only when needed
case response do
{:data, ^chn, _, new_data} -> connection_module.adjust_window(conn, chn, byte_size(new_data))
_ -> :ok
end
case response do
{:data, ^chn, 1, new_data} -> {:loop, {chn, tout, stdout, stderr <> new_data, status, closed}}
{:data, ^chn, 0, new_data} -> {:loop, {chn, tout, stdout <> new_data, stderr, status, closed}}
{:eof, ^chn} -> {:loop, {chn, tout, stdout, stderr, status, closed}}
{:exit_signal, ^chn, _, _} -> {:loop, {chn, tout, stdout, stderr, status, closed}}
{:exit_signal, ^chn, _, _, _} -> {:loop, {chn, tout, stdout, stderr, status, closed}}
{:exit_status, ^chn, new_status} -> {:loop, {chn, tout, stdout, stderr, new_status, closed}}
{:closed, ^chn} -> {:loop, {chn, tout, stdout, stderr, status, true}}
any -> any # {:error, reason}
end
end
# Format response for given raw response and given options
#
defp format_response(raw, opts) do
case opts[:separate_streams] do
true -> raw
_ -> {:ok, stdout, stderr, status} = raw
{:ok, stdout <> stderr, status}
end
end
end
================================================
FILE: mix.exs
================================================
defmodule SSHEx.Mixfile do
use Mix.Project
def project do
[app: :sshex,
version: "2.2.1",
elixir: ">= 1.0.0",
package: package(),
deps: deps(),
description: "Simple SSH helpers for Elixir" ]
end
def application do
[ applications: [:ssh] ]
end
defp package do
[maintainers: ["Rubén Caro"],
licenses: ["MIT"],
links: %{github: "https://github.com/rubencaro/sshex"}]
end
defp deps, do: [{:ex_doc, ">= 0.0.0", only: :dev}]
end
================================================
FILE: test/configurable_client_keys_test.exs
================================================
defmodule SSHEx.ConfigurableClientKeysTest do
use ExUnit.Case
alias SSHEx.ConfigurableClientKeys
@key """
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAr7CylYwuNUCYjOV7uj0X8ZRVVKlwhKtavL0vtmCiSes/TQ+u
bQb7787djy4fILh/9ALsvOs6mzmAI9Ye6CxwG2nPhbweD76K/92cAAvbcwWFhKTS
6q2MY6XhATcORqQmYhi6JToYkz51JFeG0k38TwyiIBaLe4yKTCnZ6F0tIB9szdR8
pNOoZTMDAjXRDA1T0Y1wgxXn5dCFR4ywDcphRTu18FWhulruyPGQjRjFRzZCF8rO
PYRCVBaWCIQ9Guj6VnAOaPH3tIkkdTxAeMigflCsCbFttbKLSVbi+woQrOnpQt1G
T9YPaZed9vuXXJMI5IzUacoveyqr/X5cOfdVjwIDAQABAoIBAQCWJHRJt1WZ7s0v
w8H8A8/dhT1zL6ZXyrStjSQkQOsQLrmXGqqexBQz+V6AyRKS/PlkR8eXH5OjKf2n
IoqhMbDQzJkrmfs6y0SwqutxYrC02GglVlJledD7K7xhNHK/zfJ7bNRPkhmEZCDp
4N74BOt1hr9amsmy2QUrV6zAljhFNQtGXlMIzjSwkhAO5RyVDdNUIuXVbK4vYQ8n
60LAFUqrNMU0H5P7N1I9Wv1XdVroQ23bZkkoQ2YNg3+Xt8/ma1R8ImliODq593EF
EWDdtcm6gUeKfZSmu3V3XLX2w03/xng9PiQJWhDyGiiqylLWw61V72BOfk8Lsq03
bCI8HP5pAoGBANmMlI31H1E6zNrJ5/1+2IETRLJhwhCE5SJ14UgHpGjzWpCRVoi1
KY+N6FfUgReMn6efFtJj59cKUxkODDr4yt+8mfM0evfIUJumQ56czDv5l2m3dnm1
0JOX4jIwetFv0ROrbk7EONkg4BYLV3OFlekB+iNR0cW3mtPM/x35hvbbAoGBAM6+
Icf5pDwvU9tqgBlbYdN9D8IJa2kTC2818XAeEBcnDsQNrpKNGfG7jcOb7Os7Qs4q
ArIrfaWKH5OnRv8sxEJzvR1yQ7zfN0qL5+FgI95/5GmNzmiIOMIRltszuqvXhEbf
VfxoEOrYidiliI1P2oSkaSHnKh06vTcX/Iuve3hdAoGAPLahFuUb8l2Iol7K4dIu
tgccmvPxZw7Pq8heMO4BElEoK0SEc+6rRKcD+s8Rn/Lc87jQc7LyFu+ItWtYOnUI
mVxXUqqIzvIWnPnP0UpNLUfA2/4ZkGoPZcFznTIudJjSLr0fMdhNTTuBjmVn6JOV
fMvSdVz2QEm3afjCEil7YxUCgYEAkSPH4XUvyJTNQTfGUIbn6apdurIUNwMIvv1W
z4g7cZWY9yhHy1jFwwARqSa5L/c9kjDKDb0ci2+pdWY1IIWUDrbkKF0Ekv79+Ra5
Jm7xH44Xk8bbBmXDuvLQPnlVbrhxg7Pc0MNaRRTZyT+E2vgZh49Iw2VfGoAXQCtV
v9blTn0CgYEAoaLnpcDDIAJrSEuYJlmMkCSOLnDaUJ2Gvk7h8J3AKUJqaZKDrYtp
LkmEnkNn9pRguHw5O4t2A2/MPTMMPl9okxWUxmFol6vrLcVWJ7fHKnAgN4VeVdmV
3wC+maU88MNIdY/eZWowKv/3ZzENQAJYVSOoDKRM5prZ4UMml4xIv4c=
-----END RSA PRIVATE KEY-----
"""
@decoded_pem {
:RSAPrivateKey,
:"two-prime",
22178836200351380318740579128076760436035138298677133998095994045880250237512489621454049374968347715235055783881967698436999743499552314220245159073073380722782484638674394343203468456842721969160490509408462854393345405217052033027002266498826680781860593217442724766608969408399340485759397900671701217164071787627996052566922654086785056460976416517428062469453531934201888286810352377976660566632576939435504625681508553755882812006916212863287750386635961610258614389401711948452227087543533788958800504728999838598664499003820759527927048804714300161960621149036100364182740694238727122433513404946091683042703, 65537, 18953722005479038672989358915211180275890260321558970411086292300953891314102903798293741601596842249220589427161410575497215994540174656492260412045190058045697523798132914292381351875465619868574569967505985612493829380502486125604518301719636021034677605693414631216007271459728426119381823980696705221015479522156363868435684783617296463247438499300802671603744372814426304362919053721052072690042881679754394100902833620569116419541999627788678348085500419601780112784642580771348412671226267444389048117048054825692970232535861820613163424630322678634426098569280437755714074716898258095743415742430656778272361, 152768202594113506003906892512748290305531150912607635622225249629386697488954984355599970491458110778989746505533403503662866670470953760749589017642309508310049856672577159050375653128905845977073586607530375373027518020058386877444130805997536867700689067133764524765224528717473435950253382932149817505499, 145179663200449145917521205971590191715095399605877657860535867727166023541655152176425320419506590078056361653718878871175026174125734643334617196310933880264431271402500332782840931546131335362589195212219385352510583753009500018046055463721898551203113792493296805078356511882223907692461773372201488840797, 42634396225740208200122939165023822110993251906428332934533161660153542229168052609425568156747621132302706312254237302317680568273093737646062272192469000823821839244113039031865521701141155727614567329168722486117358203562383020102433014048475659707426385673383785616612854269230955697241777527641182266133, 101920611626859098746040147787461939524540705867934527984274451657219304776355522780797909077026392768989962056944197903228585062565435177154621093200325875415203905670958966858503264102811489825554515502983073693999716921620063267013762696345283281845431778671957714037110407177460667546919659598114321682045, 113504902981879519219294186506942993139718372538463522557610536232722856450238191548259358573620555616453272575244746754527476128589876390856908911294135573670756100331272684920761429283802284667615556476745952741230523406386621965212672405641392988569738039218761014896354457746693861804733309700621570981767,
:asn1_NOVALUE
}
@known_hosts """
github.com,192.30.252.128 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
"""
@host_key {
:RSAPublicKey,
21634204163197213132817109123906975906368888521544012567769262995559431966147970056259890368935740096079275379887017970430632559083119648736096672444000478892100121400122505155635695213610246722639150597148186404829574795017869184029845276838222700401896051725788665083080114314875103545837696279553436341967388240785773957395421170074137268759810304409727303757139265883118481355074238232002946450070460602471201997377623196017810991617729908588802783067540409316213603919494955068312762445623851275603318105356333614840694877780018045461415298887693169943421044958989561462337777142733008105081260079805978461159529,
35
}
@tmp_dir "./test/temp"
setup_all do
File.mkdir_p!(@tmp_dir)
on_exit(fn -> File.rm_rf!(@tmp_dir) end)
end
setup do
{:ok, known_hosts} = StringIO.open(@known_hosts)
{:ok, key} = StringIO.open(@key)
%{
known_hosts: known_hosts,
key: key
}
end
test "add_host_key writes an entry to known hosts if accept_hosts is true" do
{:ok, known_hosts} = StringIO.open("")
ConfigurableClientKeys.add_host_key(
"example.com",
@host_key,
[key_cb_private: [accept_hosts: true, known_hosts: known_hosts]]
)
result = StringIO.flush(known_hosts)
assert result =~ "example.com"
end
test "add_host_key returns an error if accept_hosts is false" do
{:ok, known_hosts} = StringIO.open("")
result = ConfigurableClientKeys.add_host_key(
"example.com",
@host_key,
[key_cb_private: [accept_hosts: false, known_hosts: known_hosts]])
assert {:error, _message} = result
end
test "is_host_key returns true if host and key match known hosts entry", %{known_hosts: known_hosts} do
result = ConfigurableClientKeys.is_host_key(
@host_key,
'github.com',
nil,
[key_cb_private: [accept_hosts: false, known_hosts: known_hosts]])
assert result
end
test "is_host_key returns false if host and key do not match known hosts entry", %{known_hosts: known_hosts} do
result = ConfigurableClientKeys.is_host_key(
@host_key,
'other.com',
nil,
[key_cb_private: [accept_hosts: false, known_hosts: known_hosts]])
refute result
end
test "user key returns the contents of the key option", %{key: key} do
result = ConfigurableClientKeys.user_key(
nil,
[key_cb_private: [key: key]]
)
assert result == {:ok, @decoded_pem}
end
test "get_cb_module returns IO devices for the specified files" do
key_file = Path.join(@tmp_dir, "./foo")
known_hosts_file = Path.join(@tmp_dir, "./bar")
File.touch(key_file)
File.touch(known_hosts_file)
{_module, opts} = ConfigurableClientKeys.get_cb_module([key_file: key_file, known_hosts_file: known_hosts_file ])
assert opts[:key_file]
assert opts[:known_hosts_file]
end
end
================================================
FILE: test/sshex_test.exs
================================================
defmodule SSHExTest do
use ExUnit.Case
test "connect" do
opts = [ip: '123.123.123.123',
user: 'myuser',
ssh_module: AllOKMock]
assert SSHEx.connect(opts) == {:ok, :mocked}
end
test "Plain `cmd!`" do
# send mocked response sequence to the mailbox
mocked_data = "output"
status = 123 # any would do
send_regular_sequence mocked_data, status
# actually test it
assert SSHEx.cmd!(:mocked, 'somecommand', connection_module: AllOKMock) == mocked_data
end
test "String `cmd!`" do
# send mocked response sequence to the mailbox
mocked_data = "output"
status = 123 # any would do
send_regular_sequence mocked_data, status
# actually test it
assert SSHEx.cmd!(:mocked, "somecommand", connection_module: AllOKMock) == mocked_data
end
test "Plain `run`" do
# send mocked response sequence to the mailbox
mocked_data = "output"
status = 123 # any would do
send_regular_sequence mocked_data, status
assert SSHEx.run(:mocked, 'somecommand', connection_module: AllOKMock) == {:ok, mocked_data, status}
end
test "String `run`" do
# send mocked response sequence to the mailbox
mocked_data = "output"
status = 123 # any would do
send_regular_sequence mocked_data, status
assert SSHEx.run(:mocked, "somecommand", connection_module: AllOKMock) == {:ok, mocked_data, status}
end
test "Stream long response" do
lines = ["some", "long", "output", "sequence"]
send_long_sequence(lines)
response = Enum.map(lines,&( {:stdout,&1} )) ++ [
{:stderr,"mockederror"},
{:status, 0}
]
str = SSHEx.stream :mocked, 'somecommand', connection_module: AllOKMock
assert Enum.to_list(str) == response
end
test "Separate streams" do
# send mocked response sequence to the mailbox
mocked_stdout = "output"
mocked_stderr = "something failed"
send_separated_sequence mocked_stdout, mocked_stderr
# actually test it
res = SSHEx.run :mocked, 'failingcommand', connection_module: AllOKMock, separate_streams: true
assert res == {:ok, mocked_stdout, mocked_stderr, 2}
end
test "`:ssh` error message when `run`" do
send self(), {:ssh_cm, :mocked, {:error, :reason}}
assert SSHEx.run(:mocked, 'somecommand', connection_module: AllOKMock) == {:error, :reason}
end
test "`:ssh` error message when `cmd!`" do
send self(), {:ssh_cm, :mocked, {:error, :reason}}
assert_raise RuntimeError, "{:error, :reason}", fn ->
SSHEx.cmd!(:mocked, 'somecommand', connection_module: AllOKMock)
end
end
test "`:ssh` error message while `stream`" do
lines = ["some", "long", "output", "sequence"]
send_long_sequence(lines, error: true)
response = Enum.map(lines,&( {:stdout,&1} )) ++ [ {:error, :reason} ]
str = SSHEx.stream :mocked, 'somecommand', connection_module: AllOKMock
assert Enum.to_list(str) == response
end
test "`:ssh_connection.exec` failure" do
assert SSHEx.run(:mocked, 'somecommand', connection_module: ExecFailureMock) == {:error, "Could not exec 'somecommand'!"}
str = SSHEx.stream(:mocked, 'somecommand', connection_module: ExecFailureMock)
assert Enum.to_list(str) == [error: "Could not exec 'somecommand'!"]
assert_raise RuntimeError, "{:error, \"Could not exec 'somecommand'!\"}", fn ->
SSHEx.cmd!(:mocked, 'somecommand', connection_module: ExecFailureMock)
end
end
test "`:ssh_connection.exec` error" do
assert SSHEx.run(:mocked, 'somecommand', connection_module: ExecErrorMock) == {:error, :reason}
str = SSHEx.stream(:mocked, 'somecommand', connection_module: ExecErrorMock)
assert Enum.to_list(str) == [error: :reason]
assert_raise RuntimeError, "{:error, :reason}", fn ->
SSHEx.cmd!(:mocked, 'somecommand', connection_module: ExecErrorMock)
end
end
test "`:ssh_connection.session_channel` error" do
assert SSHEx.run(:mocked, 'somecommand', connection_module: SessionChannelErrorMock) == {:error, :reason}
str = SSHEx.stream(:mocked, 'somecommand', connection_module: SessionChannelErrorMock)
assert Enum.to_list(str) == [error: :reason]
assert_raise RuntimeError, "{:error, :reason}", fn ->
SSHEx.cmd!(:mocked, 'somecommand', connection_module: SessionChannelErrorMock)
end
end
test "receive only from given connection" do
# send mocked response sequence to the mailbox for 2 different connections
status = 123 # any would do
mocked_data1 = "output1"
send_regular_sequence mocked_data1, status, conn: :mocked1
mocked_data2 = "output2"
send_regular_sequence mocked_data2, status, conn: :mocked2
# check that we only receive for the one we want
assert SSHEx.cmd!(:mocked2, 'somecommand', connection_module: AllOKMock) == mocked_data2
end
defp send_long_sequence(lines, opts \\ []) do
for l <- lines do
send self(), {:ssh_cm, :mocked, {:data, :mocked, 0, l}}
end
if opts[:error], do: send(self(), {:ssh_cm, :mocked, {:error, :reason}})
send self(), {:ssh_cm, :mocked, {:data, :mocked, 1, "mockederror"}}
send self(), {:ssh_cm, :mocked, {:eof, :mocked}}
send self(), {:ssh_cm, :mocked, {:exit_status, :mocked, 0}}
send self(), {:ssh_cm, :mocked, {:closed, :mocked}}
end
defp send_regular_sequence(mocked_data, status, opts \\ []) do
conn = opts[:conn] || :mocked
send self(), {:ssh_cm, conn, {:data, conn, 0, mocked_data}}
send self(), {:ssh_cm, conn, {:eof, conn}}
send self(), {:ssh_cm, conn, {:exit_status, conn, status}}
send self(), {:ssh_cm, conn, {:closed, conn}}
end
defp send_separated_sequence(mocked_stdout, mocked_stderr) do
send self(), {:ssh_cm, :mocked, {:data, :mocked, 0, mocked_stdout}}
send self(), {:ssh_cm, :mocked, {:data, :mocked, 1, mocked_stderr}}
send self(), {:ssh_cm, :mocked, {:eof, :mocked}}
send self(), {:ssh_cm, :mocked, {:exit_status, :mocked, 2}}
send self(), {:ssh_cm, :mocked, {:closed, :mocked}}
end
end
defmodule AllOKMock do
def connect(_,_,_,_), do: {:ok, :mocked}
def session_channel(conn,_), do: {:ok, conn}
def exec(_,_,_,_), do: :success
def adjust_window(_,_,_), do: :ok
def close(_, _), do: :ok
end
defmodule ExecFailureMock do
def session_channel(_,_), do: {:ok, :mocked}
def exec(_,_,_,_), do: :failure
def adjust_window(_,_,_), do: :ok
def close(_, _), do: :ok
end
defmodule ExecErrorMock do
def session_channel(_,_), do: {:ok, :mocked}
def exec(_,_,_,_), do: {:error, :reason}
def adjust_window(_,_,_), do: :ok
def close(_, _), do: :ok
end
defmodule SessionChannelErrorMock do
def session_channel(_,_), do: {:error, :reason}
def exec(_,_,_,_), do: :success
def adjust_window(_,_,_), do: :ok
def close(_, _), do: :ok
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
gitextract_8d5oz6yg/
├── .gitignore
├── .tool-versions
├── .travis.yml
├── LICENSE
├── README.md
├── lib/
│ ├── sshex/
│ │ ├── configurable_client_keys.ex
│ │ └── helpers.ex
│ └── sshex.ex
├── mix.exs
└── test/
├── configurable_client_keys_test.exs
├── sshex_test.exs
└── test_helper.exs
SYMBOL INDEX (61 symbols across 6 files)
FILE: lib/sshex.ex
class SSHEx (line 3) | defmodule SSHEx
method connect (line 28) | def connect(opts) do
method run (line 64) | def run(conn, cmd, opts \\ []) do
method cmd! (line 96) | def cmd!(conn, cmd, opts \\ []) do
method stream (line 140) | def stream(conn, cmd, opts \\ []) do
method do_stream_next (line 168) | defp do_stream_next(conn, channel, opts) do
method open_channel_and_exec (line 182) | defp open_channel_and_exec(conn, cmd, opts) do
method open_channel (line 191) | defp open_channel(conn, channel_timeout, connection_module) do
method exec (line 197) | defp exec(channel, conn, cmd, exec_timeout, connection_module) do
method get_response (line 207) | defp get_response(conn, channel, timeout, stdout, stderr, status, clos...
method receive_and_parse_response (line 226) | defp receive_and_parse_response(conn, chn, connection_module, tout,
method format_response (line 254) | defp format_response(raw, opts) do
FILE: lib/sshex/configurable_client_keys.ex
class SSHEx.ConfigurableClientKeys (line 1) | defmodule SSHEx.ConfigurableClientKeys
method add_host_key (line 35) | def add_host_key(hostname, key, opts) do
method is_host_key (line 57) | def is_host_key(key, hostname, _alg, opts) do
method user_key (line 66) | def user_key(_alg, opts) do
method get_cb_module (line 78) | def get_cb_module(opts) do
method key (line 87) | defp key(opts) do
method accept_hosts (line 92) | defp accept_hosts(opts) do
method known_hosts (line 97) | defp known_hosts(opts) do
method cb_opts (line 102) | defp cb_opts(opts) do
method has_fingerprint (line 106) | defp has_fingerprint(fingerprints, key, hostname) do
FILE: lib/sshex/helpers.ex
class SSHEx.Helpers (line 1) | defmodule SSHEx.Helpers
method env (line 9) | def env(key, default \\ nil), do: env(Mix.Project.get!.project[:app], ...
method env (line 10) | def env(app, key, default), do: Application.get_env(app, key, default)
method maybe_spit (line 30) | def maybe_spit(obj, opts, nil), do: do_spit(obj, opts)
method do_spit (line 35) | defp do_spit(obj, opts) do
method defaults (line 79) | def defaults(args, defs) do
method convert_values (line 83) | def convert_values(args) do
method convert_value (line 91) | def convert_value(v), do: v
FILE: mix.exs
class SSHEx.Mixfile (line 1) | defmodule SSHEx.Mixfile
method project (line 4) | def project do
method application (line 13) | def application do
method package (line 17) | defp package do
method deps (line 23) | defp deps, do: [{:ex_doc, ">= 0.0.0", only: :dev}]
FILE: test/configurable_client_keys_test.exs
class SSHEx.ConfigurableClientKeysTest (line 1) | defmodule SSHEx.ConfigurableClientKeysTest
FILE: test/sshex_test.exs
class SSHExTest (line 1) | defmodule SSHExTest
method send_long_sequence (line 139) | defp send_long_sequence(lines, opts \\ []) do
method send_regular_sequence (line 152) | defp send_regular_sequence(mocked_data, status, opts \\ []) do
method send_separated_sequence (line 160) | defp send_separated_sequence(mocked_stdout, mocked_stderr) do
class AllOKMock (line 170) | defmodule AllOKMock
method connect (line 171) | def connect(_,_,_,_), do: {:ok, :mocked}
method session_channel (line 172) | def session_channel(conn,_), do: {:ok, conn}
method exec (line 173) | def exec(_,_,_,_), do: :success
method adjust_window (line 174) | def adjust_window(_,_,_), do: :ok
method close (line 175) | def close(_, _), do: :ok
class ExecFailureMock (line 178) | defmodule ExecFailureMock
method session_channel (line 179) | def session_channel(_,_), do: {:ok, :mocked}
method exec (line 180) | def exec(_,_,_,_), do: :failure
method adjust_window (line 181) | def adjust_window(_,_,_), do: :ok
method close (line 182) | def close(_, _), do: :ok
class ExecErrorMock (line 185) | defmodule ExecErrorMock
method session_channel (line 186) | def session_channel(_,_), do: {:ok, :mocked}
method exec (line 187) | def exec(_,_,_,_), do: {:error, :reason}
method adjust_window (line 188) | def adjust_window(_,_,_), do: :ok
method close (line 189) | def close(_, _), do: :ok
class SessionChannelErrorMock (line 192) | defmodule SessionChannelErrorMock
method session_channel (line 193) | def session_channel(_,_), do: {:error, :reason}
method exec (line 194) | def exec(_,_,_,_), do: :success
method adjust_window (line 195) | def adjust_window(_,_,_), do: :ok
method close (line 196) | def close(_, _), do: :ok
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
{
"path": ".gitignore",
"chars": 127,
"preview": "/_build\n/deps\nerl_crash.dump\n*.ez\n*.swp\n*.kate-swp\n.kateproject.d\n.zedstate\n.directory\n.elixir_ls\n\n/config/prod.exs\n\n/re"
},
{
"path": ".tool-versions",
"chars": 34,
"preview": "elixir 1.8.1-otp-21\nerlang 21.2.5\n"
},
{
"path": ".travis.yml",
"chars": 67,
"preview": "language: elixir\nelixir:\n - 1.8\notp_release:\n 21.0\nsudo: false\n"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Rubén Caro\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "README.md",
"chars": 4332,
"preview": "----\nThe actively maintained version of SSHEx is now in https://github.com/witchtails/sshex\n\nThis repo has been archived"
},
{
"path": "lib/sshex/configurable_client_keys.ex",
"chars": 3406,
"preview": "defmodule SSHEx.ConfigurableClientKeys do\n @moduledoc ~S\"\"\"\n Provides public key behavior for SSH clients.\n \n valid "
},
{
"path": "lib/sshex/helpers.ex",
"chars": 2578,
"preview": "defmodule SSHEx.Helpers do\n @moduledoc \"\"\"\n require SSHEx.Helpers, as: H # the cool way\n \"\"\"\n @doc \"\"\"\n Conven"
},
{
"path": "lib/sshex.ex",
"chars": 8914,
"preview": "require SSHEx.Helpers, as: H\n\ndefmodule SSHEx do\n\n @moduledoc \"\"\"\n Module to deal with SSH connections. It uses low "
},
{
"path": "mix.exs",
"chars": 490,
"preview": "defmodule SSHEx.Mixfile do\n use Mix.Project\n\n def project do\n [app: :sshex,\n version: \"2.2.1\",\n elixir: \">="
},
{
"path": "test/configurable_client_keys_test.exs",
"chars": 8082,
"preview": "defmodule SSHEx.ConfigurableClientKeysTest do\n use ExUnit.Case\n\n alias SSHEx.ConfigurableClientKeys\n\n @key \"\"\"\n --"
},
{
"path": "test/sshex_test.exs",
"chars": 6770,
"preview": "defmodule SSHExTest do\n use ExUnit.Case\n\n test \"connect\" do\n opts = [ip: '123.123.123.123',\n user: 'myuser',\n "
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the rubencaro/sshex GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (35.1 KB), approximately 11.4k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.