Skip to content

Commit 0bd66a7

Browse files
committed
Add support for jump hosts
The jump-session function is used to obtain a session that can be connected across jump hosts. The `the-session` function is added to obtain a jsch session from a connected jump-session, or a connected jsch Session, and can be used in code that wants to support jump sessions.
1 parent 9a3ec02 commit 0bd66a7

File tree

4 files changed

+183
-24
lines changed

4 files changed

+183
-24
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ SSH tunneling is also supported:
115115
(comment do something with port 8080 here)))))
116116
```
117117

118+
Jump hosts can be used with the `jump-session`. Once the session is
119+
connected, the `the-session` function can be used to obtain a session
120+
that can be used with `ssh-exec`, etc. The `the-session` function can
121+
be used on a session returned by `session`, so you can write code that
122+
works with both a `jump-session` session and a single host session.
123+
124+
```clj
125+
(let [s (jump-session
126+
(ssh-agent {})
127+
[{:hostname "host1" :username "user"
128+
:strict-host-key-checking :no}
129+
{:hostname "final-host" :username "user"
130+
:strict-host-key-checking :no}]
131+
{})]
132+
(with-connection s
133+
(ssh-exec (the-session s) "ls" "" "" {}))
134+
```
135+
136+
137+
118138
## Documentation
119139

120140
[Annotated source](http:/hugoduncan.github.com/clj-ssh/0.5/annotated/uberdoc.html).

src/clj_ssh/ssh.clj

Lines changed: 133 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
[clj-ssh.agent :as agent]
2727
[clj-ssh.keychain :as keychain]
2828
[clj-ssh.reflect :as reflect]
29+
[clj-ssh.ssh.protocols :as protocols]
2930
[clojure.java.io :as io]
3031
[clojure.string :as string]
3132
[clojure.tools.logging :as logging])
@@ -98,6 +99,30 @@
9899
"Predicate to test for an ssh-agent."
99100
[object] (instance? JSch object))
100101

102+
;;; Session extension
103+
104+
;; This is only relevant if you want to support using jump hosts. If
105+
;; you do, the you should always use the `the-session` to get the jsch
106+
;; session object once connected.
107+
108+
;; This is here since JSch Session has a package scoped constructor,
109+
;; and doesn't implement an interface, so provides no means for
110+
;; extending it.
111+
112+
(extend-protocol protocols/Session
113+
Session
114+
(connect
115+
([session] (.connect session))
116+
([session timeout] (.connect session timeout)))
117+
(connected? [session] (.isConnected session))
118+
(disconnect [session] (.disconnect session))
119+
(session [session] session))
120+
121+
(defn ^Session the-session
122+
"Return the JSch session for the given session."
123+
[session]
124+
(protocols/session session))
125+
101126
;;; Agent
102127
(defn ssh-agent
103128
"Create a ssh-agent. By default a system ssh-agent is preferred."
@@ -257,23 +282,31 @@ keyword argument, or constructed from the other keyword arguments.
257282
(add-identity agent options)))))
258283

259284
;;; Sessions
285+
(defn- init-session
286+
"Initialise options on a session"
287+
[^Session session ^String password options]
288+
(when password
289+
(.setPassword session password))
290+
(doseq [[k v :as option] options]
291+
(.setConfig
292+
session
293+
(if (string? k)
294+
k
295+
(camelize (as-string k)))
296+
(as-string v))))
297+
260298
(defn- ^Session session-impl
261299
[^JSch agent hostname username port ^String password options]
262-
(let [session (.getSession agent username hostname port)]
263-
(when password
264-
(.setPassword session password))
265-
(doseq [[k v :as option] options]
266-
(.setConfig
267-
session
268-
(if (string? k)
269-
k
270-
(camelize (as-string k)))
271-
(as-string v)))
272-
session))
300+
(doto (.getSession agent username hostname port)
301+
(init-session password options)))
302+
303+
(defn- session-options
304+
[options]
305+
(dissoc options :username :port :password :agent))
273306

274307
(defn ^Session session
275308
"Start a SSH session.
276-
Requires hostname. you can also pass values for :username, :password and :port
309+
Requires hostname. You can also pass values for :username, :password and :port
277310
keys. All other option key pairs will be passed as SSH config options."
278311
[^JSch agent hostname
279312
{:keys [port username password] :or {port 22} :as options}]
@@ -282,7 +315,7 @@ keys. All other option key pairs will be passed as SSH config options."
282315
(or username (System/getProperty "user.name"))
283316
port
284317
password
285-
(dissoc options :username :port :password :agent)))
318+
(session-options options)))
286319

287320
(defn forward-remote-port
288321
"Start remote port forwarding"
@@ -309,7 +342,7 @@ keys. All other option key pairs will be passed as SSH config options."
309342
(unforward-remote-port ~session ~remote-port))))
310343

311344
(defn forward-local-port
312-
"Start local port forwarding"
345+
"Start local port forwarding. Returns the actual local port."
313346
([^Session session local-port remote-port remote-host]
314347
(.setPortForwardingL session local-port remote-host remote-port))
315348
([session local-port remote-port]
@@ -333,24 +366,25 @@ keys. All other option key pairs will be passed as SSH config options."
333366

334367
(defn connect
335368
"Connect a session."
336-
([^Session session]
337-
(.connect session))
338-
([^Session session timeout]
339-
(.connect session timeout)))
369+
([session]
370+
(protocols/connect session))
371+
([session timeout]
372+
(protocols/connect session timeout)))
340373

341374
(defn disconnect
342375
"Disconnect a session."
343-
[^Session session]
344-
(.disconnect session)
345-
(when-let [^Thread t (reflect/get-field
346-
com.jcraft.jsch.Session 'connectThread session)]
376+
[session]
377+
(protocols/disconnect session)
378+
(when-let [^Thread t (and (instance? Session session)
379+
(reflect/get-field
380+
com.jcraft.jsch.Session 'connectThread session))]
347381
(when (.isAlive t)
348382
(.interrupt t))))
349383

350384
(defn connected?
351385
"Predicate used to test for a connected session."
352-
[^Session session]
353-
(.isConnected session))
386+
[session]
387+
(protocols/connected? session))
354388

355389
(defmacro with-connection
356390
"Creates a context in which the session is connected. Ensures the session is
@@ -364,6 +398,81 @@ keys. All other option key pairs will be passed as SSH config options."
364398
(finally
365399
(disconnect session#)))))
366400

401+
;;; Jump Hosts
402+
(defn- jump-connect [agent hosts sessions timeout]
403+
(let [host (first hosts)
404+
s (session agent (:hostname host) (dissoc host :hostname))
405+
throw-e (fn [e s]
406+
(throw
407+
(ex-info
408+
(str "Failed to connect "
409+
(.getUserName s) "@"
410+
(.getHost s) ":"
411+
(.getPort s)
412+
" " (pr-str (into [] (.getIdentityNames agent)))
413+
" " (pr-str hosts))
414+
{:hosts hosts}
415+
e)))]
416+
(swap! sessions (fnil conj []) s)
417+
(try
418+
(connect s timeout)
419+
(catch Exception e (throw-e e s)))
420+
(.setDaemonThread s true)
421+
(loop [hosts (rest hosts)
422+
prev-s s]
423+
(if-let [{:keys [hostname port username password]
424+
:or {port 22}
425+
:as options}
426+
(first hosts)]
427+
(let [p (forward-local-port prev-s 0 port hostname)
428+
options (-> options
429+
(dissoc :hostname)
430+
(assoc :port p))
431+
s (session agent "localhost" options)]
432+
(.setDaemonThread s true)
433+
(.setHostKeyAlias s hostname)
434+
(swap! sessions conj s)
435+
(try
436+
(connect s timeout)
437+
(catch Exception e (throw-e e s)))
438+
(recur (rest hosts) s))))))
439+
440+
(defn- jump-connected? [sessions]
441+
(seq @sessions))
442+
443+
(defn- jump-disconnect
444+
[sessions]
445+
(doseq [s (reverse @sessions)]
446+
(.disconnect s))
447+
(reset! sessions nil))
448+
449+
(defn- jump-the-session
450+
[sessions]
451+
(assert (jump-connected? sessions) "not connected")
452+
(last @sessions))
453+
454+
(deftype JumpHostSession [agent hosts sessions timeout]
455+
protocols/Session
456+
(connect [session] (protocols/connect session timeout))
457+
(connect [session timeout] (jump-connect agent hosts sessions timeout))
458+
(connected? [session] (jump-connected? sessions))
459+
(disconnect [session] (jump-disconnect sessions))
460+
(session [session] (jump-the-session sessions)))
461+
462+
;; http://www.jcraft.com/jsch/examples/JumpHosts.java.html
463+
(defn jump-session
464+
"Connect via a sequence of jump hosts. Returns a session. Once the
465+
session is connected, use `the-session` to get a jsch Session object.
466+
467+
Each host is a map with :hostname, :username, :password and :port
468+
keys. All other key pairs in each host map will be passed as SSH
469+
config options."
470+
[^JSch agent hosts {:keys [timeout]}]
471+
(when-not (seq hosts)
472+
(throw (ex-info "Must provide at least one host to connect to"
473+
{:hosts hosts})))
474+
(JumpHostSession. agent hosts (atom []) (or timeout 0)))
475+
367476
;;; Channels
368477
(defn connect-channel
369478
"Connect a channel."

src/clj_ssh/ssh/protocols.clj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(ns clj-ssh.ssh.protocols
2+
"Protocols for ssh")
3+
4+
(defprotocol Session
5+
"Provides a protocol for a session."
6+
(connect [x] [x timeout] "Connect the session")
7+
(connected? [x] "Predicate for a connected session")
8+
(disconnect [x] "Disconnect the session")
9+
(session [x] "Return a Jsch Session for the session"))

test/clj_ssh/ssh_test.clj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,3 +581,24 @@
581581
(is (port-reachable? 2222)))
582582
(with-remote-port-forward [session 2222 22 "localhost"]
583583
(is (port-reachable? 2222)))))))
584+
585+
(deftest jump-session-test
586+
(is (let [s (jump-session (ssh-agent {})
587+
[{:hostname "localhost"
588+
:username (username)
589+
:strict-host-key-checking :no}]
590+
{})]
591+
(with-connection s
592+
(ssh-exec (the-session s) "ls" "" "" {})))
593+
"one host")
594+
(is (let [s (jump-session (ssh-agent {})
595+
[{:hostname "localhost"
596+
:username (username)
597+
:strict-host-key-checking :no}
598+
{:hostname "localhost"
599+
:username (username)
600+
:strict-host-key-checking :no}]
601+
{})]
602+
(with-connection s
603+
(ssh-exec (the-session s) "ls" "" "" {})))
604+
"two hosts"))

0 commit comments

Comments
 (0)