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

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