/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.client.config.hosts;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.Pair;
import org.apache.sshd.common.util.ValidateUtils;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public abstract class HostPatternsHolder {
/**
* Used in a host pattern to denote zero or more consecutive characters
*/
public static final char WILDCARD_PATTERN = '*';
public static final String ALL_HOSTS_PATTERN = String.valueOf(WILDCARD_PATTERN);
/**
* Used in a host pattern to denote any <U>one</U> character
*/
public static final char SINGLE_CHAR_PATTERN = '?';
/**
* Used to negate a host pattern
*/
public static final char NEGATION_CHAR_PATTERN = '!';
/**
* The available pattern characters
*/
public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
protected HostPatternsHolder() {
super();
}
public Collection<Pair<Pattern, Boolean>> getPatterns() {
return patterns;
}
public void setPatterns(Collection<Pair<Pattern, Boolean>> patterns) {
this.patterns = patterns;
}
/**
* Checks if a given host name / address matches the entry's host pattern(s)
*
* @param host The host name / address - ignored if {@code null}/empty
* @return {@code true} if the name / address matches the pattern(s)
* @see #isHostMatch(String, Pattern)
*/
public boolean isHostMatch(String host) {
return isHostMatch(host, getPatterns());
}
/**
* @param pattern The pattern to check - ignored if {@code null}/empty
* @return {@code true} if the pattern is not empty and contains no wildcard characters
* @see #WILDCARD_PATTERN
* @see #SINGLE_CHAR_PATTERN
* @see #SINGLE_CHAR_PATTERN
*/
public static boolean isSpecificHostPattern(String pattern) {
if (GenericUtils.isEmpty(pattern)) {
return false;
}
for (int index = 0; index < PATTERN_CHARS.length(); index++) {
char ch = PATTERN_CHARS.charAt(index);
if (pattern.indexOf(ch) >= 0) {
return false;
}
}
return true;
}
/**
* Locates all the matching entries for a give host name / address
*
* @param host The host name / address - ignored if {@code null}/empty
* @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
* @return A {@link List} of all the matching entries
* @see #isHostMatch(String)
*/
public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry... entries) {
// TODO in Java-8 use Stream(s) + predicate
if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
return Collections.emptyList();
} else {
return findMatchingEntries(host, Arrays.asList(entries));
}
}
/**
* Locates all the matching entries for a give host name / address
*
* @param host The host name / address - ignored if {@code null}/empty
* @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
* @return A {@link List} of all the matching entries
* @see #isHostMatch(String)
*/
public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
// TODO in Java-8 use Stream(s) + predicate
if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
return Collections.emptyList();
}
List<HostConfigEntry> matches = null;
for (HostConfigEntry entry : entries) {
if (!entry.isHostMatch(host)) {
continue; // debug breakpoint
}
if (matches == null) {
matches = new ArrayList<>(entries.size()); // in case ALL of them match
}
matches.add(entry);
}
if (matches == null) {
return Collections.emptyList();
} else {
return matches;
}
}
public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
if (GenericUtils.isEmpty(patterns)) {
return false;
}
boolean matchFound = false;
for (Pair<Pattern, Boolean> pp : patterns) {
Boolean negated = pp.getSecond();
/*
* If already found a match we are interested only in negations
*/
if (matchFound && (!negated)) {
continue;
}
if (!isHostMatch(host, pp.getFirst())) {
continue;
}
/*
* According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
*
* If a negated entry is matched, then the Host entry is ignored,
* regardless of whether any other patterns on the line match.
*/
if (negated) {
return false;
}
matchFound = true;
}
return matchFound;
}
/**
* Checks if a given host name / address matches a host pattern
*
* @param host The host name / address - ignored if {@code null}/empty
* @param pattern The host {@link Pattern} - ignored if {@code null}
* @return {@code true} if the name / address matches the pattern
*/
public static boolean isHostMatch(String host, Pattern pattern) {
if (GenericUtils.isEmpty(host) || (pattern == null)) {
return false;
}
Matcher m = pattern.matcher(host);
return m.matches();
}
public static List<Pair<Pattern, Boolean>> parsePatterns(CharSequence... patterns) {
return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.emptyList() : Arrays.asList(patterns));
}
public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
if (GenericUtils.isEmpty(patterns)) {
return Collections.emptyList();
}
List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
for (CharSequence p : patterns) {
result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
}
return result;
}
/**
* Converts a host pattern string to a regular expression matcher.
* <B>Note:</B> pattern matching is <U>case insensitive</U>
*
* @param pattern The original pattern string - ignored if {@code null}/empty
* @return The regular expression matcher {@link Pattern} and the indication
* whether it is a negating pattern or not - {@code null} if no original string
* @see #WILDCARD_PATTERN
* @see #SINGLE_CHAR_PATTERN
* @see #NEGATION_CHAR_PATTERN
*/
public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
if (GenericUtils.isEmpty(pattern)) {
return null;
}
StringBuilder sb = new StringBuilder(pattern.length());
boolean negated = false;
for (int curPos = 0; curPos < pattern.length(); curPos++) {
char ch = pattern.charAt(curPos);
ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
switch(ch) {
case '.': // need to escape it
sb.append('\\').append(ch);
break;
case SINGLE_CHAR_PATTERN:
sb.append('.');
break;
case WILDCARD_PATTERN:
sb.append(".*");
break;
case NEGATION_CHAR_PATTERN:
ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
negated = true;
break;
default:
sb.append(ch);
}
}
return new Pair<>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), negated);
}
/**
* Checks if the given character is valid for a host pattern. Valid
* characters are:
* <UL>
* <LI>A-Z</LI>
* <LI>a-z</LI>
* <LI>0-9</LI>
* <LI>Underscore (_)</LI>
* <LI>Hyphen (-)</LI>
* <LI>Dot (.)</LI>
* <LI>The {@link #WILDCARD_PATTERN}</LI>
* <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
* </UL>
*
* @param ch The character to validate
* @return {@code true} if valid pattern character
*/
public static boolean isValidPatternChar(char ch) {
if ((ch <= ' ') || (ch >= 0x7E)) {
return false;
}
if ((ch >= 'a') && (ch <= 'z')) {
return true;
}
if ((ch >= 'A') && (ch <= 'Z')) {
return true;
}
if ((ch >= '0') && (ch <= '9')) {
return true;
}
if ("-_.".indexOf(ch) >= 0) {
return true;
}
return PATTERN_CHARS.indexOf(ch) >= 0;
}
}