2 * Copyright 2000-2014 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;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.jcraft.jsch.Cipher;
22 import com.jcraft.jsch.JSch;
23 import com.jcraft.jsch.JSchException;
24 import com.jcraft.jsch.Session;
25 import jetbrains.buildServer.ssh.TeamCitySshKey;
26 import jetbrains.buildServer.ssh.VcsRootSshKeyManager;
27 import jetbrains.buildServer.vcs.VcsException;
28 import jetbrains.buildServer.vcs.VcsRoot;
29 import jetbrains.buildServer.version.ServerVersionHolder;
30 import jetbrains.buildServer.version.ServerVersionInfo;
31 import org.eclipse.jgit.errors.NotSupportedException;
32 import org.eclipse.jgit.errors.TransportException;
33 import org.eclipse.jgit.lib.Repository;
34 import org.eclipse.jgit.transport.*;
35 import org.eclipse.jgit.transport.http.HttpConnectionFactory;
36 import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
37 import org.eclipse.jgit.util.FS;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
44 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
45 import static java.util.Collections.emptySet;
48 * @author dmitry.neverov
50 public class TransportFactoryImpl implements TransportFactory {
52 private static Logger LOG = Logger.getInstance(TransportFactoryImpl.class.getName());
54 private final ServerPluginConfig myConfig;
55 private final Map<String,String> myJSchOptions;
56 private final VcsRootSshKeyManager mySshKeyManager;
58 public TransportFactoryImpl(@NotNull ServerPluginConfig config,
59 @NotNull VcsRootSshKeyManager sshKeyManager) {
61 myJSchOptions = getJSchCipherOptions();
62 mySshKeyManager = sshKeyManager;
63 String factoryName = myConfig.getHttpConnectionFactory();
64 HttpConnectionFactory f;
65 if ("httpClient".equals(factoryName)) {
66 f = new SNIHttpClientConnectionFactory();
67 } else if ("httpClientNoSNI".equals(factoryName)) {
68 f = new HttpClientConnectionFactory();
70 f = new TeamCityJDKHttpConnectionFactory(myConfig);
72 HttpTransport.setConnectionFactory(f);
75 public Transport createTransport(@NotNull Repository r, @NotNull URIish url, @NotNull AuthSettings authSettings) throws NotSupportedException, VcsException {
76 return createTransport(r, url, authSettings, myConfig.getIdleTimeoutSeconds());
80 public Transport createTransport(@NotNull final Repository r,
81 @NotNull final URIish url,
82 @NotNull final AuthSettings authSettings,
83 final int timeoutSeconds) throws NotSupportedException, VcsException {
86 URIish preparedURI = prepareURI(url);
87 final Transport t = Transport.open(r, preparedURI);
88 t.setCredentialsProvider(authSettings.toCredentialsProvider());
89 if (t instanceof SshTransport) {
90 SshTransport ssh = (SshTransport)t;
91 ssh.setSshSessionFactory(getSshSessionFactory(authSettings, url));
93 t.setTimeout(timeoutSeconds);
95 } catch (TransportException e) {
96 throw new VcsException("Cannot create transport", e);
101 private URIish prepareURI(@NotNull URIish uri) {
102 final String scheme = uri.getScheme();
103 //Remove a username from the http URI. A Username can contain forbidden
104 //characters, e.g. backslash (TW-21747). A username and a password will
105 //be supplied by CredentialProvider
106 if ("http".equals(scheme) || "https".equals(scheme))
107 return uri.setUser(null);
113 * This is a work-around for an issue http://youtrack.jetbrains.net/issue/TW-9933.
114 * Due to bug in jgit (https://bugs.eclipse.org/bugs/show_bug.cgi?id=315564),
115 * in the case of not-existing local repository we get an obscure exception:
116 * 'org.eclipse.jgit.errors.NotSupportedException: URI not supported: x:/git/myrepo.git',
117 * while URI is correct.
119 * It often happens when people try to access a repository located on a mapped network
120 * drive from the TeamCity started as Windows service.
122 * If repository is local and is not exists this method throws a friendly exception.
124 * @param url URL to check
125 * @throws VcsException if url points to not-existing local repository
127 private void checkUrl(final URIish url) throws VcsException {
128 String scheme = url.getScheme();
129 if (!url.isRemote() && !"http".equals(scheme) && !"https".equals(scheme)) {
130 File localRepository = new File(url.getPath());
131 if (!localRepository.exists()) {
132 String error = "Cannot access the '" + url.toString() + "' repository";
133 if (SystemInfo.isWindows) {
134 error += ". If TeamCity is run as a Windows service, it cannot access network mapped drives. Make sure this is not your case.";
136 throw new VcsException(error);
143 * Get appropriate session factory object for specified settings and url
145 * @param authSettings a vcs root settings
146 * @param url URL of interest
147 * @return session factory object
148 * @throws VcsException in case of problems with creating object
150 private SshSessionFactory getSshSessionFactory(AuthSettings authSettings, URIish url) throws VcsException {
151 switch (authSettings.getAuthMethod()) {
152 case PRIVATE_KEY_DEFAULT:
153 return new DefaultJschConfigSessionFactory(myConfig, authSettings, myJSchOptions);
154 case PRIVATE_KEY_FILE:
155 return new CustomPrivateKeySessionFactory(myConfig, authSettings, myJSchOptions);
156 case TEAMCITY_SSH_KEY:
157 return new TeamCitySshKeySessionFactory(myConfig, authSettings, myJSchOptions, mySshKeyManager);
159 return new PasswordJschConfigSessionFactory(myConfig, authSettings, myJSchOptions);
161 final AuthenticationMethod method = authSettings.getAuthMethod();
162 final String methodName = method == null ? "<null>" : method.uiName();
163 throw new VcsAuthenticationException(url.toString(), "The authentication method " + methodName + " is not supported for SSH, please provide SSH key or credentials");
168 private static class DefaultJschConfigSessionFactory extends JschConfigSessionFactory {
169 protected final ServerPluginConfig myConfig;
170 protected final AuthSettings myAuthSettings;
171 private final Map<String,String> myJschOptions;
173 private DefaultJschConfigSessionFactory(@NotNull ServerPluginConfig config,
174 @NotNull AuthSettings authSettings,
175 @NotNull Map<String,String> jschOptions) {
177 myAuthSettings = authSettings;
178 myJschOptions = jschOptions;
182 protected void configure(OpenSshConfig.Host hc, Session session) {
183 session.setProxy(myConfig.getJschProxy());//null proxy is allowed
184 if (myAuthSettings.isIgnoreKnownHosts())
185 session.setConfig("StrictHostKeyChecking", "no");
186 if (!myConfig.alwaysCheckCiphers()) {
187 for (Map.Entry<String, String> entry : myJschOptions.entrySet())
188 session.setConfig(entry.getKey(), entry.getValue());
193 private static class PasswordJschConfigSessionFactory extends DefaultJschConfigSessionFactory {
195 private PasswordJschConfigSessionFactory(@NotNull ServerPluginConfig config,
196 @NotNull AuthSettings authSettings,
197 @NotNull Map<String,String> jschOptions) {
198 super(config, authSettings, jschOptions);
202 protected void configure(OpenSshConfig.Host hc, Session session) {
203 super.configure(hc, session);
204 session.setPassword(myAuthSettings.getPassword());
209 private static class CustomPrivateKeySessionFactory extends DefaultJschConfigSessionFactory {
211 private CustomPrivateKeySessionFactory(@NotNull ServerPluginConfig config,
212 @NotNull AuthSettings authSettings,
213 @NotNull Map<String,String> jschOptions) {
214 super(config, authSettings, jschOptions);
218 protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException {
219 return createDefaultJSch(fs);
223 protected JSch createDefaultJSch(FS fs) throws JSchException {
224 final JSch jsch = new JSch();
225 jsch.addIdentity(myAuthSettings.getPrivateKeyFilePath(), myAuthSettings.getPassphrase());
230 protected void configure(OpenSshConfig.Host hc, Session session) {
231 super.configure(hc, session);
232 session.setConfig("StrictHostKeyChecking", "no");
237 private static class TeamCitySshKeySessionFactory extends DefaultJschConfigSessionFactory {
239 private final VcsRootSshKeyManager mySshKeyManager;
241 private TeamCitySshKeySessionFactory(@NotNull ServerPluginConfig config,
242 @NotNull AuthSettings authSettings,
243 @NotNull Map<String,String> jschOptions,
244 @NotNull VcsRootSshKeyManager sshKeyManager) {
245 super(config, authSettings, jschOptions);
246 mySshKeyManager = sshKeyManager;
250 protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException {
251 return createDefaultJSch(fs);
255 protected JSch createDefaultJSch(FS fs) throws JSchException {
256 final JSch jsch = new JSch();
257 final VcsRoot root = myAuthSettings.getRoot();
259 TeamCitySshKey sshKey = mySshKeyManager.getKey(root);
260 if (sshKey != null) {
262 jsch.addIdentity("", sshKey.getPrivateKey(), null, myAuthSettings.getPassphrase() != null ? myAuthSettings.getPassphrase().getBytes() : null);
263 } catch (JSchException e) {
264 String keyName = root.getProperty(VcsRootSshKeyManager.VCS_ROOT_TEAMCITY_SSH_KEY_NAME);
267 throw new JSchException(getErrorMessage(keyName, e), e);
275 private String getErrorMessage(@NotNull String keyName, @NotNull JSchException e) {
276 String msg = e.getMessage();
278 LOG.debug("Error while loading an uploaded key '" + keyName + "'", e);
279 return "Error while loading an uploaded key '" + keyName + "'";
281 int idx = msg.indexOf("[B@");
283 msg = msg.substring(0, idx);
285 if (msg.endsWith(":"))
286 msg = msg.substring(0, msg.length() - 1);
288 return "Error while loading an uploaded key '" + keyName + "': " + msg;
292 protected void configure(OpenSshConfig.Host hc, Session session) {
293 super.configure(hc, session);
294 session.setConfig("StrictHostKeyChecking", "no");
295 String teamCityVersion = getTeamCityVersion();
296 if (teamCityVersion != null) {
297 session.setClientVersion(GitUtils.getSshClientVersion(session.getClientVersion(), teamCityVersion));
304 * JSch checks available ciphers during session connect
305 * (inside method send_kexinit), which is expensive (see
306 * thread dumps attached to TW-18811). This method checks
307 * available ciphers and if they are found turns off checks
309 * @return map of cipher options for JSch which either
310 * specify found ciphers and turn off expensive cipher check,
311 * or, when no ciphers found, do nothing, so we will get
312 * exception from JSch with explanation of the problem
314 private Map<String, String> getJSchCipherOptions() {
315 LOG.debug("Check available ciphers");
317 JSch jsch = new JSch();
318 Session session = jsch.getSession("", "");
320 String cipherc2s = session.getConfig("cipher.c2s");
321 String ciphers2c = session.getConfig("cipher.s2c");
323 Set<String> notAvailable = checkCiphers(session);
324 if (!notAvailable.isEmpty()) {
325 cipherc2s = diffString(cipherc2s, notAvailable);
326 ciphers2c = diffString(ciphers2c, notAvailable);
327 if (isEmpty(cipherc2s) || isEmpty(ciphers2c)) {
328 LOG.debug("No ciphers found, use default JSch options");
329 return new HashMap<String, String>();
333 LOG.debug("Turn off ciphers checks, use found ciphers cipher.c2s: " + cipherc2s + ", cipher.s2c: " + ciphers2c);
334 Map<String, String> options = new HashMap<String, String>();
335 options.put("cipher.c2s", cipherc2s);
336 options.put("cipher.s2c", ciphers2c);
337 options.put("CheckCiphers", "");//turn off ciphers check
339 } catch (JSchException e) {
340 LOG.debug("Error while ciphers check, use default JSch options", e);
341 return new HashMap<String, String>();
345 private Set<String> checkCiphers(@NotNull Session session) {
346 String ciphers = session.getConfig("CheckCiphers");
347 if (isEmpty(ciphers))
350 Set<String> result = new HashSet<String>();
351 String[] _ciphers = ciphers.split(",");
352 for (String cipher : _ciphers) {
353 if (!checkCipher(session.getConfig(cipher)))
360 private boolean checkCipher(String cipherClassName){
362 Class klass = Class.forName(cipherClassName);
363 Cipher cipher = (Cipher)(klass.newInstance());
364 cipher.init(Cipher.ENCRYPT_MODE,
365 new byte[cipher.getBlockSize()],
366 new byte[cipher.getIVSize()]);
368 } catch(Exception e){
373 private String diffString(@NotNull String str, @NotNull Set<String> notAvailable) {
374 List<String> ciphers = new ArrayList<String>(Arrays.asList(str.split(",")));
375 ciphers.removeAll(notAvailable);
377 StringBuilder builder = new StringBuilder();
378 Iterator<String> iter = ciphers.iterator();
379 while (iter.hasNext()) {
380 String cipher = iter.next();
381 builder.append(cipher);
385 return builder.toString();
389 private static String getTeamCityVersion() {
391 ServerVersionInfo version = ServerVersionHolder.getVersion();
392 return "TeamCity Server " + version.getDisplayVersion();
393 } catch (Exception e) {