/* * 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.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.Pair; import org.apache.sshd.util.test.BaseTestSupport; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; /** * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class HostConfigEntryTest extends BaseTestSupport { public HostConfigEntryTest() { super(); } @Test public void testNegatingPatternOverridesAll() { String testHost = "37.77.34.7"; String[] elements = GenericUtils.split(testHost, '.'); StringBuilder sb = new StringBuilder(testHost.length() + Byte.SIZE); List<Pair<Pattern, Boolean>> patterns = new ArrayList<>(elements.length + 1); // all wildcard patterns are not negated - only the actual host patterns.add(HostPatternsHolder.toPattern(String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + testHost)); for (int i = 0; i < elements.length; i++) { sb.setLength(0); for (int j = 0; j < elements.length; j++) { if (j > 0) { sb.append('.'); } if (i == j) { sb.append(HostPatternsHolder.WILDCARD_PATTERN); } else { sb.append(elements[j]); } } patterns.add(HostPatternsHolder.toPattern(sb)); } for (int index = 0; index < patterns.size(); index++) { assertFalse("Unexpected match for " + patterns, HostPatternsHolder.isHostMatch(testHost, patterns)); Collections.shuffle(patterns); } } @Test public void testHostWildcardPatternMatching() { String pkgName = getClass().getPackage().getName(); String[] elements = GenericUtils.split(pkgName, '.'); StringBuilder sb = new StringBuilder(pkgName.length() + Long.SIZE + 1).append(HostPatternsHolder.WILDCARD_PATTERN); for (int index = elements.length - 1; index >= 0; index--) { sb.append('.').append(elements[index]); } String value = sb.toString(); Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(value); Pattern pattern = pp.getFirst(); String domain = value.substring(1); // chomp the wildcard prefix for (String host : new String[] { getClass().getSimpleName(), getCurrentTestName(), getClass().getSimpleName() + "-" + getCurrentTestName(), getClass().getSimpleName() + "." + getCurrentTestName(), }) { sb.setLength(0); // start from scratch sb.append(host).append(domain); testCaseInsensitivePatternMatching(sb.toString(), pattern, true); } } @Test public void testIPAddressWildcardPatternMatching() { StringBuilder sb = new StringBuilder().append("10.0.0."); int sbLen = sb.length(); Pattern pattern = HostPatternsHolder.toPattern(sb.append(HostPatternsHolder.WILDCARD_PATTERN)).getFirst(); for (int v = 0; v <= 255; v++) { sb.setLength(sbLen); // start from scratch sb.append(v); String address = sb.toString(); assertTrue("No match for " + address, HostPatternsHolder.isHostMatch(address, pattern)); } } @Test public void testHostSingleCharPatternMatching() { String value = getCurrentTestName(); StringBuilder sb = new StringBuilder(value); for (boolean restoreOriginal : new boolean[] {true, false}) { for (int index = 0; index < value.length(); index++) { sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN); testCaseInsensitivePatternMatching(value, HostPatternsHolder.toPattern(sb.toString()).getFirst(), true); if (restoreOriginal) { sb.setCharAt(index, value.charAt(index)); } } } } @Test public void testIPAddressSingleCharPatternMatching() { StringBuilder sb = new StringBuilder().append("10.0.0."); int sbLen = sb.length(); for (int v = 0; v <= 255; v++) { sb.setLength(sbLen); // start from scratch sb.append(v); String address = sb.toString(); // replace the added digits with single char pattern for (int index = sbLen; index < sb.length(); index++) { sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN); } String pattern = sb.toString(); Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(pattern); assertTrue("No match for " + address + " on pattern=" + pattern, HostPatternsHolder.isHostMatch(address, Collections.singletonList(pp))); } } @Test public void testIsValidPatternChar() { for (char ch = '\0'; ch <= ' '; ch++) { assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch)); } for (char ch = 'a'; ch <= 'z'; ch++) { assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch)); } for (char ch = 'A'; ch <= 'Z'; ch++) { assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch)); } for (char ch = '0'; ch <= '9'; ch++) { assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch)); } for (char ch : new char[] {'-', '_', '.', HostPatternsHolder.SINGLE_CHAR_PATTERN, HostPatternsHolder.WILDCARD_PATTERN}) { assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch)); } for (char ch : new char[] { '(', ')', '{', '}', '[', ']', '@', '#', '$', '^', '&', '%', '~', '<', '>', ',', '/', '\\', '\'', '"', ':', ';' }) { assertFalse("Unexpected valid character: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch)); } for (char ch = 0x7E; ch <= 0xFF; ch++) { assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch)); } } @Test public void testResolvePort() { final int originalPort = Short.MAX_VALUE; final int preferredPort = 7365; assertEquals("Mismatched entry port preference", preferredPort, HostConfigEntry.resolvePort(originalPort, preferredPort)); for (int entryPort : new int[] {-1, 0}) { assertEquals("Non-preferred original port for entry port=" + entryPort, originalPort, HostConfigEntry.resolvePort(originalPort, entryPort)); } } @Test public void testResolveUsername() { final String originalUser = getCurrentTestName(); final String preferredUser = getClass().getSimpleName(); assertSame("Mismatched entry user preference", preferredUser, HostConfigEntry.resolveUsername(originalUser, preferredUser)); for (String entryUser : new String[] {null, ""}) { assertSame("Non-preferred original user for entry user='" + entryUser + "'", originalUser, HostConfigEntry.resolveUsername(originalUser, entryUser)); } } @Test public void testReadSimpleHostsConfigEntries() throws IOException { validateHostConfigEntries(readHostConfigEntries()); } @Test public void testReadGlobalHostsConfigEntries() throws IOException { List<HostConfigEntry> entries = validateHostConfigEntries(readHostConfigEntries()); assertTrue("Not enough entries read", GenericUtils.size(entries) > 1); // global entry MUST be 1st one HostConfigEntry globalEntry = entries.get(0); assertEquals("Mismatched global entry pattern", HostPatternsHolder.ALL_HOSTS_PATTERN, globalEntry.getHost()); for (int index = 1; index < entries.size(); index++) { HostConfigEntry entry = entries.get(index); assertFalse("No target host for " + entry, GenericUtils.isEmpty(entry.getHostName())); assertTrue("No target port for " + entry, entry.getPort() > 0); assertFalse("No username for " + entry, GenericUtils.isEmpty(entry.getUsername())); assertFalse("No identities for " + entry, GenericUtils.isEmpty(entry.getIdentities())); assertFalse("No properties for " + entry, GenericUtils.isEmpty(entry.getProperties())); } } @Test public void testReadMultipleHostPatterns() throws IOException { List<HostConfigEntry> entries = validateHostConfigEntries(readHostConfigEntries()); assertEquals("Mismatched number of entries", 1, GenericUtils.size(entries)); assertEquals("Mismatched number of patterns", 3, GenericUtils.size(entries.get(0).getPatterns())); } @Test public void testResolveIdentityFilePath() throws Exception { final String hostValue = getClass().getSimpleName(); final int portValue = 7365; final String userValue = getCurrentTestName(); Exception err = null; for (String pattern : new String[] { "~/.ssh/%h.key", "%d/.ssh/%h.key", "/home/%u/.ssh/id_rsa_%p", "/home/%u/.ssh/id_%r_rsa", "/home/%u/.ssh/%h/%l.key" }) { try { String result = HostConfigEntry.resolveIdentityFilePath(pattern, hostValue, portValue, userValue); System.out.append('\t').append(pattern).append(" => ").println(result); } catch (Exception e) { System.err.append("Failed (").append(e.getClass().getSimpleName()) .append(") to process pattern=").append(pattern) .append(": ").println(e.getMessage()); err = e; } } if (err != null) { throw err; } } @Test public void testFindBestMatch() { final String hostValue = getCurrentTestName(); HostConfigEntry expected = new HostConfigEntry(hostValue, hostValue, 7365, hostValue); List<HostConfigEntry> matches = new ArrayList<>(); matches.add(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), Short.MAX_VALUE, getClass().getSimpleName())); matches.add(new HostConfigEntry(hostValue + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), getClass().getSimpleName(), Byte.MAX_VALUE, getClass().getSimpleName())); matches.add(expected); for (int index = 0; index < matches.size(); index++) { HostConfigEntry actual = HostConfigEntry.findBestMatch(matches); assertSame("Mismatched best match for " + matches, expected, actual); Collections.shuffle(matches); } } private static <C extends Collection<HostConfigEntry>> C validateHostConfigEntries(C entries) { assertFalse("No entries", GenericUtils.isEmpty(entries)); for (HostConfigEntry entry : entries) { assertFalse("No pattern for " + entry, GenericUtils.isEmpty(entry.getHost())); assertFalse("No extra properties for " + entry, GenericUtils.isEmpty(entry.getProperties())); } return entries; } private List<HostConfigEntry> readHostConfigEntries() throws IOException { return readHostConfigEntries(getCurrentTestName() + ".config.txt"); } private List<HostConfigEntry> readHostConfigEntries(String resourceName) throws IOException { URL url = getClass().getResource(resourceName); assertNotNull("Missing resource " + resourceName, url); return HostConfigEntry.readHostConfigEntries(url); } private static void testCaseInsensitivePatternMatching(String value, Pattern pattern, boolean expected) { for (int index = 0; index < value.length(); index++) { boolean actual = HostPatternsHolder.isHostMatch(value, pattern); assertEquals("Mismatched match result for " + value + " on pattern=" + pattern.pattern(), expected, actual); value = shuffleCase(value); } } }