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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /*
  2. * Copyright (C) 2018, 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 java.io.ByteArrayInputStream;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.nio.file.Files;
  15. import java.nio.file.Path;
  16. import java.security.GeneralSecurityException;
  17. import java.security.KeyPair;
  18. import java.security.PublicKey;
  19. import java.text.MessageFormat;
  20. import java.util.ArrayList;
  21. import java.util.Collections;
  22. import java.util.List;
  23. import java.util.Locale;
  24. import org.apache.sshd.common.NamedFactory;
  25. import org.apache.sshd.common.NamedResource;
  26. import org.apache.sshd.common.PropertyResolverUtils;
  27. import org.apache.sshd.common.SshConstants;
  28. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  29. import org.apache.sshd.common.config.keys.KeyUtils;
  30. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  31. import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
  32. import org.apache.sshd.common.session.Session;
  33. import org.apache.sshd.common.util.buffer.Buffer;
  34. import org.apache.sshd.common.util.security.SecurityUtils;
  35. import org.apache.sshd.common.util.threads.CloseableExecutorService;
  36. import org.apache.sshd.common.util.threads.ThreadUtils;
  37. import org.apache.sshd.server.ServerAuthenticationManager;
  38. import org.apache.sshd.server.ServerFactoryManager;
  39. import org.apache.sshd.server.SshServer;
  40. import org.apache.sshd.server.auth.UserAuth;
  41. import org.apache.sshd.server.auth.gss.GSSAuthenticator;
  42. import org.apache.sshd.server.auth.gss.UserAuthGSS;
  43. import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
  44. import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
  45. import org.apache.sshd.server.command.AbstractCommandSupport;
  46. import org.apache.sshd.server.command.Command;
  47. import org.apache.sshd.server.session.ServerSession;
  48. import org.apache.sshd.server.shell.UnknownCommand;
  49. import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
  50. import org.eclipse.jgit.annotations.NonNull;
  51. import org.eclipse.jgit.lib.Repository;
  52. import org.eclipse.jgit.transport.ReceivePack;
  53. import org.eclipse.jgit.transport.RemoteConfig;
  54. import org.eclipse.jgit.transport.UploadPack;
  55. /**
  56. * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd.
  57. * <p>
  58. * Supports only a single repository. Authenticates only the given test user
  59. * against his given test public key. Supports fetch and push.
  60. * </p>
  61. *
  62. * @since 5.2
  63. */
  64. public class SshTestGitServer {
  65. @NonNull
  66. protected final String testUser;
  67. @NonNull
  68. protected final Repository repository;
  69. @NonNull
  70. protected final List<KeyPair> hostKeys = new ArrayList<>();
  71. protected final SshServer server;
  72. @NonNull
  73. protected PublicKey testKey;
  74. private final CloseableExecutorService executorService = ThreadUtils
  75. .newFixedThreadPool("SshTestGitServerPool", 2);
  76. /**
  77. * Creates a ssh git <em>test</em> server. It serves one single repository,
  78. * and accepts public-key authentication for exactly one test user.
  79. *
  80. * @param testUser
  81. * user name of the test user
  82. * @param testKey
  83. * <em>private</em> key file of the test user; the server will
  84. * only user the public key from it
  85. * @param repository
  86. * to serve
  87. * @param hostKey
  88. * the unencrypted private key to use as host key
  89. * @throws IOException
  90. * @throws GeneralSecurityException
  91. */
  92. public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
  93. @NonNull Repository repository, @NonNull byte[] hostKey)
  94. throws IOException, GeneralSecurityException {
  95. this.testUser = testUser;
  96. setTestUserPublicKey(testKey);
  97. this.repository = repository;
  98. server = SshServer.setUpDefaultServer();
  99. // Set host key
  100. try (ByteArrayInputStream in = new ByteArrayInputStream(hostKey)) {
  101. SecurityUtils.loadKeyPairIdentities(null, null, in, null)
  102. .forEach((k) -> hostKeys.add(k));
  103. } catch (IOException | GeneralSecurityException e) {
  104. // Ignore.
  105. }
  106. server.setKeyPairProvider((session) -> hostKeys);
  107. configureAuthentication();
  108. List<NamedFactory<Command>> subsystems = configureSubsystems();
  109. if (!subsystems.isEmpty()) {
  110. server.setSubsystemFactories(subsystems);
  111. }
  112. configureShell();
  113. server.setCommandFactory(command -> {
  114. if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) {
  115. return new GitUploadPackCommand(command, executorService);
  116. } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
  117. return new GitReceivePackCommand(command, executorService);
  118. }
  119. return new UnknownCommand(command);
  120. });
  121. }
  122. private static class FakeUserAuthGSS extends UserAuthGSS {
  123. @Override
  124. protected Boolean doAuth(Buffer buffer, boolean initial)
  125. throws Exception {
  126. // We always reply that we did do this, but then we fail at the
  127. // first token message. That way we can test that the client-side
  128. // sends the correct initial request and then is skipped correctly,
  129. // even if it causes a GSSException if Kerberos isn't configured at
  130. // all.
  131. if (initial) {
  132. ServerSession session = getServerSession();
  133. Buffer b = session.createBuffer(
  134. SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST);
  135. b.putBytes(KRB5_MECH.getDER());
  136. session.writePacket(b);
  137. return null;
  138. }
  139. return Boolean.FALSE;
  140. }
  141. }
  142. private List<NamedFactory<UserAuth>> getAuthFactories() {
  143. List<NamedFactory<UserAuth>> authentications = new ArrayList<>();
  144. authentications.add(new UserAuthGSSFactory() {
  145. @Override
  146. public UserAuth create() {
  147. return new FakeUserAuthGSS();
  148. }
  149. });
  150. authentications.add(
  151. ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
  152. authentications.add(
  153. ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY);
  154. authentications.add(
  155. ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY);
  156. return authentications;
  157. }
  158. /**
  159. * Configures the authentication mechanisms of this test server. Invoked
  160. * from the constructor. The default sets up public key authentication for
  161. * the test user, and a gssapi-with-mic authenticator that pretends to
  162. * support this mechanism, but that then refuses to authenticate anyone.
  163. */
  164. protected void configureAuthentication() {
  165. server.setUserAuthFactories(getAuthFactories());
  166. // Disable some authentications
  167. server.setPasswordAuthenticator(null);
  168. server.setKeyboardInteractiveAuthenticator(null);
  169. server.setHostBasedAuthenticator(null);
  170. // Pretend we did gssapi-with-mic.
  171. server.setGSSAuthenticator(new GSSAuthenticator() {
  172. @Override
  173. public boolean validateInitialUser(ServerSession session,
  174. String user) {
  175. return false;
  176. }
  177. });
  178. // Accept only the test user/public key
  179. server.setPublickeyAuthenticator((userName, publicKey, session) -> {
  180. return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
  181. .compareKeys(SshTestGitServer.this.testKey, publicKey);
  182. });
  183. }
  184. /**
  185. * Configures the test server's subsystems (sftp, scp). Invoked from the
  186. * constructor. The default provides a simple SFTP setup with the root
  187. * directory as the given repository's .git directory's parent. (I.e., at
  188. * the directory containing the .git directory.)
  189. *
  190. * @return A possibly empty collection of subsystems.
  191. */
  192. @NonNull
  193. protected List<NamedFactory<Command>> configureSubsystems() {
  194. // SFTP.
  195. server.setFileSystemFactory(new VirtualFileSystemFactory() {
  196. @Override
  197. protected Path computeRootDir(Session session) throws IOException {
  198. return SshTestGitServer.this.repository.getDirectory()
  199. .getParentFile().getAbsoluteFile().toPath();
  200. }
  201. });
  202. return Collections
  203. .singletonList((new SftpSubsystemFactory.Builder()).build());
  204. }
  205. /**
  206. * Configures shell access for the test server. The default provides no
  207. * shell at all.
  208. */
  209. protected void configureShell() {
  210. // No shell
  211. server.setShellFactory(null);
  212. }
  213. /**
  214. * Adds an additional host key to the server.
  215. *
  216. * @param key
  217. * path to the private key file; should not be encrypted
  218. * @param inFront
  219. * whether to add the new key before other existing keys
  220. * @throws IOException
  221. * if the file denoted by the {@link Path} {@code key} cannot be
  222. * read
  223. * @throws GeneralSecurityException
  224. * if the key contained in the file cannot be read
  225. */
  226. public void addHostKey(@NonNull Path key, boolean inFront)
  227. throws IOException, GeneralSecurityException {
  228. try (InputStream in = Files.newInputStream(key)) {
  229. KeyPair pair = SecurityUtils
  230. .loadKeyPairIdentities(null,
  231. NamedResource.ofName(key.toString()), in, null)
  232. .iterator().next();
  233. if (inFront) {
  234. hostKeys.add(0, pair);
  235. } else {
  236. hostKeys.add(pair);
  237. }
  238. }
  239. }
  240. /**
  241. * Enable password authentication. The server will accept the test user's
  242. * name, converted to all upper-case, as password.
  243. */
  244. public void enablePasswordAuthentication() {
  245. server.setPasswordAuthenticator((user, pwd, session) -> {
  246. return testUser.equals(user)
  247. && testUser.toUpperCase(Locale.ROOT).equals(pwd);
  248. });
  249. }
  250. /**
  251. * Enable keyboard-interactive authentication. The server will accept the
  252. * test user's name, converted to all upper-case, as password.
  253. */
  254. public void enableKeyboardInteractiveAuthentication() {
  255. server.setPasswordAuthenticator((user, pwd, session) -> {
  256. return testUser.equals(user)
  257. && testUser.toUpperCase(Locale.ROOT).equals(pwd);
  258. });
  259. server.setKeyboardInteractiveAuthenticator(
  260. DefaultKeyboardInteractiveAuthenticator.INSTANCE);
  261. }
  262. /**
  263. * Starts the test server, listening on a random port.
  264. *
  265. * @return the port the server listens on; test clients should connect to
  266. * that port
  267. * @throws IOException
  268. */
  269. public int start() throws IOException {
  270. server.start();
  271. return server.getPort();
  272. }
  273. /**
  274. * Stops the test server.
  275. *
  276. * @throws IOException
  277. */
  278. public void stop() throws IOException {
  279. executorService.shutdownNow();
  280. server.stop(true);
  281. }
  282. /**
  283. * Sets the test user's public key on the server.
  284. *
  285. * @param key
  286. * to set
  287. * @throws IOException
  288. * if the file cannot be read
  289. * @throws GeneralSecurityException
  290. * if the public key cannot be extracted from the file
  291. */
  292. public void setTestUserPublicKey(Path key)
  293. throws IOException, GeneralSecurityException {
  294. this.testKey = AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
  295. .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
  296. }
  297. /**
  298. * Sets the lines the server sends before its server identification in the
  299. * initial protocol version exchange.
  300. *
  301. * @param lines
  302. * to send
  303. * @since 5.5
  304. */
  305. public void setPreamble(String... lines) {
  306. if (lines != null && lines.length > 0) {
  307. PropertyResolverUtils.updateProperty(this.server,
  308. ServerFactoryManager.SERVER_EXTRA_IDENTIFICATION_LINES,
  309. String.join("|", lines));
  310. }
  311. }
  312. private class GitUploadPackCommand extends AbstractCommandSupport {
  313. protected GitUploadPackCommand(String command,
  314. CloseableExecutorService executorService) {
  315. super(command, ThreadUtils.noClose(executorService));
  316. }
  317. @Override
  318. public void run() {
  319. UploadPack uploadPack = new UploadPack(repository);
  320. String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
  321. if (gitProtocol != null) {
  322. uploadPack
  323. .setExtraParameters(Collections.singleton(gitProtocol));
  324. }
  325. try {
  326. uploadPack.upload(getInputStream(), getOutputStream(),
  327. getErrorStream());
  328. onExit(0);
  329. } catch (IOException e) {
  330. log.warn(
  331. MessageFormat.format("Could not run {0}", getCommand()),
  332. e);
  333. onExit(-1, e.toString());
  334. }
  335. }
  336. }
  337. private class GitReceivePackCommand extends AbstractCommandSupport {
  338. protected GitReceivePackCommand(String command,
  339. CloseableExecutorService executorService) {
  340. super(command, ThreadUtils.noClose(executorService));
  341. }
  342. @Override
  343. public void run() {
  344. try {
  345. new ReceivePack(repository).receive(getInputStream(),
  346. getOutputStream(), getErrorStream());
  347. onExit(0);
  348. } catch (IOException e) {
  349. log.warn(
  350. MessageFormat.format("Could not run {0}", getCommand()),
  351. e);
  352. onExit(-1, e.toString());
  353. }
  354. }
  355. }
  356. }