mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7dd73b9c8 | ||
|
|
d628c47bae | ||
|
|
6e7fb96d07 | ||
|
|
d5d6096d5d | ||
|
|
2551f8e559 | ||
|
|
430cbfcf13 | ||
|
|
ec467a3875 | ||
|
|
1b258f0677 | ||
|
|
559384ac91 | ||
|
|
5674072666 | ||
|
|
f33bfecbf5 | ||
|
|
c0f6000ff5 | ||
|
|
3de0302c84 | ||
|
|
d7e402c557 | ||
|
|
8ef996b406 | ||
|
|
e9cb90901c | ||
|
|
69812e9a81 | ||
|
|
9a939d029b | ||
|
|
50efeb6519 | ||
|
|
aabb1be52e | ||
|
|
32329e547e | ||
|
|
8cf63a96a9 | ||
|
|
cab7731928 | ||
|
|
50073db6c1 | ||
|
|
90099bbf5e | ||
|
|
ce0a7d5193 | ||
|
|
ced27fc898 | ||
|
|
624747c527 | ||
|
|
d8697c2228 | ||
|
|
7c14098f7d | ||
|
|
d5805a6c64 | ||
|
|
8a66dc5336 | ||
|
|
a5c10ab50f |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
|||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
|
src/itest/docker-image/** eol=lf
|
||||||
|
|||||||
2
.github/workflows/gradle.yml
vendored
2
.github/workflows/gradle.yml
vendored
@@ -24,6 +24,8 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew check
|
run: ./gradlew check
|
||||||
|
- name: Codecov
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
|
||||||
integration:
|
integration:
|
||||||
name: Integration test
|
name: Integration test
|
||||||
|
|||||||
28
README.adoc
28
README.adoc
@@ -1,7 +1,7 @@
|
|||||||
= sshj - SSHv2 library for Java
|
= sshj - SSHv2 library for Java
|
||||||
Jeroen van Erp
|
Jeroen van Erp
|
||||||
:sshj_groupid: com.hierynomus
|
:sshj_groupid: com.hierynomus
|
||||||
:sshj_version: 0.31.0
|
:sshj_version: 0.32.0
|
||||||
:source-highlighter: pygments
|
:source-highlighter: pygments
|
||||||
|
|
||||||
image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"]
|
image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"]
|
||||||
@@ -104,8 +104,32 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
|
|||||||
Fork away!
|
Fork away!
|
||||||
|
|
||||||
== Release history
|
== 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.
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/741[#741]: Add support for testcontainers in test setup to test more scenarios
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/733[#733]: Send correct key proposal if client knows CA key
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/746[#746]: Fix bug in reading Putty private key file with passphrase
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/742[#742]: Use Config.keyAlgorithms to determine rsa-sha2 support
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/754[#754]: Use SFTP protocol version to set FXP rename flags conditionally
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/752[#752]: Correctly start and terminate KeepAlive thread
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/753[#753]: Provide better thread names
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/724[#724]: Add parameter to limit read ahead length
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/763[#763]: Try all public key algorithms for a specific key type
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/756[#756]: Remove deprecated proxy connect methods
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/770[#770]: Add support for `ed25519` `aes-128-cbc` keys
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/773[#773]: Fix NPE when reading empty OpenSSHKeyV1KeyFile
|
||||||
|
* Merged https://github.com/hierynomus/sshj/pull/777[#777]: Don't request too many read-ahead packets
|
||||||
|
|
||||||
SSHJ 0.32.0 (2021-10-12)::
|
SSHJ 0.32.0 (2021-10-12)::
|
||||||
* Send EOF on channel close (Fixes https://github.com/hierynomus/sshj/issue/143[#143], https://github.com/hierynomus/sshj/issue/496[#496], https://github.com/hierynomus/sshj/issue/553[#553], https://github.com/hierynomus/sshj/issue/554[#554])
|
* Send EOF on channel close (Fixes https://github.com/hierynomus/sshj/issues/143[#143], https://github.com/hierynomus/sshj/issues/496[#496], https://github.com/hierynomus/sshj/issues/553[#553], https://github.com/hierynomus/sshj/issues/554[#554])
|
||||||
* Merged https://github.com/hierynomus/sshj/pull/726[#726]: Parse OpenSSH v1 keys with full CRT information present
|
* Merged https://github.com/hierynomus/sshj/pull/726[#726]: Parse OpenSSH v1 keys with full CRT information present
|
||||||
* Merged https://github.com/hierynomus/sshj/pull/721[#721]: Prefer known host key algorithm for host key verification
|
* Merged https://github.com/hierynomus/sshj/pull/721[#721]: Prefer known host key algorithm for host key verification
|
||||||
* Merged https://github.com/hierynomus/sshj/pull/716[#716], https://github.com/hierynomus/sshj/pull/729[#729] and https://github.com/hierynomus/sshj/pull/730[#730]: Add full support for PuTTY v3 key files.
|
* Merged https://github.com/hierynomus/sshj/pull/716[#716], https://github.com/hierynomus/sshj/pull/729[#729] and https://github.com/hierynomus/sshj/pull/730[#730]: Add full support for PuTTY v3 key files.
|
||||||
|
|||||||
64
build.gradle
64
build.gradle
@@ -1,7 +1,3 @@
|
|||||||
import java.text.SimpleDateFormat
|
|
||||||
import com.bmuschko.gradle.docker.tasks.container.*
|
|
||||||
import com.bmuschko.gradle.docker.tasks.image.*
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "groovy"
|
id "groovy"
|
||||||
@@ -39,11 +35,11 @@ project.version = scmVersion.version
|
|||||||
|
|
||||||
configurations.implementation.transitive = false
|
configurations.implementation.transitive = false
|
||||||
|
|
||||||
def bouncycastleVersion = "1.69"
|
def bouncycastleVersion = "1.70"
|
||||||
def sshdVersion = "2.1.0"
|
def sshdVersion = "2.8.0"
|
||||||
|
|
||||||
dependencies {
|
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:bcprov-jdk15on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
|
||||||
implementation "com.jcraft:jzlib:1.1.3"
|
implementation "com.jcraft:jzlib:1.1.3"
|
||||||
@@ -51,16 +47,16 @@ dependencies {
|
|||||||
|
|
||||||
implementation "net.i2p.crypto:eddsa:0.3.0"
|
implementation "net.i2p.crypto:eddsa:0.3.0"
|
||||||
|
|
||||||
testImplementation "junit:junit:4.12"
|
testImplementation "junit:junit:4.13.2"
|
||||||
testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
|
testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
|
||||||
testImplementation "org.mockito:mockito-core:2.28.2"
|
testImplementation "org.mockito:mockito-core:4.2.0"
|
||||||
testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
|
testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
|
||||||
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
|
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
|
||||||
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
|
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
|
||||||
testRuntimeOnly "ch.qos.logback:logback-classic:1.2.6"
|
testImplementation "ch.qos.logback:logback-classic:1.2.11"
|
||||||
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
|
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
|
||||||
testImplementation 'org.apache.httpcomponents:httpclient:4.5.9'
|
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||||
|
testImplementation 'org.testcontainers:testcontainers:1.16.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
license {
|
license {
|
||||||
@@ -69,7 +65,12 @@ license {
|
|||||||
mapping {
|
mapping {
|
||||||
java = 'SLASHSTAR_STYLE'
|
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()) {
|
if (!JavaVersion.current().isJava9Compatible()) {
|
||||||
@@ -276,48 +277,11 @@ jacocoTestReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
task buildItestImage(type: DockerBuildImage) {
|
|
||||||
inputDir = file('src/itest/docker-image')
|
|
||||||
images.add('sshj/sshd-itest:latest')
|
|
||||||
}
|
|
||||||
|
|
||||||
task createItestContainer(type: DockerCreateContainer) {
|
|
||||||
dependsOn buildItestImage
|
|
||||||
targetImageId buildItestImage.getImageId()
|
|
||||||
hostConfig.portBindings = ['2222:22']
|
|
||||||
hostConfig.autoRemove = true
|
|
||||||
}
|
|
||||||
|
|
||||||
task startItestContainer(type: DockerStartContainer) {
|
|
||||||
dependsOn createItestContainer
|
|
||||||
targetContainerId createItestContainer.getContainerId()
|
|
||||||
}
|
|
||||||
|
|
||||||
task logItestContainer(type: DockerLogsContainer) {
|
|
||||||
dependsOn createItestContainer
|
|
||||||
targetContainerId createItestContainer.getContainerId()
|
|
||||||
showTimestamps = true
|
|
||||||
stdErr = true
|
|
||||||
stdOut = true
|
|
||||||
tailAll = true
|
|
||||||
}
|
|
||||||
|
|
||||||
task stopItestContainer(type: DockerStopContainer) {
|
|
||||||
targetContainerId createItestContainer.getContainerId()
|
|
||||||
}
|
|
||||||
|
|
||||||
task forkedUploadRelease(type: GradleBuild) {
|
task forkedUploadRelease(type: GradleBuild) {
|
||||||
buildFile = project.buildFile
|
buildFile = project.buildFile
|
||||||
tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"]
|
tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"]
|
||||||
}
|
}
|
||||||
|
|
||||||
project.tasks.integrationTest.dependsOn(startItestContainer)
|
|
||||||
project.tasks.integrationTest.finalizedBy(stopItestContainer)
|
|
||||||
|
|
||||||
// Being enabled, it pollutes logs on CI. Uncomment when debugging some test to get sshd logs.
|
|
||||||
// project.tasks.stopItestContainer.dependsOn(logItestContainer)
|
|
||||||
|
|
||||||
project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build])
|
project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build])
|
||||||
project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease)
|
project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease)
|
||||||
project.tasks.jacocoTestReport.dependsOn(project.tasks.test)
|
project.tasks.jacocoTestReport.dependsOn(project.tasks.test)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<groupId>com.hierynomus</groupId>
|
<groupId>com.hierynomus</groupId>
|
||||||
<artifactId>sshj-examples</artifactId>
|
<artifactId>sshj-examples</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>0.19.1</version>
|
<version>0.33.0</version>
|
||||||
|
|
||||||
<name>sshj-examples</name>
|
<name>sshj-examples</name>
|
||||||
<description>Examples for SSHv2 library for Java</description>
|
<description>Examples for SSHv2 library for Java</description>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.hierynomus</groupId>
|
<groupId>com.hierynomus</groupId>
|
||||||
<artifactId>sshj</artifactId>
|
<artifactId>sshj</artifactId>
|
||||||
<version>0.31.0</version>
|
<version>0.33.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ public class KeepAlive {
|
|||||||
final SSHClient ssh = new SSHClient(defaultConfig);
|
final SSHClient ssh = new SSHClient(defaultConfig);
|
||||||
try {
|
try {
|
||||||
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
||||||
|
// Set interval to enable keep-alive before connecting
|
||||||
|
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||||
ssh.connect(args[0]);
|
ssh.connect(args[0]);
|
||||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5); //every 60sec
|
|
||||||
ssh.authPassword(args[1], args[2]);
|
ssh.authPassword(args[1], args[2]);
|
||||||
Session session = ssh.startSession();
|
Session session = ssh.startSession();
|
||||||
session.allocateDefaultPTY();
|
session.allocateDefaultPTY();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class RemotePF {
|
|||||||
client.loadKnownHosts();
|
client.loadKnownHosts();
|
||||||
|
|
||||||
client.connect("localhost");
|
client.connect("localhost");
|
||||||
|
client.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
client.authPublickey(System.getProperty("user.name"));
|
client.authPublickey(System.getProperty("user.name"));
|
||||||
@@ -33,8 +34,6 @@ public class RemotePF {
|
|||||||
// what we do with incoming connections that are forwarded to us
|
// what we do with incoming connections that are forwarded to us
|
||||||
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
|
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
|
||||||
|
|
||||||
client.getTransport().setHeartbeatInterval(30);
|
|
||||||
|
|
||||||
// Something to hang on to so that the forwarding stays
|
// Something to hang on to so that the forwarding stays
|
||||||
client.getTransport().join();
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with 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
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
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.
|
# 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"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
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."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
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.
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@@ -105,79 +140,95 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
warn "Could not query maximum file descriptor limit"
|
||||||
fi
|
esac
|
||||||
ulimit -n $MAX_FD
|
case $MAX_FD in #(
|
||||||
if [ $? -ne 0 ] ; then
|
'' | soft) :;; #(
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
*)
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
else
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
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" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --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
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
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" "$@"
|
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
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
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_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@@ -64,28 +64,14 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
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
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
FROM sickp/alpine-sshd:7.5-r2
|
|
||||||
|
|
||||||
ADD authorized_keys /home/sshj/.ssh/authorized_keys
|
|
||||||
|
|
||||||
ADD test-container/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key
|
|
||||||
ADD test-container/ssh_host_ecdsa_key.pub /etc/ssh/ssh_host_ecdsa_key.pub
|
|
||||||
ADD test-container/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
|
||||||
ADD test-container/ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
ADD test-container/sshd_config /etc/ssh/sshd_config
|
|
||||||
COPY test-container/trusted_ca_keys /etc/ssh/trusted_ca_keys
|
|
||||||
COPY test-container/host_keys/* /etc/ssh/
|
|
||||||
|
|
||||||
RUN apk add --no-cache tini
|
|
||||||
RUN \
|
|
||||||
echo "root:smile" | chpasswd && \
|
|
||||||
adduser -D -s /bin/ash sshj && \
|
|
||||||
passwd -u sshj && \
|
|
||||||
echo "sshj:ultrapassword" | chpasswd && \
|
|
||||||
chmod 600 /home/sshj/.ssh/authorized_keys && \
|
|
||||||
chmod 600 /etc/ssh/ssh_host_*_key && \
|
|
||||||
chmod 644 /etc/ssh/*.pub && \
|
|
||||||
chown -R sshj:sshj /home/sshj
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "/entrypoint.sh", "-o", "LogLevel=DEBUG2"]
|
|
||||||
@@ -3,6 +3,7 @@ ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHQiZm0w
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaWrwt3drIOjeBq2LSHRavxAT7ja2f+5soOUJl/zKSI ajvanerp@Heimdall.xebialabs.com
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaWrwt3drIOjeBq2LSHRavxAT7ja2f+5soOUJl/zKSI ajvanerp@Heimdall.xebialabs.com
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBi sshjtest@TranceLove
|
||||||
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase
|
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase
|
||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKRyZAtOJJfAhPU6xE6ZXY564vwErAI3n3Yn4lTHL9bxev9Ily6eCqPLcV0WbSV04pztngFn9MjT7yb8mcXheHpIaWEH569sMpmpOtyfn4p68SceuXBGyyPGMIcfOTknkASd1JYSD4EPkd9rZmCzcx3vEnLu8ChnA/G221xSVQ5VC/jD/c/CgNUayhQ+xbn57qHKKtZwfTa21QmwIabGYJNwlVjlKTCdddeVnZfKqKrG7cxHQApsxd21rhM9IT/C/f4Y/Tx3WUUVeam0iZ265oiPHoPALqJIWSQIUheRYAxYAQqJwSQ0Or9MM8XXun2Iy3RUSGk6eIvrCsFbNURsHNs7Pu0UnpYv6FZ3vCkFep/1pAT6fQvY7pDOOWDHKXArD4watc9gIWaQBH73wDW/KgBcnMRSoGWgQjsYqIamP4oV1+HqUI3lRAsXZaX+eiBGt3+3A5KebP27UJ1YUwhwlzs7wzTKaCu0OaL+hOsP1F2AxAa995bgFksMd23645ux3YCJKXG4sGpJ1Z/Hs49K72gv+QjLZVxXqY623c8+3OUhlixqoEFd4iG7UMc5a552ch/VA+jaspmLZoFhPz99aBRVb1oCSPxSwLw+Q/wxv6pZmT+14rqTzY2farjU53hM+CsUPh7dnWXhGG7RuA5wCdeOXOYjuksfzAoHIZhPqTgQ== ajvanerp@Heimdall.local
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKRyZAtOJJfAhPU6xE6ZXY564vwErAI3n3Yn4lTHL9bxev9Ily6eCqPLcV0WbSV04pztngFn9MjT7yb8mcXheHpIaWEH569sMpmpOtyfn4p68SceuXBGyyPGMIcfOTknkASd1JYSD4EPkd9rZmCzcx3vEnLu8ChnA/G221xSVQ5VC/jD/c/CgNUayhQ+xbn57qHKKtZwfTa21QmwIabGYJNwlVjlKTCdddeVnZfKqKrG7cxHQApsxd21rhM9IT/C/f4Y/Tx3WUUVeam0iZ265oiPHoPALqJIWSQIUheRYAxYAQqJwSQ0Or9MM8XXun2Iy3RUSGk6eIvrCsFbNURsHNs7Pu0UnpYv6FZ3vCkFep/1pAT6fQvY7pDOOWDHKXArD4watc9gIWaQBH73wDW/KgBcnMRSoGWgQjsYqIamP4oV1+HqUI3lRAsXZaX+eiBGt3+3A5KebP27UJ1YUwhwlzs7wzTKaCu0OaL+hOsP1F2AxAa995bgFksMd23645ux3YCJKXG4sGpJ1Z/Hs49K72gv+QjLZVxXqY623c8+3OUhlixqoEFd4iG7UMc5a552ch/VA+jaspmLZoFhPz99aBRVb1oCSPxSwLw+Q/wxv6pZmT+14rqTzY2farjU53hM+CsUPh7dnWXhGG7RuA5wCdeOXOYjuksfzAoHIZhPqTgQ== ajvanerp@Heimdall.local
|
||||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMvfRYSe44VQGwxexOMibcM3+fWeUP1jrBofOxFDRRrzRF8dK/vll2svqTPXMRnITnT1UoemEcB5OHtvH4hzfh/HFeDxJ5S7UncYxoClTSa8MeMFG2Zj9CoUZs1SHbwSGg== root@sshj
|
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMvfRYSe44VQGwxexOMibcM3+fWeUP1jrBofOxFDRRrzRF8dK/vll2svqTPXMRnITnT1UoemEcB5OHtvH4hzfh/HFeDxJ5S7UncYxoClTSa8MeMFG2Zj9CoUZs1SHbwSGg== root@sshj
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
# $OpenBSD: sshd_config,v 1.101 2017/03/14 07:19:07 djm Exp $
|
|
||||||
|
|
||||||
# This is the sshd server system-wide configuration file. See
|
|
||||||
# sshd_config(5) for more information.
|
|
||||||
|
|
||||||
# This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin
|
|
||||||
|
|
||||||
# The strategy used for options in the default sshd_config shipped with
|
|
||||||
# OpenSSH is to specify options with their default value where
|
|
||||||
# possible, but leave them commented. Uncommented options override the
|
|
||||||
# default value.
|
|
||||||
|
|
||||||
#Port 22
|
|
||||||
#AddressFamily any
|
|
||||||
#ListenAddress 0.0.0.0
|
|
||||||
#ListenAddress ::
|
|
||||||
|
|
||||||
#HostKey /etc/ssh/ssh_host_rsa_key
|
|
||||||
#HostKey /etc/ssh/ssh_host_dsa_key
|
|
||||||
#HostKey /etc/ssh/ssh_host_ecdsa_key
|
|
||||||
#HostKey /etc/ssh/ssh_host_ed25519_key
|
|
||||||
|
|
||||||
# Ciphers and keying
|
|
||||||
#RekeyLimit default none
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
#SyslogFacility AUTH
|
|
||||||
#LogLevel INFO
|
|
||||||
|
|
||||||
# Authentication:
|
|
||||||
|
|
||||||
#LoginGraceTime 2m
|
|
||||||
PermitRootLogin yes
|
|
||||||
#StrictModes yes
|
|
||||||
#MaxAuthTries 6
|
|
||||||
#MaxSessions 10
|
|
||||||
|
|
||||||
#PubkeyAuthentication yes
|
|
||||||
|
|
||||||
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
|
|
||||||
# but this is overridden so installations will only check .ssh/authorized_keys
|
|
||||||
AuthorizedKeysFile .ssh/authorized_keys
|
|
||||||
|
|
||||||
#AuthorizedPrincipalsFile none
|
|
||||||
|
|
||||||
#AuthorizedKeysCommand none
|
|
||||||
#AuthorizedKeysCommandUser nobody
|
|
||||||
|
|
||||||
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
|
|
||||||
#HostbasedAuthentication no
|
|
||||||
# Change to yes if you don't trust ~/.ssh/known_hosts for
|
|
||||||
# HostbasedAuthentication
|
|
||||||
#IgnoreUserKnownHosts no
|
|
||||||
# Don't read the user's ~/.rhosts and ~/.shosts files
|
|
||||||
#IgnoreRhosts yes
|
|
||||||
|
|
||||||
# To disable tunneled clear text passwords, change to no here!
|
|
||||||
#PasswordAuthentication yes
|
|
||||||
#PermitEmptyPasswords no
|
|
||||||
|
|
||||||
# Change to no to disable s/key passwords
|
|
||||||
#ChallengeResponseAuthentication yes
|
|
||||||
|
|
||||||
# Kerberos options
|
|
||||||
#KerberosAuthentication no
|
|
||||||
#KerberosOrLocalPasswd yes
|
|
||||||
#KerberosTicketCleanup yes
|
|
||||||
#KerberosGetAFSToken no
|
|
||||||
|
|
||||||
# GSSAPI options
|
|
||||||
#GSSAPIAuthentication no
|
|
||||||
#GSSAPICleanupCredentials yes
|
|
||||||
|
|
||||||
# Set this to 'yes' to enable PAM authentication, account processing,
|
|
||||||
# and session processing. If this is enabled, PAM authentication will
|
|
||||||
# be allowed through the ChallengeResponseAuthentication and
|
|
||||||
# PasswordAuthentication. Depending on your PAM configuration,
|
|
||||||
# PAM authentication via ChallengeResponseAuthentication may bypass
|
|
||||||
# the setting of "PermitRootLogin without-password".
|
|
||||||
# If you just want the PAM account and session checks to run without
|
|
||||||
# PAM authentication, then enable this but set PasswordAuthentication
|
|
||||||
# and ChallengeResponseAuthentication to 'no'.
|
|
||||||
#UsePAM no
|
|
||||||
|
|
||||||
#AllowAgentForwarding yes
|
|
||||||
#AllowTcpForwarding yes
|
|
||||||
#GatewayPorts no
|
|
||||||
#X11Forwarding no
|
|
||||||
#X11DisplayOffset 10
|
|
||||||
#X11UseLocalhost yes
|
|
||||||
#PermitTTY yes
|
|
||||||
#PrintMotd yes
|
|
||||||
#PrintLastLog yes
|
|
||||||
#TCPKeepAlive yes
|
|
||||||
#UseLogin no
|
|
||||||
#PermitUserEnvironment no
|
|
||||||
#Compression delayed
|
|
||||||
#ClientAliveInterval 0
|
|
||||||
#ClientAliveCountMax 3
|
|
||||||
#UseDNS no
|
|
||||||
#PidFile /run/sshd.pid
|
|
||||||
#MaxStartups 10:30:100
|
|
||||||
#PermitTunnel no
|
|
||||||
#ChrootDirectory none
|
|
||||||
#VersionAddendum none
|
|
||||||
|
|
||||||
# no default banner path
|
|
||||||
#Banner none
|
|
||||||
|
|
||||||
# override default of no subsystems
|
|
||||||
Subsystem sftp /usr/lib/ssh/sftp-server
|
|
||||||
|
|
||||||
# the following are HPN related configuration options
|
|
||||||
# tcp receive buffer polling. disable in non autotuning kernels
|
|
||||||
#TcpRcvBufPoll yes
|
|
||||||
|
|
||||||
# disable hpn performance boosts
|
|
||||||
#HPNDisabled no
|
|
||||||
|
|
||||||
# buffer size for hpn to non-hpn connections
|
|
||||||
#HPNBufferSize 2048
|
|
||||||
|
|
||||||
|
|
||||||
# Example of overriding settings on a per-user basis
|
|
||||||
#Match User anoncvs
|
|
||||||
# X11Forwarding no
|
|
||||||
# AllowTcpForwarding no
|
|
||||||
# PermitTTY no
|
|
||||||
# ForceCommand cvs server
|
|
||||||
|
|
||||||
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1
|
|
||||||
macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com
|
|
||||||
|
|
||||||
TrustedUserCAKeys /etc/ssh/trusted_ca_keys
|
|
||||||
|
|
||||||
Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_rsa_key
|
|
||||||
HostKey /etc/ssh/ssh_host_dsa_key
|
|
||||||
HostKey /etc/ssh/ssh_host_ecdsa_key
|
|
||||||
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_ecdsa_256_key
|
|
||||||
HostCertificate /etc/ssh/ssh_host_ecdsa_256_key-cert.pub
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_ecdsa_384_key
|
|
||||||
HostCertificate /etc/ssh/ssh_host_ecdsa_384_key-cert.pub
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_ecdsa_521_key
|
|
||||||
HostCertificate /etc/ssh/ssh_host_ecdsa_521_key-cert.pub
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_ed25519_384_key
|
|
||||||
HostCertificate /etc/ssh/ssh_host_ed25519_384_key-cert.pub
|
|
||||||
|
|
||||||
HostKey /etc/ssh/ssh_host_rsa_2048_key
|
|
||||||
HostCertificate /etc/ssh/ssh_host_rsa_2048_key-cert.pub
|
|
||||||
|
|
||||||
LogLevel DEBUG2
|
|
||||||
@@ -1,42 +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 com.hierynomus.sshj
|
|
||||||
|
|
||||||
import net.schmizz.sshj.Config
|
|
||||||
import net.schmizz.sshj.DefaultConfig
|
|
||||||
import net.schmizz.sshj.SSHClient
|
|
||||||
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
|
|
||||||
import spock.lang.Specification
|
|
||||||
|
|
||||||
class IntegrationBaseSpec extends Specification {
|
|
||||||
protected static final int DOCKER_PORT = 2222
|
|
||||||
protected static final String USERNAME = "sshj"
|
|
||||||
protected static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
|
|
||||||
protected final static String SERVER_IP = System.getProperty("serverIP", "127.0.0.1")
|
|
||||||
|
|
||||||
protected static SSHClient getConnectedClient(Config config) {
|
|
||||||
SSHClient sshClient = new SSHClient(config)
|
|
||||||
sshClient.addHostKeyVerifier(new PromiscuousVerifier())
|
|
||||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
|
||||||
|
|
||||||
return sshClient
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static SSHClient getConnectedClient() throws IOException {
|
|
||||||
return getConnectedClient(new DefaultConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -20,9 +20,15 @@ import net.schmizz.sshj.DefaultConfig
|
|||||||
import net.schmizz.sshj.SSHClient
|
import net.schmizz.sshj.SSHClient
|
||||||
import net.schmizz.sshj.transport.TransportException
|
import net.schmizz.sshj.transport.TransportException
|
||||||
import net.schmizz.sshj.userauth.UserAuthException
|
import net.schmizz.sshj.userauth.UserAuthException
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class IntegrationSpec extends IntegrationBaseSpec {
|
class IntegrationSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should accept correct key for #signatureName"() {
|
def "should accept correct key for #signatureName"() {
|
||||||
@@ -33,7 +39,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
|||||||
sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint
|
sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint
|
||||||
|
|
||||||
when:
|
when:
|
||||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
sshClient.isConnected()
|
sshClient.isConnected()
|
||||||
@@ -50,7 +56,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
|||||||
sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3")
|
sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3")
|
||||||
|
|
||||||
when:
|
when:
|
||||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
thrown(TransportException.class)
|
thrown(TransportException.class)
|
||||||
@@ -59,11 +65,11 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
|||||||
@Unroll
|
@Unroll
|
||||||
def "should authenticate with key #key"() {
|
def "should authenticate with key #key"() {
|
||||||
given:
|
given:
|
||||||
SSHClient client = getConnectedClient()
|
SSHClient client = sshd.getConnectedClient()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def keyProvider = passphrase != null ? client.loadKeys("src/itest/resources/keyfiles/$key", passphrase) : client.loadKeys("src/itest/resources/keyfiles/$key")
|
def keyProvider = passphrase != null ? client.loadKeys("src/itest/resources/keyfiles/$key", passphrase) : client.loadKeys("src/itest/resources/keyfiles/$key")
|
||||||
client.authPublickey(USERNAME, keyProvider)
|
client.authPublickey(IntegrationTestUtil.USERNAME, keyProvider)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.isAuthenticated()
|
client.isAuthenticated()
|
||||||
@@ -74,6 +80,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
|||||||
"id_ecdsa_opensshv1" | null
|
"id_ecdsa_opensshv1" | null
|
||||||
"id_ed25519_opensshv1" | null
|
"id_ed25519_opensshv1" | null
|
||||||
"id_ed25519_opensshv1_aes256cbc.pem" | "foobar"
|
"id_ed25519_opensshv1_aes256cbc.pem" | "foobar"
|
||||||
|
"id_ed25519_opensshv1_aes128cbc.pem" | "sshjtest"
|
||||||
"id_ed25519_opensshv1_protected" | "sshjtest"
|
"id_ed25519_opensshv1_protected" | "sshjtest"
|
||||||
"id_rsa" | null
|
"id_rsa" | null
|
||||||
"id_rsa_opensshv1" | null
|
"id_rsa_opensshv1" | null
|
||||||
@@ -83,7 +90,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
|||||||
|
|
||||||
def "should not authenticate with wrong key"() {
|
def "should not authenticate with wrong key"() {
|
||||||
given:
|
given:
|
||||||
SSHClient client = getConnectedClient()
|
SSHClient client = sshd.getConnectedClient()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")
|
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")
|
||||||
|
|||||||
@@ -13,14 +13,9 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.backport;
|
package com.hierynomus.sshj
|
||||||
|
|
||||||
public class JavaVersion {
|
|
||||||
public static boolean isJava7OrEarlier() {
|
|
||||||
String property = System.getProperty("java.specification.version");
|
|
||||||
float diff = Float.parseFloat(property) - 1.7f;
|
|
||||||
|
|
||||||
return diff < 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class IntegrationTestUtil {
|
||||||
|
static final String USERNAME = "sshj"
|
||||||
|
static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.hierynomus.sshj.key.KeyAlgorithms
|
||||||
|
import net.schmizz.sshj.Config
|
||||||
|
import net.schmizz.sshj.DefaultConfig
|
||||||
|
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder
|
||||||
|
import spock.lang.Specification
|
||||||
|
import spock.lang.Unroll
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that SSHJ is able to work with OpenSSH 8.8, which removed ssh-rsa signature from the default setup.
|
||||||
|
*/
|
||||||
|
class RsaShaKeySignatureTest extends Specification {
|
||||||
|
private static final Map<String, KeyAlgorithms.Factory> SSH_HOST_KEYS_AND_FACTORIES = [
|
||||||
|
'ssh_host_ecdsa_256_key': KeyAlgorithms.ECDSASHANistp256(),
|
||||||
|
'ssh_host_ecdsa_384_key': KeyAlgorithms.ECDSASHANistp384(),
|
||||||
|
'ssh_host_ecdsa_521_key': KeyAlgorithms.ECDSASHANistp521(),
|
||||||
|
'ssh_host_ed25519_384_key': KeyAlgorithms.EdDSA25519(),
|
||||||
|
'ssh_host_rsa_2048_key': KeyAlgorithms.RSASHA512(),
|
||||||
|
]
|
||||||
|
|
||||||
|
private static void dockerfileBuilder(DockerfileBuilder it, String hostKey, String pubkeyAcceptedAlgorithms) {
|
||||||
|
it.from("archlinux:base")
|
||||||
|
it.run('yes | pacman -Sy core/openssh' +
|
||||||
|
' && (' +
|
||||||
|
' V=$(echo $(/usr/sbin/sshd -h 2>&1) | grep -o \'OpenSSH_[0-9][0-9]*[.][0-9][0-9]*p[0-9]\');' +
|
||||||
|
' if [[ "$V" < OpenSSH_8.8p1 ]]; then' +
|
||||||
|
' echo $V is too old 1>&2;' +
|
||||||
|
' exit 1;' +
|
||||||
|
' fi' +
|
||||||
|
')' +
|
||||||
|
' && set -o pipefail ' +
|
||||||
|
' && useradd --create-home sshj' +
|
||||||
|
' && echo \"sshj:ultrapassword\" | chpasswd')
|
||||||
|
it.add("authorized_keys", "/home/sshj/.ssh/")
|
||||||
|
it.add(hostKey, '/etc/ssh/')
|
||||||
|
it.run('chmod go-rwx /etc/ssh/ssh_host_*' +
|
||||||
|
' && chown -R sshj /home/sshj/.ssh' +
|
||||||
|
' && chmod -R go-rwx /home/sshj/.ssh')
|
||||||
|
it.expose(22)
|
||||||
|
|
||||||
|
def cmd = [
|
||||||
|
'/usr/sbin/sshd',
|
||||||
|
'-D',
|
||||||
|
'-e',
|
||||||
|
'-f', '/dev/null',
|
||||||
|
'-o', 'LogLevel=DEBUG2',
|
||||||
|
'-o', "HostKey=/etc/ssh/$hostKey",
|
||||||
|
]
|
||||||
|
if (pubkeyAcceptedAlgorithms != null) {
|
||||||
|
cmd += ['-o', "PubkeyAcceptedAlgorithms=$pubkeyAcceptedAlgorithms"]
|
||||||
|
}
|
||||||
|
it.cmd(cmd as String[])
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SshdContainer makeSshdContainer(String hostKey, String pubkeyAcceptedAlgorithms) {
|
||||||
|
return new SshdContainer(new SshdContainer.DebugLoggingImageFromDockerfile()
|
||||||
|
.withFileFromPath("authorized_keys", Paths.get("src/itest/docker-image/authorized_keys"))
|
||||||
|
.withFileFromPath(hostKey, Paths.get("src/itest/docker-image/test-container/host_keys/$hostKey"))
|
||||||
|
.withDockerfileFromBuilder {
|
||||||
|
dockerfileBuilder(it, hostKey, pubkeyAcceptedAlgorithms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Unroll
|
||||||
|
def "connect to a server with host key #hostKey that does not support ssh-rsa"() {
|
||||||
|
given:
|
||||||
|
SshdContainer sshd = makeSshdContainer(hostKey, "rsa-sha2-512,rsa-sha2-256,ssh-ed25519")
|
||||||
|
sshd.start()
|
||||||
|
|
||||||
|
and:
|
||||||
|
Config config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [
|
||||||
|
KeyAlgorithms.RSASHA512(),
|
||||||
|
KeyAlgorithms.RSASHA256(),
|
||||||
|
SSH_HOST_KEYS_AND_FACTORIES[hostKey],
|
||||||
|
]
|
||||||
|
|
||||||
|
when:
|
||||||
|
def sshClient = sshd.getConnectedClient(config)
|
||||||
|
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
|
||||||
|
|
||||||
|
then:
|
||||||
|
sshClient.isAuthenticated()
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
sshClient?.disconnect()
|
||||||
|
sshd.stop()
|
||||||
|
|
||||||
|
where:
|
||||||
|
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Unroll
|
||||||
|
def "connect to a default server with host key #hostKey using a default config"() {
|
||||||
|
given:
|
||||||
|
SshdContainer sshd = makeSshdContainer(hostKey, null)
|
||||||
|
sshd.start()
|
||||||
|
|
||||||
|
when:
|
||||||
|
def sshClient = sshd.getConnectedClient()
|
||||||
|
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
|
||||||
|
|
||||||
|
then:
|
||||||
|
sshClient.isAuthenticated()
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
sshClient?.disconnect()
|
||||||
|
sshd.stop()
|
||||||
|
|
||||||
|
where:
|
||||||
|
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Unroll
|
||||||
|
def "connect to a server with host key #hostkey that supports only ssh-rsa"() {
|
||||||
|
given:
|
||||||
|
SshdContainer sshd = makeSshdContainer(hostKey, "ssh-rsa,ssh-ed25519")
|
||||||
|
sshd.start()
|
||||||
|
|
||||||
|
and:
|
||||||
|
Config config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [
|
||||||
|
KeyAlgorithms.SSHRSA(),
|
||||||
|
SSH_HOST_KEYS_AND_FACTORIES[hostKey],
|
||||||
|
]
|
||||||
|
|
||||||
|
when:
|
||||||
|
def sshClient = sshd.getConnectedClient(config)
|
||||||
|
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
|
||||||
|
|
||||||
|
then:
|
||||||
|
sshClient.isAuthenticated()
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
sshClient.disconnect()
|
||||||
|
sshd.stop()
|
||||||
|
|
||||||
|
where:
|
||||||
|
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.testcontainers.containers.wait.strategy.WaitStrategy;
|
||||||
|
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wait strategy designed for {@link SshdContainer} to wait until the SSH server is ready, to avoid races when a test
|
||||||
|
* tries to connect to a server before the server has started.
|
||||||
|
*/
|
||||||
|
public class SshServerWaitStrategy implements WaitStrategy {
|
||||||
|
private Duration startupTimeout = Duration.ofMinutes(1);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void waitUntilReady(WaitStrategyTarget waitStrategyTarget) {
|
||||||
|
long expectedEnd = System.nanoTime() + startupTimeout.toNanos();
|
||||||
|
while (waitStrategyTarget.isRunning()) {
|
||||||
|
long attemptStart = System.nanoTime();
|
||||||
|
IOException error = null;
|
||||||
|
byte[] buffer = new byte[7];
|
||||||
|
try (Socket socket = new Socket()) {
|
||||||
|
socket.setSoTimeout(500);
|
||||||
|
socket.connect(new InetSocketAddress(
|
||||||
|
waitStrategyTarget.getHost(), waitStrategyTarget.getFirstMappedPort()));
|
||||||
|
// Haven't seen any SSH server that sends the version in two or more packets.
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
socket.getInputStream().read(buffer);
|
||||||
|
if (!Arrays.equals(buffer, "SSH-2.0".getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
error = new IOException("The version message doesn't look like an SSH server version");
|
||||||
|
}
|
||||||
|
} catch (IOException err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
break;
|
||||||
|
} else if (System.nanoTime() >= expectedEnd) {
|
||||||
|
throw new RuntimeException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(Math.max(0L, 500L - (System.nanoTime() - attemptStart) / 1_000_000));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WaitStrategy withStartupTimeout(Duration startupTimeout) {
|
||||||
|
this.startupTimeout = startupTimeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/itest/groovy/com/hierynomus/sshj/SshdContainer.java
Normal file
155
src/itest/groovy/com/hierynomus/sshj/SshdContainer.java
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* 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 ch.qos.logback.classic.Level;
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
import net.schmizz.sshj.Config;
|
||||||
|
import net.schmizz.sshj.DefaultConfig;
|
||||||
|
import net.schmizz.sshj.SSHClient;
|
||||||
|
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.images.builder.ImageFromDockerfile;
|
||||||
|
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
|
||||||
|
import org.testcontainers.utility.DockerLoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JUnit4 rule for launching a generic SSH server container.
|
||||||
|
*/
|
||||||
|
public class SshdContainer extends GenericContainer<SshdContainer> {
|
||||||
|
/**
|
||||||
|
* A workaround for strange logger names of testcontainers. They contain no dots, but contain slashes,
|
||||||
|
* square brackets, and even emoji. It's uneasy to set the logging level via the XML file of logback, the
|
||||||
|
* result would be less readable than the code below.
|
||||||
|
*/
|
||||||
|
public static class DebugLoggingImageFromDockerfile extends ImageFromDockerfile {
|
||||||
|
public DebugLoggingImageFromDockerfile() {
|
||||||
|
super();
|
||||||
|
Logger logger = (Logger) LoggerFactory.getILoggerFactory()
|
||||||
|
.getLogger(DockerLoggerFactory.getLogger(getDockerImageName()).getName());
|
||||||
|
logger.setLevel(Level.DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
public static final String DEFAULT_SSHD_CONFIG = "" +
|
||||||
|
"PermitRootLogin yes\n" +
|
||||||
|
"AuthorizedKeysFile .ssh/authorized_keys\n" +
|
||||||
|
"Subsystem sftp /usr/lib/ssh/sftp-server\n" +
|
||||||
|
"KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1\n" +
|
||||||
|
"macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com\n" +
|
||||||
|
"TrustedUserCAKeys /etc/ssh/trusted_ca_keys\n" +
|
||||||
|
"Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_rsa_key\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_dsa_key\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ecdsa_key\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ed25519_key\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ecdsa_256_key\n" +
|
||||||
|
"HostCertificate /etc/ssh/ssh_host_ecdsa_256_key-cert.pub\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ecdsa_384_key\n" +
|
||||||
|
"HostCertificate /etc/ssh/ssh_host_ecdsa_384_key-cert.pub\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ecdsa_521_key\n" +
|
||||||
|
"HostCertificate /etc/ssh/ssh_host_ecdsa_521_key-cert.pub\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_ed25519_384_key\n" +
|
||||||
|
"HostCertificate /etc/ssh/ssh_host_ed25519_384_key-cert.pub\n" +
|
||||||
|
"HostKey /etc/ssh/ssh_host_rsa_2048_key\n" +
|
||||||
|
"HostCertificate /etc/ssh/ssh_host_rsa_2048_key-cert.pub\n" +
|
||||||
|
"LogLevel DEBUG2\n";
|
||||||
|
|
||||||
|
public static void defaultDockerfileBuilder(@NotNull DockerfileBuilder builder) {
|
||||||
|
builder.from("sickp/alpine-sshd:7.5-r2");
|
||||||
|
|
||||||
|
builder.add("authorized_keys", "/home/sshj/.ssh/authorized_keys");
|
||||||
|
|
||||||
|
builder.add("test-container/ssh_host_ecdsa_key", "/etc/ssh/ssh_host_ecdsa_key");
|
||||||
|
builder.add("test-container/ssh_host_ecdsa_key.pub", "/etc/ssh/ssh_host_ecdsa_key.pub");
|
||||||
|
builder.add("test-container/ssh_host_ed25519_key", "/etc/ssh/ssh_host_ed25519_key");
|
||||||
|
builder.add("test-container/ssh_host_ed25519_key.pub", "/etc/ssh/ssh_host_ed25519_key.pub");
|
||||||
|
builder.copy("test-container/trusted_ca_keys", "/etc/ssh/trusted_ca_keys");
|
||||||
|
builder.copy("test-container/host_keys/*", "/etc/ssh/");
|
||||||
|
|
||||||
|
builder.run("apk add --no-cache tini"
|
||||||
|
+ " && echo \"root:smile\" | chpasswd"
|
||||||
|
+ " && adduser -D -s /bin/ash sshj"
|
||||||
|
+ " && passwd -u sshj"
|
||||||
|
+ " && echo \"sshj:ultrapassword\" | chpasswd"
|
||||||
|
+ " && chmod 600 /home/sshj/.ssh/authorized_keys"
|
||||||
|
+ " && chmod 600 /etc/ssh/ssh_host_*_key"
|
||||||
|
+ " && chmod 644 /etc/ssh/*.pub"
|
||||||
|
+ " && chown -R sshj:sshj /home/sshj");
|
||||||
|
builder.entryPoint("/sbin/tini", "/entrypoint.sh", "-o", "LogLevel=DEBUG2");
|
||||||
|
|
||||||
|
builder.add("sshd_config", "/etc/ssh/sshd_config");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull String sshdConfig = DEFAULT_SSHD_CONFIG;
|
||||||
|
|
||||||
|
public @NotNull Builder withSshdConfig(@NotNull String sshdConfig) {
|
||||||
|
this.sshdConfig = sshdConfig;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull SshdContainer build() {
|
||||||
|
return new SshdContainer(buildInner());
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Future<String> buildInner() {
|
||||||
|
return new DebugLoggingImageFromDockerfile()
|
||||||
|
.withDockerfileFromBuilder(Builder::defaultDockerfileBuilder)
|
||||||
|
.withFileFromPath(".", Paths.get("src/itest/docker-image"))
|
||||||
|
.withFileFromString("sshd_config", sshdConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") // Used dynamically by Spock
|
||||||
|
public SshdContainer() {
|
||||||
|
this(new SshdContainer.Builder().buildInner());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SshdContainer(@NotNull Future<String> future) {
|
||||||
|
super(future);
|
||||||
|
withExposedPorts(22);
|
||||||
|
setWaitStrategy(new SshServerWaitStrategy());
|
||||||
|
withLogConsumer(outputFrame -> {
|
||||||
|
switch (outputFrame.getType()) {
|
||||||
|
case STDOUT:
|
||||||
|
logger().info("sshd stdout: {}", outputFrame.getUtf8String().stripTrailing());
|
||||||
|
break;
|
||||||
|
case STDERR:
|
||||||
|
logger().info("sshd stderr: {}", outputFrame.getUtf8String().stripTrailing());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSHClient getConnectedClient(Config config) throws IOException {
|
||||||
|
SSHClient sshClient = new SSHClient(config);
|
||||||
|
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
|
||||||
|
sshClient.connect("127.0.0.1", getFirstMappedPort());
|
||||||
|
|
||||||
|
return sshClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSHClient getConnectedClient() throws IOException {
|
||||||
|
return getConnectedClient(new DefaultConfig());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,21 +15,28 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.sftp
|
package com.hierynomus.sshj.sftp
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import net.schmizz.sshj.SSHClient
|
import net.schmizz.sshj.SSHClient
|
||||||
import net.schmizz.sshj.sftp.OpenMode
|
import net.schmizz.sshj.sftp.OpenMode
|
||||||
import net.schmizz.sshj.sftp.RemoteFile
|
import net.schmizz.sshj.sftp.RemoteFile
|
||||||
import net.schmizz.sshj.sftp.SFTPClient
|
import net.schmizz.sshj.sftp.SFTPClient
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
|
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
|
||||||
|
|
||||||
class FileWriteSpec extends IntegrationBaseSpec {
|
class FileWriteSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
def "should append to file (GH issue #390)"() {
|
def "should append to file (GH issue #390)"() {
|
||||||
given:
|
given:
|
||||||
SSHClient client = getConnectedClient()
|
SSHClient client = sshd.getConnectedClient()
|
||||||
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
||||||
SFTPClient sftp = client.newSFTPClient()
|
SFTPClient sftp = client.newSFTPClient()
|
||||||
def file = "/home/sshj/test.txt"
|
def file = "/home/sshj/test.txt"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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.signature
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.SshdContainer
|
||||||
|
import net.schmizz.sshj.DefaultConfig
|
||||||
|
import net.schmizz.sshj.SSHClient
|
||||||
|
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
|
||||||
|
import spock.lang.Specification
|
||||||
|
import spock.lang.Unroll
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a brief test for verifying connection to a server using keys with certificates.
|
||||||
|
*
|
||||||
|
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
|
||||||
|
*/
|
||||||
|
class HostKeyWithCertificateSpec extends Specification {
|
||||||
|
@Unroll
|
||||||
|
def "accepting a signed host public key #hostKey"() {
|
||||||
|
given:
|
||||||
|
SshdContainer sshd = new SshdContainer.Builder()
|
||||||
|
.withSshdConfig("""
|
||||||
|
PasswordAuthentication yes
|
||||||
|
HostKey /etc/ssh/$hostKey
|
||||||
|
HostCertificate /etc/ssh/${hostKey}-cert.pub
|
||||||
|
""".stripMargin())
|
||||||
|
.build()
|
||||||
|
sshd.start()
|
||||||
|
|
||||||
|
and:
|
||||||
|
File knownHosts = Files.createTempFile("known_hosts", "").toFile()
|
||||||
|
knownHosts.deleteOnExit()
|
||||||
|
|
||||||
|
and:
|
||||||
|
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub")
|
||||||
|
def address = "127.0.0.1"
|
||||||
|
String knownHostsFileContents = "" +
|
||||||
|
"@cert-authority ${ address} ${caPubKey.text}" +
|
||||||
|
"\n@cert-authority [${address}]:${sshd.firstMappedPort} ${caPubKey.text}"
|
||||||
|
knownHosts.write(knownHostsFileContents)
|
||||||
|
|
||||||
|
and:
|
||||||
|
SSHClient sshClient = new SSHClient(new DefaultConfig())
|
||||||
|
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(knownHosts))
|
||||||
|
sshClient.connect(address, sshd.firstMappedPort)
|
||||||
|
|
||||||
|
when:
|
||||||
|
sshClient.authPassword("sshj", "ultrapassword")
|
||||||
|
|
||||||
|
then:
|
||||||
|
sshClient.authenticated
|
||||||
|
|
||||||
|
and:
|
||||||
|
knownHosts.getText() == knownHostsFileContents
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
sshd.stop()
|
||||||
|
|
||||||
|
where:
|
||||||
|
hostKey << [
|
||||||
|
"ssh_host_ecdsa_256_key",
|
||||||
|
"ssh_host_ecdsa_384_key",
|
||||||
|
"ssh_host_ecdsa_521_key",
|
||||||
|
"ssh_host_ed25519_384_key",
|
||||||
|
"ssh_host_rsa_2048_key",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,29 +15,34 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.signature
|
package com.hierynomus.sshj.signature
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import net.schmizz.sshj.DefaultConfig
|
||||||
import net.schmizz.sshj.SSHClient
|
import net.schmizz.sshj.SSHClient
|
||||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
|
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a brief test for verifying connection to a server using keys with certificates.
|
* This is a brief test for verifying connection to a server using keys with certificates.
|
||||||
*
|
*
|
||||||
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
|
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
|
||||||
*/
|
*/
|
||||||
class KeyWithCertificateSpec extends IntegrationBaseSpec {
|
class PublicKeyAuthWithCertificateSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "authorising with a signed public key #keyName"() {
|
def "authorising with a signed public key #keyName"() {
|
||||||
given:
|
given:
|
||||||
def client = getConnectedClient()
|
SSHClient client = new SSHClient(new DefaultConfig())
|
||||||
|
client.addHostKeyVerifier(new PromiscuousVerifier())
|
||||||
|
client.connect("127.0.0.1", sshd.firstMappedPort)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/certificates/$keyName")
|
client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/$keyName")
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
@@ -73,43 +78,4 @@ class KeyWithCertificateSpec extends IntegrationBaseSpec {
|
|||||||
"id_ed25519_384_rfc4716_signed_by_rsa",
|
"id_ed25519_384_rfc4716_signed_by_rsa",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@Unroll
|
|
||||||
def "accepting a signed host public key with type #hostKeyAlgo"() {
|
|
||||||
given:
|
|
||||||
File knownHosts = Files.createTempFile("known_hosts", "").toFile()
|
|
||||||
knownHosts.deleteOnExit()
|
|
||||||
|
|
||||||
and:
|
|
||||||
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub")
|
|
||||||
String knownHostsFileContents = "" +
|
|
||||||
"@cert-authority $SERVER_IP ${caPubKey.text}" +
|
|
||||||
"\n@cert-authority [$SERVER_IP]:$DOCKER_PORT ${caPubKey.text}"
|
|
||||||
knownHosts.write(knownHostsFileContents)
|
|
||||||
|
|
||||||
and:
|
|
||||||
def config = new DefaultConfig()
|
|
||||||
config.keyAlgorithms = config.keyAlgorithms.stream()
|
|
||||||
.filter { it.name == hostKeyAlgo }
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
SSHClient sshClient = new SSHClient(config)
|
|
||||||
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(knownHosts))
|
|
||||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
|
||||||
|
|
||||||
when:
|
|
||||||
sshClient.authPassword("sshj", "ultrapassword")
|
|
||||||
|
|
||||||
then:
|
|
||||||
sshClient.authenticated
|
|
||||||
|
|
||||||
and:
|
|
||||||
knownHosts.getText() == knownHostsFileContents
|
|
||||||
|
|
||||||
where:
|
|
||||||
hostKeyAlgo << [
|
|
||||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
|
||||||
"ssh-ed25519-cert-v01@openssh.com",
|
|
||||||
"ssh-rsa-cert-v01@openssh.com",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,18 +15,25 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.signature
|
package com.hierynomus.sshj.signature
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import com.hierynomus.sshj.SshdContainer
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class RsaSignatureClientKeySpec extends IntegrationBaseSpec {
|
class RsaSignatureClientKeySpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should correctly connect using publickey auth with RSA key with signature"() {
|
def "should correctly connect using publickey auth with RSA key with signature"() {
|
||||||
given:
|
given:
|
||||||
def client = getConnectedClient(new DefaultConfig())
|
def client = sshd.getConnectedClient()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
client.authPublickey(IntegrationTestUtil.USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
|
|||||||
@@ -15,22 +15,29 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.signature
|
package com.hierynomus.sshj.signature
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithms
|
import com.hierynomus.sshj.key.KeyAlgorithms
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import net.schmizz.sshj.DefaultConfig
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class SignatureSpec extends IntegrationBaseSpec {
|
class SignatureSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should correctly connect with #sig Signature"() {
|
def "should correctly connect with #sig Signature"() {
|
||||||
given:
|
given:
|
||||||
def cfg = new DefaultConfig()
|
def cfg = new DefaultConfig()
|
||||||
cfg.setKeyAlgorithms(Collections.singletonList(sigFactory))
|
cfg.setKeyAlgorithms(Collections.singletonList(sigFactory))
|
||||||
def client = getConnectedClient(cfg)
|
def client = sshd.getConnectedClient(cfg)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, KEYFILE)
|
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
|
|||||||
@@ -15,21 +15,28 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.transport.cipher
|
package com.hierynomus.sshj.transport.cipher
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import net.schmizz.sshj.DefaultConfig
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class CipherSpec extends IntegrationBaseSpec {
|
class CipherSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should correctly connect with #cipher Cipher"() {
|
def "should correctly connect with #cipher Cipher"() {
|
||||||
given:
|
given:
|
||||||
def cfg = new DefaultConfig()
|
def cfg = new DefaultConfig()
|
||||||
cfg.setCipherFactories(cipherFactory)
|
cfg.setCipherFactories(cipherFactory)
|
||||||
def client = getConnectedClient(cfg)
|
def client = sshd.getConnectedClient(cfg)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, KEYFILE)
|
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
|
|||||||
@@ -15,29 +15,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.transport.kex
|
package com.hierynomus.sshj.transport.kex
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
import com.hierynomus.sshj.transport.mac.Macs
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import net.schmizz.sshj.DefaultConfig
|
||||||
import net.schmizz.sshj.transport.kex.Curve25519DH
|
|
||||||
import net.schmizz.sshj.transport.kex.Curve25519SHA256
|
import net.schmizz.sshj.transport.kex.Curve25519SHA256
|
||||||
import net.schmizz.sshj.transport.kex.DH
|
|
||||||
import net.schmizz.sshj.transport.kex.DHGexSHA1
|
import net.schmizz.sshj.transport.kex.DHGexSHA1
|
||||||
import net.schmizz.sshj.transport.kex.DHGexSHA256
|
import net.schmizz.sshj.transport.kex.DHGexSHA256
|
||||||
import net.schmizz.sshj.transport.kex.ECDH
|
|
||||||
import net.schmizz.sshj.transport.kex.ECDHNistP
|
import net.schmizz.sshj.transport.kex.ECDHNistP
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class KexSpec extends IntegrationBaseSpec {
|
class KexSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should correctly connect with #kex Key Exchange"() {
|
def "should correctly connect with #kex Key Exchange"() {
|
||||||
given:
|
given:
|
||||||
def cfg = new DefaultConfig()
|
def cfg = new DefaultConfig()
|
||||||
cfg.setKeyExchangeFactories(kexFactory)
|
cfg.setKeyExchangeFactories(kexFactory)
|
||||||
def client = getConnectedClient(cfg)
|
def client = sshd.getConnectedClient(cfg)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, KEYFILE)
|
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
|
|||||||
@@ -15,21 +15,28 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.transport.mac
|
package com.hierynomus.sshj.transport.mac
|
||||||
|
|
||||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
import com.hierynomus.sshj.IntegrationTestUtil
|
||||||
|
import com.hierynomus.sshj.SshdContainer
|
||||||
import net.schmizz.sshj.DefaultConfig
|
import net.schmizz.sshj.DefaultConfig
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import spock.lang.Shared
|
||||||
|
import spock.lang.Specification
|
||||||
import spock.lang.Unroll
|
import spock.lang.Unroll
|
||||||
|
|
||||||
class MacSpec extends IntegrationBaseSpec {
|
class MacSpec extends Specification {
|
||||||
|
@Shared
|
||||||
|
@ClassRule
|
||||||
|
SshdContainer sshd
|
||||||
|
|
||||||
@Unroll
|
@Unroll
|
||||||
def "should correctly connect with #mac MAC"() {
|
def "should correctly connect with #mac MAC"() {
|
||||||
given:
|
given:
|
||||||
def cfg = new DefaultConfig()
|
def cfg = new DefaultConfig()
|
||||||
cfg.setMACFactories(macFactory)
|
cfg.setMACFactories(macFactory)
|
||||||
def client = getConnectedClient(cfg)
|
def client = sshd.getConnectedClient(cfg)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, KEYFILE)
|
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
@@ -47,10 +54,10 @@ class MacSpec extends IntegrationBaseSpec {
|
|||||||
given:
|
given:
|
||||||
def cfg = new DefaultConfig()
|
def cfg = new DefaultConfig()
|
||||||
cfg.setMACFactories(macFactory)
|
cfg.setMACFactories(macFactory)
|
||||||
def client = getConnectedClient(cfg)
|
def client = sshd.getConnectedClient(cfg)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey(USERNAME, KEYFILE)
|
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
client.authenticated
|
client.authenticated
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABDfrL8SxDyrkNlsJdAmc7Z0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBiAAAAkLVqaDIfs+sPBNyy7ytdLnP/xH7Nt5FIXx3Upw6wKuMGdBzbFQLcvu60Le+SFP3uUfXE8TcHramXbH0n+UBMW6raCAKOkHUU1BtrKxPG1eKU/LBx3Bk5FxyKm7fo0XsCUmqSVK25EHOJfYq1QwIbWICkvQUNu+2Hg8/MQKoFJMentI+GqjdaG76f6Wf+aj9UwA==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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.common;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
public interface RemoteAddressProvider {
|
||||||
|
/**
|
||||||
|
* Get Remote Socket Address associated with transport connection
|
||||||
|
*
|
||||||
|
* @return Remote Socket Address or null when not connected
|
||||||
|
*/
|
||||||
|
InetSocketAddress getRemoteSocketAddress();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.common;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
public class ThreadNameProvider {
|
||||||
|
private static final String DISCONNECTED = "DISCONNECTED";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Thread Name prefixed with sshj followed by class and remote address when connected
|
||||||
|
*
|
||||||
|
* @param thread Class of Thread being named
|
||||||
|
* @param remoteAddressProvider Remote Address Provider associated with Thread
|
||||||
|
*/
|
||||||
|
public static void setThreadName(final Thread thread, final RemoteAddressProvider remoteAddressProvider) {
|
||||||
|
final InetSocketAddress remoteSocketAddress = remoteAddressProvider.getRemoteSocketAddress();
|
||||||
|
final String address = remoteSocketAddress == null ? DISCONNECTED : remoteSocketAddress.toString();
|
||||||
|
final long started = System.currentTimeMillis();
|
||||||
|
final String threadName = String.format("sshj-%s-%s-%d", thread.getClass().getSimpleName(), address, started);
|
||||||
|
thread.setName(threadName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public class KeyAlgorithms {
|
public class KeyAlgorithms {
|
||||||
|
|
||||||
public static List<String> SSH_RSA_SHA2_ALGORITHMS = Arrays.asList("rsa-sha2-512", "rsa-sha2-256");
|
|
||||||
|
|
||||||
public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); }
|
public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); }
|
||||||
public static Factory SSHRSACertV01() { return new Factory("ssh-rsa-cert-v01@openssh.com", new SignatureRSA.FactoryCERT(), KeyType.RSA_CERT); }
|
public static Factory SSHRSACertV01() { return new Factory("ssh-rsa-cert-v01@openssh.com", new SignatureRSA.FactoryCERT(), KeyType.RSA_CERT); }
|
||||||
public static Factory RSASHA256() { return new Factory("rsa-sha2-256", new SignatureRSA.FactoryRSASHA256(), KeyType.RSA); }
|
public static Factory RSASHA256() { return new Factory("rsa-sha2-256", new SignatureRSA.FactoryRSASHA256(), KeyType.RSA); }
|
||||||
@@ -61,6 +59,10 @@ public class KeyAlgorithms {
|
|||||||
return algorithmName;
|
return algorithmName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeyType getKeyType() {
|
||||||
|
return keyType;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeyAlgorithm create() {
|
public KeyAlgorithm create() {
|
||||||
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);
|
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);
|
||||||
|
|||||||
@@ -177,11 +177,11 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
|||||||
Arrays.fill(charBuffer.array(), '\u0000');
|
Arrays.fill(charBuffer.array(), '\u0000');
|
||||||
Arrays.fill(byteBuffer.array(), (byte) 0);
|
Arrays.fill(byteBuffer.array(), (byte) 0);
|
||||||
}
|
}
|
||||||
byte[] keyiv = new byte[48];
|
byte[] keyiv = new byte[cipher.getIVSize()+ cipher.getBlockSize()];
|
||||||
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
|
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
|
||||||
Arrays.fill(passphrase, (byte) 0);
|
Arrays.fill(passphrase, (byte) 0);
|
||||||
byte[] key = Arrays.copyOfRange(keyiv, 0, 32);
|
byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize());
|
||||||
byte[] iv = Arrays.copyOfRange(keyiv, 32, 48);
|
byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize());
|
||||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
|
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
|
||||||
@@ -193,6 +193,8 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
|||||||
return BlockCiphers.AES256CTR().create();
|
return BlockCiphers.AES256CTR().create();
|
||||||
} else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) {
|
} else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) {
|
||||||
return BlockCiphers.AES256CBC().create();
|
return BlockCiphers.AES256CBC().create();
|
||||||
|
} else if (cipherName.equals(BlockCiphers.AES128CBC().getName())) {
|
||||||
|
return BlockCiphers.AES128CBC().create();
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
|
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
|
||||||
}
|
}
|
||||||
@@ -216,6 +218,9 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
|||||||
while (line != null && !line.startsWith(BEGIN)) {
|
while (line != null && !line.startsWith(BEGIN)) {
|
||||||
line = reader.readLine();
|
line = reader.readLine();
|
||||||
}
|
}
|
||||||
|
if (line == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
line = line.substring(BEGIN.length());
|
line = line.substring(BEGIN.length());
|
||||||
return line.startsWith(OPENSSH_PRIVATE_KEY);
|
return line.startsWith(OPENSSH_PRIVATE_KEY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ public class Promise<V, T extends Throwable> {
|
|||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
} catch (InterruptedException ie) {
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw chainer.chain(ie);
|
throw chainer.chain(ie);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class Heartbeater
|
|||||||
extends KeepAlive {
|
extends KeepAlive {
|
||||||
|
|
||||||
Heartbeater(ConnectionImpl conn) {
|
Heartbeater(ConnectionImpl conn) {
|
||||||
super(conn, "heartbeater");
|
super(conn, "sshj-Heartbeater");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import net.schmizz.sshj.connection.ConnectionImpl;
|
|||||||
import net.schmizz.sshj.transport.TransportException;
|
import net.schmizz.sshj.transport.TransportException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public abstract class KeepAlive extends Thread {
|
public abstract class KeepAlive extends Thread {
|
||||||
protected final Logger log;
|
protected final Logger log;
|
||||||
protected final ConnectionImpl conn;
|
protected final ConnectionImpl conn;
|
||||||
@@ -33,40 +35,49 @@ public abstract class KeepAlive extends Thread {
|
|||||||
setDaemon(true);
|
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() {
|
public synchronized int getKeepAliveInterval() {
|
||||||
return keepAliveInterval;
|
return keepAliveInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set KeepAlive interval in seconds
|
||||||
|
*
|
||||||
|
* @param keepAliveInterval KeepAlive interval in seconds
|
||||||
|
*/
|
||||||
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
|
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
|
||||||
this.keepAliveInterval = keepAliveInterval;
|
this.keepAliveInterval = keepAliveInterval;
|
||||||
if (keepAliveInterval > 0 && getState() == State.NEW) {
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized protected int getPositiveInterval()
|
|
||||||
throws InterruptedException {
|
|
||||||
while (keepAliveInterval <= 0) {
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
return keepAliveInterval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
log.debug("Starting {}, sending keep-alive every {} seconds", getClass().getSimpleName(), keepAliveInterval);
|
log.debug("{} Started with interval [{} seconds]", getClass().getSimpleName(), keepAliveInterval);
|
||||||
try {
|
try {
|
||||||
while (!isInterrupted()) {
|
while (!isInterrupted()) {
|
||||||
final int hi = getPositiveInterval();
|
final int interval = getKeepAliveInterval();
|
||||||
if (conn.getTransport().isRunning()) {
|
if (conn.getTransport().isRunning()) {
|
||||||
log.debug("Sending keep-alive since {} seconds elapsed", hi);
|
log.debug("{} Sending after interval [{} seconds]", getClass().getSimpleName(), interval);
|
||||||
doKeepAlive();
|
doKeepAlive();
|
||||||
}
|
}
|
||||||
Thread.sleep(hi * 1000);
|
TimeUnit.SECONDS.sleep(interval);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// Interrupt signal may be catched when sleeping.
|
// 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) {
|
} catch (Exception e) {
|
||||||
// If we weren't interrupted, kill the transport, then this exception was unexpected.
|
// If we weren't interrupted, kill the transport, then this exception was unexpected.
|
||||||
// Else we're in shutdown-mode already, so don't forcibly kill the transport.
|
// Else we're in shutdown-mode already, so don't forcibly kill the transport.
|
||||||
@@ -74,9 +85,7 @@ public abstract class KeepAlive extends Thread {
|
|||||||
conn.getTransport().die(e);
|
conn.getTransport().die(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.debug("{} Stopped", getClass().getSimpleName());
|
||||||
log.debug("Stopping {}", getClass().getSimpleName());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void doKeepAlive() throws TransportException, ConnectionException;
|
protected abstract void doKeepAlive() throws TransportException, ConnectionException;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class KeepAliveRunner extends KeepAlive {
|
|||||||
new LinkedList<Promise<SSHPacket, ConnectionException>>();
|
new LinkedList<Promise<SSHPacket, ConnectionException>>();
|
||||||
|
|
||||||
KeepAliveRunner(ConnectionImpl conn) {
|
KeepAliveRunner(ConnectionImpl conn) {
|
||||||
super(conn, "keep-alive");
|
super(conn, "sshj-KeepAliveRunner");
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public int getMaxAliveCount() {
|
synchronized public int getMaxAliveCount() {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import net.schmizz.sshj.transport.mac.MAC;
|
|||||||
import net.schmizz.sshj.transport.random.Random;
|
import net.schmizz.sshj.transport.random.Random;
|
||||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -188,4 +189,30 @@ public class ConfigImpl
|
|||||||
public void setVerifyHostKeyCertificates(boolean value) {
|
public void setVerifyHostKeyCertificates(boolean value) {
|
||||||
verifyHostKeyCertificates = value;
|
verifyHostKeyCertificates = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modern servers neglect the key algorithm ssh-rsa. OpenSSH 8.8 even dropped its support by default in favour
|
||||||
|
* of rsa-sha2-*. However, there are legacy servers like Apache SSHD that don't support the newer replacements
|
||||||
|
* for ssh-rsa.
|
||||||
|
*
|
||||||
|
* If ssh-rsa factory is in {@link #getKeyAlgorithms()}, this methods makes ssh-rsa key algorithm more preferred
|
||||||
|
* than any of rsa-sha2-*. Otherwise, nothing happens.
|
||||||
|
*/
|
||||||
|
public void prioritizeSshRsaKeyAlgorithm() {
|
||||||
|
List<Factory.Named<KeyAlgorithm>> keyAlgorithms = getKeyAlgorithms();
|
||||||
|
for (int sshRsaIndex = 0; sshRsaIndex < keyAlgorithms.size(); ++ sshRsaIndex) {
|
||||||
|
if ("ssh-rsa".equals(keyAlgorithms.get(sshRsaIndex).getName())) {
|
||||||
|
for (int i = 0; i < sshRsaIndex; ++i) {
|
||||||
|
final String algo = keyAlgorithms.get(i).getName();
|
||||||
|
if ("rsa-sha2-256".equals(algo) || "rsa-sha2-512".equals(algo)) {
|
||||||
|
keyAlgorithms = new ArrayList<>(keyAlgorithms);
|
||||||
|
keyAlgorithms.add(i, keyAlgorithms.remove(sshRsaIndex));
|
||||||
|
setKeyAlgorithms(keyAlgorithms);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,9 @@ import net.schmizz.sshj.transport.kex.Curve25519SHA256;
|
|||||||
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
||||||
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
||||||
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
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.JCERandom;
|
||||||
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
||||||
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
|
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.PKCS8KeyFile;
|
||||||
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
|
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -162,7 +160,6 @@ public class DefaultConfig
|
|||||||
setFileKeyProviderFactories(
|
setFileKeyProviderFactories(
|
||||||
new OpenSSHKeyV1KeyFile.Factory(),
|
new OpenSSHKeyV1KeyFile.Factory(),
|
||||||
new PKCS8KeyFile.Factory(),
|
new PKCS8KeyFile.Factory(),
|
||||||
new PKCS5KeyFile.Factory(),
|
|
||||||
new OpenSSHKeyFile.Factory(),
|
new OpenSSHKeyFile.Factory(),
|
||||||
new PuTTYKeyFile.Factory());
|
new PuTTYKeyFile.Factory());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj;
|
package net.schmizz.sshj;
|
||||||
|
|
||||||
|
import net.schmizz.keepalive.KeepAlive;
|
||||||
|
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||||
import net.schmizz.sshj.common.*;
|
import net.schmizz.sshj.common.*;
|
||||||
import net.schmizz.sshj.connection.Connection;
|
import net.schmizz.sshj.connection.Connection;
|
||||||
import net.schmizz.sshj.connection.ConnectionException;
|
import net.schmizz.sshj.connection.ConnectionException;
|
||||||
@@ -38,6 +40,7 @@ import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
|||||||
import net.schmizz.sshj.transport.verification.FingerprintVerifier;
|
import net.schmizz.sshj.transport.verification.FingerprintVerifier;
|
||||||
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
||||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
|
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
|
||||||
|
import net.schmizz.sshj.userauth.AuthResult;
|
||||||
import net.schmizz.sshj.userauth.UserAuth;
|
import net.schmizz.sshj.userauth.UserAuth;
|
||||||
import net.schmizz.sshj.userauth.UserAuthException;
|
import net.schmizz.sshj.userauth.UserAuthException;
|
||||||
import net.schmizz.sshj.userauth.UserAuthImpl;
|
import net.schmizz.sshj.userauth.UserAuthImpl;
|
||||||
@@ -55,6 +58,7 @@ import javax.security.auth.login.LoginContext;
|
|||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
@@ -215,13 +219,30 @@ public class SSHClient
|
|||||||
throws UserAuthException, TransportException {
|
throws UserAuthException, TransportException {
|
||||||
checkConnected();
|
checkConnected();
|
||||||
final Deque<UserAuthException> savedEx = new LinkedList<UserAuthException>();
|
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);
|
method.setLoggerFactory(loggerFactory);
|
||||||
|
|
||||||
try {
|
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;
|
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) {
|
} catch (UserAuthException e) {
|
||||||
savedEx.push(e);
|
savedEx.push(e);
|
||||||
|
tried.add(method);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new UserAuthException("Exhausted available authentication methods", savedEx.peek());
|
throw new UserAuthException("Exhausted available authentication methods", savedEx.peek());
|
||||||
@@ -424,6 +445,7 @@ public class SSHClient
|
|||||||
@Override
|
@Override
|
||||||
public void disconnect()
|
public void disconnect()
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
conn.getKeepAlive().interrupt();
|
||||||
for (LocalPortForwarder forwarder : forwarders) {
|
for (LocalPortForwarder forwarder : forwarders) {
|
||||||
try {
|
try {
|
||||||
forwarder.close();
|
forwarder.close();
|
||||||
@@ -441,6 +463,16 @@ public class SSHClient
|
|||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Remote Socket Address from Transport
|
||||||
|
*
|
||||||
|
* @return Remote Socket Address or null when not connected
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress getRemoteSocketAddress() {
|
||||||
|
return trans.getRemoteSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the character set used to communicate with the remote machine for certain strings (like paths).
|
* Returns the character set used to communicate with the remote machine for certain strings (like paths).
|
||||||
*
|
*
|
||||||
@@ -537,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:
|
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>PKCS8 (OpenSSH uses this format)</li>
|
* <li>PKCS8 (OpenSSH uses this format)</li>
|
||||||
* <li>PKCS5</li>
|
* <li>PEM-encoded PKCS1</li>
|
||||||
* <li>Putty keyfile</li>
|
* <li>Putty keyfile</li>
|
||||||
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
|
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -791,7 +823,17 @@ public class SSHClient
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
super.onConnect();
|
super.onConnect();
|
||||||
trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream());
|
trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream());
|
||||||
doKex();
|
final KeepAlive keepAliveThread = conn.getKeepAlive();
|
||||||
|
if (keepAliveThread.isEnabled()) {
|
||||||
|
ThreadNameProvider.setThreadName(conn.getKeepAlive(), trans);
|
||||||
|
keepAliveThread.start();
|
||||||
|
}
|
||||||
|
if (trans.isKeyExchangeRequired()) {
|
||||||
|
log.debug("Initiating Key Exchange for new connection");
|
||||||
|
doKex();
|
||||||
|
} else {
|
||||||
|
log.debug("Key Exchange already completed for new connection");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj;
|
package net.schmizz.sshj;
|
||||||
|
|
||||||
import com.hierynomus.sshj.backport.JavaVersion;
|
|
||||||
import com.hierynomus.sshj.backport.Jdk7HttpProxySocket;
|
|
||||||
import net.schmizz.sshj.connection.channel.Channel;
|
import net.schmizz.sshj.connection.channel.Channel;
|
||||||
import net.schmizz.sshj.connection.channel.direct.DirectConnection;
|
import net.schmizz.sshj.connection.channel.direct.DirectConnection;
|
||||||
|
|
||||||
@@ -26,7 +24,6 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Proxy;
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
public abstract class SocketClient {
|
public abstract class SocketClient {
|
||||||
@@ -57,73 +54,6 @@ public abstract class SocketClient {
|
|||||||
return new InetSocketAddress(hostname, port);
|
return new InetSocketAddress(hostname, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to a host via a proxy.
|
|
||||||
* @param hostname The host name to connect to.
|
|
||||||
* @param proxy The proxy to connect via.
|
|
||||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
|
||||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void connect(String hostname, Proxy proxy) throws IOException {
|
|
||||||
connect(hostname, defaultPort, proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to a host via a proxy.
|
|
||||||
* @param hostname The host name to connect to.
|
|
||||||
* @param port The port to connect to.
|
|
||||||
* @param proxy The proxy to connect via.
|
|
||||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
|
||||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void connect(String hostname, int port, Proxy proxy) throws IOException {
|
|
||||||
this.hostname = hostname;
|
|
||||||
this.port = port;
|
|
||||||
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
|
|
||||||
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
|
|
||||||
socket = new Jdk7HttpProxySocket(proxy);
|
|
||||||
} else {
|
|
||||||
socket = new Socket(proxy);
|
|
||||||
}
|
|
||||||
socket.connect(makeInetSocketAddress(hostname, port), connectTimeout);
|
|
||||||
onConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to a host via a proxy.
|
|
||||||
* @param host The host address to connect to.
|
|
||||||
* @param proxy The proxy to connect via.
|
|
||||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
|
||||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void connect(InetAddress host, Proxy proxy) throws IOException {
|
|
||||||
connect(host, defaultPort, proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to a host via a proxy.
|
|
||||||
* @param host The host address to connect to.
|
|
||||||
* @param port The port to connect to.
|
|
||||||
* @param proxy The proxy to connect via.
|
|
||||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
|
||||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void connect(InetAddress host, int port, Proxy proxy) throws IOException {
|
|
||||||
this.port = port;
|
|
||||||
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
|
|
||||||
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
|
|
||||||
socket = new Jdk7HttpProxySocket(proxy);
|
|
||||||
} else {
|
|
||||||
socket = new Socket(proxy);
|
|
||||||
}
|
|
||||||
socket.connect(new InetSocketAddress(host, port), connectTimeout);
|
|
||||||
onConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connect(String hostname) throws IOException {
|
public void connect(String hostname) throws IOException {
|
||||||
connect(hostname, defaultPort);
|
connect(hostname, defaultPort);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,8 +145,14 @@ public class StreamCopier {
|
|||||||
final double sizeKiB = count / 1024.0;
|
final double sizeKiB = count / 1024.0;
|
||||||
log.debug(String.format("%1$,.1f KiB transferred in %2$,.1f seconds (%3$,.2f KiB/s)", sizeKiB, timeSeconds, (sizeKiB / timeSeconds)));
|
log.debug(String.format("%1$,.1f KiB transferred in %2$,.1f seconds (%3$,.2f KiB/s)", sizeKiB, timeSeconds, (sizeKiB / timeSeconds)));
|
||||||
|
|
||||||
if (length != -1 && read == -1)
|
// Did we encounter EOF?
|
||||||
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
|
if (read == -1) {
|
||||||
|
// If InputStream was closed we should also close OutputStream
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
if (length != -1)
|
||||||
|
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
private void gotChannelRequest(SSHPacket buf)
|
||||||
throws ConnectionException, TransportException {
|
throws ConnectionException, TransportException {
|
||||||
final String reqType;
|
final String reqType;
|
||||||
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
|
|||||||
+ rwin + " >";
|
+ rwin + " >";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface TransportRunnable {
|
||||||
|
void run() throws TransportException, ConnectionException;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ public final class ChannelInputStream
|
|||||||
try {
|
try {
|
||||||
buf.wait();
|
buf.wait();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw (IOException) new InterruptedIOException().initCause(e);
|
throw (IOException) new InterruptedIOException().initCause(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
*/
|
*/
|
||||||
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
|
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
|
||||||
|
|
||||||
private final Channel chan;
|
private final AbstractChannel chan;
|
||||||
private final Transport trans;
|
private final Transport trans;
|
||||||
private final Window.Remote win;
|
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 SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
|
||||||
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
|
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() {
|
DataBuffer() {
|
||||||
headerOffset = packet.rpos();
|
headerOffset = packet.rpos();
|
||||||
@@ -99,8 +105,9 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
|||||||
if (leftOverBytes > 0) {
|
if (leftOverBytes > 0) {
|
||||||
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
|
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
|
||||||
}
|
}
|
||||||
|
if (!chan.whileOpen(packetWriteRunnable)) {
|
||||||
trans.write(packet);
|
throwStreamClosed();
|
||||||
|
}
|
||||||
win.consume(writeNow);
|
win.consume(writeNow);
|
||||||
|
|
||||||
packet.rpos(headerOffset);
|
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.chan = chan;
|
||||||
this.trans = trans;
|
this.trans = trans;
|
||||||
this.win = win;
|
this.win = win;
|
||||||
@@ -157,7 +164,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
|||||||
if (error != null) {
|
if (error != null) {
|
||||||
throw error;
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
throw new ConnectionException("Stream closed");
|
throwStreamClosed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,9 +172,14 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
|||||||
@Override
|
@Override
|
||||||
public synchronized void close() throws IOException {
|
public synchronized void close() throws IOException {
|
||||||
// Not closed yet, and underlying channel is open to flush the data to.
|
// Not closed yet, and underlying channel is open to flush the data to.
|
||||||
if (!closed.getAndSet(true) && chan.isOpen()) {
|
if (!closed.getAndSet(true)) {
|
||||||
buffer.flush(false);
|
chan.whileOpen(new AbstractChannel.TransportRunnable() {
|
||||||
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
@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() + " >";
|
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");
|
throw new ConnectionException("Timeout when trying to expand the window size");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException ie) {
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw new ConnectionException(ie);
|
throw new ConnectionException(ie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.connection.channel.direct;
|
package net.schmizz.sshj.connection.channel.direct;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||||
import net.schmizz.sshj.common.SSHException;
|
import net.schmizz.sshj.common.SSHException;
|
||||||
|
|
||||||
/** A factory interface for creating SSH {@link Session session channels}. */
|
/** A factory interface for creating SSH {@link Session session channels}. */
|
||||||
public interface SessionFactory {
|
public interface SessionFactory extends RemoteAddressProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String)
|
* Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String)
|
||||||
@@ -27,7 +28,7 @@ public interface SessionFactory {
|
|||||||
*
|
*
|
||||||
* @return the opened {@code session} channel
|
* @return the opened {@code session} channel
|
||||||
*
|
*
|
||||||
* @throws SSHException
|
* @throws SSHException Thrown on session initialization failures
|
||||||
* @see Session
|
* @see Session
|
||||||
*/
|
*/
|
||||||
Session startSession()
|
Session startSession()
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ public class RemotePortForwarder
|
|||||||
// Listen on all IPv4
|
// Listen on all IPv4
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ("0.0.0.0".equals(address) && "0:0:0:0:0:0:0:0".equals(channelForward.address)) {
|
||||||
|
// Handle IPv4 requests on IPv6 channel forward
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class PacketReader extends Thread {
|
|||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
log = engine.getLoggerFactory().getLogger(getClass());
|
log = engine.getLoggerFactory().getLogger(getClass());
|
||||||
this.in = engine.getSubsystem().getInputStream();
|
this.in = engine.getSubsystem().getInputStream();
|
||||||
setName("sftp reader");
|
setName("sshj-PacketReader");
|
||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -220,49 +222,104 @@ public class RemoteFile
|
|||||||
|
|
||||||
public class ReadAheadRemoteFileInputStream
|
public class ReadAheadRemoteFileInputStream
|
||||||
extends InputStream {
|
extends InputStream {
|
||||||
|
private class UnconfirmedRead {
|
||||||
|
private final long offset;
|
||||||
|
private final Promise<Response, SFTPException> promise;
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
private UnconfirmedRead(long offset, int length, Promise<Response, SFTPException> promise) {
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
this.promise = promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnconfirmedRead(long offset, int length) throws IOException {
|
||||||
|
this(offset, length, RemoteFile.this.asyncRead(offset, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOffset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Promise<Response, SFTPException> getPromise() {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final byte[] b = new byte[1];
|
private final byte[] b = new byte[1];
|
||||||
|
|
||||||
private final int maxUnconfirmedReads;
|
private final int maxUnconfirmedReads;
|
||||||
private final Queue<Promise<Response, SFTPException>> unconfirmedReads = new LinkedList<Promise<Response, SFTPException>>();
|
private final long readAheadLimit;
|
||||||
private final Queue<Long> unconfirmedReadOffsets = new LinkedList<Long>();
|
private final Deque<UnconfirmedRead> unconfirmedReads = new ArrayDeque<>();
|
||||||
|
|
||||||
private long requestOffset;
|
private long currentOffset;
|
||||||
private long responseOffset;
|
private int maxReadLength = Integer.MAX_VALUE;
|
||||||
private boolean eof;
|
private boolean eof;
|
||||||
|
|
||||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
||||||
assert 0 <= maxUnconfirmedReads;
|
this(maxUnconfirmedReads, 0L);
|
||||||
|
}
|
||||||
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
|
/**
|
||||||
|
*
|
||||||
|
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
|
||||||
|
* @param fileOffset Initial offset in file to read from
|
||||||
|
* @param readAheadLimit Read ahead is disabled after this limit has been reached
|
||||||
|
*/
|
||||||
|
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset, long readAheadLimit) {
|
||||||
assert 0 <= maxUnconfirmedReads;
|
assert 0 <= maxUnconfirmedReads;
|
||||||
assert 0 <= fileOffset;
|
assert 0 <= fileOffset;
|
||||||
|
|
||||||
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
||||||
this.requestOffset = this.responseOffset = fileOffset;
|
this.currentOffset = fileOffset;
|
||||||
|
this.readAheadLimit = readAheadLimit > 0 ? fileOffset + readAheadLimit : Long.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]);
|
private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]);
|
||||||
|
|
||||||
private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException {
|
private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException {
|
||||||
if (unconfirmedReads.size() <= 0) {
|
final UnconfirmedRead unconfirmedRead = unconfirmedReads.peek();
|
||||||
|
if (unconfirmedRead == null || !blocking && !unconfirmedRead.getPromise().isDelivered()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
unconfirmedReads.remove(unconfirmedRead);
|
||||||
|
|
||||||
if (!blocking && !unconfirmedReads.peek().isDelivered()) {
|
final Response res = unconfirmedRead.promise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
unconfirmedReadOffsets.remove();
|
|
||||||
final Response res = unconfirmedReads.remove().retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
|
||||||
switch (res.getType()) {
|
switch (res.getType()) {
|
||||||
case DATA:
|
case DATA:
|
||||||
int recvLen = res.readUInt32AsInt();
|
int recvLen = res.readUInt32AsInt();
|
||||||
responseOffset += recvLen;
|
if (unconfirmedRead.offset == currentOffset) {
|
||||||
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
|
currentOffset += recvLen;
|
||||||
|
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
|
||||||
|
|
||||||
|
if (recvLen < unconfirmedRead.length) {
|
||||||
|
// The server returned a packet smaller than the client had requested.
|
||||||
|
// It can be caused by at least one of the following:
|
||||||
|
// * The file has been read fully. Then, few futile read requests can be sent during
|
||||||
|
// the next read(), but the file will be downloaded correctly anyway.
|
||||||
|
// * The server shapes the request length. Then, the read window will be adjusted,
|
||||||
|
// and all further read-ahead requests won't be shaped.
|
||||||
|
// * The file on the server is not a regular file, it is something like fifo.
|
||||||
|
// Then, the window will shrink, and the client will start reading the file slower than it
|
||||||
|
// hypothetically can. It must be a rare case, and it is not worth implementing a sort of
|
||||||
|
// congestion control algorithm here.
|
||||||
|
maxReadLength = recvLen;
|
||||||
|
unconfirmedReads.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATUS:
|
case STATUS:
|
||||||
@@ -290,40 +347,31 @@ public class RemoteFile
|
|||||||
// we also need to go here for len <= 0, because pending may be at
|
// we also need to go here for len <= 0, because pending may be at
|
||||||
// EOF in which case it would return -1 instead of 0
|
// EOF in which case it would return -1 instead of 0
|
||||||
|
|
||||||
|
long requestOffset;
|
||||||
|
if (unconfirmedReads.isEmpty()) {
|
||||||
|
requestOffset = currentOffset;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final UnconfirmedRead lastRequest = unconfirmedReads.getLast();
|
||||||
|
requestOffset = lastRequest.offset + lastRequest.length;
|
||||||
|
}
|
||||||
while (unconfirmedReads.size() <= maxUnconfirmedReads) {
|
while (unconfirmedReads.size() <= maxUnconfirmedReads) {
|
||||||
// Send read requests as long as there is no EOF and we have not reached the maximum parallelism
|
// Send read requests as long as there is no EOF and we have not reached the maximum parallelism
|
||||||
int reqLen = Math.max(1024, len); // don't be shy!
|
int reqLen = Math.min(Math.max(1024, len), maxReadLength);
|
||||||
unconfirmedReads.add(RemoteFile.this.asyncRead(requestOffset, reqLen));
|
if (readAheadLimit > requestOffset) {
|
||||||
unconfirmedReadOffsets.add(requestOffset);
|
long remaining = readAheadLimit - requestOffset;
|
||||||
|
if (reqLen > remaining) {
|
||||||
|
reqLen = (int) remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unconfirmedReads.add(new UnconfirmedRead(requestOffset, reqLen));
|
||||||
requestOffset += reqLen;
|
requestOffset += reqLen;
|
||||||
|
if (requestOffset >= readAheadLimit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long nextOffset = unconfirmedReadOffsets.peek();
|
if (!retrieveUnconfirmedRead(true /*blocking*/)) {
|
||||||
if (responseOffset != nextOffset) {
|
|
||||||
|
|
||||||
// the server could not give us all the data we needed, so
|
|
||||||
// we try to fill the gap synchronously
|
|
||||||
|
|
||||||
assert responseOffset < nextOffset;
|
|
||||||
assert 0 < (nextOffset - responseOffset);
|
|
||||||
assert (nextOffset - responseOffset) <= Integer.MAX_VALUE;
|
|
||||||
|
|
||||||
byte[] buf = new byte[(int) (nextOffset - responseOffset)];
|
|
||||||
int recvLen = RemoteFile.this.read(responseOffset, buf, 0, buf.length);
|
|
||||||
|
|
||||||
if (recvLen < 0) {
|
|
||||||
eof = true;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 == recvLen) {
|
|
||||||
// avoid infinite loops
|
|
||||||
throw new SFTPException("Unexpected response size (0), bailing out");
|
|
||||||
}
|
|
||||||
|
|
||||||
responseOffset += recvLen;
|
|
||||||
pending = new ByteArrayInputStream(buf, 0, recvLen);
|
|
||||||
} else if (!retrieveUnconfirmedRead(true /*blocking*/)) {
|
|
||||||
|
|
||||||
// this may happen if we change prefetch strategy
|
// this may happen if we change prefetch strategy
|
||||||
// currently, we should never get here...
|
// currently, we should never get here...
|
||||||
|
|||||||
@@ -232,21 +232,41 @@ public class SFTPClient
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
xfer.download(source, dest);
|
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)
|
public void put(String source, String dest)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
xfer.upload(source, dest);
|
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)
|
public void get(String source, LocalDestFile dest)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
xfer.download(source, dest);
|
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)
|
public void put(LocalSourceFile source, String dest)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
xfer.upload(source, dest);
|
xfer.upload(source, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void put(LocalSourceFile source, String dest, long byteOffset)
|
||||||
|
throws IOException {
|
||||||
|
xfer.upload(source, dest, byteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close()
|
public void close()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.sftp;
|
package net.schmizz.sshj.sftp;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||||
import net.schmizz.concurrent.Promise;
|
import net.schmizz.concurrent.Promise;
|
||||||
import net.schmizz.sshj.common.IOUtils;
|
import net.schmizz.sshj.common.IOUtils;
|
||||||
import net.schmizz.sshj.common.LoggerFactory;
|
import net.schmizz.sshj.common.LoggerFactory;
|
||||||
@@ -68,6 +69,7 @@ public class SFTPEngine
|
|||||||
sub = session.startSubsystem("sftp");
|
sub = session.startSubsystem("sftp");
|
||||||
out = sub.getOutputStream();
|
out = sub.getOutputStream();
|
||||||
reader = new PacketReader(this);
|
reader = new PacketReader(this);
|
||||||
|
ThreadNameProvider.setThreadName(reader, ssh);
|
||||||
pathHelper = new PathHelper(new PathHelper.Canonicalizer() {
|
pathHelper = new PathHelper(new PathHelper.Canonicalizer() {
|
||||||
@Override
|
@Override
|
||||||
public String canonicalize(String path)
|
public String canonicalize(String path)
|
||||||
@@ -235,14 +237,17 @@ public class SFTPEngine
|
|||||||
if (operativeVersion < 1)
|
if (operativeVersion < 1)
|
||||||
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
|
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
|
||||||
|
|
||||||
long renameFlagMask = 0L;
|
final Request request = newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset());
|
||||||
for (RenameFlags flag : flags) {
|
// SFTP Version 5 introduced rename flags according to Section 6.5 of the specification
|
||||||
renameFlagMask = renameFlagMask | flag.longValue();
|
if (operativeVersion >= 5) {
|
||||||
|
long renameFlagMask = 0L;
|
||||||
|
for (RenameFlags flag : flags) {
|
||||||
|
renameFlagMask = renameFlagMask | flag.longValue();
|
||||||
|
}
|
||||||
|
request.putUInt32(renameFlagMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
doRequest(
|
doRequest(request).ensureStatusPacketIsOK();
|
||||||
newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset()).putUInt32(renameFlagMask)
|
|
||||||
).ensureStatusPacketIsOK();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String canonicalize(String path)
|
public String canonicalize(String path)
|
||||||
|
|||||||
@@ -50,25 +50,47 @@ public class SFTPFileTransfer
|
|||||||
@Override
|
@Override
|
||||||
public void upload(String source, String dest)
|
public void upload(String source, String dest)
|
||||||
throws IOException {
|
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
|
@Override
|
||||||
public void download(String source, String dest)
|
public void download(String source, String dest)
|
||||||
throws IOException {
|
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
|
@Override
|
||||||
public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
|
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
|
@Override
|
||||||
public void download(String source, LocalDestFile dest) throws IOException {
|
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 PathComponents pathComponents = engine.getPathHelper().getComponents(source);
|
||||||
final FileAttributes attributes = engine.stat(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) {
|
public void setUploadFilter(LocalFileFilter uploadFilter) {
|
||||||
@@ -92,7 +114,8 @@ public class SFTPFileTransfer
|
|||||||
@SuppressWarnings("PMD.MissingBreakInSwitch")
|
@SuppressWarnings("PMD.MissingBreakInSwitch")
|
||||||
private void download(final TransferListener listener,
|
private void download(final TransferListener listener,
|
||||||
final RemoteResourceInfo remote,
|
final RemoteResourceInfo remote,
|
||||||
final LocalDestFile local) throws IOException {
|
final LocalDestFile local,
|
||||||
|
final long byteOffset) throws IOException {
|
||||||
final LocalDestFile adjustedFile;
|
final LocalDestFile adjustedFile;
|
||||||
switch (remote.getAttributes().getType()) {
|
switch (remote.getAttributes().getType()) {
|
||||||
case DIRECTORY:
|
case DIRECTORY:
|
||||||
@@ -101,8 +124,9 @@ public class SFTPFileTransfer
|
|||||||
case UNKNOWN:
|
case UNKNOWN:
|
||||||
log.warn("Server did not supply information about the type of file at `{}` " +
|
log.warn("Server did not supply information about the type of file at `{}` " +
|
||||||
"-- assuming it is a regular file!", remote.getPath());
|
"-- assuming it is a regular file!", remote.getPath());
|
||||||
|
// fall through
|
||||||
case REGULAR:
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IOException(remote + " is not a regular file or directory");
|
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());
|
final RemoteDirectory rd = engine.openDir(remote.getPath());
|
||||||
try {
|
try {
|
||||||
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
|
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 {
|
} finally {
|
||||||
rd.close();
|
rd.close();
|
||||||
}
|
}
|
||||||
@@ -128,13 +152,15 @@ public class SFTPFileTransfer
|
|||||||
|
|
||||||
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
|
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
|
||||||
final RemoteResourceInfo remote,
|
final RemoteResourceInfo remote,
|
||||||
final LocalDestFile local)
|
final LocalDestFile local,
|
||||||
|
final long byteOffset)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
|
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
|
||||||
final RemoteFile rf = engine.open(remote.getPath());
|
final RemoteFile rf = engine.open(remote.getPath());
|
||||||
try {
|
try {
|
||||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16);
|
log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
|
||||||
final OutputStream os = adjusted.getOutputStream();
|
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
|
||||||
|
final OutputStream os = adjusted.getOutputStream(byteOffset != 0);
|
||||||
try {
|
try {
|
||||||
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
||||||
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
||||||
@@ -173,17 +199,17 @@ public class SFTPFileTransfer
|
|||||||
this.remote = remote;
|
this.remote = remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upload(final TransferListener listener) throws IOException {
|
private void upload(final TransferListener listener, long byteOffset) throws IOException {
|
||||||
if (source.isDirectory()) {
|
if (source.isDirectory()) {
|
||||||
makeDirIfNotExists(remote); // Ensure that the directory exists
|
makeDirIfNotExists(remote); // Ensure that the directory exists
|
||||||
uploadDir(listener.directory(source.getName()), source, remote);
|
uploadDir(listener.directory(source.getName()), source, remote);
|
||||||
setAttributes(source, remote);
|
setAttributes(source, remote);
|
||||||
} else if (source.isFile() && isDirectory(remote)) {
|
} else if (source.isFile() && isDirectory(remote)) {
|
||||||
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
|
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);
|
setAttributes(source, adjustedRemote);
|
||||||
} else if (source.isFile()) {
|
} 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);
|
setAttributes(source, remote);
|
||||||
} else {
|
} else {
|
||||||
throw new IOException(source + " is not a file or directory");
|
throw new IOException(source + " is not a file or directory");
|
||||||
@@ -192,13 +218,14 @@ public class SFTPFileTransfer
|
|||||||
|
|
||||||
private void upload(final TransferListener listener,
|
private void upload(final TransferListener listener,
|
||||||
final LocalSourceFile local,
|
final LocalSourceFile local,
|
||||||
final String remote)
|
final String remote,
|
||||||
|
final long byteOffset)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
final String adjustedPath;
|
final String adjustedPath;
|
||||||
if (local.isDirectory()) {
|
if (local.isDirectory()) {
|
||||||
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
|
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
|
||||||
} else if (local.isFile()) {
|
} 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 {
|
} else {
|
||||||
throw new IOException(local + " is not a file or directory");
|
throw new IOException(local + " is not a file or directory");
|
||||||
}
|
}
|
||||||
@@ -217,22 +244,34 @@ public class SFTPFileTransfer
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
makeDirIfNotExists(remote);
|
makeDirIfNotExists(remote);
|
||||||
for (LocalSourceFile f : local.getChildren(getUploadFilter()))
|
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;
|
return remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String uploadFile(final StreamCopier.Listener listener,
|
private String uploadFile(final StreamCopier.Listener listener,
|
||||||
final LocalSourceFile local,
|
final LocalSourceFile local,
|
||||||
final String remote)
|
final String remote,
|
||||||
|
final long byteOffset)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
final String adjusted = prepareFile(local, remote);
|
final String adjusted = prepareFile(local, remote, byteOffset);
|
||||||
RemoteFile rf = null;
|
RemoteFile rf = null;
|
||||||
InputStream fis = null;
|
InputStream fis = null;
|
||||||
RemoteFile.RemoteFileOutputStream rfos = null;
|
RemoteFile.RemoteFileOutputStream rfos = null;
|
||||||
|
EnumSet<OpenMode> modes;
|
||||||
try {
|
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();
|
fis = local.getInputStream();
|
||||||
rfos = rf.new RemoteFileOutputStream(0, 16);
|
fis.skip(byteOffset);
|
||||||
|
rfos = rf.new RemoteFileOutputStream(byteOffset, 16);
|
||||||
new StreamCopier(fis, rfos, engine.getLoggerFactory())
|
new StreamCopier(fis, rfos, engine.getLoggerFactory())
|
||||||
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
|
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
|
||||||
.keepFlushing(false)
|
.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 {
|
throws IOException {
|
||||||
final FileAttributes attrs;
|
final FileAttributes attrs;
|
||||||
try {
|
try {
|
||||||
@@ -309,7 +348,7 @@ public class SFTPFileTransfer
|
|||||||
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
|
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
|
||||||
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
|
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
|
||||||
} else {
|
} 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;
|
return remote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,6 @@ final class KeyExchanger
|
|||||||
negotiatedAlgs.getKeyExchangeAlgorithm());
|
negotiatedAlgs.getKeyExchangeAlgorithm());
|
||||||
transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(),
|
transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(),
|
||||||
negotiatedAlgs.getSignatureAlgorithm()));
|
negotiatedAlgs.getSignatureAlgorithm()));
|
||||||
transport.setRSASHA2Support(negotiatedAlgs.getRSASHA2Support());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
kex.init(transport,
|
kex.init(transport,
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ public final class NegotiatedAlgorithms {
|
|||||||
private final String c2sComp;
|
private final String c2sComp;
|
||||||
private final String s2cComp;
|
private final String s2cComp;
|
||||||
|
|
||||||
private final boolean rsaSHA2Support;
|
|
||||||
|
|
||||||
NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC,
|
NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC,
|
||||||
String c2sComp, String s2cComp, boolean rsaSHA2Support) {
|
String c2sComp, String s2cComp) {
|
||||||
this.kex = kex;
|
this.kex = kex;
|
||||||
this.sig = sig;
|
this.sig = sig;
|
||||||
this.c2sCipher = c2sCipher;
|
this.c2sCipher = c2sCipher;
|
||||||
@@ -38,7 +36,6 @@ public final class NegotiatedAlgorithms {
|
|||||||
this.s2cMAC = s2cMAC;
|
this.s2cMAC = s2cMAC;
|
||||||
this.c2sComp = c2sComp;
|
this.c2sComp = c2sComp;
|
||||||
this.s2cComp = s2cComp;
|
this.s2cComp = s2cComp;
|
||||||
this.rsaSHA2Support = rsaSHA2Support;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getKeyExchangeAlgorithm() {
|
public String getKeyExchangeAlgorithm() {
|
||||||
@@ -73,10 +70,6 @@ public final class NegotiatedAlgorithms {
|
|||||||
return s2cComp;
|
return s2cComp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getRSASHA2Support() {
|
|
||||||
return rsaSHA2Support;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return ("[ " +
|
return ("[ " +
|
||||||
@@ -88,7 +81,6 @@ public final class NegotiatedAlgorithms {
|
|||||||
"s2cMAC=" + s2cMAC + "; " +
|
"s2cMAC=" + s2cMAC + "; " +
|
||||||
"c2sComp=" + c2sComp + "; " +
|
"c2sComp=" + c2sComp + "; " +
|
||||||
"s2cComp=" + s2cComp + "; " +
|
"s2cComp=" + s2cComp + "; " +
|
||||||
"rsaSHA2Support=" + rsaSHA2Support +
|
|
||||||
" ]");
|
" ]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.transport;
|
package net.schmizz.sshj.transport;
|
||||||
|
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
|
||||||
import net.schmizz.sshj.Config;
|
import net.schmizz.sshj.Config;
|
||||||
import net.schmizz.sshj.common.Buffer;
|
import net.schmizz.sshj.common.Buffer;
|
||||||
import net.schmizz.sshj.common.Factory;
|
import net.schmizz.sshj.common.Factory;
|
||||||
@@ -140,8 +139,8 @@ class Proposal {
|
|||||||
firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(),
|
firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(),
|
||||||
other.getClient2ServerCompressionAlgorithms()),
|
other.getClient2ServerCompressionAlgorithms()),
|
||||||
firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(),
|
firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(),
|
||||||
other.getServer2ClientCompressionAlgorithms()),
|
other.getServer2ClientCompressionAlgorithms())
|
||||||
other.getHostKeyAlgorithms().containsAll(KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) {
|
private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public final class Reader
|
|||||||
public Reader(TransportImpl trans) {
|
public Reader(TransportImpl trans) {
|
||||||
this.trans = trans;
|
this.trans = trans;
|
||||||
log = trans.getConfig().getLoggerFactory().getLogger(getClass());
|
log = trans.getConfig().getLoggerFactory().getLogger(getClass());
|
||||||
setName("reader");
|
setName("sshj-Reader");
|
||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.transport;
|
package net.schmizz.sshj.transport;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||||
import net.schmizz.sshj.Config;
|
import net.schmizz.sshj.Config;
|
||||||
import net.schmizz.sshj.Service;
|
import net.schmizz.sshj.Service;
|
||||||
@@ -27,11 +28,12 @@ import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
|||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/** Transport layer of the SSH protocol. */
|
/** Transport layer of the SSH protocol. */
|
||||||
public interface Transport
|
public interface Transport
|
||||||
extends SSHPacketHandler {
|
extends SSHPacketHandler, RemoteAddressProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the host information and the streams to be used by this transport. Identification information is exchanged
|
* Sets the host information and the streams to be used by this transport. Identification information is exchanged
|
||||||
@@ -69,6 +71,13 @@ public interface Transport
|
|||||||
void doKex()
|
void doKex()
|
||||||
throws TransportException;
|
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" */
|
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
|
||||||
String getClientVersion();
|
String getClientVersion();
|
||||||
|
|
||||||
@@ -208,7 +217,7 @@ public interface Transport
|
|||||||
/**
|
/**
|
||||||
* Specify a {@code listener} that will be notified upon disconnection.
|
* Specify a {@code listener} that will be notified upon disconnection.
|
||||||
*
|
*
|
||||||
* @param listener
|
* @param listener Disconnect Listener to be configured
|
||||||
*/
|
*/
|
||||||
void setDisconnectListener(DisconnectListener listener);
|
void setDisconnectListener(DisconnectListener listener);
|
||||||
|
|
||||||
@@ -223,5 +232,5 @@ public interface Transport
|
|||||||
void die(Exception e);
|
void die(Exception e);
|
||||||
|
|
||||||
KeyAlgorithm getHostKeyAlgorithm();
|
KeyAlgorithm getHostKeyAlgorithm();
|
||||||
KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException;
|
List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.transport;
|
package net.schmizz.sshj.transport;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||||
import com.hierynomus.sshj.transport.IdentificationStringParser;
|
import com.hierynomus.sshj.transport.IdentificationStringParser;
|
||||||
@@ -22,7 +23,6 @@ import net.schmizz.concurrent.ErrorDeliveryUtil;
|
|||||||
import net.schmizz.concurrent.Event;
|
import net.schmizz.concurrent.Event;
|
||||||
import net.schmizz.sshj.AbstractService;
|
import net.schmizz.sshj.AbstractService;
|
||||||
import net.schmizz.sshj.Config;
|
import net.schmizz.sshj.Config;
|
||||||
import net.schmizz.sshj.SSHClient;
|
|
||||||
import net.schmizz.sshj.Service;
|
import net.schmizz.sshj.Service;
|
||||||
import net.schmizz.sshj.common.*;
|
import net.schmizz.sshj.common.*;
|
||||||
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
||||||
@@ -32,6 +32,8 @@ import org.slf4j.Logger;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@@ -86,8 +88,6 @@ public final class TransportImpl
|
|||||||
|
|
||||||
private KeyAlgorithm hostKeyAlgorithm;
|
private KeyAlgorithm hostKeyAlgorithm;
|
||||||
|
|
||||||
private boolean rsaSHA2Support;
|
|
||||||
|
|
||||||
private final Event<TransportException> serviceAccept;
|
private final Event<TransportException> serviceAccept;
|
||||||
|
|
||||||
private final Event<TransportException> close;
|
private final Event<TransportException> close;
|
||||||
@@ -130,8 +130,8 @@ public final class TransportImpl
|
|||||||
public TransportImpl(Config config) {
|
public TransportImpl(Config config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.loggerFactory = config.getLoggerFactory();
|
this.loggerFactory = config.getLoggerFactory();
|
||||||
this.serviceAccept = new Event<TransportException>("service accept", TransportException.chainer, loggerFactory);
|
this.serviceAccept = new Event<>("service accept", TransportException.chainer, loggerFactory);
|
||||||
this.close = new Event<TransportException>("transport close", TransportException.chainer, loggerFactory);
|
this.close = new Event<>("transport close", TransportException.chainer, loggerFactory);
|
||||||
this.nullService = new NullService(this);
|
this.nullService = new NullService(this);
|
||||||
this.service = nullService;
|
this.service = nullService;
|
||||||
this.log = loggerFactory.getLogger(getClass());
|
this.log = loggerFactory.getLogger(getClass());
|
||||||
@@ -165,9 +165,20 @@ public final class TransportImpl
|
|||||||
throw new TransportException(e);
|
throw new TransportException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThreadNameProvider.setThreadName(reader, this);
|
||||||
reader.start();
|
reader.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Remote Socket Address using Connection Information
|
||||||
|
*
|
||||||
|
* @return Remote Socket Address or null when not connected
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress getRemoteSocketAddress() {
|
||||||
|
return connInfo == null ? null : new InetSocketAddress(getRemoteHost(), getRemotePort());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TransportImpl implements its own default DisconnectListener.
|
* TransportImpl implements its own default DisconnectListener.
|
||||||
*/
|
*/
|
||||||
@@ -211,7 +222,7 @@ public final class TransportImpl
|
|||||||
*
|
*
|
||||||
* @param buffer The buffer to read from.
|
* @param buffer The buffer to read from.
|
||||||
* @return empty string if full ident string has not yet been received
|
* @return empty string if full ident string has not yet been received
|
||||||
* @throws IOException
|
* @throws IOException Thrown when protocol version is not supported
|
||||||
*/
|
*/
|
||||||
private String readIdentification(Buffer.PlainBuffer buffer)
|
private String readIdentification(Buffer.PlainBuffer buffer)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -243,6 +254,16 @@ public final class TransportImpl
|
|||||||
kexer.startKex(true);
|
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() {
|
public boolean isKexDone() {
|
||||||
return kexer.isKexDone();
|
return kexer.isKexDone();
|
||||||
}
|
}
|
||||||
@@ -544,7 +565,7 @@ public final class TransportImpl
|
|||||||
* Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly.
|
* Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly.
|
||||||
*
|
*
|
||||||
* @param packet The 'unimplemented' packet received
|
* @param packet The 'unimplemented' packet received
|
||||||
* @throws TransportException
|
* @throws TransportException Thrown when key exchange is ongoing
|
||||||
*/
|
*/
|
||||||
private void gotUnimplemented(SSHPacket packet)
|
private void gotUnimplemented(SSHPacket packet)
|
||||||
throws SSHException {
|
throws SSHException {
|
||||||
@@ -626,21 +647,19 @@ public final class TransportImpl
|
|||||||
return this.hostKeyAlgorithm;
|
return this.hostKeyAlgorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRSASHA2Support(boolean rsaSHA2Support) {
|
|
||||||
this.rsaSHA2Support = rsaSHA2Support;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException {
|
public List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException {
|
||||||
if (keyType != KeyType.RSA || !rsaSHA2Support) {
|
|
||||||
return Factory.Named.Util.create(getConfig().getKeyAlgorithms(), keyType.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms();
|
List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms();
|
||||||
|
List<KeyAlgorithm> available = new ArrayList<>();
|
||||||
if (factories != null)
|
if (factories != null)
|
||||||
for (Factory.Named<KeyAlgorithm> f : factories)
|
for (Factory.Named<KeyAlgorithm> f : factories)
|
||||||
if (f.getName().equals("ssh-rsa") || KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS.contains(f.getName()))
|
if (
|
||||||
return f.create();
|
f instanceof KeyAlgorithms.Factory && ((KeyAlgorithms.Factory) f).getKeyType().equals(keyType)
|
||||||
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
|
|| !(f instanceof KeyAlgorithms.Factory) && f.getName().equals(keyType.toString())
|
||||||
|
)
|
||||||
|
available.add(f.create());
|
||||||
|
if (available.isEmpty())
|
||||||
|
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
|
||||||
|
return available;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,19 @@ public class OpenSSHKnownHosts
|
|||||||
for (KnownHostEntry e : entries) {
|
for (KnownHostEntry e : entries) {
|
||||||
try {
|
try {
|
||||||
if (e.appliesTo(adjustedHostname)) {
|
if (e.appliesTo(adjustedHostname)) {
|
||||||
knownHostAlgorithms.add(e.getType().toString());
|
final KeyType type = e.getType();
|
||||||
|
if (e instanceof HostEntry && ((HostEntry) e).marker == Marker.CA_CERT) {
|
||||||
|
// Only the CA key type is known, but the type of the host key is not.
|
||||||
|
// Adding all supported types for keys with certificates.
|
||||||
|
for (final KeyType candidate : KeyType.values()) {
|
||||||
|
if (candidate.getParent() != null) {
|
||||||
|
knownHostAlgorithms.add(candidate.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
knownHostAlgorithms.add(type.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
}
|
}
|
||||||
|
|||||||
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 nextService the service to set on successful authentication
|
||||||
* @param methods the {@link AuthMethod}'s to try
|
* @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 UserAuthException in case of authentication failure
|
||||||
* @throws TransportException if there was a transport-layer error
|
* @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;
|
throws UserAuthException, TransportException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class UserAuthImpl
|
|||||||
extends AbstractService
|
extends AbstractService
|
||||||
implements UserAuth {
|
implements UserAuth {
|
||||||
|
|
||||||
private final Promise<Boolean, UserAuthException> authenticated;
|
private final Promise<AuthResult, UserAuthException> authenticated;
|
||||||
|
|
||||||
// Externally available
|
// Externally available
|
||||||
private volatile String banner = "";
|
private volatile String banner = "";
|
||||||
@@ -53,13 +53,13 @@ public class UserAuthImpl
|
|||||||
|
|
||||||
public UserAuthImpl(Transport trans) {
|
public UserAuthImpl(Transport trans) {
|
||||||
super("ssh-userauth", 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
|
@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 {
|
throws UserAuthException, TransportException {
|
||||||
final boolean outcome;
|
final AuthResult outcome;
|
||||||
|
|
||||||
authenticated.lock();
|
authenticated.lock();
|
||||||
try {
|
try {
|
||||||
@@ -73,8 +73,10 @@ public class UserAuthImpl
|
|||||||
currentMethod.request();
|
currentMethod.request();
|
||||||
outcome = authenticated.retrieve(timeoutMs, TimeUnit.MILLISECONDS);
|
outcome = authenticated.retrieve(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
if (outcome) {
|
if (outcome == AuthResult.SUCCESS) {
|
||||||
log.debug("`{}` auth successful", method.getName());
|
log.debug("`{}` auth successful", method.getName());
|
||||||
|
} else if (outcome == AuthResult.PARTIAL) {
|
||||||
|
log.debug("`{}` auth partially successful", method.getName());
|
||||||
} else {
|
} else {
|
||||||
log.debug("`{}` auth failed", method.getName());
|
log.debug("`{}` auth failed", method.getName());
|
||||||
}
|
}
|
||||||
@@ -124,7 +126,7 @@ public class UserAuthImpl
|
|||||||
// Should fix https://github.com/hierynomus/sshj/issues/237
|
// Should fix https://github.com/hierynomus/sshj/issues/237
|
||||||
trans.setAuthenticated(); // So it can put delayed compression into force if applicable
|
trans.setAuthenticated(); // So it can put delayed compression into force if applicable
|
||||||
trans.setService(nextService); // We aren't in charge anymore, next service is
|
trans.setService(nextService); // We aren't in charge anymore, next service is
|
||||||
authenticated.deliver(true);
|
authenticated.deliver(AuthResult.SUCCESS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case USERAUTH_FAILURE:
|
case USERAUTH_FAILURE:
|
||||||
@@ -133,7 +135,7 @@ public class UserAuthImpl
|
|||||||
if (allowedMethods.contains(currentMethod.getName()) && currentMethod.shouldRetry()) {
|
if (allowedMethods.contains(currentMethod.getName()) && currentMethod.shouldRetry()) {
|
||||||
currentMethod.request();
|
currentMethod.request();
|
||||||
} else {
|
} else {
|
||||||
authenticated.deliver(false);
|
authenticated.deliver(partialSuccess ? AuthResult.PARTIAL : AuthResult.FAILURE);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,9 @@
|
|||||||
package net.schmizz.sshj.userauth.keyprovider;
|
package net.schmizz.sshj.userauth.keyprovider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @version $Id:$
|
* Key File Formats
|
||||||
*/
|
*/
|
||||||
public enum KeyFormat {
|
public enum KeyFormat {
|
||||||
PKCS5,
|
|
||||||
PKCS8,
|
PKCS8,
|
||||||
OpenSSH,
|
OpenSSH,
|
||||||
OpenSSHv1,
|
OpenSSHv1,
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ public class KeyProviderUtil {
|
|||||||
* <p/>
|
* <p/>
|
||||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
* 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
|
* @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)
|
public static KeyFormat detectKeyFileFormat(File location)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -45,7 +45,7 @@ public class KeyProviderUtil {
|
|||||||
* @param privateKey Private key stored in a string
|
* @param privateKey Private key stored in a string
|
||||||
* @param separatePubKey Is the public key stored separately from the private key
|
* @param separatePubKey Is the public key stored separately from the private key
|
||||||
* @return name of the key file format
|
* @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)
|
public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -60,7 +60,7 @@ public class KeyProviderUtil {
|
|||||||
* @param privateKey Private key accessible through a {@code Reader}
|
* @param privateKey Private key accessible through a {@code Reader}
|
||||||
* @param separatePubKey Is the public key stored separately from the private key
|
* @param separatePubKey Is the public key stored separately from the private key
|
||||||
* @return name of the key file format
|
* @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)
|
public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -94,10 +94,8 @@ public class KeyProviderUtil {
|
|||||||
} else if (separatePubKey) {
|
} else if (separatePubKey) {
|
||||||
// Can delay asking for password since have unencrypted pubkey
|
// Can delay asking for password since have unencrypted pubkey
|
||||||
return KeyFormat.OpenSSH;
|
return KeyFormat.OpenSSH;
|
||||||
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
|
|
||||||
return KeyFormat.PKCS8;
|
|
||||||
} else {
|
} else {
|
||||||
return KeyFormat.PKCS5;
|
return KeyFormat.PKCS8;
|
||||||
}
|
}
|
||||||
} else if (header.startsWith("PuTTY-User-Key-File-")) {
|
} else if (header.startsWith("PuTTY-User-Key-File-")) {
|
||||||
return KeyFormat.PuTTY;
|
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.io.IOException;
|
||||||
import java.security.KeyPair;
|
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 class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||||
|
|
||||||
public static class Factory
|
public static class Factory
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ public class AuthGssApiWithMic
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GSSContext run() throws GSSException {
|
public GSSContext run() throws GSSException {
|
||||||
GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME);
|
GSSCredential clientCreds = manager.createCredential(GSSCredential.INITIATE_ONLY);
|
||||||
GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
|
|
||||||
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
|
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
|
||||||
|
|
||||||
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
|
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
|
||||||
|
|||||||
@@ -27,17 +27,36 @@ import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
public abstract class KeyedAuthMethod
|
public abstract class KeyedAuthMethod
|
||||||
extends AbstractAuthMethod {
|
extends AbstractAuthMethod {
|
||||||
|
|
||||||
protected final KeyProvider kProv;
|
protected final KeyProvider kProv;
|
||||||
|
private Queue<KeyAlgorithm> available;
|
||||||
|
|
||||||
public KeyedAuthMethod(String name, KeyProvider kProv) {
|
public KeyedAuthMethod(String name, KeyProvider kProv) {
|
||||||
super(name);
|
super(name);
|
||||||
this.kProv = kProv;
|
this.kProv = kProv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private KeyAlgorithm getPublicKeyAlgorithm(KeyType keyType) throws TransportException {
|
||||||
|
if (available == null) {
|
||||||
|
available = new LinkedList<>(params.getTransport().getClientKeyAlgorithms(keyType));
|
||||||
|
}
|
||||||
|
return available.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRetry() {
|
||||||
|
if (available != null) {
|
||||||
|
available.poll();
|
||||||
|
return !available.isEmpty();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected SSHPacket putPubKey(SSHPacket reqBuf)
|
protected SSHPacket putPubKey(SSHPacket reqBuf)
|
||||||
throws UserAuthException {
|
throws UserAuthException {
|
||||||
PublicKey key;
|
PublicKey key;
|
||||||
@@ -50,13 +69,16 @@ public abstract class KeyedAuthMethod
|
|||||||
// public key as 2 strings: [ key type | key blob ]
|
// public key as 2 strings: [ key type | key blob ]
|
||||||
KeyType keyType = KeyType.fromKey(key);
|
KeyType keyType = KeyType.fromKey(key);
|
||||||
try {
|
try {
|
||||||
KeyAlgorithm ka = params.getTransport().getClientKeyAlgorithm(keyType);
|
KeyAlgorithm ka = getPublicKeyAlgorithm(keyType);
|
||||||
reqBuf.putString(ka.getKeyAlgorithm())
|
if (ka != null) {
|
||||||
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
reqBuf.putString(ka.getKeyAlgorithm())
|
||||||
return reqBuf;
|
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
||||||
|
return reqBuf;
|
||||||
|
}
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType);
|
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType, ioe);
|
||||||
}
|
}
|
||||||
|
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SSHPacket putSig(SSHPacket reqBuf)
|
protected SSHPacket putSig(SSHPacket reqBuf)
|
||||||
@@ -71,7 +93,7 @@ public abstract class KeyedAuthMethod
|
|||||||
final KeyType kt = KeyType.fromKey(key);
|
final KeyType kt = KeyType.fromKey(key);
|
||||||
Signature signature;
|
Signature signature;
|
||||||
try {
|
try {
|
||||||
signature = params.getTransport().getClientKeyAlgorithm(kt).newSignature();
|
signature = getPublicKeyAlgorithm(kt).newSignature();
|
||||||
} catch (TransportException e) {
|
} catch (TransportException e) {
|
||||||
throw new UserAuthException("No KeyAlgorithm configured for key " + kt);
|
throw new UserAuthException("No KeyAlgorithm configured for key " + kt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
|
|||||||
public class PasswordResponseProvider
|
public class PasswordResponseProvider
|
||||||
implements ChallengeResponseProvider {
|
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];
|
private static final char[] EMPTY_RESPONSE = new char[0];
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.userauth.password;
|
package net.schmizz.sshj.userauth.password;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.CharBuffer;
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -64,6 +65,9 @@ public class PasswordUtils {
|
|||||||
*/
|
*/
|
||||||
public static byte[] toByteArray(char[] password) {
|
public static byte[] toByteArray(char[] password) {
|
||||||
CharBuffer charBuffer = CharBuffer.wrap(password);
|
CharBuffer charBuffer = CharBuffer.wrap(password);
|
||||||
return StandardCharsets.UTF_8.encode(charBuffer).array();
|
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
|
||||||
|
byte[] bytes = new byte[byteBuffer.remaining()];
|
||||||
|
byteBuffer.get(bytes, 0, bytes.length);
|
||||||
|
return bytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ public class FileSystemFile
|
|||||||
@Override
|
@Override
|
||||||
public OutputStream getOutputStream()
|
public OutputStream getOutputStream()
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return new FileOutputStream(file);
|
return getOutputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream(boolean append)
|
||||||
|
throws IOException {
|
||||||
|
return new FileOutputStream(file, append);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -31,6 +31,19 @@ public interface FileTransfer {
|
|||||||
void upload(String localPath, String remotePath)
|
void upload(String localPath, String remotePath)
|
||||||
throws IOException;
|
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.
|
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||||
* a {@link FileSystemFile}.
|
* a {@link FileSystemFile}.
|
||||||
@@ -43,6 +56,19 @@ public interface FileTransfer {
|
|||||||
void download(String remotePath, String localPath)
|
void download(String remotePath, String localPath)
|
||||||
throws IOException;
|
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}.
|
* Upload {@code localFile} to {@code remotePath}.
|
||||||
*
|
*
|
||||||
@@ -54,6 +80,18 @@ public interface FileTransfer {
|
|||||||
void upload(LocalSourceFile localFile, String remotePath)
|
void upload(LocalSourceFile localFile, String remotePath)
|
||||||
throws IOException;
|
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}.
|
* Download {@code remotePath} to {@code localFile}.
|
||||||
*
|
*
|
||||||
@@ -65,6 +103,18 @@ public interface FileTransfer {
|
|||||||
void download(String remotePath, LocalDestFile localFile)
|
void download(String remotePath, LocalDestFile localFile)
|
||||||
throws IOException;
|
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();
|
TransferListener getTransferListener();
|
||||||
|
|
||||||
void setTransferListener(TransferListener listener);
|
void setTransferListener(TransferListener listener);
|
||||||
|
|||||||
@@ -20,8 +20,13 @@ import java.io.OutputStream;
|
|||||||
|
|
||||||
public interface LocalDestFile {
|
public interface LocalDestFile {
|
||||||
|
|
||||||
|
long getLength();
|
||||||
|
|
||||||
OutputStream getOutputStream()
|
OutputStream getOutputStream()
|
||||||
throws IOException;
|
throws IOException;
|
||||||
|
|
||||||
|
OutputStream getOutputStream(boolean append)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
/** @return A child file/directory of this directory with given {@code name}. */
|
/** @return A child file/directory of this directory with given {@code name}. */
|
||||||
LocalDestFile getChild(String name);
|
LocalDestFile getChild(String name);
|
||||||
|
|||||||
@@ -52,24 +52,49 @@ public class SCPFileTransfer
|
|||||||
@Override
|
@Override
|
||||||
public void upload(String localPath, String remotePath)
|
public void upload(String localPath, String remotePath)
|
||||||
throws IOException {
|
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
|
@Override
|
||||||
public void download(String remotePath, String localPath)
|
public void download(String remotePath, String localPath)
|
||||||
throws IOException {
|
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
|
@Override
|
||||||
public void download(String remotePath, LocalDestFile localFile)
|
public void download(String remotePath, LocalDestFile localFile)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
download(remotePath, localFile, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||||
|
throws IOException {
|
||||||
|
checkByteOffsetSupport(byteOffset);
|
||||||
newSCPDownloadClient().copy(remotePath, localFile);
|
newSCPDownloadClient().copy(remotePath, localFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void upload(LocalSourceFile localFile, String remotePath)
|
public void upload(LocalSourceFile localFile, String remotePath)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
upload(localFile, remotePath, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||||
|
throws IOException {
|
||||||
|
checkByteOffsetSupport(byteOffset);
|
||||||
newSCPUploadClient().copy(localFile, remotePath);
|
newSCPUploadClient().copy(localFile, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,4 +104,11 @@ public class SCPFileTransfer
|
|||||||
}
|
}
|
||||||
return this;
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package com.hierynomus.sshj.userauth.keyprovider
|
package com.hierynomus.sshj.userauth.keyprovider
|
||||||
|
|
||||||
import com.hierynomus.sshj.test.SshFixture
|
import com.hierynomus.sshj.test.SshFixture
|
||||||
|
import net.schmizz.sshj.DefaultConfig
|
||||||
import net.schmizz.sshj.SSHClient
|
import net.schmizz.sshj.SSHClient
|
||||||
import net.schmizz.sshj.userauth.keyprovider.KeyFormat
|
import net.schmizz.sshj.userauth.keyprovider.KeyFormat
|
||||||
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
|
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
|
||||||
@@ -39,7 +40,14 @@ class FileKeyProviderSpec extends Specification {
|
|||||||
@Unroll
|
@Unroll
|
||||||
def "should have #format FileKeyProvider enabled by default"() {
|
def "should have #format FileKeyProvider enabled by default"() {
|
||||||
given:
|
given:
|
||||||
SSHClient client = fixture.setupConnectedDefaultClient()
|
// `fixture` is backed by Apache SSHD server. Looks like it doesn't support rsa-sha2-512 public key signature.
|
||||||
|
// Changing the default config to prioritize the former default implementation of RSA signature.
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
and:
|
||||||
|
SSHClient client = fixture.setupClient(config)
|
||||||
|
fixture.connectClient(client)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
client.authPublickey("jeroen", keyfile)
|
client.authPublickey("jeroen", keyfile)
|
||||||
@@ -48,11 +56,11 @@ class FileKeyProviderSpec extends Specification {
|
|||||||
client.isAuthenticated()
|
client.isAuthenticated()
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
client.disconnect()
|
client?.disconnect()
|
||||||
|
|
||||||
where:
|
where:
|
||||||
format | keyfile
|
format | keyfile
|
||||||
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5"
|
KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
|
||||||
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
|
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/test/groovy/net/schmizz/sshj/ConfigImplSpec.groovy
Normal file
87
src/test/groovy/net/schmizz/sshj/ConfigImplSpec.groovy
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.key.KeyAlgorithms
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class ConfigImplSpec extends Specification {
|
||||||
|
static def ECDSA = KeyAlgorithms.ECDSASHANistp521()
|
||||||
|
static def ED25519 = KeyAlgorithms.EdDSA25519()
|
||||||
|
static def RSA_SHA_256 = KeyAlgorithms.RSASHA256()
|
||||||
|
static def RSA_SHA_512 = KeyAlgorithms.RSASHA512()
|
||||||
|
static def SSH_RSA = KeyAlgorithms.SSHRSA()
|
||||||
|
|
||||||
|
def "prioritizeSshRsaKeyAlgorithm does nothing if there is no ssh-rsa"() {
|
||||||
|
given:
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [RSA_SHA_512, ED25519]
|
||||||
|
|
||||||
|
when:
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
then:
|
||||||
|
config.keyAlgorithms == [RSA_SHA_512, ED25519]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "prioritizeSshRsaKeyAlgorithm does nothing if there is no rsa-sha2-any"() {
|
||||||
|
given:
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [ED25519, SSH_RSA, ECDSA]
|
||||||
|
|
||||||
|
when:
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
then:
|
||||||
|
config.keyAlgorithms == [ED25519, SSH_RSA, ECDSA]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "prioritizeSshRsaKeyAlgorithm does nothing if ssh-rsa already has higher priority"() {
|
||||||
|
given:
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||||
|
|
||||||
|
when:
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
then:
|
||||||
|
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "prioritizeSshRsaKeyAlgorithm prioritizes ssh-rsa if there is one rsa-sha2-any is prioritized"() {
|
||||||
|
given:
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [ED25519, RSA_SHA_512, ECDSA, SSH_RSA]
|
||||||
|
|
||||||
|
when:
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
then:
|
||||||
|
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "prioritizeSshRsaKeyAlgorithm prioritizes ssh-rsa if there are two rsa-sha2-any is prioritized"() {
|
||||||
|
given:
|
||||||
|
def config = new DefaultConfig()
|
||||||
|
config.keyAlgorithms = [ED25519, RSA_SHA_512, ECDSA, RSA_SHA_256, SSH_RSA]
|
||||||
|
|
||||||
|
when:
|
||||||
|
config.prioritizeSshRsaKeyAlgorithm()
|
||||||
|
|
||||||
|
then:
|
||||||
|
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA, RSA_SHA_256]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* 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.connection.channel.forwarded;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.test.HttpServer;
|
||||||
|
import com.hierynomus.sshj.test.SshFixture;
|
||||||
|
import com.hierynomus.sshj.test.util.FileUtil;
|
||||||
|
import net.schmizz.sshj.SSHClient;
|
||||||
|
import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder;
|
||||||
|
import net.schmizz.sshj.connection.channel.direct.Parameters;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
public class LocalPortForwarderTest {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LocalPortForwarderTest.class);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public SshFixture fixture = new SshFixture();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public HttpServer httpServer = new HttpServer();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws IOException {
|
||||||
|
fixture.getServer().setForwardingFilter(new AcceptAllForwardingFilter());
|
||||||
|
File file = httpServer.getDocRoot().newFile("index.html");
|
||||||
|
FileUtil.writeToFile(file, "<html><head/><body><h1>Hi!</h1></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldHaveWorkingHttpServer() throws IOException {
|
||||||
|
// Just to check that we have a working http server...
|
||||||
|
assertThat(httpGet("127.0.0.1", 8080), equalTo(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldHaveHttpServerThatClosesConnectionAfterResponse() throws IOException {
|
||||||
|
// Just to check that the test server does close connections before we try through the forwarder...
|
||||||
|
httpGetAndAssertConnectionClosedByServer(8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 10_000)
|
||||||
|
public void shouldCloseConnectionWhenRemoteServerClosesConnection() throws IOException {
|
||||||
|
SSHClient sshClient = getFixtureClient();
|
||||||
|
|
||||||
|
ServerSocket serverSocket = new ServerSocket();
|
||||||
|
serverSocket.setReuseAddress(true);
|
||||||
|
serverSocket.bind(new InetSocketAddress("0.0.0.0", 12345));
|
||||||
|
LocalPortForwarder localPortForwarder = sshClient.newLocalPortForwarder(new Parameters("0.0.0.0", 12345, "localhost", 8080), serverSocket);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
localPortForwarder.listen();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}, "local port listener").start();
|
||||||
|
|
||||||
|
// Test once to prove that the local HTTP connection is closed when the remote HTTP connection is closed.
|
||||||
|
httpGetAndAssertConnectionClosedByServer(12345);
|
||||||
|
|
||||||
|
// Test again to prove that the tunnel is still open, even after HTTP connection was closed.
|
||||||
|
httpGetAndAssertConnectionClosedByServer(12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void httpGetAndAssertConnectionClosedByServer(int port) throws IOException {
|
||||||
|
System.out.println("HTTP GET to port: " + port);
|
||||||
|
try (Socket socket = new Socket("localhost", port)) {
|
||||||
|
// Send a basic HTTP GET
|
||||||
|
// It returns 400 Bad Request because it's missing a bunch of info, but the HTTP response doesn't matter, we just want to test the connection closing.
|
||||||
|
OutputStream outputStream = socket.getOutputStream();
|
||||||
|
PrintWriter writer = new PrintWriter(outputStream);
|
||||||
|
writer.println("GET / HTTP/1.1");
|
||||||
|
writer.println("");
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
// Read the HTTP response
|
||||||
|
InputStream inputStream = socket.getInputStream();
|
||||||
|
InputStreamReader reader = new InputStreamReader(inputStream);
|
||||||
|
int buf = -2;
|
||||||
|
while (true) {
|
||||||
|
buf = reader.read();
|
||||||
|
System.out.print((char)buf);
|
||||||
|
if (buf == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to read more. If the server has closed the connection this will return -1
|
||||||
|
int read = inputStream.read();
|
||||||
|
|
||||||
|
// Assert input stream was closed by server.
|
||||||
|
Assert.assertEquals(-1, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int httpGet(String server, int port) throws IOException {
|
||||||
|
HttpClient client = HttpClientBuilder.create().build();
|
||||||
|
String urlString = "http://" + server + ":" + port;
|
||||||
|
log.info("Trying: GET " + urlString);
|
||||||
|
HttpResponse execute = client.execute(new HttpGet(urlString));
|
||||||
|
return execute.getStatusLine().getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSHClient getFixtureClient() throws IOException {
|
||||||
|
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||||
|
sshClient.authPassword("jeroen", "jeroen");
|
||||||
|
return sshClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,14 +37,14 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
|
|
||||||
public class RemotePortForwarderTest {
|
public class RemotePortForwarderTest {
|
||||||
private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class);
|
private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class);
|
||||||
|
|
||||||
private static final PortRange RANGE = new PortRange(9000, 9999);
|
private static final PortRange RANGE = new PortRange(9000, 9999);
|
||||||
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress("127.0.0.1", 8080);
|
private static final String LOCALHOST = "127.0.0.1";
|
||||||
|
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress(LOCALHOST, 8080);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public SshFixture fixture = new SshFixture();
|
public SshFixture fixture = new SshFixture();
|
||||||
@@ -62,61 +62,63 @@ public class RemotePortForwarderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldHaveWorkingHttpServer() throws IOException {
|
public void shouldHaveWorkingHttpServer() throws IOException {
|
||||||
// Just to check that we have a working http server...
|
// Just to check that we have a working http server...
|
||||||
assertThat(httpGet("127.0.0.1", 8080), equalTo(200));
|
assertEquals(200, httpGet( 8080));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldDynamicallyForwardPortForLocalhost() throws IOException {
|
public void shouldDynamicallyForwardPortForLocalhost() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0));
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0));
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldDynamicallyForwardPortForAllIPv4() throws IOException {
|
public void shouldDynamicallyForwardPortForAllIPv4() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", new SinglePort(0));
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", new SinglePort(0));
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldDynamicallyForwardPortForAllProtocols() throws IOException {
|
public void shouldDynamicallyForwardPortForAllProtocols() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0));
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0));
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldForwardPortForLocalhost() throws IOException {
|
public void shouldForwardPortForLocalhost() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE);
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE);
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldForwardPortForAllIPv4() throws IOException {
|
public void shouldForwardPortForAllIPv4() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE);
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE);
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldForwardPortForAllProtocols() throws IOException {
|
public void shouldForwardPortForAllProtocols() throws IOException {
|
||||||
SSHClient sshClient = getFixtureClient();
|
SSHClient sshClient = getFixtureClient();
|
||||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", RANGE);
|
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", RANGE);
|
||||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
assertHttpGetSuccess(bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHttpGetSuccess(final RemotePortForwarder.Forward bind) throws IOException {
|
||||||
|
assertEquals(200, httpGet(bind.getPort()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException {
|
private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
RemotePortForwarder.Forward forward = sshClient.getRemotePortForwarder().bind(
|
return sshClient.getRemotePortForwarder().bind(
|
||||||
// where the server should listen
|
// where the server should listen
|
||||||
new RemotePortForwarder.Forward(address, portRange.nextPort()),
|
new RemotePortForwarder.Forward(address, portRange.nextPort()),
|
||||||
// what we do with incoming connections that are forwarded to us
|
// what we do with incoming connections that are forwarded to us
|
||||||
new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR));
|
new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR));
|
||||||
|
|
||||||
return forward;
|
|
||||||
} catch (ConnectionException ce) {
|
} catch (ConnectionException ce) {
|
||||||
if (!portRange.hasNext()) {
|
if (!portRange.hasNext()) {
|
||||||
throw ce;
|
throw ce;
|
||||||
@@ -125,9 +127,9 @@ public class RemotePortForwarderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int httpGet(String server, int port) throws IOException {
|
private int httpGet(int port) throws IOException {
|
||||||
HttpClient client = HttpClientBuilder.create().build();
|
HttpClient client = HttpClientBuilder.create().build();
|
||||||
String urlString = "http://" + server + ":" + port;
|
String urlString = "http://" + LOCALHOST + ":" + port;
|
||||||
log.info("Trying: GET " + urlString);
|
log.info("Trying: GET " + urlString);
|
||||||
HttpResponse execute = client.execute(new HttpGet(urlString));
|
HttpResponse execute = client.execute(new HttpGet(urlString));
|
||||||
return execute.getStatusLine().getStatusCode();
|
return execute.getStatusLine().getStatusCode();
|
||||||
@@ -140,7 +142,7 @@ public class RemotePortForwarderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class PortRange {
|
private static class PortRange {
|
||||||
private int upper;
|
private final int upper;
|
||||||
private int current;
|
private int current;
|
||||||
|
|
||||||
public PortRange(int lower, int upper) {
|
public PortRange(int lower, int upper) {
|
||||||
|
|||||||
@@ -15,54 +15,67 @@
|
|||||||
*/
|
*/
|
||||||
package com.hierynomus.sshj.keepalive;
|
package com.hierynomus.sshj.keepalive;
|
||||||
|
|
||||||
import com.hierynomus.sshj.test.KnownFailingTests;
|
|
||||||
import com.hierynomus.sshj.test.SlowTests;
|
|
||||||
import com.hierynomus.sshj.test.SshFixture;
|
import com.hierynomus.sshj.test.SshFixture;
|
||||||
|
import net.schmizz.keepalive.KeepAlive;
|
||||||
import net.schmizz.keepalive.KeepAliveProvider;
|
import net.schmizz.keepalive.KeepAliveProvider;
|
||||||
import net.schmizz.sshj.DefaultConfig;
|
import net.schmizz.sshj.DefaultConfig;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import net.schmizz.sshj.userauth.UserAuthException;
|
import net.schmizz.sshj.userauth.UserAuthException;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.experimental.categories.Category;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.management.ManagementFactory;
|
|
||||||
import java.lang.management.ThreadInfo;
|
|
||||||
import java.lang.management.ThreadMXBean;
|
|
||||||
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class KeepAliveThreadTerminationTest {
|
public class KeepAliveThreadTerminationTest {
|
||||||
|
|
||||||
|
private static final int KEEP_ALIVE_SECONDS = 1;
|
||||||
|
|
||||||
|
private static final long STOP_SLEEP = 1500;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public SshFixture fixture = new SshFixture();
|
public SshFixture fixture = new SshFixture();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Category({SlowTests.class, KnownFailingTests.class})
|
public void shouldNotStartThreadOnSetKeepAliveInterval() {
|
||||||
public void shouldCorrectlyTerminateThreadOnDisconnect() throws IOException, InterruptedException {
|
final SSHClient sshClient = setupClient();
|
||||||
DefaultConfig defaultConfig = new DefaultConfig();
|
|
||||||
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
SSHClient sshClient = fixture.setupClient(defaultConfig);
|
|
||||||
fixture.connectClient(sshClient);
|
|
||||||
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(1);
|
|
||||||
try {
|
|
||||||
sshClient.authPassword("bad", "credentials");
|
|
||||||
fail("Should not auth.");
|
|
||||||
} catch (UserAuthException e) {
|
|
||||||
// OK
|
|
||||||
}
|
|
||||||
fixture.stopClient();
|
|
||||||
Thread.sleep(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
|
||||||
for (long l : threadMXBean.getAllThreadIds()) {
|
assertTrue(keepAlive.isDaemon());
|
||||||
ThreadInfo threadInfo = threadMXBean.getThreadInfo(l);
|
assertFalse(keepAlive.isAlive());
|
||||||
if (threadInfo.getThreadName().equals("keep-alive") && threadInfo.getThreadState() != Thread.State.TERMINATED) {
|
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||||
fail("Found alive keep-alive thread in state " + threadInfo.getThreadState());
|
}
|
||||||
}
|
|
||||||
}
|
@Test
|
||||||
|
public void shouldStartThreadOnConnectAndInterruptOnDisconnect() throws IOException, InterruptedException {
|
||||||
|
final SSHClient sshClient = setupClient();
|
||||||
|
|
||||||
|
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
|
||||||
|
assertTrue(keepAlive.isDaemon());
|
||||||
|
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||||
|
|
||||||
|
fixture.connectClient(sshClient);
|
||||||
|
|
||||||
|
assertThrows(UserAuthException.class, () -> sshClient.authPassword("bad", "credentials"));
|
||||||
|
|
||||||
|
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
|
||||||
|
|
||||||
|
fixture.stopClient();
|
||||||
|
Thread.sleep(STOP_SLEEP);
|
||||||
|
|
||||||
|
assertFalse(keepAlive.isAlive());
|
||||||
|
assertEquals(Thread.State.TERMINATED, keepAlive.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSHClient setupClient() {
|
||||||
|
final DefaultConfig defaultConfig = new DefaultConfig();
|
||||||
|
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
|
||||||
|
final SSHClient sshClient = fixture.setupClient(defaultConfig);
|
||||||
|
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(KEEP_ALIVE_SECONDS);
|
||||||
|
return sshClient;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,25 @@ package com.hierynomus.sshj.sftp;
|
|||||||
|
|
||||||
import com.hierynomus.sshj.test.SshFixture;
|
import com.hierynomus.sshj.test.SshFixture;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
|
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||||
import net.schmizz.sshj.sftp.OpenMode;
|
import net.schmizz.sshj.sftp.OpenMode;
|
||||||
import net.schmizz.sshj.sftp.RemoteFile;
|
import net.schmizz.sshj.sftp.RemoteFile;
|
||||||
import net.schmizz.sshj.sftp.SFTPEngine;
|
import net.schmizz.sshj.sftp.SFTPEngine;
|
||||||
|
import net.schmizz.sshj.sftp.SFTPException;
|
||||||
|
import org.apache.sshd.common.util.io.IoUtils;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
import java.security.SecureRandom;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class RemoteFileTest {
|
public class RemoteFileTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -84,4 +88,141 @@ public class RemoteFileTest {
|
|||||||
|
|
||||||
assertThat("The written and received data should match", data, equalTo(test2));
|
assertThat("The written and received data should match", data, equalTo(test2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotReadAheadAfterLimitInputStream() throws IOException {
|
||||||
|
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||||
|
ssh.authPassword("test", "test");
|
||||||
|
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||||
|
|
||||||
|
RemoteFile rf;
|
||||||
|
File file = temp.newFile("SftpReadAheadLimitTest.bin");
|
||||||
|
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
|
||||||
|
byte[] data = new byte[8192];
|
||||||
|
new Random(53).nextBytes(data);
|
||||||
|
data[3072] = 1;
|
||||||
|
rf.write(0, data, 0, data.length);
|
||||||
|
rf.close();
|
||||||
|
|
||||||
|
assertThat("The file should exist", file.exists());
|
||||||
|
|
||||||
|
rf = sftp.open(file.getPath());
|
||||||
|
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
|
||||||
|
|
||||||
|
byte[] test = new byte[4097];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
while (n < 2048) {
|
||||||
|
n += rs.read(test, n, 2048 - n);
|
||||||
|
}
|
||||||
|
|
||||||
|
rf.close();
|
||||||
|
|
||||||
|
while (n < 3072) {
|
||||||
|
n += rs.read(test, n, 3072 - n);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat("buffer overrun", test[3072] == 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
rs.read(test, n, test.length - n);
|
||||||
|
fail("Content must not be buffered");
|
||||||
|
} catch (SFTPException e){
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void limitedReadAheadInputStream() throws IOException {
|
||||||
|
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||||
|
ssh.authPassword("test", "test");
|
||||||
|
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||||
|
|
||||||
|
RemoteFile rf;
|
||||||
|
File file = temp.newFile("SftpReadAheadLimitedTest.bin");
|
||||||
|
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
|
||||||
|
byte[] data = new byte[8192];
|
||||||
|
new Random(53).nextBytes(data);
|
||||||
|
data[3072] = 1;
|
||||||
|
rf.write(0, data, 0, data.length);
|
||||||
|
rf.close();
|
||||||
|
|
||||||
|
assertThat("The file should exist", file.exists());
|
||||||
|
|
||||||
|
rf = sftp.open(file.getPath());
|
||||||
|
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
|
||||||
|
|
||||||
|
byte[] test = new byte[4097];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
while (n < 2048) {
|
||||||
|
n += rs.read(test, n, 2048 - n);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (n < 3072) {
|
||||||
|
n += rs.read(test, n, 3072 - n);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat("buffer overrun", test[3072] == 0);
|
||||||
|
|
||||||
|
n += rs.read(test, n, test.length - n); // --> ArrayIndexOutOfBoundsException
|
||||||
|
|
||||||
|
byte[] test2 = new byte[data.length];
|
||||||
|
System.arraycopy(test, 0, test2, 0, test.length);
|
||||||
|
|
||||||
|
while (n < data.length) {
|
||||||
|
n += rs.read(test2, n, data.length - n);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat("The written and received data should match", data, equalTo(test2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReadCorrectlyWhenWrappedInBufferedStream_FullSizeBuffer() throws IOException {
|
||||||
|
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReadCorrectlyWhenWrappedInBufferedStream_HalfSizeBuffer() throws IOException {
|
||||||
|
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 512 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReadCorrectlyWhenWrappedInBufferedStream_QuarterSizeBuffer() throws IOException {
|
||||||
|
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 256 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReadCorrectlyWhenWrappedInBufferedStream_SmallSizeBuffer() throws IOException {
|
||||||
|
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTestShouldReadCorrectlyWhenWrappedInBufferedStream(int fileSize, int bufferSize) throws IOException {
|
||||||
|
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||||
|
ssh.authPassword("test", "test");
|
||||||
|
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||||
|
|
||||||
|
final byte[] expected = new byte[fileSize];
|
||||||
|
new SecureRandom(new byte[] { 31 }).nextBytes(expected);
|
||||||
|
|
||||||
|
File file = temp.newFile("shouldReadCorrectlyWhenWrappedInBufferedStream.bin");
|
||||||
|
try (OutputStream fStream = new FileOutputStream(file)) {
|
||||||
|
IoUtils.copy(new ByteArrayInputStream(expected), fStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteFile rf = sftp.open(file.getPath());
|
||||||
|
final byte[] actual;
|
||||||
|
try (InputStream inputStream = new BufferedInputStream(
|
||||||
|
rf.new ReadAheadRemoteFileInputStream(10),
|
||||||
|
bufferSize)
|
||||||
|
) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
IoUtils.copy(inputStream, baos, expected.length);
|
||||||
|
actual = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("The file should be fully read", expected.length, actual.length);
|
||||||
|
assertThat("The file should be read correctly",
|
||||||
|
ByteArrayUtils.equals(expected, 0, actual, 0, expected.length));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,18 @@ import net.schmizz.sshj.Config;
|
|||||||
import net.schmizz.sshj.DefaultConfig;
|
import net.schmizz.sshj.DefaultConfig;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
|
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
|
||||||
import org.apache.sshd.common.NamedFactory;
|
|
||||||
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
|
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.SshServer;
|
||||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
|
||||||
import org.apache.sshd.server.command.Command;
|
|
||||||
import org.apache.sshd.server.command.CommandFactory;
|
|
||||||
import org.apache.sshd.server.scp.ScpCommandFactory;
|
|
||||||
import org.apache.sshd.server.session.ServerSession;
|
|
||||||
import org.apache.sshd.server.shell.ProcessShellFactory;
|
import org.apache.sshd.server.shell.ProcessShellFactory;
|
||||||
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
|
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||||
import org.junit.rules.ExternalResource;
|
import org.junit.rules.ExternalResource;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,10 +39,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
public class SshFixture extends ExternalResource {
|
public class SshFixture extends ExternalResource {
|
||||||
public static final String hostkey = "hostkey.pem";
|
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 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 SshServer server = defaultSshServer();
|
private final SshServer server = defaultSshServer();
|
||||||
private SSHClient client = null;
|
private SSHClient client = null;
|
||||||
private AtomicBoolean started = new AtomicBoolean(false);
|
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||||
private boolean autoStart = true;
|
private boolean autoStart = true;
|
||||||
|
|
||||||
public SshFixture(boolean autoStart) {
|
public SshFixture(boolean autoStart) {
|
||||||
@@ -108,38 +106,23 @@ public class SshFixture extends ExternalResource {
|
|||||||
sshServer.setPort(randomPort());
|
sshServer.setPort(randomPort());
|
||||||
ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey);
|
ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey);
|
||||||
sshServer.setKeyPairProvider(fileKeyPairProvider);
|
sshServer.setKeyPairProvider(fileKeyPairProvider);
|
||||||
sshServer.setPasswordAuthenticator(new PasswordAuthenticator() {
|
sshServer.setPasswordAuthenticator((username, password, session) -> username.equals(password));
|
||||||
@Override
|
|
||||||
public boolean authenticate(String username, String password, ServerSession session) {
|
|
||||||
return username.equals(password);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sshServer.setGSSAuthenticator(new BogusGSSAuthenticator());
|
sshServer.setGSSAuthenticator(new BogusGSSAuthenticator());
|
||||||
sshServer.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
|
sshServer.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
|
||||||
ScpCommandFactory commandFactory = new ScpCommandFactory();
|
ScpCommandFactory commandFactory = new ScpCommandFactory();
|
||||||
commandFactory.setDelegateCommandFactory(new CommandFactory() {
|
commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
|
||||||
@Override
|
|
||||||
public Command createCommand(String command) {
|
|
||||||
return new ProcessShellFactory(command.split(" ")).create();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sshServer.setCommandFactory(commandFactory);
|
sshServer.setCommandFactory(commandFactory);
|
||||||
sshServer.setShellFactory(new ProcessShellFactory("ls"));
|
sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
|
||||||
return sshServer;
|
return sshServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int randomPort() {
|
private int randomPort() {
|
||||||
try {
|
try {
|
||||||
ServerSocket s = null;
|
try (final ServerSocket s = new ServerSocket(0)) {
|
||||||
try {
|
|
||||||
s = new ServerSocket(0);
|
|
||||||
return s.getLocalPort();
|
return s.getLocalPort();
|
||||||
} finally {
|
|
||||||
if (s != null)
|
|
||||||
s.close();
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,8 @@ public class FileUtil {
|
|||||||
IOUtils.closeQuietly(fileInputStream);
|
IOUtils.closeQuietly(fileInputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean compareFileContents(File f1, File f2) throws IOException {
|
||||||
|
return readFromFile(f1).equals(readFromFile(f2));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ import net.schmizz.sshj.common.Factory;
|
|||||||
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
||||||
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
||||||
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
||||||
import org.apache.sshd.common.NamedFactory;
|
|
||||||
import org.apache.sshd.common.kex.BuiltinDHFactories;
|
import org.apache.sshd.common.kex.BuiltinDHFactories;
|
||||||
import org.apache.sshd.common.kex.KeyExchange;
|
import org.apache.sshd.common.kex.KeyExchangeFactory;
|
||||||
import org.apache.sshd.server.SshServer;
|
import org.apache.sshd.server.SshServer;
|
||||||
import org.apache.sshd.server.kex.DHGEXServer;
|
import org.apache.sshd.server.kex.DHGEXServer;
|
||||||
import org.apache.sshd.server.kex.DHGServer;
|
import org.apache.sshd.server.kex.DHGServer;
|
||||||
@@ -38,6 +37,7 @@ import java.util.Collections;
|
|||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class KeyExchangeTest extends BaseAlgorithmTest {
|
public class KeyExchangeTest extends BaseAlgorithmTest {
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Parameterized.Parameters(name = "algorithm={0}")
|
@Parameterized.Parameters(name = "algorithm={0}")
|
||||||
public static Collection<Object[]> getParameters() {
|
public static Collection<Object[]> getParameters() {
|
||||||
return Arrays.asList(new Object[][]{
|
return Arrays.asList(new Object[][]{
|
||||||
@@ -56,10 +56,10 @@ public class KeyExchangeTest extends BaseAlgorithmTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
|
private final Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
|
||||||
private NamedFactory<KeyExchange> serverFactory;
|
private final KeyExchangeFactory serverFactory;
|
||||||
|
|
||||||
public KeyExchangeTest(NamedFactory<KeyExchange> serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) {
|
public KeyExchangeTest(KeyExchangeFactory serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) {
|
||||||
this.clientFactory = clientFactory;
|
this.clientFactory = clientFactory;
|
||||||
this.serverFactory = serverFactory;
|
this.serverFactory = serverFactory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,8 @@ import net.schmizz.sshj.SSHClient;
|
|||||||
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
||||||
import net.schmizz.sshj.userauth.method.ChallengeResponseProvider;
|
import net.schmizz.sshj.userauth.method.ChallengeResponseProvider;
|
||||||
import net.schmizz.sshj.userauth.password.Resource;
|
import net.schmizz.sshj.userauth.password.Resource;
|
||||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||||
import org.apache.sshd.common.NamedFactory;
|
import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
|
||||||
import org.apache.sshd.server.auth.UserAuth;
|
|
||||||
import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractive;
|
|
||||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
|
||||||
import org.apache.sshd.server.session.ServerSession;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -43,28 +39,8 @@ public class AuthKeyboardInteractiveTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setKeyboardInteractiveAuthenticator() throws IOException {
|
public void setKeyboardInteractiveAuthenticator() throws IOException {
|
||||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthKeyboardInteractiveFactory()));
|
||||||
@Override
|
fixture.getServer().setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
|
||||||
public String getName() {
|
|
||||||
return UserAuthKeyboardInteractiveFactory.NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserAuth get() {
|
|
||||||
return new UserAuthKeyboardInteractive();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserAuth create() {
|
|
||||||
return get();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
|
||||||
@Override
|
|
||||||
public boolean authenticate(String username, String password, ServerSession session) {
|
|
||||||
return password.equals(username);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fixture.getServer().start();
|
fixture.getServer().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +51,7 @@ public class AuthKeyboardInteractiveTest {
|
|||||||
sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() {
|
sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() {
|
||||||
@Override
|
@Override
|
||||||
public List<String> getSubmethods() {
|
public List<String> getSubmethods() {
|
||||||
return new ArrayList<String>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -21,56 +21,29 @@ import net.schmizz.sshj.userauth.UserAuthException;
|
|||||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||||
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
|
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
|
||||||
import net.schmizz.sshj.userauth.password.Resource;
|
import net.schmizz.sshj.userauth.password.Resource;
|
||||||
import org.apache.sshd.common.NamedFactory;
|
|
||||||
import org.apache.sshd.common.util.buffer.Buffer;
|
|
||||||
import org.apache.sshd.server.auth.UserAuth;
|
|
||||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||||
import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
|
import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
|
||||||
import org.apache.sshd.server.auth.password.UserAuthPassword;
|
|
||||||
import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
|
import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
|
||||||
import org.apache.sshd.server.session.ServerSession;
|
import org.apache.sshd.server.session.ServerSession;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public class AuthPasswordTest {
|
public class AuthPasswordTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public SshFixture fixture = new SshFixture(false);
|
public SshFixture fixture = new SshFixture(false);
|
||||||
|
|
||||||
@Rule
|
|
||||||
public ExpectedException expectedException = ExpectedException.none();
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setPasswordAuthenticator() throws IOException {
|
public void setPasswordAuthenticator() throws IOException {
|
||||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthPasswordFactory()));
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return UserAuthPasswordFactory.NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserAuth get() {
|
|
||||||
return new UserAuthPassword() {
|
|
||||||
@Override
|
|
||||||
protected Boolean handleClientPasswordChangeRequest(Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) throws Exception {
|
|
||||||
return session.getPasswordAuthenticator().authenticate(username, newPassword, session);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserAuth create() {
|
|
||||||
return get();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||||
@Override
|
@Override
|
||||||
public boolean authenticate(String username, String password, ServerSession session) {
|
public boolean authenticate(String username, String password, ServerSession session) {
|
||||||
@@ -80,6 +53,12 @@ public class AuthPasswordTest {
|
|||||||
return password.equals(username);
|
return password.equals(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleClientPasswordChangeRequest(
|
||||||
|
ServerSession session, String username, String oldPassword, String newPassword) {
|
||||||
|
return username.equals(newPassword);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
fixture.getServer().start();
|
fixture.getServer().start();
|
||||||
}
|
}
|
||||||
@@ -87,8 +66,7 @@ public class AuthPasswordTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
|
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
|
||||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||||
expectedException.expect(UserAuthException.class);
|
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", "changeme"));
|
||||||
sshClient.authPassword("jeroen", "changeme");
|
|
||||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +90,7 @@ public class AuthPasswordTest {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
|
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
|
||||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||||
expectedException.expect(UserAuthException.class);
|
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", new PasswordFinder() {
|
||||||
sshClient.authPassword("jeroen", new PasswordFinder() {
|
|
||||||
@Override
|
@Override
|
||||||
public char[] reqPassword(Resource<?> resource) {
|
public char[] reqPassword(Resource<?> resource) {
|
||||||
return "changeme".toCharArray();
|
return "changeme".toCharArray();
|
||||||
@@ -123,7 +100,7 @@ public class AuthPasswordTest {
|
|||||||
public boolean shouldRetry(Resource<?> resource) {
|
public boolean shouldRetry(Resource<?> resource) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, new StaticPasswordUpdateProvider("bad"));
|
}, new StaticPasswordUpdateProvider("bad")));
|
||||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +130,7 @@ public class AuthPasswordTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
|
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
|
||||||
private Stack<String> newPasswords = new Stack<String>();
|
private final Stack<String> newPasswords = new Stack<>();
|
||||||
|
|
||||||
public StaticPasswordUpdateProvider(String... newPasswords) {
|
public StaticPasswordUpdateProvider(String... newPasswords) {
|
||||||
for (int i = newPasswords.length - 1; i >= 0; i--) {
|
for (int i = newPasswords.length - 1; i >= 0; i--) {
|
||||||
|
|||||||
@@ -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();
|
SSHClient client = fixture.setupConnectedDefaultClient();
|
||||||
client.authPassword("test", "test");
|
client.authPassword("test", "test");
|
||||||
Session s = client.startSession();
|
Session s = client.startSession();
|
||||||
Session.Command c = s.exec("ls");
|
Session.Command c = s.exec(SshFixture.listCommand);
|
||||||
IOUtils.readFully(c.getErrorStream());
|
IOUtils.readFully(c.getErrorStream());
|
||||||
IOUtils.readFully(c.getInputStream());
|
IOUtils.readFully(c.getInputStream());
|
||||||
c.close();
|
c.close();
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ public class KeyProviderUtilTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPkcs5() throws IOException {
|
public void testPkcs1Rsa() throws IOException {
|
||||||
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs5"));
|
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs1-rsa"));
|
||||||
assertEquals(KeyFormat.PKCS5, format);
|
assertEquals(KeyFormat.PKCS8, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
@@ -247,6 +248,12 @@ public class OpenSSHKeyFileTest {
|
|||||||
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", true);
|
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldLoadProtectedED25519PrivateKeyAes128CBC() throws IOException {
|
||||||
|
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes128cbc.pem", "sshjtest", false);
|
||||||
|
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes128cbc.pem", "sshjtest", true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = KeyDecryptionFailedException.class)
|
@Test(expected = KeyDecryptionFailedException.class)
|
||||||
public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException {
|
public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException {
|
||||||
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
|
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
|
||||||
@@ -437,6 +444,14 @@ public class OpenSSHKeyFileTest {
|
|||||||
corruptedKeyFile.getPublic());
|
corruptedKeyFile.getPublic());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyPrivateKey() {
|
||||||
|
FileKeyProvider keyProvider = new OpenSSHKeyV1KeyFile();
|
||||||
|
keyProvider.init(new StringReader(""));
|
||||||
|
|
||||||
|
assertThrows("This key is not in 'openssh-key-v1' format", IOException.class, keyProvider::getPrivate);
|
||||||
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void checkBCRegistration() {
|
public void checkBCRegistration() {
|
||||||
if (!SecurityUtils.isBouncyCastleRegistered()) {
|
if (!SecurityUtils.isBouncyCastleRegistered()) {
|
||||||
|
|||||||
@@ -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;
|
package net.schmizz.sshj.keyprovider;
|
||||||
|
|
||||||
|
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||||
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
|
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
|
||||||
import net.schmizz.sshj.common.KeyType;
|
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.keyprovider.PKCS8KeyFile;
|
||||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||||
|
import net.schmizz.sshj.userauth.password.Resource;
|
||||||
import net.schmizz.sshj.util.KeyUtil;
|
import net.schmizz.sshj.util.KeyUtil;
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.net.URL;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public class PKCS8KeyFileTest {
|
public class PKCS8KeyFileTest {
|
||||||
|
|
||||||
@Rule
|
|
||||||
public ExpectedException expectedException = ExpectedException.none();
|
|
||||||
|
|
||||||
static final FileKeyProvider rsa = new PKCS8KeyFile();
|
|
||||||
|
|
||||||
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
|
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
|
||||||
static final String pubExp = "23";
|
static final String pubExp = "23";
|
||||||
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
|
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
|
||||||
|
static final String KEY_PASSPHRASE = "passphrase";
|
||||||
|
static final String INCORRECT_PASSPHRASE = String.class.getSimpleName();
|
||||||
|
|
||||||
@Before
|
@Test
|
||||||
public void setUp()
|
public void testKeys() throws GeneralSecurityException, IOException {
|
||||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
if (!SecurityUtils.isBouncyCastleRegistered())
|
provider.init(new File("src/test/resources/id_rsa"));
|
||||||
throw new AssertionError("bouncy castle needed");
|
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
|
||||||
rsa.init(new File("src/test/resources/id_rsa"));
|
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
|
||||||
|
assertEquals(provider.getType(), KeyType.RSA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testKeys()
|
public void testPkcs1Rsa() throws IOException {
|
||||||
throws IOException, GeneralSecurityException {
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic());
|
provider.init(getFile("pkcs1-rsa"));
|
||||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate());
|
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||||
|
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testType()
|
public void testPkcs1Encrypted() throws IOException, GeneralSecurityException {
|
||||||
throws IOException {
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
assertEquals(rsa.getType(), KeyType.RSA);
|
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
|
@Test
|
||||||
public void testPkcs8Rsa() throws IOException {
|
public void testPkcs8Rsa() throws IOException {
|
||||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
provider.init(getReader("pkcs8-rsa-2048"));
|
provider.init(getFile("pkcs8-rsa-2048"));
|
||||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPkcs8RsaEncrypted() throws IOException {
|
public void testPkcs8RsaEncrypted() throws IOException {
|
||||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray());
|
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray());
|
||||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPkcs8RsaEncryptedIncorrectPassword() throws IOException {
|
public void testPkcs8RsaEncryptedIncorrectPassword() {
|
||||||
expectedException.expect(KeyDecryptionFailedException.class);
|
|
||||||
|
|
||||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray());
|
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(INCORRECT_PASSPHRASE.toCharArray());
|
||||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||||
provider.getPrivate();
|
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
|
@Test
|
||||||
public void testPkcs8Ecdsa() throws IOException {
|
public void testPkcs8Ecdsa() throws IOException {
|
||||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
provider.init(getReader("pkcs8-ecdsa"));
|
provider.init(getFile("pkcs8-ecdsa"));
|
||||||
assertEquals("ECDSA", provider.getPublic().getAlgorithm());
|
assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm());
|
||||||
assertEquals("ECDSA", provider.getPrivate().getAlgorithm());
|
assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPkcs8Dsa() throws IOException {
|
public void testPkcs8Dsa() throws IOException {
|
||||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||||
provider.init(getReader("pkcs8-dsa"));
|
provider.init(getFile("pkcs8-dsa"));
|
||||||
assertEquals("DSA", provider.getPublic().getAlgorithm());
|
assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm());
|
||||||
assertEquals("DSA", provider.getPrivate().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 String path = String.format("/keyformats/%s", filename);
|
||||||
final InputStream inputStream = getClass().getResourceAsStream(path);
|
final URL resource = getClass().getResource(path);
|
||||||
if (inputStream == null) {
|
if (resource == null) {
|
||||||
throw new IllegalArgumentException(String.format("Key File [%s] not found", path));
|
throw new IllegalArgumentException(String.format("Key File [%s] not found", path));
|
||||||
}
|
}
|
||||||
return new InputStreamReader(inputStream);
|
return new File(resource.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,25 @@ public class PuTTYKeyFileTest {
|
|||||||
"oYhmT2+0DKBuBVCAM4qRdA==\n" +
|
"oYhmT2+0DKBuBVCAM4qRdA==\n" +
|
||||||
"Private-MAC: 40ccc8b9a7291ec64e5be0c99badbc8a012bf220\n";
|
"Private-MAC: 40ccc8b9a7291ec64e5be0c99badbc8a012bf220\n";
|
||||||
|
|
||||||
|
final static String ppk1024_umlaut_passphrase = "PuTTY-User-Key-File-2: ssh-rsa\n" +
|
||||||
|
"Encryption: aes256-cbc\n" +
|
||||||
|
"Comment: user@host\n" +
|
||||||
|
"Public-Lines: 4\n" +
|
||||||
|
"AAAAB3NzaC1yc2EAAAADAQABAAAAgQDsQv60HaW0301hX/xV3AUcutbDDAJp7KWc\n" +
|
||||||
|
"6swL+H6jhwe3N7FK/SA4492bK5oHwU3ea3X6moLuapTMawMQbRy1kfQm99wcYc7C\n" +
|
||||||
|
"6PJO3uouzjDatc/aByDejbo5OL9kK4Vy7qm6tw1hC0JIM+TCvItKu+t6Myl7xzv4\n" +
|
||||||
|
"KbSHiMzulQ==\n" +
|
||||||
|
"Private-Lines: 8\n" +
|
||||||
|
"hPS6HYs4t8WChglZzo5G/B0ohnw2DQS19HMPllyVr9XfDyT2Xk8ZSTye84r5CtMP\n" +
|
||||||
|
"xF4Qc0nkoStyw9p9Tm762FhkM0iGghLWeCdTyqXVlAA9l3sr0BMJ9AoMvjQBqqns\n" +
|
||||||
|
"gjfPvmtNPFn8sfApHVOv1qSLSGOMZFm/q6KtGuR+IyTnMuZ71b/cQYYHbsAQxt09\n" +
|
||||||
|
"96I7jDhup/4uoi/tcPYhe998wRFSSldkAtcmYGUnDWCiivlP+gZsXvOI2zs2gCxx\n" +
|
||||||
|
"ECEwZNTR/j3G0muRUMf91iZSMBije+41j345F+ZHJ43gYXW6lxjFtI5jr9LRGWF1\n" +
|
||||||
|
"hTeY6IlLt4EBBGNrO8Rn0oGVuQdFQAZaredlt1V5FsgcSaMgg3rlScoz0IHHD66Q\n" +
|
||||||
|
"Hglp/IYN6Sx6OEGjh3oLGImag+Mz9/9WWGXPLhZ4MUpFAWqcTD4qPK0jYxTCM6QC\n" +
|
||||||
|
"TybFqMeCSEKiHSOiOGf2oQ==\n" +
|
||||||
|
"Private-MAC: 6aec23b6267edcb87b05ddef52a80894e3a246c4";
|
||||||
|
|
||||||
final static String ppkdsa_passphrase = "PuTTY-User-Key-File-2: ssh-dss\n" +
|
final static String ppkdsa_passphrase = "PuTTY-User-Key-File-2: ssh-dss\n" +
|
||||||
"Encryption: aes256-cbc\n" +
|
"Encryption: aes256-cbc\n" +
|
||||||
"Comment: dsa-key-20140507\n" +
|
"Comment: dsa-key-20140507\n" +
|
||||||
@@ -502,6 +521,15 @@ public class PuTTYKeyFileTest {
|
|||||||
assertNotNull(key.getPublic());
|
assertNotNull(key.getPublic());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCorrectPassphraseUmlautRsa() throws Exception {
|
||||||
|
PuTTYKeyFile key = new PuTTYKeyFile();
|
||||||
|
key.init(new StringReader(ppk1024_umlaut_passphrase), new UnitTestPasswordFinder("äöü"));
|
||||||
|
// Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size
|
||||||
|
assertNotNull(key.getPrivate());
|
||||||
|
assertNotNull(key.getPublic());
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
@Test(expected = IOException.class)
|
||||||
public void testWrongPassphraseRsa() throws Exception {
|
public void testWrongPassphraseRsa() throws Exception {
|
||||||
PuTTYKeyFile key = new PuTTYKeyFile();
|
PuTTYKeyFile key = new PuTTYKeyFile();
|
||||||
|
|||||||
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
|
||||||
3
src/test/resources/keytypes/ed25519_aes128cbc.pem
Normal file
3
src/test/resources/keytypes/ed25519_aes128cbc.pem
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABDfrL8SxDyrkNlsJdAmc7Z0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBiAAAAkLVqaDIfs+sPBNyy7ytdLnP/xH7Nt5FIXx3Upw6wKuMGdBzbFQLcvu60Le+SFP3uUfXE8TcHramXbH0n+UBMW6raCAKOkHUU1BtrKxPG1eKU/LBx3Bk5FxyKm7fo0XsCUmqSVK25EHOJfYq1QwIbWICkvQUNu+2Hg8/MQKoFJMentI+GqjdaG76f6Wf+aj9UwA==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user