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

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