mirror of
https://github.com/iterate-ch/cyberduck.git
synced 2026-05-26 19:10:49 +00:00
Add support for the Match directive with criteria keyword host.
This commit is contained in:
+77
-6
@@ -87,6 +87,12 @@ public class OpenSshConfig {
|
||||
private Map<String, Host> hosts
|
||||
= Collections.emptyMap();
|
||||
|
||||
/**
|
||||
* Cached Match host blocks read out of the configuration file.
|
||||
*/
|
||||
private List<MatchBlock> matchBlocks
|
||||
= Collections.emptyList();
|
||||
|
||||
/**
|
||||
* Obtain the user's configuration data.
|
||||
* <p/>
|
||||
@@ -125,6 +131,12 @@ public class OpenSshConfig {
|
||||
log.debug("Found host match in SSH config:{}", e.getValue());
|
||||
h.copyFrom(e.getValue());
|
||||
}
|
||||
for(final MatchBlock mb : matchBlocks) {
|
||||
if(isMatchHostApplicable(mb.patterns, hostName)) {
|
||||
log.debug("Found match block applicable for {} in SSH config: {}", hostName, mb.host);
|
||||
h.copyFrom(mb.host);
|
||||
}
|
||||
}
|
||||
if(h.port == 0) {
|
||||
h.port = -1;
|
||||
}
|
||||
@@ -136,13 +148,16 @@ public class OpenSshConfig {
|
||||
final long mtime = configuration.attributes().getModificationDate();
|
||||
if(mtime != lastModified) {
|
||||
try {
|
||||
final List<MatchBlock> newMatchBlocks = new ArrayList<>();
|
||||
try(final InputStream in = configuration.getInputStream()) {
|
||||
hosts = this.parse(in, configuration.getParent(), new HashSet<>());
|
||||
hosts = this.parse(in, configuration.getParent(), new HashSet<>(), newMatchBlocks);
|
||||
}
|
||||
matchBlocks = newMatchBlocks;
|
||||
}
|
||||
catch(AccessDeniedException | IOException none) {
|
||||
log.warn("Failure reading {}", configuration);
|
||||
hosts = Collections.emptyMap();
|
||||
matchBlocks = Collections.emptyList();
|
||||
}
|
||||
lastModified = mtime;
|
||||
}
|
||||
@@ -151,11 +166,12 @@ public class OpenSshConfig {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param in Configuration file input stream
|
||||
* @param directory Directory containing the configuration file.
|
||||
* @param seen Previously read configuration files.
|
||||
* @param in Configuration file input stream
|
||||
* @param directory Directory containing the configuration file.
|
||||
* @param seen Previously read configuration files.
|
||||
* @param matchBlocks Accumulator for Match host blocks found during parsing.
|
||||
*/
|
||||
private Map<String, Host> parse(final InputStream in, final Local directory, final Set<Local> seen) throws IOException {
|
||||
private Map<String, Host> parse(final InputStream in, final Local directory, final Set<Local> seen, final List<MatchBlock> matchBlocks) throws IOException {
|
||||
final Map<String, Host> m = new LinkedHashMap<>();
|
||||
final BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
final List<Host> current = new ArrayList<>(4);
|
||||
@@ -181,6 +197,26 @@ public class OpenSshConfig {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if("Match".equalsIgnoreCase(keyword)) {
|
||||
current.clear();
|
||||
// Only the "host" criteria keyword is supported; skip any other Match block
|
||||
final String[] tokens = argValue.split("[ \t]+", 2);
|
||||
if(tokens.length == 2 && "host".equalsIgnoreCase(tokens[0])) {
|
||||
final List<String> patterns = new ArrayList<>();
|
||||
for(final String p : tokens[1].split(",")) {
|
||||
final String trimmed = dequote(p.trim());
|
||||
if(!trimmed.isEmpty()) {
|
||||
patterns.add(trimmed);
|
||||
}
|
||||
}
|
||||
if(!patterns.isEmpty()) {
|
||||
final Host matchHost = new Host();
|
||||
matchBlocks.add(new MatchBlock(patterns, matchHost));
|
||||
current.add(matchHost);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if("Include".equalsIgnoreCase(keyword)) {
|
||||
for(final String pattern : argValue.split("[ \t]")) {
|
||||
for(final Local included : resolve(directory, dequote(pattern))) {
|
||||
@@ -190,7 +226,7 @@ public class OpenSshConfig {
|
||||
}
|
||||
try {
|
||||
try(final InputStream i = included.getInputStream()) {
|
||||
final Map<String, Host> sub = this.parse(i, included.getParent(), seen);
|
||||
final Map<String, Host> sub = this.parse(i, included.getParent(), seen, matchBlocks);
|
||||
for(final Map.Entry<String, Host> e : sub.entrySet()) {
|
||||
m.computeIfAbsent(e.getKey(), k -> e.getValue());
|
||||
}
|
||||
@@ -321,6 +357,31 @@ public class OpenSshConfig {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates whether a {@code Match host} block applies to the given hostname.
|
||||
* <p>
|
||||
* A block applies when at least one positive pattern matches (or all patterns are negated) and no negated pattern
|
||||
* matches. Negated patterns are prefixed with {@code !}.
|
||||
*/
|
||||
private static boolean isMatchHostApplicable(final List<String> patterns, final String hostName) {
|
||||
boolean hasPositive = false;
|
||||
boolean anyPositiveMatch = false;
|
||||
for(final String pattern : patterns) {
|
||||
if(pattern.startsWith("!")) {
|
||||
if(isHostMatch(pattern.substring(1), hostName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
hasPositive = true;
|
||||
if(isHostMatch(pattern, hostName)) {
|
||||
anyPositiveMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !hasPositive || anyPositiveMatch;
|
||||
}
|
||||
|
||||
private static boolean isHostPattern(final String s) {
|
||||
return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
|
||||
}
|
||||
@@ -500,6 +561,16 @@ public class OpenSshConfig {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MatchBlock {
|
||||
final List<String> patterns;
|
||||
final Host host;
|
||||
|
||||
MatchBlock(final List<String> patterns, final Host host) {
|
||||
this.patterns = patterns;
|
||||
this.host = host;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("OpenSshConfig{");
|
||||
|
||||
@@ -75,4 +75,48 @@ public class OpenSshConfigTest {
|
||||
final OpenSshConfig.Host host = config.lookup("circular-host");
|
||||
assertEquals("circular.example.com", host.getHostName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchHostGlobPattern() {
|
||||
final OpenSshConfig config = new OpenSshConfig(new Local("src/test/resources", "openssh/config-match-host"));
|
||||
final OpenSshConfig.Host host = config.lookup("foo.example.com");
|
||||
assertEquals("matchuser", host.getUser());
|
||||
assertEquals(2222, host.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchHostExactPattern() {
|
||||
final OpenSshConfig config = new OpenSshConfig(new Local("src/test/resources", "openssh/config-match-host"));
|
||||
final OpenSshConfig.Host host = config.lookup("exact.example.com");
|
||||
// Exact pattern block sets IdentityFile
|
||||
assertEquals("~/.ssh/exact-key", host.getIdentityFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchHostNegationExcludesHost() {
|
||||
final OpenSshConfig config = new OpenSshConfig(new Local("src/test/resources", "openssh/config-match-host"));
|
||||
// excluded.example.com matches *.example.com but is negated in the third block
|
||||
final OpenSshConfig.Host excluded = config.lookup("excluded.example.com");
|
||||
assertEquals(null, excluded.getIdentityAgent());
|
||||
// other.example.com is not excluded so it should get the IdentityAgent
|
||||
final OpenSshConfig.Host other = config.lookup("other.example.com");
|
||||
assertEquals("~/.ssh/agent.sock", other.getIdentityAgent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchHostNoMatchForUnrelatedHost() {
|
||||
final OpenSshConfig config = new OpenSshConfig(new Local("src/test/resources", "openssh/config-match-host"));
|
||||
// Host outside *.example.com should not pick up any Match host settings
|
||||
final OpenSshConfig.Host host = config.lookup("unrelated.org");
|
||||
assertEquals(null, host.getUser());
|
||||
assertEquals(-1, host.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchUnsupportedCriteriaIsIgnored() {
|
||||
final OpenSshConfig config = new OpenSshConfig(new Local("src/test/resources", "openssh/config-match-host"));
|
||||
// "Match user alice" must be ignored; Port 9999 must not leak to any host lookup
|
||||
final OpenSshConfig.Host host = config.lookup("foo.example.com");
|
||||
assertEquals(2222, host.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
Match host *.example.com
|
||||
User matchuser
|
||||
Port 2222
|
||||
|
||||
Match host exact.example.com
|
||||
IdentityFile ~/.ssh/exact-key
|
||||
|
||||
Match host *.example.com,!excluded.example.com
|
||||
IdentityAgent ~/.ssh/agent.sock
|
||||
|
||||
Match user alice
|
||||
Port 9999
|
||||
Reference in New Issue
Block a user