mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 07:10:53 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7dd73b9c8 | ||
|
|
d628c47bae | ||
|
|
6e7fb96d07 | ||
|
|
d5d6096d5d | ||
|
|
2551f8e559 | ||
|
|
430cbfcf13 | ||
|
|
ec467a3875 | ||
|
|
1b258f0677 | ||
|
|
559384ac91 | ||
|
|
5674072666 | ||
|
|
f33bfecbf5 | ||
|
|
c0f6000ff5 | ||
|
|
3de0302c84 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
*.bat text eol=crlf
|
||||
src/itest/docker-image/** eol=lf
|
||||
|
||||
@@ -104,6 +104,13 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
|
||||
Fork away!
|
||||
|
||||
== Release history
|
||||
SSHJ 0.34.0 (2022-08-10)::
|
||||
* Merged https://github.com/hierynomus/sshj/pull/743[#743]: Use default client credentials for AuthGssApiWithMic
|
||||
* Merged https://github.com/hierynomus/sshj/pull/801[#801]: Restore thread interrupt status after catching InterruptedException
|
||||
* Merged https://github.com/hierynomus/sshj/pull/793[#793]: Merge PKCS5 and PKCS8 classes
|
||||
* Upgraded dependencies SLF4J (1.7.36) and Logback (1.2.11)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/791[#791]: Update KeepAlive examples
|
||||
* Merged https://github.com/hierynomus/sshj/pull/775[#775]: Add SFTP resume support
|
||||
SSHJ 0.33.0 (2022-04-22)::
|
||||
* Upgraded dependencies BouncyCastle (1.70)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/687[#687]: Correctly close connection when remote closes connection.
|
||||
|
||||
11
build.gradle
11
build.gradle
@@ -39,7 +39,7 @@ def bouncycastleVersion = "1.70"
|
||||
def sshdVersion = "2.8.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:1.7.32"
|
||||
implementation "org.slf4j:slf4j-api:1.7.36"
|
||||
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
|
||||
implementation "com.jcraft:jzlib:1.1.3"
|
||||
@@ -53,7 +53,7 @@ dependencies {
|
||||
testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
|
||||
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
|
||||
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
|
||||
testImplementation "ch.qos.logback:logback-classic:1.2.9"
|
||||
testImplementation "ch.qos.logback:logback-classic:1.2.11"
|
||||
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
|
||||
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||
testImplementation 'org.testcontainers:testcontainers:1.16.2'
|
||||
@@ -65,7 +65,12 @@ license {
|
||||
mapping {
|
||||
java = 'SLASHSTAR_STYLE'
|
||||
}
|
||||
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java'])
|
||||
excludes([
|
||||
'**/djb/Curve25519.java',
|
||||
'**/sshj/common/Base64.java',
|
||||
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
|
||||
'**/files/test_file_*.txt',
|
||||
])
|
||||
}
|
||||
|
||||
if (!JavaVersion.current().isJava9Compatible()) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj-examples</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.19.1</version>
|
||||
<version>0.33.0</version>
|
||||
|
||||
<name>sshj-examples</name>
|
||||
<description>Examples for SSHv2 library for Java</description>
|
||||
@@ -55,7 +55,7 @@
|
||||
<dependency>
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj</artifactId>
|
||||
<version>0.31.0</version>
|
||||
<version>0.33.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ public class KeepAlive {
|
||||
final SSHClient ssh = new SSHClient(defaultConfig);
|
||||
try {
|
||||
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
||||
// Set interval to enable keep-alive before connecting
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
ssh.connect(args[0]);
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5); //every 60sec
|
||||
ssh.authPassword(args[1], args[2]);
|
||||
Session session = ssh.startSession();
|
||||
session.allocateDefaultPTY();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class RemotePF {
|
||||
client.loadKnownHosts();
|
||||
|
||||
client.connect("localhost");
|
||||
client.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
try {
|
||||
|
||||
client.authPublickey(System.getProperty("user.name"));
|
||||
@@ -33,8 +34,6 @@ public class RemotePF {
|
||||
// what we do with incoming connections that are forwarded to us
|
||||
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
|
||||
|
||||
client.getTransport().setHeartbeatInterval(30);
|
||||
|
||||
// Something to hang on to so that the forwarding stays
|
||||
client.getTransport().join();
|
||||
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
269
gradlew
vendored
269
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,78 +17,113 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -105,79 +140,95 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,28 +64,14 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
74
src/itest/groovy/com/hierynomus/sshj/ManyChannelsSpec.groovy
Normal file
74
src/itest/groovy/com/hierynomus/sshj/ManyChannelsSpec.groovy
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 com.hierynomus.sshj
|
||||
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.common.IOUtils
|
||||
import net.schmizz.sshj.connection.channel.direct.Session
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.util.concurrent.*
|
||||
|
||||
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
|
||||
|
||||
class ManyChannelsSpec extends Specification {
|
||||
|
||||
def "should work with many channels without nonexistent channel error (GH issue #805)"() {
|
||||
given:
|
||||
SshdContainer sshd = new SshdContainer.Builder()
|
||||
.withSshdConfig("""${SshdContainer.Builder.DEFAULT_SSHD_CONFIG}
|
||||
MaxSessions 200
|
||||
""".stripMargin())
|
||||
.build()
|
||||
sshd.start()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
||||
|
||||
when:
|
||||
List<Future<Exception>> futures = []
|
||||
ExecutorService executorService = Executors.newCachedThreadPool()
|
||||
|
||||
for (int i in 0..20) {
|
||||
futures.add(executorService.submit((Callable<Exception>) {
|
||||
return execute(client)
|
||||
}))
|
||||
}
|
||||
executorService.shutdown()
|
||||
executorService.awaitTermination(1, TimeUnit.DAYS)
|
||||
|
||||
then:
|
||||
futures*.get().findAll { it != null }.empty
|
||||
|
||||
cleanup:
|
||||
client.close()
|
||||
}
|
||||
|
||||
|
||||
private static Exception execute(SSHClient sshClient) {
|
||||
try {
|
||||
for (def i in 0..100) {
|
||||
withCloseable (sshClient.startSession()) {sshSession ->
|
||||
Session.Command sshCommand = sshSession.exec("ls -la")
|
||||
IOUtils.readFully(sshCommand.getInputStream()).toString()
|
||||
sshCommand.close()
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return e
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ public class ThreadNameProvider {
|
||||
public static void setThreadName(final Thread thread, final RemoteAddressProvider remoteAddressProvider) {
|
||||
final InetSocketAddress remoteSocketAddress = remoteAddressProvider.getRemoteSocketAddress();
|
||||
final String address = remoteSocketAddress == null ? DISCONNECTED : remoteSocketAddress.toString();
|
||||
final String threadName = String.format("sshj-%s-%s", thread.getClass().getSimpleName(), address);
|
||||
final long started = System.currentTimeMillis();
|
||||
final String threadName = String.format("sshj-%s-%s-%d", thread.getClass().getSimpleName(), address, started);
|
||||
thread.setName(threadName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ public class Promise<V, T extends Throwable> {
|
||||
}
|
||||
return val;
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw chainer.chain(ie);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
||||
@@ -35,14 +35,29 @@ public abstract class KeepAlive extends Thread {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeepAlive enabled based on KeepAlive interval
|
||||
*
|
||||
* @return Enabled when KeepInterval is greater than 0
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return keepAliveInterval > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KeepAlive interval in seconds
|
||||
*
|
||||
* @return KeepAlive interval in seconds defaults to 0
|
||||
*/
|
||||
public synchronized int getKeepAliveInterval() {
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set KeepAlive interval in seconds
|
||||
*
|
||||
* @param keepAliveInterval KeepAlive interval in seconds
|
||||
*/
|
||||
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
|
||||
this.keepAliveInterval = keepAliveInterval;
|
||||
}
|
||||
@@ -60,6 +75,8 @@ public abstract class KeepAlive extends Thread {
|
||||
TimeUnit.SECONDS.sleep(interval);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// this is almost certainly a planned interruption, but even so, no harm in setting the interrupt flag
|
||||
Thread.currentThread().interrupt();
|
||||
log.trace("{} Interrupted while sleeping", getClass().getSimpleName());
|
||||
} catch (Exception e) {
|
||||
// If we weren't interrupted, kill the transport, then this exception was unexpected.
|
||||
|
||||
@@ -36,11 +36,9 @@ import net.schmizz.sshj.transport.kex.Curve25519SHA256;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
||||
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
||||
import net.schmizz.sshj.transport.random.BouncyCastleRandom;
|
||||
import net.schmizz.sshj.transport.random.JCERandom;
|
||||
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
||||
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
|
||||
import org.slf4j.Logger;
|
||||
@@ -162,7 +160,6 @@ public class DefaultConfig
|
||||
setFileKeyProviderFactories(
|
||||
new OpenSSHKeyV1KeyFile.Factory(),
|
||||
new PKCS8KeyFile.Factory(),
|
||||
new PKCS5KeyFile.Factory(),
|
||||
new OpenSSHKeyFile.Factory(),
|
||||
new PuTTYKeyFile.Factory());
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
||||
import net.schmizz.sshj.transport.verification.FingerprintVerifier;
|
||||
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
|
||||
import net.schmizz.sshj.userauth.AuthResult;
|
||||
import net.schmizz.sshj.userauth.UserAuth;
|
||||
import net.schmizz.sshj.userauth.UserAuthException;
|
||||
import net.schmizz.sshj.userauth.UserAuthImpl;
|
||||
@@ -218,13 +219,30 @@ public class SSHClient
|
||||
throws UserAuthException, TransportException {
|
||||
checkConnected();
|
||||
final Deque<UserAuthException> savedEx = new LinkedList<UserAuthException>();
|
||||
for (AuthMethod method: methods) {
|
||||
final List<AuthMethod> tried = new LinkedList<AuthMethod>();
|
||||
|
||||
for (Iterator<AuthMethod> it = methods.iterator(); it.hasNext();) {
|
||||
AuthMethod method = it.next();
|
||||
method.setLoggerFactory(loggerFactory);
|
||||
|
||||
try {
|
||||
if (auth.authenticate(username, (Service) conn, method, trans.getTimeoutMs()))
|
||||
AuthResult result = auth.authenticate(username, (Service) conn, method, trans.getTimeoutMs());
|
||||
|
||||
if (result == AuthResult.SUCCESS) {
|
||||
return;
|
||||
} else if (result == AuthResult.PARTIAL) {
|
||||
// Put all remaining methods in the tried list, so that we can try them for the second round of authentication
|
||||
while (it.hasNext()) {
|
||||
tried.add(it.next());
|
||||
}
|
||||
|
||||
auth(username, tried);
|
||||
return;
|
||||
}
|
||||
tried.add(method);
|
||||
} catch (UserAuthException e) {
|
||||
savedEx.push(e);
|
||||
tried.add(method);
|
||||
}
|
||||
}
|
||||
throw new UserAuthException("Exhausted available authentication methods", savedEx.peek());
|
||||
@@ -551,7 +569,7 @@ public class SSHClient
|
||||
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
|
||||
* <ul>
|
||||
* <li>PKCS8 (OpenSSH uses this format)</li>
|
||||
* <li>PKCS5</li>
|
||||
* <li>PEM-encoded PKCS1</li>
|
||||
* <li>Putty keyfile</li>
|
||||
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
|
||||
* </ul>
|
||||
@@ -810,7 +828,12 @@ public class SSHClient
|
||||
ThreadNameProvider.setThreadName(conn.getKeepAlive(), trans);
|
||||
keepAliveThread.start();
|
||||
}
|
||||
doKex();
|
||||
if (trans.isKeyExchangeRequired()) {
|
||||
log.debug("Initiating Key Exchange for new connection");
|
||||
doKex();
|
||||
} else {
|
||||
log.debug("Key Exchange already completed for new connection");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -304,6 +304,25 @@ public abstract class AbstractChannel
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent CHANNEL_CLOSE to be sent between isOpen and a Transport.write call in the runnable, otherwise
|
||||
// a disconnect with a "packet referred to nonexistent channel" message can occur.
|
||||
//
|
||||
// This particularly happens when the transport.Reader thread passes an eof from the server to the
|
||||
// ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the
|
||||
// transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.
|
||||
boolean whileOpen(TransportRunnable runnable) throws TransportException, ConnectionException {
|
||||
openCloseLock.lock();
|
||||
try {
|
||||
if (isOpen()) {
|
||||
runnable.run();
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
openCloseLock.unlock();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void gotChannelRequest(SSHPacket buf)
|
||||
throws ConnectionException, TransportException {
|
||||
final String reqType;
|
||||
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
|
||||
+ rwin + " >";
|
||||
}
|
||||
|
||||
public interface TransportRunnable {
|
||||
void run() throws TransportException, ConnectionException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public final class ChannelInputStream
|
||||
try {
|
||||
buf.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw (IOException) new InterruptedIOException().initCause(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
*/
|
||||
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
|
||||
|
||||
private final Channel chan;
|
||||
private final AbstractChannel chan;
|
||||
private final Transport trans;
|
||||
private final Window.Remote win;
|
||||
|
||||
@@ -47,6 +47,12 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
|
||||
private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
|
||||
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
|
||||
private final AbstractChannel.TransportRunnable packetWriteRunnable = new AbstractChannel.TransportRunnable() {
|
||||
@Override
|
||||
public void run() throws TransportException {
|
||||
trans.write(packet);
|
||||
}
|
||||
};
|
||||
|
||||
DataBuffer() {
|
||||
headerOffset = packet.rpos();
|
||||
@@ -99,8 +105,9 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
if (leftOverBytes > 0) {
|
||||
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
|
||||
}
|
||||
|
||||
trans.write(packet);
|
||||
if (!chan.whileOpen(packetWriteRunnable)) {
|
||||
throwStreamClosed();
|
||||
}
|
||||
win.consume(writeNow);
|
||||
|
||||
packet.rpos(headerOffset);
|
||||
@@ -119,7 +126,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
|
||||
}
|
||||
|
||||
public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) {
|
||||
public ChannelOutputStream(AbstractChannel chan, Transport trans, Window.Remote win) {
|
||||
this.chan = chan;
|
||||
this.trans = trans;
|
||||
this.win = win;
|
||||
@@ -157,7 +164,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
if (error != null) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new ConnectionException("Stream closed");
|
||||
throwStreamClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,9 +172,14 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
// Not closed yet, and underlying channel is open to flush the data to.
|
||||
if (!closed.getAndSet(true) && chan.isOpen()) {
|
||||
buffer.flush(false);
|
||||
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
||||
if (!closed.getAndSet(true)) {
|
||||
chan.whileOpen(new AbstractChannel.TransportRunnable() {
|
||||
@Override
|
||||
public void run() throws TransportException, ConnectionException {
|
||||
buffer.flush(false);
|
||||
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,4 +200,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
return "< ChannelOutputStream for Channel #" + chan.getID() + " >";
|
||||
}
|
||||
|
||||
private static void throwStreamClosed() throws ConnectionException {
|
||||
throw new ConnectionException("Stream closed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ public abstract class Window {
|
||||
throw new ConnectionException("Timeout when trying to expand the window size");
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ConnectionException(ie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,16 @@ public class RemoteFile
|
||||
private boolean eof;
|
||||
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
||||
this(maxUnconfirmedReads, 0L, -1L);
|
||||
this(maxUnconfirmedReads, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
|
||||
* @param fileOffset Initial offset in file to read from
|
||||
*/
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
|
||||
this(maxUnconfirmedReads, fileOffset, -1L);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -232,21 +232,41 @@ public class SFTPClient
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(String source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest)
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
|
||||
@@ -50,25 +50,47 @@ public class SFTPFileTransfer
|
||||
@Override
|
||||
public void upload(String source, String dest)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest);
|
||||
upload(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest));
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener());
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest) throws IOException {
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest, long byteOffset) throws IOException {
|
||||
final PathComponents pathComponents = engine.getPathHelper().getComponents(source);
|
||||
final FileAttributes attributes = engine.stat(source);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest, byteOffset);
|
||||
}
|
||||
|
||||
public void setUploadFilter(LocalFileFilter uploadFilter) {
|
||||
@@ -92,7 +114,8 @@ public class SFTPFileTransfer
|
||||
@SuppressWarnings("PMD.MissingBreakInSwitch")
|
||||
private void download(final TransferListener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local) throws IOException {
|
||||
final LocalDestFile local,
|
||||
final long byteOffset) throws IOException {
|
||||
final LocalDestFile adjustedFile;
|
||||
switch (remote.getAttributes().getType()) {
|
||||
case DIRECTORY:
|
||||
@@ -101,8 +124,9 @@ public class SFTPFileTransfer
|
||||
case UNKNOWN:
|
||||
log.warn("Server did not supply information about the type of file at `{}` " +
|
||||
"-- assuming it is a regular file!", remote.getPath());
|
||||
// fall through
|
||||
case REGULAR:
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local);
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local, byteOffset);
|
||||
break;
|
||||
default:
|
||||
throw new IOException(remote + " is not a regular file or directory");
|
||||
@@ -119,7 +143,7 @@ public class SFTPFileTransfer
|
||||
final RemoteDirectory rd = engine.openDir(remote.getPath());
|
||||
try {
|
||||
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
|
||||
download(listener, rri, adjusted.getChild(rri.getName()));
|
||||
download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files
|
||||
} finally {
|
||||
rd.close();
|
||||
}
|
||||
@@ -128,13 +152,15 @@ public class SFTPFileTransfer
|
||||
|
||||
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local)
|
||||
final LocalDestFile local,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
|
||||
final RemoteFile rf = engine.open(remote.getPath());
|
||||
try {
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16);
|
||||
final OutputStream os = adjusted.getOutputStream();
|
||||
log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
|
||||
final OutputStream os = adjusted.getOutputStream(byteOffset != 0);
|
||||
try {
|
||||
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
||||
@@ -173,17 +199,17 @@ public class SFTPFileTransfer
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
private void upload(final TransferListener listener) throws IOException {
|
||||
private void upload(final TransferListener listener, long byteOffset) throws IOException {
|
||||
if (source.isDirectory()) {
|
||||
makeDirIfNotExists(remote); // Ensure that the directory exists
|
||||
uploadDir(listener.directory(source.getName()), source, remote);
|
||||
setAttributes(source, remote);
|
||||
} else if (source.isFile() && isDirectory(remote)) {
|
||||
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote, byteOffset);
|
||||
setAttributes(source, adjustedRemote);
|
||||
} else if (source.isFile()) {
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote, byteOffset);
|
||||
setAttributes(source, remote);
|
||||
} else {
|
||||
throw new IOException(source + " is not a file or directory");
|
||||
@@ -192,13 +218,14 @@ public class SFTPFileTransfer
|
||||
|
||||
private void upload(final TransferListener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjustedPath;
|
||||
if (local.isDirectory()) {
|
||||
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
|
||||
} else if (local.isFile()) {
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote);
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote, byteOffset);
|
||||
} else {
|
||||
throw new IOException(local + " is not a file or directory");
|
||||
}
|
||||
@@ -217,22 +244,34 @@ public class SFTPFileTransfer
|
||||
throws IOException {
|
||||
makeDirIfNotExists(remote);
|
||||
for (LocalSourceFile f : local.getChildren(getUploadFilter()))
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()));
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()), 0); // not supporting individual byte offsets for these files
|
||||
return remote;
|
||||
}
|
||||
|
||||
private String uploadFile(final StreamCopier.Listener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjusted = prepareFile(local, remote);
|
||||
final String adjusted = prepareFile(local, remote, byteOffset);
|
||||
RemoteFile rf = null;
|
||||
InputStream fis = null;
|
||||
RemoteFile.RemoteFileOutputStream rfos = null;
|
||||
EnumSet<OpenMode> modes;
|
||||
try {
|
||||
rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC));
|
||||
if (byteOffset == 0) {
|
||||
// Starting at the beginning, overwrite/create
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC);
|
||||
} else {
|
||||
// Starting at some offset, append
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND);
|
||||
}
|
||||
|
||||
log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset);
|
||||
rf = engine.open(adjusted, modes);
|
||||
fis = local.getInputStream();
|
||||
rfos = rf.new RemoteFileOutputStream(0, 16);
|
||||
fis.skip(byteOffset);
|
||||
rfos = rf.new RemoteFileOutputStream(byteOffset, 16);
|
||||
new StreamCopier(fis, rfos, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
|
||||
.keepFlushing(false)
|
||||
@@ -294,7 +333,7 @@ public class SFTPFileTransfer
|
||||
}
|
||||
}
|
||||
|
||||
private String prepareFile(final LocalSourceFile local, final String remote)
|
||||
private String prepareFile(final LocalSourceFile local, final String remote, final long byteOffset)
|
||||
throws IOException {
|
||||
final FileAttributes attrs;
|
||||
try {
|
||||
@@ -309,7 +348,7 @@ public class SFTPFileTransfer
|
||||
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
|
||||
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
|
||||
} else {
|
||||
log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType());
|
||||
log.debug("probeFile: {} is a {} file that will be {}", remote, attrs.getMode().getType(), byteOffset > 0 ? "resumed" : "replaced");
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ public interface Transport
|
||||
void doKex()
|
||||
throws TransportException;
|
||||
|
||||
/**
|
||||
* Is Key Exchange required based on current transport status
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
boolean isKeyExchangeRequired();
|
||||
|
||||
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
|
||||
String getClientVersion();
|
||||
|
||||
|
||||
@@ -254,6 +254,16 @@ public final class TransportImpl
|
||||
kexer.startKex(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Key Exchange required returns true when Key Exchange is not done and when Key Exchange is not ongoing
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
@Override
|
||||
public boolean isKeyExchangeRequired() {
|
||||
return !kexer.isKexDone() && !kexer.isKexOngoing();
|
||||
}
|
||||
|
||||
public boolean isKexDone() {
|
||||
return kexer.isKexDone();
|
||||
}
|
||||
|
||||
22
src/main/java/net/schmizz/sshj/userauth/AuthResult.java
Normal file
22
src/main/java/net/schmizz/sshj/userauth/AuthResult.java
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 net.schmizz.sshj.userauth;
|
||||
|
||||
public enum AuthResult {
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
PARTIAL
|
||||
}
|
||||
@@ -37,12 +37,12 @@ public interface UserAuth {
|
||||
* @param nextService the service to set on successful authentication
|
||||
* @param methods the {@link AuthMethod}'s to try
|
||||
*
|
||||
* @return whether authentication was successful
|
||||
* @return whether authentication was successful, failed, or partially successful
|
||||
*
|
||||
* @throws UserAuthException in case of authentication failure
|
||||
* @throws TransportException if there was a transport-layer error
|
||||
*/
|
||||
boolean authenticate(String username, Service nextService, AuthMethod methods, int timeoutMs)
|
||||
AuthResult authenticate(String username, Service nextService, AuthMethod methods, int timeoutMs)
|
||||
throws UserAuthException, TransportException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ public class UserAuthImpl
|
||||
extends AbstractService
|
||||
implements UserAuth {
|
||||
|
||||
private final Promise<Boolean, UserAuthException> authenticated;
|
||||
private final Promise<AuthResult, UserAuthException> authenticated;
|
||||
|
||||
// Externally available
|
||||
private volatile String banner = "";
|
||||
@@ -53,13 +53,13 @@ public class UserAuthImpl
|
||||
|
||||
public UserAuthImpl(Transport trans) {
|
||||
super("ssh-userauth", trans);
|
||||
authenticated = new Promise<Boolean, UserAuthException>("authenticated", UserAuthException.chainer, trans.getConfig().getLoggerFactory());
|
||||
authenticated = new Promise<AuthResult, UserAuthException>("authenticated", UserAuthException.chainer, trans.getConfig().getLoggerFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authenticate(String username, Service nextService, AuthMethod method, int timeoutMs)
|
||||
public AuthResult authenticate(String username, Service nextService, AuthMethod method, int timeoutMs)
|
||||
throws UserAuthException, TransportException {
|
||||
final boolean outcome;
|
||||
final AuthResult outcome;
|
||||
|
||||
authenticated.lock();
|
||||
try {
|
||||
@@ -73,8 +73,10 @@ public class UserAuthImpl
|
||||
currentMethod.request();
|
||||
outcome = authenticated.retrieve(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
|
||||
if (outcome) {
|
||||
if (outcome == AuthResult.SUCCESS) {
|
||||
log.debug("`{}` auth successful", method.getName());
|
||||
} else if (outcome == AuthResult.PARTIAL) {
|
||||
log.debug("`{}` auth partially successful", method.getName());
|
||||
} else {
|
||||
log.debug("`{}` auth failed", method.getName());
|
||||
}
|
||||
@@ -124,7 +126,7 @@ public class UserAuthImpl
|
||||
// Should fix https://github.com/hierynomus/sshj/issues/237
|
||||
trans.setAuthenticated(); // So it can put delayed compression into force if applicable
|
||||
trans.setService(nextService); // We aren't in charge anymore, next service is
|
||||
authenticated.deliver(true);
|
||||
authenticated.deliver(AuthResult.SUCCESS);
|
||||
break;
|
||||
|
||||
case USERAUTH_FAILURE:
|
||||
@@ -133,7 +135,7 @@ public class UserAuthImpl
|
||||
if (allowedMethods.contains(currentMethod.getName()) && currentMethod.shouldRetry()) {
|
||||
currentMethod.request();
|
||||
} else {
|
||||
authenticated.deliver(false);
|
||||
authenticated.deliver(partialSuccess ? AuthResult.PARTIAL : AuthResult.FAILURE);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
/**
|
||||
* @version $Id:$
|
||||
* Key File Formats
|
||||
*/
|
||||
public enum KeyFormat {
|
||||
PKCS5,
|
||||
PKCS8,
|
||||
OpenSSH,
|
||||
OpenSSHv1,
|
||||
|
||||
@@ -27,9 +27,9 @@ public class KeyProviderUtil {
|
||||
* <p/>
|
||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
||||
*
|
||||
* @param location
|
||||
* @param location File Path to key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(File location)
|
||||
throws IOException {
|
||||
@@ -45,7 +45,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key stored in a string
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -60,7 +60,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key accessible through a {@code Reader}
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -94,10 +94,8 @@ public class KeyProviderUtil {
|
||||
} else if (separatePubKey) {
|
||||
// Can delay asking for password since have unencrypted pubkey
|
||||
return KeyFormat.OpenSSH;
|
||||
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
|
||||
return KeyFormat.PKCS8;
|
||||
} else {
|
||||
return KeyFormat.PKCS5;
|
||||
return KeyFormat.PKCS8;
|
||||
}
|
||||
} else if (header.startsWith("PuTTY-User-Key-File-")) {
|
||||
return KeyFormat.PuTTY;
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
||||
import net.schmizz.sshj.common.Base64;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.transport.cipher.*;
|
||||
import net.schmizz.sshj.transport.digest.Digest;
|
||||
import net.schmizz.sshj.transport.digest.MD5;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc.
|
||||
*/
|
||||
public class PKCS5KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
|
||||
@Override
|
||||
public FileKeyProvider create() {
|
||||
return new PKCS5KeyFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PKCS5";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a format issue with PKCS5 data
|
||||
*/
|
||||
public static class FormatException
|
||||
extends IOException {
|
||||
|
||||
FormatException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a problem decrypting the data
|
||||
*/
|
||||
public static class DecryptException
|
||||
extends IOException {
|
||||
|
||||
DecryptException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] data;
|
||||
|
||||
protected KeyPair readKeyPair()
|
||||
throws IOException {
|
||||
|
||||
BufferedReader reader = new BufferedReader(resource.getReader());
|
||||
try {
|
||||
String line = null;
|
||||
Cipher cipher = new NoneCipher();
|
||||
StringBuffer sb = new StringBuffer();
|
||||
byte[] iv = new byte[0]; // salt
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) {
|
||||
int end = line.length() - 17;
|
||||
if (end > 11) {
|
||||
String s = line.substring(11, line.length() - 17);
|
||||
if ("RSA".equals(s)) {
|
||||
type = KeyType.RSA;
|
||||
} else if ("DSA".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else if ("DSS".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else {
|
||||
throw new FormatException("Unrecognized PKCS5 key type");
|
||||
}
|
||||
} else {
|
||||
throw new FormatException("Bad header; possibly PKCS8 format?");
|
||||
}
|
||||
} else if (line.startsWith("-----END")) {
|
||||
break;
|
||||
} else if (type != null) {
|
||||
if (line.startsWith("Proc-Type: ")) {
|
||||
if (!"4,ENCRYPTED".equals(line.substring(11))) {
|
||||
throw new FormatException("Unrecognized Proc-Type");
|
||||
}
|
||||
} else if (line.startsWith("DEK-Info: ")) {
|
||||
int ptr = line.indexOf(",");
|
||||
if (ptr == -1) {
|
||||
throw new FormatException("Unrecognized DEK-Info");
|
||||
} else {
|
||||
String algorithm = line.substring(10, ptr);
|
||||
if ("DES-EDE3-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.TripleDESCBC().create();
|
||||
} else if ("AES-128-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES128CBC().create();
|
||||
} else if ("AES-192-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES192CBC().create();
|
||||
} else if ("AES-256-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES256CBC().create();
|
||||
} else {
|
||||
throw new FormatException("Not a supported algorithm: " + algorithm);
|
||||
}
|
||||
iv = Arrays.copyOfRange(ByteArrayUtils.parseHex(line.substring(ptr + 1)), 0, cipher.getIVSize());
|
||||
}
|
||||
} else if (line.length() > 0) {
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == null) {
|
||||
throw new FormatException("PKCS5 header not found");
|
||||
}
|
||||
ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv));
|
||||
switch (type) {
|
||||
case RSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.RSA);
|
||||
asn.readNext();
|
||||
BigInteger modulus = asn.readNext();
|
||||
BigInteger pubExp = asn.readNext();
|
||||
BigInteger prvExp = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp));
|
||||
PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
case DSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.DSA);
|
||||
asn.readNext();
|
||||
BigInteger p = asn.readNext();
|
||||
BigInteger q = asn.readNext();
|
||||
BigInteger g = asn.readNext();
|
||||
BigInteger pub = asn.readNext();
|
||||
BigInteger prv = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g));
|
||||
PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
default:
|
||||
throw new IOException("Unrecognized PKCS5 key type: " + type);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PKCS5KeyFile{resource=" + resource + "}";
|
||||
}
|
||||
|
||||
private byte[] getPassphraseBytes() {
|
||||
CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource));
|
||||
ByteBuffer bb = IOUtils.UTF8.encode(cb);
|
||||
byte[] result = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit());
|
||||
Arrays.fill(cb.array(), '\u0000');
|
||||
Arrays.fill(bb.array(), (byte) 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] raw, Cipher cipher, byte[] iv) throws DecryptException {
|
||||
if (pwdf == null) {
|
||||
return raw;
|
||||
}
|
||||
Digest md5 = new MD5();
|
||||
int bsize = cipher.getBlockSize();
|
||||
int hsize = md5.getBlockSize();
|
||||
int hnlen = bsize / hsize * hsize + (bsize % hsize == 0 ? 0 : hsize);
|
||||
do {
|
||||
md5.init();
|
||||
byte[] hn = new byte[hnlen];
|
||||
byte[] tmp = null;
|
||||
byte[] passphrase = getPassphraseBytes();
|
||||
for (int i = 0; i + hsize <= hn.length; ) {
|
||||
if (tmp != null) {
|
||||
md5.update(tmp, 0, tmp.length);
|
||||
}
|
||||
md5.update(passphrase, 0, passphrase.length);
|
||||
md5.update(iv, 0, iv.length > 8 ? 8 : iv.length);
|
||||
tmp = md5.digest();
|
||||
System.arraycopy(tmp, 0, hn, i, tmp.length);
|
||||
i += tmp.length;
|
||||
}
|
||||
Arrays.fill(passphrase, (byte) 0);
|
||||
byte[] key = Arrays.copyOfRange(hn, 0, bsize);
|
||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||
Arrays.fill(key, (byte) 0);
|
||||
byte[] decrypted = Arrays.copyOf(raw, raw.length);
|
||||
cipher.update(decrypted, 0, decrypted.length);
|
||||
if (ASN1Data.MAGIC == decrypted[0]) {
|
||||
return decrypted;
|
||||
}
|
||||
} while (pwdf.shouldRetry(resource));
|
||||
throw new DecryptException("Decryption failed");
|
||||
}
|
||||
|
||||
class ASN1Data {
|
||||
static final byte MAGIC = (byte) 0x30;
|
||||
|
||||
private byte[] buff;
|
||||
private int index, length;
|
||||
|
||||
ASN1Data(byte[] buff) throws FormatException {
|
||||
this.buff = buff;
|
||||
index = 0;
|
||||
if (buff[index++] != MAGIC) {
|
||||
throw new FormatException("Not ASN.1 data");
|
||||
}
|
||||
length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
if ((index + length) > buff.length) {
|
||||
throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
|
||||
}
|
||||
}
|
||||
|
||||
BigInteger readNext() throws IOException {
|
||||
if (index >= length) {
|
||||
throw new EOFException();
|
||||
} else if (buff[index++] != 0x02) {
|
||||
throw new IOException("Not an int code: " + Integer.toHexString(0xff & buff[index]));
|
||||
}
|
||||
int length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
byte[] sequence = new byte[length];
|
||||
System.arraycopy(buff, index, sequence, 0, length);
|
||||
index += length;
|
||||
return new BigInteger(sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,9 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
|
||||
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
|
||||
/**
|
||||
* Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption
|
||||
*/
|
||||
public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
|
||||
@@ -84,8 +84,7 @@ public class AuthGssApiWithMic
|
||||
|
||||
@Override
|
||||
public GSSContext run() throws GSSException {
|
||||
GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME);
|
||||
GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
|
||||
GSSCredential clientCreds = manager.createCredential(GSSCredential.INITIATE_ONLY);
|
||||
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
|
||||
|
||||
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
|
||||
|
||||
@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
|
||||
public class PasswordResponseProvider
|
||||
implements ChallengeResponseProvider {
|
||||
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword:\\s?\\z", Pattern.DOTALL);
|
||||
// FreeBSD prompt is "Password for user@host:"
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword(?: for .*)?:\\s?\\z", Pattern.DOTALL);
|
||||
|
||||
private static final char[] EMPTY_RESPONSE = new char[0];
|
||||
|
||||
|
||||
@@ -71,7 +71,13 @@ public class FileSystemFile
|
||||
@Override
|
||||
public OutputStream getOutputStream()
|
||||
throws IOException {
|
||||
return new FileOutputStream(file);
|
||||
return getOutputStream(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream(boolean append)
|
||||
throws IOException {
|
||||
return new FileOutputStream(file, append);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,19 @@ public interface FileTransfer {
|
||||
void upload(String localPath, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #upload(LocalSourceFile, String)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(String localPath, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}.
|
||||
@@ -43,6 +56,19 @@ public interface FileTransfer {
|
||||
void download(String remotePath, String localPath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, String localPath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}.
|
||||
*
|
||||
@@ -54,6 +80,18 @@ public interface FileTransfer {
|
||||
void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}.
|
||||
*
|
||||
@@ -65,6 +103,18 @@ public interface FileTransfer {
|
||||
void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
TransferListener getTransferListener();
|
||||
|
||||
void setTransferListener(TransferListener listener);
|
||||
|
||||
@@ -20,8 +20,13 @@ import java.io.OutputStream;
|
||||
|
||||
public interface LocalDestFile {
|
||||
|
||||
long getLength();
|
||||
|
||||
OutputStream getOutputStream()
|
||||
throws IOException;
|
||||
|
||||
OutputStream getOutputStream(boolean append)
|
||||
throws IOException;
|
||||
|
||||
/** @return A child file/directory of this directory with given {@code name}. */
|
||||
LocalDestFile getChild(String name);
|
||||
|
||||
@@ -52,24 +52,49 @@ public class SCPFileTransfer
|
||||
@Override
|
||||
public void upload(String localPath, String remotePath)
|
||||
throws IOException {
|
||||
newSCPUploadClient().copy(new FileSystemFile(localPath), remotePath);
|
||||
upload(localPath, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(localFile), remotePath, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath)
|
||||
throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath));
|
||||
download(remotePath, localPath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath, long byteOffset) throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException {
|
||||
download(remotePath, localFile, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPDownloadClient().copy(remotePath, localFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException {
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPUploadClient().copy(localFile, remotePath);
|
||||
}
|
||||
|
||||
@@ -79,4 +104,11 @@ public class SCPFileTransfer
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void checkByteOffsetSupport(long byteOffset) throws IOException {
|
||||
// TODO - implement byte offsets on SCP, if possible.
|
||||
if (byteOffset > 0) {
|
||||
throw new SCPException("Byte offset on SCP file transfers is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class FileKeyProviderSpec extends Specification {
|
||||
|
||||
where:
|
||||
format | keyfile
|
||||
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5"
|
||||
KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
|
||||
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,11 @@ public class KeepAliveThreadTerminationTest {
|
||||
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||
|
||||
fixture.connectClient(sshClient);
|
||||
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
|
||||
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("bad", "credentials"));
|
||||
|
||||
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
|
||||
|
||||
fixture.stopClient();
|
||||
Thread.sleep(STOP_SLEEP);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
|
||||
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
|
||||
import org.apache.sshd.common.util.OsUtils;
|
||||
import org.apache.sshd.scp.server.ScpCommandFactory;
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.server.shell.ProcessShellFactory;
|
||||
@@ -38,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class SshFixture extends ExternalResource {
|
||||
public static final String hostkey = "hostkey.pem";
|
||||
public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db";
|
||||
public static final String listCommand = OsUtils.isWin32() ? "cmd.exe /C dir" : "ls";
|
||||
|
||||
private final SshServer server = defaultSshServer();
|
||||
private SSHClient client = null;
|
||||
@@ -110,7 +112,7 @@ public class SshFixture extends ExternalResource {
|
||||
ScpCommandFactory commandFactory = new ScpCommandFactory();
|
||||
commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
|
||||
sshServer.setCommandFactory(commandFactory);
|
||||
sshServer.setShellFactory(new ProcessShellFactory("ls", "ls"));
|
||||
sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
|
||||
return sshServer;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,8 @@ public class FileUtil {
|
||||
IOUtils.closeQuietly(fileInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean compareFileContents(File f1, File f2) throws IOException {
|
||||
return readFromFile(f1).equals(readFromFile(f2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 com.hierynomus.sshj.userauth.method;
|
||||
|
||||
import net.schmizz.sshj.userauth.method.PasswordResponseProvider;
|
||||
import net.schmizz.sshj.userauth.password.AccountResource;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PasswordResponseProviderTest {
|
||||
private static final char[] PASSWORD = "the_password".toCharArray();
|
||||
private static final AccountResource ACCOUNT_RESOURCE = new AccountResource("user", "host");
|
||||
|
||||
@Test
|
||||
public void shouldMatchCommonPrompts() {
|
||||
PasswordResponseProvider responseProvider = createDefaultResponseProvider(false);
|
||||
shouldMatch(responseProvider, "Password: ");
|
||||
shouldMatch(responseProvider, "password: ");
|
||||
shouldMatch(responseProvider, "Password:");
|
||||
shouldMatch(responseProvider, "password:");
|
||||
shouldMatch(responseProvider, "user@host's Password: ");
|
||||
shouldMatch(responseProvider, "user@host's password: ");
|
||||
shouldMatch(responseProvider, "user@host's Password:");
|
||||
shouldMatch(responseProvider, "user@host's password:");
|
||||
shouldMatch(responseProvider, "user@host: Password: ");
|
||||
shouldMatch(responseProvider, "(user@host) Password: ");
|
||||
shouldMatch(responseProvider, "any prefix Password for user@host: ");
|
||||
shouldMatch(responseProvider, "any prefix password for user@host: ");
|
||||
shouldMatch(responseProvider, "any prefix Password for user@host:");
|
||||
shouldMatch(responseProvider, "any prefix password for user@host:");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMatchOtherPrompts() {
|
||||
PasswordResponseProvider responseProvider = createDefaultResponseProvider(false);
|
||||
shouldNotMatch(responseProvider, "Password");
|
||||
shouldNotMatch(responseProvider, "password");
|
||||
shouldNotMatch(responseProvider, "Password: ");
|
||||
shouldNotMatch(responseProvider, "password: suffix");
|
||||
shouldNotMatch(responseProvider, "Password of user@host:");
|
||||
shouldNotMatch(responseProvider, "");
|
||||
shouldNotMatch(responseProvider, "password :");
|
||||
shouldNotMatch(responseProvider, "something else");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPassRetry() {
|
||||
Assert.assertFalse(createDefaultResponseProvider(false).shouldRetry());
|
||||
Assert.assertTrue(createDefaultResponseProvider(true).shouldRetry());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHaveNoSubmethods() {
|
||||
Assert.assertEquals(createDefaultResponseProvider(true).getSubmethods(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWorkWithCustomPattern() {
|
||||
PasswordFinder passwordFinder = new TestPasswordFinder(true);
|
||||
PasswordResponseProvider responseProvider = new PasswordResponseProvider(passwordFinder, Pattern.compile(".*custom.*"));
|
||||
responseProvider.init(ACCOUNT_RESOURCE, "name", "instruction");
|
||||
shouldMatch(responseProvider, "prefix custom suffix: ");
|
||||
shouldNotMatch(responseProvider, "something else");
|
||||
}
|
||||
|
||||
private static void shouldMatch(PasswordResponseProvider responseProvider, String prompt) {
|
||||
checkPrompt(responseProvider, prompt, PASSWORD);
|
||||
}
|
||||
|
||||
private static void shouldNotMatch(PasswordResponseProvider responseProvider, String prompt) {
|
||||
checkPrompt(responseProvider, prompt, new char[0]);
|
||||
}
|
||||
|
||||
private static void checkPrompt(PasswordResponseProvider responseProvider, String prompt, char[] expected) {
|
||||
Assert.assertArrayEquals("Prompt '" + prompt + "'", expected, responseProvider.getResponse(prompt, false));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static PasswordResponseProvider createDefaultResponseProvider(final boolean shouldRetry) {
|
||||
PasswordFinder passwordFinder = new TestPasswordFinder(shouldRetry);
|
||||
PasswordResponseProvider responseProvider = new PasswordResponseProvider(passwordFinder);
|
||||
responseProvider.init(ACCOUNT_RESOURCE, "name", "instruction");
|
||||
return responseProvider;
|
||||
}
|
||||
|
||||
private static class TestPasswordFinder implements PasswordFinder {
|
||||
private final boolean shouldRetry;
|
||||
|
||||
public TestPasswordFinder(boolean shouldRetry) {
|
||||
this.shouldRetry = shouldRetry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] reqPassword(Resource<?> resource) {
|
||||
Assert.assertEquals(resource, ACCOUNT_RESOURCE);
|
||||
return PASSWORD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry(Resource<?> resource) {
|
||||
Assert.assertEquals(resource, ACCOUNT_RESOURCE);
|
||||
return shouldRetry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class LoadsOfConnects {
|
||||
SSHClient client = fixture.setupConnectedDefaultClient();
|
||||
client.authPassword("test", "test");
|
||||
Session s = client.startSession();
|
||||
Session.Command c = s.exec("ls");
|
||||
Session.Command c = s.exec(SshFixture.listCommand);
|
||||
IOUtils.readFully(c.getErrorStream());
|
||||
IOUtils.readFully(c.getInputStream());
|
||||
c.close();
|
||||
|
||||
@@ -35,9 +35,9 @@ public class KeyProviderUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs5() throws IOException {
|
||||
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs5"));
|
||||
assertEquals(KeyFormat.PKCS5, format);
|
||||
public void testPkcs1Rsa() throws IOException {
|
||||
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs1-rsa"));
|
||||
assertEquals(KeyFormat.PKCS8, format);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 net.schmizz.sshj.keyprovider;
|
||||
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import net.schmizz.sshj.util.KeyUtil;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class PKCS5KeyFileTest {
|
||||
|
||||
static final FileKeyProvider rsa = new PKCS5KeyFile();
|
||||
|
||||
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
|
||||
static final String pubExp = "23";
|
||||
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
|
||||
|
||||
final char[] correctPassphrase = "passphrase".toCharArray();
|
||||
final char[] incorrectPassphrase = "incorrect".toCharArray();
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||
rsa.init(new File("src/test/resources/id_rsa"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeys()
|
||||
throws IOException, GeneralSecurityException {
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testType()
|
||||
throws IOException {
|
||||
assertEquals(rsa.getType(), KeyType.RSA);
|
||||
}
|
||||
|
||||
final PasswordFinder givesOn3rdTry = new PasswordFinder() {
|
||||
int triesLeft = 3;
|
||||
|
||||
@Override
|
||||
public char[] reqPassword(Resource resource) {
|
||||
if (triesLeft == 0)
|
||||
return correctPassphrase;
|
||||
else {
|
||||
triesLeft--;
|
||||
return incorrectPassphrase;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry(Resource resource) {
|
||||
return triesLeft >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void retries()
|
||||
throws IOException, GeneralSecurityException {
|
||||
FileKeyProvider rsa = new PKCS5KeyFile();
|
||||
rsa.init(new File("src/test/resources/rsa.pk5"), givesOn3rdTry);
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate());
|
||||
}
|
||||
}
|
||||
@@ -15,110 +15,135 @@
|
||||
*/
|
||||
package net.schmizz.sshj.keyprovider;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import net.schmizz.sshj.util.KeyUtil;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class PKCS8KeyFileTest {
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
static final FileKeyProvider rsa = new PKCS8KeyFile();
|
||||
|
||||
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
|
||||
static final String pubExp = "23";
|
||||
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
|
||||
static final String KEY_PASSPHRASE = "passphrase";
|
||||
static final String INCORRECT_PASSPHRASE = String.class.getSimpleName();
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||
if (!SecurityUtils.isBouncyCastleRegistered())
|
||||
throw new AssertionError("bouncy castle needed");
|
||||
rsa.init(new File("src/test/resources/id_rsa"));
|
||||
@Test
|
||||
public void testKeys() throws GeneralSecurityException, IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(new File("src/test/resources/id_rsa"));
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
|
||||
assertEquals(provider.getType(), KeyType.RSA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeys()
|
||||
throws IOException, GeneralSecurityException {
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate());
|
||||
public void testPkcs1Rsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getFile("pkcs1-rsa"));
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testType()
|
||||
throws IOException {
|
||||
assertEquals(rsa.getType(), KeyType.RSA);
|
||||
public void testPkcs1Encrypted() throws IOException, GeneralSecurityException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getFile("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray()));
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Rsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-rsa-2048"));
|
||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-rsa-2048"));
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncrypted() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray());
|
||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray());
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncryptedIncorrectPassword() throws IOException {
|
||||
expectedException.expect(KeyDecryptionFailedException.class);
|
||||
|
||||
public void testPkcs8RsaEncryptedIncorrectPassword() {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray());
|
||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
provider.getPrivate();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(INCORRECT_PASSPHRASE.toCharArray());
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertThrows(KeyDecryptionFailedException.class, provider::getPrivate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncryptedRetryPassword() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = new PasswordFinder() {
|
||||
private boolean retryEnabled = true;
|
||||
|
||||
@Override
|
||||
public char[] reqPassword(Resource<?> resource) {
|
||||
final char[] password;
|
||||
if (retryEnabled) {
|
||||
password = INCORRECT_PASSPHRASE.toCharArray();
|
||||
} else {
|
||||
password = KEY_PASSPHRASE.toCharArray();
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry(Resource<?> resource) {
|
||||
final boolean shouldRetry = retryEnabled;
|
||||
if (retryEnabled) {
|
||||
retryEnabled = false;
|
||||
}
|
||||
return shouldRetry;
|
||||
}
|
||||
};
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Ecdsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-ecdsa"));
|
||||
assertEquals("ECDSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("ECDSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-ecdsa"));
|
||||
assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Dsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-dsa"));
|
||||
assertEquals("DSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("DSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-dsa"));
|
||||
assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.DSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
private Reader getReader(final String filename) {
|
||||
private File getFile(final String filename) {
|
||||
final String path = String.format("/keyformats/%s", filename);
|
||||
final InputStream inputStream = getClass().getResourceAsStream(path);
|
||||
if (inputStream == null) {
|
||||
final URL resource = getClass().getResource(path);
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException(String.format("Key File [%s] not found", path));
|
||||
}
|
||||
return new InputStreamReader(inputStream);
|
||||
return new File(resource.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
185
src/test/java/net/schmizz/sshj/sftp/SFTPFileTransferTest.java
Normal file
185
src/test/java/net/schmizz/sshj/sftp/SFTPFileTransferTest.java
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed 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 net.schmizz.sshj.sftp;
|
||||
|
||||
import com.hierynomus.sshj.test.SshFixture;
|
||||
import com.hierynomus.sshj.test.util.FileUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.common.StreamCopier;
|
||||
import net.schmizz.sshj.xfer.TransferListener;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SFTPFileTransferTest {
|
||||
|
||||
public static final String TARGET_FILE_NAME = "target.txt";
|
||||
|
||||
File targetDir;
|
||||
File targetFile;
|
||||
File sourceFile;
|
||||
|
||||
File partialFile;
|
||||
|
||||
SSHClient sshClient;
|
||||
SFTPFileTransfer xfer;
|
||||
ByteCounter listener;
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
@Before
|
||||
public void init() throws IOException {
|
||||
targetDir = tempFolder.newFolder();
|
||||
targetFile = new File(targetDir, TARGET_FILE_NAME);
|
||||
sourceFile = new File("src/test/resources/files/test_file_full.txt");
|
||||
|
||||
partialFile = new File("src/test/resources/files/test_file_partial.txt");
|
||||
|
||||
sshClient = fixture.setupConnectedDefaultClient();
|
||||
sshClient.authPassword("test", "test");
|
||||
xfer = sshClient.newSFTPClient().getFileTransfer();
|
||||
xfer.setTransferListener(listener = new ByteCounter());
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
|
||||
if (targetDir.exists()) {
|
||||
targetDir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void performDownload(long byteOffset) throws IOException {
|
||||
assertTrue(listener.getBytesTransferred() == 0);
|
||||
|
||||
long expectedBytes = 0;
|
||||
|
||||
// Using the resume param this way to call the different entry points into the FileTransfer interface
|
||||
if (byteOffset > 0) {
|
||||
expectedBytes = sourceFile.length() - targetFile.length(); // only the difference between what is there and what should be
|
||||
xfer.download(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), byteOffset);
|
||||
} else {
|
||||
expectedBytes = sourceFile.length(); // the entire source file should be transferred
|
||||
xfer.download(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
assertTrue(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
assertTrue(listener.getBytesTransferred() == expectedBytes);
|
||||
}
|
||||
|
||||
private void performUpload(long byteOffset) throws IOException {
|
||||
assertTrue(listener.getBytesTransferred() == 0);
|
||||
|
||||
long expectedBytes = 0;
|
||||
|
||||
// Using the resume param this way to call the different entry points into the FileTransfer interface
|
||||
if (byteOffset > 0) {
|
||||
expectedBytes = sourceFile.length() - targetFile.length(); // only the difference between what is there and what should be
|
||||
xfer.upload(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), byteOffset);
|
||||
} else {
|
||||
expectedBytes = sourceFile.length(); // the entire source file should be transferred
|
||||
xfer.upload(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
|
||||
}
|
||||
assertTrue(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
assertTrue(listener.getBytesTransferred() == expectedBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownload() throws IOException {
|
||||
performDownload(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadResumePartial() throws IOException {
|
||||
FileUtil.writeToFile(targetFile, FileUtil.readFromFile(partialFile));
|
||||
assertFalse(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
performDownload(targetFile.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadResumeNothing() throws IOException {
|
||||
assertFalse(targetFile.exists());
|
||||
performDownload(targetFile.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadResumePreviouslyCompleted() throws IOException {
|
||||
FileUtil.writeToFile(targetFile, FileUtil.readFromFile(sourceFile));
|
||||
assertTrue(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
performDownload(targetFile.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpload() throws IOException {
|
||||
performUpload(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadResumePartial() throws IOException {
|
||||
FileUtil.writeToFile(targetFile, FileUtil.readFromFile(partialFile));
|
||||
assertFalse(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
performUpload(targetFile.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadResumeNothing() throws IOException {
|
||||
assertFalse(targetFile.exists());
|
||||
performUpload(targetFile.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadResumePreviouslyCompleted() throws IOException {
|
||||
FileUtil.writeToFile(targetFile, FileUtil.readFromFile(sourceFile));
|
||||
assertTrue(FileUtil.compareFileContents(sourceFile, targetFile));
|
||||
performUpload(targetFile.length());
|
||||
}
|
||||
|
||||
public class ByteCounter implements TransferListener, StreamCopier.Listener {
|
||||
long bytesTransferred;
|
||||
|
||||
public long getBytesTransferred() {
|
||||
return bytesTransferred;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransferListener directory(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamCopier.Listener file(String name, long size) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportProgress(long transferred) throws IOException {
|
||||
bytesTransferred = transferred;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/test/resources/files/test_file_full.txt
Normal file
10
src/test/resources/files/test_file_full.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
6
src/test/resources/files/test_file_partial.txt
Normal file
6
src/test/resources/files/test_file_partial.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
|
||||
abcdefghijklmnopqrstuvwxyzABCDEF
|
||||
Reference in New Issue
Block a user