Skip to content

Instantly share code, notes, and snippets.

@candera
Last active February 9, 2019 07:50
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save candera/11310395 to your computer and use it in GitHub Desktop.
Save candera/11310395 to your computer and use it in GitHub Desktop.
ssh-repl

Embedding an SSH-accessible REPL in a Clojure process

N.B. This is now a library, thanks to the efforts of the wonderful @mtnygard. And the README does a good job of making clear just how terrible an idea it is to actually do this. :)

As any Clojurist knows, the REPL is an incredibly handy development tool. It can also be useful as a tool for debugging running programs. Of course, this raises the question of how to limit access to the REPL to authorized parties. With the Apache SSHD library, you can embed an SSH server in any JVM process. It takes only a little code to hook this up to a REPL, and to limit access either by public key or password.

Start by including a reference to the Apache SSHD library. If you’re using Leiningen, your dependency will look something like this:

[org.apache.sshd/sshd-core "0.11.0"]

The SSHD project is fairly active, so be sure to check for the latest version; version 0.11.0 was current as of April 2014.

You can also use lein try to start an experimental REPL with the latest available SSHD library, like this:

lein try org.apache.sshd/sshd-core org.slf4j/slf4j-simple

On to the code!

We need to start by pulling in a few namespaces and classes:

(import '[org.apache.sshd SshServer]
        '[org.apache.sshd.server Command PasswordAuthenticator PublickeyAuthenticator]
        '[org.apache.sshd.common Factory]
        '[org.apache.sshd.server.keyprovider SimpleGeneratorHostKeyProvider])

(require '[clojure.java.io :as io])

Then we can create a server object, using the defaults for things like hash algorithms and other ssh-y stuff:

(def sshd (SshServer/setUpDefaultServer))

Tell the server what port to run on when it starts:

(.setPort sshd 2022)

Give a path where we can save our host keys, and generate them if they don’t exist. This way, if the server gets restarted, its identity will remain the same.

(.setKeyPairProvider sshd (SimpleGeneratorHostKeyProvider. "hostkey.ser"))

Here’s where the real magic is - we specify a “shell factory”, which knows how to create an instance of Command that will start a REPL on a separate thread. Because this is Java, there’s a whole bunch of state to keep track of, but we can do that in an atom. Note the use of bound-fn*, which lets us preserve our bindings of *in*, *out*, and *err* despite the fact that we’re running the REPL on its own thread.

(.setShellFactory sshd
                  (reify Factory
                    (create [this]
                      (let [state (atom {})]
                        (reify Command
                          (destroy [this]
                            (when-let [fut (:future @state)]
                              (future-cancel fut)))
                          (setErrorStream [this err]
                            (.setNoDelay err true)
                            (swap! state assoc-in [:streams :err] err))
                          (setExitCallback [this cb]
                            (swap! state assoc :exit-callback cb))
                          (setInputStream [this in]
                            (swap! state assoc-in [:streams :in] in))
                          (setOutputStream [this out]
                            (.setNoDelay out true)
                            (swap! state assoc-in [:streams :out] out))
                          (start [this env]
                            (binding [*in* (-> @state
                                               :streams
                                               :in
                                               io/reader
                                               clojure.lang.LineNumberingPushbackReader.)
                                      *out* (-> @state
                                                :streams
                                                :out
                                                io/writer)
                                      *err* (-> @state
                                                :streams
                                                :err
                                                io/writer)]
                              (swap! state
                                     assoc
                                     :future
                                     (future-call (bound-fn* clojure.main/repl))))))))))

We need to configure security, or the sshd server won’t start. For now, let’s use a simple authenticator that just checks that the password is “foo”:

(.setPasswordAuthenticator sshd
                           (reify PasswordAuthenticator
                             (authenticate [this username password session]
                               (= password "foo"))))

Now we simply start the server:

(.start sshd)

At this point, we can do ssh -T -p 2022 localhost, provide the hardcoded password “foo” and we have a REPL! (Note: you will need to type ~. to disconnect.) The -T option prevents the allocation of a pseudo-tty. I have no idea what that means, other than that it lets you see what you type.

Authentication via public key

Passwords - especially passwords embedded in our programs - are less than desirable. Which is why it’s great that SSHD also provides support for public key authentication. It requires a bit of code to go from a SSH public key file to an instance of java.security.PublicKey, but it’s not too bad:

(import '[java.math BigInteger]
        '[java.security KeyFactory PublicKey]
        '[java.security.spec DSAPublicKeySpec RSAPublicKeySpec]
        '[java.util Scanner])

(defn decode-string
  "Decodes a string from a ByteBuffer."
  [bb]
  (let [len (.getInt bb)
        buf (byte-array len)]
    (.get bb buf)
    (String. buf)))

(defn decode-bigint
  "Decodes a java.math.BigInteger from a ByteBuffer."
  [bb]
  (let [len (.getInt bb)
        buf (byte-array len)]
    (.get bb buf)
    (BigInteger. buf)))

(defn read-ssh-key
  "Reads in the SSH key at `path`, returning an instance of
  `java.security.PublicKey`."
  [path]
  (let [contents (slurp path)
        parts    (clojure.string/split contents #" ")
        bytes    (->> parts
                      (filter #(.startsWith % "AAAA"))
                      first
                      javax.xml.bind.DatatypeConverter/parseBase64Binary)
        bb       (-> bytes
                     alength
                     java.nio.ByteBuffer/allocate
                     (.put bytes)
                     .flip)]
    (case (decode-string bb)
      "ssh-rsa" (.generatePublic (KeyFactory/getInstance "RSA")
                                 (let [[e m] (repeatedly 2 #(decode-bigint bb))]
                                      (RSAPublicKeySpec. m e)))
      "ssh-dss" (.generatePublic (KeyFactory/getInstance "DSA")
                                 (let [[p q g y] (repeatedly 4 #(decode-bigint bb))]
                                   (DSAPublicKeySpec. y p q g)))
      (throw (ex-info "Unknown key type"
                      {:reason ::unknown-key-type
                       :type   type})))))

But now that we have it, we can easily use it.

(let [allowed-key (read-ssh-key "/Users/candera/.ssh/id_rsa.pub")]
 (.setPublickeyAuthenticator sshd
                             (reify PublickeyAuthenticator
                               (authenticate [this username key session]
                                 (clojure.tools.logging/info :username username
                                                             :key key)
                                 (and (= username "repl")
                                      (= key allowed-key))))))

And then we can connect with ssh -T -p 2022 repl@localhost, without using a password! Obviously, you will have to change the path to the public key to get this to work for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment