502d22a33fa9e47bc6b79f4238c9f4e9948985da
[teamcity/git-plugin.git] / git-agent / src / jetbrains / buildServer / buildTriggers / vcs / git / agent / ssl / SSLInvestigator.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent.ssl;
18
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;
30
31 import javax.net.ssl.*;
32 import java.io.File;
33 import java.io.IOException;
34 import java.security.*;
35
36 /**
37  * Component for investigate either we need use custom ssl certificates for git fetch or not.
38  *
39  * @author Mikhail Khorkov
40  * @since 2018.1.2
41  */
42 public class SSLInvestigator {
43
44   private final static Logger LOG = Logger.getLogger(SSLInvestigator.class);
45
46   private final static String CERT_FILE = "git_custom_certificates.crt";
47
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;
53
54   private volatile Boolean myNeedCustomCertificate = null;
55   private volatile String myCAInfoPath = null;
56
57   public SSLInvestigator(@NotNull final URIish fetchURL, @NotNull final String tempDirectory, @NotNull final String homeDirectory) {
58     this(fetchURL, tempDirectory, homeDirectory, new SSLCheckerImpl(), new SSLContextRetrieverImpl());
59   }
60
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;
68
69     if (!"https".equals(myFetchURL.getScheme())) {
70       myNeedCustomCertificate = false;
71     }
72     if (!TeamCityProperties.getBooleanOrTrue("teamcity.ssl.useCustomTrustStore.git")) {
73       myNeedCustomCertificate = false;
74     }
75   }
76
77   public void setCertificateOptions(@NotNull final GitFacade gitFacade) {
78     if (!isNeedCustomCertificates()) {
79       deleteSslOption(gitFacade);
80       return;
81     }
82
83     final String caInfoPath = caInfoPath();
84     if (caInfoPath != null) {
85       setSslOption(gitFacade, caInfoPath);
86     }
87   }
88
89   @Nullable
90   private String caInfoPath() {
91     String caInfoPath = myCAInfoPath;
92     if (caInfoPath == null) {
93       synchronized (this) {
94         caInfoPath = myCAInfoPath;
95         if (caInfoPath != null) {
96           return caInfoPath;
97         }
98
99         caInfoPath = generateCertificateFile();
100         myCAInfoPath = caInfoPath;
101       }
102     }
103     return caInfoPath;
104   }
105
106   @Nullable
107   private String generateCertificateFile() {
108     try {
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();
115       }
116     } catch (IOException e) {
117       LOG.error("Can not write file with certificates", e);
118     }
119     return null;
120   }
121
122   private boolean isNeedCustomCertificates() {
123     Boolean need = myNeedCustomCertificate;
124     if (need == null) {
125       synchronized (this) {
126         need = myNeedCustomCertificate;
127         if (need != null) {
128           return need;
129         }
130
131         need = doesCanConnectWithCustomCertificate();
132         myNeedCustomCertificate = need;
133       }
134     }
135     return need;
136   }
137
138   private boolean doesCanConnectWithCustomCertificate() {
139     try {
140       final SSLContext sslContext = mySSLContextRetriever.retrieve(myHomeDirectory);
141       if (sslContext == null) {
142         /* there are no custom certificate */
143         return false;
144       }
145
146       final int port = myFetchURL.getPort() > 0 ? myFetchURL.getPort() : 443;
147       return mySSLChecker.canConnect(sslContext, myFetchURL.getHost(), port);
148
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 */
153       return false;
154     }
155   }
156
157   private void deleteSslOption(@NotNull final GitFacade gitFacade) {
158     try {
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();
163       }
164     } catch (Exception e) {
165       /* option was not exist, ignore exception then */
166     }
167   }
168
169   private void setSslOption(@NotNull final GitFacade gitFacade, @NotNull final String path) {
170     try {
171       gitFacade.setConfig().setPropertyName("http.sslCAInfo").setValue(path).call();
172     } catch (Exception e) {
173       LOG.error("Error while setting sslCAInfo git option");
174     }
175   }
176
177   public interface SSLContextRetriever {
178     @Nullable
179     SSLContext retrieve(@NotNull String homeDirectory) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
180   }
181
182   public static class SSLContextRetrieverImpl implements SSLContextRetriever {
183
184     @Override
185     @Nullable
186     public SSLContext retrieve(@NotNull final String homeDirectory)
187       throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
188       final X509TrustManager manager = trustManager(homeDirectory);
189       if (manager == null) {
190         return null;
191       }
192
193       final SSLContext context = SSLContext.getInstance("TLS");
194       context.init(null, new TrustManager[]{manager}, new SecureRandom());
195
196       return context;
197     }
198
199     @Nullable
200     private X509TrustManager trustManager(@NotNull final String homeDirectory) throws NoSuchAlgorithmException, KeyStoreException {
201       final KeyStore trustStore = trustStore(homeDirectory);
202       if (trustStore == null) {
203         return null;
204       }
205
206       final TrustManagerFactory manager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
207       manager.init(trustStore);
208
209       return (X509TrustManager)manager.getTrustManagers()[0];
210     }
211
212     @Nullable
213     private KeyStore trustStore(@NotNull final String homeDirectory) {
214       final String certDirectory = TrustedCertificatesDirectory.getAllCertificatesDirectoryFromHome(homeDirectory);
215       return TrustStoreIO.readTrustStoreFromDirectory(certDirectory);
216     }
217   }
218
219   public interface SSLChecker {
220     boolean canConnect(@NotNull final SSLContext sslContext, @NotNull final String host, int port) throws Exception;
221   }
222
223   public static class SSLCheckerImpl implements SSLChecker {
224     @Override
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));
228       try {
229         socket.startHandshake();
230         socket.close();
231       } catch (Exception e) {
232         /* can't connect with custom certificate */
233         return false;
234       }
235       return true;
236     }
237   }
238 }