| /* |
| * Copyright (C) 2008, 2014, Google Inc. |
| * 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.BufferedReader; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.jgit.errors.InvalidPatternException; |
| import org.eclipse.jgit.fnmatch.FileNameMatcher; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.StringUtils; |
| |
| /** |
| * Simple configuration parser for the OpenSSH ~/.ssh/config file. |
| * <p> |
| * Since JSch does not (currently) have the ability to parse an OpenSSH |
| * configuration file this is a simple parser to read that file and make the |
| * critical options available to {@link SshSessionFactory}. |
| */ |
| public class OpenSshConfig { |
| /** IANA assigned port number for SSH. */ |
| static final int SSH_PORT = 22; |
| |
| /** |
| * Obtain the user's configuration data. |
| * <p> |
| * 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, ".ssh"), Constants.CONFIG); //$NON-NLS-1$ |
| final OpenSshConfig osc = new OpenSshConfig(home, config); |
| osc.refresh(); |
| return osc; |
| } |
| |
| /** The user's home directory, as key files may be relative to here. */ |
| private final File home; |
| |
| /** The .ssh/config file we read and monitor for updates. */ |
| private final File configFile; |
| |
| /** Modification time of {@link #configFile} when {@link #hosts} loaded. */ |
| private long lastModified; |
| |
| /** Cached entries read out of the configuration file. */ |
| private Map<String, Host> hosts; |
| |
| OpenSshConfig(final File h, final File cfg) { |
| home = h; |
| configFile = cfg; |
| hosts = Collections.emptyMap(); |
| } |
| |
| /** |
| * 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(final String hostName) { |
| final Map<String, Host> cache = refresh(); |
| Host h = cache.get(hostName); |
| if (h == null) |
| h = new Host(); |
| if (h.patternsApplied) |
| return h; |
| |
| for (final Map.Entry<String, Host> e : cache.entrySet()) { |
| if (!isHostPattern(e.getKey())) |
| continue; |
| if (!isHostMatch(e.getKey(), hostName)) |
| continue; |
| h.copyFrom(e.getValue()); |
| } |
| |
| if (h.hostName == null) |
| h.hostName = hostName; |
| if (h.user == null) |
| h.user = OpenSshConfig.userName(); |
| if (h.port == 0) |
| h.port = OpenSshConfig.SSH_PORT; |
| if (h.connectionAttempts == 0) |
| h.connectionAttempts = 1; |
| h.patternsApplied = true; |
| return h; |
| } |
| |
| private synchronized Map<String, Host> refresh() { |
| final long mtime = configFile.lastModified(); |
| if (mtime != lastModified) { |
| try { |
| final FileInputStream in = new FileInputStream(configFile); |
| try { |
| hosts = parse(in); |
| } finally { |
| in.close(); |
| } |
| } catch (FileNotFoundException none) { |
| hosts = Collections.emptyMap(); |
| } catch (IOException err) { |
| hosts = Collections.emptyMap(); |
| } |
| lastModified = mtime; |
| } |
| return hosts; |
| } |
| |
| private Map<String, Host> parse(final InputStream in) throws IOException { |
| final Map<String, Host> m = new LinkedHashMap<>(); |
| final BufferedReader br = new BufferedReader(new InputStreamReader(in)); |
| final List<Host> current = new ArrayList<>(4); |
| String line; |
| |
| while ((line = br.readLine()) != null) { |
| line = line.trim(); |
| if (line.length() == 0 || line.startsWith("#")) //$NON-NLS-1$ |
| continue; |
| |
| final String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$ |
| final String keyword = parts[0].trim(); |
| final String argValue = parts[1].trim(); |
| |
| if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$ |
| current.clear(); |
| for (final String pattern : argValue.split("[ \t]")) { //$NON-NLS-1$ |
| final String name = dequote(pattern); |
| Host c = m.get(name); |
| if (c == null) { |
| c = new Host(); |
| m.put(name, c); |
| } |
| current.add(c); |
| } |
| continue; |
| } |
| |
| if (current.isEmpty()) { |
| // We received an option outside of a Host block. We |
| // don't know who this should match against, so skip. |
| // |
| continue; |
| } |
| |
| if (StringUtils.equalsIgnoreCase("HostName", keyword)) { //$NON-NLS-1$ |
| for (final Host c : current) |
| if (c.hostName == null) |
| c.hostName = dequote(argValue); |
| } else if (StringUtils.equalsIgnoreCase("User", keyword)) { //$NON-NLS-1$ |
| for (final Host c : current) |
| if (c.user == null) |
| c.user = dequote(argValue); |
| } else if (StringUtils.equalsIgnoreCase("Port", keyword)) { //$NON-NLS-1$ |
| try { |
| final int port = Integer.parseInt(dequote(argValue)); |
| for (final Host c : current) |
| if (c.port == 0) |
| c.port = port; |
| } catch (NumberFormatException nfe) { |
| // Bad port number. Don't set it. |
| } |
| } else if (StringUtils.equalsIgnoreCase("IdentityFile", keyword)) { //$NON-NLS-1$ |
| for (final Host c : current) |
| if (c.identityFile == null) |
| c.identityFile = toFile(dequote(argValue)); |
| } else if (StringUtils.equalsIgnoreCase( |
| "PreferredAuthentications", keyword)) { //$NON-NLS-1$ |
| for (final Host c : current) |
| if (c.preferredAuthentications == null) |
| c.preferredAuthentications = nows(dequote(argValue)); |
| } else if (StringUtils.equalsIgnoreCase("BatchMode", keyword)) { //$NON-NLS-1$ |
| for (final Host c : current) |
| if (c.batchMode == null) |
| c.batchMode = yesno(dequote(argValue)); |
| } else if (StringUtils.equalsIgnoreCase( |
| "StrictHostKeyChecking", keyword)) { //$NON-NLS-1$ |
| String value = dequote(argValue); |
| for (final Host c : current) |
| if (c.strictHostKeyChecking == null) |
| c.strictHostKeyChecking = value; |
| } else if (StringUtils.equalsIgnoreCase( |
| "ConnectionAttempts", keyword)) { //$NON-NLS-1$ |
| try { |
| final int connectionAttempts = Integer.parseInt(dequote(argValue)); |
| if (connectionAttempts > 0) { |
| for (final Host c : current) |
| if (c.connectionAttempts == 0) |
| c.connectionAttempts = connectionAttempts; |
| } |
| } catch (NumberFormatException nfe) { |
| // ignore bad values |
| } |
| } |
| } |
| |
| return m; |
| } |
| |
| private static boolean isHostPattern(final String s) { |
| return s.indexOf('*') >= 0 || s.indexOf('?') >= 0; |
| } |
| |
| private static boolean isHostMatch(final String pattern, final String name) { |
| final FileNameMatcher fn; |
| try { |
| fn = new FileNameMatcher(pattern, null); |
| } catch (InvalidPatternException e) { |
| return false; |
| } |
| fn.append(name); |
| return fn.isMatch(); |
| } |
| |
| private static String dequote(final String value) { |
| if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$ //$NON-NLS-2$ |
| return value.substring(1, value.length() - 1); |
| return value; |
| } |
| |
| private static String nows(final String value) { |
| final StringBuilder b = new StringBuilder(); |
| for (int i = 0; i < value.length(); i++) { |
| if (!Character.isSpaceChar(value.charAt(i))) |
| b.append(value.charAt(i)); |
| } |
| return b.toString(); |
| } |
| |
| private static Boolean yesno(final String value) { |
| if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$ |
| return Boolean.TRUE; |
| return Boolean.FALSE; |
| } |
| |
| private File toFile(final String path) { |
| if (path.startsWith("~/")) //$NON-NLS-1$ |
| return new File(home, path.substring(2)); |
| File ret = new File(path); |
| if (ret.isAbsolute()) |
| return ret; |
| return new File(home, path); |
| } |
| |
| static String userName() { |
| return AccessController.doPrivileged(new PrivilegedAction<String>() { |
| @Override |
| public String run() { |
| return System.getProperty("user.name"); //$NON-NLS-1$ |
| } |
| }); |
| } |
| |
| /** |
| * Configuration of one "Host" block in the configuration file. |
| * <p> |
| * 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. |
| * <p> |
| * 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 { |
| boolean patternsApplied; |
| |
| String hostName; |
| |
| int port; |
| |
| File identityFile; |
| |
| String user; |
| |
| String preferredAuthentications; |
| |
| Boolean batchMode; |
| |
| String strictHostKeyChecking; |
| |
| int connectionAttempts; |
| |
| void copyFrom(final Host src) { |
| if (hostName == null) |
| hostName = src.hostName; |
| if (port == 0) |
| port = src.port; |
| if (identityFile == null) |
| identityFile = src.identityFile; |
| if (user == null) |
| user = src.user; |
| if (preferredAuthentications == null) |
| preferredAuthentications = src.preferredAuthentications; |
| if (batchMode == null) |
| batchMode = src.batchMode; |
| if (strictHostKeyChecking == null) |
| strictHostKeyChecking = src.strictHostKeyChecking; |
| if (connectionAttempts == 0) |
| connectionAttempts = src.connectionAttempts; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @return the real IP address or host name to connect to; never null. |
| */ |
| public String getHostName() { |
| return hostName; |
| } |
| |
| /** |
| * @return the real port number to connect to; never 0. |
| */ |
| public int getPort() { |
| return port; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @return the real user name to connect as; never null. |
| */ |
| public String getUser() { |
| return user; |
| } |
| |
| /** |
| * @return the preferred authentication methods, separated by commas if |
| * more than one authentication method is preferred. |
| */ |
| public String getPreferredAuthentications() { |
| return preferredAuthentications; |
| } |
| |
| /** |
| * @return true if batch (non-interactive) mode is preferred for this |
| * host connection. |
| */ |
| public boolean isBatchMode() { |
| return batchMode != null && batchMode.booleanValue(); |
| } |
| |
| /** |
| * @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; |
| } |
| } |
| } |