@ -1,5 +1,5 @@
/ *
* Copyright ( C ) 2008 , 2014 , Google Inc .
* Copyright ( C ) 2008 , 2017 , Google Inc .
* and other copyright owners as documented in the project ' s IP log .
*
* This program and the accompanying materials are made available
@ -46,17 +46,19 @@ 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.HashMap ;
import java.util.HashSet ;
import java.util.LinkedHashMap ;
import java.util.List ;
import java.util.Locale ;
import java.util.Map ;
import java.util.Set ;
import org.eclipse.jgit.errors.InvalidPatternException ;
import org.eclipse.jgit.fnmatch.FileNameMatcher ;
@ -64,14 +66,46 @@ import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.FS ;
import org.eclipse.jgit.util.StringUtils ;
import com.jcraft.jsch.ConfigRepository ;
/ * *
* Simple configuration parser for the OpenSSH ~ / . ssh / config file .
* Fairly complete configuration parser for the OpenSSH ~ / . ssh / config file .
* < p >
* 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 :
* < ul >
* < li > it splits lines of the format "keyword = value" wrongly : you ' d end up
* with the value "= value" .
* < li > its "Host" keyword is not case insensitive .
* < li > it doesn ' t handle quoted values .
* < li > JSch ' s OpenSSHConfig doesn ' t monitor for config file changes .
* < / ul >
* < p >
* Therefore implement our own parser to read an OpenSSH configuration file . It
* makes the critical options available to { @link SshSessionFactory } via
* { @link Host } objects returned by { @link # lookup ( String ) } , and implements a
* fully conforming { @link ConfigRepository } providing
* { @link com . jcraft . jsch . ConfigRepository . Config } s via
* { @link # getConfig ( String ) } .
* < / p >
* < 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 } .
* Limitations compared to the full OpenSSH 7 . 5 parser :
* < / p >
* < ul >
* < li > This parser does not handle Match or Include keywords .
* < li > This parser does not do % - substitutions .
* < li > This parser does not do host name canonicalization ( Jsch ignores it
* anyway ) .
* < / ul >
* Note that OpenSSH ' s readconf . c is a validating parser ; Jsch ' s
* ConfigRepository OTOH treats all option values as plain strings , so any
* validation must happen in Jsch outside of the parser . Thus this parser does
* not validate option values , except for a few options when constructing a
* { @link Host } object .
* /
public class OpenSshConfig {
public class OpenSshConfig implements ConfigRepository {
/** IANA assigned port number for SSH. */
static final int SSH_PORT = 22 ;
@ -105,16 +139,25 @@ public class OpenSshConfig {
/** The .ssh/config file we read and monitor for updates. */
private final File configFile ;
/** Modification time of {@link #configFile} when {@link #hosts} loaded. */
/** Modification time of {@link #configFile} when it was last loaded. */
private long lastModified ;
/** Cached entries read out of the configuration file. */
private Map < String , Host > hosts ;
/ * *
* Encapsulates entries read out of the configuration file , and
* { @link Host } s created from that .
* /
private static class State {
Map < String , HostEntry > entries = new LinkedHashMap < > ( ) ;
Map < String , Host > hosts = new HashMap < > ( ) ;
}
/** State read from the config file, plus {@link Host}s created from it. */
private State state ;
OpenSshConfig ( final File h , final File cfg ) {
home = h ;
configFile = cfg ;
hosts = Collections . emptyMap ( ) ;
state = new State ( ) ;
}
/ * *
@ -127,75 +170,80 @@ public class OpenSshConfig {
* @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 )
final State cache = refresh ( ) ;
Host h = cache . hosts . get ( hostName ) ;
if ( h ! = null ) {
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 ;
}
HostEntry fullConfig = new HostEntry ( ) ;
// Initialize with default entries at the top of the file, before the
// first Host block.
fullConfig . merge ( cache . entries . get ( HostEntry . DEFAULT_NAME ) ) ;
for ( final Map . Entry < String , HostEntry > e : cache . entries . entrySet ( ) ) {
String key = e . getKey ( ) ;
if ( isHostMatch ( key , hostName ) ) {
fullConfig . merge ( e . getValue ( ) ) ;
}
}
h = new Host ( fullConfig , hostName , home ) ;
cache . hosts . put ( hostName , h ) ;
return h ;
}
private synchronized Map < String , Host > refresh ( ) {
private synchronized State 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 ( ) ;
State newState = new State ( ) ;
try ( FileInputStream in = new FileInputStream ( configFile ) ) {
newState . entries = parse ( in ) ;
} catch ( IOException none ) {
// Ignore -- we'll set and return an empty state
}
lastModified = mtime ;
state = newState ;
}
return hosts ;
return state ;
}
private Map < String , Host > parse ( final InputStream in ) throws IOException {
final Map < String , Host > m = new LinkedHashMap < > ( ) ;
private Map < String , HostEntry > parse ( final InputStream in )
throws IOException {
final Map < String , HostEntry > m = new LinkedHashMap < > ( ) ;
final BufferedReader br = new BufferedReader ( new InputStreamReader ( in ) ) ;
final List < Host > current = new ArrayList < > ( 4 ) ;
final List < HostEntry > current = new ArrayList < > ( 4 ) ;
String line ;
// The man page doesn't say so, but the OpenSSH parser (readconf.c)
// starts out in active mode and thus always applies any lines that
// occur before the first host block. We gather those options in a
// HostEntry for DEFAULT_NAME.
HostEntry defaults = new HostEntry ( ) ;
current . add ( defaults ) ;
m . put ( HostEntry . DEFAULT_NAME , defaults ) ;
while ( ( line = br . readLine ( ) ) ! = null ) {
line = line . trim ( ) ;
if ( line . length ( ) = = 0 | | line . startsWith ( "#" ) ) //$NON-NLS-1$
if ( line . isEmpty ( ) | | 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 ( ) ;
}
String [ ] parts = line . split ( "[ \t]*[= \t]" , 2 ) ; //$NON-NLS-1$
// Although the ssh-config man page doesn't say so, the OpenSSH
// parser does allow quoted keywords.
String keyword = dequote ( parts [ 0 ] . trim ( ) ) ;
// man 5 ssh-config says lines had the format "keyword arguments",
// with no indication that arguments were optional. However, let's
// not crap out on missing arguments. See bug 444319.
String argValue = parts . length > 1 ? parts [ 1 ] . trim ( ) : "" ; //$NON-NLS-1$
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 ) ;
for ( String name : HostEntry . parseList ( argValue ) ) {
if ( name = = null | | name . isEmpty ( ) ) {
// null should not occur, but better be safe than sorry.
continue ;
}
HostEntry c = m . get ( name ) ;
if ( c = = null ) {
c = new Host ( ) ;
c = new HostEntry ( ) ;
m . put ( name , c ) ;
}
current . add ( c ) ;
@ -206,57 +254,18 @@ public class OpenSshConfig {
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.
if ( HostEntry . isListKey ( keyword ) ) {
List < String > args = HostEntry . parseList ( argValue ) ;
for ( HostEntry entry : current ) {
entry . setValue ( keyword , args ) ;
}
} 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
} else if ( ! argValue . isEmpty ( ) ) {
argValue = dequote ( argValue ) ;
for ( HostEntry entry : current ) {
entry . setValue ( keyword , argValue ) ;
}
}
}
@ -264,23 +273,35 @@ public class OpenSshConfig {
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 ) {
if ( pattern . startsWith ( "!" ) ) { //$NON-NLS-1$
return ! patternMatchesHost ( pattern . substring ( 1 ) , name ) ;
} else {
return patternMatchesHost ( pattern , name ) ;
}
}
private static boolean isHostMatch ( final String pattern , final String name ) {
final FileNameMatcher fn ;
try {
fn = new FileNameMatcher ( pattern , null ) ;
} catch ( InvalidPatternException e ) {
return false ;
private static boolean patternMatchesHost ( final String pattern ,
final String name ) {
if ( pattern . indexOf ( '*' ) > = 0 | | pattern . indexOf ( '?' ) > = 0 ) {
final FileNameMatcher fn ;
try {
fn = new FileNameMatcher ( pattern , null ) ;
} catch ( InvalidPatternException e ) {
return false ;
}
fn . append ( name ) ;
return fn . isMatch ( ) ;
} else {
// Not a pattern but a full host name
return pattern . equals ( name ) ;
}
fn . append ( name ) ;
return fn . isMatch ( ) ;
}
private static String dequote ( final String value ) {
if ( value . startsWith ( "\"" ) & & value . endsWith ( "\"" ) ) //$NON-NLS-1$ //$NON-NLS-2$
if ( value . startsWith ( "\"" ) & & value . endsWith ( "\"" ) //$NON-NLS-1$ //$NON-NLS-2$
& & value . length ( ) > 1 )
return value . substring ( 1 , value . length ( ) - 1 ) ;
return value ;
}
@ -300,13 +321,15 @@ public class OpenSshConfig {
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 ) ;
private static int positive ( final String value ) {
if ( value ! = null ) {
try {
return Integer . parseUnsignedInt ( value ) ;
} catch ( NumberFormatException e ) {
// Ignore
}
}
return - 1 ;
}
static String userName ( ) {
@ -318,6 +341,293 @@ public class OpenSshConfig {
} ) ;
}
private static class HostEntry implements ConfigRepository . Config {
/ * *
* "Host name" of the HostEntry for the default options before the first
* host block in a config file .
* /
public static final String DEFAULT_NAME = "" ; //$NON-NLS-1$
// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
// to ssh-config keys.
private static final Map < String , String > KEY_MAP = new HashMap < > ( ) ;
static {
KEY_MAP . put ( "kex" , "KexAlgorithms" ) ; //$NON-NLS-1$//$NON-NLS-2$
KEY_MAP . put ( "server_host_key" , "HostKeyAlgorithms" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "cipher.c2s" , "Ciphers" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "cipher.s2c" , "Ciphers" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "mac.c2s" , "Macs" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "mac.s2c" , "Macs" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "compression.s2c" , "Compression" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "compression.c2s" , "Compression" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "compression_level" , "CompressionLevel" ) ; //$NON-NLS-1$ //$NON-NLS-2$
KEY_MAP . put ( "MaxAuthTries" , "NumberOfPasswordPrompts" ) ; //$NON-NLS-1$ //$NON-NLS-2$
}
/ * *
* Keys that can be specified multiple times , building up a list . ( I . e . ,
* those are the keys that do not follow the general rule of " first
* occurrence wins " . )
* /
private static final Set < String > MULTI_KEYS = new HashSet < > ( ) ;
static {
MULTI_KEYS . add ( "CERTIFICATEFILE" ) ; //$NON-NLS-1$
MULTI_KEYS . add ( "IDENTITYFILE" ) ; //$NON-NLS-1$
MULTI_KEYS . add ( "LOCALFORWARD" ) ; //$NON-NLS-1$
MULTI_KEYS . add ( "REMOTEFORWARD" ) ; //$NON-NLS-1$
MULTI_KEYS . add ( "SENDENV" ) ; //$NON-NLS-1$
}
/ * *
* Keys that take a whitespace - separated list of elements as argument .
* Because the dequote - handling is different , we must handle those in
* the parser . There are a few other keys that take comma - separated
* lists as arguments , but for the parser those are single arguments
* that must be quoted if they contain whitespace , and taking them apart
* is the responsibility of the user of those keys .
* /
private static final Set < String > LIST_KEYS = new HashSet < > ( ) ;
static {
LIST_KEYS . add ( "CANONICALDOMAINS" ) ; //$NON-NLS-1$
LIST_KEYS . add ( "GLOBALKNOWNHOSTSFILE" ) ; //$NON-NLS-1$
LIST_KEYS . add ( "SENDENV" ) ; //$NON-NLS-1$
LIST_KEYS . add ( "USERKNOWNHOSTSFILE" ) ; //$NON-NLS-1$
}
private Map < String , String > options ;
private Map < String , List < String > > multiOptions ;
private Map < String , List < String > > listOptions ;
@Override
public String getHostname ( ) {
return getValue ( "HOSTNAME" ) ; //$NON-NLS-1$
}
@Override
public String getUser ( ) {
return getValue ( "USER" ) ; //$NON-NLS-1$
}
@Override
public int getPort ( ) {
return positive ( getValue ( "PORT" ) ) ; //$NON-NLS-1$
}
private static String mapKey ( String key ) {
String k = KEY_MAP . get ( key ) ;
if ( k = = null ) {
k = key ;
}
return k . toUpperCase ( Locale . ROOT ) ;
}
private String findValue ( String key ) {
String k = mapKey ( key ) ;
String result = options ! = null ? options . get ( k ) : null ;
if ( result = = null ) {
// Also check the list and multi options. Modern OpenSSH treats
// UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
// and so does this parser. Jsch 0.1.54 in general doesn't know
// about list-valued options (it _does_ know multi-valued
// options, though), and will ask for a single value for such
// options.
//
// Let's be lenient and return at least the first value from
// a list-valued or multi-valued key for which Jsch asks for a
// single value.
List < String > values = listOptions ! = null ? listOptions . get ( k )
: null ;
if ( values = = null ) {
values = multiOptions ! = null ? multiOptions . get ( k ) : null ;
}
if ( values ! = null & & ! values . isEmpty ( ) ) {
result = values . get ( 0 ) ;
}
}
return result ;
}
@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$
String foo = findValue ( key ) ;
if ( foo = = null | | foo . equals ( "no" ) ) { //$NON-NLS-1$
return "none,zlib@openssh.com,zlib" ; //$NON-NLS-1$
}
return "zlib@openssh.com,zlib,none" ; //$NON-NLS-1$
}
return findValue ( key ) ;
}
@Override
public String [ ] getValues ( String key ) {
String k = mapKey ( key ) ;
List < String > values = listOptions ! = null ? listOptions . get ( k )
: null ;
if ( values = = null ) {
values = multiOptions ! = null ? multiOptions . get ( k ) : null ;
}
if ( values = = null | | values . isEmpty ( ) ) {
return new String [ 0 ] ;
}
return values . toArray ( new String [ values . size ( ) ] ) ;
}
public void setValue ( String key , String value ) {
String k = key . toUpperCase ( Locale . ROOT ) ;
if ( MULTI_KEYS . contains ( k ) ) {
if ( multiOptions = = null ) {
multiOptions = new HashMap < > ( ) ;
}
List < String > values = multiOptions . get ( k ) ;
if ( values = = null ) {
values = new ArrayList < > ( 4 ) ;
multiOptions . put ( k , values ) ;
}
values . add ( value ) ;
} else {
if ( options = = null ) {
options = new HashMap < > ( ) ;
}
if ( ! options . containsKey ( k ) ) {
options . put ( k , value ) ;
}
}
}
public void setValue ( String key , List < String > values ) {
if ( values . isEmpty ( ) ) {
// Can occur only on a missing argument: ignore.
return ;
}
String k = key . toUpperCase ( Locale . ROOT ) ;
// Check multi-valued keys first; because of the replacement
// strategy, they must take precedence over list-valued keys
// which always follow the "first occurrence wins" strategy.
//
// Note that SendEnv is a multi-valued list-valued key. (It's
// rather immaterial for JGit, though.)
if ( MULTI_KEYS . contains ( k ) ) {
if ( multiOptions = = null ) {
multiOptions = new HashMap < > ( 2 * MULTI_KEYS . size ( ) ) ;
}
List < String > items = multiOptions . get ( k ) ;
if ( items = = null ) {
items = new ArrayList < > ( values ) ;
multiOptions . put ( k , items ) ;
} else {
items . addAll ( values ) ;
}
} else {
if ( listOptions = = null ) {
listOptions = new HashMap < > ( 2 * LIST_KEYS . size ( ) ) ;
}
if ( ! listOptions . containsKey ( k ) ) {
listOptions . put ( k , values ) ;
}
}
}
public static boolean isListKey ( String key ) {
return LIST_KEYS . contains ( key . toUpperCase ( Locale . ROOT ) ) ;
}
/ * *
* Splits the argument into a list of whitespace - separated elements .
* Elements containing whitespace must be quoted and will be de - quoted .
*
* @param argument
* argument part of the configuration line as read from the
* config file
* @return a { @link List } of elements , possibly empty and possibly
* containing empty elements
* /
public static List < String > parseList ( String argument ) {
List < String > result = new ArrayList < > ( 4 ) ;
int start = 0 ;
int length = argument . length ( ) ;
while ( start < length ) {
// Skip whitespace
if ( Character . isSpaceChar ( argument . charAt ( start ) ) ) {
start + + ;
continue ;
}
if ( argument . charAt ( start ) = = '"' ) {
int stop = argument . indexOf ( '"' , start + 1 ) ;
if ( stop < = start ) {
// No closing double quote: skip
break ;
}
result . add ( argument . substring ( start + 1 , stop ) ) ;
start = stop + 1 ;
} else {
int stop = start + 1 ;
while ( stop < length
& & ! Character . isSpaceChar ( argument . charAt ( stop ) ) ) {
stop + + ;
}
result . add ( argument . substring ( start , stop ) ) ;
start = stop + 1 ;
}
}
return result ;
}
protected void merge ( HostEntry entry ) {
if ( entry = = null ) {
// Can occur if we could not read the config file
return ;
}
if ( entry . options ! = null ) {
if ( options = = null ) {
options = new HashMap < > ( ) ;
}
for ( Map . Entry < String , String > item : entry . options
. entrySet ( ) ) {
if ( ! options . containsKey ( item . getKey ( ) ) ) {
options . put ( item . getKey ( ) , item . getValue ( ) ) ;
}
}
}
if ( entry . listOptions ! = null ) {
if ( listOptions = = null ) {
listOptions = new HashMap < > ( 2 * LIST_KEYS . size ( ) ) ;
}
for ( Map . Entry < String , List < String > > item : entry . listOptions
. entrySet ( ) ) {
if ( ! listOptions . containsKey ( item . getKey ( ) ) ) {
listOptions . put ( item . getKey ( ) , item . getValue ( ) ) ;
}
}
}
if ( entry . multiOptions ! = null ) {
if ( multiOptions = = null ) {
multiOptions = new HashMap < > ( 2 * MULTI_KEYS . size ( ) ) ;
}
for ( Map . Entry < String , List < String > > item : entry . multiOptions
. entrySet ( ) ) {
List < String > values = multiOptions . get ( item . getKey ( ) ) ;
if ( values = = null ) {
values = new ArrayList < > ( item . getValue ( ) ) ;
multiOptions . put ( item . getKey ( ) , values ) ;
} else {
values . addAll ( item . getValue ( ) ) ;
}
}
}
}
}
/ * *
* Configuration of one "Host" block in the configuration file .
* < p >
@ -330,8 +640,6 @@ public class OpenSshConfig {
* already merged into this block .
* /
public static class Host {
boolean patternsApplied ;
String hostName ;
int port ;
@ -348,23 +656,18 @@ public class OpenSshConfig {
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 ;
private Config config ;
/ * *
* Creates a new uninitialized { @link Host } .
* /
public Host ( ) {
// For API backwards compatibility with pre-4.9 JGit
}
Host ( Config config , String hostName , File homeDir ) {
this . config = config ;
complete ( hostName , homeDir ) ;
}
/ * *
@ -432,5 +735,71 @@ public class OpenSshConfig {
public int getConnectionAttempts ( ) {
return connectionAttempts ;
}
private void complete ( String initialHostName , File homeDir ) {
// Try to set values from the options.
hostName = config . getHostname ( ) ;
user = config . getUser ( ) ;
port = config . getPort ( ) ;
connectionAttempts = positive (
config . getValue ( "ConnectionAttempts" ) ) ; //$NON-NLS-1$
strictHostKeyChecking = config . getValue ( "StrictHostKeyChecking" ) ; //$NON-NLS-1$
String value = config . getValue ( "BatchMode" ) ; //$NON-NLS-1$
if ( value ! = null ) {
batchMode = yesno ( value ) ;
}
value = config . getValue ( "PreferredAuthentications" ) ; //$NON-NLS-1$
if ( value ! = null ) {
preferredAuthentications = nows ( value ) ;
}
// Fill in defaults if still not set
if ( hostName = = null ) {
hostName = initialHostName ;
}
if ( user = = null ) {
user = OpenSshConfig . userName ( ) ;
}
if ( port < = 0 ) {
port = OpenSshConfig . SSH_PORT ;
}
if ( connectionAttempts < = 0 ) {
connectionAttempts = 1 ;
}
String [ ] identityFiles = config . getValues ( "IdentityFile" ) ; //$NON-NLS-1$
if ( identityFiles ! = null & & identityFiles . length > 0 ) {
identityFile = toFile ( identityFiles [ 0 ] , homeDir ) ;
}
}
private File toFile ( String path , File home ) {
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 ) ;
}
Config getConfig ( ) {
return config ;
}
}
/ * *
* Retrieves the full { @link com . jcraft . jsch . ConfigRepository . Config Config }
* for the given host name .
*
* @param hostName
* to get the config for
* @return the configuration for the host
* @since 4 . 9
* /
@Override
public Config getConfig ( String hostName ) {
Host host = lookup ( hostName ) ;
return host . getConfig ( ) ;
}
}