Skip to content

Commit 9975e44

Browse files
committed
Test clj-libssh2.authentication and clj-libssh2.error.
Refactor the authentication code in the process.
1 parent 8e44703 commit 9975e44

File tree

7 files changed

+180
-52
lines changed

7 files changed

+180
-52
lines changed

src/clj_libssh2/agent.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
id)
3737
false)))
3838
(throw (Exception. "Failed to authenticate with the agent.")))
39+
true
3940
(finally
4041
(handle-errors session (libssh2-agent/disconnect ssh-agent))
4142
(libssh2-agent/free ssh-agent)))))

src/clj_libssh2/authentication.clj

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,63 @@
22
(:require [clojure.java.io :refer [file]]
33
[clj-libssh2.libssh2.userauth :as libssh2-userauth]
44
[clj-libssh2.agent :as ssh-agent]
5-
[clj-libssh2.error :refer [handle-errors]]))
5+
[clj-libssh2.error :refer [handle-errors]])
6+
(:import clojure.lang.PersistentArrayMap))
67

7-
(defmulti authenticate
8-
(fn [session credentials]
9-
(cond
10-
(and (:username credentials) (:agent credentials))
11-
:agent
8+
(defprotocol Credentials
9+
(valid? [credentials]))
1210

13-
(and (:passphrase credentials)
14-
(:public-key credentials)
15-
(:private-key credentials)
16-
(:username credentials))
17-
:key
11+
(defrecord AgentCredentials [username]
12+
Credentials
13+
(valid? [credentials] (some? username)))
1814

19-
(and (:username credentials) (:password credentials))
20-
:password
15+
(defrecord KeyCredentials [username passphrase private-key public-key]
16+
Credentials
17+
(valid? [credentials] (and (some? username)
18+
(some? passphrase)
19+
(some? private-key)
20+
(some? public-key)
21+
(.exists (file private-key))
22+
(.exists (file public-key)))))
2123

22-
:else :invalid)))
24+
(defrecord PasswordCredentials [username password]
25+
Credentials
26+
(valid? [credentials] (and (some? username) (some? password))))
2327

24-
(defmethod authenticate :invalid
25-
[session credentials]
26-
(throw (Exception. "Invalid credentials.")))
28+
(defmulti authenticate
29+
(fn [session credentials] (type credentials)))
2730

28-
(defmethod authenticate :agent
31+
(defmethod authenticate AgentCredentials
2932
[session credentials]
3033
(ssh-agent/authenticate session (:username credentials)))
3134

32-
(defmethod authenticate :key
35+
(defmethod authenticate KeyCredentials
3336
[session credentials]
34-
(let [require-exists (fn [path]
35-
(when-not (.exists (file path))
36-
(throw (Exception.
37-
(format "%s does not exist." path)))))
38-
passphrase (:passphrase credentials)
39-
privkey (:private-key credentials)
40-
pubkey (:public-key credentials)
41-
username (:username credentials)]
42-
(require-exists privkey)
43-
(require-exists pubkey)
44-
(handle-errors session
45-
(libssh2-userauth/publickey-fromfile (:session session)
46-
username
47-
pubkey
48-
privkey
49-
passphrase))))
50-
51-
(defmethod authenticate :password
37+
(doseq [keyfile (map #(% credentials) [:private-key :public-key])]
38+
(when-not (.exists (file keyfile))
39+
(throw (Exception. (format "%s does not exist." keyfile)))))
40+
(handle-errors session
41+
(libssh2-userauth/publickey-fromfile (:session session)
42+
(:username credentials)
43+
(:public-key credentials)
44+
(:private-key credentials)
45+
(:passphrase credentials)))
46+
true)
47+
48+
(defmethod authenticate PasswordCredentials
49+
[session credentials]
50+
(handle-errors session
51+
(libssh2-userauth/password (:session session)
52+
(:username credentials)
53+
(:password credentials)))
54+
true)
55+
56+
(defmethod authenticate PersistentArrayMap
5257
[session credentials]
53-
(let [username (:username credentials)
54-
password (:password credentials)]
55-
(handle-errors session
56-
(libssh2-userauth/password (:session session) username password))))
58+
(loop [m [map->KeyCredentials map->PasswordCredentials map->AgentCredentials]]
59+
(let [creds ((first m) credentials)]
60+
(if (valid? creds)
61+
(authenticate session creds)
62+
(if (< 1 (count m))
63+
(recur (rest m))
64+
(throw (Exception. "Invalid credentials")))))))

test/clj_libssh2/test_agent.clj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
[]
1212
(session/open test/ssh-host
1313
test/ssh-port
14-
{:username (test/ssh-user)
15-
:agent true}))
14+
{:username (test/ssh-user)}))
1615

