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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.junit.ssh;
  44. import java.io.ByteArrayInputStream;
  45. import java.io.IOException;
  46. import java.io.InputStream;
  47. import java.nio.file.Files;
  48. import java.nio.file.Path;
  49. import java.security.GeneralSecurityException;
  50. import java.security.KeyPair;
  51. import java.security.PublicKey;
  52. import java.text.MessageFormat;
  53. import java.util.ArrayList;
  54. import java.util.Collections;
  55. import java.util.List;
  56. import java.util.concurrent.ExecutorService;
  57. import java.util.concurrent.Executors;
  58. import org.apache.sshd.common.NamedFactory;
  59. import org.apache.sshd.common.SshConstants;
  60. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  61. import org.apache.sshd.common.config.keys.KeyUtils;
  62. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  63. import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
  64. import org.apache.sshd.common.session.Session;
  65. import org.apache.sshd.common.util.buffer.Buffer;
  66. import org.apache.sshd.common.util.security.SecurityUtils;
  67. import org.apache.sshd.server.ServerAuthenticationManager;
  68. import org.apache.sshd.server.SshServer;
  69. import org.apache.sshd.server.auth.UserAuth;
  70. import org.apache.sshd.server.auth.gss.GSSAuthenticator;
  71. import org.apache.sshd.server.auth.gss.UserAuthGSS;
  72. import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
  73. import org.apache.sshd.server.command.AbstractCommandSupport;
  74. import org.apache.sshd.server.command.Command;
  75. import org.apache.sshd.server.session.ServerSession;
  76. import org.apache.sshd.server.shell.UnknownCommand;
  77. import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
  78. import org.eclipse.jgit.annotations.NonNull;
  79. import org.eclipse.jgit.lib.Repository;
  80. import org.eclipse.jgit.transport.ReceivePack;
  81. import org.eclipse.jgit.transport.RemoteConfig;
  82. import org.eclipse.jgit.transport.UploadPack;
  83. /**
  84. * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd.
  85. * <p>
  86. * Supports only a single repository. Authenticates only the given test user
  87. * against his given test public key. Supports fetch and push.
  88. * </p>
  89. *
  90. * @since 5.2
  91. */
  92. public class SshTestGitServer {
  93. @NonNull
  94. protected final String testUser;
  95. @NonNull
  96. protected final Repository repository;
  97. @NonNull
  98. protected final List<KeyPair> hostKeys = new ArrayList<>();
  99. protected final SshServer server;
  100. @NonNull
  101. protected PublicKey testKey;
  102. private final ExecutorService executorService = Executors
  103. .newFixedThreadPool(2);
  104. /**
  105. * Creates a ssh git <em>test</em> server. It serves one single repository,
  106. * and accepts public-key authentication for exactly one test user.
  107. *
  108. * @param testUser
  109. * user name of the test user
  110. * @param testKey
  111. * <em>private</em> key file of the test user; the server will
  112. * only user the public key from it
  113. * @param repository
  114. * to serve
  115. * @param hostKey
  116. * the unencrypted private key to use as host key
  117. * @throws IOException
  118. * @throws GeneralSecurityException
  119. */
  120. public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
  121. @NonNull Repository repository, @NonNull byte[] hostKey)
  122. throws IOException, GeneralSecurityException {
  123. this.testUser = testUser;
  124. setTestUserPublicKey(testKey);
  125. this.repository = repository;
  126. server = SshServer.setUpDefaultServer();
  127. // Set host key
  128. try (ByteArrayInputStream in = new ByteArrayInputStream(hostKey)) {
  129. hostKeys.add(SecurityUtils.loadKeyPairIdentity("", in, null));
  130. } catch (IOException | GeneralSecurityException e) {
  131. // Ignore.
  132. }
  133. server.setKeyPairProvider(() -> hostKeys);
  134. configureAuthentication();
  135. List<NamedFactory<Command>> subsystems = configureSubsystems();
  136. if (!subsystems.isEmpty()) {
  137. server.setSubsystemFactories(subsystems);
  138. }
  139. configureShell();
  140. server.setCommandFactory(command -> {
  141. if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) {
  142. return new GitUploadPackCommand(command, executorService);
  143. } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
  144. return new GitReceivePackCommand(command, executorService);
  145. }
  146. return new UnknownCommand(command);
  147. });
  148. }
  149. private static class FakeUserAuthGSS extends UserAuthGSS {
  150. @Override
  151. protected Boolean doAuth(Buffer buffer, boolean initial)
  152. throws Exception {
  153. // We always reply that we did do this, but then we fail at the
  154. // first token message. That way we can test that the client-side
  155. // sends the correct initial request and then is skipped correctly,
  156. // even if it causes a GSSException if Kerberos isn't configured at
  157. // all.
  158. if (initial) {
  159. ServerSession session = getServerSession();
  160. Buffer b = session.createBuffer(
  161. SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST);
  162. b.putBytes(KRB5_MECH.getDER());
  163. session.writePacket(b);
  164. return null;
  165. }
  166. return Boolean.FALSE;
  167. }
  168. }
  169. private List<NamedFactory<UserAuth>> getAuthFactories() {
  170. List<NamedFactory<UserAuth>> authentications = new ArrayList<>();
  171. authentications.add(
  172. ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
  173. authentications.add(new UserAuthGSSFactory() {
  174. @Override
  175. public UserAuth create() {
  176. return new FakeUserAuthGSS();
  177. }
  178. });
  179. return authentications;
  180. }
  181. /**
  182. * Configures the authentication mechanisms of this test server. Invoked
  183. * from the constructor. The default sets up public key authentication for
  184. * the test user, and a gssapi-with-mic authenticator that pretends to
  185. * support this mechanism, but that then refuses to authenticate anyone.
  186. */
  187. protected void configureAuthentication() {
  188. server.setUserAuthFactories(getAuthFactories());
  189. // Disable some authentications
  190. server.setPasswordAuthenticator(null);
  191. server.setKeyboardInteractiveAuthenticator(null);
  192. server.setHostBasedAuthenticator(null);
  193. // Pretend we did gssapi-with-mic.
  194. server.setGSSAuthenticator(new GSSAuthenticator() {
  195. @Override
  196. public boolean validateInitialUser(ServerSession session,
  197. String user) {
  198. return false;
  199. }
  200. });
  201. // Accept only the test user/public key
  202. server.setPublickeyAuthenticator((userName, publicKey, session) -> {
  203. return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
  204. .compareKeys(SshTestGitServer.this.testKey, publicKey);
  205. });
  206. }
  207. /**
  208. * Configures the test server's subsystems (sftp, scp). Invoked from the
  209. * constructor. The default provides a simple SFTP setup with the root
  210. * directory as the given repository's .git directory's parent. (I.e., at
  211. * the directory containing the .git directory.)
  212. *
  213. * @return A possibly empty collection of subsystems.
  214. */
  215. @NonNull
  216. protected List<NamedFactory<Command>> configureSubsystems() {
  217. // SFTP.
  218. server.setFileSystemFactory(new VirtualFileSystemFactory() {
  219. @Override
  220. protected Path computeRootDir(Session session) throws IOException {
  221. return SshTestGitServer.this.repository.getDirectory()
  222. .getParentFile().getAbsoluteFile().toPath();
  223. }
  224. });
  225. return Collections
  226. .singletonList((new SftpSubsystemFactory.Builder()).build());
  227. }
  228. /**
  229. * Configures shell access for the test server. The default provides no
  230. * shell at all.
  231. */
  232. protected void configureShell() {
  233. // No shell
  234. server.setShellFactory(null);
  235. }
  236. /**
  237. * Adds an additional host key to the server.
  238. *
  239. * @param key
  240. * path to the private key file; should not be encrypted
  241. * @param inFront
  242. * whether to add the new key before other existing keys
  243. * @throws IOException
  244. * if the file denoted by the {@link Path} {@code key} cannot be
  245. * read
  246. * @throws GeneralSecurityException
  247. * if the key contained in the file cannot be read
  248. */
  249. public void addHostKey(@NonNull Path key, boolean inFront)
  250. throws IOException, GeneralSecurityException {
  251. try (InputStream in = Files.newInputStream(key)) {
  252. KeyPair pair = SecurityUtils.loadKeyPairIdentity(key.toString(), in,
  253. null);
  254. if (inFront) {
  255. hostKeys.add(0, pair);
  256. } else {
  257. hostKeys.add(pair);
  258. }
  259. }
  260. }
  261. /**
  262. * Starts the test server, listening on a random port.
  263. *
  264. * @return the port the server listens on; test clients should connect to
  265. * that port
  266. * @throws IOException
  267. */
  268. public int start() throws IOException {
  269. server.start();
  270. return server.getPort();
  271. }
  272. /**
  273. * Stops the test server.
  274. *
  275. * @throws IOException
  276. */
  277. public void stop() throws IOException {
  278. executorService.shutdownNow();
  279. server.stop(true);
  280. }
  281. public void setTestUserPublicKey(Path key)
  282. throws IOException, GeneralSecurityException {
  283. this.testKey = AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
  284. .resolvePublicKey(PublicKeyEntryResolver.IGNORING);
  285. }
  286. private class GitUploadPackCommand extends AbstractCommandSupport {
  287. protected GitUploadPackCommand(String command,
  288. ExecutorService executorService) {
  289. super(command, executorService, false);
  290. }
  291. @Override
  292. public void run() {
  293. UploadPack uploadPack = new UploadPack(repository);
  294. String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
  295. if (gitProtocol != null) {
  296. uploadPack
  297. .setExtraParameters(Collections.singleton(gitProtocol));
  298. }
  299. try {
  300. uploadPack.upload(getInputStream(), getOutputStream(),
  301. getErrorStream());
  302. onExit(0);
  303. } catch (IOException e) {
  304. log.warn(
  305. MessageFormat.format("Could not run {0}", getCommand()),
  306. e);
  307. onExit(-1, e.toString());
  308. }
  309. }
  310. }
  311. private class GitReceivePackCommand extends AbstractCommandSupport {
  312. protected GitReceivePackCommand(String command,
  313. ExecutorService executorService) {
  314. super(command, executorService, false);
  315. }
  316. @Override
  317. public void run() {
  318. try {
  319. new ReceivePack(repository).receive(getInputStream(),
  320. getOutputStream(), getErrorStream());
  321. onExit(0);
  322. } catch (IOException e) {
  323. log.warn(
  324. MessageFormat.format("Could not run {0}", getCommand()),
  325. e);
  326. onExit(-1, e.toString());
  327. }
  328. }
  329. }
  330. }