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