* Embedding an SSH-accessible REPL in a Clojure process *N.B.* This is now [[https://github.com/mtnygard/ssh-repl][a library]], thanks to the efforts of the wonderful [[https://twitter.com/mtnygard][@mtnygard]]. And [[https://github.com/mtnygard/ssh-repl/blob/master/README.md][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 [[http://mina.apache.org/sshd-project/index.html][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: #+begin_src clojure [org.apache.sshd/sshd-core "0.11.0"] #+end_src 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 [[https://github.com/rkneufeld/lein-try][lein try]] to start an experimental REPL with the latest available SSHD library, like this: #+begin_src sh lein try org.apache.sshd/sshd-core org.slf4j/slf4j-simple #+end_src On to the code! We need to start by pulling in a few namespaces and classes: #+begin_src clojure :results silent (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]) #+end_src Then we can create a server object, using the defaults for things like hash algorithms and other ssh-y stuff: #+begin_src clojure :results silent (def sshd (SshServer/setUpDefaultServer)) #+end_src Tell the server what port to run on when it starts: #+begin_src clojure :results silent (.setPort sshd 2022) #+end_src 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. #+begin_src clojure :results silent (.setKeyPairProvider sshd (SimpleGeneratorHostKeyProvider. "hostkey.ser")) #+end_src 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. #+begin_src clojure :results silent (.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)))))))))) #+end_src 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": #+begin_src clojure :results silent (.setPasswordAuthenticator sshd (reify PasswordAuthenticator (authenticate [this username password session] (= password "foo")))) #+end_src Now we simply start the server: #+begin_src clojure :results silent (.start sshd) #+end_src 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: #+begin_src clojure :results silent (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}))))) #+end_src But now that we have it, we can easily use it. #+begin_src clojure :results silent (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)))))) #+end_src 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.