Skip to content

Instantly share code, notes, and snippets.

@candera
Last active February 9, 2019 07:50
Show Gist options
  • Select an option

  • Save candera/11310395 to your computer and use it in GitHub Desktop.

Select an option

Save candera/11310395 to your computer and use it in GitHub Desktop.

Revisions

  1. candera revised this gist May 12, 2014. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions ssh-repl.org
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,7 @@
    * 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
  2. candera revised this gist Apr 26, 2014. 1 changed file with 9 additions and 35 deletions.
    44 changes: 9 additions & 35 deletions ssh-repl.org
    Original file line number Diff line number Diff line change
    @@ -29,7 +29,7 @@ On to the code!

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

    #+begin_src clojure
    #+begin_src clojure :results silent
    (import '[org.apache.sshd SshServer]
    '[org.apache.sshd.server Command PasswordAuthenticator PublickeyAuthenticator]
    '[org.apache.sshd.common Factory]
    @@ -38,40 +38,28 @@ We need to start by pulling in a few namespaces and classes:
    (require '[clojure.java.io :as io])
    #+end_src

    #+RESULTS:
    : nil

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

    #+begin_src clojure
    #+begin_src clojure :results silent
    (def sshd (SshServer/setUpDefaultServer))
    #+end_src

    #+RESULTS:
    : #'user/sshd

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

    #+begin_src clojure
    #+begin_src clojure :results silent
    (.setPort sshd 2022)
    #+end_src

    #+RESULTS:
    : nil


    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
    #+begin_src clojure :results silent
    (.setKeyPairProvider sshd (SimpleGeneratorHostKeyProvider. "hostkey.ser"))
    #+end_src

    #+RESULTS:
    : nil

    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
    @@ -80,7 +68,7 @@ state to keep track of, but we can do that in an atom. Note the use of
    and =*err*= despite the fact that we're running the REPL on its own
    thread.

    #+begin_src clojure
    #+begin_src clojure :results silent
    (.setShellFactory sshd
    (reify Factory
    (create [this]
    @@ -119,32 +107,24 @@ thread.
    (future-call (bound-fn* clojure.main/repl))))))))))
    #+end_src

    #+RESULTS:
    : nil

    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
    #+begin_src clojure :results silent
    (.setPasswordAuthenticator sshd
    (reify PasswordAuthenticator
    (authenticate [this username password session]
    (= password "foo"))))
    #+end_src

    #+RESULTS:
    : nil

    Now we simply start the server:

    #+begin_src clojure
    #+begin_src clojure :results silent
    (.start sshd)
    #+end_src

    #+RESULTS:
    : nil

    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
    @@ -159,7 +139,7 @@ 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
    #+begin_src clojure :results silent
    (import '[java.math BigInteger]
    '[java.security KeyFactory PublicKey]
    '[java.security.spec DSAPublicKeySpec RSAPublicKeySpec]
    @@ -208,12 +188,9 @@ of =java.security.PublicKey=, but it's not too bad:
    :type type})))))
    #+end_src

    #+RESULTS:
    : #'user/read-ssh-key

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

    #+begin_src clojure
    #+begin_src clojure :results silent
    (let [allowed-key (read-ssh-key "/Users/candera/.ssh/id_rsa.pub")]
    (.setPublickeyAuthenticator sshd
    (reify PublickeyAuthenticator
    @@ -224,9 +201,6 @@ But now that we have it, we can easily use it.
    (= key allowed-key))))))
    #+end_src

    #+RESULTS:
    : nil

    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.
  3. candera revised this gist Apr 26, 2014. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions ssh-repl.org
    Original file line number Diff line number Diff line change
    @@ -154,9 +154,10 @@ you see what you type.
    * Authentication via public key

    Passwords - especially passwords embedded in our programs -
    are less than desirable. SSHD also provides support for public key
    authentication, though. It requires a bit of code to go from a SSH
    public key file to an instance of =java.security.PublicKey= though.
    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
    (import '[java.math BigInteger]
  4. candera created this gist Apr 26, 2014.
    231 changes: 231 additions & 0 deletions ssh-repl.org
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,231 @@
    * Embedding an SSH-accessible REPL in a Clojure process

    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
    (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

    #+RESULTS:
    : nil

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

    #+begin_src clojure
    (def sshd (SshServer/setUpDefaultServer))
    #+end_src

    #+RESULTS:
    : #'user/sshd

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

    #+begin_src clojure
    (.setPort sshd 2022)
    #+end_src

    #+RESULTS:
    : nil


    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
    (.setKeyPairProvider sshd (SimpleGeneratorHostKeyProvider. "hostkey.ser"))
    #+end_src

    #+RESULTS:
    : nil

    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
    (.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

    #+RESULTS:
    : nil

    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
    (.setPasswordAuthenticator sshd
    (reify PasswordAuthenticator
    (authenticate [this username password session]
    (= password "foo"))))
    #+end_src

    #+RESULTS:
    : nil

    Now we simply start the server:

    #+begin_src clojure
    (.start sshd)
    #+end_src

    #+RESULTS:
    : nil

    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. SSHD also provides support for public key
    authentication, though. It requires a bit of code to go from a SSH
    public key file to an instance of =java.security.PublicKey= though.

    #+begin_src clojure
    (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

    #+RESULTS:
    : #'user/read-ssh-key

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

    #+begin_src clojure
    (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

    #+RESULTS:
    : nil

    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.