26
26
[clj-ssh.agent :as agent]
27
27
[clj-ssh.keychain :as keychain]
28
28
[clj-ssh.reflect :as reflect]
29
+ [clj-ssh.ssh.protocols :as protocols]
29
30
[clojure.java.io :as io]
30
31
[clojure.string :as string]
31
32
[clojure.tools.logging :as logging])
98
99
" Predicate to test for an ssh-agent."
99
100
[object] (instance? JSch object))
100
101
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
+
101
126
; ;; Agent
102
127
(defn ssh-agent
103
128
" 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.
257
282
(add-identity agent options)))))
258
283
259
284
; ;; 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
+
260
298
(defn- ^Session session-impl
261
299
[^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 ))
273
306
274
307
(defn ^Session session
275
308
" 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
277
310
keys. All other option key pairs will be passed as SSH config options."
278
311
[^JSch agent hostname
279
312
{: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."
282
315
(or username (System/getProperty " user.name" ))
283
316
port
284
317
password
285
- (dissoc options :username :port :password :agent )))
318
+ (session- options options )))
286
319
287
320
(defn forward-remote-port
288
321
" Start remote port forwarding"
@@ -309,7 +342,7 @@ keys. All other option key pairs will be passed as SSH config options."
309
342
(unforward-remote-port ~session ~remote-port))))
310
343
311
344
(defn forward-local-port
312
- " Start local port forwarding"
345
+ " Start local port forwarding. Returns the actual local port. "
313
346
([^Session session local-port remote-port remote-host]
314
347
(.setPortForwardingL session local-port remote-host remote-port))
315
348
([session local-port remote-port]
@@ -333,24 +366,25 @@ keys. All other option key pairs will be passed as SSH config options."
333
366
334
367
(defn connect
335
368
" 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)))
340
373
341
374
(defn disconnect
342
375
" 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))]
347
381
(when (.isAlive t)
348
382
(.interrupt t))))
349
383
350
384
(defn connected?
351
385
" Predicate used to test for a connected session."
352
- [^Session session]
353
- (.isConnected session))
386
+ [session]
387
+ (protocols/connected? session))
354
388
355
389
(defmacro with-connection
356
390
" 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."
364
398
(finally
365
399
(disconnect session#)))))
366
400
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
+
367
476
; ;; Channels
368
477
(defn connect-channel
369
478
" Connect a channel."
0 commit comments