1716
(defn open-and-close
1817
[]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
(ns clj-libssh2.test-authentication
2+
(:require [clojure.test :refer :all]
3+
[clj-libssh2.libssh2 :as libssh2]
4+
[clj-libssh2.libssh2.userauth :as libssh2-userauth]
5+
[clj-libssh2.session :as session]
6+
[clj-libssh2.test-utils :as test])
7+
(:use clj-libssh2.authentication))
8+
9+
(test/fixtures)
10+
11+
(defn auth
12+
[creds]
13+
(is (= 0 (count @session/sessions)))
14+
(let [session (session/open test/ssh-host test/ssh-port creds)]
15+
(is (= 1 (count @session/sessions)))
16+
(session/close session))
17+
(is (= 0 (count @session/sessions))))
18+
19+
; This is more fully tested in clj-libssh2.test-authentication
20+
(deftest agent-authentication-works
21+
(is (auth (->AgentCredentials (test/ssh-user)))))
22+
23+
(deftest key-authentication-works
24+
(let [user (test/ssh-user)
25+
privkey (fn [keyname] (format "test/tmp/id_rsa%s" keyname))
26+
pubkey (fn [keyname] (str (privkey keyname) ".pub"))
27+
no-passphrase (->KeyCredentials user "" (privkey "") (pubkey ""))
28+
with-passphrase (->KeyCredentials user
29+
"correct horse battery staple"
30+
(privkey "_with_passphrase")
31+
(pubkey "_with_passphrase"))
32+
with-wrong-passphrase (->KeyCredentials user
33+
"bad horse"
34+
(privkey "_with_passphrase")
35+
(pubkey "_with_passphrase"))
36+
unauthorized (->KeyCredentials user
37+
""
38+
(privkey "_never_authorised")
39+
(pubkey "_never_authorised"))
40+
bad-privkey (->KeyCredentials user "" (privkey "") "/bad")
41+
bad-pubkey (->KeyCredentials user "" "/bad" (pubkey ""))]
42+
(testing "A passphrase-less key works"
43+
(is (valid? no-passphrase))
44+
(is (auth no-passphrase)))
45+
(testing "A key with a passphrase works"
46+
(is (valid? with-passphrase))
47+
(is (auth with-passphrase)))
48+
(testing "A valid but unauthorized key does not work"
49+
(is (valid? unauthorized))
50+
(is (thrown? Exception (auth unauthorized))))
51+
(testing "It fails if the private key file doesn't exist"
52+
(is (not (valid? bad-privkey)))
53+
(is (thrown? Exception (auth bad-privkey))))
54+
(testing "It fails if the public key file doesn't exist"
55+
(is (not (valid? bad-pubkey)))
56+
(is (thrown? Exception (auth bad-pubkey))))
57+
(testing "It fails if the passphrase is incorrect"
58+
(is (valid? with-wrong-passphrase))
59+
(is (thrown? Exception (auth with-wrong-passphrase))))))
60+
61+
; We can't test this all the way without knowing a password on the local
62+
; machine. We can test with libssh2_userauth_password stubbed and some error
63+
; cases as well. In any case, password authentication is disabled by default on
64+
; most sshd installations (in favour of publickey and keyboard-interactive) so
65+
; we expect this method to only be used in odd circumstances.
66+
(deftest password-authentication-works
67+
(let [password-creds (fn [password]
68+
(->PasswordCredentials (test/ssh-user) password))]
69+
(testing "A successful authentication returns true"
70+
(with-redefs [libssh2-userauth/password (constantly 0)]
71+
(is (auth (password-creds "doesn't matter")))))
72+
(testing "It fails to authenticate with the wrong password"
73+
(is (thrown? Exception (auth (password-creds "the wrong password")))))
74+
(testing "A library error does not result in a crash"
75+
(with-redefs [libssh2-userauth/password (constantly libssh2/ERROR_ALLOC)]
76+
(is (thrown? Exception (auth (password-creds "doesn't matter"))))))))
77+
78+
(deftest authenticating-with-a-map-fails-if-there's-no-equivalent-record
79+
(is (thrown-with-msg?
80+
Exception
81+
#"Invalid credentials"
82+
(auth {:password "foo"}))))

test/clj_libssh2/test_error.clj

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
(ns clj-libssh2.test-error
2+
(:require [clojure.test :refer :all]
3+
[clj-libssh2.libssh2 :as libssh2]
4+
[clj-libssh2.libssh2.session :as libssh2-session]
5+
[clj-libssh2.error :as error]
6+
[clj-libssh2.test-utils :as test]))
7+
8+
(test/fixtures)
9+
10+
(deftest session-error-message-works
11+
(testing "It's nil safe"
12+
(is (nil? (error/session-error-message nil))))
13+
(testing "It returns nil when there was no error"
14+
(with-redefs [libssh2-session/last-error (constantly 0)]
15+
(is (nil? (error/session-error-message nil))))))
16+
17+
(deftest maybe-throw-error-works
18+
(testing "It doesn't throw when the error code is nil"
19+
(is (nil? (error/maybe-throw-error nil nil))))
20+
(testing "It doesn't throw unless the error code is negative"
21+
(is (nil? (error/maybe-throw-error nil 0))))
22+
(is (nil? (error/maybe-throw-error nil 1)))
23+
(testing "It doesn't throw for EAGAIN"
24+
(is (nil? (error/maybe-throw-error nil libssh2/ERROR_EAGAIN))))
25+
(testing "It throws on negative error codes"
26+
(is (thrown? Exception (error/maybe-throw-error nil -1))))
27+
(testing "It prefers error messages from the session object"
28+
(let [session-message "This is a fake error message from the session."]
29+
(with-redefs [error/session-error-message (constantly session-message)]
30+
(is (thrown-with-msg?
31+
Exception
32+
(re-pattern session-message)
33+
(error/maybe-throw-error nil libssh2/ERROR_ALLOC))))))
34+
(testing "It produces an error message when there's none from the session."
35+
(is (thrown-with-msg? Exception
36+
#".+"
37+
(error/maybe-throw-error nil libssh2/ERROR_ALLOC))))
38+
(testing "It produces an error message even when the error code is bogus."
39+
(is (thrown-with-msg? Exception
40+
#".+"
41+
(error/maybe-throw-error nil -10000)))))

test/clj_libssh2/test_session.clj

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,14 @@
1414
(merge {:username (test/ssh-user)}
1515
(apply hash-map creds))))
1616

