|
|
|
@ -20,7 +20,9 @@ import com.google.common.collect.Ordering; |
|
|
|
import com.google.common.collect.Streams; |
|
|
|
import lombok.Data; |
|
|
|
import lombok.Getter; |
|
|
|
import org.thingsboard.server.common.data.StringUtils; |
|
|
|
import lombok.RequiredArgsConstructor; |
|
|
|
import org.apache.commons.io.FileUtils; |
|
|
|
import org.apache.commons.lang3.StringUtils; |
|
|
|
import org.apache.sshd.common.util.security.SecurityUtils; |
|
|
|
import org.eclipse.jgit.api.CloneCommand; |
|
|
|
import org.eclipse.jgit.api.Git; |
|
|
|
@ -49,6 +51,7 @@ import org.eclipse.jgit.transport.CredentialsProvider; |
|
|
|
import org.eclipse.jgit.transport.FetchResult; |
|
|
|
import org.eclipse.jgit.transport.RefSpec; |
|
|
|
import org.eclipse.jgit.transport.SshTransport; |
|
|
|
import org.eclipse.jgit.transport.URIish; |
|
|
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; |
|
|
|
import org.eclipse.jgit.transport.sshd.JGitKeyCache; |
|
|
|
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; |
|
|
|
@ -61,8 +64,8 @@ import org.thingsboard.server.common.data.page.PageData; |
|
|
|
import org.thingsboard.server.common.data.page.PageLink; |
|
|
|
import org.thingsboard.server.common.data.page.SortOrder; |
|
|
|
import org.thingsboard.server.common.data.sync.vc.BranchInfo; |
|
|
|
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; |
|
|
|
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; |
|
|
|
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; |
|
|
|
|
|
|
|
import java.io.ByteArrayInputStream; |
|
|
|
import java.io.ByteArrayOutputStream; |
|
|
|
@ -70,6 +73,7 @@ import java.io.File; |
|
|
|
import java.io.IOException; |
|
|
|
import java.net.InetSocketAddress; |
|
|
|
import java.nio.charset.StandardCharsets; |
|
|
|
import java.nio.file.Files; |
|
|
|
import java.security.KeyPair; |
|
|
|
import java.security.PublicKey; |
|
|
|
import java.util.ArrayList; |
|
|
|
@ -78,70 +82,67 @@ import java.util.Comparator; |
|
|
|
import java.util.HashSet; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.UUID; |
|
|
|
import java.util.function.Function; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
public class GitRepository { |
|
|
|
|
|
|
|
private final Git git; |
|
|
|
private final AuthHandler authHandler; |
|
|
|
@Getter |
|
|
|
private final RepositorySettings settings; |
|
|
|
private final CredentialsProvider credentialsProvider; |
|
|
|
private final SshdSessionFactory sshSessionFactory; |
|
|
|
|
|
|
|
@Getter |
|
|
|
private final String directory; |
|
|
|
|
|
|
|
private ObjectId headId; |
|
|
|
|
|
|
|
private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { |
|
|
|
private GitRepository(Git git, RepositorySettings settings, AuthHandler authHandler, String directory) { |
|
|
|
this.git = git; |
|
|
|
this.settings = settings; |
|
|
|
this.credentialsProvider = credentialsProvider; |
|
|
|
this.sshSessionFactory = sshSessionFactory; |
|
|
|
this.authHandler = authHandler; |
|
|
|
this.directory = directory; |
|
|
|
} |
|
|
|
|
|
|
|
public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException { |
|
|
|
CredentialsProvider credentialsProvider = null; |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { |
|
|
|
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); |
|
|
|
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { |
|
|
|
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); |
|
|
|
} |
|
|
|
CloneCommand cloneCommand = Git.cloneRepository() |
|
|
|
.setURI(settings.getRepositoryUri()) |
|
|
|
.setDirectory(directory) |
|
|
|
.setNoCheckout(true); |
|
|
|
configureTransportCommand(cloneCommand, credentialsProvider, sshSessionFactory); |
|
|
|
AuthHandler authHandler = AuthHandler.createFor(settings, directory); |
|
|
|
authHandler.configureCommand(cloneCommand); |
|
|
|
Git git = cloneCommand.call(); |
|
|
|
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); |
|
|
|
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath()); |
|
|
|
} |
|
|
|
|
|
|
|
public static GitRepository open(File directory, RepositorySettings settings) throws IOException { |
|
|
|
Git git = Git.open(directory); |
|
|
|
CredentialsProvider credentialsProvider = null; |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { |
|
|
|
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); |
|
|
|
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { |
|
|
|
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); |
|
|
|
} |
|
|
|
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); |
|
|
|
AuthHandler authHandler = AuthHandler.createFor(settings, directory); |
|
|
|
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath()); |
|
|
|
} |
|
|
|
|
|
|
|
public static void test(RepositorySettings settings, File directory) throws GitAPIException { |
|
|
|
CredentialsProvider credentialsProvider = null; |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { |
|
|
|
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); |
|
|
|
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { |
|
|
|
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); |
|
|
|
public static void test(RepositorySettings settings, File directory) throws Exception { |
|
|
|
AuthHandler authHandler = AuthHandler.createFor(settings, directory); |
|
|
|
if (settings.isReadOnly()) { |
|
|
|
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri()); |
|
|
|
authHandler.configureCommand(lsRemoteCommand); |
|
|
|
lsRemoteCommand.call(); |
|
|
|
} else { |
|
|
|
Files.createDirectories(directory.toPath()); |
|
|
|
try { |
|
|
|
Git git = Git.init().setDirectory(directory).call(); |
|
|
|
GitRepository repository = new GitRepository(git, settings, authHandler, directory.getAbsolutePath()); |
|
|
|
repository.execute(repository.git.remoteAdd() |
|
|
|
.setName("origin") |
|
|
|
.setUri(new URIish(settings.getRepositoryUri()))); |
|
|
|
repository.push("", UUID.randomUUID().toString()); // trying to delete non-existing branch on remote repo
|
|
|
|
} finally { |
|
|
|
try { |
|
|
|
FileUtils.forceDelete(directory); |
|
|
|
} catch (Exception ignored) {} |
|
|
|
} |
|
|
|
} |
|
|
|
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri()); |
|
|
|
configureTransportCommand(lsRemoteCommand, credentialsProvider, sshSessionFactory); |
|
|
|
lsRemoteCommand.call(); |
|
|
|
} |
|
|
|
|
|
|
|
public void fetch() throws GitAPIException { |
|
|
|
@ -363,12 +364,12 @@ public class GitRepository { |
|
|
|
|
|
|
|
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException { |
|
|
|
if (command instanceof TransportCommand) { |
|
|
|
configureTransportCommand((TransportCommand) command, credentialsProvider, sshSessionFactory); |
|
|
|
authHandler.configureCommand((TransportCommand) command); |
|
|
|
} |
|
|
|
return command.call(); |
|
|
|
} |
|
|
|
|
|
|
|
private static Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> { |
|
|
|
private static final Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> { |
|
|
|
SortOrder sortOrder = pageLink.getSortOrder(); |
|
|
|
if (sortOrder != null |
|
|
|
&& sortOrder.getProperty().equals("timestamp") |
|
|
|
@ -405,59 +406,76 @@ public class GitRepository { |
|
|
|
return new PageData<>(data, totalPages, totalElements, hasNext); |
|
|
|
} |
|
|
|
|
|
|
|
private static void configureTransportCommand(TransportCommand transportCommand, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory) { |
|
|
|
if (credentialsProvider != null) { |
|
|
|
transportCommand.setCredentialsProvider(credentialsProvider); |
|
|
|
} |
|
|
|
if (sshSessionFactory != null) { |
|
|
|
transportCommand.setTransportConfigCallback(transport -> { |
|
|
|
if (transport instanceof SshTransport) { |
|
|
|
SshTransport sshTransport = (SshTransport) transport; |
|
|
|
sshTransport.setSshSessionFactory(sshSessionFactory); |
|
|
|
} |
|
|
|
}); |
|
|
|
@RequiredArgsConstructor |
|
|
|
private static class AuthHandler { |
|
|
|
private final CredentialsProvider credentialsProvider; |
|
|
|
private final SshdSessionFactory sshSessionFactory; |
|
|
|
|
|
|
|
protected static AuthHandler createFor(RepositorySettings settings, File directory) { |
|
|
|
CredentialsProvider credentialsProvider = null; |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { |
|
|
|
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); |
|
|
|
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { |
|
|
|
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); |
|
|
|
} |
|
|
|
return new AuthHandler(credentialsProvider, sshSessionFactory); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static CredentialsProvider newCredentialsProvider(String username, String password) { |
|
|
|
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password); |
|
|
|
} |
|
|
|
protected void configureCommand(TransportCommand command) { |
|
|
|
if (credentialsProvider != null) { |
|
|
|
command.setCredentialsProvider(credentialsProvider); |
|
|
|
} |
|
|
|
if (sshSessionFactory != null) { |
|
|
|
command.setTransportConfigCallback(transport -> { |
|
|
|
if (transport instanceof SshTransport) { |
|
|
|
SshTransport sshTransport = (SshTransport) transport; |
|
|
|
sshTransport.setSshSessionFactory(sshSessionFactory); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) { |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (StringUtils.isNotBlank(privateKey)) { |
|
|
|
Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password); |
|
|
|
sshSessionFactory = new SshdSessionFactoryBuilder() |
|
|
|
.setPreferredAuthentications("publickey") |
|
|
|
.setDefaultKeysProvider(file -> keyPairs) |
|
|
|
.setHomeDirectory(directory) |
|
|
|
.setSshDirectory(directory) |
|
|
|
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() { |
|
|
|
@Override |
|
|
|
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
private static CredentialsProvider newCredentialsProvider(String username, String password) { |
|
|
|
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
}) |
|
|
|
.build(new JGitKeyCache()); |
|
|
|
private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) { |
|
|
|
SshdSessionFactory sshSessionFactory = null; |
|
|
|
if (StringUtils.isNotBlank(privateKey)) { |
|
|
|
Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password); |
|
|
|
sshSessionFactory = new SshdSessionFactoryBuilder() |
|
|
|
.setPreferredAuthentications("publickey") |
|
|
|
.setDefaultKeysProvider(file -> keyPairs) |
|
|
|
.setHomeDirectory(directory) |
|
|
|
.setSshDirectory(directory) |
|
|
|
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() { |
|
|
|
@Override |
|
|
|
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
}) |
|
|
|
.build(new JGitKeyCache()); |
|
|
|
} |
|
|
|
return sshSessionFactory; |
|
|
|
} |
|
|
|
return sshSessionFactory; |
|
|
|
} |
|
|
|
|
|
|
|
private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) { |
|
|
|
Iterable<KeyPair> keyPairs = null; |
|
|
|
try { |
|
|
|
keyPairs = SecurityUtils.loadKeyPairIdentities(null, |
|
|
|
null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password); |
|
|
|
} catch (Exception e) {} |
|
|
|
if (keyPairs == null) { |
|
|
|
throw new IllegalArgumentException("Failed to load ssh private key"); |
|
|
|
private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) { |
|
|
|
Iterable<KeyPair> keyPairs = null; |
|
|
|
try { |
|
|
|
keyPairs = SecurityUtils.loadKeyPairIdentities(null, |
|
|
|
null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password); |
|
|
|
} catch (Exception e) {} |
|
|
|
if (keyPairs == null) { |
|
|
|
throw new IllegalArgumentException("Failed to load ssh private key"); |
|
|
|
} |
|
|
|
return keyPairs; |
|
|
|
} |
|
|
|
return keyPairs; |
|
|
|
} |
|
|
|
|
|
|
|
private static class NoMergesAndCommitMessageFilter extends RevFilter { |
|
|
|
|