/*
* 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" );
}
} );
}
}