1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.junit.ssh;
11
12 import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES;
13 import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.OutputStream;
19 import java.nio.charset.StandardCharsets;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.security.GeneralSecurityException;
23 import java.security.KeyPair;
24 import java.security.PublicKey;
25 import java.text.MessageFormat;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.concurrent.TimeUnit;
32
33 import org.apache.sshd.common.NamedFactory;
34 import org.apache.sshd.common.NamedResource;
35 import org.apache.sshd.common.PropertyResolver;
36 import org.apache.sshd.common.SshConstants;
37 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
38 import org.apache.sshd.common.config.keys.KeyUtils;
39 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
40 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
41 import org.apache.sshd.common.signature.BuiltinSignatures;
42 import org.apache.sshd.common.signature.Signature;
43 import org.apache.sshd.common.util.buffer.Buffer;
44 import org.apache.sshd.common.util.security.SecurityUtils;
45 import org.apache.sshd.common.util.threads.CloseableExecutorService;
46 import org.apache.sshd.common.util.threads.ThreadUtils;
47 import org.apache.sshd.server.ServerAuthenticationManager;
48 import org.apache.sshd.server.ServerBuilder;
49 import org.apache.sshd.server.SshServer;
50 import org.apache.sshd.server.auth.UserAuth;
51 import org.apache.sshd.server.auth.UserAuthFactory;
52 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
53 import org.apache.sshd.server.auth.gss.UserAuthGSS;
54 import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
55 import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
56 import org.apache.sshd.server.command.AbstractCommandSupport;
57 import org.apache.sshd.server.session.ServerSession;
58 import org.apache.sshd.server.shell.UnknownCommand;
59 import org.apache.sshd.server.subsystem.SubsystemFactory;
60 import org.apache.sshd.sftp.server.SftpSubsystemFactory;
61 import org.eclipse.jgit.annotations.NonNull;
62 import org.eclipse.jgit.annotations.Nullable;
63 import org.eclipse.jgit.lib.Repository;
64 import org.eclipse.jgit.transport.ReceivePack;
65 import org.eclipse.jgit.transport.RemoteConfig;
66 import org.eclipse.jgit.transport.UploadPack;
67
68
69
70
71
72
73
74
75
76
77 public class SshTestGitServer {
78
79
80
81
82
83
84
85
86
87 public static final String ECHO_COMMAND = "echo";
88
89 @NonNull
90 protected final String testUser;
91
92 @NonNull
93 protected final Repository repository;
94
95 @NonNull
96 protected final List<KeyPair> hostKeys = new ArrayList<>();
97
98 protected final SshServer server;
99
100 @NonNull
101 protected PublicKey testKey;
102
103 private final CloseableExecutorService executorService = ThreadUtils
104 .newFixedThreadPool("SshTestGitServerPool", 2);
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
122 @NonNull Repository repository, @NonNull byte[] hostKey)
123 throws IOException, GeneralSecurityException {
124 this(testUser, readPublicKey(testKey), repository,
125 readKeyPair(hostKey));
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
145 @NonNull Repository repository, @NonNull KeyPair hostKey)
146 throws IOException, GeneralSecurityException {
147 this(testUser, readPublicKey(testKey), repository, hostKey);
148 }
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 public SshTestGitServer(@NonNull String testUser,
165 @NonNull PublicKey testKey, @NonNull Repository repository,
166 @NonNull KeyPair hostKey) {
167 this.testUser = testUser;
168 setTestUserPublicKey(testKey);
169 this.repository = repository;
170 ServerBuilder builder = ServerBuilder.builder()
171 .signatureFactories(getSignatureFactories());
172 server = builder.build();
173 hostKeys.add(hostKey);
174 server.setKeyPairProvider((session) -> hostKeys);
175
176 configureAuthentication();
177
178 List<SubsystemFactory> subsystems = configureSubsystems();
179 if (!subsystems.isEmpty()) {
180 server.setSubsystemFactories(subsystems);
181 }
182
183 configureShell();
184
185 server.setCommandFactory((channel, command) -> {
186 if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) {
187 return new GitUploadPackCommand(command, executorService);
188 } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
189 return new GitReceivePackCommand(command, executorService);
190 } else if (command.startsWith(ECHO_COMMAND)) {
191 return new EchoCommand(command, executorService);
192 }
193 return new UnknownCommand(command);
194 });
195 }
196
197
198
199
200
201
202
203 @SuppressWarnings("deprecation")
204 private static List<NamedFactory<Signature>> getSignatureFactories() {
205
206 return Arrays.asList(
207 BuiltinSignatures.nistp256_cert,
208 BuiltinSignatures.nistp384_cert,
209 BuiltinSignatures.nistp521_cert,
210 BuiltinSignatures.ed25519_cert,
211 BuiltinSignatures.rsaSHA512_cert,
212 BuiltinSignatures.rsaSHA256_cert,
213 BuiltinSignatures.rsa_cert,
214 BuiltinSignatures.nistp256,
215 BuiltinSignatures.nistp384,
216 BuiltinSignatures.nistp521,
217 BuiltinSignatures.ed25519,
218 BuiltinSignatures.sk_ecdsa_sha2_nistp256,
219 BuiltinSignatures.sk_ssh_ed25519,
220 BuiltinSignatures.rsaSHA512,
221 BuiltinSignatures.rsaSHA256,
222 BuiltinSignatures.rsa,
223 BuiltinSignatures.dsa_cert,
224 BuiltinSignatures.dsa);
225
226 }
227
228 private static PublicKey readPublicKey(Path key)
229 throws IOException, GeneralSecurityException {
230 return AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
231 .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
232 }
233
234 private static KeyPair readKeyPair(byte[] keyMaterial)
235 throws IOException, GeneralSecurityException {
236 try (ByteArrayInputStream in = new ByteArrayInputStream(keyMaterial)) {
237 return SecurityUtils.loadKeyPairIdentities(null, null, in, null)
238 .iterator().next();
239 }
240 }
241
242 private static class FakeUserAuthGSS extends UserAuthGSS {
243 @Override
244 protected @Nullable Boolean doAuth(Buffer buffer, boolean initial)
245 throws Exception {
246
247
248
249
250
251 if (initial) {
252 ServerSession session = getServerSession();
253 Buffer b = session.createBuffer(
254 SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST);
255 b.putBytes(KRB5_MECH.getDER());
256 session.writePacket(b);
257 return null;
258 }
259 return Boolean.FALSE;
260 }
261 }
262
263 private List<UserAuthFactory> getAuthFactories() {
264 List<UserAuthFactory> authentications = new ArrayList<>();
265 authentications.add(new UserAuthGSSFactory() {
266 @Override
267 public UserAuth createUserAuth(ServerSession session)
268 throws IOException {
269 return new FakeUserAuthGSS();
270 }
271 });
272 authentications.add(
273 ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
274 authentications.add(
275 ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY);
276 authentications.add(
277 ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY);
278 return authentications;
279 }
280
281
282
283
284
285
286
287 protected void configureAuthentication() {
288 server.setUserAuthFactories(getAuthFactories());
289
290 server.setPasswordAuthenticator(null);
291 server.setKeyboardInteractiveAuthenticator(null);
292 server.setHostBasedAuthenticator(null);
293
294 server.setGSSAuthenticator(new GSSAuthenticator() {
295 @Override
296 public boolean validateInitialUser(ServerSession session,
297 String user) {
298 return false;
299 }
300 });
301
302 server.setPublickeyAuthenticator((userName, publicKey, session) -> {
303 return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
304 .compareKeys(SshTestGitServer.this.testKey, publicKey);
305 });
306 }
307
308
309
310
311
312
313
314
315
316 @NonNull
317 protected List<SubsystemFactory> configureSubsystems() {
318
319 server.setFileSystemFactory(new VirtualFileSystemFactory(repository
320 .getDirectory().getParentFile().getAbsoluteFile().toPath()));
321 return Collections
322 .singletonList((new SftpSubsystemFactory.Builder()).build());
323 }
324
325
326
327
328
329 protected void configureShell() {
330
331 server.setShellFactory(null);
332 }
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347 public void addHostKey(@NonNull Path key, boolean inFront)
348 throws IOException, GeneralSecurityException {
349 try (InputStream in = Files.newInputStream(key)) {
350 KeyPair pair = SecurityUtils
351 .loadKeyPairIdentities(null,
352 NamedResource.ofName(key.toString()), in, null)
353 .iterator().next();
354 addHostKey(pair, inFront);
355 }
356 }
357
358
359
360
361
362
363
364
365
366
367 public void addHostKey(@NonNull KeyPair key, boolean inFront) {
368 if (inFront) {
369 hostKeys.add(0, key);
370 } else {
371 hostKeys.add(key);
372 }
373 }
374
375
376
377
378
379 public void enablePasswordAuthentication() {
380 server.setPasswordAuthenticator((user, pwd, session) -> {
381 return testUser.equals(user)
382 && testUser.toUpperCase(Locale.ROOT).equals(pwd);
383 });
384 }
385
386
387
388
389
390 public void enableKeyboardInteractiveAuthentication() {
391 server.setPasswordAuthenticator((user, pwd, session) -> {
392 return testUser.equals(user)
393 && testUser.toUpperCase(Locale.ROOT).equals(pwd);
394 });
395 server.setKeyboardInteractiveAuthenticator(
396 DefaultKeyboardInteractiveAuthenticator.INSTANCE);
397 }
398
399
400
401
402
403
404
405
406 public PropertyResolver getPropertyResolver() {
407 return server;
408 }
409
410
411
412
413
414
415
416
417 public int start() throws IOException {
418 server.start();
419 return server.getPort();
420 }
421
422
423
424
425
426
427 public void stop() throws IOException {
428 executorService.shutdownNow();
429 server.stop(true);
430 }
431
432
433
434
435
436
437
438
439
440
441
442 public void setTestUserPublicKey(Path key)
443 throws IOException, GeneralSecurityException {
444 this.testKey = readPublicKey(key);
445 }
446
447
448
449
450
451
452
453
454
455 public void setTestUserPublicKey(@NonNull PublicKey key) {
456 this.testKey = key;
457 }
458
459
460
461
462
463
464
465
466
467 public void setPreamble(String... lines) {
468 if (lines != null && lines.length > 0) {
469 SERVER_EXTRA_IDENTIFICATION_LINES.set(server, String.join(
470 String.valueOf(SERVER_EXTRA_IDENT_LINES_SEPARATOR), lines));
471 }
472 }
473
474 private class GitUploadPackCommand extends AbstractCommandSupport {
475
476 protected GitUploadPackCommand(String command,
477 CloseableExecutorService executorService) {
478 super(command, ThreadUtils.noClose(executorService));
479 }
480
481 @Override
482 public void run() {
483 UploadPack uploadPack = new UploadPack(repository);
484 String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
485 if (gitProtocol != null) {
486 uploadPack
487 .setExtraParameters(Collections.singleton(gitProtocol));
488 }
489 try {
490 uploadPack.upload(getInputStream(), getOutputStream(),
491 getErrorStream());
492 onExit(0);
493 } catch (IOException e) {
494 log.warn(
495 MessageFormat.format("Could not run {0}", getCommand()),
496 e);
497 onExit(-1, e.toString());
498 }
499 }
500
501 }
502
503 private class GitReceivePackCommand extends AbstractCommandSupport {
504
505 protected GitReceivePackCommand(String command,
506 CloseableExecutorService executorService) {
507 super(command, ThreadUtils.noClose(executorService));
508 }
509
510 @Override
511 public void run() {
512 try {
513 new ReceivePack(repository).receive(getInputStream(),
514 getOutputStream(), getErrorStream());
515 onExit(0);
516 } catch (IOException e) {
517 log.warn(
518 MessageFormat.format("Could not run {0}", getCommand()),
519 e);
520 onExit(-1, e.toString());
521 }
522 }
523
524 }
525
526
527
528
529
530
531 private static class EchoCommand extends AbstractCommandSupport {
532
533 protected EchoCommand(String command,
534 CloseableExecutorService executorService) {
535 super(command, ThreadUtils.noClose(executorService));
536 }
537
538 @Override
539 public void run() {
540 String[] parts = getCommand().split(" ");
541 int timeout = 0;
542 if (parts.length >= 2) {
543 try {
544 timeout = Integer.parseInt(parts[1]);
545 } catch (NumberFormatException e) {
546
547 }
548 if (timeout > 0) {
549 try {
550 Thread.sleep(TimeUnit.SECONDS.toMillis(timeout));
551 } catch (InterruptedException e) {
552
553 }
554 }
555 }
556 try {
557 doEcho(getCommand(), getOutputStream());
558 onExit(0);
559 } catch (IOException e) {
560 log.warn(
561 MessageFormat.format("Could not run {0}", getCommand()),
562 e);
563 onExit(-1, e.toString());
564 }
565 }
566
567 private void doEcho(String text, OutputStream stream)
568 throws IOException {
569 stream.write(text.getBytes(StandardCharsets.UTF_8));
570 stream.flush();
571 }
572 }
573 }