/* * Copyright (C) 2018, Sasa Zivkov * Copyright (C) 2016, Mark Ingram * Copyright (C) 2009, Constantine Plotnikov * Copyright (C) 2008-2009, Google Inc. * Copyright (C) 2009, Google, Inc. * Copyright (C) 2009, JetBrains s.r.o. * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport.ssh.jsch; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.ConnectException; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.ssh.jsch.CredentialsProviderUserInfo; import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.RemoteSession; import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jcraft.jsch.ConfigRepository; import com.jcraft.jsch.ConfigRepository.Config; import com.jcraft.jsch.HostKey; import com.jcraft.jsch.HostKeyRepository; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; /** * The base session factory that loads known hosts and private keys from * $HOME/.ssh. *

* 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. *

* The factory does not provide UI behavior. Override the method * {@link #configure(org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig.Host, Session)} * to supply appropriate {@link com.jcraft.jsch.UserInfo} to the session. * * @since 6.0 */ public class JschConfigSessionFactory extends SshSessionFactory { private static final String JSCH = "jsch"; //$NON-NLS-1$ private static final Logger LOG = LoggerFactory .getLogger(JschConfigSessionFactory.class); /** * We use different Jsch instances for hosts that have an IdentityFile * configured in ~/.ssh/config. Jsch by default would cache decrypted keys * only per session, which results in repeated password prompts. Using * different Jsch instances, we can cache the keys on these instances so * that they will be re-used for successive sessions, and thus the user is * prompted for a key password only once while Eclipse runs. */ private final Map 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); 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( JSchText.get().transportSSHRetryInterrupt, e1); } } } } return new JschSession(session, uri); } catch (JSchException je) { final Throwable c = je.getCause(); if (c instanceof UnknownHostException) { throw new TransportException(uri, JSchText.get().unknownHost, je); } if (c instanceof ConnectException) { throw new TransportException(uri, c.getMessage(), je); } throw new TransportException(uri, je.getMessage(), je); } } @Override public String getType() { return JSCH; } 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$ } /** * Use for tests only * * @param credentialsProvider * credentials provide * @param fs * FS object to use * @param user * user * @param pass * password * @param host * host name * @param port * port number * @param hc * host config * @return session the session * @throws JSchException * jsch failed */ public 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); // Jsch will have overridden the explicit user by the one from the SSH // config file... setUserName(session, user); // Jsch will also have overridden the port. if (port > 0 && port != session.getPort()) { session.setPort(port); } // 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)); } safeConfig(session, hc.getConfig()); if (hc.getConfig().getValue("HostKeyAlgorithms") == null) { //$NON-NLS-1$ setPreferredKeyTypesOrder(session); } configure(hc, session); return session; } private void safeConfig(Session session, Config cfg) { // Ensure that Jsch checks all configured algorithms, not just its // built-in ones. Otherwise it may propose an algorithm for which it // doesn't have an implementation, and then run into an NPE if that // algorithm ends up being chosen. copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$ copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$ copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$ "CheckSignatures"); //$NON-NLS-1$ } private static void setPreferredKeyTypesOrder(Session session) { HostKeyRepository hkr = session.getHostKeyRepository(); HostKey[] hostKeys = hkr.getHostKey(hostName(session), null); if (hostKeys == null) { return; } List known = Stream.of(hostKeys) .map(HostKey::getType) .collect(toList()); if (!known.isEmpty()) { String serverHostKey = "server_host_key"; //$NON-NLS-1$ String current = session.getConfig(serverHostKey); if (current == null) { session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$ return; } String knownFirst = Stream.concat( known.stream(), Stream.of(current.split(",")) //$NON-NLS-1$ .filter(s -> !known.contains(s))) .collect(joining(",")); //$NON-NLS-1$ session.setConfig(serverHostKey, knownFirst); } } private static String hostName(Session s) { if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) { return s.getHost(); } return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$ Integer.valueOf(s.getPort())); } private void copyConfigValueToSession(Session session, Config cfg, String from, String to) { String value = cfg.getValue(from); if (value != null) { session.setConfig(to, value); } } private void setUserName(Session session, String userName) { // Jsch 0.1.54 picks up the user name from the ssh config, even if an // explicit user name was given! We must correct that if ~/.ssh/config // has a different user name. if (userName == null || userName.isEmpty() || userName.equals(session.getUserName())) { return; } try { Class[] parameterTypes = { String.class }; Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$ parameterTypes); method.setAccessible(true); method.invoke(session, userName); } catch (NullPointerException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { LOG.error(MessageFormat.format(JSchText.get().sshUserNameError, userName, session.getUserName()), e); } } /** * 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 com.jcraft.jsch.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 com.jcraft.jsch.UserInfo}. * * @param hc * host configuration * @param session * session to configure */ protected void configure(OpenSshConfig.Host hc, Session session) { // No additional configuration required. } /** * 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 com.jcraft.jsch.JSchException * the user configuration could not be created. */ protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException { if (defaultJSch == null) { defaultJSch = createDefaultJSch(fs); if (defaultJSch.getConfigRepository() == null) { defaultJSch.setConfigRepository( new JschBugFixingConfigRepository(config)); } 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); if (jsch.getConfigRepository() == null) { jsch.setConfigRepository(defaultJSch.getConfigRepository()); } jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository()); jsch.addIdentity(identityKey); byIdentityFile.put(identityKey, jsch); } return jsch; } /** * Create default instance of jsch * * @param fs * the file system abstraction which will be necessary to perform * certain file system operations. * @return the new default JSch implementation. * @throws com.jcraft.jsch.JSchException * known host keys cannot be loaded. */ protected JSch createDefaultJSch(FS fs) throws JSchException { final JSch jsch = new JSch(); // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=537790 and // https://bugs.eclipse.org/bugs/show_bug.cgi?id=576604 copyGlobalConfigIfNotSet("signature.rsa", "ssh-rsa"); //$NON-NLS-1$ //$NON-NLS-2$ copyGlobalConfigIfNotSet("signature.dss", "ssh-dss"); //$NON-NLS-1$ //$NON-NLS-2$ configureJSch(jsch); knownHosts(jsch, fs); identities(jsch, fs); return jsch; } private void copyGlobalConfigIfNotSet(String from, String to) { String toValue = JSch.getConfig(to); if (StringUtils.isEmptyOrNull(toValue)) { String fromValue = JSch.getConfig(from); if (!StringUtils.isEmptyOrNull(fromValue)) { JSch.setConfig(to, fromValue); } } } private static void knownHosts(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 (FileInputStream in = new FileInputStream(known_hosts)) { sch.setKnownHosts(in); } 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(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(JSch sch, File priv) { if (priv.isFile()) { try { sch.addIdentity(priv.getAbsolutePath()); } catch (JSchException e) { // Instead, pretend the key doesn't exist. } } } private static class JschBugFixingConfigRepository implements ConfigRepository { private final ConfigRepository base; public JschBugFixingConfigRepository(ConfigRepository base) { this.base = base; } @Override public Config getConfig(String host) { return new JschBugFixingConfig(base.getConfig(host)); } /** * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms * some values from the config file into the format Jsch 0.1.54 expects. * This is a work-around for bugs in Jsch. *

* Additionally, this config hides the IdentityFile config entries from * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords * (or rather, decrypted keys) only for a single session, resulting in * multiple password prompts for user operations that use several Jsch * sessions. */ private static class JschBugFixingConfig implements Config { private static final String[] NO_IDENTITIES = {}; private final Config real; public JschBugFixingConfig(Config delegate) { real = delegate; } @Override public String getHostname() { return real.getHostname(); } @Override public String getUser() { return real.getUser(); } @Override public int getPort() { return real.getPort(); } @Override public String getValue(String key) { String k = key.toUpperCase(Locale.ROOT); if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ return null; } String result = real.getValue(key); if (result != null) { if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$ || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$ // These values are in seconds. Jsch 0.1.54 passes them // on as is to java.net.Socket.setSoTimeout(), which // expects milliseconds. So convert here to // milliseconds. try { int timeout = Integer.parseInt(result); result = Long.toString( TimeUnit.SECONDS.toMillis(timeout)); } catch (NumberFormatException e) { // Ignore } } } return result; } @Override public String[] getValues(String key) { String k = key.toUpperCase(Locale.ROOT); if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ return NO_IDENTITIES; } return real.getValues(key); } } } /** * Set the {@link OpenSshConfig} to use. Intended for use in tests. * * @param config * to use */ public synchronized void setConfig(OpenSshConfig config) { this.config = config; } } X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Length: 9750 Content-Disposition: inline; filename="JschSession.java" Last-Modified: Mon, 14 Jul 2025 22:30:24 GMT Expires: Mon, 14 Jul 2025 22:35:24 GMT ETag: "ad58ae1c8e2bf211f75b0c20752a90953aeec80a" /* * Copyright (C) 2009, Constantine Plotnikov * Copyright (C) 2008-2009, Google Inc. * Copyright (C) 2009, Google, Inc. * Copyright (C) 2009, JetBrains s.r.o. * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport.ssh.jsch; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText; import org.eclipse.jgit.transport.FtpChannel; import org.eclipse.jgit.transport.RemoteSession2; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.io.IsolatedOutputStream; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpException; /** * Run remote commands using Jsch. *

* This class is the default session implementation using Jsch. Note that * {@link org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory} is used to * create the actual session passed to the constructor. * * @since 6.0 */ public class JschSession implements RemoteSession2 { final Session sock; final URIish uri; /** * Create a new session object by passing the real Jsch session and the URI * information. * * @param session * the real Jsch session created elsewhere. * @param uri * the URI information for the remote connection */ public JschSession(Session session, URIish uri) { sock = session; this.uri = uri; } @Override public Process exec(String command, int timeout) throws IOException { return exec(command, Collections.emptyMap(), timeout); } @Override public Process exec(String command, Map environment, int timeout) throws IOException { return new JschProcess(command, environment, timeout); } @Override public void disconnect() { if (sock.isConnected()) sock.disconnect(); } /** * {@inheritDoc} * * @since 5.2 */ @Override public FtpChannel getFtpChannel() { return new JschFtpChannel(); } /** * Implementation of Process for running a single command using Jsch. *

* Uses the Jsch session to do actual command execution and manage the * execution. */ private class JschProcess extends Process { private ChannelExec channel; final int timeout; private InputStream inputStream; private OutputStream outputStream; private InputStream errStream; /** * Opens a channel on the session ("sock") for executing the given * command, opens streams, and starts command execution. * * @param commandName * the command to execute * @param environment * environment variables to pass on * @param tms * the timeout value, in seconds, for the command. * @throws TransportException * on problems opening a channel or connecting to the remote * host * @throws IOException * on problems opening streams */ JschProcess(String commandName, Map environment, int tms) throws TransportException, IOException { timeout = tms; try { channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$ if (environment != null) { for (Map.Entry envVar : environment .entrySet()) { channel.setEnv(envVar.getKey(), envVar.getValue()); } } channel.setCommand(commandName); setupStreams(); channel.connect(timeout > 0 ? timeout * 1000 : 0); if (!channel.isConnected()) { closeOutputStream(); throw new TransportException(uri, JSchText.get().connectionFailed); } } catch (JSchException e) { closeOutputStream(); throw new TransportException(uri, e.getMessage(), e); } } private void closeOutputStream() { if (outputStream != null) { try { outputStream.close(); } catch (IOException ioe) { // ignore } } } private void setupStreams() throws IOException { inputStream = channel.getInputStream(); // JSch won't let us interrupt writes when we use our InterruptTimer // to break out of a long-running write operation. To work around // that we spawn a background thread to shuttle data through a pipe, // as we can issue an interrupted write out of that. Its slower, so // we only use this route if there is a timeout. OutputStream out = channel.getOutputStream(); if (timeout <= 0) { outputStream = out; } else { IsolatedOutputStream i = new IsolatedOutputStream(out); outputStream = new BufferedOutputStream(i, 16 * 1024); } errStream = channel.getErrStream(); } @Override public InputStream getInputStream() { return inputStream; } @Override public OutputStream getOutputStream() { return outputStream; } @Override public InputStream getErrorStream() { return errStream; } @Override public int exitValue() { if (isRunning()) throw new IllegalThreadStateException(); return channel.getExitStatus(); } private boolean isRunning() { return channel.getExitStatus() < 0 && channel.isConnected(); } @Override public void destroy() { if (channel.isConnected()) channel.disconnect(); closeOutputStream(); } @Override public int waitFor() throws InterruptedException { while (isRunning()) Thread.sleep(100); return exitValue(); } } private class JschFtpChannel implements FtpChannel { private ChannelSftp ftp; @Override public void connect(int timeout, TimeUnit unit) throws IOException { try { ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ ftp.connect((int) unit.toMillis(timeout)); } catch (JSchException e) { ftp = null; throw new IOException(e.getLocalizedMessage(), e); } } @Override public void disconnect() { ftp.disconnect(); ftp = null; } private T map(Callable op) throws IOException { try { return op.call(); } catch (Exception e) { if (e instanceof SftpException) { throw new FtpChannel.FtpException(e.getLocalizedMessage(), ((SftpException) e).id, e); } throw new IOException(e.getLocalizedMessage(), e); } } @Override public boolean isConnected() { return ftp != null && sock.isConnected(); } @Override public void cd(String path) throws IOException { map(() -> { ftp.cd(path); return null; }); } @Override public String pwd() throws IOException { return map(() -> ftp.pwd()); } @Override public Collection ls(String path) throws IOException { return map(() -> { List result = new ArrayList<>(); for (Object e : ftp.ls(path)) { ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; result.add(new DirEntry() { @Override public String getFilename() { return entry.getFilename(); } @Override public long getModifiedTime() { return entry.getAttrs().getMTime(); } @Override public boolean isDirectory() { return entry.getAttrs().isDir(); } }); } return result; }); } @Override public void rmdir(String path) throws IOException { map(() -> { ftp.rm(path); return null; }); } @Override public void mkdir(String path) throws IOException { map(() -> { ftp.mkdir(path); return null; }); } @Override public InputStream get(String path) throws IOException { return map(() -> ftp.get(path)); } @Override public OutputStream put(String path) throws IOException { return map(() -> ftp.put(path)); } @Override public void rm(String path) throws IOException { map(() -> { ftp.rm(path); return null; }); } @Override public void rename(String from, String to) throws IOException { map(() -> { // Plain FTP rename will fail if "to" exists. Jsch knows about // the FTP extension "posix-rename@openssh.com", which will // remove "to" first if it exists. if (hasPosixRename()) { ftp.rename(from, to); } else if (!to.equals(from)) { // Try to remove "to" first. With git, we typically get this // when a lock file is moved over the file locked. Note that // the check for to being equal to from may still fail in // the general case, but for use with JGit's TransportSftp // it should be good enough. delete(to); ftp.rename(from, to); } return null; }); } /** * Determine whether the server has the posix-rename extension. * * @return {@code true} if it is supported, {@code false} otherwise * @see OpenSSH * deviations and extensions to the published SSH protocol * @see stdio.h: * rename() */ private boolean hasPosixRename() { return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ } } } X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Length: 10965 Content-Disposition: inline; filename="OpenSshConfig.java" Last-Modified: Mon, 14 Jul 2025 22:30:24 GMT Expires: Mon, 14 Jul 2025 22:35:24 GMT ETag: "7da2e987a3e94aca7c07c39e0b7330552d60ce42" /* * Copyright (C) 2008, 2018, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport.ssh.jsch; import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive; import java.io.File; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry; import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.util.FS; import com.jcraft.jsch.ConfigRepository; /** * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file. *

* JSch does have its own config file parser * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a * number of problems: *

    *
  • it splits lines of the format "keyword = value" wrongly: you'd end up * with the value "= value". *
  • its "Host" keyword is not case insensitive. *
  • it doesn't handle quoted values. *
  • JSch's OpenSSHConfig doesn't monitor for config file changes. *
*

* This parser makes the critical options available to * {@link org.eclipse.jgit.transport.SshSessionFactory} via * {@link org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig.Host} objects returned * by {@link #lookup(String)}, and implements a fully conforming * {@link com.jcraft.jsch.ConfigRepository} providing * {@link com.jcraft.jsch.ConfigRepository.Config}s via * {@link #getConfig(String)}. *

* * @see OpenSshConfigFile * @since 6.0 */ public class OpenSshConfig implements ConfigRepository { /** * Obtain the user's configuration data. *

* The configuration file is always returned to the caller, even if no file * exists in the user's home directory at the time the call was made. Lookup * requests are cached and are automatically updated if the user modifies * the configuration file since the last time it was cached. * * @param fs * the file system abstraction which will be necessary to * perform certain file system operations. * @return a caching reader of the user's configuration file. */ public static OpenSshConfig get(FS fs) { File home = fs.userHome(); if (home == null) home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ final File config = new File(new File(home, SshConstants.SSH_DIR), SshConstants.CONFIG); return new OpenSshConfig(home, config); } /** The base file. */ private OpenSshConfigFile configFile; /** * Create an OpenSshConfig * * @param h * user's home directory * @param cfg * ssh configuration file */ public OpenSshConfig(File h, File cfg) { configFile = new OpenSshConfigFile(h, cfg, SshSessionFactory.getLocalUserName()); } /** * Locate the configuration for a specific host request. * * @param hostName * the name the user has supplied to the SSH tool. This may be a * real host name, or it may just be a "Host" block in the * configuration file. * @return r configuration for the requested name. Never null. */ public Host lookup(String hostName) { HostEntry entry = configFile.lookup(hostName, -1, null); return new Host(entry, hostName, configFile.getLocalUserName()); } /** * Configuration of one "Host" block in the configuration file. *

* If returned from {@link OpenSshConfig#lookup(String)} some or all of the * properties may not be populated. The properties which are not populated * should be defaulted by the caller. *

* When returned from {@link OpenSshConfig#lookup(String)} any wildcard * entries which appear later in the configuration file will have been * already merged into this block. */ public static class Host { String hostName; int port; File identityFile; String user; String preferredAuthentications; Boolean batchMode; String strictHostKeyChecking; int connectionAttempts; private HostEntry entry; private Config config; // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys // to ssh-config keys. private static final Map KEY_MAP = new TreeMap<>( String.CASE_INSENSITIVE_ORDER); static { KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$ KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$ KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$ KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$ KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$ KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$ KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$ KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$ KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$ SshConstants.NUMBER_OF_PASSWORD_PROMPTS); } private static String mapKey(String key) { String k = KEY_MAP.get(key); return k != null ? k : key; } /** * Creates a new uninitialized {@link Host}. */ public Host() { // For API backwards compatibility with pre-4.9 JGit } Host(HostEntry entry, String hostName, String localUserName) { this.entry = entry; complete(hostName, localUserName); } /** * Get the value StrictHostKeyChecking property * * @return the value StrictHostKeyChecking property, the valid values * are "yes" (unknown hosts are not accepted), "no" (unknown * hosts are always accepted), and "ask" (user should be asked * before accepting the host) */ public String getStrictHostKeyChecking() { return strictHostKeyChecking; } /** * Get hostname * * @return the real IP address or host name to connect to; never null. */ public String getHostName() { return hostName; } /** * Get port * * @return the real port number to connect to; never 0. */ public int getPort() { return port; } /** * Get identity file * * @return path of the private key file to use for authentication; null * if the caller should use default authentication strategies. */ public File getIdentityFile() { return identityFile; } /** * Get user * * @return the real user name to connect as; never null. */ public String getUser() { return user; } /** * Get preferred authentication methods * * @return the preferred authentication methods, separated by commas if * more than one authentication method is preferred. */ public String getPreferredAuthentications() { return preferredAuthentications; } /** * Whether batch mode is preferred * * @return true if batch (non-interactive) mode is preferred for this * host connection. */ public boolean isBatchMode() { return batchMode != null && batchMode.booleanValue(); } /** * Get connection attempts * * @return the number of tries (one per second) to connect before * exiting. The argument must be an integer. This may be useful * in scripts if the connection sometimes fails. The default is * 1. * @since 3.4 */ public int getConnectionAttempts() { return connectionAttempts; } private void complete(String initialHostName, String localUserName) { // Try to set values from the options. hostName = entry.getValue(SshConstants.HOST_NAME); user = entry.getValue(SshConstants.USER); port = positive(entry.getValue(SshConstants.PORT)); connectionAttempts = positive( entry.getValue(SshConstants.CONNECTION_ATTEMPTS)); strictHostKeyChecking = entry .getValue(SshConstants.STRICT_HOST_KEY_CHECKING); batchMode = Boolean.valueOf(OpenSshConfigFile .flag(entry.getValue(SshConstants.BATCH_MODE))); preferredAuthentications = entry .getValue(SshConstants.PREFERRED_AUTHENTICATIONS); // Fill in defaults if still not set if (hostName == null || hostName.isEmpty()) { hostName = initialHostName; } if (user == null || user.isEmpty()) { user = localUserName; } if (port <= 0) { port = SshConstants.SSH_DEFAULT_PORT; } if (connectionAttempts <= 0) { connectionAttempts = 1; } List identityFiles = entry .getValues(SshConstants.IDENTITY_FILE); if (identityFiles != null && !identityFiles.isEmpty()) { identityFile = new File(identityFiles.get(0)); } } /** * Get the ssh configuration * * @return the ssh configuration */ public Config getConfig() { if (config == null) { config = new Config() { @Override public String getHostname() { return Host.this.getHostName(); } @Override public String getUser() { return Host.this.getUser(); } @Override public int getPort() { return Host.this.getPort(); } @Override public String getValue(String key) { // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() // for this special case. if (key.equals("compression.s2c") //$NON-NLS-1$ || key.equals("compression.c2s")) { //$NON-NLS-1$ if (!OpenSshConfigFile.flag( Host.this.entry.getValue(mapKey(key)))) { return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ } return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ } return Host.this.entry.getValue(mapKey(key)); } @Override public String[] getValues(String key) { List values = Host.this.entry .getValues(mapKey(key)); if (values == null) { return new String[0]; } return values.toArray(new String[0]); } }; } return config; } @Override @SuppressWarnings("nls") public String toString() { return "Host [hostName=" + hostName + ", port=" + port + ", identityFile=" + identityFile + ", user=" + user + ", preferredAuthentications=" + preferredAuthentications + ", batchMode=" + batchMode + ", strictHostKeyChecking=" + strictHostKeyChecking + ", connectionAttempts=" + connectionAttempts + ", entry=" + entry + "]"; } } /** * {@inheritDoc} *

* Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config} * for the given host name. Should be called only by Jsch and tests. * * @since 4.9 */ @Override public Config getConfig(String hostName) { Host host = lookup(hostName); return host.getConfig(); } @Override public String toString() { return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$ } } X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Length: 558 Content-Disposition: inline; filename="package-info.java" Last-Modified: Mon, 14 Jul 2025 22:30:24 GMT Expires: Mon, 14 Jul 2025 22:35:24 GMT ETag: "dc2915a09f2459dbfc1c89ba02982c45a73fe714" /** * Provides a JGit {@link org.eclipse.jgit.transport.SshSessionFactory} * implemented via JSch. *

* This package should be considered deprecated. It is essentially * unmaintained and the JGit project may decide to remove it completely without * further ado at any time. *

*

* The officially supported Java SSH implementation for JGit is in bundle * {@code org.eclipse.jgit.ssh.apache} and is built upon * Apache MINA sshd. */ package org.eclipse.jgit.transport.ssh.jsch;