Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
SSHD |
|
| 1.25;1.25 | ||||
SSHD$1 |
|
| 1.25;1.25 | ||||
SSHD$2 |
|
| 1.25;1.25 |
1 | /** | |
2 | * Copyright (c) 2014-2015, jcabi.com | |
3 | * All rights reserved. | |
4 | * | |
5 | * Redistribution and use in source and binary forms, with or without | |
6 | * modification, are permitted provided that the following conditions | |
7 | * are met: 1) Redistributions of source code must retain the above | |
8 | * copyright notice, this list of conditions and the following | |
9 | * disclaimer. 2) Redistributions in binary form must reproduce the above | |
10 | * copyright notice, this list of conditions and the following | |
11 | * disclaimer in the documentation and/or other materials provided | |
12 | * with the distribution. 3) Neither the name of the jcabi.com nor | |
13 | * the names of its contributors may be used to endorse or promote | |
14 | * products derived from this software without specific prior written | |
15 | * permission. | |
16 | * | |
17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
18 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT | |
19 | * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
20 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL | |
21 | * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, | |
22 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
25 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, | |
26 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED | |
28 | * OF THE POSSIBILITY OF SUCH DAMAGE. | |
29 | */ | |
30 | package com.jcabi.ssh; | |
31 | ||
32 | import com.jcabi.log.VerboseProcess; | |
33 | import java.io.Closeable; | |
34 | import java.io.File; | |
35 | import java.io.FileOutputStream; | |
36 | import java.io.IOException; | |
37 | import java.net.ServerSocket; | |
38 | import java.util.concurrent.TimeUnit; | |
39 | import org.apache.commons.io.IOUtils; | |
40 | import org.apache.commons.lang3.CharEncoding; | |
41 | ||
42 | /** | |
43 | * Test SSHD daemon (only for Linux). | |
44 | * | |
45 | * <p>It is a convenient class for unit testing of your SSH | |
46 | * clients: | |
47 | * | |
48 | * <pre> try (SSHD sshd = new SSHD()) { | |
49 | * String uptime = new Shell.Plain( | |
50 | * SSH(sshd.host(), sshd.login(), sshd.port(), sshd.key()) | |
51 | * ).exec("uptime"); | |
52 | * }</pre> | |
53 | * | |
54 | * <p>If you forget to call {@link #close()}, SSH daemon will be | |
55 | * up and running until a shutdown of the JVM.</p> | |
56 | * | |
57 | * @author Yegor Bugayenko (yegor@teamed.io) | |
58 | * @version $Id: 8a7f6e6f48d8cfd0672a271fd83f443ec5f6e340 $ | |
59 | * @since 1.0 | |
60 | * @checkstyle MultipleStringLiteralsCheck (500 lines) | |
61 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) | |
62 | */ | |
63 | @SuppressWarnings("PMD.DoNotUseThreads") | |
64 | 2 | public final class SSHD implements Closeable { |
65 | ||
66 | /** | |
67 | * Temp directory. | |
68 | */ | |
69 | private final transient File dir; | |
70 | ||
71 | /** | |
72 | * The process with SSHD. | |
73 | */ | |
74 | private final transient Process process; | |
75 | ||
76 | /** | |
77 | * Port. | |
78 | */ | |
79 | private final transient int prt; | |
80 | ||
81 | /** | |
82 | * Ctor. | |
83 | * @throws IOException If fails | |
84 | * @since 1.5 | |
85 | */ | |
86 | public SSHD() throws IOException { | |
87 | 1 | this(new File(System.getProperty("java.io.tmpdir"))); |
88 | 1 | } |
89 | ||
90 | /** | |
91 | * Ctor. | |
92 | * @param path Directory to work in | |
93 | * @throws IOException If fails | |
94 | */ | |
95 | 2 | public SSHD(final File path) throws IOException { |
96 | 2 | this.dir = path; |
97 | 2 | final File rsa = new File(this.dir, "host_rsa_key"); |
98 | 2 | IOUtils.copy( |
99 | this.getClass().getResourceAsStream("ssh_host_rsa_key"), | |
100 | new FileOutputStream(rsa) | |
101 | ); | |
102 | 2 | final File keys = new File(this.dir, "authorized"); |
103 | 2 | IOUtils.copy( |
104 | this.getClass().getResourceAsStream("authorized_keys"), | |
105 | new FileOutputStream(keys) | |
106 | ); | |
107 | 2 | new VerboseProcess( |
108 | new ProcessBuilder().command( | |
109 | "chmod", "600", | |
110 | keys.getAbsolutePath(), | |
111 | rsa.getAbsolutePath() | |
112 | ) | |
113 | ).stdout(); | |
114 | 2 | this.prt = SSHD.reserve(); |
115 | 2 | this.process = new ProcessBuilder().command( |
116 | "/usr/sbin/sshd", | |
117 | "-p", | |
118 | Integer.toString(this.prt), | |
119 | "-h", | |
120 | rsa.getAbsolutePath(), | |
121 | "-D", | |
122 | "-e", | |
123 | "-o", String.format("PidFile=%s", new File(this.dir, "pid")), | |
124 | "-o", "UsePAM=no", | |
125 | "-o", String.format("AuthorizedKeysFile=%s", keys), | |
126 | "-o", "StrictModes=no" | |
127 | ).start(); | |
128 | 2 | new Thread( |
129 | 2 | new Runnable() { |
130 | @Override | |
131 | public void run() { | |
132 | 2 | new VerboseProcess(SSHD.this.process).stdout(); |
133 | 2 | } |
134 | } | |
135 | ).start(); | |
136 | 2 | Runtime.getRuntime().addShutdownHook( |
137 | new Thread( | |
138 | 2 | new Runnable() { |
139 | @Override | |
140 | public void run() { | |
141 | 0 | SSHD.this.close(); |
142 | 0 | } |
143 | } | |
144 | ) | |
145 | ); | |
146 | try { | |
147 | 2 | TimeUnit.SECONDS.sleep(1L); |
148 | 0 | } catch (final InterruptedException ex) { |
149 | 0 | Thread.currentThread().interrupt(); |
150 | 0 | throw new IllegalStateException(ex); |
151 | 2 | } |
152 | 2 | } |
153 | ||
154 | @Override | |
155 | public void close() { | |
156 | 2 | this.process.destroy(); |
157 | 2 | } |
158 | ||
159 | /** | |
160 | * Get home dir. | |
161 | * @return Dir | |
162 | */ | |
163 | public File home() { | |
164 | 0 | return this.dir; |
165 | } | |
166 | ||
167 | /** | |
168 | * Get user name to login. | |
169 | * @return User name | |
170 | */ | |
171 | public String login() { | |
172 | 2 | return new VerboseProcess( |
173 | new ProcessBuilder().command("id", "-n", "-u") | |
174 | ).stdout().trim(); | |
175 | } | |
176 | ||
177 | /** | |
178 | * Get host of SSH. | |
179 | * @return Hostname | |
180 | * @since 1.1 | |
181 | */ | |
182 | public String host() { | |
183 | 2 | return new VerboseProcess( |
184 | new ProcessBuilder().command("hostname") | |
185 | ).stdout().trim(); | |
186 | } | |
187 | ||
188 | /** | |
189 | * Get port. | |
190 | * | |
191 | * <p>Don't forget to start | |
192 | * | |
193 | * @return Port number | |
194 | * @since 1.1 | |
195 | */ | |
196 | public int port() { | |
197 | 2 | return this.prt; |
198 | } | |
199 | ||
200 | /** | |
201 | * Get private SSH key for login. | |
202 | * @return Key | |
203 | * @throws IOException If fails | |
204 | */ | |
205 | public String key() throws IOException { | |
206 | 1 | return IOUtils.toString( |
207 | this.getClass().getResourceAsStream("id_rsa"), | |
208 | CharEncoding.UTF_8 | |
209 | ); | |
210 | } | |
211 | ||
212 | /** | |
213 | * Get an instance of Shell. | |
214 | * @return Shell | |
215 | * @throws IOException If fails | |
216 | */ | |
217 | public Shell connect() throws IOException { | |
218 | 1 | return new SSH(this.host(), this.port(), this.login(), this.key()); |
219 | } | |
220 | ||
221 | /** | |
222 | * Get a random TCP port. | |
223 | * @return Port | |
224 | * @throws IOException If fails | |
225 | */ | |
226 | private static int reserve() throws IOException { | |
227 | 2 | final ServerSocket socket = new ServerSocket(0); |
228 | try { | |
229 | 2 | return socket.getLocalPort(); |
230 | } finally { | |
231 | 2 | socket.close(); |
232 | } | |
233 | } | |
234 | ||
235 | } |