1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.mirror;
18
19 import static com.linecorp.centraldogma.server.internal.mirror.credential.PublicKeyMirrorCredential.publicKeyPreview;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.net.URI;
24 import java.net.URISyntaxException;
25 import java.security.GeneralSecurityException;
26 import java.security.KeyPair;
27 import java.util.Collection;
28
29 import javax.annotation.Nullable;
30
31 import org.apache.sshd.client.ClientBuilder;
32 import org.apache.sshd.client.SshClient;
33 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
34 import org.apache.sshd.client.session.ClientSession;
35 import org.apache.sshd.common.NamedResource;
36 import org.apache.sshd.common.config.keys.FilePasswordProvider;
37 import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
38 import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser;
39 import org.apache.sshd.common.config.keys.loader.pem.PKCS8PEMResourceKeyPairParser;
40 import org.apache.sshd.common.file.nonefs.NoneFileSystemFactory;
41 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
42 import org.apache.sshd.common.util.security.SecurityUtils;
43 import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleRandom;
44 import org.apache.sshd.git.GitModuleProperties;
45 import org.apache.sshd.git.transport.GitSshdSessionFactory;
46 import org.eclipse.jgit.api.TransportCommand;
47 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
48 import org.eclipse.jgit.transport.CredentialItem;
49 import org.eclipse.jgit.transport.CredentialsProvider;
50 import org.eclipse.jgit.transport.SshTransport;
51 import org.eclipse.jgit.transport.URIish;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.cronutils.model.Cron;
56 import com.google.common.annotations.VisibleForTesting;
57
58 import com.linecorp.centraldogma.server.MirrorException;
59 import com.linecorp.centraldogma.server.command.CommandExecutor;
60 import com.linecorp.centraldogma.server.internal.mirror.credential.PasswordMirrorCredential;
61 import com.linecorp.centraldogma.server.internal.mirror.credential.PublicKeyMirrorCredential;
62 import com.linecorp.centraldogma.server.mirror.MirrorCredential;
63 import com.linecorp.centraldogma.server.mirror.MirrorDirection;
64 import com.linecorp.centraldogma.server.storage.repository.Repository;
65
66 final class SshGitMirror extends AbstractGitMirror {
67
68 private static final Logger logger = LoggerFactory.getLogger(SshGitMirror.class);
69
70 private static final KeyPairResourceParser keyPairResourceParser = KeyPairResourceParser.aggregate(
71
72 SecurityUtils.getBouncycastleKeyPairResourceParser(),
73 PKCS8PEMResourceKeyPairParser.INSTANCE,
74 OpenSSHKeyPairResourceParser.INSTANCE);
75
76
77
78
79
80 private static final BouncyCastleRandom bounceCastleRandom = new BouncyCastleRandom();
81
82 SshGitMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential,
83 Repository localRepo, String localPath,
84 URI remoteRepoUri, String remotePath, @Nullable String remoteBranch,
85 @Nullable String gitignore) {
86 super(schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, remoteBranch,
87 gitignore);
88 }
89
90 @Override
91 protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception {
92 final URIish remoteUri = remoteUri();
93 try (SshClient sshClient = createSshClient();
94 ClientSession session = createSession(sshClient, remoteUri)) {
95 final DefaultGitSshdSessionFactory sessionFactory =
96 new DefaultGitSshdSessionFactory(sshClient, session);
97 try (GitWithAuth git = openGit(workDir, remoteUri, sessionFactory::configureCommand)) {
98 mirrorLocalToRemote(git, maxNumFiles, maxNumBytes);
99 }
100 }
101 }
102
103 @Override
104 protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor,
105 int maxNumFiles, long maxNumBytes) throws Exception {
106 final URIish remoteUri = remoteUri();
107 try (SshClient sshClient = createSshClient();
108 ClientSession session = createSession(sshClient, remoteUri)) {
109 final DefaultGitSshdSessionFactory sessionFactory =
110 new DefaultGitSshdSessionFactory(sshClient, session);
111 try (GitWithAuth git = openGit(workDir, remoteUri, sessionFactory::configureCommand)) {
112 mirrorRemoteToLocal(git, executor, maxNumFiles, maxNumBytes);
113 }
114 }
115 }
116
117 private URIish remoteUri() throws URISyntaxException {
118
119 final String username;
120 if (credential() instanceof PasswordMirrorCredential) {
121 username = ((PasswordMirrorCredential) credential()).username();
122 } else if (credential() instanceof PublicKeyMirrorCredential) {
123 username = ((PublicKeyMirrorCredential) credential()).username();
124 } else {
125 username = null;
126 }
127
128 assert !remoteRepoUri().getRawAuthority().contains("@") : remoteRepoUri().getRawAuthority();
129 final String jGitUri;
130 if (username != null) {
131 jGitUri = "ssh://" + username + '@' + remoteRepoUri().getRawAuthority() +
132 remoteRepoUri().getRawPath();
133 } else {
134 jGitUri = "ssh://" + remoteRepoUri().getRawAuthority() + remoteRepoUri().getRawPath();
135 }
136 return new URIish(jGitUri);
137 }
138
139 private SshClient createSshClient() {
140 final ClientBuilder builder = ClientBuilder.builder();
141
142 builder.hostConfigEntryResolver(HostConfigEntryResolver.EMPTY);
143 builder.fileSystemFactory(NoneFileSystemFactory.INSTANCE);
144
145 builder.serverKeyVerifier((clientSession, remoteAddress, serverKey) -> true);
146 builder.randomFactory(() -> bounceCastleRandom);
147 final SshClient client = builder.build();
148 try {
149 configureCredential(client);
150 client.start();
151 return client;
152 } catch (Throwable t) {
153 client.stop();
154 throw t;
155 }
156 }
157
158 @VisibleForTesting
159 static ClientSession createSession(SshClient sshClient, URIish uri) {
160 int port = uri.getPort();
161 if (port <= 0) {
162 port = 22;
163 }
164 logger.trace("Connecting to {}:{}", uri.getHost(), port);
165 ClientSession session = null;
166 try {
167 session = sshClient.connect(uri.getUser(), uri.getHost(), port)
168 .verify(GitModuleProperties.CONNECT_TIMEOUT.getRequired(
169 sshClient))
170 .getSession();
171 session.auth().verify(GitModuleProperties.AUTH_TIMEOUT.getRequired(session));
172 logger.trace("The session established: {}", session);
173 return session;
174 } catch (Throwable t) {
175 if (session != null) {
176 session.close(true);
177 }
178 throw new RuntimeException("failed to create a session for " + uri + " from " + sshClient, t);
179 }
180 }
181
182 private void configureCredential(SshClient client) {
183 final MirrorCredential c = credential();
184 if (c instanceof PasswordMirrorCredential) {
185 client.setFilePasswordProvider(passwordProvider(((PasswordMirrorCredential) c).password()));
186 } else if (c instanceof PublicKeyMirrorCredential) {
187 final PublicKeyMirrorCredential cred = (PublicKeyMirrorCredential) credential();
188 final Collection<KeyPair> keyPairs;
189 try {
190 keyPairs = keyPairResourceParser.loadKeyPairs(null, NamedResource.ofName(cred.username()),
191 passwordProvider(cred.passphrase()),
192 cred.privateKey());
193 client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPairs));
194 } catch (IOException | GeneralSecurityException e) {
195 throw new MirrorException("Unexpected exception while loading private key. username: " +
196 cred.username() + ", publicKey: " +
197 publicKeyPreview(cred.publicKey()), e);
198 }
199 }
200 }
201
202 private static FilePasswordProvider passwordProvider(@Nullable String passphrase) {
203 if (passphrase == null) {
204 return FilePasswordProvider.EMPTY;
205 }
206
207 return FilePasswordProvider.of(passphrase);
208 }
209
210 private static final class DefaultGitSshdSessionFactory extends GitSshdSessionFactory {
211 DefaultGitSshdSessionFactory(SshClient client, ClientSession session) {
212
213 super(client, session);
214 }
215
216 private void configureCommand(TransportCommand<?, ?> command) {
217
218 command.setCredentialsProvider(NoopCredentialsProvider.INSTANCE);
219 command.setTransportConfigCallback(transport -> {
220 final SshTransport sshTransport = (SshTransport) transport;
221 sshTransport.setSshSessionFactory(this);
222 });
223 }
224 }
225
226 static final class NoopCredentialsProvider extends CredentialsProvider {
227
228 static final CredentialsProvider INSTANCE = new NoopCredentialsProvider();
229
230 @Override
231 public boolean isInteractive() {
232 return true;
233 }
234
235 @Override
236 public boolean supports(CredentialItem... items) {
237 return false;
238 }
239
240 @Override
241 public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
242 return false;
243 }
244 }
245 }