├── .circleci
├── config.yml
└── config.yml~
├── .github
└── CODEOWNERS
├── .gitignore
├── .travis.yml.foo
├── ORIGINATOR
├── README.md
├── ReleaseNotes.md
├── brick.clj
├── profiles.clj
├── project.clj
├── src
└── clj_ssh
│ ├── agent.clj
│ ├── cli.clj
│ ├── keychain.clj
│ ├── reflect.clj
│ ├── ssh.clj
│ └── ssh
│ └── protocols.clj
└── test
├── clj_ssh
├── cli_test.clj
├── ssh_test.clj
├── test_keys.clj
└── test_utils.clj
├── log4j.xml
└── logback.xml
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | workflows:
4 | build-deploy:
5 | jobs:
6 | - build:
7 | filters:
8 | tags:
9 | only: /.*/
10 |
11 | - deploy:
12 | requires:
13 | - build
14 | filters:
15 | branches:
16 | ignore: /.*/
17 | tags:
18 | only: /Release-.*/
19 | context:
20 | - CLOJARS_DEPLOY
21 |
22 | jobs:
23 | build:
24 | machine:
25 | image: ubuntu-2004:2023.07.1
26 |
27 | resource_class: medium
28 |
29 | working_directory: ~/repo
30 |
31 | environment:
32 | LEIN_ROOT: "true"
33 | # Customize the JVM maximum heap limit
34 | JVM_OPTS: -Xmx3200m
35 |
36 | steps:
37 | - checkout
38 |
39 | # Download and cache dependencies
40 | - restore_cache:
41 | keys:
42 | - v1-dependencies-{{ checksum "project.clj" }}
43 | # fallback to using the latest cache if no exact match is found
44 | - v1-dependencies-
45 |
46 | - run:
47 | name: Setup environment
48 | command: sudo apt-get update && sudo apt-get install -y openjdk-8-jdk leiningen
49 |
50 | - run:
51 | name: Fetch project dependencies
52 | command: lein deps
53 |
54 | - save_cache:
55 | paths:
56 | - ~/.m2
57 | key: v1-dependencies-{{ checksum "project.clj" }}
58 |
59 | - run:
60 | name: Setup tests
61 | command: |
62 | [[ -f ~/.ssh/id_rsa ]] || ssh-keygen -N "" -f ~/.ssh/id_rsa
63 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
64 | ssh-keygen -f ~/.ssh/clj_ssh -t rsa -C "key for test clj-ssh" -N ""
65 | ssh-keygen -f ~/.ssh/clj_ssh_pp -t rsa -C "key for test clj-ssh" -N "clj-ssh"
66 | echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh.pub)" >> ~/.ssh/authorized_keys
67 | echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh_pp.pub)" >> ~/.ssh/authorized_keys
68 | eval $(ssh-agent)
69 | cat \<< EOF > pp
70 | #!/bin/sh
71 | echo "clj-ssh"
72 | EOF
73 | chmod +x pp
74 | export SSH_ASKPASS=./pp
75 | export DISPLAY=1
76 | setsid ssh-add ~/.ssh/clj_ssh_pp < /dev/null >/dev/null 2>&1
77 |
78 | - run:
79 | name: Run tests
80 | command: lein test
81 |
82 | deploy:
83 | machine:
84 | image: ubuntu-2004:2023.07.1
85 |
86 | resource_class: medium
87 |
88 | working_directory: ~/repo
89 |
90 | environment:
91 | LEIN_ROOT: "true"
92 | # Customize the JVM maximum heap limit
93 | JVM_OPTS: -Xmx3200m
94 |
95 | steps:
96 | - checkout
97 |
98 | - run:
99 | name: Setup environment
100 | command: sudo apt-get update && sudo apt-get install -y openjdk-8-jdk leiningen
101 |
102 | # Download and cache dependencies
103 | - restore_cache:
104 | keys:
105 | - v1-dependencies-{{ checksum "project.clj" }}
106 | # fallback to using the latest cache if no exact match is found
107 | - v1-dependencies-
108 |
109 | # Download and cache dependencies
110 | - restore_cache:
111 | keys:
112 | - v1-dependencies-{{ checksum "project.clj" }}
113 | # fallback to using the latest cache if no exact match is found
114 | - v1-dependencies-
115 |
116 | - run:
117 | name: Install babashka
118 | command: |
119 | curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install -o install.sh
120 | sudo bash install.sh
121 | rm install.sh
122 | - run:
123 | name: Install deployment-script
124 | command: |
125 | curl -s https://raw.githubusercontent.com/clj-commons/infra/main/deployment/circle-maybe-deploy.bb -o circle-maybe-deploy.bb
126 | chmod a+x circle-maybe-deploy.bb
127 |
128 | - run: lein deps
129 |
130 | - run:
131 | name: Setup GPG signing key
132 | command: |
133 | sudo apt-get update
134 | sudo apt-get install -y make gnupg
135 | GNUPGHOME="$HOME/.gnupg"
136 | export GNUPGHOME
137 | mkdir -p "$GNUPGHOME"
138 | chmod 0700 "$GNUPGHOME"
139 |
140 | echo "$GPG_KEY" \
141 | | base64 --decode --ignore-garbage \
142 | | gpg --batch --allow-secret-key-import --import
143 |
144 | gpg --keyid-format LONG --list-secret-keys
145 |
146 | - save_cache:
147 | paths:
148 | - ~/.m2
149 | key: v1-dependencies-{{ checksum "project.clj" }}
150 | - run:
151 | name: Deploy
152 | command: |
153 | GPG_TTY=$(tty)
154 | export GPG_TTY
155 | echo $GPG_TTY
156 | ./circle-maybe-deploy.bb lein deploy clojars
157 |
158 |
--------------------------------------------------------------------------------
/.circleci/config.yml~:
--------------------------------------------------------------------------------
1 | # Clojure CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/clojure:lein-2.7.1
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/postgres:9.4
16 |
17 | working_directory: ~/repo
18 |
19 | environment:
20 | LEIN_ROOT: "true"
21 | # Customize the JVM maximum heap limit
22 | JVM_OPTS: -Xmx3200m
23 |
24 | steps:
25 | - checkout
26 |
27 | # Download and cache dependencies
28 | - restore_cache:
29 | keys:
30 | - v1-dependencies-{{ checksum "project.clj" }}
31 | # fallback to using the latest cache if no exact match is found
32 | - v1-dependencies-
33 |
34 | - run: lein deps
35 |
36 | - save_cache:
37 | paths:
38 | - ~/.m2
39 | key: v1-dependencies-{{ checksum "project.clj" }}
40 |
41 | # run tests!
42 | - run: lein test
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @slipset
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | target
3 | *jar
4 | lib
5 | classes
6 | autodoc/**
7 | doc/**
8 | .lein-*
9 | .nrepl-*
10 |
--------------------------------------------------------------------------------
/.travis.yml.foo:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein2
3 | before_script:
4 | - ssh-keygen -N "" -f ~/.ssh/id_rsa
5 | - cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
6 | - ssh-keygen -f ~/.ssh/clj_ssh -t rsa -C "key for test clj-ssh" -N ""
7 | - ssh-keygen -f ~/.ssh/clj_ssh_pp -t rsa -C "key for test clj-ssh" -N "clj-ssh"
8 | - echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh.pub)" >> ~/.ssh/authorized_keys
9 | - echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh_pp.pub)" >> ~/.ssh/authorized_keys
10 | - eval $(ssh-agent)
11 | - echo "clj-ssh" > pp
12 | - chmod +x pp
13 | - setsid ssh-add ~/.ssh/clj_ssh_pp < pp
14 | script: lein2 test
15 | after_success:
16 | - lein2 pallet-release push
17 | env:
18 | global:
19 | secure: eOBqYhJhOJMtRiMKs9ZgG4pEHFy7YqiBZ5NUEWUYD6qav6sMRHqqR5F04NRI37SmnIupzeTChqfRgX0DOwHeTl4u+QJnwRDH2z3avu75FbtZWgiGrxzE39SESpVj/zsyDrEUzT7ZiMayXKyNa3ObiJ8vBUFT7x/OZyRp/1rJxHU=
20 |
--------------------------------------------------------------------------------
/ORIGINATOR:
--------------------------------------------------------------------------------
1 | @hugoduncan
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://clojars.org/clj-commons/clj-ssh)
3 | [](https://cljdoc.org/d/clj-commons/clj-ssh/CURRENT)
4 | [](https://circleci.com/gh/clj-commons/clj-ssh)
5 |
6 | # clj-ssh
7 |
8 | SSH in clojure. Uses [jsch](https://github.com/mwiede/jsch).
9 | (See section RSA Private Key format if using openssl generated keys)
10 |
11 | ## Usage
12 |
13 | ### REPL
14 |
15 | The `clj-ssh.cli` namespace provides some functions for ease of use at the REPL.
16 |
17 | ```clj
18 | (use 'clj-ssh.cli)
19 | ```
20 |
21 | Use `ssh` to execute a command, say `ls`, on a remote host "my-host",
22 |
23 | ```clj
24 | (ssh "my-host" "ls")
25 | => {:exit 0 :out "file1\nfile2\n" :err "")
26 | ```
27 |
28 | By default this will use the system ssh-agent to obtain your ssh keys, and it
29 | uses your current username, but this can be specified:
30 |
31 | ```clj
32 | (ssh "my-host" "ls" :username "remote-user")
33 | => {:exit 0 :out "file1\nfile2\n" :err "")
34 | ```
35 |
36 | Strict host key checking can be turned off:
37 |
38 | ```clj
39 | (default-session-options {:strict-host-key-checking :no})
40 | ```
41 |
42 | SFTP is also supported. For example, to copy a local file to a remote host
43 | "my-host":
44 |
45 | ```clj
46 | (sftp "my-host" :put "/from/this/path" "to/this/path")
47 | ```
48 |
49 | Note that any sftp commands that change the state of the sftp session (such as
50 | cd) do not work with the simplified interface, as a new session is created each
51 | time.
52 |
53 | If your key has a passphrase, you will need to explicitly add your key either to
54 | the system's ssh-agent, or to clj-ssh's ssh-agent with the appropriate
55 | `add-identity` call.
56 |
57 | ### Non REPL
58 |
59 | The `clj-ssh.ssh` namespace should be used for SSH from functional code.
60 |
61 | ```clj
62 | (let [agent (ssh-agent {})]
63 | (let [session (session agent "host-ip" {:strict-host-key-checking :no})]
64 | (with-connection session
65 | (let [result (ssh session {:in "echo hello"})]
66 | (println (result :out)))
67 | (let [result (ssh session {:cmd "ls"})]
68 | (println (second result)))))))
69 | ```
70 |
71 | The above example shows using `:in` to pass commands to a shell, and using
72 | `:cmd` to exec a command without a shell. When using `:cmd` you can still pass
73 | a stream or a string to `:in` to be used as the process' standard input.
74 |
75 | By default, the system ssh-agent is used, which means the ssh keys you use at
76 | the command line level should automatically be picked up (this should also work
77 | with `pageant` on windows).
78 |
79 | You can forward the ssh-agent, which allows you to run ssh based commands on the
80 | remote host using the credentials in your local ssh-agent:
81 |
82 | ```clj
83 | (let [agent (ssh-agent {})]
84 | (let [session (session agent "host-ip" {:strict-host-key-checking :no})]
85 | (with-connection session
86 | (let [result (ssh session {:in "ssh somehost ls" :agent-forwarding true})]
87 | (println (result :out))))))
88 | ```
89 |
90 | If you prefer not to use the system ssh-agent, or one is not running on your
91 | system, then a local, isolated ssh-agent can be used.
92 |
93 | ```clj
94 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
95 | (add-identity agent {:private-key-path "/user/name/.ssh/id_rsa"})
96 | (let [session (session agent "host-ip" {:strict-host-key-checking :no})]
97 | (with-connection session
98 | (let [result (ssh session {:in "echo hello"})]
99 | (println (result :out)))))
100 | ```
101 |
102 | SFTP is supported:
103 |
104 | ```clj
105 | (let [agent (ssh-agent {})]
106 | (let [session (session agent "host-ip" {:strict-host-key-checking :no})]
107 | (with-connection session
108 | (let [channel (ssh-sftp session)]
109 | (with-channel-connection channel
110 | (sftp channel {} :cd "/remote/path")
111 | (sftp channel {} :put "/some/file" "filename"))))))
112 | ```
113 |
114 | SSH tunneling is also supported:
115 |
116 | ```clj
117 | (let [agent (ssh-agent {})]
118 | (let [session (session agent "host-ip" {:strict-host-key-checking :no})]
119 | (with-connection session
120 | (with-local-port-forward [session 8080 80]
121 | (comment do something with port 8080 here)))))
122 | ```
123 |
124 | Jump hosts can be used with the `jump-session`. Once the session is
125 | connected, the `the-session` function can be used to obtain a session
126 | that can be used with `ssh-exec`, etc. The `the-session` function can
127 | be used on a session returned by `session`, so you can write code that
128 | works with both a `jump-session` session and a single host session.
129 |
130 | ```clj
131 | (let [s (jump-session
132 | (ssh-agent {})
133 | [{:hostname "host1" :username "user"
134 | :strict-host-key-checking :no}
135 | {:hostname "final-host" :username "user"
136 | :strict-host-key-checking :no}]
137 | {})]
138 | (with-connection s
139 | (ssh-exec (the-session s) "ls" "" "" {}))
140 | ```
141 |
142 |
143 |
144 | ## Documentation
145 |
146 | [Annotated source](http:/hugoduncan.github.com/clj-ssh/0.5/annotated/uberdoc.html).
147 | [API](http:/hugoduncan.github.com/clj-ssh/0.5/api/index.html).
148 |
149 | ## RSA Private Key Format and clj-ssh
150 |
151 | There have been changes to the header of RSA Private Keys. With the upgrade of
152 | com.jcraft/jsch to "0.1.55", the older openssh headers work with ssh will throw
153 | an authentication failure.
154 |
155 | Older format
156 | ```
157 | -----BEGIN OPENSSH PRIVATE KEY-----`
158 | ```
159 |
160 | New RSA format
161 | ```
162 | -----BEGIN RSA PRIVATE KEY-----
163 | ```
164 |
165 | Old private keys can be easily converted to the new format, through the use of
166 | ssh-keygen's passphrase changing command. This will change the file in place.
167 | ```
168 | ssh-keygen -p -f privateKeyFile -m pem -P passphrase -N passphrase
169 | ```
170 | The -m flag will force the file to pem format, fixing the header.
171 | The -P (for old passphrase) and -N (new passphrase) can be ommitted to generate
172 | an interactive query instead.
173 | (enter "" at either -P or -N to identify no passphrase)
174 |
175 | ### Note: clj-ssh key generation
176 | clj-ssh does have the ability to generate the public / private key pairs for both
177 | RSA and DSA (found in clj-ssh.ssh/generate-keypair).
178 |
179 | Unlike ssh-keygen, the RSA passphrase on the private key will be limited to
180 | DES-EDE3-CBC DEK format to encrypt/decrypt the passphrase if created within clj-ssh.
181 | ssh-keygen will likely use what is standard in your operating system's crypto suite,
182 | (e.g. AES-128-CBC)
183 |
184 |
185 | ## FAQ
186 |
187 | Q: What does
188 | "4: Failure @ com.jcraft.jsch.ChannelSftp.throwStatusError(ChannelSftp.java:2289)"
189 | during an sftp transfer signify?
190 |
191 | A: Probably a disk full, or permission error.
192 |
193 | ### TTY's and background processes
194 |
195 | Some useful links about ssh and background processes:
196 |
197 | - [Snail book](http://www.snailbook.com/faq/background-jobs.auto.html)
198 | - [ssh -t question on stackoverflow](http://stackoverflow.com/questions/14679178/why-does-ssh-wait-for-my-subshells-without-t-and-kill-them-with-t)
199 | - [sudo and tty question on stackoverflow](http://stackoverflow.com/questions/8441637/to-run-sudo-commands-on-a-ec2-instance)
200 |
201 | Thanks to [Ryan Stradling](http://github.com/rstradling) for these.
202 |
203 | ## Installation
204 |
205 | Via [clojars](http://clojars.org) and
206 | [Leiningen](http://github.com/technomancy/leiningen).
207 |
208 | :dependencies [org.clj-commons/clj-ssh "0.6.2"]
209 |
210 | or your favourite maven repository aware tool.
211 |
212 | ## Tests
213 |
214 | The test rely on several keys being authorized on localhost:
215 |
216 | ```shell
217 | ssh-keygen -f ~/.ssh/clj_ssh -t rsa -C "key for test clj-ssh" -N ""
218 | ssh-keygen -f ~/.ssh/clj_ssh_pp -t rsa -C "key for test clj-ssh" -N "clj-ssh"
219 | cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak
220 | echo "from=\"localhost\" $(cat ~/.ssh/clj_ssh.pub)" >> ~/.ssh/authorized_keys
221 | echo "from=\"localhost\" $(cat ~/.ssh/clj_ssh_pp.pub)" >> ~/.ssh/authorized_keys
222 | ```
223 |
224 | The `clj_ssh_pp` key should have a passphrase, and should be registered with your `ssh-agent`.
225 |
226 | ```shell
227 | ssh-add ~/.ssh/clj_ssh_pp
228 | ```
229 |
230 | On OS X, use:
231 |
232 | ```shell
233 | ssh-add -K ~/.ssh/clj_ssh_pp
234 | ```
235 |
236 | ## Other Libraries
237 |
238 | For plain `ftp`, you might want to look at [clj-ftp](https://github.com/miner/clj-ftp).
239 |
240 | ## License
241 |
242 | Copyright © 2012 Hugo Duncan
243 |
244 | Licensed under [EPL](http://www.eclipse.org/legal/epl-v10.html)
245 |
--------------------------------------------------------------------------------
/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | ## 0.6.4
2 |
3 | - Moved jsch to version 0.2.12
4 |
5 | ## 0.6.3
6 |
7 | - Moved jsch to version 0.2.11
8 |
9 | ## 0.6.2
10 |
11 | - Moved jsch to supported fork from com.github.mwiede/jsch version 0.2.9
12 |
13 | ## 0.5.16
14 |
15 | - Moved jsch to version 0.1.55
16 |
17 | - Moved clojure tools.logging to 1.2.4
18 |
19 | - Moved logback-classic to 1.4.7
20 |
21 | - Changed ssh/generate-keypair to now match jsch API
22 | (and remove setPassphrase method which is listed as depricated)
23 |
24 | - Included section in readme.md regarding RSA header issues and
25 | compatibilities
26 |
27 | ## 0.5.14
28 |
29 | - Remove println from scp code
30 |
31 | ## 0.5.13
32 |
33 | - Make clojure dependency have provided scope
34 | Closes #25
35 |
36 | - Fix race condition in ssh-exec function when command finishes before we
37 | enter with-channel-connection
38 |
39 | - ssh/download: fix :recursive and :preserve flags arity errors.
40 | Recursive still doesn't work correctly: clj-ssh writes every file into the
41 | same output.
42 |
43 | - Fixed recursive scp invocation.
44 |
45 | - fix trace/tracef mixup.
46 |
47 | - Fix missing parens
48 |
49 | ## 0.5.12
50 |
51 | - Update jsch and jsch agentproxy
52 | jsch 0.1.53 agentproxy-version 0.0.9
53 |
54 | - Remove extra arg to throw
55 | This makes clj-ssh compatible with Clojure 1.8, which does not allow the
56 | extra argument to throw.
57 |
58 | ## 0.5.11
59 |
60 | - Lock known hosts file
61 | Try and prevent concurrent read and writes to the known hosts file.
62 |
63 | - Fix :out :stream in the cli namespace
64 | Caller becomes responsible for closing the session.
65 |
66 | Closes #29
67 |
68 | ## 0.5.10
69 |
70 | - Fix session? predicate
71 |
72 | - Factor out ssh-exec-proc and ssh-shell-proc
73 | These provide a lower level interface with more flexible stream handling.
74 |
75 | - Add java.awt.headless to default java opts
76 |
77 | ## 0.5.9
78 |
79 | - Publish new jars
80 | The 0.5.8 jars were not correctly published and are identical to 0.5.7.
81 |
82 | ## 0.5.8
83 |
84 | - Enable introspection of sessions
85 | Adds the session? predicate for testing for a Session object, an the
86 | session-hostname and session-port functions for querying a session.
87 |
88 | - Enable copying identities between agents
89 | The copy-identities can be used to copy identities from one agent to
90 | another. This is useful to allow copying identities from a system agent
91 | to a non system agent.
92 |
93 | - Add support for jump hosts
94 | The jump-session function is used to obtain a session that can be
95 | connected across jump hosts.
96 |
97 | The `the-session` function is added to obtain a jsch session from a
98 | connected jump-session, or a connected jsch Session, and can be used in
99 | code that wants to support jump sessions.
100 |
101 | - Add fingerprint function on keypairs
102 |
103 | - Allow keypair construction from just a public key
104 |
105 | - Update tools.logging to 0.2.6
106 |
107 | - Update to jsch 0.1.51
108 | Adds support for private keys in PKCS#8 format.
109 |
110 | Fixes several session crash issues.
111 |
112 | - Update to jsch.agentproxy 0.0.7
113 |
114 | ## 0.5.7
115 |
116 | - Update to jsch.agentproxy 0.0.6
117 |
118 | ## 0.5.6
119 |
120 | - Allow generate-keypair to write key files
121 |
122 | - Factor out keypair generation
123 | Simplifies add-identity by factoring out the keypair creation.
124 |
125 | - Update to jsch 0.1.50
126 |
127 | ## 0.5.5
128 |
129 | - Wrap open-channel exceptions
130 | When .openChannel throws an exception, wrap it in an ex-info exception.
131 | This allows easier procession of the exceptions in consuming code.
132 |
133 | ## 0.5.4
134 |
135 | - Ensure literal keys get a non-blank comment
136 | When adding a literal key, use "Added by clj-ssh" as the comment.
137 |
138 | - Ensure literal key strings are handled correctly
139 |
140 |
141 | ## 0.5.3
142 |
143 | - Add ssh/exit-status for querying exit code
144 |
145 | - Add type hints to session functions
146 |
147 | ## 0.5.2
148 |
149 | - Update to official jsch.agentproxy relase jars
150 |
151 | ## 0.5.1
152 |
153 | - Fix adding key string identities for ssh-agent
154 | Adding string based ssh keys to an ssh-agent was broken.
155 |
156 | ## 0.5.0
157 |
158 | - Require clojure 1.4.0
159 | Drops usage of slingshot.
160 |
161 | ## 0.4.4
162 |
163 | - Make ssh key test more robust
164 | The add-identity-test ssh-agent case was failing for no apparent reason.
165 |
166 | - Remove some reflection warnings
167 |
168 | ## 0.4.3
169 |
170 | - Use passphrase when adding key to agent, and honour the key name
171 |
172 | ## 0.4.2
173 |
174 | - Update to jsch 0.1.49 and enable adding keys to ssh-agent. Support for
175 | adding passphrase-less keys only.
176 |
177 | ## 0.4.1
178 |
179 | - add option to ssh-agent for :known-hosts-path. Fixes #16.
180 |
181 | - remap log levels to be less verbose by default. Fixes #17
182 |
183 | ## 0.4.0
184 |
185 | - Split out clj-ssh.cli
186 |
187 | clj-ssh.ssh is designed for composability and programmatic use. It takes
188 | map arguments for options and is fully functional.
189 |
190 | clj-ssh.cli is intended to simplify repl usage. It takes variadic
191 | arguments for options and uses dynamic vars to provide defaults.
192 |
193 | ## 0.3.3
194 |
195 | - Add a :agent-forwarding option
196 | A boolean value is passed with :agent-forwarding to clj-ssh.ssh/ssh.
197 |
198 | - Add support for system ssh-agent
199 | Support the system ssh-agent (or pageant on windows when using putty) via
200 | jsch-agent-proxy. Introduces a new agent function, clj-ssh.ssh/ssh-agent.
201 |
202 | ## 0.3.2
203 |
204 | - Add remote port forwarding support
205 |
206 | - Fix documentation for with-local-port-forward
207 |
208 | - Allow specification of session options as strings
209 | This should allow options like:
210 | (default-session-options {"GSSAPIAuthentication" "no"})
211 |
212 | ## 0.3.1
213 |
214 | - Allow clj-ssh to work with a wide range of slingshot versions
215 | Tested with slingshot 0.2.0 and 0.10.1
216 |
217 | - Added SSH tunneling.
218 |
219 | ## 0.3.0
220 |
221 | * Changes
222 |
223 | - Remove use of monolithic contrib
224 | In preparation for clojure 1.3. Use slingshot instead of
225 | contrib.condition. Use local copy of contrib.reflect.
226 |
227 | - Switch to tools.logging
228 |
229 | - Allow specification of PipedInputStream buffer size
230 | The buffer size (in bytes) for the piped stream used to implement the
231 | :stream option for :out. If the ssh commands generate a high volume of
232 | output, then this buffer size can become a bottleneck. The buffer size
233 | can be specified by binding *piped-stream-buffer-size*, and defaults to
234 | 10Kb.
235 |
236 | - Add scp-from and scp-to, for copy files over ssh-exec
237 | Add support for scp using ssh-exec. copies files from a remote machine.
238 | copies to a remote machine.
239 |
240 | - Drop clojure 1.1.0 support
241 |
--------------------------------------------------------------------------------
/brick.clj:
--------------------------------------------------------------------------------
1 | (defbrick clj-ssh
2 | :images {:vmfest [{:image {:os-family :ubuntu :os-version-matches "12.04"
3 | :os-64-bit true}
4 | :selectors [:default]}]})
5 |
--------------------------------------------------------------------------------
/profiles.clj:
--------------------------------------------------------------------------------
1 | {:dev
2 | {:dependencies [[ch.qos.logback/logback-classic "1.4.7"]]
3 | :aliases {"test" ["with-profile"
4 | "clojure-1.4.0:clojure-1.5.1:clojure-1.6.0:clojure-1.7.0:clojure-1.8.0:clojure-1.9.0:clojure-1.10.0:clojure-1.10.1:clojure-1.10.2:clojure-1.10.3:clojure-1.11.0:clojure-1.11.1"
5 | "test"]}}
6 | :clojure-1.4.0 {:dependencies [[org.clojure/clojure "1.4.0"]]}
7 | :clojure-1.5.1 {:dependencies [[org.clojure/clojure "1.5.1"]]}
8 | :clojure-1.6.0 {:dependencies [[org.clojure/clojure "1.6.0"]]}
9 | :clojure-1.7.0 {:dependencies [[org.clojure/clojure "1.7.0"]]}
10 | :clojure-1.8.0 {:dependencies [[org.clojure/clojure "1.8.0"]]}
11 | :clojure-1.9.0 {:dependencies [[org.clojure/clojure "1.9.0"]]}
12 | :clojure-1.10.0 {:dependencies [[org.clojure/clojure "1.10.0"]]}
13 | :clojure-1.10.1 {:dependencies [[org.clojure/clojure "1.10.1"]]}
14 | :clojure-1.10.2 {:dependencies [[org.clojure/clojure "1.10.2"]]}
15 | :clojure-1.10.3 {:dependencies [[org.clojure/clojure "1.10.3"]]}
16 | :clojure-1.11.0 {:dependencies [[org.clojure/clojure "1.11.0"]]}
17 | :clojure-1.11.1 {:dependencies [[org.clojure/clojure "1.11.1"]]}
18 |
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject org.clj-commons/clj-ssh
2 | (or (System/getenv "PROJECT_VERSION") "0.6.0-SNAPSHOT")
3 | :description "Library for using SSH from clojure."
4 | :url "https://github.com/clj-commons/clj-ssh"
5 | :license {:name "Eclipse Public License"
6 | :url "http://www.eclipse.org/legal/epl-v10.html"}
7 | :deploy-repositories [["clojars" {:url "https://repo.clojars.org"
8 | :username :env/clojars_username
9 | :password :env/clojars_org_clj_commons_password
10 | :sign-releases true}]]
11 |
12 | :dependencies [[org.clojure/tools.logging "1.2.4"
13 | :exclusions [org.clojure/clojure]]
14 | [com.github.mwiede/jsch "0.2.15"]
15 | [net.java.dev.jna/jna "5.14.0"]
16 | [com.kohlschutter.junixsocket/junixsocket-core "2.8.3" :extension "pom"]]
17 | :jvm-opts ["-Djava.awt.headless=true"]
18 | :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.1"]]}})
19 |
20 |
--------------------------------------------------------------------------------
/src/clj_ssh/agent.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.agent
2 | "Agent integration (using jsch-agent-proxy)"
3 | (:require
4 | [clojure.tools.logging :as logging])
5 | (:import
6 | [com.jcraft.jsch
7 | JSch AgentProxyException AgentIdentityRepository
8 | PageantConnector SSHAgentConnector JUnixSocketFactory]))
9 |
10 | (defn sock-agent-connector
11 | []
12 | (try
13 | (let [con (SSHAgentConnector.)]
14 | (when (.isAvailable con)
15 | con))
16 | (catch AgentProxyException e
17 | (logging/warnf
18 | e "Failed to load JNA connector, although SSH_AUTH_SOCK is set"))))
19 |
20 | (defn pageant-connector
21 | []
22 | (try
23 | (let [con (PageantConnector.)]
24 | (when (.isAvailable con)
25 | con))
26 | (catch AgentProxyException e
27 | (logging/warn
28 | e "Failed to load Pageant connector, although running on windows"))))
29 |
30 | (defn connect
31 | "Connect the specified jsch object to the system ssh-agent."
32 | [^JSch jsch]
33 | (when-let [connector (or (sock-agent-connector) (pageant-connector))]
34 | (doto jsch
35 | ;(.setConfig "PreferredAuthentications" "publickey")
36 | (.setIdentityRepository (AgentIdentityRepository. connector)))))
37 |
--------------------------------------------------------------------------------
/src/clj_ssh/cli.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.cli
2 | "Provides a REPL friendly interface for clj-ssh.
3 |
4 | (use 'clj-ssh.cli)
5 |
6 | Use `ssh` to execute a command, say `ls`, on a remote host \"my-host\",
7 |
8 | (ssh \"my-host\" \"ls\")
9 | => {:exit 0 :out \"file1\\nfile2\\n\" :err \"\")
10 |
11 | By default this will use the system ssh-agent to obtain your ssh keys, and it
12 | uses your current username, but this can be specified:
13 |
14 | (ssh \"my-host\" \"ls\" :username \"remote-user\")
15 | => {:exit 0 :out \"file1\\nfile2\\n\" :err \"\")
16 |
17 | Strict host key checking can be turned off:
18 |
19 | (default-session-options {:strict-host-key-checking :no})
20 |
21 | SFTP is also supported. For example, to copy a local file to a remote host
22 | \"my-host\":
23 |
24 | (sftp \"my-host\" :put \"/from/this/path\" \"to/this/path\")
25 |
26 | Note that any sftp commands that change the state of the sftp session (such as
27 | cd) do not work with the simplified interface, as a new session is created each
28 | time.
29 |
30 | If your key has a passphrase, you will need to explicitly add your key either to
31 | the system's ssh-agent, or to clj-ssh's ssh-agent with the appropriate
32 | `add-identity` call."
33 | (:require
34 | [clj-ssh.ssh :as ssh]
35 | [clojure.string :as string]))
36 |
37 | ;;; Agent
38 | (def ^{:doc "SSH agent used to manage identities." :dynamic true}
39 | *ssh-agent* (ssh/ssh-agent {}))
40 |
41 | (defmacro with-ssh-agent
42 | "Bind an ssh-agent for use as identity manager. An existing agent instance is
43 | passed as the first argument."
44 | [agent & body]
45 | `(binding [*ssh-agent* ~agent] ~@body))
46 |
47 | ;;; Identities
48 | (defn has-identity?
49 | "Check if the given identity is present."
50 | [name] (ssh/has-identity? *ssh-agent* name))
51 |
52 | (defn add-identity
53 | "Add an identity to the agent.
54 |
55 | :private-key A string specifying the private key
56 | :public-key A string specifying the public key
57 | :private-key-path A string specifying a path to the private key
58 | :public-key-path A string specifying a path to the public key
59 | :identity A jsch Identity object (see make-identity)
60 | :passphrase A byte array containing the passphrase"
61 | [& {:keys [agent
62 | ^String name
63 | ^String public-key
64 | ^String private-key
65 | public-key-path
66 | private-key-path
67 | ^Identity identity
68 | ^bytes passphrase]
69 | :or {agent *ssh-agent*}
70 | :as options}]
71 | {:pre [(map? options)]}
72 | (ssh/add-identity agent (dissoc options :agent)))
73 |
74 | (defn add-identity-with-keychain
75 | "Add a private key, only if not already known, using the keychain to obtain
76 | a passphrase if required"
77 | [& {:keys [agent
78 | ^String name
79 | ^String public-key-path
80 | ^String private-key-path
81 | ^Identity identity
82 | ^bytes passphrase]
83 | :or {agent *ssh-agent*}
84 | :as options}]
85 | (ssh/add-identity-with-keychain agent (dissoc options :agent)))
86 |
87 | ;;; Session
88 | (def
89 | ^{:doc "Default SSH options"
90 | :dynamic true}
91 | *default-session-options* {})
92 |
93 | (defmacro with-default-session-options
94 | "Set the default session options"
95 | [options & body]
96 | `(binding [*default-session-options* ~options]
97 | ~@body))
98 |
99 | (defn default-session-options
100 | "Set the default session options"
101 | [options]
102 | {:pre [(map? options)]}
103 | (alter-var-root #'*default-session-options* #(identity %2) options))
104 |
105 | (defn- default-session [agent hostname options]
106 | (ssh/session agent hostname (merge *default-session-options* options)))
107 |
108 | (defn session
109 | "Start a SSH session.
110 | Requires hostname. you can also pass values for :username, :password and :port
111 | keys. All other option key pairs will be passed as SSH config options."
112 | [hostname & {:keys [port username agent password]
113 | :or {agent *ssh-agent* port 22}
114 | :as options}]
115 | (default-session agent hostname (dissoc options :agent)))
116 |
117 | ;;; Operations
118 | (defn- parse-args
119 | "Takes a seq of 'ssh' arguments and returns a map of option keywords
120 | to option values."
121 | [args]
122 | (loop [[arg :as args] args
123 | opts {:args []}]
124 | (if-not args
125 | opts
126 | (if (keyword? arg)
127 | (recur (nnext args) (assoc opts arg (second args)))
128 | (recur (next args) (update-in opts [:args] conj arg))))))
129 |
130 | (defn ssh
131 | "Execute commands over ssh.
132 |
133 | Options are:
134 |
135 | :cmd specifies a command to exec. If no cmd is given, a shell is started
136 | and input is taken from :in.
137 | :in specifies input to the remote shell. A string or a stream.
138 |
139 | :out specify :stream to obtain a an [inputstream shell]
140 | specify :bytes to obtain a byte array
141 | or specify a string with an encoding specification for a
142 | result string.
143 | In the case of :stream, the shell can be polled for connected
144 | status, and the session (in the :session key of the return value)
145 | must be disconnected by the caller.
146 | :username username to use for authentication
147 | :password password to use for authentication
148 | :port port to use if no session specified
149 |
150 | sh returns a map of
151 | :exit => sub-process's exit code
152 | :out => sub-process's stdout (as byte[] or String)
153 | :err => sub-process's stderr (as byte[] or String)"
154 | [hostname & args]
155 | (let [{:keys [cmd in out username password port ssh-agent args]
156 | :or {ssh-agent *ssh-agent*}
157 | :as options} (parse-args args)
158 | session (default-session ssh-agent hostname options)
159 | arg (if (seq args)
160 | (merge {:cmd (string/join " " args)} options)
161 | options)]
162 | (if (= :stream (:out options))
163 | (do
164 | (ssh/connect session)
165 | (assoc (ssh/ssh session arg)
166 | :session session))
167 | (ssh/with-connection session
168 | (ssh/ssh session arg)))))
169 |
170 | (defn sftp
171 | "Execute SFTP commands.
172 |
173 | sftp host-or-session cmd & options
174 |
175 | cmd specifies a command to exec. Valid commands are:
176 | :ls
177 | :put
178 | :get
179 | :chmod
180 | :chown
181 | :chgrp
182 | :cd
183 | :lcd
184 | :pwd
185 | :lpwd
186 | :rm
187 | :rmdir
188 | :stat
189 | :symlink
190 |
191 | Options are
192 | :username username to use for authentication
193 | :password password to use for authentication
194 | :port port to use if no session specified
195 | :with-monitor
196 | :modes"
197 | [hostname cmd & args]
198 | (let [{:keys [ssh-agent args]
199 | :or {ssh-agent *ssh-agent*}
200 | :as options} (parse-args args)
201 | session (default-session ssh-agent hostname options)]
202 | (ssh/with-connection session
203 | (apply ssh/sftp session (dissoc options :args) cmd args))))
204 |
205 | ;;; Keypairs
206 | (defn generate-keypair
207 | "Generate a keypair, returned as [private public] byte arrays.
208 | Valid types are :rsa and :dsa. key-size is in bytes. passphrase
209 | can be a string or byte array."
210 | ([key-type key-size passphrase]
211 | (ssh/generate-keypair *ssh-agent* key-type key-size passphrase)))
212 |
--------------------------------------------------------------------------------
/src/clj_ssh/keychain.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.keychain
2 | "Primitive keychain support for clj-ssh. Only implemented on OSX at the
3 | moment."
4 | (:require
5 | [clojure.tools.logging :as logging]
6 | [clojure.java.shell :as shell]))
7 |
8 | (defn ask-passphrase [path]
9 | (if-let [console (. System console)]
10 | (do (print "Passphrase for" path ": ")
11 | (.readPassword console))
12 | (throw (ex-info "No means to ask for passphrase"
13 | {:type :clj-ssh/no-passphrase-available}))))
14 |
15 | (defmulti keychain-passphrase "Obtain password for path"
16 | (fn [system path] system))
17 |
18 | (defmethod keychain-passphrase :default
19 | [system path]
20 | (logging/warn "Passphrase required, but no keychain implemented.")
21 | (ask-passphrase path))
22 |
23 | (defmethod keychain-passphrase "Mac OS X"
24 | [system path]
25 | (let [result (shell/sh
26 | "/usr/bin/security" "find-generic-password" "-a"
27 | (format "%s" path)
28 | "-g")]
29 | (when (zero? (result :exit))
30 | (when-let [^String pw (second
31 | (re-find #"password: \"(.*)\"" (result :err)))]
32 | (.getBytes pw "UTF-8")))))
33 |
34 | (defn passphrase
35 | "Obtain a passphrase for the given key path"
36 | [path]
37 | (keychain-passphrase (System/getProperty "os.name") path))
38 |
--------------------------------------------------------------------------------
/src/clj_ssh/reflect.clj:
--------------------------------------------------------------------------------
1 | ;;;; taken from clojure.contrib.reflect 1.2.0
2 |
3 | ;;; Copyright (c) 2010 Stuart Halloway & Contributors. All rights
4 | ;;; reserved. The use and distribution terms for this software are
5 | ;;; covered by the Eclipse Public License 1.0
6 | ;;; (http://opensource.org/licenses/eclipse-1.0.php) which can be
7 | ;;; found in the file epl-v10.html at the root of this distribution.
8 | ;;; By using this software in any fashion, you are agreeing to be
9 | ;;; bound by the terms of this license. You must not remove this
10 | ;;; notice, or any other, from this software.
11 |
12 | (ns clj-ssh.reflect)
13 |
14 | (defn call-method
15 | "Calls a private or protected method.
16 |
17 | params is a vector of classes which correspond to the arguments to
18 | the method e
19 |
20 | obj is nil for static methods, the instance object otherwise.
21 |
22 | The method-name is given a symbol or a keyword (something Named)."
23 | [^Class klass method-name params obj & args]
24 | (-> klass (.getDeclaredMethod (name method-name)
25 | (into-array Class params))
26 | (doto (.setAccessible true))
27 | (.invoke obj (into-array Object args))))
28 |
29 | (defn get-field
30 | "Access to private or protected field. field-name is a symbol or
31 | keyword."
32 | [^Class klass field-name obj]
33 | (->
34 | klass
35 | (.getDeclaredField (name field-name))
36 | (doto (.setAccessible true))
37 | (.get obj)))
38 |
--------------------------------------------------------------------------------
/src/clj_ssh/ssh.clj:
--------------------------------------------------------------------------------
1 | (ns ^{:author "Hugo Duncan"}
2 | clj-ssh.ssh
3 | "API for using SSH in clojure.
4 |
5 | ## Usage
6 |
7 | (use 'clj-ssh.ssh)
8 |
9 | (let [agent (ssh-agent {})]
10 | (add-identity agent {:private-key-path path-to-private-key})
11 | (let [session (session agent hostname :strict-host-key-checking :no)]
12 | (with-connection session
13 | (let [result (ssh session {:in commands-string})]
14 | (println (:out result)))
15 | (let [result (ssh session {:cmd some-cmd-string})]
16 | (println (:out result))))))
17 |
18 | (let [agent (ssh-agent {})]
19 | (let [session (session agent \"localhost\" {:strict-host-key-checking :no})]
20 | (with-connection session
21 | (let [channel (ssh-sftp session)]
22 | (with-channel-connection channel
23 | (sftp channel :cd \"/remote/path\")
24 | (sftp channel :put \"/some/file\" \"filename\"))))))"
25 | (:require
26 | [clj-ssh.agent :as agent]
27 | [clj-ssh.keychain :as keychain]
28 | [clj-ssh.reflect :as reflect]
29 | [clj-ssh.ssh.protocols :as protocols]
30 | [clojure.java.io :as io]
31 | [clojure.string :as string]
32 | [clojure.tools.logging :as logging])
33 | (:import
34 | [java.io
35 | File InputStream OutputStream StringReader
36 | FileInputStream FileOutputStream
37 | ByteArrayInputStream ByteArrayOutputStream
38 | PipedInputStream PipedOutputStream]
39 | [com.jcraft.jsch
40 | JSch Session Channel ChannelShell ChannelExec ChannelSftp JSchException
41 | Identity IdentityFile IdentityRepository Logger KeyPair LocalIdentityRepository]))
42 |
43 | ;;; forward jsch's logging to java logging
44 | (def ^{:dynamic true}
45 | ssh-log-levels
46 | (atom
47 | {com.jcraft.jsch.Logger/DEBUG :trace
48 | com.jcraft.jsch.Logger/INFO :debug
49 | com.jcraft.jsch.Logger/WARN :warn
50 | com.jcraft.jsch.Logger/ERROR :error
51 | com.jcraft.jsch.Logger/FATAL :fatal}))
52 |
53 | (deftype SshLogger
54 | [log-level]
55 | com.jcraft.jsch.Logger
56 | (isEnabled
57 | [_ level]
58 | (>= level log-level))
59 | (log
60 | [_ level message]
61 | (logging/log "clj-ssh.ssh" (@ssh-log-levels level) nil message)))
62 |
63 | (JSch/setLogger (SshLogger. com.jcraft.jsch.Logger/DEBUG))
64 |
65 | ;;; Helpers
66 | (defn- ^String file-path [string-or-file]
67 | (if (string? string-or-file)
68 | string-or-file
69 | (.getPath ^File string-or-file)))
70 |
71 | (defn ^String capitalize
72 | "Converts first character of the string to upper-case."
73 | [^String s]
74 | (if (< (count s) 2)
75 | (.toUpperCase s)
76 | (str (.toUpperCase ^String (subs s 0 1))
77 | (subs s 1))))
78 |
79 | (defn- camelize [^String a]
80 | (apply str (map capitalize (.split a "-"))))
81 |
82 | (defn- ^String as-string [arg]
83 | (cond
84 | (symbol? arg) (name arg)
85 | (keyword? arg) (name arg)
86 | :else (str arg)))
87 |
88 | (def ^java.nio.charset.Charset ascii
89 | (java.nio.charset.Charset/forName "US-ASCII"))
90 |
91 | (def ^java.nio.charset.Charset utf-8
92 | (java.nio.charset.Charset/forName "UTF-8"))
93 |
94 | (defn- ^{:tag (Class/forName "[B")} as-bytes
95 | "Return arg as a byte array. arg must be a string or a byte array."
96 | [arg]
97 | (if (string? arg)
98 | (.getBytes ^String arg ascii)
99 | arg))
100 |
101 | (defn ssh-agent?
102 | "Predicate to test for an ssh-agent."
103 | [object] (instance? JSch object))
104 |
105 | ;;; Session extension
106 |
107 | ;; This is only relevant if you want to support using jump hosts. If
108 | ;; you do, the you should always use the `the-session` to get the jsch
109 | ;; session object once connected.
110 |
111 | ;; This is here since JSch Session has a package scoped constructor,
112 | ;; and doesn't implement an interface, so provides no means for
113 | ;; extending it.
114 |
115 | (extend-protocol protocols/Session
116 | Session
117 | (connect
118 | ([session] (.connect session))
119 | ([session timeout] (.connect session timeout)))
120 | (connected? [session] (.isConnected session))
121 | (disconnect [session] (.disconnect session))
122 | (session [session] session))
123 |
124 | (defn ^Session the-session
125 | "Return the JSch session for the given session."
126 | [session]
127 | (protocols/session session))
128 |
129 | (defn session?
130 | "Predicate to test for a session"
131 | [x]
132 | (satisfies? protocols/Session x))
133 |
134 | ;;; Agent
135 | (def ^:private hosts-file
136 | "Something to lock to tray and prevent concurrent updates/reads to
137 | hosts file."
138 | (Object.))
139 |
140 | (defn ssh-agent
141 | "Create a ssh-agent. By default a system ssh-agent is preferred."
142 | [{:keys [use-system-ssh-agent ^String known-hosts-path]
143 | :or {use-system-ssh-agent true
144 | known-hosts-path (str (. System getProperty "user.home")
145 | "/.ssh/known_hosts")}}]
146 | (let [agent (JSch.)]
147 | (when use-system-ssh-agent
148 | (agent/connect agent))
149 | (when known-hosts-path
150 | (locking hosts-file
151 | (.setKnownHosts agent known-hosts-path)))
152 | agent))
153 |
154 | ;;; Identities
155 | (defn has-identity?
156 | "Check if the given identity is present."
157 | [^JSch agent name]
158 | (some #(= name %) (.getIdentityNames agent)))
159 |
160 | (defn ^Identity make-identity
161 | "Create a JSch identity. This can be used to check whether the key is
162 | encrypted."
163 | [^JSch agent ^String private-key-path ^String public-key-path]
164 | (logging/tracef "Make identity %s %s" private-key-path public-key-path)
165 | (.addIdentity agent private-key-path public-key-path nil)
166 | (.lastElement (.getIdentities (.getIdentityRepository agent))))
167 |
168 | (defn ^KeyPair keypair
169 | "Return a KeyPair object for the given options.
170 |
171 | :private-key A string specifying the private key
172 | :public-key A string specifying the public key
173 | :private-key-path A string specifying a path to the private key
174 | :public-key-path A string specifying a path to the public key
175 | :passphrase A byte array containing the passphrase
176 | :comment A comment for the key"
177 | [^JSch agent {:keys [^String public-key
178 | ^String private-key
179 | ^String public-key-path
180 | ^String private-key-path
181 | ^String comment
182 | ^bytes passphrase]
183 | :as options}]
184 | {:pre [(map? options)]}
185 | (cond
186 | private-key
187 | (let [^KeyPair keypair
188 | (KeyPair/load agent (as-bytes private-key) (as-bytes public-key))]
189 | (when passphrase
190 | (.decrypt keypair passphrase))
191 | (.setPublicKeyComment keypair comment)
192 | keypair)
193 |
194 | public-key
195 | (let [^KeyPair keypair (KeyPair/load agent nil (as-bytes public-key))]
196 | (.setPublicKeyComment keypair comment)
197 | keypair)
198 |
199 | (and public-key-path private-key-path)
200 | (let [keypair (KeyPair/load agent private-key-path public-key-path)]
201 | (when passphrase
202 | (.decrypt keypair passphrase))
203 | (.setPublicKeyComment keypair comment)
204 | keypair)
205 |
206 | private-key-path
207 | (let [keypair (KeyPair/load agent private-key-path)]
208 | (when passphrase
209 | (.decrypt keypair passphrase))
210 | (.setPublicKeyComment keypair comment)
211 | keypair)
212 |
213 | :else
214 | (throw
215 | (ex-info
216 | "Don't know how to create keypair"
217 | {:reason :do-not-know-how-to-create-keypair
218 | :args options}))))
219 |
220 | (defn fingerprint
221 | "Return a keypair's fingerprint."
222 | [^KeyPair keypair]
223 | (.getFingerPrint keypair))
224 |
225 | (defn copy-identities
226 | [^JSch from-agent ^JSch to-agent]
227 | (let [^IdentityRepository ir (.getIdentityRepository from-agent)]
228 | (doseq [^Identity id (.getIdentities ir)]
229 | (.addIdentity to-agent id nil))))
230 |
231 | ;; JSch's IdentityFile has a private constructor that would let us avoid this
232 | ;; were it public.
233 | (deftype KeyPairIdentity [^JSch jsch ^String identity ^KeyPair kpair]
234 | Identity
235 | (^boolean setPassphrase [_ ^bytes passphrase] (.. kpair (decrypt passphrase)))
236 | (getPublicKeyBlob [_] (.. kpair getPublicKeyBlob))
237 | (^bytes getSignature [_ ^bytes data] (.. kpair (getSignature data)))
238 | (^bytes getSignature [_ ^bytes data ^String alg] (.. kpair (getSignature data alg)))
239 | (getAlgName [_]
240 | (.getKeyTypeString kpair))
241 | (getName [_] identity)
242 | (isEncrypted [_] (.. kpair isEncrypted))
243 | (clear [_] (.. kpair dispose)))
244 |
245 | (defn add-identity
246 | "Add an identity to the agent. The identity is passed with the :identity
247 | keyword argument, or constructed from the other keyword arguments.
248 |
249 | :private-key A string specifying the private key
250 | :public-key A string specifying the public key
251 | :private-key-path A string specifying a path to the private key
252 | :public-key-path A string specifying a path to the public key
253 | :identity A jsch Identity object (see make-identity)
254 | :passphrase A byte array containing the passphrase"
255 | [^JSch agent {:keys [^String name
256 | ^String public-key
257 | ^String private-key
258 | ^String public-key-path
259 | ^String private-key-path
260 | ^Identity identity
261 | ^bytes passphrase]
262 | :as options}]
263 | {:pre [(map? options)]}
264 | (let [^String comment (or name private-key-path public-key)
265 | ^Identity identity
266 | (or identity
267 | (KeyPairIdentity.
268 | agent comment (keypair agent (assoc options :comment comment))))]
269 | (.addIdentity agent identity passphrase)))
270 |
271 | (defn add-identity-with-keychain
272 | "Add a private key, only if not already known, using the keychain to obtain
273 | a passphrase if required"
274 | [^JSch agent {:keys [^String name
275 | ^String public-key-path
276 | ^String private-key-path
277 | ^Identity identity
278 | ^bytes passphrase]
279 | :as options}]
280 | (logging/debugf
281 | "add-identity-with-keychain has-identity? %s" (has-identity? agent name))
282 | (when-not (has-identity? agent name)
283 | (let [name (or name private-key-path)
284 | public-key-path (or public-key-path (str private-key-path ".pub"))
285 | identity (if private-key-path
286 | (make-identity
287 | agent
288 | (file-path private-key-path)
289 | (file-path public-key-path)))]
290 | (logging/debugf
291 | "add-identity-with-keychain is-encrypted? %s" (.isEncrypted identity))
292 | (if (.isEncrypted identity)
293 | (if-let [passphrase (keychain/passphrase private-key-path)]
294 | (add-identity agent (assoc options :passphrase passphrase))
295 | (do
296 | (logging/error "Passphrase required, but none findable.")
297 | (throw
298 | (ex-info
299 | (str "Passphrase required for key " name ", but none findable.")
300 | {:reason :passphrase-not-found
301 | :key-name name}))))
302 | (add-identity agent options)))))
303 |
304 | ;;; Sessions
305 | (defn- init-session
306 | "Initialise options on a session"
307 | [^Session session ^String password options]
308 | (when password
309 | (.setPassword session password))
310 | (doseq [[k v :as option] options]
311 | (.setConfig
312 | session
313 | (if (string? k)
314 | k
315 | (camelize (as-string k)))
316 | (as-string v))))
317 |
318 | (defn- ^Session session-impl
319 | [^JSch agent hostname username port ^String password options]
320 | (doto (.getSession agent username hostname port)
321 | (init-session password options)))
322 |
323 | (defn- session-options
324 | [options]
325 | (dissoc options :username :port :password :agent))
326 |
327 | (defn ^Session session
328 | "Start a SSH session.
329 | Requires hostname. You can also pass values for :username, :password and :port
330 | keys. All other option key pairs will be passed as SSH config options."
331 | [^JSch agent hostname
332 | {:keys [port username password] :or {port 22} :as options}]
333 | (session-impl
334 | agent hostname
335 | (or username (System/getProperty "user.name"))
336 | port
337 | password
338 | (session-options options)))
339 |
340 | (defn ^String session-hostname
341 | "Return the hostname for a session"
342 | [^Session session]
343 | (.getHost session))
344 |
345 | (defn ^int session-port
346 | "Return the port for a session"
347 | [^Session session]
348 | (.getPort session))
349 |
350 | (defn forward-remote-port
351 | "Start remote port forwarding"
352 | ([^Session session remote-port local-port ^String local-host]
353 | (.setPortForwardingR
354 | session (int remote-port) local-host (int local-port)))
355 | ([session remote-port local-port]
356 | (forward-remote-port session remote-port local-port "localhost")))
357 |
358 | (defn unforward-remote-port
359 | "Remove remote port forwarding"
360 | [^Session session remote-port]
361 | (.delPortForwardingR session remote-port))
362 |
363 | (defmacro with-remote-port-forward
364 | "Creates a context in which a remote SSH tunnel is established for the
365 | session. (Use after the connection is opened.)"
366 | [[session remote-port local-port & [local-host & _]] & body]
367 | `(try
368 | (forward-remote-port
369 | ~session ~remote-port ~local-port ~(or local-host "localhost"))
370 | ~@body
371 | (finally
372 | (unforward-remote-port ~session ~remote-port))))
373 |
374 | (defn forward-local-port
375 | "Start local port forwarding. Returns the actual local port."
376 | ([^Session session local-port remote-port remote-host]
377 | (.setPortForwardingL session local-port remote-host remote-port))
378 | ([session local-port remote-port]
379 | (forward-local-port session local-port remote-port "localhost")))
380 |
381 | (defn unforward-local-port
382 | "Remove local port forwarding"
383 | [^Session session local-port]
384 | (.delPortForwardingL session local-port))
385 |
386 | (defmacro with-local-port-forward
387 | "Creates a context in which a local SSH tunnel is established for the session.
388 | (Use after the connection is opened.)"
389 | [[session local-port remote-port & [remote-host & _]] & body]
390 | `(try
391 | (forward-local-port
392 | ~session ~local-port ~remote-port ~(or remote-host "localhost"))
393 | ~@body
394 | (finally
395 | (unforward-local-port ~session ~local-port))))
396 |
397 | (defn connect
398 | "Connect a session."
399 | ([session]
400 | (locking hosts-file
401 | (protocols/connect session)))
402 | ([session timeout]
403 | (locking hosts-file
404 | (protocols/connect session timeout))))
405 |
406 | (defn disconnect
407 | "Disconnect a session."
408 | [session]
409 | (protocols/disconnect session)
410 | (when-let [^Thread t (and (instance? Session session)
411 | (reflect/get-field
412 | com.jcraft.jsch.Session 'connectThread session))]
413 | (when (.isAlive t)
414 | (.interrupt t))))
415 |
416 | (defn connected?
417 | "Predicate used to test for a connected session."
418 | [session]
419 | (protocols/connected? session))
420 |
421 | (defmacro with-connection
422 | "Creates a context in which the session is connected. Ensures the session is
423 | disconnected on exit."
424 | [session & body]
425 | `(let [session# ~session]
426 | (try
427 | (when-not (connected? session#)
428 | (connect session#))
429 | ~@body
430 | (finally
431 | (disconnect session#)))))
432 |
433 | ;;; Jump Hosts
434 | (defn- jump-connect [agent hosts sessions timeout]
435 | (let [host (first hosts)
436 | s (session agent (:hostname host) (dissoc host :hostname))
437 | throw-e (fn [e s]
438 | (throw
439 | (ex-info
440 | (str "Failed to connect "
441 | (.getUserName s) "@"
442 | (.getHost s) ":"
443 | (.getPort s)
444 | " " (pr-str (into [] (.getIdentityNames agent)))
445 | " " (pr-str hosts))
446 | {:hosts hosts}
447 | e)))]
448 | (swap! sessions (fnil conj []) s)
449 | (try
450 | (connect s timeout)
451 | (catch Exception e (throw-e e s)))
452 | (.setDaemonThread s true)
453 | (loop [hosts (rest hosts)
454 | prev-s s]
455 | (if-let [{:keys [hostname port username password]
456 | :or {port 22}
457 | :as options}
458 | (first hosts)]
459 | (let [p (forward-local-port prev-s 0 port hostname)
460 | options (-> options
461 | (dissoc :hostname)
462 | (assoc :port p))
463 | s (session agent "localhost" options)]
464 | (.setDaemonThread s true)
465 | (.setHostKeyAlias s hostname)
466 | (swap! sessions conj s)
467 | (try
468 | (connect s timeout)
469 | (catch Exception e (throw-e e s)))
470 | (recur (rest hosts) s))))))
471 |
472 | (defn- jump-connected? [sessions]
473 | (seq @sessions))
474 |
475 | (defn- jump-disconnect
476 | [sessions]
477 | (doseq [s (reverse @sessions)]
478 | (.disconnect s))
479 | (reset! sessions nil))
480 |
481 | (defn- jump-the-session
482 | [sessions]
483 | (assert (jump-connected? sessions) "not connected")
484 | (last @sessions))
485 |
486 | (deftype JumpHostSession [agent hosts sessions timeout]
487 | protocols/Session
488 | (connect [session] (protocols/connect session timeout))
489 | (connect [session timeout] (jump-connect agent hosts sessions timeout))
490 | (connected? [session] (jump-connected? sessions))
491 | (disconnect [session] (jump-disconnect sessions))
492 | (session [session] (jump-the-session sessions)))
493 |
494 | ;; http://www.jcraft.com/jsch/examples/JumpHosts.java.html
495 | (defn jump-session
496 | "Connect via a sequence of jump hosts. Returns a session. Once the
497 | session is connected, use `the-session` to get a jsch Session object.
498 |
499 | Each host is a map with :hostname, :username, :password and :port
500 | keys. All other key pairs in each host map will be passed as SSH
501 | config options."
502 | [^JSch agent hosts {:keys [timeout]}]
503 | (when-not (seq hosts)
504 | (throw (ex-info "Must provide at least one host to connect to"
505 | {:hosts hosts})))
506 | (JumpHostSession. agent hosts (atom []) (or timeout 0)))
507 |
508 | ;;; Channels
509 | (defn connect-channel
510 | "Connect a channel."
511 | [^Channel channel]
512 | (.connect channel))
513 |
514 | (defn disconnect-channel
515 | "Disconnect a session."
516 | [^Channel channel]
517 | (.disconnect channel))
518 |
519 | (defn connected-channel?
520 | "Predicate used to test for a connected channel."
521 | [^Channel channel]
522 | (.isConnected channel))
523 |
524 | (defmacro with-channel-connection
525 | "Creates a context in which the channel is connected. Ensures the channel is
526 | disconnected on exit."
527 | [channel & body]
528 | `(let [channel# ~channel]
529 | (try
530 | (when-not (connected-channel? channel#)
531 | (connect-channel channel#))
532 | ~@body
533 | (finally
534 | (disconnect-channel channel#)))))
535 |
536 | (defn open-channel
537 | "Open a channel of the specified type in the session."
538 | [^Session session session-type]
539 | (try
540 | (.openChannel session (name session-type))
541 | (catch JSchException e
542 | (let [msg (.getMessage e)]
543 | (cond
544 | (= msg "session is down")
545 | (throw (ex-info (format "clj-ssh open-channel failure: %s" msg)
546 | {:type :clj-ssh/open-channel-failure
547 | :reason :clj-ssh/session-down}
548 | e))
549 | (= msg "channel is not opened.")
550 | (throw (ex-info
551 | (format
552 | "clj-ssh open-channel failure: %s (possible session timeout)"
553 | msg)
554 | {:type :clj-ssh/open-channel-failure
555 | :reason :clj-ssh/channel-open-failed}
556 | e))
557 | :else (throw (ex-info (format "clj-ssh open-channel failure: %s" msg)
558 | {:type :clj-ssh/open-channel-failure
559 | :reason :clj-ssh/unknown}
560 | e)))))))
561 |
562 | (defn sftp-channel
563 | "Open a SFTP channel in the session."
564 | [^Session session]
565 | (open-channel session :sftp))
566 |
567 | (defn exec-channel
568 | "Open an Exec channel in the session."
569 | [^Session session]
570 | (open-channel session :exec))
571 |
572 | (defn shell-channel
573 | "Open a Shell channel in the session."
574 | [^Session session]
575 | (open-channel session :shell))
576 |
577 | (defn exit-status
578 | "Return the exit status of a channel."
579 | [^Channel channel]
580 | (.getExitStatus channel))
581 |
582 | (def
583 | ^{:dynamic true
584 | :doc (str "The buffer size (in bytes) for the piped stream used to implement
585 | the :stream option for :out. If your ssh commands generate a high volume of
586 | output, then this buffer size can become a bottleneck. You might also
587 | increase the frequency with which you read the output stream if this is an
588 | issue.")}
589 | *piped-stream-buffer-size* (* 1024 10))
590 |
591 | (defn- streams-for-out
592 | [out]
593 | (if (= :stream out)
594 | (let [os (PipedOutputStream.)]
595 | [os (PipedInputStream. os (int *piped-stream-buffer-size*))])
596 | [(ByteArrayOutputStream.) nil]))
597 |
598 | (defn- streams-for-in
599 | []
600 | (let [os (PipedInputStream. (int *piped-stream-buffer-size*))]
601 | [os (PipedOutputStream. os)]))
602 |
603 | (defn string-stream
604 | "Return an input stream with content from the string s."
605 | [^String s]
606 | {:pre [(string? s)]}
607 | (ByteArrayInputStream. (.getBytes s utf-8)))
608 |
609 | (defn ssh-shell-proc
610 | "Run a ssh-shell."
611 | [^Session session in {:keys [agent-forwarding pty out err] :as opts}]
612 | {:pre [in]}
613 | (let [^ChannelShell shell (open-channel session :shell)]
614 | (doto shell
615 | (.setInputStream in false))
616 | (when out
617 | (.setOutputStream shell out))
618 | (when (contains? opts :pty)
619 | (.setPty shell (boolean (opts :pty))))
620 | (when (contains? opts :agent-forwarding)
621 | (.setAgentForwarding shell (boolean (opts :agent-forwarding))))
622 | (connect-channel shell)
623 | {:channel shell
624 | :out (or out (.getInputStream shell))
625 | :in (or in (.getOutputStream shell))}))
626 |
627 | (defn ssh-shell
628 | "Run a ssh-shell."
629 | [^Session session in out opts]
630 | (let [[out-stream out-inputstream] (streams-for-out out)
631 | resp (ssh-shell-proc
632 | session
633 | (if (string? in) (string-stream (str in ";exit $?;\n")) in)
634 | (merge {:out out-stream} opts))
635 | ^ChannelShell shell (:channel resp)]
636 | (if out-inputstream
637 | {:channel shell :out-stream out-inputstream}
638 | (with-channel-connection shell
639 | (while (connected-channel? shell)
640 | (Thread/sleep 100))
641 | {:exit (.getExitStatus shell)
642 | :out (if (= :bytes out)
643 | (.toByteArray ^ByteArrayOutputStream out-stream)
644 | (.toString out-stream))}))))
645 |
646 | (defn ssh-exec-proc
647 | "Run a command via exec, returning a map with the process streams."
648 | [^Session session ^String cmd
649 | {:keys [agent-forwarding pty in out err] :as opts}]
650 | (let [^ChannelExec exec (open-channel session :exec)]
651 | (doto exec
652 | (.setCommand cmd)
653 | (.setInputStream in false))
654 | (when (contains? opts :pty)
655 | (.setPty exec (boolean (opts :pty))))
656 | (when (contains? opts :agent-forwarding)
657 | (.setAgentForwarding exec (boolean (opts :agent-forwarding))))
658 |
659 | (when out
660 | (.setOutputStream exec out))
661 | (when err
662 | (.setErrStream exec err))
663 | (let [resp {:channel exec
664 | :out (or out (.getInputStream exec))
665 | :err (or err (.getErrStream exec))
666 | :in (or in (.getOutputStream exec))}]
667 | (connect-channel exec)
668 | resp)))
669 |
670 | (defn ssh-exec
671 | "Run a command via ssh-exec."
672 | [^Session session ^String cmd in out opts]
673 | (let [[^PipedOutputStream out-stream
674 | ^PipedInputStream out-inputstream] (streams-for-out out)
675 | [^PipedOutputStream err-stream
676 | ^PipedInputStream err-inputstream] (streams-for-out out)
677 | proc (ssh-exec-proc
678 | session cmd
679 | (merge
680 | {:in (if (string? in) (string-stream in) in)
681 | :out out-stream
682 | :err err-stream}
683 | opts))
684 | ^ChannelExec exec (:channel proc)]
685 | (if out-inputstream
686 | {:channel exec
687 | :out-stream out-inputstream
688 | :err-stream err-inputstream}
689 | (do (while (connected-channel? exec)
690 | (Thread/sleep 100))
691 | {:exit (.getExitStatus exec)
692 | :out (if (= :bytes out)
693 | (.toByteArray ^ByteArrayOutputStream out-stream)
694 | (.toString out-stream))
695 | :err (if (= :bytes out)
696 | (.toByteArray ^ByteArrayOutputStream err-stream)
697 | (.toString err-stream))}))))
698 |
699 | (defn ssh
700 | "Execute commands over ssh.
701 |
702 | Options are:
703 |
704 | :cmd specifies a command string to exec. If no cmd is given, a shell
705 | is started and input is taken from :in.
706 | :in specifies input to the remote shell. A string or a stream.
707 | :out specify :stream to obtain a an [inputstream shell]
708 | specify :bytes to obtain a byte array
709 | or specify a string with an encoding specification for a
710 | result string. In the case of :stream, the shell can
711 | be polled for connected status.
712 |
713 | sh returns a map of
714 | :exit => sub-process's exit code
715 | :out => sub-process's stdout (as byte[] or String)
716 | :err => sub-process's stderr (as byte[] or String)"
717 | [session {:keys [cmd in out] :as options}]
718 | (let [connected (connected? session)]
719 | (try
720 | (when-not connected
721 | (connect session))
722 | (if cmd
723 | (ssh-exec session cmd in out (dissoc options :in :out :cmd))
724 | (ssh-shell session in out (dissoc options :in :out :cmd))))))
725 |
726 | (defn ssh-sftp
727 | "Obtain a connected ftp channel."
728 | [^Session session]
729 | {:pre (connected? session)}
730 | (let [channel (open-channel session :sftp)]
731 | (connect-channel channel)
732 | channel))
733 |
734 | (defmacro memfn-varargs [name klass]
735 | `(fn [^{:tag ~klass} target# args#]
736 | (condp = (count args#)
737 | 0 (. target# (~name))
738 | 1 (. target# (~name (first args#)))
739 | 2 (. target# (~name (first args#) (second args#)))
740 | 3 (. target# (~name (first args#) (second args#) (nth args# 2)))
741 | 4 (. target#
742 | (~name (first args#) (second args#) (nth args# 2) (nth args# 3)))
743 | 5 (. target#
744 | (~name (first args#) (second args#) (nth args# 2) (nth args# 3)
745 | (nth args# 4)))
746 | (throw
747 | (java.lang.IllegalArgumentException.
748 | (str "Too many arguments passed. Limit 5, passed " (count args#)))))))
749 |
750 | (def sftp-modemap { :overwrite ChannelSftp/OVERWRITE
751 | :resume ChannelSftp/RESUME
752 | :append ChannelSftp/APPEND })
753 |
754 | (defn ssh-sftp-cmd
755 | "Command on a ftp channel."
756 | [^ChannelSftp channel cmd args options]
757 | (case cmd
758 | :ls (.ls channel (or (first args) "."))
759 | :cd (.cd channel (first args))
760 | :lcd (.lcd channel (first args))
761 | :chmod (.chmod channel (first args) (second args))
762 | :chown (.chown channel (first args) (second args))
763 | :chgrp (.chgrp channel (first args) (second args))
764 | :pwd (.pwd channel)
765 | :lpwd (.lpwd channel)
766 | :rm (.rm channel (first args))
767 | :rmdir (.rmdir channel (first args))
768 | :mkdir (.mkdir channel (first args))
769 | :stat (.stat channel (first args))
770 | :lstat (.lstat channel (first args))
771 | :rename (.rename channel (first args) (second args))
772 | :symlink (.symlink channel (first args) (second args))
773 | :readlink (.readlink channel (first args))
774 | :realpath (.realpath channel (first args))
775 | :get-home (.getHome channel)
776 | :get-server-version (.getServerVersion channel)
777 | :get-extension (.getExtension channel (first args))
778 | :get (let [args (if (options :with-monitor)
779 | (conj args (options :with-monitor))
780 | args)
781 | args (if (options :mode)
782 | (conj args (sftp-modemap (options :mode)))
783 | args)]
784 | ((memfn-varargs get ChannelSftp) channel args))
785 | :put (let [args (if (options :with-monitor)
786 | (conj args (options :with-monitor))
787 | args)
788 | args (if (options :mode)
789 | (conj args (sftp-modemap (options :mode)))
790 | args)]
791 | ((memfn-varargs put ChannelSftp) channel args))
792 | (throw
793 | (java.lang.IllegalArgumentException. (str "Unknown SFTP command " cmd)))))
794 |
795 | (defn sftp
796 | "Execute SFTP commands.
797 |
798 | sftp host-or-session options cmd & args
799 |
800 | cmd specifies a command to exec. Valid commands are:
801 | :ls
802 | :put
803 | :get
804 | :chmod
805 | :chown
806 | :chgrp
807 | :cd
808 | :lcd
809 | :pwd
810 | :lpwd
811 | :rm
812 | :rmdir
813 | :stat
814 | :symlink"
815 | [session-or-channel {:keys [with-monitor modes] :as opts} cmd & args]
816 | (let [channel-given (instance? com.jcraft.jsch.ChannelSftp session-or-channel)
817 | session-given (instance? com.jcraft.jsch.Session session-or-channel)
818 | session (when session-given session-or-channel)
819 | channel (if channel-given
820 | session-or-channel
821 | (ssh-sftp session))]
822 | (try
823 | (when (and session (not (connected? session)))
824 | (connect session))
825 | (ssh-sftp-cmd channel cmd (vec args) opts)
826 | (finally
827 | (when-not channel-given
828 | (disconnect-channel channel))
829 | (when-not (or session-given channel-given)
830 | (disconnect session))))))
831 |
832 | (defn- scp-send-ack
833 | "Send acknowledgement to the specified output stream"
834 | ([^OutputStream out] (scp-send-ack out 0))
835 | ([^OutputStream out code]
836 | (.write out (byte-array [(byte code)]))
837 | (.flush out)))
838 |
839 | (defn- scp-receive-ack
840 | "Check for an acknowledgement byte from the given input stream"
841 | [^InputStream in]
842 | (let [code (.read in)]
843 | (when-not (zero? code)
844 | (throw
845 | (ex-info
846 | (format
847 | "clj-ssh scp failure: %s"
848 | (case code
849 | 1 "scp error"
850 | 2 "scp fatal error"
851 | -1 "disconnect error"
852 | "unknown error"))
853 | {:type :clj-ssh/scp-failure})))))
854 |
855 | (defn- scp-send-command
856 | "Send command to the specified output stream"
857 | [^OutputStream out ^InputStream in ^String cmd-string]
858 | (.write out (.getBytes (str cmd-string "\n")))
859 | (.flush out)
860 | (logging/tracef "Sent command %s" cmd-string)
861 | (scp-receive-ack in)
862 | (logging/trace "Received ACK"))
863 |
864 | (defn- scp-receive-command
865 | "Receive command on the specified input stream"
866 | [^OutputStream out ^InputStream in]
867 | (let [buffer-size 1024
868 | buffer (byte-array buffer-size)]
869 | (let [cmd (loop [offset 0]
870 | (let [n (.read in buffer offset (- buffer-size offset))]
871 | (logging/tracef
872 | "scp-receive-command: %s"
873 | (String. buffer (int 0) (int (+ offset n))))
874 | (if (= \newline (char (aget buffer (+ offset n -1))))
875 | (String. buffer (int 0) (int (+ offset n)))
876 | (recur (+ offset n)))))]
877 | (logging/tracef "Received command %s" cmd)
878 | (scp-send-ack out)
879 | (logging/trace "Sent ACK")
880 | cmd)))
881 |
882 | (defn- scp-copy-file
883 | "Send acknowledgement to the specified output stream"
884 | [^OutputStream send ^InputStream recv ^File file {:keys [mode buffer-size preserve]
885 | :or {mode 0644 buffer-size 1492 preserve false}}]
886 |
887 | (when preserve
888 | (scp-send-command
889 | send recv
890 | (format "P%d 0 %d 0" (.lastModified file) (.lastModified file))))
891 | (scp-send-command
892 | send recv
893 | (format "C%04o %d %s" mode (.length file) (.getName file)))
894 | (logging/tracef "Sending %s" (.getAbsolutePath file))
895 | (io/copy file send :buffer-size buffer-size)
896 | (scp-send-ack send)
897 | (logging/trace "Receiving ACK after send")
898 | (scp-receive-ack recv))
899 |
900 | (defn- scp-copy-dir
901 | "Send acknowledgement to the specified output stream"
902 | [send recv ^File dir {:keys [dir-mode] :or {dir-mode 0755} :as options}]
903 | (logging/tracef "Sending directory %s" (.getAbsolutePath dir))
904 | (scp-send-command
905 | send recv
906 | (format "D%04o 0 %s" dir-mode (.getName dir)))
907 | (doseq [^File file (.listFiles dir)]
908 | (cond
909 | (.isFile file) (scp-copy-file send recv file options)
910 | (.isDirectory file) (scp-copy-dir send recv file options)))
911 | (scp-send-command send recv "E"))
912 |
913 | (defn- scp-files
914 | [paths recursive]
915 | (let [f (if recursive
916 | #(File. ^String %)
917 | (fn [^String path]
918 | (let [file (File. path)]
919 | (when (.isDirectory file)
920 | (throw
921 | (ex-info
922 | (format
923 | "Copy of dir %s requested without recursive flag" path)
924 | {:type :clj-ssh/scp-directory-copy-requested})))
925 | file)))]
926 | (map f paths)))
927 |
928 | (defn session-cipher-none
929 | "Reset the session to use no cipher"
930 | [^Session session]
931 | (logging/trace "Set session to prefer none cipher")
932 | (doto session
933 | (.setConfig
934 | "cipher.s2c" "none,aes128-cbc,3des-cbc,blowfish-cbc")
935 | (.setConfig
936 | "cipher.c2s" "none,aes128-cbc,3des-cbc,blowfish-cbc")
937 | (.rekey)))
938 |
939 | (defn scp-parse-times
940 | [cmd]
941 | (let [s (StringReader. cmd)]
942 | (.skip s 1) ;; skip T
943 | (let [scanner (java.util.Scanner. s)
944 | mtime (.nextLong scanner)
945 | zero (.nextInt scanner)
946 | atime (.nextLong scanner)]
947 | [mtime atime])))
948 |
949 | (defn scp-parse-copy
950 | [cmd]
951 | (let [s (StringReader. cmd)]
952 | (.skip s 1) ;; skip C or D
953 | (let [scanner (java.util.Scanner. s)
954 | mode (.nextInt scanner 8)
955 | length (.nextLong scanner)
956 | filename (.next scanner)]
957 | [mode length filename])))
958 |
959 | (defn scp-sink-file
960 | "Sink a file"
961 | [^OutputStream send ^InputStream recv
962 | ^File file mode length {:keys [buffer-size] :or {buffer-size 2048}}]
963 | (logging/tracef "Sinking %d bytes to file %s" length (.getPath file))
964 | (let [buffer (byte-array buffer-size)]
965 | (with-open [file-stream (FileOutputStream. file)]
966 | (loop [length length]
967 | (let [size (.read recv buffer 0 (min length buffer-size))]
968 | (when (pos? size)
969 | (.write file-stream buffer 0 size))
970 | (when (and (pos? size) (< size length))
971 | (recur (- length size))))))
972 | (scp-receive-ack recv)
973 | (logging/trace "Received ACK after sink of file")
974 | (scp-send-ack send)
975 | (logging/trace "Sent ACK after sink of file")))
976 |
977 | (defn scp-sink
978 | "Sink scp commands to file"
979 | [^OutputStream send ^InputStream recv ^File file times {:as options}]
980 | (let [cmd (scp-receive-command send recv)]
981 | (case (first cmd)
982 | \C (let [[mode length ^String filename] (scp-parse-copy cmd)
983 | file (if (and (.exists file) (.isDirectory file))
984 | (doto (File. file filename) (.createNewFile))
985 | (doto file (.createNewFile)))]
986 | (scp-sink-file send recv file mode length options)
987 | (when times
988 | (.setLastModified file (first times))))
989 | \T (scp-sink send recv file (scp-parse-times cmd) options)
990 | \D (let [[mode ^String filename] (scp-parse-copy cmd)
991 | dir (File. file filename)]
992 | (when (and (.exists dir) (not (.isDirectory dir)))
993 | (.delete dir))
994 | (when (not (.exists dir))
995 | (.mkdir dir))
996 | (scp-sink send recv dir nil options))
997 | \E nil)))
998 |
999 |
1000 | ;; http://blogs.sun.com/janp/entry/how_the_scp_protocol_works
1001 | ;; https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works
1002 | (defn scp-to
1003 | "Copy local path(s) to remote path via scp.
1004 |
1005 | Options are:
1006 |
1007 | :username username to use for authentication
1008 | :password password to use for authentication
1009 | :port port to use if no session specified
1010 | :mode mode, as a 4 digit octal number (default 0644)
1011 | :dir-mode directory mode, as a 4 digit octal number (default 0755)
1012 | :recursive flag for recursive operation
1013 | :preserve flag for preserving mode, mtime and atime. atime is not available
1014 | in java, so is set to mtime. mode is not readable in java."
1015 | [session local-paths remote-path
1016 | & {:keys [username password port mode dir-mode recursive preserve] :as opts}]
1017 | (let [local-paths (if (sequential? local-paths) local-paths [local-paths])
1018 | files (scp-files local-paths recursive)]
1019 | (when (and session (not (connected? session)))
1020 | (connect session))
1021 | (let [[^PipedInputStream in
1022 | ^PipedOutputStream send] (streams-for-in)
1023 | cmd (format "scp %s %s -t %s" (:remote-flags opts "") (if recursive "-r" "") remote-path)
1024 | _ (logging/tracef "scp-to: %s" cmd)
1025 | {:keys [^ChannelExec channel ^PipedInputStream out-stream]}
1026 | (ssh-exec session cmd in :stream opts)
1027 | exec channel
1028 | recv out-stream]
1029 | (logging/tracef
1030 | "scp-to %s %s" (string/join " " local-paths) remote-path)
1031 | (logging/trace "Receive initial ACK")
1032 | (scp-receive-ack recv)
1033 | (doseq [^File file files]
1034 | (logging/tracef "scp-to: from %s" (.getPath file))
1035 | (if (.isDirectory file)
1036 | (scp-copy-dir send recv file opts)
1037 | (scp-copy-file send recv file opts)))
1038 | (logging/trace "Closing streams")
1039 | (.close send)
1040 | (.close recv)
1041 | (disconnect-channel exec)
1042 | nil)))
1043 |
1044 | (defn scp-from
1045 | "Copy remote path(s) to local path via scp.
1046 |
1047 | Options are:
1048 |
1049 | :username username to use for authentication
1050 | :password password to use for authentication
1051 | :port port to use if no session specified
1052 | :mode mode, as a 4 digit octal number (default 0644)
1053 | :dir-mode directory mode, as a 4 digit octal number (default 0755)
1054 | :recursive flag for recursive operation
1055 | :preserve flag for preserving mode, mtime and atime. atime is not available
1056 | in java, so is set to mtime. mode is not readable in java."
1057 | [session remote-paths ^String local-path
1058 | & {:keys [username password port mode dir-mode recursive preserve] :as opts}]
1059 | (let [remote-paths (if (sequential? remote-paths) remote-paths [remote-paths])
1060 | file (File. local-path)
1061 | _ (when (and (.exists file)
1062 | (not (.isDirectory file))
1063 | (> (count remote-paths) 1))
1064 | (throw
1065 | (ex-info
1066 | (format "Copy of multiple files to file %s requested" local-path)
1067 | {:type :clj-ssh/scp-copy-multiple-files-to-file-requested})))]
1068 | (when (and session (not (connected? session)))
1069 | (connect session))
1070 | (let [[^PipedInputStream in
1071 | ^PipedOutputStream send] (streams-for-in)
1072 | flags {:recursive "-r" :preserve "-p"}
1073 | cmd (format
1074 | "scp %s -f %s"
1075 | (:remote-flags
1076 | opts
1077 | (string/join
1078 | " "
1079 | (->>
1080 | (select-keys opts [:recursive :preserve])
1081 | (filter val)
1082 | (map (comp flags key)))))
1083 | (string/join " " remote-paths))
1084 | _ (logging/tracef "scp-from: %s" cmd)
1085 | {:keys [^ChannelExec channel
1086 | ^PipedInputStream out-stream]}
1087 | (ssh-exec session cmd in :stream opts)
1088 | exec channel
1089 | recv out-stream]
1090 | (logging/tracef
1091 | "scp-from %s %s" (string/join " " remote-paths) local-path)
1092 | (scp-send-ack send)
1093 | (logging/trace "Sent initial ACK")
1094 | (scp-sink send recv file nil opts)
1095 | (logging/trace "Closing streams")
1096 | (.close send)
1097 | (.close recv)
1098 | (disconnect-channel exec)
1099 | nil)))
1100 |
1101 | (def ^{:private true} key-types {:rsa KeyPair/RSA :dsa KeyPair/DSA})
1102 |
1103 | (defn generate-keypair
1104 | "Generate a keypair, returned as [private public] byte arrays.
1105 | Valid types are :rsa and :dsa. key-size is in bytes. passphrase can be a
1106 | string or byte array. Optionally writes the keypair to the paths specified
1107 | using the :private-key-path and :public-key-path keys."
1108 | [agent key-type key-size passphrase
1109 | & {:keys [comment private-key-path public-key-path]}]
1110 | (let [keypair (KeyPair/genKeyPair agent (key-type key-types) key-size)
1111 | write-pub (if (nil? comment)
1112 | (fn [x] (.writePublicKey keypair x ""))
1113 | (fn [x] (.writePublicKey keypair x comment)))
1114 | write-pvt (if (nil? passphrase)
1115 | (fn [x] (.writePrivateKey keypair x))
1116 | (fn [x] (.writePrivateKey keypair x
1117 | (if (= (type passphrase) String)
1118 | (.getBytes passphrase)
1119 | passphrase))))
1120 | ]
1121 | (when public-key-path
1122 | (write-pub public-key-path))
1123 | (when private-key-path
1124 | (write-pvt private-key-path))
1125 | (let [pub-baos (ByteArrayOutputStream.)
1126 | pri-baos (ByteArrayOutputStream.)]
1127 | (write-pub pub-baos)
1128 | (write-pvt pri-baos)
1129 | [(.toByteArray pri-baos) (.toByteArray pub-baos)])))
1130 |
--------------------------------------------------------------------------------
/src/clj_ssh/ssh/protocols.clj:
--------------------------------------------------------------------------------
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"))
10 |
--------------------------------------------------------------------------------
/test/clj_ssh/cli_test.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.cli-test
2 | (:use
3 | clojure.test
4 | clj-ssh.cli
5 | clj-ssh.test-keys
6 | [clj-ssh.ssh
7 | :only [connect connected? disconnect ssh-agent ssh-agent? with-connection]]
8 | [clj-ssh.test-utils
9 | :only [quiet-ssh-logging sftp-monitor sftp-monitor-done home]])
10 | (:require [clojure.java.io :as io])
11 | (:import com.jcraft.jsch.JSch))
12 |
13 | (use-fixtures :once quiet-ssh-logging)
14 |
15 | (deftest with-ssh-agent-test
16 | (testing "initialised"
17 | (is (ssh-agent? *ssh-agent*)))
18 | (testing "system ssh-agent"
19 | (with-ssh-agent (ssh-agent {})
20 | (is (ssh-agent? *ssh-agent*))
21 | (is (pos? (count (.getIdentityNames *ssh-agent*))))))
22 | (testing "local ssh-agent"
23 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
24 | (is (ssh-agent? *ssh-agent*))
25 | (is (zero? (count (.getIdentityNames *ssh-agent*))))))
26 | (let [agent (ssh-agent {})]
27 | (with-ssh-agent agent
28 | (is (= *ssh-agent* agent)))))
29 |
30 | (deftest add-identity-test
31 | (let [key (private-key-path)]
32 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
33 | (add-identity :private-key-path key)
34 | (is (= 1 (count (.getIdentityNames *ssh-agent*))))
35 | (add-identity :agent *ssh-agent* :private-key-path key)))
36 | (testing "passing byte arrays"
37 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
38 | (add-identity
39 | :name "name"
40 | :private-key (.getBytes (slurp (private-key-path)))
41 | :public-key (.getBytes (slurp (public-key-path))))
42 | (is (= 1 (count (.getIdentityNames *ssh-agent*))))
43 | (is (= "name" (first (.getIdentityNames *ssh-agent*)))))))
44 |
45 | (deftest has-identity?-test
46 | (let [key (private-key-path)]
47 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
48 | (is (not (has-identity? key)))
49 | (is (zero? (count (.getIdentityNames *ssh-agent*))))
50 | (add-identity :private-key-path key)
51 | (is (= 1 (count (.getIdentityNames *ssh-agent*))))
52 | (is (has-identity? key)))))
53 |
54 | (deftest session-test
55 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
56 | (let [session (session "localhost" :username (username) :port 22)]
57 | (is (instance? com.jcraft.jsch.Session session))
58 | (is (not (connected? session))))))
59 |
60 | (deftest session-connect-test
61 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
62 | (default-session-options
63 | {:username (username) :strict-host-key-checking :no})
64 | (add-identity :private-key-path (private-key-path))
65 | (let [session (session "localhost")]
66 | (is (instance? com.jcraft.jsch.Session session))
67 | (is (not (connected? session)))
68 | (connect session)
69 | (is (connected? session))
70 | (disconnect session)
71 | (is (not (connected? session))))
72 | (let [session (session "localhost")]
73 | (with-connection session
74 | (is (connected? session)))
75 | (is (not (connected? session)))))
76 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
77 | (try (add-identity-with-keychain
78 | :private-key-path (encrypted-private-key-path)
79 | :passphrase "clj-ssh")
80 | (let [session (session "localhost")]
81 | (is (instance? com.jcraft.jsch.Session session))
82 | (is (not (connected? session)))
83 | (connect session)
84 | (is (connected? session))
85 | (disconnect session)
86 | (is (not (connected? session))))
87 | (let [session (session "localhost")]
88 | (with-connection session
89 | (is (connected? session)))
90 | (is (not (connected? session))))
91 | (catch Exception e
92 | (when-not (= :clj-ssh/no-passphrase-available (:type (ex-data e)))
93 | (throw e)))))
94 | (with-ssh-agent (ssh-agent {})
95 | (let [session (session "localhost")]
96 | (is (instance? com.jcraft.jsch.Session session))
97 | (is (not (connected? session)))
98 | (connect session)
99 | (is (connected? session))
100 | (disconnect session)
101 | (is (not (connected? session))))
102 | (let [session (session "localhost")]
103 | (with-connection session
104 | (is (connected? session)))
105 | (is (not (connected? session))))))
106 |
107 | (deftest ssh-test
108 | (with-ssh-agent (ssh-agent {:use-system-ssh-agent false})
109 | (add-identity :private-key-path (private-key-path))
110 | (default-session-options
111 | {:username (username) :strict-host-key-checking :no})
112 | (let [{:keys [exit out]} (ssh "localhost" :in "echo hello")]
113 | (is (zero? exit))
114 | (is (.contains out "hello")))
115 | (let [{:keys [exit out err]} (ssh "localhost" :cmd "/bin/bash -c 'ls /'")]
116 | (is (zero? exit))
117 | (is (.contains out "bin"))
118 | (is (= "" err)))
119 | (let [{:keys [exit out err]} (ssh "localhost" "/bin/bash -c 'ls /'")]
120 | (is (zero? exit))
121 | (is (.contains out "bin"))
122 | (is (= "" err)))
123 | (let [{:keys [exit out err]} (ssh "localhost" "/bin/bash" "-c" "'ls /'")]
124 | (is (zero? exit))
125 | (is (.contains out "bin"))
126 | (is (= "" err)))
127 | (let [{:keys [exit out]}
128 | (ssh "localhost" :in "echo hello" :username (username))]
129 | (is (zero? exit))
130 | (is (.contains out "hello")))
131 | (let [{:keys [exit out err]}
132 | (ssh "localhost" :cmd "/bin/bash -c 'ls /'" :username (username))]
133 | (is (zero? exit))
134 | (is (.contains out "bin"))
135 | (is (= "" err)))
136 | (let [{:keys [exit out]}
137 | (ssh "localhost" :in "tty -s" :pty true :username (username))]
138 | (is (zero? exit)))
139 | (let [{:keys [exit out]}
140 | (ssh "localhost" :in "tty -s" :pty false :username (username))]
141 | (is (= 1 exit)))
142 | (let [{:keys [exit out]}
143 | (ssh "localhost" :in "ssh-add -l" :agent-forwarding true
144 | :username (username))]
145 | (is (zero? exit)))))
146 |
147 |
148 | (deftest sftp-test
149 | (let [home (home)
150 | dir (sftp "localhost" :ls home)]
151 | (sftp "localhost" :cd "/")
152 | (is (= home (sftp "localhost" :pwd)))
153 | (let [tmpfile1 (java.io.File/createTempFile "clj-ssh" "test")
154 | tmpfile2 (java.io.File/createTempFile "clj-ssh" "test")
155 | file1 (.getPath tmpfile1)
156 | file2 (.getPath tmpfile2)
157 | content "content"
158 | content2 "content2"]
159 | (try
160 | (.setWritable tmpfile1 true false)
161 | (.setWritable tmpfile2 true false)
162 | (io/copy content tmpfile1)
163 | (sftp "localhost" :put file1 file2)
164 | (is (= content (slurp file2)))
165 | (io/copy content2 tmpfile2)
166 | (sftp "localhost" :get file2 file1)
167 | (is (= content2 (slurp file1)))
168 | (sftp
169 | "localhost" :put (java.io.ByteArrayInputStream. (.getBytes content)) file1)
170 | (is (= content (slurp file1)))
171 | (let [[monitor state] (sftp-monitor)]
172 | (sftp "localhost" :put (java.io.ByteArrayInputStream. (.getBytes content))
173 | file2 :with-monitor monitor)
174 | (is (sftp-monitor-done state)))
175 | (is (= content (slurp file2)))
176 | (finally
177 | (.delete tmpfile1)
178 | (.delete tmpfile2))))))
179 |
180 |
181 | (deftest out-stream-test
182 | (testing "exec"
183 | (testing ":out :string"
184 | (let [proc (ssh "localhost" "ls")]
185 | (is (zero? (:exit proc)))
186 | (is (pos? (count (:out proc))) "no options")))
187 | (testing ":out :stream"
188 | (let [proc (ssh "localhost" "ls" :out :stream :pty true)]
189 | (is (clj-ssh.ssh/connected-channel? (:channel proc))
190 | ":channel not connected")
191 | (is (> (count (slurp (:out-stream proc))) 1) ":out-stream")
192 | (Thread/sleep 100)
193 | (is (zero? (clj-ssh.ssh/exit-status (:channel proc)))
194 | "zero exit status")
195 | (clj-ssh.ssh/disconnect (:session proc)))))
196 | (testing "shell"
197 | (testing ":out :string"
198 | (let [proc (ssh "localhost" :in "ls")]
199 | (is (pos? (count (:out proc))) ":out has content")
200 | (is (zero? (:exit proc)) "zero exit status")))
201 | (testing ":out stream"
202 | (let [proc (ssh "localhost" :in "ls" :out :stream)]
203 | (is (clj-ssh.ssh/connected-channel? (:channel proc))
204 | ":channel connected")
205 | (is (> (count (slurp (:out-stream proc))) 1) ":out-stream")
206 | (Thread/sleep 100)
207 | (is (zero? (clj-ssh.ssh/exit-status (:channel proc)))
208 | "zero exit status")
209 | (clj-ssh.ssh/disconnect (:session proc))))))
210 |
--------------------------------------------------------------------------------
/test/clj_ssh/ssh_test.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.ssh-test
2 | (:use
3 | clojure.test
4 | clj-ssh.ssh
5 | clj-ssh.test-keys
6 | [clj-ssh.test-utils
7 | :only [quiet-ssh-logging sftp-monitor sftp-monitor-done]])
8 | (:require [clojure.java.io :as io])
9 | (:import com.jcraft.jsch.JSch))
10 |
11 | (use-fixtures :once quiet-ssh-logging)
12 |
13 | (deftest file-path-test
14 | (is (= "abc" (#'clj-ssh.ssh/file-path "abc")))
15 | (is (= "abc" (#'clj-ssh.ssh/file-path (java.io.File. "abc")))))
16 |
17 | (deftest camelize-test
18 | (is (= "StrictHostKeyChecking"
19 | (#'clj-ssh.ssh/camelize "strict-host-key-checking"))))
20 |
21 | (deftest ssh-agent?-test
22 | (is (ssh-agent? (JSch.)))
23 | (is (not (ssh-agent? "i'm not an ssh-agent"))))
24 |
25 | (deftest create-ssh-agent-test
26 | (is (ssh-agent? (ssh-agent {})))
27 | (is (ssh-agent? (ssh-agent {:use-system-ssh-agent false}))))
28 |
29 | (deftest make-identity-test
30 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
31 | (let [path (private-key-path)
32 | identity (make-identity agent path (str path ".pub"))]
33 | (is (instance? com.jcraft.jsch.Identity identity))
34 | (is (not (.isEncrypted identity)))
35 | (add-identity agent {:identity identity})
36 | (is (= 1 (count (.getIdentityNames agent)))))
37 | (let [path (encrypted-private-key-path)
38 | identity (make-identity agent path (str path ".pub"))]
39 | (is (instance? com.jcraft.jsch.Identity identity))
40 | (is (.isEncrypted identity))
41 | (add-identity agent {:identity identity :name "clj-ssh"})
42 | (is (= 2 (count (.getIdentityNames agent)))))))
43 |
44 | (deftest add-identity-test
45 | (let [key (private-key-path)]
46 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
47 | (add-identity agent {:private-key-path key})
48 | (is (= 1 (count (.getIdentityNames agent))))
49 | (add-identity agent {:private-key-path key})))
50 | (testing "passing byte arrays"
51 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
52 | (add-identity
53 | agent
54 | {:name "name"
55 | :private-key (.getBytes (slurp (private-key-path)))
56 | :public-key (.getBytes (slurp (public-key-path)))})
57 | (is (= 1 (count (.getIdentityNames agent))))
58 | (is (= "name" (first (.getIdentityNames agent))))))
59 | (testing "ssh-agent"
60 | (let [agent (ssh-agent {})]
61 | (let [n (count (.getIdentityNames agent))
62 | test-key-comment "key for test clj-ssh"
63 | names (vec (.getIdentityNames agent))
64 | has-key (some #(= (private-key-path) %) names)]
65 | (add-identity
66 | agent
67 | {:private-key-path (private-key-path)
68 | :public-key-path (public-key-path)})
69 | (let [names (.getIdentityNames agent)]
70 | (is (or has-key (= (inc n) (count names))))
71 | (is (some #(= (private-key-path) %) names)))))))
72 |
73 | (deftest has-identity?-test
74 | (let [key (private-key-path)
75 | pub-key (public-key-path)]
76 | (testing "private-key-path only"
77 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
78 | (is (not (has-identity? agent key)))
79 | (is (zero? (count (.getIdentityNames agent))))
80 | (add-identity agent {:private-key-path key})
81 | (is (= 1 (count (.getIdentityNames agent))))
82 | (is (has-identity? agent key))))
83 | (testing "private-key-path and public-key-path"
84 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
85 | (is (not (has-identity? agent key)))
86 | (is (zero? (count (.getIdentityNames agent))))
87 | (add-identity agent {:private-key-path key
88 | :public-key-path pub-key})
89 | (is (= 1 (count (.getIdentityNames agent))))
90 | (is (has-identity? agent key))))))
91 |
92 | (deftest session-impl-test
93 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
94 | (let [session
95 | (#'clj-ssh.ssh/session-impl agent "somehost" (username) 22 nil {})]
96 | (is (instance? com.jcraft.jsch.Session session))
97 | (is (not (connected? session))))
98 | (let [session
99 | (#'clj-ssh.ssh/session-impl agent "localhost" (username) 22 nil
100 | {:strict-host-key-checking :no})]
101 | (is (instance? com.jcraft.jsch.Session session))
102 | (is (not (connected? session)))
103 | (is (= "no" (.getConfig session "StrictHostKeyChecking"))))))
104 |
105 | (deftest session-test
106 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
107 | (let [session (session agent "localhost" {:username (username) :port 22})]
108 | (is (instance? com.jcraft.jsch.Session session))
109 | (is (not (connected? session)))))
110 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
111 | (let [session (session agent "localhost" {:username (username)})]
112 | (is (instance? com.jcraft.jsch.Session session))
113 | (is (not (connected? session)))))
114 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
115 | (let [session (session agent "localhost" {:username (username)})]
116 | (is (instance? com.jcraft.jsch.Session session))
117 | (is (not (connected? session))))))
118 |
119 | (deftest session-connect-test
120 | (testing "internal agent"
121 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
122 | (add-identity agent {:private-key-path (private-key-path)})
123 | (let [session (session
124 | agent
125 | "localhost"
126 | {:username (username) :strict-host-key-checking :no})]
127 | (is (instance? com.jcraft.jsch.Session session))
128 | (is (not (connected? session)))
129 | (connect session)
130 | (is (connected? session))
131 | (disconnect session)
132 | (is (not (connected? session))))
133 | (let [session (session
134 | agent
135 | "localhost"
136 | {:username (username) :strict-host-key-checking :no})]
137 | (with-connection session
138 | (is (connected? session)))
139 | (is (not (connected? session)))))
140 | (testing "key with passphrase"
141 | (try (let [agent (ssh-agent {:use-system-ssh-agent false})]
142 | (add-identity-with-keychain
143 | agent
144 | {:private-key-path (encrypted-private-key-path)
145 | :passphrase "clj-ssh"})
146 | (let [session (session
147 | agent
148 | "localhost"
149 | {:username (username)
150 | :strict-host-key-checking :no})]
151 | (is (instance? com.jcraft.jsch.Session session))
152 | (is (not (connected? session)))
153 | (connect session)
154 | (is (connected? session))
155 | (disconnect session)
156 | (is (not (connected? session))))
157 | (let [session (session
158 | agent
159 | "localhost"
160 | {:username (username)
161 | :strict-host-key-checking :no})]
162 | (with-connection session
163 | (is (connected? session)))
164 | (is (not (connected? session)))))
165 | (catch Exception e
166 | (when-not (= :clj-ssh/no-passphrase-available (:type (ex-data e)))
167 | (throw e))))))
168 | (testing "system ssh-agent"
169 | (let [agent (ssh-agent {})]
170 | (let [session (session
171 | agent
172 | "localhost"
173 | {:username (username)
174 | :strict-host-key-checking :no})]
175 | (is (instance? com.jcraft.jsch.Session session))
176 | (is (not (connected? session)))
177 | (connect session)
178 | (is (connected? session))
179 | (disconnect session)
180 | (is (not (connected? session))))
181 | (let [session (session
182 | agent
183 | "localhost"
184 | {:username (username)
185 | :strict-host-key-checking :no})]
186 | (with-connection session
187 | (is (connected? session)))
188 | (is (not (connected? session)))))))
189 |
190 | (deftest open-shell-channel-test
191 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
192 | (add-identity agent {:private-key-path (private-key-path)})
193 | (let [session (session
194 | agent
195 | "localhost"
196 | {:username (username) :strict-host-key-checking :no})]
197 | (with-connection session
198 | (let [shell (open-channel session :shell)
199 | os (java.io.ByteArrayOutputStream.)]
200 | (.setInputStream
201 | shell
202 | (java.io.ByteArrayInputStream. (.getBytes "ls /;exit 0;\n"))
203 | false)
204 | (.setOutputStream shell os)
205 | (with-channel-connection shell
206 | (while (connected-channel? shell)
207 | (Thread/sleep 1000))
208 | (is (.contains (str os) "bin"))))))))
209 |
210 | (deftest ssh-shell-test
211 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
212 | (add-identity agent {:private-key-path (private-key-path)})
213 | (let [session (session
214 | agent
215 | "localhost"
216 | {:username (username) :strict-host-key-checking :no})]
217 | (with-connection session
218 | (let [{:keys [exit out]} (ssh-shell session "echo hello" "UTF-8" {})]
219 | (is (zero? exit))
220 | (is (.contains out "hello")))
221 | (let [{:keys [exit out]} (ssh-shell session "echo hello" :bytes {})]
222 | (is (zero? exit))
223 | (is (.contains (String. out) "hello")))
224 | (let [{:keys [exit out]} (ssh-shell
225 | session "echo hello;exit 1" "UTF-8" {})]
226 | (is (= 1 exit))
227 | (is (.contains out "hello")))
228 | (let [{:keys [channel out-stream]} (ssh-shell
229 | session "echo hello;exit 1" :stream {})]
230 | (while (connected-channel? channel) (Thread/sleep 100))
231 | (is (= 1 (.getExitStatus channel)))
232 | (is (pos? (.available out-stream)))
233 | (let [bytes (byte-array 1024)
234 | n (.read out-stream bytes 0 1024)]
235 | (is (.contains (String. bytes 0 n) "hello"))))
236 | (let [{:keys [exit out]} (ssh-shell
237 | session "exit $(tty -s)" "UTF-8" {:pty true})]
238 | (is (zero? exit)))
239 | (let [{:keys [exit out]} (ssh-shell
240 | session "exit $(tty -s)" "UTF-8" {:pty nil})]
241 | (is (= 1 exit)))
242 | (let [{:keys [exit out]} (ssh-shell
243 | session "ssh-add -l" "UTF-8" {})]
244 | (is (pos? exit)))
245 | (let [{:keys [exit out]} (ssh-shell
246 | session "ssh-add -l" "UTF-8"
247 | {:agent-forwarding false})]
248 | (is (pos? exit)))
249 | (let [{:keys [exit out]} (ssh-shell
250 | session "ssh-add -l" "UTF-8"
251 | {:agent-forwarding true})]
252 | (is (re-find #"RSA" out))
253 | (is (zero? exit)))))))
254 |
255 | (deftest ssh-exec-test
256 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
257 | (add-identity agent {:private-key-path (private-key-path)})
258 | (let [session (session
259 | agent
260 | "localhost"
261 | {:username (username)
262 | :strict-host-key-checking :no})]
263 | (with-connection session
264 | (let [{:keys [exit out err]}
265 | (ssh-exec session "/bin/bash -c 'ls /'" nil "UTF-8" {})]
266 | (is (zero? exit))
267 | (is (.contains out "bin"))
268 | (is (= "" err)))
269 | (let [{:keys [exit out err]}
270 | (ssh-exec session "/bin/bash -c 'lsxxxxx /'" nil "UTF-8" {})]
271 | (is (pos? exit))
272 | (is (= "" out))
273 | (is (.contains err "command not found")))
274 | (let [{:keys [channel out-stream err-stream]}
275 | (ssh-exec
276 | session "/bin/bash -c 'ls / && lsxxxxx /'" nil :stream {})]
277 | (while (connected-channel? channel) (Thread/sleep 100))
278 | (is (not= 0 (.getExitStatus channel)))
279 | (is (pos? (.available out-stream)))
280 | (is (pos? (.available err-stream)))
281 | (let [out-bytes (byte-array 1024)
282 | out-n (.read out-stream out-bytes 0 1024)
283 | err-bytes (byte-array 1024)
284 | err-n (.read err-stream err-bytes 0 1024)]
285 | (is (.contains (String. out-bytes 0 out-n) "bin"))
286 | (is (.contains
287 | (String. err-bytes 0 err-n)
288 | "command not found"))))))))
289 |
290 | (deftest ssh-test
291 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
292 | (add-identity agent {:private-key-path (private-key-path)})
293 | (let [session (session
294 | agent
295 | "localhost"
296 | {:username (username)
297 | :strict-host-key-checking :no})]
298 | (with-connection session
299 | (let [{:keys [exit out]} (ssh session {:in "echo hello"})]
300 | (is (zero? exit))
301 | (is (.contains out "hello")))
302 | (let [{:keys [exit out err]} (ssh session {:cmd "/bin/bash -c 'ls /'"})]
303 | (is (zero? exit))
304 | (is (.contains out "bin"))
305 | (is (= "" err)))
306 | (let [{:keys [exit out]} (ssh
307 | session
308 | {:in "echo hello" :username (username)})]
309 | (is (zero? exit))
310 | (is (.contains out "hello")))
311 | (let [{:keys [exit out err]}
312 | (ssh session {:cmd "/bin/bash -c 'ls /'" :username (username)})]
313 | (is (zero? exit))
314 | (is (.contains out "bin"))
315 | (is (= "" err)))
316 | (let [{:keys [exit out]}
317 | (ssh session {:in "tty -s" :pty true :username (username)})]
318 | (is (zero? exit)))
319 | (let [{:keys [exit out]}
320 | (ssh session {:in "tty -s" :pty false :username (username)})]
321 | (is (= 1 exit)))
322 | (let [{:keys [exit out]}
323 | (ssh session {:in "ssh-add -l" :agent-forwarding true
324 | :username (username)})]
325 | (is (zero? exit))))))
326 | (let [agent (ssh-agent {:use-system-ssh-agent :false})]
327 | (add-identity agent {:private-key-path (private-key-path)})
328 | (let [session (session agent "localhost" {:username (username)
329 | :strict-host-key-checking :no})]
330 | (with-connection session
331 | (let [{:keys [exit out]}
332 | (ssh session {:in "echo hello" :username (username)})]
333 | (is (zero? exit))
334 | (is (.contains out "hello")))
335 | (let [{:keys [exit out]}
336 | (ssh session {:in "echo hello" :username (username)})]
337 | (is (zero? exit))
338 | (is (.contains out "hello")))
339 | (let [{:keys [exit out err]}
340 | (ssh session {:cmd "/bin/bash -c 'ls /'" :username (username)})]
341 | (is (zero? exit))
342 | (is (.contains out "bin"))
343 | (is (= "" err)))
344 | (let [{:keys [exit out err]}
345 | (ssh session {:cmd "/bin/bash -c 'ls /'" :return-map true
346 | :username (username)})]
347 | (is (zero? exit))
348 | (is (.contains out "bin"))
349 | (is (= "" err)))))))
350 |
351 | (deftest ssh-sftp-cmd-test
352 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
353 | (add-identity agent {:private-key-path (private-key-path)})
354 | (let [session (session agent "localhost" {:username (username)
355 | :strict-host-key-checking :no})]
356 | (with-connection session
357 | (let [channel (ssh-sftp session)
358 | dir (ssh-sftp-cmd channel :ls ["/"] {})]
359 | (ssh-sftp-cmd channel :cd ["/"] {})
360 | (is (= "/" (ssh-sftp-cmd channel :pwd [] {})))
361 | ;; value equality comparison on lsentry is borked
362 | (is (= (map str dir)
363 | (map str (ssh-sftp-cmd channel :ls [] {})))))))))
364 |
365 | (defn test-sftp-with [channel]
366 | (let [dir (sftp channel {} :ls "/")]
367 | (sftp channel {} :cd "/")
368 | (is (= "/" (sftp channel {} :pwd)))
369 | ;; value equality comparison on lsentry is borked
370 | (is (= (map str dir)
371 | (map str (sftp channel {} :ls))))
372 | (let [tmpfile1 (java.io.File/createTempFile "clj-ssh" "test")
373 | tmpfile2 (java.io.File/createTempFile "clj-ssh" "test")
374 | file1 (.getPath tmpfile1)
375 | file2 (.getPath tmpfile2)
376 | content "content"
377 | content2 "content2"]
378 | (try
379 | (.setWritable tmpfile1 true false)
380 | (.setWritable tmpfile2 true false)
381 | (io/copy content tmpfile1)
382 | (sftp channel {} :put file1 file2)
383 | (is (= content (slurp file2)))
384 | (io/copy content2 tmpfile2)
385 | (sftp channel {} :get file2 file1)
386 | (is (= content2 (slurp file1)))
387 | (sftp
388 | channel {}
389 | :put (java.io.ByteArrayInputStream. (.getBytes content)) file1)
390 | (is (= content (slurp file1)))
391 | (let [[monitor state] (sftp-monitor)]
392 | (sftp channel {:with-monitor monitor}
393 | :put (java.io.ByteArrayInputStream. (.getBytes content)) file2)
394 | (is (sftp-monitor-done state)))
395 | (is (= content (slurp file2)))
396 | (finally
397 | (.delete tmpfile1)
398 | (.delete tmpfile2))))))
399 |
400 | (defn test-sftp-transient-with [channel {:as options}]
401 | (let [dir (sftp channel options :ls "/")]
402 | (sftp channel options :cd "/")
403 | (is (not= "/" (sftp channel options :pwd)))
404 | (let [tmpfile1 (java.io.File/createTempFile "clj-ssh" "test")
405 | tmpfile2 (java.io.File/createTempFile "clj-ssh" "test")
406 | file1 (.getPath tmpfile1)
407 | file2 (.getPath tmpfile2)
408 | content "content"
409 | content2 "othercontent"]
410 | (try
411 | (.setWritable tmpfile1 true false)
412 | (.setWritable tmpfile2 true false)
413 | (io/copy content tmpfile1)
414 | (sftp channel options :put file1 file2)
415 | (is (= content (slurp file2)))
416 | (io/copy content2 tmpfile2)
417 | (sftp channel options :get file2 file1)
418 | (is (= content2 (slurp file1)))
419 | (sftp channel options
420 | :put (java.io.ByteArrayInputStream. (.getBytes content))
421 | file1)
422 | (is (= content (slurp file1)))
423 | (let [[monitor state] (sftp-monitor)]
424 | (sftp channel (assoc options :with-monitor monitor)
425 | :put (java.io.ByteArrayInputStream. (.getBytes content))
426 | file2)
427 | (is (sftp-monitor-done state)))
428 | (is (= content (slurp file2)))
429 | (finally
430 | (.delete tmpfile1)
431 | (.delete tmpfile2))))))
432 |
433 | (deftest sftp-session-test
434 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
435 | (add-identity agent {:private-key-path (private-key-path)})
436 | (let [session (session agent "localhost" {:username (username)
437 | :strict-host-key-checking :no})]
438 | (with-connection session
439 | (let [channel (ssh-sftp session)]
440 | (with-channel-connection channel
441 | (test-sftp-with channel)))
442 | (test-sftp-transient-with session {})))))
443 |
444 | (defn test-scp-to-with
445 | [session]
446 | (let [tmpfile1 (java.io.File/createTempFile "clj-ssh" "test")
447 | tmpfile2 (java.io.File/createTempFile "clj-ssh" "test")
448 | file1 (.getPath tmpfile1)
449 | file2 (.getPath tmpfile2)
450 | content "content"
451 | content2 "content2"]
452 | (try
453 | (.setWritable tmpfile1 true false)
454 | (.setWritable tmpfile2 true false)
455 | (io/copy content tmpfile1)
456 | (scp-to session file1 file2)
457 | (is (= content (slurp file2)) "scp-to should copy content")
458 | (io/copy content2 tmpfile1)
459 | (scp-to session file1 file2)
460 | (is (= content2 (slurp file2))
461 | "scp-to with implicit session should copy content")
462 | (finally
463 | (.delete tmpfile1)
464 | (.delete tmpfile2)))))
465 |
466 | (defn test-scp-from-with
467 | [session]
468 | (let [tmpfile1 (java.io.File/createTempFile "clj-ssh" "test")
469 | tmpfile2 (java.io.File/createTempFile "clj-ssh" "test")
470 | file1 (.getPath tmpfile1)
471 | file2 (.getPath tmpfile2)
472 | content "content"
473 | content2 "content2"]
474 | (try
475 | (.setWritable tmpfile1 true false)
476 | (.setWritable tmpfile2 true false)
477 | (io/copy content tmpfile1)
478 | (scp-from session file1 file2)
479 | (is (= content (slurp file2))
480 | "scp-from should copy content")
481 | (io/copy content2 tmpfile1)
482 | (scp-from session file1 file2
483 | :cipher-none true
484 | :username (username)
485 | :strict-host-key-checking :no)
486 | (is (= content2 (slurp file2))
487 | "scp-from with implicit session should copy content")
488 | (finally
489 | (.delete tmpfile1)
490 | (.delete tmpfile2)))))
491 |
492 | (deftest scp-test
493 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
494 | (add-identity agent {:private-key-path (private-key-path)})
495 | (let [session (session
496 | agent
497 | "localhost"
498 | {:username (username) :strict-host-key-checking :no})]
499 | (with-connection session
500 | (test-scp-to-with session)
501 | (test-scp-from-with session)))))
502 |
503 | (deftest keypair-test
504 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
505 | (is (keypair agent {:private-key-path (private-key-path)}))
506 | (is (keypair agent {:private-key-path (private-key-path)
507 | :public-key-path (public-key-path)}))))
508 |
509 | (deftest generate-keypair-test
510 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
511 | (let [[priv pub] (generate-keypair agent :rsa 1024 "hello")]
512 | (add-identity agent {:name "name"
513 | :private-key priv
514 | :public-key pub
515 | :passphrase (.getBytes "hello")}))))
516 |
517 | (defn port-reachable?
518 | ([ip port timeout]
519 | (let [socket (doto (java.net.Socket.)
520 | (.setReuseAddress false)
521 | (.setSoLinger false 1)
522 | (.setSoTimeout timeout))]
523 | (try
524 | (.connect socket (java.net.InetSocketAddress. ip port))
525 | true
526 | (catch java.io.IOException _)
527 | (finally
528 | (try (.close socket) (catch java.io.IOException _))))))
529 | ([ip port]
530 | (port-reachable? ip port 2000))
531 | ([port]
532 | (port-reachable? "localhost" port)))
533 |
534 | (deftest forward-local-port-test
535 | (testing "minimal test"
536 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
537 | (add-identity agent {:private-key-path (private-key-path)})
538 | (let [session (session
539 | agent
540 | "localhost"
541 | {:username (username)
542 | :strict-host-key-checking :no})]
543 | (is (instance? com.jcraft.jsch.Session session))
544 | (is (not (connected? session)))
545 | (is (not (port-reachable? 22222)))
546 | (connect session)
547 | (is (connected? session))
548 | (forward-local-port session 22222 22)
549 | (is (port-reachable? 22222))
550 | (unforward-local-port session 22222)
551 | (forward-local-port session 22223 22 "localhost")
552 | (is (port-reachable? 22223))
553 | (unforward-local-port session 22223)
554 | (with-local-port-forward [session 22224 22]
555 | (is (port-reachable? 22224)))
556 | (with-local-port-forward [session 22225 22 "localhost"]
557 | (is (port-reachable? 22225)))))))
558 |
559 | (deftest forward-remote-port-test
560 | (testing "minimal test"
561 | (let [agent (ssh-agent {:use-system-ssh-agent false})]
562 | (is (zero? (count (.getIdentityNames agent))))
563 | (add-identity agent {:private-key-path (private-key-path)
564 | :public-key-path (public-key-path)})
565 | (is (pos? (count (.getIdentityNames agent))))
566 | (let [session (session
567 | agent
568 | "localhost"
569 | {:username (username)
570 | :strict-host-key-checking :no})]
571 | (is (instance? com.jcraft.jsch.Session session))
572 | (is (not (connected? session)))
573 | (is (not (port-reachable? 22222)))
574 | (connect session)
575 | (is (connected? session))
576 | (forward-remote-port session 22222 22)
577 | (is (port-reachable? 22222))
578 | (unforward-remote-port session 22222)
579 | (forward-remote-port session 22222 22 "localhost")
580 | (unforward-remote-port session 22222)
581 | (with-remote-port-forward [session 22222 22]
582 | (is (port-reachable? 22222)))
583 | (with-remote-port-forward [session 22222 22 "localhost"]
584 | (is (port-reachable? 22222)))))))
585 |
586 | (deftest jump-session-test
587 | (is (let [s (jump-session (ssh-agent {})
588 | [{:hostname "localhost"
589 | :username (username)
590 | :strict-host-key-checking :no}]
591 | {})]
592 | (with-connection s
593 | (ssh-exec (the-session s) "ls" "" "" {})))
594 | "one host")
595 | (is (let [s (jump-session (ssh-agent {})
596 | [{:hostname "localhost"
597 | :username (username)
598 | :strict-host-key-checking :no}
599 | {:hostname "localhost"
600 | :username (username)
601 | :strict-host-key-checking :no}]
602 | {})]
603 | (with-connection s
604 | (ssh-exec (the-session s) "ls" "" "" {})))
605 | "two hosts"))
606 |
607 |
608 | (deftest out-stream-test
609 | (let [s (session (ssh-agent {}) "localhost" {})]
610 | (with-connection s
611 | (testing "exec"
612 | (testing ":out :string"
613 | (let [proc (ssh s {:cmd "ls"})]
614 | (is (zero? (:exit proc)))
615 | (is (pos? (count (:out proc))) "no options")))
616 | (testing ":out :stream"
617 | (let [proc (ssh s {:cmd "ls" :out :stream :pty true})]
618 | (is (> (count (slurp (:out-stream proc))) 1) ":out-stream")
619 | (while (connected-channel? (:channel proc))
620 | (Thread/sleep 100))
621 | (is (not (connected-channel? (:channel proc)))
622 | ":channel not connected")
623 | (is (zero? (exit-status (:channel proc)))
624 | "zero exit status"))))
625 | (testing "shell"
626 | (testing ":out :string"
627 | (let [proc (ssh s {:in "ls"})]
628 | (is (pos? (count (:out proc))) ":out has content")
629 | (is (zero? (:exit proc)) "zero exit status")))
630 | (testing ":out stream"
631 | (let [proc (ssh s {:in "ls" :out :stream})]
632 | (is (> (count (slurp (:out-stream proc))) 1) ":out-stream")
633 | (while (connected-channel? (:channel proc))
634 | (Thread/sleep 100))
635 | (is (not (connected-channel? (:channel proc)))
636 | ":channel not connected")
637 | (is (zero? (exit-status (:channel proc)))
638 | "zero exit status")))))))
639 |
--------------------------------------------------------------------------------
/test/clj_ssh/test_keys.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.test-keys)
2 |
3 | ;; TEST SETUP
4 | ;;
5 | ;; The tests assume the following setup
6 | ;;
7 | ;; ssh-keygen -f ~/.ssh/clj_ssh -t rsa -C "key for test clj-ssh" -N ""
8 | ;; ssh-keygen -f ~/.ssh/clj_ssh_pp -t rsa -C "key for test clj-ssh" -N "clj-ssh"
9 | ;; cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak
10 | ;; echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh.pub)" \
11 | ;; >> ~/.ssh/authorized_keys
12 | ;; echo "from=\"127.0.0.1,localhost,0.0.0.0\" $(cat ~/.ssh/clj_ssh_pp.pub)" \
13 | ;; >> ~/.ssh/authorized_keys
14 | ;; ssh-add -K ~/.ssh/clj_ssh_pp # add the key to the keychain
15 |
16 | (defn private-key-path
17 | [] (str (. System getProperty "user.home") "/.ssh/clj_ssh"))
18 |
19 | (defn public-key-path
20 | [] (str (. System getProperty "user.home") "/.ssh/clj_ssh.pub"))
21 |
22 | (defn encrypted-private-key-path
23 | [] (str (. System getProperty "user.home") "/.ssh/clj_ssh_pp"))
24 |
25 | (defn encrypted-public-key-path
26 | [] (str (. System getProperty "user.home") "/.ssh/clj_ssh_pp.pub"))
27 |
28 | (defn username
29 | [] (or (. System getProperty "ssh.username")
30 | (. System getProperty "user.name")))
31 |
--------------------------------------------------------------------------------
/test/clj_ssh/test_utils.clj:
--------------------------------------------------------------------------------
1 | (ns clj-ssh.test-utils
2 | (:use
3 | [clj-ssh.ssh :only [ssh-log-levels]])
4 | (:import com.jcraft.jsch.Logger))
5 |
6 | (def debug-log-levels
7 | {com.jcraft.jsch.Logger/DEBUG :debug
8 | com.jcraft.jsch.Logger/INFO :debug
9 | com.jcraft.jsch.Logger/WARN :debug
10 | com.jcraft.jsch.Logger/ERROR :error
11 | com.jcraft.jsch.Logger/FATAL :fatal})
12 |
13 | (defn quiet-ssh-logging [f]
14 | (let [levels @ssh-log-levels]
15 | (try
16 | (reset! ssh-log-levels debug-log-levels)
17 | (f)
18 | (finally
19 | (reset! ssh-log-levels levels)))))
20 |
21 | (defn sftp-monitor
22 | "Create a SFTP progress monitor"
23 | []
24 | (let [operation (atom nil)
25 | source (atom nil)
26 | destination (atom nil)
27 | number (atom nil)
28 | done (atom false)
29 | progress (atom 0)
30 | continue (atom true)]
31 | [ (proxy [com.jcraft.jsch.SftpProgressMonitor] []
32 | (init [op src dest max]
33 | (do
34 | (reset! operation op)
35 | (reset! source src)
36 | (reset! destination dest)
37 | (reset! number max)
38 | (reset! done false)))
39 | (count [n]
40 | (reset! progress n)
41 | @continue)
42 | (end []
43 | (reset! done true)))
44 | [operation source destination number done progress continue]]))
45 |
46 | (defn sftp-monitor-done [state]
47 | @(nth state 4))
48 |
49 | (defn cwd
50 | [] (. System getProperty "user.dir"))
51 |
52 | (defn home
53 | [] (. System getProperty "user.home"))
54 |
--------------------------------------------------------------------------------
/test/log4j.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 | logs/ssh.log
10 |
11 | logs/old/ssh.%d{yyyy-MM-dd}.log
12 | 3
13 |
14 |
15 | %date %level [%thread] %logger{10} %msg%n
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------