/* * Copyright (c) 2010 Red Hat, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * <http://www.gnu.org/licenses>. */ package org.commonjava.sshwrap.config; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; 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.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; /** * Forked from git://egit.eclipse.org/jgit.git@94207f0a43a44261b8170d3cdba3028059775d9d 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 DefaultSSHConfiguration implements SSHConfiguration { /** IANA assigned port number for SSH. */ static final int SSH_PORT = 22; private final Set<File> privateKeys; private final File configFile; private final File knownHosts; /** Cached entries read out of the configuration file. */ private Map<String, Host> hosts; private byte[] knownHostsBuffer; /** * 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. * </p> * <p> * Uses ${user.home}/.ssh/config as the configuration file. * </p> */ public DefaultSSHConfiguration() { this( new File( userHome(), ".ssh" ).getAbsoluteFile() ); } /** * 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. * </p> * * @param sshDir The base directory where all SSH configurations are housed. */ public DefaultSSHConfiguration( final File sshDir ) { configFile = new File( sshDir, "config" ); knownHosts = new File( sshDir, "known_hosts" ); hosts = parseHosts(); privateKeys = initPrivateKeys( sshDir ); } /** * 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. * </p> * * @param sshDir The base directory where all SSH configurations are housed. */ public DefaultSSHConfiguration( final File config, final File knownHosts, final File... identities ) { this.configFile = config; this.knownHosts = knownHosts; this.privateKeys = new HashSet<File>( Arrays.asList( identities ) ); hosts = parseHosts(); } @Override public Set<File> getIdentities() { return privateKeys; } @Override public synchronized InputStream getKnownHosts() throws IOException { if ( knownHostsBuffer == null ) { if ( knownHosts != null && knownHosts.exists() && knownHosts.canRead() ) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileInputStream fis = null; try { fis = new FileInputStream( knownHosts ); IOUtils.copy( fis, baos ); } finally { IOUtils.closeQuietly( fis ); } knownHostsBuffer = baos.toByteArray(); } else { knownHostsBuffer = new byte[0]; } } return new ByteArrayInputStream( knownHostsBuffer ); } /** * {@inheritDoc} * * @see org.commonjava.sshwrap.config.SSHConfiguration#lookup(java.lang.String) */ @Override public Host lookup( final String hostName ) { boolean isNew = false; Host h = hosts.get( hostName ); if ( h == null ) { isNew = true; h = new Host(); } if ( h.isPatternsApplied() ) { return h; } if ( h.getHostName() == null ) { h.setHostName( hostName ); } if ( h.getUser() == null ) { h.setUser( userName() ); } if ( h.getPort() < 1 ) { h.setPort( SSH_PORT ); } h.setPatternsApplied( true ); if ( isNew ) { hosts.put( hostName, h ); } return h; } private synchronized Map<String, Host> parseHosts() { if ( configFile.exists() && configFile.canRead() ) { FileInputStream in = null; try { in = new FileInputStream( configFile ); hosts = parse( in ); } catch ( final IOException err ) { hosts = Collections.emptyMap(); } finally { IOUtils.closeQuietly( in ); } } return hosts; } private Map<String, Host> parse( final InputStream in ) throws IOException { final Map<String, Host> m = new LinkedHashMap<String, Host>(); final BufferedReader br = new BufferedReader( new InputStreamReader( in ) ); final List<Host> current = new ArrayList<Host>( 4 ); String line; while ( ( line = br.readLine() ) != null ) { line = line.trim(); if ( line.length() == 0 || line.startsWith( "#" ) ) { continue; } final String[] parts = line.split( "[ \t]*[= \t]", 2 ); final String keyword = parts[0].trim(); final String argValue = parts[1].trim(); if ( StringUtils.equalsIgnoreCase( "Host", keyword ) ) { current.clear(); for ( final String pattern : argValue.split( "[ \t]" ) ) { 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 ) ) { for ( final Host c : current ) { if ( c.getHostName() == null ) { c.setHostName( dequote( argValue ) ); } } } else if ( StringUtils.equalsIgnoreCase( "User", keyword ) ) { for ( final Host c : current ) { if ( c.getUser() == null ) { c.setUser( dequote( argValue ) ); } } } else if ( StringUtils.equalsIgnoreCase( "Port", keyword ) ) { try { final int port = Integer.parseInt( dequote( argValue ) ); for ( final Host c : current ) { if ( c.getPort() < 1 ) { c.setPort( port ); } } } catch ( final NumberFormatException nfe ) { // Bad port number. Don't set it. } } else if ( StringUtils.equalsIgnoreCase( "IdentityFile", keyword ) ) { for ( final Host c : current ) { if ( c.getIdentityFile() == null ) { c.setIdentityFile( toFile( dequote( argValue ) ) ); } } } else if ( StringUtils.equalsIgnoreCase( "PreferredAuthentications", keyword ) ) { for ( final Host c : current ) { if ( c.getPreferredAuthentications() == null ) { c.setPreferredAuthentications( nows( dequote( argValue ) ) ); } } } else if ( StringUtils.equalsIgnoreCase( "BatchMode", keyword ) ) { for ( final Host c : current ) { if ( c.getBatchMode() == null ) { c.setBatchMode( yesno( dequote( argValue ) ) ); } } } else if ( StringUtils.equalsIgnoreCase( "StrictHostKeyChecking", keyword ) ) { final String value = dequote( argValue ); for ( final Host c : current ) { if ( c.getStrictHostKeyChecking() == null ) { c.setStrictHostKeyChecking( value ); } } } else if ( StringUtils.equalsIgnoreCase( "LocalForward", keyword ) ) { final String[] argParts = argValue.split( ":" ); LocalForward lf = null; if ( argParts.length > 3 ) { lf = new LocalForward( argParts[0], Integer.parseInt( argParts[1] ), argParts[2], Integer.parseInt( argParts[3] ) ); } else if ( argParts.length > 2 ) { lf = new LocalForward( Integer.parseInt( argParts[0] ), argParts[1], Integer.parseInt( argParts[2] ) ); } if ( lf != null ) { for ( final Host host : current ) { host.addLocalForward( lf ); } } } else if ( StringUtils.equalsIgnoreCase( "RemoteForward", keyword ) ) { final String[] argParts = argValue.split( ":" ); RemoteForward rf = null; if ( argParts.length > 3 ) { rf = new RemoteForward( argParts[0], Integer.parseInt( argParts[1] ), argParts[2], Integer.parseInt( argParts[3] ) ); } else if ( argParts.length > 2 ) { rf = new RemoteForward( Integer.parseInt( argParts[0] ), argParts[1], Integer.parseInt( argParts[2] ) ); } if ( rf != null ) { for ( final Host host : current ) { host.addRemoteForward( rf ); } } } } return m; } private static String dequote( final String value ) { if ( value.startsWith( "\"" ) && value.endsWith( "\"" ) ) { 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 ) ) { return Boolean.TRUE; } return Boolean.FALSE; } private File toFile( final String path ) { if ( path.startsWith( "~/" ) ) { return new File( userHome(), path.substring( 2 ) ); } final File ret = new File( path ); if ( ret.isAbsolute() ) { return ret; } return new File( userHome(), path ); } private Set<File> initPrivateKeys( final File sshDir ) { final Set<File> privateKeys = new HashSet<File>(); privateKeys.add( new File( sshDir, "identity" ) ); privateKeys.add( new File( sshDir, "id_rsa" ) ); privateKeys.add( new File( sshDir, "id_dsa" ) ); validatePrivateKeys(); return privateKeys; } private void validatePrivateKeys() { for ( final Iterator<File> it = privateKeys.iterator(); it.hasNext(); ) { final File file = it.next(); if ( !file.canRead() ) { it.remove(); } } } static String userName() { return AccessController.doPrivileged( new PrivilegedAction<String>() { @Override public String run() { return System.getProperty( "user.name" ); } } ); } static String userHome() { return AccessController.doPrivileged( new PrivilegedAction<String>() { @Override public String run() { return System.getProperty( "user.home" ); } } ); } }