![JAR search and dependency download from the Maven repository](/logo.png)
org.eclipse.jgit.junit.ssh.SshTestHarness Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.eclipse.jgit.junit.ssh Show documentation
Show all versions of org.eclipse.jgit.junit.ssh Show documentation
Utility classes to support Ssh based JUnit testing of JGit applications.
The newest version!
/*
* Copyright (C) 2018, 2020 Thomas Wolf and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.junit.ssh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.junit.After;
/**
* Root class for ssh tests. Sets up the ssh test server. A set of pre-computed
* keys for testing is provided in the bundle and can be used in test cases via
* {@link #copyTestResource(String, File)}. These test key files names have four
* components, separated by a single underscore: "id", the algorithm, the bits
* (if variable), and the password if the private key is encrypted. For instance
* "{@code id_ecdsa_384_testpass}" is an encrypted ECDSA-384 key. The passphrase
* to decrypt is "testpass". The key "{@code id_ecdsa_384}" is the same but
* unencrypted. All keys were generated and encrypted via ssh-keygen. Note that
* DSA and ec25519 have no "bits" component. Available keys are listed in
* {@link SshTestBase#KEY_RESOURCES}.
*/
public abstract class SshTestHarness extends RepositoryTestCase {
protected static final String TEST_USER = "testuser";
protected File sshDir;
protected File privateKey1;
protected File privateKey2;
protected File publicKey1;
/**
* @since 5.10
*/
protected File publicKey2;
protected SshTestGitServer server;
private SshSessionFactory factory;
protected int testPort;
protected File knownHosts;
@Override
public void setUp() throws Exception {
super.setUp();
writeTrashFile("file.txt", "something");
try (Git git = new Git(db)) {
git.add().addFilepattern("file.txt").call();
git.commit().setMessage("Initial commit").call();
}
// The home directory is mocked here
sshDir = new File(FS.DETECTED.userHome(), ".ssh");
assertTrue(sshDir.mkdir());
File serverDir = new File(getTemporaryDirectory(), "srv");
assertTrue(serverDir.mkdir());
// Create two key pairs. Let's not call them "id_rsa".
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
privateKey1 = new File(sshDir, "first_key");
privateKey2 = new File(sshDir, "second_key");
publicKey1 = createKeyPair(generator.generateKeyPair(), privateKey1);
publicKey2 = createKeyPair(generator.generateKeyPair(), privateKey2);
// Create a host key
KeyPair hostKey = generator.generateKeyPair();
// Start a server with our test user and the first key.
server = new SshTestGitServer(TEST_USER, publicKey1.toPath(), db,
hostKey);
testPort = server.start();
assertTrue(testPort > 0);
knownHosts = new File(sshDir, "known_hosts");
StringBuilder knownHostsLine = new StringBuilder();
knownHostsLine.append("[localhost]:").append(testPort).append(' ');
PublicKeyEntry.appendPublicKeyEntry(knownHostsLine,
hostKey.getPublic());
Files.write(knownHosts.toPath(),
Collections.singleton(knownHostsLine.toString()));
factory = createSessionFactory();
SshSessionFactory.setInstance(factory);
}
private static File createKeyPair(KeyPair newKey, File privateKeyFile)
throws Exception {
// Write PKCS#8 PEM unencrypted. Both JSch and sshd can read that.
PrivateKey privateKey = newKey.getPrivate();
String format = privateKey.getFormat();
if (!"PKCS#8".equalsIgnoreCase(format)) {
throw new IOException("Cannot write " + privateKey.getAlgorithm()
+ " key in " + format + " format");
}
try (BufferedWriter writer = Files.newBufferedWriter(
privateKeyFile.toPath(), StandardCharsets.US_ASCII)) {
writer.write("-----BEGIN PRIVATE KEY-----");
writer.newLine();
write(writer, privateKey.getEncoded(), 64);
writer.write("-----END PRIVATE KEY-----");
writer.newLine();
}
File publicKeyFile = new File(privateKeyFile.getParentFile(),
privateKeyFile.getName() + ".pub");
StringBuilder builder = new StringBuilder();
PublicKeyEntry.appendPublicKeyEntry(builder, newKey.getPublic());
builder.append(' ').append(TEST_USER);
try (OutputStream out = new FileOutputStream(publicKeyFile)) {
out.write(builder.toString().getBytes(StandardCharsets.US_ASCII));
}
return publicKeyFile;
}
private static void write(BufferedWriter out, byte[] bytes, int lineLength)
throws IOException {
String data = Base64.getEncoder().encodeToString(bytes);
int last = data.length();
for (int i = 0; i < last; i += lineLength) {
if (i + lineLength <= last) {
out.write(data.substring(i, i + lineLength));
} else {
out.write(data.substring(i));
}
out.newLine();
}
Arrays.fill(bytes, (byte) 0);
}
/**
* Creates a new known_hosts file with one entry for the given host and port
* taken from the given public key file.
*
* @param file
* to write the known_hosts file to
* @param host
* for the entry
* @param port
* for the entry
* @param publicKey
* to use
* @return the public-key part of the line
* @throws IOException
* if an IO error occurred
*/
protected static String createKnownHostsFile(File file, String host,
int port, File publicKey) throws IOException {
List lines = Files.readAllLines(publicKey.toPath(),
StandardCharsets.UTF_8);
assertEquals("Public key has too many lines", 1, lines.size());
String pubKey = lines.get(0);
// Strip off the comment.
String[] parts = pubKey.split("\\s+");
assertTrue("Unexpected key content",
parts.length == 2 || parts.length == 3);
String keyPart = parts[0] + ' ' + parts[1];
String line = '[' + host + "]:" + port + ' ' + keyPart;
Files.write(file.toPath(), Collections.singletonList(line));
return keyPart;
}
/**
* Checks whether there is a line for the given host and port that also
* matches the given key part in the list of lines.
*
* @param host
* to look for
* @param port
* to look for
* @param keyPart
* to look for
* @param lines
* to look in
* @return {@code true} if found, {@code false} otherwise
*/
protected boolean hasHostKey(String host, int port, String keyPart,
List lines) {
String h = '[' + host + "]:" + port;
return lines.stream()
.anyMatch(l -> l.contains(h) && l.contains(keyPart));
}
@After
public void shutdownServer() throws Exception {
if (server != null) {
server.stop();
server = null;
}
SshSessionFactory.setInstance(null);
factory = null;
}
protected abstract SshSessionFactory createSessionFactory();
protected SshSessionFactory getSessionFactory() {
return factory;
}
protected abstract void installConfig(String... config);
/**
* Copies a test data file contained in the test bundle to the given file.
* Equivalent to {@link #copyTestResource(Class, String, File)} with
* {@code SshTestHarness.class} as first parameter.
*
* @param resourceName
* of the test resource to copy
* @param to
* file to copy the resource to
* @throws IOException
* if the resource cannot be copied
*/
protected void copyTestResource(String resourceName, File to)
throws IOException {
copyTestResource(SshTestHarness.class, resourceName, to);
}
/**
* Copies a test data file contained in the test bundle to the given file,
* using {@link Class#getResourceAsStream(String)} to get the test resource.
*
* @param loader
* {@link Class} to use to load the resource
* @param resourceName
* of the test resource to copy
* @param to
* file to copy the resource to
* @throws IOException
* if the resource cannot be copied
*/
protected void copyTestResource(Class> loader, String resourceName,
File to) throws IOException {
try (InputStream in = loader.getResourceAsStream(resourceName)) {
Files.copy(in, to.toPath());
}
}
protected File cloneWith(String uri, File to, CredentialsProvider provider,
String... config) throws Exception {
installConfig(config);
CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true)
.setDirectory(to).setURI(uri);
if (provider != null) {
clone.setCredentialsProvider(provider);
}
try (Git git = clone.call()) {
Repository repo = git.getRepository();
assertNotNull(repo.resolve("master"));
assertNotEquals(db.getWorkTree(),
git.getRepository().getWorkTree());
assertTrue(new File(git.getRepository().getWorkTree(), "file.txt")
.exists());
return repo.getWorkTree();
}
}
protected void pushTo(File localClone) throws Exception {
pushTo(null, localClone);
}
protected void pushTo(CredentialsProvider provider, File localClone)
throws Exception {
RevCommit commit;
File newFile = null;
try (Git git = Git.open(localClone)) {
// Write a new file and modify a file.
Repository local = git.getRepository();
newFile = File.createTempFile("new", "sshtest",
local.getWorkTree());
write(newFile, "something new");
File existingFile = new File(local.getWorkTree(), "file.txt");
write(existingFile, "something else");
git.add().addFilepattern("file.txt")
.addFilepattern(newFile.getName())
.call();
commit = git.commit().setMessage("Local commit").call();
// Push
PushCommand push = git.push().setPushAll();
if (provider != null) {
push.setCredentialsProvider(provider);
}
Iterable results = push.call();
for (PushResult result : results) {
for (RemoteRefUpdate u : result.getRemoteUpdates()) {
assertEquals(
"Could not update " + u.getRemoteName() + ' '
+ u.getMessage(),
RemoteRefUpdate.Status.OK, u.getStatus());
}
}
}
// Now check "master" in the remote repo directly:
assertEquals("Unexpected remote commit", commit, db.resolve("master"));
assertEquals("Unexpected remote commit", commit,
db.resolve(Constants.HEAD));
File remoteFile = new File(db.getWorkTree(), newFile.getName());
assertFalse("File should not exist on remote", remoteFile.exists());
try (Git git = new Git(db)) {
git.reset().setMode(ResetType.HARD).setRef(Constants.HEAD).call();
}
assertTrue("File does not exist on remote", remoteFile.exists());
checkFile(remoteFile, "something new");
}
protected static class TestCredentialsProvider extends CredentialsProvider {
private final List stringStore;
private final Iterator strings;
public TestCredentialsProvider(String... strings) {
if (strings == null || strings.length == 0) {
stringStore = Collections.emptyList();
} else {
stringStore = Arrays.asList(strings);
}
this.strings = stringStore.iterator();
}
@Override
public boolean isInteractive() {
return true;
}
@Override
public boolean supports(CredentialItem... items) {
return true;
}
@Override
public boolean get(URIish uri, CredentialItem... items)
throws UnsupportedCredentialItem {
System.out.println("URI: " + uri);
for (CredentialItem item : items) {
System.out.println(item.getClass().getSimpleName() + ' '
+ item.getPromptText());
}
logItems(uri, items);
for (CredentialItem item : items) {
if (item instanceof CredentialItem.InformationalMessage) {
continue;
}
if (item instanceof CredentialItem.YesNoType) {
((CredentialItem.YesNoType) item).setValue(true);
} else if (item instanceof CredentialItem.CharArrayType) {
if (strings.hasNext()) {
((CredentialItem.CharArrayType) item)
.setValue(strings.next().toCharArray());
} else {
return false;
}
} else if (item instanceof CredentialItem.StringType) {
if (strings.hasNext()) {
((CredentialItem.StringType) item)
.setValue(strings.next());
} else {
return false;
}
} else {
return false;
}
}
return true;
}
private List log = new ArrayList<>();
private void logItems(URIish uri, CredentialItem... items) {
log.add(new LogEntry(uri, Arrays.asList(items)));
}
public List getLog() {
return log;
}
}
protected static class LogEntry {
private URIish uri;
private List items;
public LogEntry(URIish uri, List items) {
this.uri = uri;
this.items = items;
}
public URIish getURIish() {
return uri;
}
public List getItems() {
return items;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy