2 * Copyright 2000-2018 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent.ssl;
19 import jetbrains.buildServer.agent.ssl.TrustedCertificatesDirectory;
20 import jetbrains.buildServer.buildTriggers.vcs.git.agent.GitFacade;
21 import jetbrains.buildServer.serverSide.TeamCityProperties;
22 import jetbrains.buildServer.util.FileUtil;
23 import jetbrains.buildServer.util.StringUtil;
24 import jetbrains.buildServer.util.ssl.TrustStoreIO;
25 import org.apache.commons.codec.CharEncoding;
26 import org.apache.log4j.Logger;
27 import org.eclipse.jgit.transport.URIish;
28 import org.jetbrains.annotations.NotNull;
29 import org.jetbrains.annotations.Nullable;
31 import javax.net.ssl.*;
33 import java.io.IOException;
34 import java.security.*;
37 * Component for investigate either we need use custom ssl certificates for git fetch or not.
39 * @author Mikhail Khorkov
42 public class SSLInvestigator {
44 private final static Logger LOG = Logger.getLogger(SSLInvestigator.class);
46 private final static String CERT_FILE = "git_custom_certificates.crt";
48 private final URIish myFetchURL;
49 private final String myTempDirectory;
50 private final String myHomeDirectory;
51 private final SSLChecker mySSLChecker;
52 private final SSLContextRetriever mySSLContextRetriever;
54 private volatile Boolean myNeedCustomCertificate = null;
55 private volatile String myCAInfoPath = null;
57 public SSLInvestigator(@NotNull final URIish fetchURL, @NotNull final String tempDirectory, @NotNull final String homeDirectory) {
58 this(fetchURL, tempDirectory, homeDirectory, new SSLCheckerImpl(), new SSLContextRetrieverImpl());
61 public SSLInvestigator(@NotNull final URIish fetchURL, @NotNull final String tempDirectory, @NotNull final String homeDirectory,
62 @NotNull final SSLChecker sslChecker, @NotNull final SSLContextRetriever sslContextRetriever) {
63 myFetchURL = fetchURL;
64 myTempDirectory = tempDirectory;
65 myHomeDirectory = homeDirectory;
66 mySSLChecker = sslChecker;
67 mySSLContextRetriever = sslContextRetriever;
69 if (!"https".equals(myFetchURL.getScheme())) {
70 myNeedCustomCertificate = false;
72 if (!TeamCityProperties.getBooleanOrTrue("teamcity.ssl.useCustomTrustStore.git")) {
73 myNeedCustomCertificate = false;
77 public void setCertificateOptions(@NotNull final GitFacade gitFacade) {
78 if (!isNeedCustomCertificates()) {
79 deleteSslOption(gitFacade);
83 final String caInfoPath = caInfoPath();
84 if (caInfoPath != null) {
85 setSslOption(gitFacade, caInfoPath);
90 private String caInfoPath() {
91 String caInfoPath = myCAInfoPath;
92 if (caInfoPath == null) {
94 caInfoPath = myCAInfoPath;
95 if (caInfoPath != null) {
99 caInfoPath = generateCertificateFile();
100 myCAInfoPath = caInfoPath;
107 private String generateCertificateFile() {
109 final String certDirectory = TrustedCertificatesDirectory.getAllCertificatesDirectoryFromHome(myHomeDirectory);
110 final String pemContent = TrustStoreIO.pemContentFromDirectory(certDirectory);
111 if (!pemContent.isEmpty()) {
112 final File file = new File(myTempDirectory, CERT_FILE);
113 FileUtil.writeFile(file, pemContent, CharEncoding.UTF_8);
114 return file.getPath();
116 } catch (IOException e) {
117 LOG.error("Can not write file with certificates", e);
122 private boolean isNeedCustomCertificates() {
123 Boolean need = myNeedCustomCertificate;
125 synchronized (this) {
126 need = myNeedCustomCertificate;
131 need = doesCanConnectWithCustomCertificate();
132 myNeedCustomCertificate = need;
138 private boolean doesCanConnectWithCustomCertificate() {
140 final SSLContext sslContext = mySSLContextRetriever.retrieve(myHomeDirectory);
141 if (sslContext == null) {
142 /* there are no custom certificate */
146 final int port = myFetchURL.getPort() > 0 ? myFetchURL.getPort() : 443;
147 return mySSLChecker.canConnect(sslContext, myFetchURL.getHost(), port);
149 } catch (Exception e) {
150 LOG.error("Unexpected error while try to connect to git server " + myFetchURL.toString()
151 + " for checking custom certificates", e);
152 /* unexpected error. do not use custom certificate then */
157 private void deleteSslOption(@NotNull final GitFacade gitFacade) {
159 final String previous = gitFacade.getConfig().setPropertyName("http.sslCAInfo").call();
160 if (!StringUtil.isEmptyOrSpaces(previous)) {
161 /* do not need custom certificate then remove corresponding options if exists */
162 gitFacade.setConfig().setPropertyName("http.sslCAInfo").unSet().call();
164 } catch (Exception e) {
165 /* option was not exist, ignore exception then */
169 private void setSslOption(@NotNull final GitFacade gitFacade, @NotNull final String path) {
171 gitFacade.setConfig().setPropertyName("http.sslCAInfo").setValue(path).call();
172 } catch (Exception e) {
173 LOG.error("Error while setting sslCAInfo git option");
177 public interface SSLContextRetriever {
179 SSLContext retrieve(@NotNull String homeDirectory) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
182 public static class SSLContextRetrieverImpl implements SSLContextRetriever {
186 public SSLContext retrieve(@NotNull final String homeDirectory)
187 throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
188 final X509TrustManager manager = trustManager(homeDirectory);
189 if (manager == null) {
193 final SSLContext context = SSLContext.getInstance("TLS");
194 context.init(null, new TrustManager[]{manager}, new SecureRandom());
200 private X509TrustManager trustManager(@NotNull final String homeDirectory) throws NoSuchAlgorithmException, KeyStoreException {
201 final KeyStore trustStore = trustStore(homeDirectory);
202 if (trustStore == null) {
206 final TrustManagerFactory manager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
207 manager.init(trustStore);
209 return (X509TrustManager)manager.getTrustManagers()[0];
213 private KeyStore trustStore(@NotNull final String homeDirectory) {
214 final String certDirectory = TrustedCertificatesDirectory.getAllCertificatesDirectoryFromHome(homeDirectory);
215 return TrustStoreIO.readTrustStoreFromDirectory(certDirectory);
219 public interface SSLChecker {
220 boolean canConnect(@NotNull final SSLContext sslContext, @NotNull final String host, int port) throws Exception;
223 public static class SSLCheckerImpl implements SSLChecker {
225 public boolean canConnect(@NotNull final SSLContext sslContext, @NotNull final String host, final int port) throws Exception {
226 final SSLSocket socket = (SSLSocket)sslContext.getSocketFactory().createSocket(host, port);
227 socket.setSoTimeout(TeamCityProperties.getInteger("teamcity.ssl.checkTimeout.git", 10 * 1000));
229 socket.startHandshake();
231 } catch (Exception e) {
232 /* can't connect with custom certificate */