mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830a39dc24 | ||
|
|
dcfa1833d7 | ||
|
|
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
|
||||
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
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew check
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
integration:
|
||||
name: Integration test
|
||||
|
||||
33
README.adoc
33
README.adoc
@@ -1,7 +1,7 @@
|
||||
= sshj - SSHv2 library for Java
|
||||
Jeroen van Erp
|
||||
:sshj_groupid: com.hierynomus
|
||||
:sshj_version: 0.31.0
|
||||
:sshj_version: 0.32.0
|
||||
: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"]
|
||||
@@ -104,8 +104,37 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
|
||||
Fork away!
|
||||
|
||||
== Release history
|
||||
SSHJ 0.35.0 (2023-01-30)::
|
||||
* Merged https://github.com/hierynomus/sshj/pull/835[#835]: TimeoutException message improved
|
||||
* Merged https://github.com/hierynomus/sshj/pull/815[#815]: Support authPassword on FreeBSD
|
||||
* Merged https://github.com/hierynomus/sshj/pull/813[#813]: Prevent `CHANNEL_CLOSE` between isOpen and write call.
|
||||
* Merged https://github.com/hierynomus/sshj/pull/811[#811]: Add `Transport.isKeyExchangeREquired` to prevent unnecessary KEXINIT
|
||||
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)::
|
||||
* 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/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.
|
||||
|
||||
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 {
|
||||
id "java"
|
||||
id "groovy"
|
||||
@@ -39,11 +35,11 @@ project.version = scmVersion.version
|
||||
|
||||
configurations.implementation.transitive = false
|
||||
|
||||
def bouncycastleVersion = "1.69"
|
||||
def sshdVersion = "2.1.0"
|
||||
def bouncycastleVersion = "1.70"
|
||||
def sshdVersion = "2.8.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:1.7.32"
|
||||
implementation "org.slf4j:slf4j-api:1.7.36"
|
||||
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
|
||||
implementation "com.jcraft:jzlib:1.1.3"
|
||||
@@ -51,16 +47,16 @@ dependencies {
|
||||
|
||||
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.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-sftp:$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.apache.httpcomponents:httpclient:4.5.9'
|
||||
|
||||
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||
testImplementation 'org.testcontainers:testcontainers:1.16.2'
|
||||
}
|
||||
|
||||
license {
|
||||
@@ -69,7 +65,12 @@ license {
|
||||
mapping {
|
||||
java = 'SLASHSTAR_STYLE'
|
||||
}
|
||||
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java'])
|
||||
excludes([
|
||||
'**/djb/Curve25519.java',
|
||||
'**/sshj/common/Base64.java',
|
||||
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
|
||||
'**/files/test_file_*.txt',
|
||||
])
|
||||
}
|
||||
|
||||
if (!JavaVersion.current().isJava9Compatible()) {
|
||||
@@ -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) {
|
||||
buildFile = project.buildFile
|
||||
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.finalizedBy(project.tasks.forkedUploadRelease)
|
||||
project.tasks.jacocoTestReport.dependsOn(project.tasks.test)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj-examples</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.19.1</version>
|
||||
<version>0.33.0</version>
|
||||
|
||||
<name>sshj-examples</name>
|
||||
<description>Examples for SSHv2 library for Java</description>
|
||||
@@ -55,7 +55,7 @@
|
||||
<dependency>
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj</artifactId>
|
||||
<version>0.31.0</version>
|
||||
<version>0.33.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ public class KeepAlive {
|
||||
final SSHClient ssh = new SSHClient(defaultConfig);
|
||||
try {
|
||||
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
||||
// Set interval to enable keep-alive before connecting
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
ssh.connect(args[0]);
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5); //every 60sec
|
||||
ssh.authPassword(args[1], args[2]);
|
||||
Session session = ssh.startSession();
|
||||
session.allocateDefaultPTY();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class RemotePF {
|
||||
client.loadKnownHosts();
|
||||
|
||||
client.connect("localhost");
|
||||
client.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
try {
|
||||
|
||||
client.authPublickey(System.getProperty("user.name"));
|
||||
@@ -33,8 +34,6 @@ public class RemotePF {
|
||||
// what we do with incoming connections that are forwarded to us
|
||||
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
|
||||
|
||||
client.getTransport().setHeartbeatInterval(30);
|
||||
|
||||
// Something to hang on to so that the forwarding stays
|
||||
client.getTransport().join();
|
||||
|
||||
|
||||
@@ -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 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local
|
||||
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 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
|
||||
|
||||
@@ -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.transport.TransportException
|
||||
import net.schmizz.sshj.userauth.UserAuthException
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class IntegrationSpec extends IntegrationBaseSpec {
|
||||
class IntegrationSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
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
|
||||
|
||||
when:
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||
|
||||
then:
|
||||
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")
|
||||
|
||||
when:
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||
|
||||
then:
|
||||
thrown(TransportException.class)
|
||||
@@ -59,11 +65,11 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
@Unroll
|
||||
def "should authenticate with key #key"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
|
||||
when:
|
||||
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:
|
||||
client.isAuthenticated()
|
||||
@@ -74,6 +80,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
"id_ecdsa_opensshv1" | null
|
||||
"id_ed25519_opensshv1" | null
|
||||
"id_ed25519_opensshv1_aes256cbc.pem" | "foobar"
|
||||
"id_ed25519_opensshv1_aes128cbc.pem" | "sshjtest"
|
||||
"id_ed25519_opensshv1_protected" | "sshjtest"
|
||||
"id_rsa" | null
|
||||
"id_rsa_opensshv1" | null
|
||||
@@ -83,7 +90,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
|
||||
def "should not authenticate with wrong key"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
|
||||
when:
|
||||
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")
|
||||
|
||||
@@ -13,14 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hierynomus.sshj.backport;
|
||||
|
||||
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;
|
||||
}
|
||||
package com.hierynomus.sshj
|
||||
|
||||
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
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import com.hierynomus.sshj.IntegrationTestUtil
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.sftp.OpenMode
|
||||
import net.schmizz.sshj.sftp.RemoteFile
|
||||
import net.schmizz.sshj.sftp.SFTPClient
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
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)"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
||||
SFTPClient sftp = client.newSFTPClient()
|
||||
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
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
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 java.nio.file.Files
|
||||
import java.util.stream.Collectors
|
||||
|
||||
/**
|
||||
* 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 KeyWithCertificateSpec extends IntegrationBaseSpec {
|
||||
class PublicKeyAuthWithCertificateSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "authorising with a signed public key #keyName"() {
|
||||
given:
|
||||
def client = getConnectedClient()
|
||||
SSHClient client = new SSHClient(new DefaultConfig())
|
||||
client.addHostKeyVerifier(new PromiscuousVerifier())
|
||||
client.connect("127.0.0.1", sshd.firstMappedPort)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/certificates/$keyName")
|
||||
client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/$keyName")
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
@@ -73,43 +78,4 @@ class KeyWithCertificateSpec extends IntegrationBaseSpec {
|
||||
"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
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import com.hierynomus.sshj.IntegrationTestUtil
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class RsaSignatureClientKeySpec extends IntegrationBaseSpec {
|
||||
class RsaSignatureClientKeySpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect using publickey auth with RSA key with signature"() {
|
||||
given:
|
||||
def client = getConnectedClient(new DefaultConfig())
|
||||
def client = sshd.getConnectedClient()
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,22 +15,29 @@
|
||||
*/
|
||||
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 net.schmizz.sshj.DefaultConfig
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class SignatureSpec extends IntegrationBaseSpec {
|
||||
class SignatureSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect with #sig Signature"() {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setKeyAlgorithms(Collections.singletonList(sigFactory))
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,21 +15,28 @@
|
||||
*/
|
||||
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 org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class CipherSpec extends IntegrationBaseSpec {
|
||||
class CipherSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect with #cipher Cipher"() {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setCipherFactories(cipherFactory)
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,29 +15,32 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.kex
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import com.hierynomus.sshj.transport.mac.Macs
|
||||
import com.hierynomus.sshj.IntegrationTestUtil
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
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.DH
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA1
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA256
|
||||
import net.schmizz.sshj.transport.kex.ECDH
|
||||
import net.schmizz.sshj.transport.kex.ECDHNistP
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class KexSpec extends IntegrationBaseSpec {
|
||||
class KexSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect with #kex Key Exchange"() {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setKeyExchangeFactories(kexFactory)
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,21 +15,28 @@
|
||||
*/
|
||||
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 org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
class MacSpec extends IntegrationBaseSpec {
|
||||
class MacSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect with #mac MAC"() {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setMACFactories(macFactory)
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
@@ -47,10 +54,10 @@ class MacSpec extends IntegrationBaseSpec {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setMACFactories(macFactory)
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
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 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 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); }
|
||||
@@ -61,6 +59,10 @@ public class KeyAlgorithms {
|
||||
return algorithmName;
|
||||
}
|
||||
|
||||
public KeyType getKeyType() {
|
||||
return keyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAlgorithm create() {
|
||||
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);
|
||||
|
||||
@@ -177,11 +177,11 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
Arrays.fill(charBuffer.array(), '\u0000');
|
||||
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);
|
||||
Arrays.fill(passphrase, (byte) 0);
|
||||
byte[] key = Arrays.copyOfRange(keyiv, 0, 32);
|
||||
byte[] iv = Arrays.copyOfRange(keyiv, 32, 48);
|
||||
byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize());
|
||||
byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize());
|
||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||
} else {
|
||||
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
|
||||
@@ -193,6 +193,8 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
return BlockCiphers.AES256CTR().create();
|
||||
} else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) {
|
||||
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");
|
||||
}
|
||||
@@ -216,6 +218,9 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
while (line != null && !line.startsWith(BEGIN)) {
|
||||
line = reader.readLine();
|
||||
}
|
||||
if (line == null) {
|
||||
return false;
|
||||
}
|
||||
line = line.substring(BEGIN.length());
|
||||
return line.startsWith(OPENSSH_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ public class Promise<V, T extends Throwable> {
|
||||
throws T {
|
||||
final V value = tryRetrieve(timeout, unit);
|
||||
if (value == null)
|
||||
throw chainer.chain(new TimeoutException("Timeout expired"));
|
||||
throw chainer.chain(new TimeoutException("Timeout expired: " + timeout + " " + unit));
|
||||
else
|
||||
return value;
|
||||
}
|
||||
@@ -176,6 +176,7 @@ public class Promise<V, T extends Throwable> {
|
||||
}
|
||||
return val;
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw chainer.chain(ie);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
||||
@@ -24,7 +24,7 @@ final class Heartbeater
|
||||
extends KeepAlive {
|
||||
|
||||
Heartbeater(ConnectionImpl conn) {
|
||||
super(conn, "heartbeater");
|
||||
super(conn, "sshj-Heartbeater");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,8 @@ import net.schmizz.sshj.connection.ConnectionImpl;
|
||||
import net.schmizz.sshj.transport.TransportException;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class KeepAlive extends Thread {
|
||||
protected final Logger log;
|
||||
protected final ConnectionImpl conn;
|
||||
@@ -33,40 +35,49 @@ public abstract class KeepAlive extends Thread {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeepAlive enabled based on KeepAlive interval
|
||||
*
|
||||
* @return Enabled when KeepInterval is greater than 0
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return keepAliveInterval > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KeepAlive interval in seconds
|
||||
*
|
||||
* @return KeepAlive interval in seconds defaults to 0
|
||||
*/
|
||||
public synchronized int getKeepAliveInterval() {
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set KeepAlive interval in seconds
|
||||
*
|
||||
* @param keepAliveInterval KeepAlive interval in seconds
|
||||
*/
|
||||
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
|
||||
this.keepAliveInterval = keepAliveInterval;
|
||||
if (keepAliveInterval > 0 && getState() == State.NEW) {
|
||||
start();
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
synchronized protected int getPositiveInterval()
|
||||
throws InterruptedException {
|
||||
while (keepAliveInterval <= 0) {
|
||||
wait();
|
||||
}
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.debug("Starting {}, sending keep-alive every {} seconds", getClass().getSimpleName(), keepAliveInterval);
|
||||
log.debug("{} Started with interval [{} seconds]", getClass().getSimpleName(), keepAliveInterval);
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
final int hi = getPositiveInterval();
|
||||
final int interval = getKeepAliveInterval();
|
||||
if (conn.getTransport().isRunning()) {
|
||||
log.debug("Sending keep-alive since {} seconds elapsed", hi);
|
||||
log.debug("{} Sending after interval [{} seconds]", getClass().getSimpleName(), interval);
|
||||
doKeepAlive();
|
||||
}
|
||||
Thread.sleep(hi * 1000);
|
||||
TimeUnit.SECONDS.sleep(interval);
|
||||
}
|
||||
} 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) {
|
||||
// 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.
|
||||
@@ -74,9 +85,7 @@ public abstract class KeepAlive extends Thread {
|
||||
conn.getTransport().die(e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Stopping {}", getClass().getSimpleName());
|
||||
|
||||
log.debug("{} Stopped", getClass().getSimpleName());
|
||||
}
|
||||
|
||||
protected abstract void doKeepAlive() throws TransportException, ConnectionException;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class KeepAliveRunner extends KeepAlive {
|
||||
new LinkedList<Promise<SSHPacket, ConnectionException>>();
|
||||
|
||||
KeepAliveRunner(ConnectionImpl conn) {
|
||||
super(conn, "keep-alive");
|
||||
super(conn, "sshj-KeepAliveRunner");
|
||||
}
|
||||
|
||||
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.userauth.keyprovider.FileKeyProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -188,4 +189,30 @@ public class ConfigImpl
|
||||
public void setVerifyHostKeyCertificates(boolean 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.DHGexSHA256;
|
||||
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
||||
import net.schmizz.sshj.transport.random.BouncyCastleRandom;
|
||||
import net.schmizz.sshj.transport.random.JCERandom;
|
||||
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
||||
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
|
||||
import org.slf4j.Logger;
|
||||
@@ -162,7 +160,6 @@ public class DefaultConfig
|
||||
setFileKeyProviderFactories(
|
||||
new OpenSSHKeyV1KeyFile.Factory(),
|
||||
new PKCS8KeyFile.Factory(),
|
||||
new PKCS5KeyFile.Factory(),
|
||||
new OpenSSHKeyFile.Factory(),
|
||||
new PuTTYKeyFile.Factory());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj;
|
||||
|
||||
import net.schmizz.keepalive.KeepAlive;
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import net.schmizz.sshj.common.*;
|
||||
import net.schmizz.sshj.connection.Connection;
|
||||
import net.schmizz.sshj.connection.ConnectionException;
|
||||
@@ -55,6 +57,7 @@ import javax.security.auth.login.LoginContext;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.KeyPair;
|
||||
@@ -424,6 +427,7 @@ public class SSHClient
|
||||
@Override
|
||||
public void disconnect()
|
||||
throws IOException {
|
||||
conn.getKeepAlive().interrupt();
|
||||
for (LocalPortForwarder forwarder : forwarders) {
|
||||
try {
|
||||
forwarder.close();
|
||||
@@ -441,6 +445,16 @@ public class SSHClient
|
||||
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).
|
||||
*
|
||||
@@ -537,7 +551,7 @@ public class SSHClient
|
||||
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
|
||||
* <ul>
|
||||
* <li>PKCS8 (OpenSSH uses this format)</li>
|
||||
* <li>PKCS5</li>
|
||||
* <li>PEM-encoded PKCS1</li>
|
||||
* <li>Putty keyfile</li>
|
||||
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
|
||||
* </ul>
|
||||
@@ -791,7 +805,17 @@ public class SSHClient
|
||||
throws IOException {
|
||||
super.onConnect();
|
||||
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;
|
||||
|
||||
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.direct.DirectConnection;
|
||||
|
||||
@@ -26,7 +24,6 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.Socket;
|
||||
|
||||
public abstract class SocketClient {
|
||||
@@ -57,73 +54,6 @@ public abstract class SocketClient {
|
||||
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 {
|
||||
connect(hostname, defaultPort);
|
||||
}
|
||||
|
||||
@@ -145,8 +145,14 @@ public class StreamCopier {
|
||||
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)));
|
||||
|
||||
if (length != -1 && read == -1)
|
||||
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
|
||||
// Did we encounter EOF?
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -304,6 +304,25 @@ public abstract class AbstractChannel
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent CHANNEL_CLOSE to be sent between isOpen and a Transport.write call in the runnable, otherwise
|
||||
// a disconnect with a "packet referred to nonexistent channel" message can occur.
|
||||
//
|
||||
// This particularly happens when the transport.Reader thread passes an eof from the server to the
|
||||
// ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the
|
||||
// transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.
|
||||
boolean whileOpen(TransportRunnable runnable) throws TransportException, ConnectionException {
|
||||
openCloseLock.lock();
|
||||
try {
|
||||
if (isOpen()) {
|
||||
runnable.run();
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
openCloseLock.unlock();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void gotChannelRequest(SSHPacket buf)
|
||||
throws ConnectionException, TransportException {
|
||||
final String reqType;
|
||||
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
|
||||
+ rwin + " >";
|
||||
}
|
||||
|
||||
public interface TransportRunnable {
|
||||
void run() throws TransportException, ConnectionException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public final class ChannelInputStream
|
||||
try {
|
||||
buf.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw (IOException) new InterruptedIOException().initCause(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
*/
|
||||
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
|
||||
|
||||
private final Channel chan;
|
||||
private final AbstractChannel chan;
|
||||
private final Transport trans;
|
||||
private final Window.Remote win;
|
||||
|
||||
@@ -47,6 +47,12 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
|
||||
private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
|
||||
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
|
||||
private final AbstractChannel.TransportRunnable packetWriteRunnable = new AbstractChannel.TransportRunnable() {
|
||||
@Override
|
||||
public void run() throws TransportException {
|
||||
trans.write(packet);
|
||||
}
|
||||
};
|
||||
|
||||
DataBuffer() {
|
||||
headerOffset = packet.rpos();
|
||||
@@ -99,8 +105,9 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
if (leftOverBytes > 0) {
|
||||
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
|
||||
}
|
||||
|
||||
trans.write(packet);
|
||||
if (!chan.whileOpen(packetWriteRunnable)) {
|
||||
throwStreamClosed();
|
||||
}
|
||||
win.consume(writeNow);
|
||||
|
||||
packet.rpos(headerOffset);
|
||||
@@ -119,7 +126,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
|
||||
}
|
||||
|
||||
public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) {
|
||||
public ChannelOutputStream(AbstractChannel chan, Transport trans, Window.Remote win) {
|
||||
this.chan = chan;
|
||||
this.trans = trans;
|
||||
this.win = win;
|
||||
@@ -157,7 +164,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
if (error != null) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new ConnectionException("Stream closed");
|
||||
throwStreamClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,9 +172,14 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
// Not closed yet, and underlying channel is open to flush the data to.
|
||||
if (!closed.getAndSet(true) && chan.isOpen()) {
|
||||
buffer.flush(false);
|
||||
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
||||
if (!closed.getAndSet(true)) {
|
||||
chan.whileOpen(new AbstractChannel.TransportRunnable() {
|
||||
@Override
|
||||
public void run() throws TransportException, ConnectionException {
|
||||
buffer.flush(false);
|
||||
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,4 +200,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
return "< ChannelOutputStream for Channel #" + chan.getID() + " >";
|
||||
}
|
||||
|
||||
private static void throwStreamClosed() throws ConnectionException {
|
||||
throw new ConnectionException("Stream closed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ public abstract class Window {
|
||||
throw new ConnectionException("Timeout when trying to expand the window size");
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ConnectionException(ie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
*/
|
||||
package net.schmizz.sshj.connection.channel.direct;
|
||||
|
||||
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||
import net.schmizz.sshj.common.SSHException;
|
||||
|
||||
/** 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)
|
||||
@@ -27,7 +28,7 @@ public interface SessionFactory {
|
||||
*
|
||||
* @return the opened {@code session} channel
|
||||
*
|
||||
* @throws SSHException
|
||||
* @throws SSHException Thrown on session initialization failures
|
||||
* @see Session
|
||||
*/
|
||||
Session startSession()
|
||||
|
||||
@@ -142,6 +142,10 @@ public class RemotePortForwarder
|
||||
// Listen on all IPv4
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public class PacketReader extends Thread {
|
||||
this.engine = engine;
|
||||
log = engine.getLoggerFactory().getLogger(getClass());
|
||||
this.in = engine.getSubsystem().getInputStream();
|
||||
setName("sftp reader");
|
||||
setName("sshj-PacketReader");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -220,49 +222,104 @@ public class RemoteFile
|
||||
|
||||
public class ReadAheadRemoteFileInputStream
|
||||
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 int maxUnconfirmedReads;
|
||||
private final Queue<Promise<Response, SFTPException>> unconfirmedReads = new LinkedList<Promise<Response, SFTPException>>();
|
||||
private final Queue<Long> unconfirmedReadOffsets = new LinkedList<Long>();
|
||||
private final long readAheadLimit;
|
||||
private final Deque<UnconfirmedRead> unconfirmedReads = new ArrayDeque<>();
|
||||
|
||||
private long requestOffset;
|
||||
private long responseOffset;
|
||||
private long currentOffset;
|
||||
private int maxReadLength = Integer.MAX_VALUE;
|
||||
private boolean eof;
|
||||
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
||||
assert 0 <= maxUnconfirmedReads;
|
||||
|
||||
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
||||
this(maxUnconfirmedReads, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
|
||||
* @param fileOffset Initial offset in file to read from
|
||||
*/
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
|
||||
this(maxUnconfirmedReads, fileOffset, -1L);
|
||||
}
|
||||
|
||||
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 <= fileOffset;
|
||||
|
||||
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 boolean retrieveUnconfirmedRead(boolean blocking) throws IOException {
|
||||
if (unconfirmedReads.size() <= 0) {
|
||||
final UnconfirmedRead unconfirmedRead = unconfirmedReads.peek();
|
||||
if (unconfirmedRead == null || !blocking && !unconfirmedRead.getPromise().isDelivered()) {
|
||||
return false;
|
||||
}
|
||||
unconfirmedReads.remove(unconfirmedRead);
|
||||
|
||||
if (!blocking && !unconfirmedReads.peek().isDelivered()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unconfirmedReadOffsets.remove();
|
||||
final Response res = unconfirmedReads.remove().retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||
final Response res = unconfirmedRead.promise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||
switch (res.getType()) {
|
||||
case DATA:
|
||||
int recvLen = res.readUInt32AsInt();
|
||||
responseOffset += recvLen;
|
||||
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
|
||||
if (unconfirmedRead.offset == currentOffset) {
|
||||
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;
|
||||
|
||||
case STATUS:
|
||||
@@ -290,40 +347,31 @@ public class RemoteFile
|
||||
// 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
|
||||
|
||||
long requestOffset;
|
||||
if (unconfirmedReads.isEmpty()) {
|
||||
requestOffset = currentOffset;
|
||||
}
|
||||
else {
|
||||
final UnconfirmedRead lastRequest = unconfirmedReads.getLast();
|
||||
requestOffset = lastRequest.offset + lastRequest.length;
|
||||
}
|
||||
while (unconfirmedReads.size() <= maxUnconfirmedReads) {
|
||||
// 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!
|
||||
unconfirmedReads.add(RemoteFile.this.asyncRead(requestOffset, reqLen));
|
||||
unconfirmedReadOffsets.add(requestOffset);
|
||||
int reqLen = Math.min(Math.max(1024, len), maxReadLength);
|
||||
if (readAheadLimit > requestOffset) {
|
||||
long remaining = readAheadLimit - requestOffset;
|
||||
if (reqLen > remaining) {
|
||||
reqLen = (int) remaining;
|
||||
}
|
||||
}
|
||||
unconfirmedReads.add(new UnconfirmedRead(requestOffset, reqLen));
|
||||
requestOffset += reqLen;
|
||||
if (requestOffset >= readAheadLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
long nextOffset = unconfirmedReadOffsets.peek();
|
||||
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*/)) {
|
||||
if (!retrieveUnconfirmedRead(true /*blocking*/)) {
|
||||
|
||||
// this may happen if we change prefetch strategy
|
||||
// currently, we should never get here...
|
||||
|
||||
@@ -232,21 +232,41 @@ public class SFTPClient
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(String source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest)
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.sftp;
|
||||
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import net.schmizz.concurrent.Promise;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.LoggerFactory;
|
||||
@@ -68,6 +69,7 @@ public class SFTPEngine
|
||||
sub = session.startSubsystem("sftp");
|
||||
out = sub.getOutputStream();
|
||||
reader = new PacketReader(this);
|
||||
ThreadNameProvider.setThreadName(reader, ssh);
|
||||
pathHelper = new PathHelper(new PathHelper.Canonicalizer() {
|
||||
@Override
|
||||
public String canonicalize(String path)
|
||||
@@ -235,14 +237,17 @@ public class SFTPEngine
|
||||
if (operativeVersion < 1)
|
||||
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
|
||||
|
||||
long renameFlagMask = 0L;
|
||||
for (RenameFlags flag : flags) {
|
||||
renameFlagMask = renameFlagMask | flag.longValue();
|
||||
final Request request = newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset());
|
||||
// SFTP Version 5 introduced rename flags according to Section 6.5 of the specification
|
||||
if (operativeVersion >= 5) {
|
||||
long renameFlagMask = 0L;
|
||||
for (RenameFlags flag : flags) {
|
||||
renameFlagMask = renameFlagMask | flag.longValue();
|
||||
}
|
||||
request.putUInt32(renameFlagMask);
|
||||
}
|
||||
|
||||
doRequest(
|
||||
newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset()).putUInt32(renameFlagMask)
|
||||
).ensureStatusPacketIsOK();
|
||||
doRequest(request).ensureStatusPacketIsOK();
|
||||
}
|
||||
|
||||
public String canonicalize(String path)
|
||||
|
||||
@@ -50,25 +50,47 @@ public class SFTPFileTransfer
|
||||
@Override
|
||||
public void upload(String source, String dest)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest);
|
||||
upload(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest));
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener());
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest) throws IOException {
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest, long byteOffset) throws IOException {
|
||||
final PathComponents pathComponents = engine.getPathHelper().getComponents(source);
|
||||
final FileAttributes attributes = engine.stat(source);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest, byteOffset);
|
||||
}
|
||||
|
||||
public void setUploadFilter(LocalFileFilter uploadFilter) {
|
||||
@@ -92,7 +114,8 @@ public class SFTPFileTransfer
|
||||
@SuppressWarnings("PMD.MissingBreakInSwitch")
|
||||
private void download(final TransferListener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local) throws IOException {
|
||||
final LocalDestFile local,
|
||||
final long byteOffset) throws IOException {
|
||||
final LocalDestFile adjustedFile;
|
||||
switch (remote.getAttributes().getType()) {
|
||||
case DIRECTORY:
|
||||
@@ -101,8 +124,9 @@ public class SFTPFileTransfer
|
||||
case UNKNOWN:
|
||||
log.warn("Server did not supply information about the type of file at `{}` " +
|
||||
"-- assuming it is a regular file!", remote.getPath());
|
||||
// fall through
|
||||
case REGULAR:
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local);
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local, byteOffset);
|
||||
break;
|
||||
default:
|
||||
throw new IOException(remote + " is not a regular file or directory");
|
||||
@@ -119,7 +143,7 @@ public class SFTPFileTransfer
|
||||
final RemoteDirectory rd = engine.openDir(remote.getPath());
|
||||
try {
|
||||
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
|
||||
download(listener, rri, adjusted.getChild(rri.getName()));
|
||||
download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files
|
||||
} finally {
|
||||
rd.close();
|
||||
}
|
||||
@@ -128,13 +152,15 @@ public class SFTPFileTransfer
|
||||
|
||||
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local)
|
||||
final LocalDestFile local,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
|
||||
final RemoteFile rf = engine.open(remote.getPath());
|
||||
try {
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16);
|
||||
final OutputStream os = adjusted.getOutputStream();
|
||||
log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
|
||||
final OutputStream os = adjusted.getOutputStream(byteOffset != 0);
|
||||
try {
|
||||
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
||||
@@ -173,17 +199,17 @@ public class SFTPFileTransfer
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
private void upload(final TransferListener listener) throws IOException {
|
||||
private void upload(final TransferListener listener, long byteOffset) throws IOException {
|
||||
if (source.isDirectory()) {
|
||||
makeDirIfNotExists(remote); // Ensure that the directory exists
|
||||
uploadDir(listener.directory(source.getName()), source, remote);
|
||||
setAttributes(source, remote);
|
||||
} else if (source.isFile() && isDirectory(remote)) {
|
||||
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote, byteOffset);
|
||||
setAttributes(source, adjustedRemote);
|
||||
} else if (source.isFile()) {
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote, byteOffset);
|
||||
setAttributes(source, remote);
|
||||
} else {
|
||||
throw new IOException(source + " is not a file or directory");
|
||||
@@ -192,13 +218,14 @@ public class SFTPFileTransfer
|
||||
|
||||
private void upload(final TransferListener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjustedPath;
|
||||
if (local.isDirectory()) {
|
||||
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
|
||||
} else if (local.isFile()) {
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote);
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote, byteOffset);
|
||||
} else {
|
||||
throw new IOException(local + " is not a file or directory");
|
||||
}
|
||||
@@ -217,22 +244,34 @@ public class SFTPFileTransfer
|
||||
throws IOException {
|
||||
makeDirIfNotExists(remote);
|
||||
for (LocalSourceFile f : local.getChildren(getUploadFilter()))
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()));
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()), 0); // not supporting individual byte offsets for these files
|
||||
return remote;
|
||||
}
|
||||
|
||||
private String uploadFile(final StreamCopier.Listener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjusted = prepareFile(local, remote);
|
||||
final String adjusted = prepareFile(local, remote, byteOffset);
|
||||
RemoteFile rf = null;
|
||||
InputStream fis = null;
|
||||
RemoteFile.RemoteFileOutputStream rfos = null;
|
||||
EnumSet<OpenMode> modes;
|
||||
try {
|
||||
rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC));
|
||||
if (byteOffset == 0) {
|
||||
// Starting at the beginning, overwrite/create
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC);
|
||||
} else {
|
||||
// Starting at some offset, append
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND);
|
||||
}
|
||||
|
||||
log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset);
|
||||
rf = engine.open(adjusted, modes);
|
||||
fis = local.getInputStream();
|
||||
rfos = rf.new RemoteFileOutputStream(0, 16);
|
||||
fis.skip(byteOffset);
|
||||
rfos = rf.new RemoteFileOutputStream(byteOffset, 16);
|
||||
new StreamCopier(fis, rfos, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
|
||||
.keepFlushing(false)
|
||||
@@ -294,7 +333,7 @@ public class SFTPFileTransfer
|
||||
}
|
||||
}
|
||||
|
||||
private String prepareFile(final LocalSourceFile local, final String remote)
|
||||
private String prepareFile(final LocalSourceFile local, final String remote, final long byteOffset)
|
||||
throws IOException {
|
||||
final FileAttributes attrs;
|
||||
try {
|
||||
@@ -309,7 +348,7 @@ public class SFTPFileTransfer
|
||||
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
|
||||
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
|
||||
} else {
|
||||
log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType());
|
||||
log.debug("probeFile: {} is a {} file that will be {}", remote, attrs.getMode().getType(), byteOffset > 0 ? "resumed" : "replaced");
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,6 @@ final class KeyExchanger
|
||||
negotiatedAlgs.getKeyExchangeAlgorithm());
|
||||
transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(),
|
||||
negotiatedAlgs.getSignatureAlgorithm()));
|
||||
transport.setRSASHA2Support(negotiatedAlgs.getRSASHA2Support());
|
||||
|
||||
try {
|
||||
kex.init(transport,
|
||||
|
||||
@@ -26,10 +26,8 @@ public final class NegotiatedAlgorithms {
|
||||
private final String c2sComp;
|
||||
private final String s2cComp;
|
||||
|
||||
private final boolean rsaSHA2Support;
|
||||
|
||||
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.sig = sig;
|
||||
this.c2sCipher = c2sCipher;
|
||||
@@ -38,7 +36,6 @@ public final class NegotiatedAlgorithms {
|
||||
this.s2cMAC = s2cMAC;
|
||||
this.c2sComp = c2sComp;
|
||||
this.s2cComp = s2cComp;
|
||||
this.rsaSHA2Support = rsaSHA2Support;
|
||||
}
|
||||
|
||||
public String getKeyExchangeAlgorithm() {
|
||||
@@ -73,10 +70,6 @@ public final class NegotiatedAlgorithms {
|
||||
return s2cComp;
|
||||
}
|
||||
|
||||
public boolean getRSASHA2Support() {
|
||||
return rsaSHA2Support;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("[ " +
|
||||
@@ -88,7 +81,6 @@ public final class NegotiatedAlgorithms {
|
||||
"s2cMAC=" + s2cMAC + "; " +
|
||||
"c2sComp=" + c2sComp + "; " +
|
||||
"s2cComp=" + s2cComp + "; " +
|
||||
"rsaSHA2Support=" + rsaSHA2Support +
|
||||
" ]");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
@@ -140,8 +139,8 @@ class Proposal {
|
||||
firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(),
|
||||
other.getClient2ServerCompressionAlgorithms()),
|
||||
firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(),
|
||||
other.getServer2ClientCompressionAlgorithms()),
|
||||
other.getHostKeyAlgorithms().containsAll(KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS));
|
||||
other.getServer2ClientCompressionAlgorithms())
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) {
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class Reader
|
||||
public Reader(TransportImpl trans) {
|
||||
this.trans = trans;
|
||||
log = trans.getConfig().getLoggerFactory().getLogger(getClass());
|
||||
setName("reader");
|
||||
setName("sshj-Reader");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.Service;
|
||||
@@ -27,11 +28,12 @@ import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Transport layer of the SSH protocol. */
|
||||
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
|
||||
@@ -69,6 +71,13 @@ public interface Transport
|
||||
void doKex()
|
||||
throws TransportException;
|
||||
|
||||
/**
|
||||
* Is Key Exchange required based on current transport status
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
boolean isKeyExchangeRequired();
|
||||
|
||||
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
|
||||
String getClientVersion();
|
||||
|
||||
@@ -208,7 +217,7 @@ public interface Transport
|
||||
/**
|
||||
* Specify a {@code listener} that will be notified upon disconnection.
|
||||
*
|
||||
* @param listener
|
||||
* @param listener Disconnect Listener to be configured
|
||||
*/
|
||||
void setDisconnectListener(DisconnectListener listener);
|
||||
|
||||
@@ -223,5 +232,5 @@ public interface Transport
|
||||
void die(Exception e);
|
||||
|
||||
KeyAlgorithm getHostKeyAlgorithm();
|
||||
KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException;
|
||||
List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||
import com.hierynomus.sshj.transport.IdentificationStringParser;
|
||||
@@ -22,7 +23,6 @@ import net.schmizz.concurrent.ErrorDeliveryUtil;
|
||||
import net.schmizz.concurrent.Event;
|
||||
import net.schmizz.sshj.AbstractService;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.Service;
|
||||
import net.schmizz.sshj.common.*;
|
||||
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
||||
@@ -32,6 +32,8 @@ import org.slf4j.Logger;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -86,8 +88,6 @@ public final class TransportImpl
|
||||
|
||||
private KeyAlgorithm hostKeyAlgorithm;
|
||||
|
||||
private boolean rsaSHA2Support;
|
||||
|
||||
private final Event<TransportException> serviceAccept;
|
||||
|
||||
private final Event<TransportException> close;
|
||||
@@ -130,8 +130,8 @@ public final class TransportImpl
|
||||
public TransportImpl(Config config) {
|
||||
this.config = config;
|
||||
this.loggerFactory = config.getLoggerFactory();
|
||||
this.serviceAccept = new Event<TransportException>("service accept", TransportException.chainer, loggerFactory);
|
||||
this.close = new Event<TransportException>("transport close", TransportException.chainer, loggerFactory);
|
||||
this.serviceAccept = new Event<>("service accept", TransportException.chainer, loggerFactory);
|
||||
this.close = new Event<>("transport close", TransportException.chainer, loggerFactory);
|
||||
this.nullService = new NullService(this);
|
||||
this.service = nullService;
|
||||
this.log = loggerFactory.getLogger(getClass());
|
||||
@@ -165,9 +165,20 @@ public final class TransportImpl
|
||||
throw new TransportException(e);
|
||||
}
|
||||
|
||||
ThreadNameProvider.setThreadName(reader, this);
|
||||
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.
|
||||
*/
|
||||
@@ -211,7 +222,7 @@ public final class TransportImpl
|
||||
*
|
||||
* @param buffer The buffer to read from.
|
||||
* @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)
|
||||
throws IOException {
|
||||
@@ -243,6 +254,16 @@ public final class TransportImpl
|
||||
kexer.startKex(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Key Exchange required returns true when Key Exchange is not done and when Key Exchange is not ongoing
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
@Override
|
||||
public boolean isKeyExchangeRequired() {
|
||||
return !kexer.isKexDone() && !kexer.isKexOngoing();
|
||||
}
|
||||
|
||||
public boolean isKexDone() {
|
||||
return kexer.isKexDone();
|
||||
}
|
||||
@@ -544,7 +565,7 @@ public final class TransportImpl
|
||||
* Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly.
|
||||
*
|
||||
* @param packet The 'unimplemented' packet received
|
||||
* @throws TransportException
|
||||
* @throws TransportException Thrown when key exchange is ongoing
|
||||
*/
|
||||
private void gotUnimplemented(SSHPacket packet)
|
||||
throws SSHException {
|
||||
@@ -626,21 +647,19 @@ public final class TransportImpl
|
||||
return this.hostKeyAlgorithm;
|
||||
}
|
||||
|
||||
public void setRSASHA2Support(boolean rsaSHA2Support) {
|
||||
this.rsaSHA2Support = rsaSHA2Support;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException {
|
||||
if (keyType != KeyType.RSA || !rsaSHA2Support) {
|
||||
return Factory.Named.Util.create(getConfig().getKeyAlgorithms(), keyType.toString());
|
||||
}
|
||||
|
||||
public List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException {
|
||||
List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms();
|
||||
List<KeyAlgorithm> available = new ArrayList<>();
|
||||
if (factories != null)
|
||||
for (Factory.Named<KeyAlgorithm> f : factories)
|
||||
if (f.getName().equals("ssh-rsa") || KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS.contains(f.getName()))
|
||||
return f.create();
|
||||
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
|
||||
if (
|
||||
f instanceof KeyAlgorithms.Factory && ((KeyAlgorithms.Factory) f).getKeyType().equals(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) {
|
||||
try {
|
||||
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) {
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
/**
|
||||
* @version $Id:$
|
||||
* Key File Formats
|
||||
*/
|
||||
public enum KeyFormat {
|
||||
PKCS5,
|
||||
PKCS8,
|
||||
OpenSSH,
|
||||
OpenSSHv1,
|
||||
|
||||
@@ -27,9 +27,9 @@ public class KeyProviderUtil {
|
||||
* <p/>
|
||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
||||
*
|
||||
* @param location
|
||||
* @param location File Path to key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(File location)
|
||||
throws IOException {
|
||||
@@ -45,7 +45,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key stored in a string
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -60,7 +60,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key accessible through a {@code Reader}
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -94,10 +94,8 @@ public class KeyProviderUtil {
|
||||
} else if (separatePubKey) {
|
||||
// Can delay asking for password since have unencrypted pubkey
|
||||
return KeyFormat.OpenSSH;
|
||||
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
|
||||
return KeyFormat.PKCS8;
|
||||
} else {
|
||||
return KeyFormat.PKCS5;
|
||||
return KeyFormat.PKCS8;
|
||||
}
|
||||
} else if (header.startsWith("PuTTY-User-Key-File-")) {
|
||||
return KeyFormat.PuTTY;
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
||||
import net.schmizz.sshj.common.Base64;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.transport.cipher.*;
|
||||
import net.schmizz.sshj.transport.digest.Digest;
|
||||
import net.schmizz.sshj.transport.digest.MD5;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc.
|
||||
*/
|
||||
public class PKCS5KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
|
||||
@Override
|
||||
public FileKeyProvider create() {
|
||||
return new PKCS5KeyFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PKCS5";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a format issue with PKCS5 data
|
||||
*/
|
||||
public static class FormatException
|
||||
extends IOException {
|
||||
|
||||
FormatException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a problem decrypting the data
|
||||
*/
|
||||
public static class DecryptException
|
||||
extends IOException {
|
||||
|
||||
DecryptException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] data;
|
||||
|
||||
protected KeyPair readKeyPair()
|
||||
throws IOException {
|
||||
|
||||
BufferedReader reader = new BufferedReader(resource.getReader());
|
||||
try {
|
||||
String line = null;
|
||||
Cipher cipher = new NoneCipher();
|
||||
StringBuffer sb = new StringBuffer();
|
||||
byte[] iv = new byte[0]; // salt
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) {
|
||||
int end = line.length() - 17;
|
||||
if (end > 11) {
|
||||
String s = line.substring(11, line.length() - 17);
|
||||
if ("RSA".equals(s)) {
|
||||
type = KeyType.RSA;
|
||||
} else if ("DSA".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else if ("DSS".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else {
|
||||
throw new FormatException("Unrecognized PKCS5 key type");
|
||||
}
|
||||
} else {
|
||||
throw new FormatException("Bad header; possibly PKCS8 format?");
|
||||
}
|
||||
} else if (line.startsWith("-----END")) {
|
||||
break;
|
||||
} else if (type != null) {
|
||||
if (line.startsWith("Proc-Type: ")) {
|
||||
if (!"4,ENCRYPTED".equals(line.substring(11))) {
|
||||
throw new FormatException("Unrecognized Proc-Type");
|
||||
}
|
||||
} else if (line.startsWith("DEK-Info: ")) {
|
||||
int ptr = line.indexOf(",");
|
||||
if (ptr == -1) {
|
||||
throw new FormatException("Unrecognized DEK-Info");
|
||||
} else {
|
||||
String algorithm = line.substring(10, ptr);
|
||||
if ("DES-EDE3-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.TripleDESCBC().create();
|
||||
} else if ("AES-128-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES128CBC().create();
|
||||
} else if ("AES-192-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES192CBC().create();
|
||||
} else if ("AES-256-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES256CBC().create();
|
||||
} else {
|
||||
throw new FormatException("Not a supported algorithm: " + algorithm);
|
||||
}
|
||||
iv = Arrays.copyOfRange(ByteArrayUtils.parseHex(line.substring(ptr + 1)), 0, cipher.getIVSize());
|
||||
}
|
||||
} else if (line.length() > 0) {
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == null) {
|
||||
throw new FormatException("PKCS5 header not found");
|
||||
}
|
||||
ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv));
|
||||
switch (type) {
|
||||
case RSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.RSA);
|
||||
asn.readNext();
|
||||
BigInteger modulus = asn.readNext();
|
||||
BigInteger pubExp = asn.readNext();
|
||||
BigInteger prvExp = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp));
|
||||
PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
case DSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.DSA);
|
||||
asn.readNext();
|
||||
BigInteger p = asn.readNext();
|
||||
BigInteger q = asn.readNext();
|
||||
BigInteger g = asn.readNext();
|
||||
BigInteger pub = asn.readNext();
|
||||
BigInteger prv = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g));
|
||||
PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
default:
|
||||
throw new IOException("Unrecognized PKCS5 key type: " + type);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PKCS5KeyFile{resource=" + resource + "}";
|
||||
}
|
||||
|
||||
private byte[] getPassphraseBytes() {
|
||||
CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource));
|
||||
ByteBuffer bb = IOUtils.UTF8.encode(cb);
|
||||
byte[] result = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit());
|
||||
Arrays.fill(cb.array(), '\u0000');
|
||||
Arrays.fill(bb.array(), (byte) 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] raw, Cipher cipher, byte[] iv) throws DecryptException {
|
||||
if (pwdf == null) {
|
||||
return raw;
|
||||
}
|
||||
Digest md5 = new MD5();
|
||||
int bsize = cipher.getBlockSize();
|
||||
int hsize = md5.getBlockSize();
|
||||
int hnlen = bsize / hsize * hsize + (bsize % hsize == 0 ? 0 : hsize);
|
||||
do {
|
||||
md5.init();
|
||||
byte[] hn = new byte[hnlen];
|
||||
byte[] tmp = null;
|
||||
byte[] passphrase = getPassphraseBytes();
|
||||
for (int i = 0; i + hsize <= hn.length; ) {
|
||||
if (tmp != null) {
|
||||
md5.update(tmp, 0, tmp.length);
|
||||
}
|
||||
md5.update(passphrase, 0, passphrase.length);
|
||||
md5.update(iv, 0, iv.length > 8 ? 8 : iv.length);
|
||||
tmp = md5.digest();
|
||||
System.arraycopy(tmp, 0, hn, i, tmp.length);
|
||||
i += tmp.length;
|
||||
}
|
||||
Arrays.fill(passphrase, (byte) 0);
|
||||
byte[] key = Arrays.copyOfRange(hn, 0, bsize);
|
||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||
Arrays.fill(key, (byte) 0);
|
||||
byte[] decrypted = Arrays.copyOf(raw, raw.length);
|
||||
cipher.update(decrypted, 0, decrypted.length);
|
||||
if (ASN1Data.MAGIC == decrypted[0]) {
|
||||
return decrypted;
|
||||
}
|
||||
} while (pwdf.shouldRetry(resource));
|
||||
throw new DecryptException("Decryption failed");
|
||||
}
|
||||
|
||||
class ASN1Data {
|
||||
static final byte MAGIC = (byte) 0x30;
|
||||
|
||||
private byte[] buff;
|
||||
private int index, length;
|
||||
|
||||
ASN1Data(byte[] buff) throws FormatException {
|
||||
this.buff = buff;
|
||||
index = 0;
|
||||
if (buff[index++] != MAGIC) {
|
||||
throw new FormatException("Not ASN.1 data");
|
||||
}
|
||||
length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
if ((index + length) > buff.length) {
|
||||
throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
|
||||
}
|
||||
}
|
||||
|
||||
BigInteger readNext() throws IOException {
|
||||
if (index >= length) {
|
||||
throw new EOFException();
|
||||
} else if (buff[index++] != 0x02) {
|
||||
throw new IOException("Not an int code: " + Integer.toHexString(0xff & buff[index]));
|
||||
}
|
||||
int length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
byte[] sequence = new byte[length];
|
||||
System.arraycopy(buff, index, sequence, 0, length);
|
||||
index += length;
|
||||
return new BigInteger(sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,9 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
|
||||
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
|
||||
/**
|
||||
* Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption
|
||||
*/
|
||||
public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
|
||||
@@ -84,8 +84,7 @@ public class AuthGssApiWithMic
|
||||
|
||||
@Override
|
||||
public GSSContext run() throws GSSException {
|
||||
GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME);
|
||||
GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
|
||||
GSSCredential clientCreds = manager.createCredential(GSSCredential.INITIATE_ONLY);
|
||||
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
|
||||
|
||||
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
|
||||
|
||||
@@ -27,17 +27,36 @@ import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
public abstract class KeyedAuthMethod
|
||||
extends AbstractAuthMethod {
|
||||
|
||||
protected final KeyProvider kProv;
|
||||
private Queue<KeyAlgorithm> available;
|
||||
|
||||
public KeyedAuthMethod(String name, KeyProvider kProv) {
|
||||
super(name);
|
||||
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)
|
||||
throws UserAuthException {
|
||||
PublicKey key;
|
||||
@@ -50,13 +69,16 @@ public abstract class KeyedAuthMethod
|
||||
// public key as 2 strings: [ key type | key blob ]
|
||||
KeyType keyType = KeyType.fromKey(key);
|
||||
try {
|
||||
KeyAlgorithm ka = params.getTransport().getClientKeyAlgorithm(keyType);
|
||||
reqBuf.putString(ka.getKeyAlgorithm())
|
||||
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
||||
return reqBuf;
|
||||
KeyAlgorithm ka = getPublicKeyAlgorithm(keyType);
|
||||
if (ka != null) {
|
||||
reqBuf.putString(ka.getKeyAlgorithm())
|
||||
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
||||
return reqBuf;
|
||||
}
|
||||
} 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)
|
||||
@@ -71,7 +93,7 @@ public abstract class KeyedAuthMethod
|
||||
final KeyType kt = KeyType.fromKey(key);
|
||||
Signature signature;
|
||||
try {
|
||||
signature = params.getTransport().getClientKeyAlgorithm(kt).newSignature();
|
||||
signature = getPublicKeyAlgorithm(kt).newSignature();
|
||||
} catch (TransportException e) {
|
||||
throw new UserAuthException("No KeyAlgorithm configured for key " + kt);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
|
||||
public class PasswordResponseProvider
|
||||
implements ChallengeResponseProvider {
|
||||
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword:\\s?\\z", Pattern.DOTALL);
|
||||
// FreeBSD prompt is "Password for user@host:"
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword(?: for .*)?:\\s?\\z", Pattern.DOTALL);
|
||||
|
||||
private static final char[] EMPTY_RESPONSE = new char[0];
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.password;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
@@ -64,6 +65,9 @@ public class PasswordUtils {
|
||||
*/
|
||||
public static byte[] toByteArray(char[] 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
|
||||
public OutputStream getOutputStream()
|
||||
throws IOException {
|
||||
return new FileOutputStream(file);
|
||||
return getOutputStream(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream(boolean append)
|
||||
throws IOException {
|
||||
return new FileOutputStream(file, append);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,19 @@ public interface FileTransfer {
|
||||
void upload(String localPath, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #upload(LocalSourceFile, String)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(String localPath, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}.
|
||||
@@ -43,6 +56,19 @@ public interface FileTransfer {
|
||||
void download(String remotePath, String localPath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, String localPath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}.
|
||||
*
|
||||
@@ -54,6 +80,18 @@ public interface FileTransfer {
|
||||
void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}.
|
||||
*
|
||||
@@ -65,6 +103,18 @@ public interface FileTransfer {
|
||||
void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
TransferListener getTransferListener();
|
||||
|
||||
void setTransferListener(TransferListener listener);
|
||||
|
||||
@@ -20,8 +20,13 @@ import java.io.OutputStream;
|
||||
|
||||
public interface LocalDestFile {
|
||||
|
||||
long getLength();
|
||||
|
||||
OutputStream getOutputStream()
|
||||
throws IOException;
|
||||
|
||||
OutputStream getOutputStream(boolean append)
|
||||
throws IOException;
|
||||
|
||||
/** @return A child file/directory of this directory with given {@code name}. */
|
||||
LocalDestFile getChild(String name);
|
||||
|
||||
@@ -52,24 +52,49 @@ public class SCPFileTransfer
|
||||
@Override
|
||||
public void upload(String localPath, String remotePath)
|
||||
throws IOException {
|
||||
newSCPUploadClient().copy(new FileSystemFile(localPath), remotePath);
|
||||
upload(localPath, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(localFile), remotePath, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath)
|
||||
throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath));
|
||||
download(remotePath, localPath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath, long byteOffset) throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException {
|
||||
download(remotePath, localFile, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPDownloadClient().copy(remotePath, localFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException {
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPUploadClient().copy(localFile, remotePath);
|
||||
}
|
||||
|
||||
@@ -79,4 +104,11 @@ public class SCPFileTransfer
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void checkByteOffsetSupport(long byteOffset) throws IOException {
|
||||
// TODO - implement byte offsets on SCP, if possible.
|
||||
if (byteOffset > 0) {
|
||||
throw new SCPException("Byte offset on SCP file transfers is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.hierynomus.sshj.userauth.keyprovider
|
||||
|
||||
import com.hierynomus.sshj.test.SshFixture
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.userauth.keyprovider.KeyFormat
|
||||
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
|
||||
@@ -39,7 +40,14 @@ class FileKeyProviderSpec extends Specification {
|
||||
@Unroll
|
||||
def "should have #format FileKeyProvider enabled by default"() {
|
||||
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:
|
||||
client.authPublickey("jeroen", keyfile)
|
||||
@@ -48,11 +56,11 @@ class FileKeyProviderSpec extends Specification {
|
||||
client.isAuthenticated()
|
||||
|
||||
cleanup:
|
||||
client.disconnect()
|
||||
client?.disconnect()
|
||||
|
||||
where:
|
||||
format | keyfile
|
||||
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5"
|
||||
KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
|
||||
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.net.InetSocketAddress;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class RemotePortForwarderTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class);
|
||||
|
||||
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
|
||||
public SshFixture fixture = new SshFixture();
|
||||
@@ -62,61 +62,63 @@ public class RemotePortForwarderTest {
|
||||
@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));
|
||||
assertEquals(200, httpGet( 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDynamicallyForwardPortForLocalhost() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
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
|
||||
public void shouldDynamicallyForwardPortForAllIPv4() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
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
|
||||
public void shouldDynamicallyForwardPortForAllProtocols() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0));
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForLocalhost() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE);
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForAllIPv4() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE);
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForAllProtocols() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
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 {
|
||||
while (true) {
|
||||
try {
|
||||
RemotePortForwarder.Forward forward = sshClient.getRemotePortForwarder().bind(
|
||||
return sshClient.getRemotePortForwarder().bind(
|
||||
// where the server should listen
|
||||
new RemotePortForwarder.Forward(address, portRange.nextPort()),
|
||||
// what we do with incoming connections that are forwarded to us
|
||||
new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR));
|
||||
|
||||
return forward;
|
||||
} catch (ConnectionException ce) {
|
||||
if (!portRange.hasNext()) {
|
||||
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();
|
||||
String urlString = "http://" + server + ":" + port;
|
||||
String urlString = "http://" + LOCALHOST + ":" + port;
|
||||
log.info("Trying: GET " + urlString);
|
||||
HttpResponse execute = client.execute(new HttpGet(urlString));
|
||||
return execute.getStatusLine().getStatusCode();
|
||||
@@ -140,7 +142,7 @@ public class RemotePortForwarderTest {
|
||||
}
|
||||
|
||||
private static class PortRange {
|
||||
private int upper;
|
||||
private final int upper;
|
||||
private int current;
|
||||
|
||||
public PortRange(int lower, int upper) {
|
||||
|
||||
@@ -15,54 +15,67 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.keepalive;
|
||||
|
||||
import com.hierynomus.sshj.test.KnownFailingTests;
|
||||
import com.hierynomus.sshj.test.SlowTests;
|
||||
import com.hierynomus.sshj.test.SshFixture;
|
||||
import net.schmizz.keepalive.KeepAlive;
|
||||
import net.schmizz.keepalive.KeepAliveProvider;
|
||||
import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.userauth.UserAuthException;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.categories.Category;
|
||||
|
||||
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 {
|
||||
|
||||
private static final int KEEP_ALIVE_SECONDS = 1;
|
||||
|
||||
private static final long STOP_SLEEP = 1500;
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture();
|
||||
|
||||
@Test
|
||||
@Category({SlowTests.class, KnownFailingTests.class})
|
||||
public void shouldCorrectlyTerminateThreadOnDisconnect() throws IOException, InterruptedException {
|
||||
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);
|
||||
}
|
||||
public void shouldNotStartThreadOnSetKeepAliveInterval() {
|
||||
final SSHClient sshClient = setupClient();
|
||||
|
||||
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||
for (long l : threadMXBean.getAllThreadIds()) {
|
||||
ThreadInfo threadInfo = threadMXBean.getThreadInfo(l);
|
||||
if (threadInfo.getThreadName().equals("keep-alive") && threadInfo.getThreadState() != Thread.State.TERMINATED) {
|
||||
fail("Found alive keep-alive thread in state " + threadInfo.getThreadState());
|
||||
}
|
||||
}
|
||||
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
|
||||
assertTrue(keepAlive.isDaemon());
|
||||
assertFalse(keepAlive.isAlive());
|
||||
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||
}
|
||||
|
||||
@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 net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.sftp.OpenMode;
|
||||
import net.schmizz.sshj.sftp.RemoteFile;
|
||||
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.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class RemoteFileTest {
|
||||
@Rule
|
||||
@@ -84,4 +88,141 @@ public class RemoteFileTest {
|
||||
|
||||
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.SSHClient;
|
||||
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.util.OsUtils;
|
||||
import org.apache.sshd.scp.server.ScpCommandFactory;
|
||||
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.subsystem.sftp.SftpSubsystemFactory;
|
||||
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -42,10 +39,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class SshFixture extends ExternalResource {
|
||||
public static final String hostkey = "hostkey.pem";
|
||||
public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db";
|
||||
public static final String listCommand = OsUtils.isWin32() ? "cmd.exe /C dir" : "ls";
|
||||
|
||||
private SshServer server = defaultSshServer();
|
||||
private final SshServer server = defaultSshServer();
|
||||
private SSHClient client = null;
|
||||
private AtomicBoolean started = new AtomicBoolean(false);
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
private boolean autoStart = true;
|
||||
|
||||
public SshFixture(boolean autoStart) {
|
||||
@@ -108,38 +106,23 @@ public class SshFixture extends ExternalResource {
|
||||
sshServer.setPort(randomPort());
|
||||
ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey);
|
||||
sshServer.setKeyPairProvider(fileKeyPairProvider);
|
||||
sshServer.setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||
@Override
|
||||
public boolean authenticate(String username, String password, ServerSession session) {
|
||||
return username.equals(password);
|
||||
}
|
||||
});
|
||||
sshServer.setPasswordAuthenticator((username, password, session) -> username.equals(password));
|
||||
sshServer.setGSSAuthenticator(new BogusGSSAuthenticator());
|
||||
sshServer.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
|
||||
sshServer.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
|
||||
ScpCommandFactory commandFactory = new ScpCommandFactory();
|
||||
commandFactory.setDelegateCommandFactory(new CommandFactory() {
|
||||
@Override
|
||||
public Command createCommand(String command) {
|
||||
return new ProcessShellFactory(command.split(" ")).create();
|
||||
}
|
||||
});
|
||||
commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
|
||||
sshServer.setCommandFactory(commandFactory);
|
||||
sshServer.setShellFactory(new ProcessShellFactory("ls"));
|
||||
sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
|
||||
return sshServer;
|
||||
}
|
||||
|
||||
private int randomPort() {
|
||||
try {
|
||||
ServerSocket s = null;
|
||||
try {
|
||||
s = new ServerSocket(0);
|
||||
try (final ServerSocket s = new ServerSocket(0)) {
|
||||
return s.getLocalPort();
|
||||
} finally {
|
||||
if (s != null)
|
||||
s.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,8 @@ public class FileUtil {
|
||||
IOUtils.closeQuietly(fileInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean compareFileContents(File f1, File f2) throws IOException {
|
||||
return readFromFile(f1).equals(readFromFile(f2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import net.schmizz.sshj.common.Factory;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
||||
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.KeyExchange;
|
||||
import org.apache.sshd.common.kex.KeyExchangeFactory;
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.server.kex.DHGEXServer;
|
||||
import org.apache.sshd.server.kex.DHGServer;
|
||||
@@ -38,6 +37,7 @@ import java.util.Collections;
|
||||
@RunWith(Parameterized.class)
|
||||
public class KeyExchangeTest extends BaseAlgorithmTest {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Parameterized.Parameters(name = "algorithm={0}")
|
||||
public static Collection<Object[]> getParameters() {
|
||||
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 NamedFactory<KeyExchange> serverFactory;
|
||||
private final Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
|
||||
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.serverFactory = serverFactory;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,8 @@ import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
||||
import net.schmizz.sshj.userauth.method.ChallengeResponseProvider;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||
import org.apache.sshd.common.NamedFactory;
|
||||
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.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||
import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -43,28 +39,8 @@ public class AuthKeyboardInteractiveTest {
|
||||
|
||||
@Before
|
||||
public void setKeyboardInteractiveAuthenticator() throws IOException {
|
||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
||||
@Override
|
||||
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().setUserAuthFactories(Collections.singletonList(new UserAuthKeyboardInteractiveFactory()));
|
||||
fixture.getServer().setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
|
||||
fixture.getServer().start();
|
||||
}
|
||||
|
||||
@@ -75,7 +51,7 @@ public class AuthKeyboardInteractiveTest {
|
||||
sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() {
|
||||
@Override
|
||||
public List<String> getSubmethods() {
|
||||
return new ArrayList<String>();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,56 +21,29 @@ import net.schmizz.sshj.userauth.UserAuthException;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
|
||||
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.PasswordChangeRequiredException;
|
||||
import org.apache.sshd.server.auth.password.UserAuthPassword;
|
||||
import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Stack;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class AuthPasswordTest {
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture(false);
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setPasswordAuthenticator() throws IOException {
|
||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
||||
@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().setUserAuthFactories(Collections.singletonList(new UserAuthPasswordFactory()));
|
||||
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||
@Override
|
||||
public boolean authenticate(String username, String password, ServerSession session) {
|
||||
@@ -80,6 +53,12 @@ public class AuthPasswordTest {
|
||||
return password.equals(username);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleClientPasswordChangeRequest(
|
||||
ServerSession session, String username, String oldPassword, String newPassword) {
|
||||
return username.equals(newPassword);
|
||||
}
|
||||
});
|
||||
fixture.getServer().start();
|
||||
}
|
||||
@@ -87,8 +66,7 @@ public class AuthPasswordTest {
|
||||
@Test
|
||||
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
|
||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||
expectedException.expect(UserAuthException.class);
|
||||
sshClient.authPassword("jeroen", "changeme");
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", "changeme"));
|
||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||
}
|
||||
|
||||
@@ -112,8 +90,7 @@ public class AuthPasswordTest {
|
||||
@Test
|
||||
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
|
||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||
expectedException.expect(UserAuthException.class);
|
||||
sshClient.authPassword("jeroen", new PasswordFinder() {
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", new PasswordFinder() {
|
||||
@Override
|
||||
public char[] reqPassword(Resource<?> resource) {
|
||||
return "changeme".toCharArray();
|
||||
@@ -123,7 +100,7 @@ public class AuthPasswordTest {
|
||||
public boolean shouldRetry(Resource<?> resource) {
|
||||
return false;
|
||||
}
|
||||
}, new StaticPasswordUpdateProvider("bad"));
|
||||
}, new StaticPasswordUpdateProvider("bad")));
|
||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||
}
|
||||
|
||||
@@ -153,7 +130,7 @@ public class AuthPasswordTest {
|
||||
}
|
||||
|
||||
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
|
||||
private Stack<String> newPasswords = new Stack<String>();
|
||||
private final Stack<String> newPasswords = new Stack<>();
|
||||
|
||||
public StaticPasswordUpdateProvider(String... newPasswords) {
|
||||
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();
|
||||
client.authPassword("test", "test");
|
||||
Session s = client.startSession();
|
||||
Session.Command c = s.exec("ls");
|
||||
Session.Command c = s.exec(SshFixture.listCommand);
|
||||
IOUtils.readFully(c.getErrorStream());
|
||||
IOUtils.readFully(c.getInputStream());
|
||||
c.close();
|
||||
|
||||
@@ -35,9 +35,9 @@ public class KeyProviderUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs5() throws IOException {
|
||||
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs5"));
|
||||
assertEquals(KeyFormat.PKCS5, format);
|
||||
public void testPkcs1Rsa() throws IOException {
|
||||
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs1-rsa"));
|
||||
assertEquals(KeyFormat.PKCS8, format);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
@@ -247,6 +248,12 @@ public class OpenSSHKeyFileTest {
|
||||
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)
|
||||
public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException {
|
||||
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
|
||||
@@ -437,6 +444,14 @@ public class OpenSSHKeyFileTest {
|
||||
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
|
||||
public void checkBCRegistration() {
|
||||
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;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import net.schmizz.sshj.util.KeyUtil;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class PKCS8KeyFileTest {
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
static final FileKeyProvider rsa = new PKCS8KeyFile();
|
||||
|
||||
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
|
||||
static final String pubExp = "23";
|
||||
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
|
||||
static final String KEY_PASSPHRASE = "passphrase";
|
||||
static final String INCORRECT_PASSPHRASE = String.class.getSimpleName();
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||
if (!SecurityUtils.isBouncyCastleRegistered())
|
||||
throw new AssertionError("bouncy castle needed");
|
||||
rsa.init(new File("src/test/resources/id_rsa"));
|
||||
@Test
|
||||
public void testKeys() throws GeneralSecurityException, IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(new File("src/test/resources/id_rsa"));
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
|
||||
assertEquals(provider.getType(), KeyType.RSA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeys()
|
||||
throws IOException, GeneralSecurityException {
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate());
|
||||
public void testPkcs1Rsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getFile("pkcs1-rsa"));
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testType()
|
||||
throws IOException {
|
||||
assertEquals(rsa.getType(), KeyType.RSA);
|
||||
public void testPkcs1Encrypted() throws IOException, GeneralSecurityException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getFile("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray()));
|
||||
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
|
||||
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Rsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-rsa-2048"));
|
||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-rsa-2048"));
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncrypted() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray());
|
||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals("RSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("RSA", provider.getPrivate().getAlgorithm());
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray());
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncryptedIncorrectPassword() throws IOException {
|
||||
expectedException.expect(KeyDecryptionFailedException.class);
|
||||
|
||||
public void testPkcs8RsaEncryptedIncorrectPassword() {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray());
|
||||
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
provider.getPrivate();
|
||||
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(INCORRECT_PASSPHRASE.toCharArray());
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertThrows(KeyDecryptionFailedException.class, provider::getPrivate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8RsaEncryptedRetryPassword() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
final PasswordFinder passwordFinder = new PasswordFinder() {
|
||||
private boolean retryEnabled = true;
|
||||
|
||||
@Override
|
||||
public char[] reqPassword(Resource<?> resource) {
|
||||
final char[] password;
|
||||
if (retryEnabled) {
|
||||
password = INCORRECT_PASSPHRASE.toCharArray();
|
||||
} else {
|
||||
password = KEY_PASSPHRASE.toCharArray();
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry(Resource<?> resource) {
|
||||
final boolean shouldRetry = retryEnabled;
|
||||
if (retryEnabled) {
|
||||
retryEnabled = false;
|
||||
}
|
||||
return shouldRetry;
|
||||
}
|
||||
};
|
||||
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Ecdsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-ecdsa"));
|
||||
assertEquals("ECDSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("ECDSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-ecdsa"));
|
||||
assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkcs8Dsa() throws IOException {
|
||||
final PKCS8KeyFile provider = new PKCS8KeyFile();
|
||||
provider.init(getReader("pkcs8-dsa"));
|
||||
assertEquals("DSA", provider.getPublic().getAlgorithm());
|
||||
assertEquals("DSA", provider.getPrivate().getAlgorithm());
|
||||
provider.init(getFile("pkcs8-dsa"));
|
||||
assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm());
|
||||
assertEquals(KeyAlgorithm.DSA, provider.getPrivate().getAlgorithm());
|
||||
}
|
||||
|
||||
private Reader getReader(final String filename) {
|
||||
private File getFile(final String filename) {
|
||||
final String path = String.format("/keyformats/%s", filename);
|
||||
final InputStream inputStream = getClass().getResourceAsStream(path);
|
||||
if (inputStream == null) {
|
||||
final URL resource = getClass().getResource(path);
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException(String.format("Key File [%s] not found", path));
|
||||
}
|
||||
return new InputStreamReader(inputStream);
|
||||
return new File(resource.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,25 @@ public class PuTTYKeyFileTest {
|
||||
"oYhmT2+0DKBuBVCAM4qRdA==\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" +
|
||||
"Encryption: aes256-cbc\n" +
|
||||
"Comment: dsa-key-20140507\n" +
|
||||
@@ -502,6 +521,15 @@ public class PuTTYKeyFileTest {
|
||||
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)
|
||||
public void testWrongPassphraseRsa() throws Exception {
|
||||
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-----
|
||||
1
src/test/resources/keytypes/ed25519_aes128cbc.pem.pub
Normal file
1
src/test/resources/keytypes/ed25519_aes128cbc.pem.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBi sshjtest@TranceLove
|
||||
Reference in New Issue
Block a user