You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SshTestGitServer.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /*
  2. * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.junit.ssh;
  11. import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES;
  12. import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR;
  13. import java.io.ByteArrayInputStream;
  14. import java.io.IOException;
  15. import java.io.InputStream;
  16. import java.io.OutputStream;
  17. import java.nio.charset.StandardCharsets;
  18. import java.nio.file.Files;
  19. import java.nio.file.Path;
  20. import java.security.GeneralSecurityException;
  21. import java.security.KeyPair;
  22. import java.security.PublicKey;
  23. import java.text.MessageFormat;
  24. import java.util.ArrayList;
  25. import java.util.Arrays;
  26. import java.util.Collections;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.concurrent.TimeUnit;
  30. import org.apache.sshd.common.NamedFactory;
  31. import org.apache.sshd.common.NamedResource;
  32. import org.apache.sshd.common.PropertyResolver;
  33. import org.apache.sshd.common.SshConstants;
  34. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  35. import org.apache.sshd.common.config.keys.KeyUtils;
  36. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  37. import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
  38. import org.apache.sshd.common.signature.BuiltinSignatures;
  39. import org.apache.sshd.common.signature.Signature;
  40. import org.apache.sshd.common.util.buffer.Buffer;
  41. import org.apache.sshd.common.util.security.SecurityUtils;
  42. import org.apache.sshd.common.util.threads.CloseableExecutorService;
  43. import org.apache.sshd.common.util.threads.ThreadUtils;
  44. import org.apache.sshd.server.ServerAuthenticationManager;
  45. import org.apache.sshd.server.ServerBuilder;
  46. import org.apache.sshd.server.SshServer;
  47. import org.apache.sshd.server.auth.UserAuth;
  48. import org.apache.sshd.server.auth.UserAuthFactory;
  49. import org.apache.sshd.server.auth.gss.GSSAuthenticator;
  50. import org.apache.sshd.server.auth.gss.UserAuthGSS;
  51. import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
  52. import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
  53. import org.apache.sshd.server.command.AbstractCommandSupport;
  54. import org.apache.sshd.server.session.ServerSession;
  55. import org.apache.sshd.server.shell.UnknownCommand;
  56. import org.apache.sshd.server.subsystem.SubsystemFactory;
  57. import org.apache.sshd.sftp.server.SftpSubsystemFactory;
  58. import org.eclipse.jgit.annotations.NonNull;
  59. import org.eclipse.jgit.annotations.Nullable;
  60. import org.eclipse.jgit.lib.Repository;
  61. import org.eclipse.jgit.transport.ReceivePack;
  62. import org.eclipse.jgit.transport.RemoteConfig;
  63. import org.eclipse.jgit.transport.UploadPack;
  64. /**
  65. * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd.
  66. * <p>
  67. * Supports only a single repository. Authenticates only the given test user
  68. * against his given test public key. Supports fetch and push.
  69. * </p>
  70. *
  71. * @since 5.2
  72. */
  73. public class SshTestGitServer {
  74. /**
  75. * Simple echo test command. Replies with the command string as passed. If
  76. * of the form "echo [int] anything", takes the integer value as a delay in
  77. * seconds before replying, which may be useful to test various
  78. * timeout-related things.
  79. *
  80. * @since 5.9
  81. */
  82. public static final String ECHO_COMMAND = "echo";
  83. @NonNull
  84. protected final String testUser;
  85. @NonNull
  86. protected final Repository repository;
  87. @NonNull
  88. protected final List<KeyPair> hostKeys = new ArrayList<>();
  89. protected final SshServer server;
  90. @NonNull
  91. protected PublicKey testKey;
  92. private final CloseableExecutorService executorService = ThreadUtils
  93. .newFixedThreadPool("SshTestGitServerPool", 2);
  94. /**
  95. * Creates a ssh git <em>test</em> server. It serves one single repository,
  96. * and accepts public-key authentication for exactly one test user.
  97. *
  98. * @param testUser
  99. * user name of the test user
  100. * @param testKey
  101. * public key file of the test user
  102. * @param repository
  103. * to serve
  104. * @param hostKey
  105. * the unencrypted private key to use as host key
  106. * @throws IOException
  107. * @throws GeneralSecurityException
  108. */
  109. public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
  110. @NonNull Repository repository, @NonNull byte[] hostKey)
  111. throws IOException, GeneralSecurityException {
  112. this(testUser, readPublicKey(testKey), repository,
  113. readKeyPair(hostKey));
  114. }
  115. /**
  116. * Creates a ssh git <em>test</em> server. It serves one single repository,
  117. * and accepts public-key authentication for exactly one test user.
  118. *
  119. * @param testUser
  120. * user name of the test user
  121. * @param testKey
  122. * public key file of the test user
  123. * @param repository
  124. * to serve
  125. * @param hostKey
  126. * the unencrypted private key to use as host key
  127. * @throws IOException
  128. * @throws GeneralSecurityException
  129. * @since 5.9
  130. */
  131. public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
  132. @NonNull Repository repository, @NonNull KeyPair hostKey)
  133. throws IOException, GeneralSecurityException {
  134. this(testUser, readPublicKey(testKey), repository, hostKey);
  135. }
  136. /**
  137. * Creates a ssh git <em>test</em> server. It serves one single repository,
  138. * and accepts public-key authentication for exactly one test user.
  139. *
  140. * @param testUser
  141. * user name of the test user
  142. * @param testKey
  143. * the {@link PublicKey} of the test user
  144. * @param repository
  145. * to serve
  146. * @param hostKey
  147. * the {@link KeyPair} to use as host key
  148. * @since 5.9
  149. */
  150. public SshTestGitServer(@NonNull String testUser,
  151. @NonNull PublicKey testKey, @NonNull Repository repository,
  152. @NonNull KeyPair hostKey) {
  153. this.testUser = testUser;
  154. setTestUserPublicKey(testKey);
  155. this.repository = repository;
  156. ServerBuilder builder = ServerBuilder.builder()
  157. .signatureFactories(getSignatureFactories());
  158. server = builder.build();
  159. hostKeys.add(hostKey);
  160. server.setKeyPairProvider((session) -> hostKeys);
  161. configureAuthentication();
  162. List<SubsystemFactory> subsystems = configureSubsystems();
  163. if (!subsystems.isEmpty()) {
  164. server.setSubsystemFactories(subsystems);
  165. }
  166. configureShell();
  167. server.setCommandFactory((channel, command) -> {
  168. if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) {
  169. return new GitUploadPackCommand(command, executorService);
  170. } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
  171. return new GitReceivePackCommand(command, executorService);
  172. } else if (command.startsWith(ECHO_COMMAND)) {
  173. return new EchoCommand(command, executorService);
  174. }
  175. return new UnknownCommand(command);
  176. });
  177. }
  178. /**
  179. * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to
  180. * set it up explicitly to still allow users to connect with DSA keys.
  181. *
  182. * @return a list of supported signature factories
  183. */
  184. @SuppressWarnings("deprecation")
  185. private static List<NamedFactory<Signature>> getSignatureFactories() {
  186. // @formatter:off
  187. return Arrays.asList(
  188. BuiltinSignatures.nistp256_cert,
  189. BuiltinSignatures.nistp384_cert,
  190. BuiltinSignatures.nistp521_cert,
  191. BuiltinSignatures.ed25519_cert,
  192. BuiltinSignatures.rsaSHA512_cert,
  193. BuiltinSignatures.rsaSHA256_cert,
  194. BuiltinSignatures.rsa_cert,
  195. BuiltinSignatures.nistp256,
  196. BuiltinSignatures.nistp384,
  197. BuiltinSignatures.nistp521,
  198. BuiltinSignatures.ed25519,
  199. BuiltinSignatures.sk_ecdsa_sha2_nistp256,
  200. BuiltinSignatures.sk_ssh_ed25519,
  201. BuiltinSignatures.rsaSHA512,
  202. BuiltinSignatures.rsaSHA256,
  203. BuiltinSignatures.rsa,
  204. BuiltinSignatures.dsa_cert,
  205. BuiltinSignatures.dsa);
  206. // @formatter:on
  207. }
  208. private static PublicKey readPublicKey(Path key)
  209. throws IOException, GeneralSecurityException {
  210. return AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
  211. .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
  212. }
  213. private static KeyPair readKeyPair(byte[] keyMaterial)
  214. throws IOException, GeneralSecurityException {
  215. try (ByteArrayInputStream in = new ByteArrayInputStream(keyMaterial)) {
  216. return SecurityUtils.loadKeyPairIdentities(null, null, in, null)
  217. .iterator().next();
  218. }
  219. }
  220. private static class FakeUserAuthGSS extends UserAuthGSS {
  221. @Override
  222. protected @Nullable Boolean doAuth(Buffer buffer, boolean initial)
  223. throws Exception {
  224. // We always reply that we did do this, but then we fail at the
  225. // first token message. That way we can test that the client-side
  226. // sends the correct initial request and then is skipped correctly,
  227. // even if it causes a GSSException if Kerberos isn't configured at
  228. // all.
  229. if (initial) {
  230. ServerSession session = getServerSession();
  231. Buffer b = session.createBuffer(
  232. SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST);
  233. b.putBytes(KRB5_MECH.getDER());
  234. session.writePacket(b);
  235. return null;
  236. }
  237. return Boolean.FALSE;
  238. }
  239. }
  240. private List<UserAuthFactory> getAuthFactories() {
  241. List<UserAuthFactory> authentications = new ArrayList<>();
  242. authentications.add(new UserAuthGSSFactory() {
  243. @Override
  244. public UserAuth createUserAuth(ServerSession session)
  245. throws IOException {
  246. return new FakeUserAuthGSS();
  247. }
  248. });
  249. authentications.add(
  250. ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
  251. authentications.add(
  252. ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY);
  253. authentications.add(
  254. ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY);
  255. return authentications;
  256. }
  257. /**
  258. * Configures the authentication mechanisms of this test server. Invoked
  259. * from the constructor. The default sets up public key authentication for
  260. * the test user, and a gssapi-with-mic authenticator that pretends to
  261. * support this mechanism, but that then refuses to authenticate anyone.
  262. */
  263. protected void configureAuthentication() {
  264. server.setUserAuthFactories(getAuthFactories());
  265. // Disable some authentications
  266. server.setPasswordAuthenticator(null);
  267. server.setKeyboardInteractiveAuthenticator(null);
  268. server.setHostBasedAuthenticator(null);
  269. // Pretend we did gssapi-with-mic.
  270. server.setGSSAuthenticator(new GSSAuthenticator() {
  271. @Override
  272. public boolean validateInitialUser(ServerSession session,
  273. String user) {
  274. return false;
  275. }
  276. });
  277. // Accept only the test user/public key
  278. server.setPublickeyAuthenticator((userName, publicKey, session) -> {
  279. return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
  280. .compareKeys(SshTestGitServer.this.testKey, publicKey);
  281. });
  282. }
  283. /**
  284. * Configures the test server's subsystems (sftp, scp). Invoked from the
  285. * constructor. The default provides a simple SFTP setup with the root
  286. * directory as the given repository's .git directory's parent. (I.e., at
  287. * the directory containing the .git directory.)
  288. *
  289. * @return A possibly empty collection of subsystems.
  290. */
  291. @NonNull
  292. protected List<SubsystemFactory> configureSubsystems() {
  293. // SFTP.
  294. server.setFileSystemFactory(new VirtualFileSystemFactory(repository
  295. .getDirectory().getParentFile().getAbsoluteFile().toPath()));
  296. return Collections
  297. .singletonList((new SftpSubsystemFactory.Builder()).build());
  298. }
  299. /**
  300. * Configures shell access for the test server. The default provides no
  301. * shell at all.
  302. */
  303. protected void configureShell() {
  304. // No shell
  305. server.setShellFactory(null);
  306. }
  307. /**
  308. * Adds an additional host key to the server.
  309. *
  310. * @param key
  311. * path to the private key file; should not be encrypted
  312. * @param inFront
  313. * whether to add the new key before other existing keys
  314. * @throws IOException
  315. * if the file denoted by the {@link Path} {@code key} cannot be
  316. * read
  317. * @throws GeneralSecurityException
  318. * if the key contained in the file cannot be read
  319. */
  320. public void addHostKey(@NonNull Path key, boolean inFront)
  321. throws IOException, GeneralSecurityException {
  322. try (InputStream in = Files.newInputStream(key)) {
  323. KeyPair pair = SecurityUtils
  324. .loadKeyPairIdentities(null,
  325. NamedResource.ofName(key.toString()), in, null)
  326. .iterator().next();
  327. addHostKey(pair, inFront);
  328. }
  329. }
  330. /**
  331. * Adds an additional host key to the server.
  332. *
  333. * @param key
  334. * {@link KeyPair} to add
  335. * @param inFront
  336. * whether to add the new key before other existing keys
  337. * @since 5.8
  338. */
  339. public void addHostKey(@NonNull KeyPair key, boolean inFront) {
  340. if (inFront) {
  341. hostKeys.add(0, key);
  342. } else {
  343. hostKeys.add(key);
  344. }
  345. }
  346. /**
  347. * Enable password authentication. The server will accept the test user's
  348. * name, converted to all upper-case, as password.
  349. */
  350. public void enablePasswordAuthentication() {
  351. server.setPasswordAuthenticator((user, pwd, session) -> {
  352. return testUser.equals(user)
  353. && testUser.toUpperCase(Locale.ROOT).equals(pwd);
  354. });
  355. }
  356. /**
  357. * Enable keyboard-interactive authentication. The server will accept the
  358. * test user's name, converted to all upper-case, as password.
  359. */
  360. public void enableKeyboardInteractiveAuthentication() {
  361. server.setPasswordAuthenticator((user, pwd, session) -> {
  362. return testUser.equals(user)
  363. && testUser.toUpperCase(Locale.ROOT).equals(pwd);
  364. });
  365. server.setKeyboardInteractiveAuthenticator(
  366. DefaultKeyboardInteractiveAuthenticator.INSTANCE);
  367. }
  368. /**
  369. * Retrieves the server's {@link PropertyResolver}, giving access to server
  370. * properties.
  371. *
  372. * @return the {@link PropertyResolver}
  373. * @since 5.9
  374. */
  375. public PropertyResolver getPropertyResolver() {
  376. return server;
  377. }
  378. /**
  379. * Starts the test server, listening on a random port.
  380. *
  381. * @return the port the server listens on; test clients should connect to
  382. * that port
  383. * @throws IOException
  384. */
  385. public int start() throws IOException {
  386. server.start();
  387. return server.getPort();
  388. }
  389. /**
  390. * Stops the test server.
  391. *
  392. * @throws IOException
  393. */
  394. public void stop() throws IOException {
  395. executorService.shutdownNow();
  396. server.stop(true);
  397. }
  398. /**
  399. * Sets the test user's public key on the server.
  400. *
  401. * @param key
  402. * to set
  403. * @throws IOException
  404. * if the file cannot be read
  405. * @throws GeneralSecurityException
  406. * if the public key cannot be extracted from the file
  407. */
  408. public void setTestUserPublicKey(Path key)
  409. throws IOException, GeneralSecurityException {
  410. this.testKey = readPublicKey(key);
  411. }
  412. /**
  413. * Sets the test user's public key on the server.
  414. *
  415. * @param key
  416. * to set
  417. *
  418. * @since 5.8
  419. */
  420. public void setTestUserPublicKey(@NonNull PublicKey key) {
  421. this.testKey = key;
  422. }
  423. /**
  424. * Sets the lines the server sends before its server identification in the
  425. * initial protocol version exchange.
  426. *
  427. * @param lines
  428. * to send
  429. * @since 5.5
  430. */
  431. public void setPreamble(String... lines) {
  432. if (lines != null && lines.length > 0) {
  433. SERVER_EXTRA_IDENTIFICATION_LINES.set(server, String.join(
  434. String.valueOf(SERVER_EXTRA_IDENT_LINES_SEPARATOR), lines));
  435. }
  436. }
  437. private class GitUploadPackCommand extends AbstractCommandSupport {
  438. protected GitUploadPackCommand(String command,
  439. CloseableExecutorService executorService) {
  440. super(command, ThreadUtils.noClose(executorService));
  441. }
  442. @Override
  443. public void run() {
  444. UploadPack uploadPack = new UploadPack(repository);
  445. String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
  446. if (gitProtocol != null) {
  447. uploadPack
  448. .setExtraParameters(Collections.singleton(gitProtocol));
  449. }
  450. try {
  451. uploadPack.upload(getInputStream(), getOutputStream(),
  452. getErrorStream());
  453. onExit(0);
  454. } catch (IOException e) {
  455. log.warn(
  456. MessageFormat.format("Could not run {0}", getCommand()),
  457. e);
  458. onExit(-1, e.toString());
  459. }
  460. }
  461. }
  462. private class GitReceivePackCommand extends AbstractCommandSupport {
  463. protected GitReceivePackCommand(String command,
  464. CloseableExecutorService executorService) {
  465. super(command, ThreadUtils.noClose(executorService));
  466. }
  467. @Override
  468. public void run() {
  469. try {
  470. new ReceivePack(repository).receive(getInputStream(),
  471. getOutputStream(), getErrorStream());
  472. onExit(0);
  473. } catch (IOException e) {
  474. log.warn(
  475. MessageFormat.format("Could not run {0}", getCommand()),
  476. e);
  477. onExit(-1, e.toString());
  478. }
  479. }
  480. }
  481. /**
  482. * Simple echo command that echoes back the command string. If the first
  483. * argument is a positive integer, it's taken as a delay (in seconds) before
  484. * replying. Assumes UTF-8 character encoding.
  485. */
  486. private static class EchoCommand extends AbstractCommandSupport {
  487. protected EchoCommand(String command,
  488. CloseableExecutorService executorService) {
  489. super(command, ThreadUtils.noClose(executorService));
  490. }
  491. @Override
  492. public void run() {
  493. String[] parts = getCommand().split(" ");
  494. int timeout = 0;
  495. if (parts.length >= 2) {
  496. try {
  497. timeout = Integer.parseInt(parts[1]);
  498. } catch (NumberFormatException e) {
  499. // No timeout.
  500. }
  501. if (timeout > 0) {
  502. try {
  503. Thread.sleep(TimeUnit.SECONDS.toMillis(timeout));
  504. } catch (InterruptedException e) {
  505. // Ignore.
  506. }
  507. }
  508. }
  509. try {
  510. doEcho(getCommand(), getOutputStream());
  511. onExit(0);
  512. } catch (IOException e) {
  513. log.warn(
  514. MessageFormat.format("Could not run {0}", getCommand()),
  515. e);
  516. onExit(-1, e.toString());
  517. }
  518. }
  519. private void doEcho(String text, OutputStream stream)
  520. throws IOException {
  521. stream.write(text.getBytes(StandardCharsets.UTF_8));
  522. stream.flush();
  523. }
  524. }
  525. }