blob: ce14183a56f0e937413b88676385370dfca27557 [file] [log] [blame]
/*
* Copyright (C) 2016, Mark Ingram <markdingram@gmail.com>
* Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
* Copyright (C) 2008-2009, Google Inc.
* Copyright (C) 2009, Google, Inc.
* Copyright (C) 2009, JetBrains s.r.o.
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.transport;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.FS;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
/**
* The base session factory that loads known hosts and private keys from
* <code>$HOME/.ssh</code>.
* <p>
* This is the default implementation used by JGit and provides most of the
* compatibility necessary to match OpenSSH, a popular implementation of SSH
* used by C Git.
* <p>
* The factory does not provide UI behavior. Override the method
* {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)}
* to supply appropriate {@link UserInfo} to the session.
*/
public abstract class JschConfigSessionFactory extends SshSessionFactory {
private final Map<String, JSch> byIdentityFile = new HashMap<>();
private JSch defaultJSch;
private OpenSshConfig config;
@Override
public synchronized RemoteSession getSession(URIish uri,
CredentialsProvider credentialsProvider, FS fs, int tms)
throws TransportException {
String user = uri.getUser();
final String pass = uri.getPass();
String host = uri.getHost();
int port = uri.getPort();
try {
if (config == null)
config = OpenSshConfig.get(fs);
final OpenSshConfig.Host hc = config.lookup(host);
host = hc.getHostName();
if (port <= 0)
port = hc.getPort();
if (user == null)
user = hc.getUser();
Session session = createSession(credentialsProvider, fs, user,
pass, host, port, hc);
int retries = 0;
while (!session.isConnected()) {
try {
retries++;
session.connect(tms);
} catch (JSchException e) {
session.disconnect();
session = null;
// Make sure our known_hosts is not outdated
knownHosts(getJSch(hc, fs), fs);
if (isAuthenticationCanceled(e)) {
throw e;
} else if (isAuthenticationFailed(e)
&& credentialsProvider != null) {
// if authentication failed maybe credentials changed at
// the remote end therefore reset credentials and retry
if (retries < 3) {
credentialsProvider.reset(uri);
session = createSession(credentialsProvider, fs,
user, pass, host, port, hc);
} else
throw e;
} else if (retries >= hc.getConnectionAttempts()) {
throw e;
} else {
try {
Thread.sleep(1000);
session = createSession(credentialsProvider, fs,
user, pass, host, port, hc);
} catch (InterruptedException e1) {
throw new TransportException(
JGitText.get().transportSSHRetryInterrupt,
e1);
}
}
}
}
return new JschSession(session, uri);
} catch (JSchException je) {
final Throwable c = je.getCause();
if (c instanceof UnknownHostException)
throw new TransportException(uri, JGitText.get().unknownHost);
if (c instanceof ConnectException)
throw new TransportException(uri, c.getMessage());
throw new TransportException(uri, je.getMessage(), je);
}
}
private static boolean isAuthenticationFailed(JSchException e) {
return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$
}
private static boolean isAuthenticationCanceled(JSchException e) {
return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$
}
private Session createSession(CredentialsProvider credentialsProvider,
FS fs, String user, final String pass, String host, int port,
final OpenSshConfig.Host hc) throws JSchException {
final Session session = createSession(hc, user, host, port, fs);
// We retry already in getSession() method. JSch must not retry
// on its own.
session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$
if (pass != null)
session.setPassword(pass);
final String strictHostKeyCheckingPolicy = hc
.getStrictHostKeyChecking();
if (strictHostKeyCheckingPolicy != null)
session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$
strictHostKeyCheckingPolicy);
final String pauth = hc.getPreferredAuthentications();
if (pauth != null)
session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$
if (credentialsProvider != null
&& (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
session.setUserInfo(new CredentialsProviderUserInfo(session,
credentialsProvider));
}
configure(hc, session);
return session;
}
/**
* Create a new remote session for the requested address.
*
* @param hc
* host configuration
* @param user
* login to authenticate as.
* @param host
* server name to connect to.
* @param port
* port number of the SSH daemon (typically 22).
* @param fs
* the file system abstraction which will be necessary to
* perform certain file system operations.
* @return new session instance, but otherwise unconfigured.
* @throws JSchException
* the session could not be created.
*/
protected Session createSession(final OpenSshConfig.Host hc,
final String user, final String host, final int port, FS fs)
throws JSchException {
return getJSch(hc, fs).getSession(user, host, port);
}
/**
* Provide additional configuration for the JSch instance. This method could
* be overridden to supply a preferred
* {@link com.jcraft.jsch.IdentityRepository}.
*
* @param jsch
* jsch instance
* @since 4.5
*/
protected void configureJSch(JSch jsch) {
// No additional configuration required.
}
/**
* Provide additional configuration for the session based on the host
* information. This method could be used to supply {@link UserInfo}.
*
* @param hc
* host configuration
* @param session
* session to configure
*/
protected abstract void configure(OpenSshConfig.Host hc, Session session);
/**
* Obtain the JSch used to create new sessions.
*
* @param hc
* host configuration
* @param fs
* the file system abstraction which will be necessary to
* perform certain file system operations.
* @return the JSch instance to use.
* @throws JSchException
* the user configuration could not be created.
*/
protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
if (defaultJSch == null) {
defaultJSch = createDefaultJSch(fs);
for (Object name : defaultJSch.getIdentityNames())
byIdentityFile.put((String) name, defaultJSch);
}
final File identityFile = hc.getIdentityFile();
if (identityFile == null)
return defaultJSch;
final String identityKey = identityFile.getAbsolutePath();
JSch jsch = byIdentityFile.get(identityKey);
if (jsch == null) {
jsch = new JSch();
configureJSch(jsch);
jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository());
jsch.addIdentity(identityKey);
byIdentityFile.put(identityKey, jsch);
}
return jsch;
}
/**
* @param fs
* the file system abstraction which will be necessary to
* perform certain file system operations.
* @return the new default JSch implementation.
* @throws JSchException
* known host keys cannot be loaded.
*/
protected JSch createDefaultJSch(FS fs) throws JSchException {
final JSch jsch = new JSch();
configureJSch(jsch);
knownHosts(jsch, fs);
identities(jsch, fs);
return jsch;
}
private static void knownHosts(final JSch sch, FS fs) throws JSchException {
final File home = fs.userHome();
if (home == null)
return;
final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$
try {
final FileInputStream in = new FileInputStream(known_hosts);
try {
sch.setKnownHosts(in);
} finally {
in.close();
}
} catch (FileNotFoundException none) {
// Oh well. They don't have a known hosts in home.
} catch (IOException err) {
// Oh well. They don't have a known hosts in home.
}
}
private static void identities(final JSch sch, FS fs) {
final File home = fs.userHome();
if (home == null)
return;
final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$
if (sshdir.isDirectory()) {
loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$
loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$
loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$
}
}
private static void loadIdentity(final JSch sch, final File priv) {
if (priv.isFile()) {
try {
sch.addIdentity(priv.getAbsolutePath());
} catch (JSchException e) {
// Instead, pretend the key doesn't exist.
}
}
}
}