TW-41191 Add possibility to use custom ssl trust store on server side
authorMikhail Khorkov <mikhail.khorkov@jetbrains.com>
Fri, 16 Mar 2018 11:25:32 +0000 (18:25 +0700)
committerMikhail Khorkov <mikhail.khorkov@jetbrains.com>
Mon, 19 Mar 2018 10:57:35 +0000 (17:57 +0700)
16 files changed:
git-common/src/jetbrains/buildServer/buildTriggers/vcs/git/Constants.java
git-server-tc/src/META-INF/build-server-plugin-git-tc.xml
git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderBuildServer.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/FetchCommandImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/Fetcher.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProvider.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderStatic.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitVcsSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SNIHttpClientConnection.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SNIHttpClientConnectionFactory.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnection.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnectionFactory.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/TeamCityJDKHttpConnectionFactory.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/TransportFactoryImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/patch/GitPatchBuilderDispatcher.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/patch/GitPatchProcess.java

index 15265b5228e05a50ed46f711ee844ed6669a7a33..b13ef40286a69b7fcf2e727b13dd2dc210168fb1 100644 (file)
@@ -109,6 +109,8 @@ public interface Constants {
   //path to internal properties to use in Fetcher
   public static final String FETCHER_INTERNAL_PROPERTIES_FILE = "fetcherInternalPropertiesFile";
 
+  public static final String GIT_TRUST_STORE_PROVIDER = "gitTrustStoreProvider";
+
   /**
    * A prefix for build parameter with vcs branch name of git root
    */
index 7ad8f8f9fdfb8e2d394835dd924dd4f95bb8e6ad..6beae6ac2375002c0bd078e4cb6850e87f8e6a7a 100644 (file)
@@ -13,4 +13,5 @@
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitGcErrorsHealthReport"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitGcErrorsHealthPage"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GitExternalChangeViewerExtension"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GitTrustStoreProviderBuildServer"/>
 </beans>
\ No newline at end of file
diff --git a/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderBuildServer.java b/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderBuildServer.java
new file mode 100644 (file)
index 0000000..8aa2ce4
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.git;
+
+import jetbrains.buildServer.serverSide.ServerPaths;
+import jetbrains.buildServer.serverSide.TrustedCertificatesDirectory;
+import jetbrains.buildServer.util.ssl.SSLTrustStoreProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.security.KeyStore;
+
+/**
+ * Implementation of {@link GitTrustStoreProvider} for BuildServer.
+ *
+ * @author Mikhail Khorkov
+ * @since tc-2018.1
+ */
+public class GitTrustStoreProviderBuildServer implements GitTrustStoreProvider {
+
+  @NotNull
+  private final SSLTrustStoreProvider mySSLTrustStoreProvider;
+
+  @NotNull
+  private final ServerPaths myServerPaths;
+
+  public GitTrustStoreProviderBuildServer(@NotNull final SSLTrustStoreProvider sslTrustStoreProvider,
+                                          @NotNull final ServerPaths serverPaths) {
+    mySSLTrustStoreProvider = sslTrustStoreProvider;
+    myServerPaths = serverPaths;
+  }
+
+  @Nullable
+  @Override
+  public KeyStore getTrustStore() {
+    return mySSLTrustStoreProvider.getTrustStore();
+  }
+
+  @NotNull
+  @Override
+  public String serialize() {
+    return TrustedCertificatesDirectory
+      .getCertificateDirectoryForProject(myServerPaths.getProjectsDir().getPath(), TrustedCertificatesDirectory.ROOT_PROJECT_ID);
+  }
+
+  @NotNull
+  @Override
+  public GitTrustStoreProvider deserialize(@NotNull final String serialized) {
+    return this;
+  }
+}
index 0d453c1c79e50c11b57f70afdba0d50a305d70b2..99af86289d636b5e43b614d26d057213dc0b2eba 100644 (file)
@@ -57,18 +57,27 @@ public class FetchCommandImpl implements FetchCommand {
   private final TransportFactory myTransportFactory;
   private final FetcherProperties myFetcherProperties;
   private final VcsRootSshKeyManager mySshKeyManager;
+  private final GitTrustStoreProvider myGitTrustStoreProvider;
 
   public FetchCommandImpl(@NotNull ServerPluginConfig config,
                           @NotNull TransportFactory transportFactory,
                           @NotNull FetcherProperties fetcherProperties,
                           @NotNull VcsRootSshKeyManager sshKeyManager) {
+    this(config, transportFactory, fetcherProperties, sshKeyManager, new GitTrustStoreProviderStatic(null));
+  }
+
+  public FetchCommandImpl(@NotNull ServerPluginConfig config,
+                          @NotNull TransportFactory transportFactory,
+                          @NotNull FetcherProperties fetcherProperties,
+                          @NotNull VcsRootSshKeyManager sshKeyManager,
+                          @NotNull GitTrustStoreProvider gitTrustStoreProvider) {
     myConfig = config;
     myTransportFactory = transportFactory;
     myFetcherProperties = fetcherProperties;
     mySshKeyManager = sshKeyManager;
+    myGitTrustStoreProvider = gitTrustStoreProvider;
   }
 
-
   public void fetch(@NotNull Repository db,
                     @NotNull URIish fetchURI,
                     @NotNull Collection<RefSpec> refspecs,
@@ -382,6 +391,7 @@ public class FetchCommandImpl implements FetchCommand {
       properties.put(Constants.VCS_DEBUG_ENABLED, String.valueOf(Loggers.VCS.isDebugEnabled()));
       properties.put(Constants.THREAD_DUMP_FILE, threadDump.getAbsolutePath());
       properties.put(Constants.FETCHER_INTERNAL_PROPERTIES_FILE, gitProperties.getAbsolutePath());
+      properties.put(Constants.GIT_TRUST_STORE_PROVIDER, myGitTrustStoreProvider.serialize());
       return VcsUtil.propertiesToStringSecure(properties).getBytes("UTF-8");
     } catch (IOException e) {
       throw new VcsException("Error while generating fetch process input", e);
index f35f23426d59af7bdfb0b75c0cc071dc6a61cce2..214ccf0e46e5ed5f74bac9d7dc5cc88691614cd2 100644 (file)
@@ -92,12 +92,14 @@ public class Fetcher {
                             @NotNull ProgressMonitor progressMonitor) throws IOException, VcsException, URISyntaxException {
     final String fetchUrl = vcsRootProperties.get(Constants.FETCH_URL);
     final String refspecs = vcsRootProperties.get(Constants.REFSPEC);
+    final String trustedCertificatesDir = vcsRootProperties.get(Constants.GIT_TRUST_STORE_PROVIDER);
     AuthSettings auth = new AuthSettings(vcsRootProperties);
     PluginConfigImpl config = new PluginConfigImpl();
 
     GitServerUtil.configureStreamFileThreshold(Integer.MAX_VALUE);
 
-    TransportFactory transportFactory = new TransportFactoryImpl(config, new EmptyVcsRootSshKeyManager());
+    TransportFactory transportFactory = new TransportFactoryImpl(config, new EmptyVcsRootSshKeyManager(),
+                                                                 new GitTrustStoreProviderStatic(trustedCertificatesDir));
     Transport tn = null;
     try {
       Repository repository = new RepositoryBuilder().setBare().setGitDir(repositoryDir).build();
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProvider.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProvider.java
new file mode 100644 (file)
index 0000000..29b6fd5
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.git;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.security.KeyStore;
+
+/**
+ * Abstract provider of trust store for ssl connections.
+ *
+ * @author Mikhail Khorkov
+ * @since tc-2018.1
+ */
+public interface GitTrustStoreProvider {
+
+  /**
+   * Returns trust store or <code>null</code>.
+   */
+  @Nullable
+  KeyStore getTrustStore();
+
+  /**
+   * Serialize provider for sending to separate process.
+   */
+  @NotNull
+  String serialize();
+
+  /**
+   * Deserialize provider.
+   * @param serialized result of {@link #serialize()} method
+   */
+  @NotNull
+  GitTrustStoreProvider deserialize(@NotNull String serialized);
+}
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderStatic.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitTrustStoreProviderStatic.java
new file mode 100644 (file)
index 0000000..64cf7a7
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.git;
+
+import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.util.ssl.TrustStoreReader;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.security.KeyStore;
+
+/**
+ * Implementation of {@link GitTrustStoreProvider} for static folder.
+ *
+ * @author Mikhail Khorkov
+ * @since tc-2018.1
+ */
+public class GitTrustStoreProviderStatic implements GitTrustStoreProvider {
+
+  @Nullable
+  private String myTrustedCertificatesDir;
+
+  public GitTrustStoreProviderStatic(@Nullable final String trustedCertificatesDir) {
+    myTrustedCertificatesDir = trustedCertificatesDir;
+  }
+
+  @Nullable
+  @Override
+  public KeyStore getTrustStore() {
+    if (myTrustedCertificatesDir == null) {
+      return null;
+    } else {
+      return TrustStoreReader.readTrustStoreFromDirectory(myTrustedCertificatesDir);
+    }
+  }
+
+  @NotNull
+  @Override
+  public String serialize() {
+    return myTrustedCertificatesDir != null ? myTrustedCertificatesDir : "";
+  }
+
+  @NotNull
+  @Override
+  public GitTrustStoreProvider deserialize(@NotNull final String serialized) {
+    if (StringUtil.isEmptyOrSpaces(serialized)) {
+      myTrustedCertificatesDir = null;
+    } else {
+      myTrustedCertificatesDir = serialized;
+    }
+    return this;
+  }
+}
index 79cce0851517c0b1094af137c34b7757ab8db7f7..97a9fa0e21caaea90193499f545c861b9b3baff6 100755 (executable)
@@ -68,6 +68,7 @@ public class GitVcsSupport extends ServerVcsSupport
   private final CommitLoader myCommitLoader;
   private final VcsRootSshKeyManager mySshKeyManager;
   private final VcsOperationProgressProvider myProgressProvider;
+  private final GitTrustStoreProvider myGitTrustStoreProvider;
   private Collection<GitServerExtension> myExtensions = new ArrayList<GitServerExtension>();
 
   public GitVcsSupport(@NotNull ServerPluginConfig config,
@@ -80,6 +81,21 @@ public class GitVcsSupport extends ServerVcsSupport
                        @NotNull VcsOperationProgressProvider progressProvider,
                        @NotNull GitResetCacheHandler resetCacheHandler,
                        @NotNull ResetRevisionsCacheHandler resetRevisionsCacheHandler) {
+    this(config, resetCacheManager, transportFactory, repositoryManager, mapFullPath, commitLoader, sshKeyManager, progressProvider,
+         resetCacheHandler, resetRevisionsCacheHandler, new GitTrustStoreProviderStatic(null));
+  }
+
+  public GitVcsSupport(@NotNull ServerPluginConfig config,
+                       @NotNull ResetCacheRegister resetCacheManager,
+                       @NotNull TransportFactory transportFactory,
+                       @NotNull RepositoryManager repositoryManager,
+                       @NotNull GitMapFullPath mapFullPath,
+                       @NotNull CommitLoader commitLoader,
+                       @NotNull VcsRootSshKeyManager sshKeyManager,
+                       @NotNull VcsOperationProgressProvider progressProvider,
+                       @NotNull GitResetCacheHandler resetCacheHandler,
+                       @NotNull ResetRevisionsCacheHandler resetRevisionsCacheHandler,
+                       @NotNull GitTrustStoreProvider gitTrustStoreProvider) {
     myConfig = config;
     myTransportFactory = transportFactory;
     myRepositoryManager = repositoryManager;
@@ -90,6 +106,7 @@ public class GitVcsSupport extends ServerVcsSupport
     setStreamFileThreshold();
     resetCacheManager.registerHandler(resetCacheHandler);
     resetCacheManager.registerHandler(resetRevisionsCacheHandler);
+    myGitTrustStoreProvider = gitTrustStoreProvider;
   }
 
   public void setExtensionHolder(@Nullable ExtensionHolder extensionHolder) {
@@ -184,7 +201,9 @@ public class GitVcsSupport extends ServerVcsSupport
     logBuildPatch(root, fromRevision, toRevision);
     GitVcsRoot gitRoot = context.getGitRoot();
     myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
-      GitPatchBuilderDispatcher gitPatchBuilder = new GitPatchBuilderDispatcher(myConfig, mySshKeyManager, context, builder, fromRevision, toRevision, checkoutRules);
+      GitPatchBuilderDispatcher gitPatchBuilder = new GitPatchBuilderDispatcher(myConfig, mySshKeyManager, context, builder, fromRevision,
+                                                                                toRevision, checkoutRules,
+                                                                                myGitTrustStoreProvider.serialize());
       try {
         myCommitLoader.loadCommit(context, gitRoot, toRevision);
         gitPatchBuilder.buildPatch();
index 1e860448b754d58f4313fe3384a7146479c84da1..bc27c50819c88aa27e1e2bbbfbbf00f4892343be 100644 (file)
@@ -18,6 +18,7 @@ package jetbrains.buildServer.buildTriggers.vcs.git;
 
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.text.StringUtil;
+import jetbrains.buildServer.util.ssl.SSLContextUtil;
 import org.apache.http.*;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.HttpClient;
@@ -39,6 +40,7 @@ import org.eclipse.jgit.transport.http.apache.TemporaryBufferEntity;
 import org.eclipse.jgit.transport.http.apache.internal.HttpApacheText;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
+import org.jetbrains.annotations.NotNull;
 
 import javax.net.ssl.*;
 import java.io.IOException;
@@ -48,6 +50,7 @@ import java.lang.reflect.Method;
 import java.net.*;
 import java.net.ProtocolException;
 import java.security.KeyManagementException;
+import java.security.KeyStore;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
@@ -55,6 +58,7 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 
 /**
  * Same as org.eclipse.jgit.transport.http.apache.HttpClientConnection, but
@@ -89,6 +93,8 @@ public class SNIHttpClientConnection implements HttpConnection {
 
   SSLContext ctx;
 
+  private Supplier<KeyStore> trustStoreGetter = () -> null;
+
   private Map<String, Object> attributes = new HashMap<String, Object>();
 
   private HttpClient getClient() {
@@ -119,16 +125,16 @@ public class SNIHttpClientConnection implements HttpConnection {
   }
 
   private SSLContext getSSLContext() {
+    KeyStore trustStore = trustStoreGetter.get();
+    if (trustStore != null) {
+      ctx = SSLContextUtil.createUserSSLContext(trustStore);
+    }
     if (ctx == null) {
       try {
         ctx = SSLContext.getInstance("TLS"); //$NON-NLS-1$
         ctx.init(null, null, null);
-      } catch (NoSuchAlgorithmException e) {
-        throw new IllegalStateException(
-          HttpApacheText.get().unexpectedSSLContextException, e);
-      } catch (KeyManagementException e) {
-        throw new IllegalStateException(
-          HttpApacheText.get().unexpectedSSLContextException, e);
+      } catch (NoSuchAlgorithmException | KeyManagementException e) {
+        throw new IllegalStateException(HttpApacheText.get().unexpectedSSLContextException, e);
       }
     }
     return ctx;
@@ -339,6 +345,10 @@ public class SNIHttpClientConnection implements HttpConnection {
     attributes.put(name, value);
   }
 
+  public void setTrustStoreGetter(@NotNull final Supplier<KeyStore> trustStoreGetter) {
+    this.trustStoreGetter = trustStoreGetter;
+  }
+
   private class ConnectionHttpContext implements HttpContext {
     public synchronized Object getAttribute(String s) {
       return attributes.get(s);
index 0bd5f469969709e0a2a6394ffd8a1b3fb80743ee..5a50696144fcd484d93ff2c41e4a9d0c96c60edc 100644 (file)
@@ -18,18 +18,39 @@ package jetbrains.buildServer.buildTriggers.vcs.git;
 
 import org.eclipse.jgit.transport.http.HttpConnection;
 import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 import java.net.Proxy;
 import java.net.URL;
+import java.security.KeyStore;
+import java.util.function.Supplier;
 
 public class SNIHttpClientConnectionFactory implements HttpConnectionFactory {
 
+  private Supplier<KeyStore> myTrustStoreGetter;
+
+  /**
+   * @deprecated use {@link #SNIHttpClientConnectionFactory(Supplier)} instead.
+   */
+  @Deprecated
+  public SNIHttpClientConnectionFactory() {
+    this(() -> null);
+  }
+
+  public SNIHttpClientConnectionFactory(@NotNull Supplier<KeyStore> trustStoreGetter) {
+    myTrustStoreGetter = trustStoreGetter;
+  }
+
   public HttpConnection create(final URL url) throws IOException {
-    return new SNIHttpClientConnection(url.toString());
+    SNIHttpClientConnection connection = new SNIHttpClientConnection(url.toString());
+    connection.setTrustStoreGetter(myTrustStoreGetter);
+    return connection;
   }
 
   public HttpConnection create(final URL url, final Proxy proxy) throws IOException {
-    return new SNIHttpClientConnection(url.toString(), proxy);
+    SNIHttpClientConnection connection = new SNIHttpClientConnection(url.toString(), proxy);
+    connection.setTrustStoreGetter(myTrustStoreGetter);
+    return connection;
   }
 }
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnection.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnection.java
new file mode 100644 (file)
index 0000000..fa1e1b5
--- /dev/null
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.git;
+
+import jetbrains.buildServer.util.ssl.SSLContextUtil;
+import org.apache.http.*;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.CoreConnectionPNames;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.eclipse.jgit.transport.http.apache.HttpClientConnection;
+import org.eclipse.jgit.transport.http.apache.TemporaryBufferEntity;
+import org.eclipse.jgit.transport.http.apache.internal.HttpApacheText;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.jetbrains.annotations.NotNull;
+
+import javax.net.ssl.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.*;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * {@link HttpClientConnection} with support of custom ssl trust store.
+ *
+ * It is almost copy-paste of {@link HttpClientConnection}.
+ *
+ * @author Mikhail Khorkov
+ * @since 2018.1
+ */
+public class SSLHttpClientConnection extends HttpClientConnection {
+
+  HttpClient client;
+
+  String urlStr;
+
+  HttpUriRequest req;
+
+  HttpResponse resp = null;
+
+  String method = "GET"; //$NON-NLS-1$
+
+  private TemporaryBufferEntity entity;
+
+  private boolean isUsingProxy = false;
+
+  private Proxy proxy;
+
+  private Integer timeout = null;
+
+  private Integer readTimeout;
+
+  private Boolean followRedirects;
+
+  private X509HostnameVerifier hostnameverifier;
+
+  SSLContext ctx;
+
+  private Map<String, Object> attributes = new HashMap<>();
+
+  @NotNull
+  private Supplier<KeyStore> myTrustStoreGetter = () -> null;
+
+  public SSLHttpClientConnection(final String urlStr) {
+    super(urlStr);
+  }
+
+  public SSLHttpClientConnection(final String urlStr, final Proxy proxy) {
+    super(urlStr, proxy);
+  }
+
+  public SSLHttpClientConnection(final String urlStr, final Proxy proxy, final HttpClient cl) {
+    super(urlStr, proxy, cl);
+  }
+
+  private HttpClient getClient() {
+    if (client == null)
+      client = new DefaultHttpClient();
+    HttpParams params = client.getParams();
+    if (proxy != null && !Proxy.NO_PROXY.equals(proxy)) {
+      isUsingProxy = true;
+      InetSocketAddress adr = (InetSocketAddress) proxy.address();
+      params.setParameter(ConnRoutePNames.DEFAULT_PROXY,
+                          new HttpHost(adr.getHostName(), adr.getPort()));
+    }
+    if (timeout != null)
+      params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
+    if (readTimeout != null)
+      params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, readTimeout);
+    if (followRedirects != null)
+      params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, followRedirects);
+    if (hostnameverifier != null) {
+      SSLSocketFactory sf;
+      sf = new SSLSocketFactory(getSSLContext(), hostnameverifier);
+      Scheme https = new Scheme("https", 443, sf); //$NON-NLS-1$
+      client.getConnectionManager().getSchemeRegistry().register(https);
+    }
+
+    return client;
+  }
+
+  private SSLContext getSSLContext() {
+    final KeyStore trustStore = myTrustStoreGetter.get();
+    if (trustStore != null) {
+      ctx = SSLContextUtil.createUserSSLContext(trustStore);
+    }
+    if (ctx == null) {
+      try {
+        ctx = SSLContext.getInstance("TLS"); //$NON-NLS-1$
+      } catch (NoSuchAlgorithmException e) {
+        throw new IllegalStateException(
+          HttpApacheText.get().unexpectedSSLContextException, e);
+      }
+    }
+    return ctx;
+  }
+
+  /**
+   * Sets the buffer from which to take the request body
+   *
+   * @param buffer
+   */
+  public void setBuffer(TemporaryBuffer buffer) {
+    this.entity = new TemporaryBufferEntity(buffer);
+  }
+
+  public int getResponseCode() throws IOException {
+    execute();
+    return resp.getStatusLine().getStatusCode();
+  }
+
+  public URL getURL() {
+    try {
+      return new URL(urlStr);
+    } catch (MalformedURLException e) {
+      return null;
+    }
+  }
+
+  public String getResponseMessage() throws IOException {
+    execute();
+    return resp.getStatusLine().getReasonPhrase();
+  }
+
+  private void execute() throws IOException, ClientProtocolException {
+    if (resp == null)
+      if (entity != null) {
+        if (req instanceof HttpEntityEnclosingRequest) {
+          HttpEntityEnclosingRequest eReq = (HttpEntityEnclosingRequest) req;
+          eReq.setEntity(entity);
+        }
+        resp = getClient().execute(req, new SSLHttpClientConnection.ConnectionHttpContext());
+        entity.getBuffer().close();
+        entity = null;
+      } else
+        resp = getClient().execute(req, new SSLHttpClientConnection.ConnectionHttpContext());
+  }
+
+  public Map<String, List<String>> getHeaderFields() {
+    Map<String, List<String>> ret = new HashMap<>();
+    for (Header hdr : resp.getAllHeaders()) {
+      List<String> list = ret.computeIfAbsent(hdr.getName(), k -> new LinkedList<>());
+      list.add(hdr.getValue());
+    }
+    return ret;
+  }
+
+  public void setRequestProperty(String name, String value) {
+    req.addHeader(name, value);
+  }
+
+  public void setRequestMethod(String method) {
+    this.method = method;
+    if ("GET".equalsIgnoreCase(method)) //$NON-NLS-1$
+      req = new HttpGet(urlStr);
+    else if ("PUT".equalsIgnoreCase(method)) //$NON-NLS-1$
+      req = new HttpPut(urlStr);
+    else if ("POST".equalsIgnoreCase(method)) //$NON-NLS-1$
+      req = new HttpPost(urlStr);
+    else {
+      this.method = null;
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  public void setUseCaches(boolean usecaches) {
+    // not needed
+  }
+
+  public void setConnectTimeout(int timeout) {
+    this.timeout = timeout;
+  }
+
+  public void setReadTimeout(int readTimeout) {
+    this.readTimeout = readTimeout;
+  }
+
+  public String getContentType() {
+    HttpEntity responseEntity = resp.getEntity();
+    if (responseEntity != null) {
+      Header contentType = responseEntity.getContentType();
+      if (contentType != null)
+        return contentType.getValue();
+    }
+    return null;
+  }
+
+  public InputStream getInputStream() throws IOException {
+    return resp.getEntity().getContent();
+  }
+
+  // will return only the first field
+  public String getHeaderField(String name) {
+    Header header = resp.getFirstHeader(name);
+    return (header == null) ? null : header.getValue();
+  }
+
+  public int getContentLength() {
+    return Integer.parseInt(resp.getFirstHeader("content-length") //$NON-NLS-1$
+                              .getValue());
+  }
+
+  public void setInstanceFollowRedirects(boolean followRedirects) {
+    this.followRedirects = followRedirects;
+  }
+
+  public void setDoOutput(boolean dooutput) {
+    // TODO: check whether we can really ignore this.
+  }
+
+  public void setFixedLengthStreamingMode(int contentLength) {
+    if (entity != null)
+      throw new IllegalArgumentException();
+    entity = new TemporaryBufferEntity(new TemporaryBuffer.LocalFile(null));
+    entity.setContentLength(contentLength);
+  }
+
+  public OutputStream getOutputStream() {
+    if (entity == null)
+      entity = new TemporaryBufferEntity(new TemporaryBuffer.LocalFile(null));
+    return entity.getBuffer();
+  }
+
+  public void setChunkedStreamingMode(int chunklen) {
+    if (entity == null)
+      entity = new TemporaryBufferEntity(new TemporaryBuffer.LocalFile(null));
+    entity.setChunked(true);
+  }
+
+  public String getRequestMethod() {
+    return method;
+  }
+
+  public boolean usingProxy() {
+    return isUsingProxy;
+  }
+
+  public void connect() throws IOException {
+    execute();
+  }
+
+  public void setHostnameVerifier(final HostnameVerifier hostnameverifier) {
+    this.hostnameverifier = new X509HostnameVerifier() {
+      public boolean verify(String hostname, SSLSession session) {
+        return hostnameverifier.verify(hostname, session);
+      }
+
+      public void verify(String host, String[] cns, String[] subjectAlts) {
+        throw new UnsupportedOperationException(); // TODO message
+      }
+
+      public void verify(String host, X509Certificate cert) {
+        throw new UnsupportedOperationException(); // TODO message
+      }
+
+      public void verify(String host, SSLSocket ssl) {
+        hostnameverifier.verify(host, ssl.getSession());
+      }
+    };
+  }
+
+  public void configure(KeyManager[] km, TrustManager[] tm,
+                        SecureRandom random) throws KeyManagementException {
+    getSSLContext().init(km, tm, random);
+  }
+
+  public synchronized void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  private class ConnectionHttpContext implements HttpContext {
+    public synchronized Object getAttribute(String s) {
+      return attributes.get(s);
+    }
+
+    public synchronized void setAttribute(String s, Object o) {
+      attributes.put(s, o);
+    }
+
+    public synchronized Object removeAttribute(String s) {
+      return attributes.remove(s);
+    }
+  }
+
+  public void setTrustStoreGetter(@NotNull final Supplier<KeyStore> trustStoreGetter) {
+    myTrustStoreGetter = trustStoreGetter;
+  }
+}
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnectionFactory.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/SSLHttpClientConnectionFactory.java
new file mode 100644 (file)
index 0000000..44c1289
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.git;
+
+import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URL;
+import java.security.KeyStore;
+import java.util.function.Supplier;
+
+/**
+ * {@link HttpConnectionFactory} with support of custom ssl trust store.
+ *
+ * Returns instances of {@link SSLHttpClientConnection}.
+ *
+ * @author Mikhail Khorkov
+ * @since 2018.1
+ */
+public class SSLHttpClientConnectionFactory implements HttpConnectionFactory {
+
+  @NotNull
+  private final Supplier<KeyStore> myTrustStoreGetter;
+
+  public SSLHttpClientConnectionFactory(@NotNull final Supplier<KeyStore> trustStoreGetter) {
+    myTrustStoreGetter = trustStoreGetter;
+  }
+
+  @Override
+  public HttpConnection create(final URL url) throws IOException {
+    SSLHttpClientConnection connection = new SSLHttpClientConnection(url.toString());
+    connection.setTrustStoreGetter(myTrustStoreGetter);
+    return connection;
+  }
+
+  @Override
+  public HttpConnection create(final URL url, final Proxy proxy) throws IOException {
+    SSLHttpClientConnection connection = new SSLHttpClientConnection(url.toString(), proxy);
+    connection.setTrustStoreGetter(myTrustStoreGetter);
+    return connection;
+  }
+}
index 70a24bf450a7d23fb7b32fffdd4a70b23655e34c..601dbadfb81491c8ff86a26b409175cfe47038a5 100644 (file)
@@ -16,6 +16,7 @@
 
 package jetbrains.buildServer.buildTriggers.vcs.git;
 
+import jetbrains.buildServer.util.ssl.SSLContextUtil;
 import org.eclipse.jgit.transport.http.HttpConnection;
 import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.jetbrains.annotations.NotNull;
@@ -26,10 +27,12 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.*;
 import java.security.KeyManagementException;
+import java.security.KeyStore;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 
 /**
  * Basically a copy of org.eclipse.jgit.transport.http.JDKHttpConnectionFactory
@@ -38,9 +41,11 @@ import java.util.Map;
 public class TeamCityJDKHttpConnectionFactory implements HttpConnectionFactory {
 
   private final ServerPluginConfig myConfig;
+  private Supplier<KeyStore> myTrustStoreGetter;
 
-  public TeamCityJDKHttpConnectionFactory(@NotNull ServerPluginConfig config) {
+  public TeamCityJDKHttpConnectionFactory(@NotNull ServerPluginConfig config, @NotNull Supplier<KeyStore> trustStoreGetter) {
     myConfig = config;
+    myTrustStoreGetter = trustStoreGetter;
   }
 
   public HttpConnection create(URL url) throws IOException {
@@ -160,12 +165,26 @@ public class TeamCityJDKHttpConnectionFactory implements HttpConnectionFactory {
     public void configure(KeyManager[] km, TrustManager[] tm, SecureRandom random) throws NoSuchAlgorithmException, KeyManagementException {
       SSLContext ctx = SSLContext.getInstance(myConfig.getHttpConnectionSslProtocol()); //$NON-NLS-1$
       ctx.init(km, tm, random);
-      ((HttpsURLConnection) wrappedUrlConnection).setSSLSocketFactory(new SSLSocketFactoryWithSoLinger(ctx.getSocketFactory(), myConfig.getHttpsSoLinger()));
+      ((HttpsURLConnection) wrappedUrlConnection).setSSLSocketFactory(createSSLSocketFactory(ctx.getSocketFactory()));
     }
 
     private void workaroundSslDeadlock() throws IOException {
-      SSLSocketFactory defaultFactory = ((HttpsURLConnection) wrappedUrlConnection).getSSLSocketFactory();
-      ((HttpsURLConnection) wrappedUrlConnection).setSSLSocketFactory(new SSLSocketFactoryWithSoLinger(defaultFactory, myConfig.getHttpsSoLinger()));
+      ((HttpsURLConnection) wrappedUrlConnection).setSSLSocketFactory(createSSLSocketFactory());
+    }
+
+    private SSLSocketFactory createSSLSocketFactory() {
+      final SSLContext trusted = SSLContextUtil.createUserSSLContext(myTrustStoreGetter.get());
+      final SSLSocketFactory origin;
+      if (trusted != null) {
+        origin = trusted.getSocketFactory();
+      } else {
+        origin = ((HttpsURLConnection) wrappedUrlConnection).getSSLSocketFactory();
+      }
+      return createSSLSocketFactory(origin);
+    }
+
+    private SSLSocketFactory createSSLSocketFactory(@NotNull SSLSocketFactory origin) {
+      return new SSLSocketFactoryWithSoLinger(origin, myConfig.getHttpsSoLinger());
     }
 
     public void setAttribute(String name, Object value) {
index 75fb63e47540c11ae3c8840f8994ca3b820ac231..b936909629c93cbd8f9313849068f53930ce5cc5 100644 (file)
@@ -33,7 +33,6 @@ import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.*;
 import org.eclipse.jgit.transport.http.HttpConnectionFactory;
-import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
 import org.eclipse.jgit.util.FS;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -54,20 +53,28 @@ public class TransportFactoryImpl implements TransportFactory {
   private final ServerPluginConfig myConfig;
   private final Map<String,String> myJSchOptions;
   private final VcsRootSshKeyManager mySshKeyManager;
+  private final GitTrustStoreProvider myGitTrustStoreProvider;
 
   public TransportFactoryImpl(@NotNull ServerPluginConfig config,
                               @NotNull VcsRootSshKeyManager sshKeyManager) {
+    this(config, sshKeyManager, new GitTrustStoreProviderStatic(null));
+  }
+
+  public TransportFactoryImpl(@NotNull ServerPluginConfig config,
+                              @NotNull VcsRootSshKeyManager sshKeyManager,
+                              @NotNull GitTrustStoreProvider gitTrustStoreProvider) {
     myConfig = config;
+    myGitTrustStoreProvider = gitTrustStoreProvider;
     myJSchOptions = getJSchCipherOptions();
     mySshKeyManager = sshKeyManager;
     String factoryName = myConfig.getHttpConnectionFactory();
     HttpConnectionFactory f;
     if ("httpClient".equals(factoryName)) {
-      f = new SNIHttpClientConnectionFactory();
+      f = new SNIHttpClientConnectionFactory(() -> myGitTrustStoreProvider.getTrustStore());
     } else if ("httpClientNoSNI".equals(factoryName)) {
-      f = new HttpClientConnectionFactory();
+      f = new SSLHttpClientConnectionFactory(() -> myGitTrustStoreProvider.getTrustStore());
     } else {
-      f = new TeamCityJDKHttpConnectionFactory(myConfig);
+      f = new TeamCityJDKHttpConnectionFactory(myConfig, () -> myGitTrustStoreProvider.getTrustStore());
     }
     HttpTransport.setConnectionFactory(f);
   }
index e443a320fa793a85239871d72fbe64a9eb758606..c7fc9078480e54288dc9174f6913a056cbef7cf4 100644 (file)
@@ -54,6 +54,7 @@ public final class GitPatchBuilderDispatcher {
   private final String myFromRevision;
   private final String myToRevision;
   private final CheckoutRules myRules;
+  private final String myTrustedCertificatesDir;
 
   public GitPatchBuilderDispatcher(@NotNull ServerPluginConfig config,
                                    @NotNull VcsRootSshKeyManager sshKeyManager,
@@ -61,7 +62,8 @@ public final class GitPatchBuilderDispatcher {
                                    @NotNull PatchBuilder builder,
                                    @Nullable String fromRevision,
                                    @NotNull String toRevision,
-                                   @NotNull CheckoutRules rules) throws VcsException {
+                                   @NotNull CheckoutRules rules,
+                                   @NotNull String trustedCertificatesDir) throws VcsException {
     myConfig = config;
     mySshKeyManager = sshKeyManager;
     myContext = context;
@@ -70,6 +72,7 @@ public final class GitPatchBuilderDispatcher {
     myFromRevision = fromRevision;
     myToRevision = toRevision;
     myRules = rules;
+    myTrustedCertificatesDir = trustedCertificatesDir;
   }
 
   public void buildPatch() throws Exception {
@@ -132,6 +135,7 @@ public final class GitPatchBuilderDispatcher {
     props.put(Constants.PATCHER_PATCH_FILE, patchFile.getCanonicalPath());
     props.put(Constants.PATCHER_UPLOADED_KEY, getUploadedKey());
     props.put(Constants.VCS_DEBUG_ENABLED, String.valueOf(Loggers.VCS.isDebugEnabled()));
+    props.put(Constants.GIT_TRUST_STORE_PROVIDER, myTrustedCertificatesDir);
     props.putAll(myGitRoot.getProperties());
     return VcsUtil.propertiesToStringSecure(props).getBytes("UTF-8");
   }
index ddd09a75a2d7a057790405298d170b333ed4aa2e..7147462b7544d5b0a77062b130daf0ecba552288 100644 (file)
@@ -21,6 +21,7 @@ import jetbrains.buildServer.buildTriggers.vcs.git.submodules.SubmoduleFetchExce
 import jetbrains.buildServer.serverSide.CachePaths;
 import jetbrains.buildServer.ssh.TeamCitySshKey;
 import jetbrains.buildServer.ssh.VcsRootSshKeyManager;
+import jetbrains.buildServer.util.ssl.TrustStoreReader;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsRoot;
 import jetbrains.buildServer.vcs.VcsUtil;
@@ -45,9 +46,10 @@ public class GitPatchProcess {
     RepositoryManager repositoryManager = new RepositoryManagerImpl(config, new MirrorManagerImpl(config, new HashCalculatorImpl()));
     GitMapFullPath mapFullPath = new GitMapFullPath(config, new RevisionsCache(config));
     VcsRootSshKeyManager sshKeyManager = new ConstantSshKeyManager(settings.getKeyBytes());
-    TransportFactory transportFactory = new TransportFactoryImpl(config, sshKeyManager);
+    TransportFactory transportFactory = new TransportFactoryImpl(config, sshKeyManager, settings.getGitTrustStoreProvider());
     FetcherProperties fetcherProperties = new FetcherProperties(config);
-    FetchCommand fetchCommand = new FetchCommandImpl(config, transportFactory, fetcherProperties, sshKeyManager);
+    FetchCommand fetchCommand = new FetchCommandImpl(config, transportFactory, fetcherProperties, sshKeyManager,
+                                                     settings.getGitTrustStoreProvider());
     CommitLoader commitLoader = new CommitLoaderImpl(repositoryManager, fetchCommand, mapFullPath);
 
     OperationContext context = new OperationContext(commitLoader, repositoryManager, settings.getRoot(), "build patch", GitProgress.NO_OP, config);
@@ -124,6 +126,7 @@ public class GitPatchProcess {
     private final byte[] myKeyBytes;
     private final boolean myDebugEnabled;
     private final VcsRoot myRoot;
+    private final GitTrustStoreProvider myGitTrustStoreProvider;
 
     public GitPatchProcessSettings(@NotNull Map<String, String> props) {
       myInternalProperties = readInternalProperties(props);
@@ -136,6 +139,7 @@ public class GitPatchProcess {
       myKeyBytes = readKeyBytes(props);
       myDebugEnabled = readDebugEnabled(props);
       myRoot = readRoot(props);
+      myGitTrustStoreProvider = readGitTrustStoreProvider(props);
     }
 
     @NotNull
@@ -205,6 +209,11 @@ public class GitPatchProcess {
       return new VcsRootImpl(0, props);
     }
 
+    @NotNull
+    private GitTrustStoreProvider readGitTrustStoreProvider(@NotNull Map<String, String> props) {
+      return new GitTrustStoreProviderStatic(props.get(Constants.GIT_TRUST_STORE_PROVIDER));
+    }
+
 
     @NotNull
     public File getInternalProperties() {
@@ -252,6 +261,11 @@ public class GitPatchProcess {
     public VcsRoot getRoot() {
       return myRoot;
     }
+
+    @NotNull
+    public GitTrustStoreProvider getGitTrustStoreProvider() {
+      return myGitTrustStoreProvider;
+    }
   }
 
   private static boolean isImportant(Throwable t) {