@ -1,5 +1,5 @@
/ *
/ *
* Copyright ( C ) 2008 , 2017 , Google Inc .
* Copyright ( C ) 2008 , 2018 , Google Inc .
* and other copyright owners as documented in the project ' s IP log .
* and other copyright owners as documented in the project ' s IP log .
*
*
* This program and the accompanying materials are made available
* This program and the accompanying materials are made available
@ -43,31 +43,16 @@
package org.eclipse.jgit.transport ;
package org.eclipse.jgit.transport ;
import static java.nio.charset.StandardCharsets.UTF_8 ;
import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive ;
import java.io.BufferedReader ;
import java.io.File ;
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.HashMap ;
import java.util.HashSet ;
import java.util.LinkedHashMap ;
import java.util.List ;
import java.util.List ;
import java.util.Locale ;
import java.util.Map ;
import java.util.Map ;
import java.util.Set ;
import java.util.TreeMap ;
import org.eclipse.jgit.errors.InvalidPatternException ;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile ;
import org.eclipse.jgit.fnmatch.FileNameMatcher ;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry ;
import org.eclipse.jgit.lib.Constants ;
import org.eclipse.jgit.util.FS ;
import org.eclipse.jgit.util.FS ;
import org.eclipse.jgit.util.StringUtils ;
import org.eclipse.jgit.util.SystemReader ;
import com.jcraft.jsch.ConfigRepository ;
import com.jcraft.jsch.ConfigRepository ;
@ -85,8 +70,7 @@ import com.jcraft.jsch.ConfigRepository;
* < li > JSch ' s OpenSSHConfig doesn ' t monitor for config file changes .
* < li > JSch ' s OpenSSHConfig doesn ' t monitor for config file changes .
* < / ul >
* < / ul >
* < p >
* < p >
* Therefore implement our own parser to read an OpenSSH configuration file . It
* This parser makes the critical options available to
* makes the critical options available to
* { @link org . eclipse . jgit . transport . SshSessionFactory } via
* { @link org . eclipse . jgit . transport . SshSessionFactory } via
* { @link org . eclipse . jgit . transport . OpenSshConfig . Host } objects returned by
* { @link org . eclipse . jgit . transport . OpenSshConfig . Host } objects returned by
* { @link # lookup ( String ) } , and implements a fully conforming
* { @link # lookup ( String ) } , and implements a fully conforming
@ -94,49 +78,11 @@ import com.jcraft.jsch.ConfigRepository;
* { @link com . jcraft . jsch . ConfigRepository . Config } s via
* { @link com . jcraft . jsch . ConfigRepository . Config } s via
* { @link # getConfig ( String ) } .
* { @link # getConfig ( String ) } .
* < / p >
* < / p >
* < p >
*
* Limitations compared to the full OpenSSH 7 . 5 parser :
* @see OpenSshConfigFile
* < / p >
* < ul >
* < li > This parser does not handle Match or Include keywords .
* < li > This parser does not do host name canonicalization ( Jsch ignores it
* anyway ) .
* < / ul >
* < p >
* 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 org . eclipse . jgit . transport . OpenSshConfig . Host } object .
* < / p >
* < p >
* This config does % - substitutions for the following tokens :
* < / p >
* < ul >
* < li > % % - single %
* < li > % C - short - hand for % l % h % p % r . See % p and % r below ; the replacement may be
* done partially only and may leave % p or % r or both unreplaced .
* < li > % d - home directory path
* < li > % h - remote host name
* < li > % L - local host name without domain
* < li > % l - FQDN of the local host
* < li > % n - host name as specified in { @link # lookup ( String ) }
* < li > % p - port number ; replaced only if set in the config
* < li > % r - remote user name ; replaced only if set in the config
* < li > % u - local user name
* < / ul >
* < p >
* If the config doesn ' t set the port or the remote user name , % p and % r remain
* un - substituted . It ' s the caller ' s responsibility to replace them with values
* obtained from the connection URI . % i is not handled ; Java has no concept of a
* "user ID" .
* < / p >
* /
* /
public class OpenSshConfig implements ConfigRepository {
public class OpenSshConfig implements ConfigRepository {
/** IANA assigned port number for SSH. */
static final int SSH_PORT = 22 ;
/ * *
/ * *
* Obtain the user ' s configuration data .
* Obtain the user ' s configuration data .
* < p >
* < p >
@ -155,43 +101,17 @@ public class OpenSshConfig implements ConfigRepository {
if ( home = = null )
if ( home = = null )
home = new File ( "." ) . getAbsoluteFile ( ) ; //$NON-NLS-1$
home = new File ( "." ) . getAbsoluteFile ( ) ; //$NON-NLS-1$
final File config = new File ( new File ( home , ".ssh" ) , Constants . CONFIG ) ; //$NON-NLS-1$
final File config = new File ( new File ( home , SshConstants . SSH_DIR ) ,
final OpenSshConfig osc = new OpenSshConfig ( home , config ) ;
SshConstants . CONFIG ) ;
osc . refresh ( ) ;
return new OpenSshConfig ( home , config ) ;
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 it was last loaded. */
private long lastModified ;
/ * *
* 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 < > ( ) ;
@Override
@SuppressWarnings ( "nls" )
public String toString ( ) {
return "State [entries=" + entries + ", hosts=" + hosts + "]" ;
}
}
}
/** State read from the config file, plus {@link Host}s created from it . */
/** The base file. */
private State stat e;
private OpenSshConfigFile configFile ;
OpenSshConfig ( File h , File cfg ) {
OpenSshConfig ( File h , File cfg ) {
home = h ;
configFile = new OpenSshConfigFile ( h , cfg ,
configFile = cfg ;
SshSessionFactory . getLocalUserName ( ) ) ;
state = new State ( ) ;
}
}
/ * *
/ * *
@ -204,604 +124,8 @@ public class OpenSshConfig implements ConfigRepository {
* @return r configuration for the requested name . Never null .
* @return r configuration for the requested name . Never null .
* /
* /
public Host lookup ( String hostName ) {
public Host lookup ( String hostName ) {
final State cache = refresh ( ) ;
HostEntry entry = configFile . lookup ( hostName , - 1 , null ) ;
Host h = cache . hosts . get ( hostName ) ;
return new Host ( entry , hostName , configFile . getLocalUserName ( ) ) ;
if ( h ! = null ) {
return h ;
}
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 ( Map . Entry < String , HostEntry > e : cache . entries . entrySet ( ) ) {
String key = e . getKey ( ) ;
if ( isHostMatch ( key , hostName ) ) {
fullConfig . merge ( e . getValue ( ) ) ;
}
}
fullConfig . substitute ( hostName , home ) ;
h = new Host ( fullConfig , hostName , home ) ;
cache . hosts . put ( hostName , h ) ;
return h ;
}
private synchronized State refresh ( ) {
final long mtime = configFile . lastModified ( ) ;
if ( mtime ! = lastModified ) {
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 state ;
}
private Map < String , HostEntry > parse ( InputStream in )
throws IOException {
final Map < String , HostEntry > m = new LinkedHashMap < > ( ) ;
final BufferedReader br = new BufferedReader (
new InputStreamReader ( in , UTF_8 ) ) ;
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 . isEmpty ( ) | | line . startsWith ( "#" ) ) { //$NON-NLS-1$
continue ;
}
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 ( 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 HostEntry ( ) ;
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 ( HostEntry . isListKey ( keyword ) ) {
List < String > args = HostEntry . parseList ( argValue ) ;
for ( HostEntry entry : current ) {
entry . setValue ( keyword , args ) ;
}
} else if ( ! argValue . isEmpty ( ) ) {
argValue = dequote ( argValue ) ;
for ( HostEntry entry : current ) {
entry . setValue ( keyword , argValue ) ;
}
}
}
return m ;
}
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 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 ) ;
}
}
private static String dequote ( String value ) {
if ( value . startsWith ( "\"" ) & & value . endsWith ( "\"" ) //$NON-NLS-1$ //$NON-NLS-2$
& & value . length ( ) > 1 )
return value . substring ( 1 , value . length ( ) - 1 ) ;
return value ;
}
private static String nows ( 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 ( String value ) {
if ( StringUtils . equalsIgnoreCase ( "yes" , value ) ) //$NON-NLS-1$
return Boolean . TRUE ;
return Boolean . FALSE ;
}
private static 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 ) ;
}
private static int positive ( String value ) {
if ( value ! = null ) {
try {
return Integer . parseUnsignedInt ( value ) ;
} catch ( NumberFormatException e ) {
// Ignore
}
}
return - 1 ;
}
static String userName ( ) {
return AccessController . doPrivileged ( new PrivilegedAction < String > ( ) {
@Override
public String run ( ) {
return SystemReader . getInstance ( )
. getProperty ( Constants . OS_USER_NAME_KEY ) ;
}
} ) ;
}
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 [ 0 ] ) ;
}
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 ) ;
if ( stop < start ) {
// No closing double quote: skip
break ;
}
result . add ( argument . substring ( start , 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 ( ) ) ;
}
}
}
}
private class Replacer {
private final Map < Character , String > replacements = new HashMap < > ( ) ;
public Replacer ( String originalHostName , File home ) {
replacements . put ( Character . valueOf ( '%' ) , "%" ) ; //$NON-NLS-1$
replacements . put ( Character . valueOf ( 'd' ) , home . getPath ( ) ) ;
// Needs special treatment...
String host = getValue ( "HOSTNAME" ) ; //$NON-NLS-1$
replacements . put ( Character . valueOf ( 'h' ) , originalHostName ) ;
if ( host ! = null & & host . indexOf ( '%' ) > = 0 ) {
host = substitute ( host , "h" ) ; //$NON-NLS-1$
options . put ( "HOSTNAME" , host ) ; //$NON-NLS-1$
}
if ( host ! = null ) {
replacements . put ( Character . valueOf ( 'h' ) , host ) ;
}
String localhost = SystemReader . getInstance ( ) . getHostname ( ) ;
replacements . put ( Character . valueOf ( 'l' ) , localhost ) ;
int period = localhost . indexOf ( '.' ) ;
if ( period > 0 ) {
localhost = localhost . substring ( 0 , period ) ;
}
replacements . put ( Character . valueOf ( 'L' ) , localhost ) ;
replacements . put ( Character . valueOf ( 'n' ) , originalHostName ) ;
replacements . put ( Character . valueOf ( 'p' ) , getValue ( "PORT" ) ) ; //$NON-NLS-1$
replacements . put ( Character . valueOf ( 'r' ) , getValue ( "USER" ) ) ; //$NON-NLS-1$
replacements . put ( Character . valueOf ( 'u' ) , userName ( ) ) ;
replacements . put ( Character . valueOf ( 'C' ) ,
substitute ( "%l%h%p%r" , "hlpr" ) ) ; //$NON-NLS-1$ //$NON-NLS-2$
}
public String substitute ( String input , String allowed ) {
if ( input = = null | | input . length ( ) < = 1
| | input . indexOf ( '%' ) < 0 ) {
return input ;
}
StringBuilder builder = new StringBuilder ( ) ;
int start = 0 ;
int length = input . length ( ) ;
while ( start < length ) {
int percent = input . indexOf ( '%' , start ) ;
if ( percent < 0 | | percent + 1 > = length ) {
builder . append ( input . substring ( start ) ) ;
break ;
}
String replacement = null ;
char ch = input . charAt ( percent + 1 ) ;
if ( ch = = '%' | | allowed . indexOf ( ch ) > = 0 ) {
replacement = replacements . get ( Character . valueOf ( ch ) ) ;
}
if ( replacement = = null ) {
builder . append ( input . substring ( start , percent + 2 ) ) ;
} else {
builder . append ( input . substring ( start , percent ) )
. append ( replacement ) ;
}
start = percent + 2 ;
}
return builder . toString ( ) ;
}
}
private List < String > substitute ( List < String > values , String allowed ,
Replacer r ) {
List < String > result = new ArrayList < > ( values . size ( ) ) ;
for ( String value : values ) {
result . add ( r . substitute ( value , allowed ) ) ;
}
return result ;
}
private List < String > replaceTilde ( List < String > values , File home ) {
List < String > result = new ArrayList < > ( values . size ( ) ) ;
for ( String value : values ) {
result . add ( toFile ( value , home ) . getPath ( ) ) ;
}
return result ;
}
protected void substitute ( String originalHostName , File home ) {
Replacer r = new Replacer ( originalHostName , home ) ;
if ( multiOptions ! = null ) {
List < String > values = multiOptions . get ( "IDENTITYFILE" ) ; //$NON-NLS-1$
if ( values ! = null ) {
values = substitute ( values , "dhlru" , r ) ; //$NON-NLS-1$
values = replaceTilde ( values , home ) ;
multiOptions . put ( "IDENTITYFILE" , values ) ; //$NON-NLS-1$
}
values = multiOptions . get ( "CERTIFICATEFILE" ) ; //$NON-NLS-1$
if ( values ! = null ) {
values = substitute ( values , "dhlru" , r ) ; //$NON-NLS-1$
values = replaceTilde ( values , home ) ;
multiOptions . put ( "CERTIFICATEFILE" , values ) ; //$NON-NLS-1$
}
}
if ( listOptions ! = null ) {
List < String > values = listOptions . get ( "GLOBALKNOWNHOSTSFILE" ) ; //$NON-NLS-1$
if ( values ! = null ) {
values = replaceTilde ( values , home ) ;
listOptions . put ( "GLOBALKNOWNHOSTSFILE" , values ) ; //$NON-NLS-1$
}
values = listOptions . get ( "USERKNOWNHOSTSFILE" ) ; //$NON-NLS-1$
if ( values ! = null ) {
values = replaceTilde ( values , home ) ;
listOptions . put ( "USERKNOWNHOSTSFILE" , values ) ; //$NON-NLS-1$
}
}
if ( options ! = null ) {
// HOSTNAME already done in Replacer constructor
String value = options . get ( "IDENTITYAGENT" ) ; //$NON-NLS-1$
if ( value ! = null ) {
value = r . substitute ( value , "dhlru" ) ; //$NON-NLS-1$
value = toFile ( value , home ) . getPath ( ) ;
options . put ( "IDENTITYAGENT" , value ) ; //$NON-NLS-1$
}
}
// Match is not implemented and would need to be done elsewhere
// anyway. ControlPath, LocalCommand, ProxyCommand, and
// RemoteCommand are not used by Jsch.
}
@Override
@SuppressWarnings ( "nls" )
public String toString ( ) {
return "HostEntry [options=" + options + ", multiOptions="
+ multiOptions + ", listOptions=" + listOptions + "]" ;
}
}
}
/ * *
/ * *
@ -832,8 +156,34 @@ public class OpenSshConfig implements ConfigRepository {
int connectionAttempts ;
int connectionAttempts ;
private HostEntry entry ;
private Config config ;
private Config config ;
// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
// to ssh-config keys.
private static final Map < String , String > 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 } .
* Creates a new uninitialized { @link Host } .
* /
* /
@ -841,9 +191,9 @@ public class OpenSshConfig implements ConfigRepository {
// For API backwards compatibility with pre-4.9 JGit
// For API backwards compatibility with pre-4.9 JGit
}
}
Host ( Config config , String hostName , File homeDir ) {
Host ( HostEntry entry , String hostName , String localUserName ) {
this . config = config ;
this . entry = entry ;
complete ( hostName , homeDir ) ;
complete ( hostName , localUserName ) ;
}
}
/ * *
/ * *
@ -913,42 +263,84 @@ public class OpenSshConfig implements ConfigRepository {
}
}
private void complete ( String initialHostName , File homeDir ) {
private void complete ( String initialHostName , String localUserName ) {
// Try to set values from the options.
// Try to set values from the options.
hostName = config . getHostname ( ) ;
hostName = entry . getValue ( SshConstants . HOST_NAME ) ;
user = config . getUser ( ) ;
user = entry . getValue ( SshConstants . USER ) ;
port = config . getPort ( ) ;
port = positive ( entry . getValue ( SshConstants . PORT ) ) ;
connectionAttempts = positive (
connectionAttempts = positive (
config . getValue ( "ConnectionAttempts" ) ) ; //$NON-NLS-1$
entry . getValue ( SshConstants . CONNECTION_ATTEMPTS ) ) ;
strictHostKeyChecking = config . getValue ( "StrictHostKeyChecking" ) ; //$NON-NLS-1$
strictHostKeyChecking = entry
String value = config . getValue ( "BatchMode" ) ; //$NON-NLS-1$
. getValue ( SshConstants . STRICT_HOST_KEY_CHECKING ) ;
if ( value ! = null ) {
batchMode = Boolean . valueOf ( OpenSshConfigFile
batchMode = yesno ( value ) ;
. flag ( entry . getValue ( SshConstants . BATCH_MODE ) ) ) ;
}
preferredAuthentications = entry
value = config . getValue ( "PreferredAuthentications" ) ; //$NON-NLS-1$
. getValue ( SshConstants . PREFERRED_AUTHENTICATIONS ) ;
if ( value ! = null ) {
preferredAuthentications = nows ( value ) ;
}
// Fill in defaults if still not set
// Fill in defaults if still not set
if ( hostName = = null ) {
if ( hostName = = null | | hostName . isEmpty ( ) ) {
hostName = initialHostName ;
hostName = initialHostName ;
}
}
if ( user = = null ) {
if ( user = = null | | user . isEmpty ( ) ) {
user = OpenSshConfig . userName ( ) ;
user = localUserName ;
}
}
if ( port < = 0 ) {
if ( port < = 0 ) {
port = OpenSshConfig . SSH_PORT ;
port = SshConstants . SSH_DEFAULT _PORT ;
}
}
if ( connectionAttempts < = 0 ) {
if ( connectionAttempts < = 0 ) {
connectionAttempts = 1 ;
connectionAttempts = 1 ;
}
}
String [ ] identityFiles = config . getValues ( "IdentityFile" ) ; //$NON-NLS-1$
List < String > identityFiles = entry
if ( identityFiles ! = null & & identityFiles . length > 0 ) {
. getValues ( SshConstants . IDENTITY_FILE ) ;
identityFile = toFile ( identityFiles [ 0 ] , homeDir ) ;
if ( identityFiles ! = null & & ! identityFiles . isEmpty ( ) ) {
identityFile = new File ( identityFiles . get ( 0 ) ) ;
}
}
}
}
Config getConfig ( ) {
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 < String > values = Host . this . entry
. getValues ( mapKey ( key ) ) ;
if ( values = = null ) {
return new String [ 0 ] ;
}
return values . toArray ( new String [ 0 ] ) ;
}
} ;
}
return config ;
return config ;
}
}
@ -960,7 +352,7 @@ public class OpenSshConfig implements ConfigRepository {
+ ", preferredAuthentications=" + preferredAuthentications
+ ", preferredAuthentications=" + preferredAuthentications
+ ", batchMode=" + batchMode + ", strictHostKeyChecking="
+ ", batchMode=" + batchMode + ", strictHostKeyChecking="
+ strictHostKeyChecking + ", connectionAttempts="
+ strictHostKeyChecking + ", connectionAttempts="
+ connectionAttempts + ", config=" + config + "]" ;
+ connectionAttempts + ", entry=" + entry + "]" ;
}
}
}
}
@ -980,9 +372,7 @@ public class OpenSshConfig implements ConfigRepository {
/** {@inheritDoc} */
/** {@inheritDoc} */
@Override
@Override
@SuppressWarnings ( "nls" )
public String toString ( ) {
public String toString ( ) {
return "OpenSshConfig [home=" + home + ", configFile=" + configFile
return "OpenSshConfig [configFile=" + configFile + ']' ; //$NON-NLS-1$
+ ", lastModified=" + lastModified + ", state=" + state + "]" ;
}
}
}
}