17-
(defn- good-session []
18-
(open :agent true))
19-
2017
(deftest close-is-robust
2118
(testing "Closing a good, open connection does not fail."
22-
(let [session (good-session)]
19+
(let [session (open)]
2320
(is (= 1 (count @session/sessions)))
2421
(session/close session)
2522
(is (= 0 (count @session/sessions)))))
2623
(testing "Closing a good, open connection more than once does not fail."
27-
(let [session (good-session)]
24+
(let [session (open)]
2825
(is (= 1 (count @session/sessions)))
2926
(session/close session)
3027
(is (= 0 (count @session/sessions)))
@@ -36,9 +33,9 @@
3633
(deftest open-works
3734
(testing "sessions are pooled"
3835
(is (= 0 (count @session/sessions)))
39-
(let [session1 (good-session)]
36+
(let [session1 (open)]
4037
(is (= 1 (count @session/sessions)))
41-
(let [session2 (good-session)]
38+
(let [session2 (open)]
4239
(is (= 1 (count @session/sessions)))
4340
(is (= session1 session2))
4441
(session/close session1)
@@ -47,7 +44,7 @@
4744
(testing "throws but doesn't crash on handshake failure"
4845
(with-redefs [libssh2-session/handshake (constantly libssh2/ERROR_PROTO)]
4946
(is (= 0 (count @session/sessions)))
50-
(is (thrown? Exception (good-session)))
47+
(is (thrown? Exception (open)))
5148
(is (= 0 (count @session/sessions)))))
5249
(testing "throws but doesn't crash on authentication failure"
5350
(is (= 0 (count @session/sessions)))

test/script/setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ generate_user_keys() {
6767
launch_ssh_agent() {
6868
require_executable ssh-agent
6969
eval "$(ssh-agent -s -a "${TMP_DIR}/agent.sock")" > /dev/null
70-
ssh-add -D > /dev/null
70+
ssh-add -D > /dev/null 2>&1
7171
}
7272

7373
launch_sshd() {

0 commit comments

Comments
 (0)