http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java new file mode 100644 index 0000000..53bca90 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java @@ -0,0 +1,805 @@ +/* + * 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.common.util; + +import java.io.File; +import java.nio.file.FileSystem; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.StringTokenizer; + +/** + * <p>This is a utility class used by selectors and DirectoryScanner. The + * functionality more properly belongs just to selectors, but unfortunately + * DirectoryScanner exposed these as protected methods. Thus we have to + * support any subclasses of DirectoryScanner that may access these methods. + * </p> + * <p>This is a Singleton.</p> + * + * @author Arnout J. Kuiper + * <a href="mailto:[email protected]">[email protected]</a> + * @author Magesh Umasankar + * @author <a href="mailto:[email protected]">Bruce Atherton</a> + * @version $Id$ + * @since 1.5 + */ +public final class SelectorUtils { + + public static final String PATTERN_HANDLER_PREFIX = "["; + + public static final String PATTERN_HANDLER_SUFFIX = "]"; + + public static final String REGEX_HANDLER_PREFIX = "%regex" + PATTERN_HANDLER_PREFIX; + + public static final String ANT_HANDLER_PREFIX = "%ant" + PATTERN_HANDLER_PREFIX; + + /** + * Private Constructor + */ + private SelectorUtils() { + throw new UnsupportedOperationException("No instance allowed"); + } + + /** + * <p>Tests whether or not a given path matches the start of a given + * pattern up to the first "**".</p> + * + * <p>This is not a general purpose test and should only be used if you + * can live with false positives. For example, <code>pattern=**\a</code> + * and <code>str=b</code> will yield <code>true</code>.</p> + * + * @param pattern The pattern to match against. Must not be + * {@code null}. + * @param str The path to match, as a String. Must not be + * {@code null}. + * @return whether or not a given path matches the start of a given + * pattern up to the first "**". + */ + public static boolean matchPatternStart(String pattern, String str) { + return matchPatternStart(pattern, str, true); + } + + /** + * <p>Tests whether or not a given path matches the start of a given + * pattern up to the first "**".</p> + * + * <p>This is not a general purpose test and should only be used if you + * can live with false positives. For example, <code>pattern=**\a</code> + * and <code>str=b</code> will yield <code>true</code>.</p> + * + * @param pattern The pattern to match against. Must not be + * {@code null}. + * @param str The path to match, as a String. Must not be + * {@code null}. + * @param isCaseSensitive Whether or not matching should be performed + * case sensitively. + * @return whether or not a given path matches the start of a given + * pattern up to the first "**". + */ + public static boolean matchPatternStart(String pattern, String str, + boolean isCaseSensitive) { + if (pattern.length() > (REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(REGEX_HANDLER_PREFIX) && pattern.endsWith(PATTERN_HANDLER_SUFFIX)) { + // FIXME: ICK! But we can't do partial matches for regex, so we have to reserve judgement until we have + // a file to deal with, or we can definitely say this is an exclusion... + return true; + } else { + if (pattern.length() > (ANT_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(ANT_HANDLER_PREFIX) && pattern.endsWith(PATTERN_HANDLER_SUFFIX)) { + pattern = + pattern.substring(ANT_HANDLER_PREFIX.length(), pattern.length() - PATTERN_HANDLER_SUFFIX.length()); + } + + String altStr = str.replace('\\', '/'); + + return matchAntPathPatternStart(pattern, str, File.separator, isCaseSensitive) + || matchAntPathPatternStart(pattern, altStr, "/", isCaseSensitive); + } + } + + private static boolean matchAntPathPatternStart(String pattern, String str, String separator, boolean isCaseSensitive) { + // When str starts with a File.separator, pattern has to start with a + // File.separator. + // When pattern starts with a File.separator, str has to start with a + // File.separator. + if (str.startsWith(separator) != pattern.startsWith(separator)) { + return false; + } + + List<String> patDirs = tokenizePath(pattern, separator); + List<String> strDirs = tokenizePath(str, separator); + + int patIdxStart = 0; + int patIdxEnd = patDirs.size() - 1; + int strIdxStart = 0; + int strIdxEnd = strDirs.size() - 1; + + // up to first '**' + while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) { + String patDir = patDirs.get(patIdxStart); + if (patDir.equals("**")) { + break; + } + if (!match(patDir, strDirs.get(strIdxStart), + isCaseSensitive)) { + return false; + } + patIdxStart++; + strIdxStart++; + } + + // CHECKSTYLE:OFF + if (strIdxStart > strIdxEnd) { + // String is exhausted + return true; + } else { + return patIdxStart <= patIdxEnd; + } + // CHECKSTYLE:ON + } + + /** + * Tests whether or not a given path matches a given pattern. + * + * @param pattern The pattern to match against. Must not be + * {@code null}. + * @param str The path to match, as a String. Must not be + * {@code null}. + * @return <code>true</code> if the pattern matches against the string, + * or <code>false</code> otherwise. + */ + public static boolean matchPath(String pattern, String str) { + return matchPath(pattern, str, true); + } + + /** + * Tests whether or not a given path matches a given pattern. + * + * @param pattern The pattern to match against. Must not be + * {@code null}. + * @param str The path to match, as a String. Must not be + * {@code null}. + * @param isCaseSensitive Whether or not matching should be performed + * case sensitively. + * @return <code>true</code> if the pattern matches against the string, + * or <code>false</code> otherwise. + */ + public static boolean matchPath(String pattern, String str, boolean isCaseSensitive) { + if (pattern.length() > (REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(REGEX_HANDLER_PREFIX) && pattern.endsWith(PATTERN_HANDLER_SUFFIX)) { + pattern = pattern.substring(REGEX_HANDLER_PREFIX.length(), pattern.length() + - PATTERN_HANDLER_SUFFIX.length()); + + return str.matches(pattern); + } else { + if (pattern.length() > (ANT_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1) + && pattern.startsWith(ANT_HANDLER_PREFIX) && pattern.endsWith(PATTERN_HANDLER_SUFFIX)) { + pattern = + pattern.substring(ANT_HANDLER_PREFIX.length(), pattern.length() - PATTERN_HANDLER_SUFFIX.length()); + } + + return matchAntPathPattern(pattern, str, isCaseSensitive); + } + } + + private static boolean matchAntPathPattern(String pattern, String str, boolean isCaseSensitive) { + // When str starts with a File.separator, pattern has to start with a + // File.separator. + // When pattern starts with a File.separator, str has to start with a + // File.separator. + if (str.startsWith(File.separator) != pattern.startsWith(File.separator)) { + return false; + } + + List<String> patDirs = tokenizePath(pattern, File.separator); + List<String> strDirs = tokenizePath(str, File.separator); + + int patIdxStart = 0; + int patIdxEnd = patDirs.size() - 1; + int strIdxStart = 0; + int strIdxEnd = strDirs.size() - 1; + + // up to first '**' + while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) { + String patDir = patDirs.get(patIdxStart); + if (patDir.equals("**")) { + break; + } + if (!match(patDir, strDirs.get(strIdxStart), + isCaseSensitive)) { + patDirs = null; + strDirs = null; + return false; + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // String is exhausted + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs.get(i).equals("**")) { + patDirs = null; + strDirs = null; + return false; + } + } + return true; + } else { + if (patIdxStart > patIdxEnd) { + // String not exhausted, but pattern is. Failure. + patDirs = null; + strDirs = null; + return false; + } + } + + // up to last '**' + while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) { + String patDir = patDirs.get(patIdxEnd); + if (patDir.equals("**")) { + break; + } + if (!match(patDir, strDirs.get(strIdxEnd), + isCaseSensitive)) { + patDirs = null; + strDirs = null; + return false; + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // String is exhausted + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs.get(i).equals("**")) { + patDirs = null; + strDirs = null; + return false; + } + } + return true; + } + + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patDirs.get(i).equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // '**/**' situation, so skip one + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = patIdxTmp - patIdxStart - 1; + int strLength = strIdxEnd - strIdxStart + 1; + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = patDirs.get(patIdxStart + j + 1); + String subStr = strDirs.get(strIdxStart + i + j); + if (!match(subPat, subStr, isCaseSensitive)) { + continue strLoop; + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + patDirs = null; + strDirs = null; + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (!patDirs.get(i).equals("**")) { + patDirs = null; + strDirs = null; + return false; + } + } + + return true; + } + + /** + * Tests whether or not a string matches against a pattern. + * The pattern may contain two special characters:<br> + * '*' means zero or more characters<br> + * '?' means one and only one character + * + * @param pattern The pattern to match against. + * Must not be {@code null}. + * @param str The string which must be matched against the pattern. + * Must not be {@code null}. + * @return <code>true</code> if the string matches against the pattern, + * or <code>false</code> otherwise. + */ + public static boolean match(String pattern, String str) { + return match(pattern, str, true); + } + + /** + * Tests whether or not a string matches against a pattern. + * The pattern may contain two special characters:<br> + * '*' means zero or more characters<br> + * '?' means one and only one character + * + * @param pattern The pattern to match against. + * Must not be {@code null}. + * @param str The string which must be matched against the pattern. + * Must not be {@code null}. + * @param isCaseSensitive Whether or not matching should be performed + * case sensitively. + * @return <code>true</code> if the string matches against the pattern, + * or <code>false</code> otherwise. + */ + @SuppressWarnings("PMD.AssignmentInOperand") + public static boolean match(String pattern, String str, boolean isCaseSensitive) { + char[] patArr = pattern.toCharArray(); + char[] strArr = str.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = false; + for (char aPatArr : patArr) { + if (aPatArr == '*') { + containsStar = true; + break; + } + } + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if ((ch != '?') && (!equals(ch, strArr[i], isCaseSensitive))) { + return false; // Character mismatch + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + // CHECKSTYLE:OFF + while (((ch = patArr[patIdxStart]) != '*') && (strIdxStart <= strIdxEnd)) { + if ((ch != '?') && (!equals(ch, strArr[strIdxStart], isCaseSensitive))) { + return false; // Character mismatch + } + patIdxStart++; + strIdxStart++; + } + // CHECKSTYLE:ON + + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // Process characters after last star + // CHECKSTYLE:OFF + while (((ch = patArr[patIdxEnd]) != '*') && (strIdxStart <= strIdxEnd)) { + if ((ch != '?') && (!equals(ch, strArr[strIdxEnd], isCaseSensitive))) { + return false; // Character mismatch + } + patIdxEnd--; + strIdxEnd--; + } + // CHECKSTYLE:ON + + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // process pattern between stars. padIdxStart and patIdxEnd point always to a '*'. + while ((patIdxStart != patIdxEnd) && (strIdxStart <= strIdxEnd)) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = patIdxTmp - patIdxStart - 1; + int strLength = strIdxEnd - strIdxStart + 1; + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?' && !equals(ch, strArr[strIdxStart + i + j], isCaseSensitive)) { + continue strLoop; + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + /** + * Tests whether two characters are equal. + * @param c1 1st character + * @param c2 2nd character + * @param isCaseSensitive Whether to compare case sensitive + * @return {@code true} if equal characters + */ + public static boolean equals(char c1, char c2, boolean isCaseSensitive) { + if (c1 == c2) { + return true; + } + if (!isCaseSensitive) { + // NOTE: Try both upper case and lower case as done by String.equalsIgnoreCase() + if (Character.toUpperCase(c1) == Character.toUpperCase(c2) + || Character.toLowerCase(c1) == Character.toLowerCase(c2)) { + return true; + } + } + return false; + } + + /** + * Breaks a path up into a Vector of path elements, tokenizing on + * <code>File.separator</code>. + * + * @param path Path to tokenize. Must not be {@code null}. + * @return a List of path elements from the tokenized path + */ + public static List<String> tokenizePath(String path) { + return tokenizePath(path, File.separator); + } + + public static List<String> tokenizePath(String path, String separator) { + List<String> ret = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(path, separator); + while (st.hasMoreTokens()) { + ret.add(st.nextToken()); + } + return ret; + } + + /** /** + * Converts a path to one matching the target file system by applying the + * "slashification" rules, converting it to a local path and + * then translating its separator to the target file system one (if different + * than local one) + * @param path The input path + * @param pathSeparator The separator used to build the input path + * @param fs The target {@link FileSystem} - may not be {@code null} + * @return The transformed path + * @see #translateToLocalFileSystemPath(String, char, String) + */ + public static String translateToLocalFileSystemPath(String path, char pathSeparator, FileSystem fs) { + return translateToLocalFileSystemPath(path, pathSeparator, Objects.requireNonNull(fs, "No target file system").getSeparator()); + } + + /** + * Converts a path to one matching the target file system by applying the + * "slashification" rules, converting it to a local path and + * then translating its separator to the target file system one (if different + * than local one) + * @param path The input path + * @param pathSeparator The separator used to build the input path + * @param fsSeparator The target file system separator + * @return The transformed path + * @see #applySlashifyRules(String, char) + * @see #translateToLocalPath(String) + * @see #translateToFileSystemPath(String, String, String) + */ + public static String translateToLocalFileSystemPath(String path, char pathSeparator, String fsSeparator) { + // In case double slashes and other patterns are used + String slashified = applySlashifyRules(path, pathSeparator); + // In case we are running on Windows + String localPath = translateToLocalPath(slashified); + return translateToFileSystemPath(localPath, File.separator, fsSeparator); + } + + /** + * Applies the "slashification" rules as specified in + * <A HREF="http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266">Single Unix Specification version 3, section 3.266</A> + * and <A HREF="http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11">section 4.11 - Pathname resolution</A> + * @param path The original path - ignored if {@code null}/empty or does + * not contain any slashes + * @param sepChar The "slash" character + * @return The effective path - may be same as input if no changes required + */ + public static String applySlashifyRules(String path, char sepChar) { + if (GenericUtils.isEmpty(path)) { + return path; + } + + int curPos = path.indexOf(sepChar); + if (curPos < 0) { + return path; // no slashes to handle + } + + int lastPos = 0; + StringBuilder sb = null; + while (curPos < path.length()) { + curPos++; // skip the 1st '/' + + /* + * As per Single Unix Specification version 3, section 3.266: + * + * Multiple successive slashes are considered to be the + * same as one slash + */ + int nextPos = curPos; + while ((nextPos < path.length()) && (path.charAt(nextPos) == sepChar)) { + nextPos++; + } + + /* + * At this stage, nextPos is the first non-slash character after a + * possibly 'seqLen' sequence of consecutive slashes. + */ + int seqLen = nextPos - curPos; + if (seqLen > 0) { + if (sb == null) { + sb = new StringBuilder(path.length() - seqLen); + } + + if (lastPos < curPos) { + String clrText = path.substring(lastPos, curPos); + sb.append(clrText); + } + + lastPos = nextPos; + } + + if (nextPos >= path.length()) { + break; // no more data + } + + curPos = path.indexOf(sepChar, nextPos); + if (curPos < nextPos) { + break; // no more slashes + } + } + + // check if any leftovers for the modified path + if (sb != null) { + if (lastPos < path.length()) { + String clrText = path.substring(lastPos); + sb.append(clrText); + } + + path = sb.toString(); + } + + /* + * At this point we know for sure that 'path' contains only SINGLE + * slashes. According to section 4.11 - Pathname resolution + * + * A pathname that contains at least one non-slash character + * and that ends with one or more trailing slashes shall be + * resolved as if a single dot character ( '.' ) were appended + * to the pathname. + */ + if ((path.length() > 1) && (path.charAt(path.length() - 1) == sepChar)) { + return path + "."; + } else { + return path; + } + } + + /** + * Converts a possibly '/' separated path to a local path. <B>Note:</B> + * takes special care of Windows drive paths - e.g., {@code C:} + * by converting them to "C:\" + * + * @param path The original path - ignored if {@code null}/empty + * @return The local path + */ + public static String translateToLocalPath(String path) { + if (GenericUtils.isEmpty(path) || (File.separatorChar == '/')) { + return path; + } + + // This code is reached if we are running on Windows + String localPath = path.replace('/', File.separatorChar); + // check if '/c:' prefix + if ((localPath.charAt(0) == File.separatorChar) && isWindowsDriveSpecified(localPath, 1, localPath.length() - 1)) { + localPath = localPath.substring(1); + } + if (!isWindowsDriveSpecified(localPath)) { + return localPath; // assume a relative path + } + + /* + * Here we know that we have at least a "C:" string - make sure it + * is followed by the local file separator. Note: if all we have is + * just the drive, we will create a "C:\" path since this is the + * preferred Windows way to refer to root drives in the file system + */ + if (localPath.length() == 2) { + return localPath + File.separator; // all we have is "C:" + } else if (localPath.charAt(2) != File.separatorChar) { + // be nice and add the missing file separator - C:foo => C:\foo + return localPath.substring(0, 2) + File.separator + localPath.substring(2); + } else { + return localPath; + } + } + + public static boolean isWindowsDriveSpecified(CharSequence cs) { + return isWindowsDriveSpecified(cs, 0, GenericUtils.length(cs)); + } + + public static boolean isWindowsDriveSpecified(CharSequence cs, int offset, int len) { + if ((len < 2) || (cs.charAt(offset + 1) != ':')) { + return false; + } + + char drive = cs.charAt(offset); + return ((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z')); + } + + /** + * Converts a path containing a specific separator to one using the + * specified file-system one + * @param path The input path - ignored if {@code null}/empty + * @param pathSeparator The separator used to build the input path - may not + * be {@code null}/empty + * @param fs The target {@link FileSystem} - may not be {@code null} + * @return The path where the separator used to build it is replaced by + * the file-system one (if different) + * @see FileSystem#getSeparator() + * @see #translateToFileSystemPath(String, String, String) + */ + public static String translateToFileSystemPath(String path, String pathSeparator, FileSystem fs) { + return translateToFileSystemPath(path, pathSeparator, Objects.requireNonNull(fs, "No target file system").getSeparator()); + } + + /** + * Converts a path containing a specific separator to one using the + * specified file-system one + * @param path The input path - ignored if {@code null}/empty + * @param pathSeparator The separator used to build the input path - may not + * be {@code null}/empty + * @param fsSeparator The target file system separator - may not be {@code null}/empty + * @return The path where the separator used to build it is replaced by + * the file-system one (if different) + * @throws IllegalArgumentException if path or file-system separator are {@code null}/empty + * or if the separators are different and the path contains the target + * file-system separator as it would create an ambiguity + */ + public static String translateToFileSystemPath(String path, String pathSeparator, String fsSeparator) { + ValidateUtils.checkNotNullAndNotEmpty(pathSeparator, "Missing path separator"); + ValidateUtils.checkNotNullAndNotEmpty(fsSeparator, "Missing file-system separator"); + + if (GenericUtils.isEmpty(path) || Objects.equals(pathSeparator, fsSeparator)) { + return path; + } + + // make sure path does not contain the target separator + if (path.contains(fsSeparator)) { + ValidateUtils.throwIllegalArgumentException("File system replacement may yield ambiguous result for %s with separator=%s", path, fsSeparator); + } + + // check most likely case + if ((pathSeparator.length() == 1) && (fsSeparator.length() == 1)) { + return path.replace(pathSeparator.charAt(0), fsSeparator.charAt(0)); + } else { + return path.replace(pathSeparator, fsSeparator); + } + } + + /** + * Returns dependency information on these two files. If src has been + * modified later than target, it returns true. If target doesn't exist, + * it likewise returns true. Otherwise, target is newer than src and + * is not out of date, thus the method returns false. It also returns + * false if the src file doesn't even exist, since how could the + * target then be out of date. + * + * @param src the original file + * @param target the file being compared against + * @param granularity the amount in seconds of slack we will give in + * determining out of dateness + * @return whether the target is out of date + */ + public static boolean isOutOfDate(File src, File target, int granularity) { + if (!src.exists()) { + return false; + } + if (!target.exists()) { + return true; + } + return (src.lastModified() - granularity) > target.lastModified(); + } + + /** + * "Flattens" a string by removing all whitespace (space, tab, linefeed, + * carriage return, and formfeed). This uses StringTokenizer and the + * default set of tokens as documented in the single arguement constructor. + * + * @param input a String to remove all whitespace. + * @return a String that has had all whitespace removed. + */ + public static String removeWhitespace(String input) { + StringBuilder result = new StringBuilder(); + if (input != null) { + StringTokenizer st = new StringTokenizer(input); + while (st.hasMoreTokens()) { + result.append(st.nextToken()); + } + } + return result.toString(); + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/SshdEventListener.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/SshdEventListener.java b/sshd-common/src/main/java/org/apache/sshd/common/util/SshdEventListener.java new file mode 100644 index 0000000..ea6a3dd --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/SshdEventListener.java @@ -0,0 +1,44 @@ +/* + * 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.common.util; + +import java.lang.reflect.Proxy; +import java.util.EventListener; +import java.util.Objects; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface SshdEventListener extends EventListener { + + /** + * Makes sure that the listener is neither {@code null} nor a proxy + * + * @param <L> Type of {@link SshdEventListener} being validation + * @param listener The listener instance + * @param prefix Prefix text to be prepended to validation failure messages + * @return The validated instance + */ + static <L extends SshdEventListener> L validateListener(L listener, String prefix) { + Objects.requireNonNull(listener, prefix + ": no instance"); + ValidateUtils.checkTrue(!Proxy.isProxyClass(listener.getClass()), prefix + ": proxies N/A"); + return listener; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/ValidateUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/ValidateUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/ValidateUtils.java new file mode 100644 index 0000000..a55e5d6 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/ValidateUtils.java @@ -0,0 +1,216 @@ +/* + * 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.common.util; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class ValidateUtils { + private ValidateUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static <T> T checkNotNull(T t, String message) { + checkTrue(t != null, message); + return t; + } + + public static <T> T checkNotNull(T t, String message, Object arg) { + checkTrue(t != null, message, arg); + return t; + } + + public static <T> T checkNotNull(T t, String message, long value) { + checkTrue(t != null, message, value); + return t; + } + + public static <T> T checkNotNull(T t, String message, Object... args) { + checkTrue(t != null, message, args); + return t; + } + + public static String checkNotNullAndNotEmpty(String t, String message) { + t = checkNotNull(t, message).trim(); + checkTrue(GenericUtils.length(t) > 0, message); + return t; + } + + public static String checkNotNullAndNotEmpty(String t, String message, Object arg) { + t = checkNotNull(t, message, arg).trim(); + checkTrue(GenericUtils.length(t) > 0, message, arg); + return t; + } + + public static String checkNotNullAndNotEmpty(String t, String message, Object... args) { + t = checkNotNull(t, message, args).trim(); + checkTrue(GenericUtils.length(t) > 0, message, args); + return t; + } + + public static <K, V, M extends Map<K, V>> M checkNotNullAndNotEmpty(M t, String message, Object... args) { + t = checkNotNull(t, message, args); + checkTrue(GenericUtils.size(t) > 0, message, args); + return t; + } + + public static <T, C extends Collection<T>> C checkNotNullAndNotEmpty(C t, String message, Object... args) { + t = checkNotNull(t, message, args); + checkTrue(GenericUtils.size(t) > 0, message, args); + return t; + } + + public static <T, C extends Iterable<T>> C checkNotNullAndNotEmpty(C t, String message, Object... args) { + t = checkNotNull(t, message, args); + checkTrue(GenericUtils.isNotEmpty(t), message, args); + return t; + } + + public static byte[] checkNotNullAndNotEmpty(byte[] a, String message) { + a = checkNotNull(a, message); + checkTrue(NumberUtils.length(a) > 0, message); + return a; + } + + public static byte[] checkNotNullAndNotEmpty(byte[] a, String message, Object... args) { + a = checkNotNull(a, message, args); + checkTrue(NumberUtils.length(a) > 0, message, args); + return a; + } + + public static char[] checkNotNullAndNotEmpty(char[] a, String message) { + a = checkNotNull(a, message); + checkTrue(GenericUtils.length(a) > 0, message); + return a; + } + + public static char[] checkNotNullAndNotEmpty(char[] a, String message, Object... args) { + a = checkNotNull(a, message, args); + checkTrue(GenericUtils.length(a) > 0, message, args); + return a; + } + + public static int[] checkNotNullAndNotEmpty(int[] a, String message) { + a = checkNotNull(a, message); + checkTrue(NumberUtils.length(a) > 0, message); + return a; + } + + public static int[] checkNotNullAndNotEmpty(int[] a, String message, Object... args) { + a = checkNotNull(a, message, args); + checkTrue(NumberUtils.length(a) > 0, message, args); + return a; + } + + public static <T> T[] checkNotNullAndNotEmpty(T[] t, String message, Object... args) { + t = checkNotNull(t, message, args); + checkTrue(GenericUtils.length(t) > 0, message, args); + return t; + } + + public static <T> T checkInstanceOf(Object v, Class<T> expected, String message, long value) { + Class<?> actual = checkNotNull(v, message, value).getClass(); + checkTrue(expected.isAssignableFrom(actual), message, value); + return expected.cast(v); + } + + public static <T> T checkInstanceOf(Object v, Class<T> expected, String message) { + return checkInstanceOf(v, expected, message, GenericUtils.EMPTY_OBJECT_ARRAY); + } + + public static <T> T checkInstanceOf(Object v, Class<T> expected, String message, Object arg) { + Class<?> actual = checkNotNull(v, message, arg).getClass(); + checkTrue(expected.isAssignableFrom(actual), message, arg); + return expected.cast(v); + } + + public static <T> T checkInstanceOf(Object v, Class<T> expected, String message, Object... args) { + Class<?> actual = checkNotNull(v, message, args).getClass(); + checkTrue(expected.isAssignableFrom(actual), message, args); + return expected.cast(v); + } + + public static void checkTrue(boolean flag, String message) { + if (!flag) { + throwIllegalArgumentException(message, GenericUtils.EMPTY_OBJECT_ARRAY); + } + } + + public static void checkTrue(boolean flag, String message, long value) { + if (!flag) { + throwIllegalArgumentException(message, value); + } + } + + public static void checkTrue(boolean flag, String message, Object arg) { + if (!flag) { + throwIllegalArgumentException(message, arg); + } + } + + public static void checkTrue(boolean flag, String message, Object... args) { + if (!flag) { + throwIllegalArgumentException(message, args); + } + } + + public static void throwIllegalArgumentException(String format, Object... args) { + throw createFormattedException(IllegalArgumentException::new, format, args); + } + + public static void checkState(boolean flag, String message) { + if (!flag) { + throwIllegalStateException(message, GenericUtils.EMPTY_OBJECT_ARRAY); + } + } + + public static void checkState(boolean flag, String message, long value) { + if (!flag) { + throwIllegalStateException(message, value); + } + } + + public static void checkState(boolean flag, String message, Object arg) { + if (!flag) { + throwIllegalStateException(message, arg); + } + } + + public static void checkState(boolean flag, String message, Object... args) { + if (!flag) { + throwIllegalStateException(message, args); + } + } + + public static void throwIllegalStateException(String format, Object... args) { + throw createFormattedException(IllegalStateException::new, format, args); + } + + public static <T extends Throwable> T createFormattedException( + Function<? super String, ? extends T> constructor, String format, Object... args) { + String message = String.format(format, args); + return constructor.apply(message); + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/VersionInfo.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/VersionInfo.java b/sshd-common/src/main/java/org/apache/sshd/common/util/VersionInfo.java new file mode 100644 index 0000000..ed1aab7 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/VersionInfo.java @@ -0,0 +1,137 @@ +/* + * 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.common.util; + +import java.io.Serializable; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class VersionInfo implements Serializable, Comparable<VersionInfo> { + private static final long serialVersionUID = -9127482432228413836L; + + private final int majorVersion; + private final int minorVersion; + private final int release; + private final int buildNumber; + + public VersionInfo(int major, int minor) { + this(major, minor, 0, 0); + } + + public VersionInfo(int major, int minor, int release, int build) { + this.majorVersion = major; + this.minorVersion = minor; + this.release = release; + this.buildNumber = build; + } + + public final int getMajorVersion() { + return majorVersion; + } + + public final int getMinorVersion() { + return minorVersion; + } + + public final int getRelease() { + return release; + } + + public final int getBuildNumber() { + return buildNumber; + } + + @Override + public int hashCode() { + return NumberUtils.hashCode(getMajorVersion(), getMinorVersion(), getRelease(), getBuildNumber()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + return compareTo((VersionInfo) obj) == 0; + } + + @Override + public int compareTo(VersionInfo o) { + if (o == null) { + return -1; // push nulls to end + } + if (o == this) { + return 0; + } + + int nRes = Integer.compare(getMajorVersion(), o.getMajorVersion()); + if (nRes == 0) { + nRes = Integer.compare(getMinorVersion(), o.getMinorVersion()); + } + if (nRes == 0) { + nRes = Integer.compare(getRelease(), o.getRelease()); + } + if (nRes == 0) { + nRes = Integer.compare(getBuildNumber(), o.getBuildNumber()); + } + + return nRes; + } + + @Override + public String toString() { + return NumberUtils.join('.', getMajorVersion(), getMinorVersion(), getRelease(), getBuildNumber()); + } + + /** + * Parses a version string - assumed to contain at most 4 non-negative + * components separated by a '.'. If less than 4 components are found, then + * the rest are assumed to be zero. If more than 4 components found, then + * only the 1st ones are parsed. + * + * @param version The version string - ignored if {@code null}/empty + * @return The parsed {@link VersionInfo} - or {@code null} if empty input + * @throws NumberFormatException If failed to parse any of the components + * @throws IllegalArgumentException If any of the parsed components is negative + */ + public static VersionInfo parse(String version) throws NumberFormatException { + String[] comps = GenericUtils.split(version, '.'); + if (GenericUtils.isEmpty(comps)) { + return null; + } + + int[] values = new int[4]; + int maxValues = Math.min(comps.length, values.length); + for (int index = 0; index < maxValues; index++) { + String c = comps[index]; + int v = Integer.parseInt(c); + ValidateUtils.checkTrue(v >= 0, "Invalid version component in %s", version); + values[index] = v; + } + + return new VersionInfo(values[0], values[1], values[2], values[3]); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java new file mode 100644 index 0000000..dd61473 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java @@ -0,0 +1,798 @@ +/* + * 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.common.util.buffer; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +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.Objects; +import java.util.function.IntUnaryOperator; +import java.util.logging.Level; + +import org.apache.sshd.common.PropertyResolver; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.Readable; +import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser; +import org.apache.sshd.common.util.logging.SimplifiedLog; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Provides an abstract message buffer for encoding SSH messages + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class Buffer implements Readable { + protected final byte[] workBuf = new byte[Long.BYTES]; + + protected Buffer() { + super(); + } + + public abstract int rpos(); + + public abstract void rpos(int rpos); + + public abstract int wpos(); + + public abstract void wpos(int wpos); + + public abstract int capacity(); + + public abstract byte[] array(); + + public abstract void compact(); + + public byte[] getCompactData() { + int l = available(); + if (l > 0) { + byte[] b = new byte[l]; + System.arraycopy(array(), rpos(), b, 0, l); + return b; + } else { + return GenericUtils.EMPTY_BYTE_ARRAY; + } + } + + public void clear() { + clear(true); + } + + public abstract void clear(boolean wipeData); + + public boolean isValidMessageStructure(Class<?>... fieldTypes) { + return isValidMessageStructure(GenericUtils.isEmpty(fieldTypes) ? Collections.emptyList() : Arrays.asList(fieldTypes)); + } + + public boolean isValidMessageStructure(Collection<Class<?>> fieldTypes) { + if (GenericUtils.isEmpty(fieldTypes)) { + return true; + } + + int remainLen = available(); + int readOffset = 0; + for (Class<?> ft : fieldTypes) { + if ((ft == boolean.class) || (ft == Boolean.class) + || (ft == byte.class) || (ft == Byte.class)) { + if (remainLen < Byte.BYTES) { + return false; + } + + remainLen -= Byte.BYTES; + readOffset += Byte.BYTES; + } else if ((ft == short.class) || (ft == Short.class)) { + if (remainLen < Short.BYTES) { + return false; + } + + remainLen -= Short.BYTES; + readOffset += Short.BYTES; + } else if ((ft == int.class) || (ft == Integer.class)) { + if (remainLen < Integer.BYTES) { + return false; + } + + remainLen -= Integer.BYTES; + readOffset += Integer.BYTES; + } else if ((ft == long.class) || (ft == Long.class)) { + if (remainLen < Long.BYTES) { + return false; + } + + remainLen -= Long.BYTES; + readOffset += Long.BYTES; + } else if ((ft == byte[].class) || (ft == String.class)) { + if (remainLen < Integer.BYTES) { + return false; + } + + copyRawBytes(readOffset, workBuf, 0, Integer.BYTES); + remainLen -= Integer.BYTES; + readOffset += Integer.BYTES; + + long length = BufferUtils.getUInt(workBuf, 0, Integer.BYTES); + if (length > remainLen) { + return false; + } + + remainLen -= (int) length; + readOffset += (int) length; + } + } + + return true; + } + + protected abstract void copyRawBytes(int offset, byte[] buf, int pos, int len); + + public String toHex() { + return BufferUtils.toHex(array(), rpos(), available()); + } + + public void dumpHex(SimplifiedLog logger, String prefix, PropertyResolver resolver) { + dumpHex(logger, BufferUtils.DEFAULT_HEXDUMP_LEVEL, prefix, resolver); + } + + public void dumpHex(SimplifiedLog logger, Level level, String prefix, PropertyResolver resolver) { + BufferUtils.dumpHex(logger, level, prefix, resolver, BufferUtils.DEFAULT_HEX_SEPARATOR, array(), rpos(), available()); + } + + /*====================== + Read methods + ======================*/ + + public int getUByte() { + return getByte() & 0xFF; + } + + public byte getByte() { + ensureAvailable(Byte.BYTES); + getRawBytes(workBuf, 0, Byte.BYTES); + return workBuf[0]; + } + + public short getShort() { + ensureAvailable(Short.BYTES); + getRawBytes(workBuf, 0, Short.BYTES); + short v = (short) ((workBuf[1] << Byte.SIZE) & 0xFF00); + v |= (short) (workBuf[0] & 0xF); + return v; + } + + public int getInt() { + return (int) getUInt(); + } + + public long getUInt() { + ensureAvailable(Integer.BYTES); + getRawBytes(workBuf, 0, Integer.BYTES); + return BufferUtils.getUInt(workBuf, 0, Integer.BYTES); + } + + public long getLong() { + ensureAvailable(Long.BYTES); + getRawBytes(workBuf, 0, Long.BYTES); + long l = ((long) workBuf[0] << 56) & 0xff00000000000000L; + l |= ((long) workBuf[1] << 48) & 0x00ff000000000000L; + l |= ((long) workBuf[2] << 40) & 0x0000ff0000000000L; + l |= ((long) workBuf[3] << 32) & 0x000000ff00000000L; + l |= ((long) workBuf[4] << 24) & 0x00000000ff000000L; + l |= ((long) workBuf[5] << 16) & 0x0000000000ff0000L; + l |= ((long) workBuf[6] << 8) & 0x000000000000ff00L; + l |= (workBuf[7]) & 0x00000000000000ffL; + return l; + } + + @SuppressWarnings("PMD.BooleanGetMethodName") + public boolean getBoolean() { + return getByte() != 0; + } + + public String getString() { + return getString(StandardCharsets.UTF_8); + } + + /** + * @param usePrependedLength If {@code true} then there is a 32-bit + * value indicating the number of strings to read. Otherwise, the + * method will use a "greedy" reading of strings while more + * data available + * @return A {@link Collection} of the read strings + * @see #getStringList(boolean, Charset) + */ + public Collection<String> getStringList(boolean usePrependedLength) { + return getStringList(usePrependedLength, StandardCharsets.UTF_8); + } + + /** + * @param usePrependedLength If {@code true} then there is a 32-bit + * value indicating the number of strings to read. Otherwise, the + * method will use a "greedy" reading of strings while more + * data available + * @param charset The {@link Charset} to use for the string + * @return A {@link Collection} of the read strings + * @see #getStringList(int, Charset) + * @see #getAvailableStrings() + */ + public Collection<String> getStringList(boolean usePrependedLength, Charset charset) { + if (usePrependedLength) { + int count = getInt(); + return getStringList(count, charset); + } else { + return getAvailableStrings(charset); + } + } + + /** + * @return The remaining data as a list of strings + * @see #getAvailableStrings(Charset) + */ + public Collection<String> getAvailableStrings() { + return getAvailableStrings(StandardCharsets.UTF_8); + } + + /** + * @param charset The {@link Charset} to use for the strings + * @return The remaining data as a list of strings + * @see #available() + * @see #getString(Charset) + */ + public Collection<String> getAvailableStrings(Charset charset) { + Collection<String> list = new LinkedList<>(); + while (available() > 0) { + String s = getString(charset); + list.add(s); + } + + return list; + } + + /** + * @param count The <U>exact</U> number of strings to read - can be zero + * @return A {@link List} with the specified number of strings + * @see #getStringList(int, Charset) + */ + public List<String> getStringList(int count) { + return getStringList(count, StandardCharsets.UTF_8); + } + + /** + * @param count The <U>exact</U> number of strings to read - can be zero + * @param charset The {@link Charset} of the strings + * @return A {@link List} with the specified number of strings + * @see #getString(Charset) + */ + public List<String> getStringList(int count, Charset charset) { + if (count == 0) { + return Collections.emptyList(); + } + + List<String> list = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + String s = getString(charset); + list.add(s); + } + + return list; + } + + public abstract String getString(Charset charset); + + public BigInteger getMPInt() { + return new BigInteger(getMPIntAsBytes()); + } + + public byte[] getMPIntAsBytes() { + return getBytes(); + } + + public byte[] getBytes() { + int len = getInt(); + if (len < 0) { + throw new BufferException("Bad item length: " + len); + } + ensureAvailable(len); + byte[] b = new byte[len]; + getRawBytes(b); + return b; + } + + public void getRawBytes(byte[] buf) { + getRawBytes(buf, 0, buf.length); + } + + public PublicKey getPublicKey() throws SshException { + return getPublicKey(BufferPublicKeyParser.DEFAULT); + } + + /** + * @param parser A {@link BufferPublicKeyParser} to extract the key from the buffer + * - never {@code null} + * @return The extracted {@link PublicKey} - may be {@code null} if the parser so decided + * @throws SshException If failed to extract the key + * @see #getRawPublicKey(BufferPublicKeyParser) + */ + public PublicKey getPublicKey(BufferPublicKeyParser<? extends PublicKey> parser) throws SshException { + int ow = wpos(); + int len = getInt(); + wpos(rpos() + len); + try { + return getRawPublicKey(parser); + } finally { + wpos(ow); + } + } + + public PublicKey getRawPublicKey() throws SshException { + return getRawPublicKey(BufferPublicKeyParser.DEFAULT); + } + + /** + * @param parser A {@link BufferPublicKeyParser} to extract the key from the buffer + * - never {@code null} + * @return The extracted {@link PublicKey} - may be {@code null} if the parser so decided + * @throws SshException If failed to extract the key + */ + public PublicKey getRawPublicKey(BufferPublicKeyParser<? extends PublicKey> parser) throws SshException { + Objects.requireNonNull(parser, "No key data parser"); + try { + String keyType = getString(); + if (!parser.isKeyTypeSupported(keyType)) { + throw new NoSuchAlgorithmException("Key type=" + keyType + ") not supported by parser=" + parser); + } + + return parser.getRawPublicKey(keyType, this); + } catch (GeneralSecurityException e) { + throw new SshException(e); + } + } + + public KeyPair getKeyPair() throws SshException { + try { + final PublicKey pub; + final PrivateKey prv; + final String keyAlg = getString(); + if (KeyPairProvider.SSH_RSA.equals(keyAlg)) { + BigInteger e = getMPInt(); + BigInteger n = getMPInt(); + BigInteger d = getMPInt(); + BigInteger qInv = getMPInt(); + BigInteger q = getMPInt(); + BigInteger p = getMPInt(); + BigInteger dP = d.remainder(p.subtract(BigInteger.valueOf(1))); + BigInteger dQ = d.remainder(q.subtract(BigInteger.valueOf(1))); + KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM); + pub = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); + prv = keyFactory.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, dP, dQ, qInv)); + } else if (KeyPairProvider.SSH_DSS.equals(keyAlg)) { + BigInteger p = getMPInt(); + BigInteger q = getMPInt(); + BigInteger g = getMPInt(); + BigInteger y = getMPInt(); + BigInteger x = getMPInt(); + KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.DSS_ALGORITHM); + pub = keyFactory.generatePublic(new DSAPublicKeySpec(y, p, q, g)); + prv = keyFactory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)); + } else if (KeyPairProvider.SSH_ED25519.equals(keyAlg)) { + return SecurityUtils.extractEDDSAKeyPair(this, keyAlg); + } else { + ECCurves curve = ECCurves.fromKeyType(keyAlg); + if (curve == null) { + throw new NoSuchAlgorithmException("Unsupported key pair algorithm: " + keyAlg); + } + String curveName = curve.getName(); + ECParameterSpec params = curve.getParameters(); + return extractEC(curveName, params); + } + + return new KeyPair(pub, prv); + } catch (GeneralSecurityException e) { + throw new SshException(e); + } + } + + protected KeyPair extractEC(String expectedCurveName, ECParameterSpec spec) throws GeneralSecurityException { + String curveName = getString(); + if (!expectedCurveName.equals(curveName)) { + throw new InvalidKeySpecException("extractEC(" + expectedCurveName + ") mismatched curve name: " + curveName); + } + + byte[] groupBytes = getBytes(); + BigInteger exponent = getMPInt(); + + if (spec == null) { + throw new InvalidKeySpecException("extractEC(" + expectedCurveName + ") missing parameters for curve"); + } + + ECPoint group; + try { + group = ECCurves.octetStringToEcPoint(groupBytes); + } catch (RuntimeException e) { + throw new InvalidKeySpecException("extractEC(" + expectedCurveName + ")" + + " failed (" + e.getClass().getSimpleName() + ")" + + " to decode EC group for curve: " + e.getMessage(), + e); + } + + KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM); + PublicKey pubKey = keyFactory.generatePublic(new ECPublicKeySpec(group, spec)); + PrivateKey privKey = keyFactory.generatePrivate(new ECPrivateKeySpec(exponent, spec)); + return new KeyPair(pubKey, privKey); + } + + public void ensureAvailable(int reqLen) throws BufferException { + int availLen = available(); + if (availLen < reqLen) { + throw new BufferException("Underflow: requested=" + reqLen + ", available=" + availLen); + } + } + + /*====================== + Write methods + ======================*/ + + public void putByte(byte b) { + ensureCapacity(Byte.BYTES); + workBuf[0] = b; + putRawBytes(workBuf, 0, Byte.BYTES); + } + + public void putBuffer(Readable buffer) { + putBuffer(buffer, true); + } + + public abstract int putBuffer(Readable buffer, boolean expand); + + public abstract void putBuffer(ByteBuffer buffer); + + /** + * Writes 16 bits + * + * @param i The 16-bit value + */ + public void putShort(int i) { + ensureCapacity(Short.BYTES); + workBuf[0] = (byte) (i >> 8); + workBuf[1] = (byte) i; + putRawBytes(workBuf, 0, Short.BYTES); + } + + /** + * Writes 32 bits + * + * @param i The 32-bit value + */ + public void putInt(long i) { + BufferUtils.validateInt32Value(i, "Invalid 32-bit value: %d"); + ensureCapacity(Integer.BYTES); + BufferUtils.putUInt(i, workBuf, 0, Integer.BYTES); + putRawBytes(workBuf, 0, Integer.BYTES); + } + + /** + * Writes 64 bits + * + * @param i The 64-bit value + */ + public void putLong(long i) { + ensureCapacity(Long.BYTES); + workBuf[0] = (byte) (i >> 56); + workBuf[1] = (byte) (i >> 48); + workBuf[2] = (byte) (i >> 40); + workBuf[3] = (byte) (i >> 32); + workBuf[4] = (byte) (i >> 24); + workBuf[5] = (byte) (i >> 16); + workBuf[6] = (byte) (i >> 8); + workBuf[7] = (byte) i; + putRawBytes(workBuf, 0, Long.BYTES); + } + + public void putBoolean(boolean b) { + putByte(b ? (byte) 1 : (byte) 0); + } + + /** + * Adds the bytes to the buffer and wipes the data from the + * input buffer <U>after</U> having added it - useful for sensitive + * information such as password + * + * @param b The buffer to add - OK if {@code null} + */ + public void putAndWipeBytes(byte[] b) { + putAndWipeBytes(b, 0, NumberUtils.length(b)); + } + + public void putAndWipeBytes(byte[] b, int off, int len) { + putBytes(b, off, len); + + for (int pos = off, index = 0; index < len; pos++, index++) { + b[pos] = (byte) 0; + } + } + + public void putBytes(byte[] b) { + putBytes(b, 0, NumberUtils.length(b)); + } + + public void putBytes(byte[] b, int off, int len) { + putInt(len); + putRawBytes(b, off, len); + } + + /** + * Encodes the {@link Objects#toString(Object, String) toString} value of each member. + * + * @param objects The objects to be encoded in the buffer - OK if + * {@code null}/empty + * @param prependLength If {@code true} then the list is preceded by + * a 32-bit count of the number of members in the list + * @see #putStringList(Collection, Charset, boolean) + */ + public void putStringList(Collection<?> objects, boolean prependLength) { + putStringList(objects, StandardCharsets.UTF_8, prependLength); + } + + /** + * Encodes the {@link Objects#toString(Object, String) toString} value of each member + * + * @param objects The objects to be encoded in the buffer - OK if + * {@code null}/empty + * @param charset The {@link Charset} to use for encoding + * @param prependLength If {@code true} then the list is preceded by + * a 32-bit count of the number of members in the list + * @see #putString(String, Charset) + */ + public void putStringList(Collection<?> objects, Charset charset, boolean prependLength) { + int numObjects = GenericUtils.size(objects); + if (prependLength) { + putInt(numObjects); + } + + if (numObjects <= 0) { + return; + } + + objects.forEach(o -> putString(Objects.toString(o, null), charset)); + } + + public void putString(String string) { + putString(string, StandardCharsets.UTF_8); + } + + public void putString(String string, Charset charset) { + if (GenericUtils.isEmpty(string)) { + putBytes(GenericUtils.EMPTY_BYTE_ARRAY); + } else { + putBytes(string.getBytes(charset)); + } + } + + /** + * Zeroes the input array <U>after</U> having put the characters in the + * buffer - useful for sensitive information such as passwords + * + * @param chars The characters to put in the buffer - may be {@code null}/empty + * @see #putAndWipeChars(char[], Charset) + * @see #putChars(char[], Charset) + */ + public void putAndWipeChars(char[] chars) { + putAndWipeChars(chars, 0, GenericUtils.length(chars)); + } + + public void putAndWipeChars(char[] chars, int offset, int len) { + putAndWipeChars(chars, offset, len, StandardCharsets.UTF_8); + } + + public void putAndWipeChars(char[] chars, Charset charset) { + putAndWipeChars(chars, 0, GenericUtils.length(chars), charset); + } + + public void putAndWipeChars(char[] chars, int offset, int len, Charset charset) { + putChars(chars, offset, len, charset); + for (int pos = offset, index = 0; index < len; index++, pos++) { + chars[pos] = '\0'; + } + } + + public void putChars(char[] chars) { + putChars(chars, 0, GenericUtils.length(chars)); + } + + public void putChars(char[] chars, int offset, int len) { + putChars(chars, offset, len, StandardCharsets.UTF_8); + } + + public void putChars(char[] chars, Charset charset) { + putChars(chars, 0, GenericUtils.length(chars), charset); + } + + public void putChars(char[] chars, int offset, int len, Charset charset) { + if (len <= 0) { + putBytes(GenericUtils.EMPTY_BYTE_ARRAY); + } else { + putBuffer(charset.encode(CharBuffer.wrap(chars, offset, len))); + } + } + + public void putMPInt(BigInteger bi) { + putMPInt(bi.toByteArray()); + } + + public void putMPInt(byte[] foo) { + if ((foo[0] & 0x80) != 0) { + putInt(foo.length + 1 /* padding */); + putByte((byte) 0); + } else { + putInt(foo.length); + } + putRawBytes(foo); + } + + public void putRawBytes(byte[] d) { + putRawBytes(d, 0, d.length); + } + + public abstract void putRawBytes(byte[] d, int off, int len); + + public void putPublicKey(PublicKey key) { + int ow = wpos(); + putInt(0); + int ow1 = wpos(); + putRawPublicKey(key); + int ow2 = wpos(); + wpos(ow); + putInt(ow2 - ow1); + wpos(ow2); + } + + public void putRawPublicKey(PublicKey key) { + Objects.requireNonNull(key, "No key"); + if (key instanceof RSAPublicKey) { + RSAPublicKey rsaPub = (RSAPublicKey) key; + + putString(KeyPairProvider.SSH_RSA); + putMPInt(rsaPub.getPublicExponent()); + putMPInt(rsaPub.getModulus()); + } else if (key instanceof DSAPublicKey) { + DSAPublicKey dsaPub = (DSAPublicKey) key; + DSAParams dsaParams = dsaPub.getParams(); + + putString(KeyPairProvider.SSH_DSS); + putMPInt(dsaParams.getP()); + putMPInt(dsaParams.getQ()); + putMPInt(dsaParams.getG()); + putMPInt(dsaPub.getY()); + } else if (key instanceof ECPublicKey) { + ECPublicKey ecKey = (ECPublicKey) key; + ECParameterSpec ecParams = ecKey.getParams(); + ECCurves curve = ECCurves.fromCurveParameters(ecParams); + if (curve == null) { + throw new BufferException("Unsupported EC curve parameters"); + } + + putString(curve.getKeyType()); + putString(curve.getName()); + putBytes(ECCurves.encodeECPoint(ecKey.getW(), ecParams)); + } else if (SecurityUtils.EDDSA.equals(key.getAlgorithm())) { + SecurityUtils.putRawEDDSAPublicKey(this, key); + } else { + throw new BufferException("Unsupported raw public key algorithm: " + key.getAlgorithm()); + } + } + + public void putKeyPair(KeyPair kp) { + PublicKey pubKey = kp.getPublic(); + PrivateKey prvKey = kp.getPrivate(); + if (prvKey instanceof RSAPrivateCrtKey) { + RSAPublicKey rsaPub = (RSAPublicKey) pubKey; + RSAPrivateCrtKey rsaPrv = (RSAPrivateCrtKey) prvKey; + + putString(KeyPairProvider.SSH_RSA); + putMPInt(rsaPub.getPublicExponent()); + putMPInt(rsaPub.getModulus()); + putMPInt(rsaPrv.getPrivateExponent()); + putMPInt(rsaPrv.getCrtCoefficient()); + putMPInt(rsaPrv.getPrimeQ()); + putMPInt(rsaPrv.getPrimeP()); + } else if (pubKey instanceof DSAPublicKey) { + DSAPublicKey dsaPub = (DSAPublicKey) pubKey; + DSAParams dsaParams = dsaPub.getParams(); + DSAPrivateKey dsaPrv = (DSAPrivateKey) prvKey; + + putString(KeyPairProvider.SSH_DSS); + putMPInt(dsaParams.getP()); + putMPInt(dsaParams.getQ()); + putMPInt(dsaParams.getG()); + putMPInt(dsaPub.getY()); + putMPInt(dsaPrv.getX()); + } else if (pubKey instanceof ECPublicKey) { + ECPublicKey ecPub = (ECPublicKey) pubKey; + ECPrivateKey ecPriv = (ECPrivateKey) prvKey; + ECParameterSpec ecParams = ecPub.getParams(); + ECCurves curve = ECCurves.fromCurveParameters(ecParams); + if (curve == null) { + throw new BufferException("Unsupported EC curve parameters"); + } + + putString(curve.getKeyType()); + putString(curve.getName()); + putBytes(ECCurves.encodeECPoint(ecPub.getW(), ecParams)); + putMPInt(ecPriv.getS()); + } else if (SecurityUtils.EDDSA.equals(pubKey.getAlgorithm())) { + SecurityUtils.putEDDSAKeyPair(this, pubKey, prvKey); + } else { + throw new BufferException("Unsupported key pair algorithm: " + pubKey.getAlgorithm()); + } + } + + protected void ensureCapacity(int capacity) { + ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR); + } + + /** + * @param capacity The requires capacity + * @param growthFactor An {@link IntUnaryOperator} that is invoked + * if the current capacity is insufficient. The argument is the minimum + * required new data length, the function result should be the + * effective new data length to be allocated - if less than minimum + * then an exception is thrown + */ + public abstract void ensureCapacity(int capacity, IntUnaryOperator growthFactor); + + protected abstract int size(); + + @Override + public String toString() { + return "Buffer [rpos=" + rpos() + ", wpos=" + wpos() + ", size=" + size() + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferException.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferException.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferException.java new file mode 100644 index 0000000..93f4e6b --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferException.java @@ -0,0 +1,30 @@ +/* + * 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.common.util.buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class BufferException extends RuntimeException { + private static final long serialVersionUID = 658645233475011039L; + + public BufferException(String message) { + super(message); + } +} \ No newline at end of file
