Compare commits

...

33 Commits

Author SHA1 Message Date
Jeroen van Erp
d7dd73b9c8 Retry authentication with all remaining auth methods after partial success
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-09-24 10:02:07 +02:00
Jeroen van Erp
d628c47bae Upgrade Gradle
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-09-23 22:42:24 +02:00
kegelh
6e7fb96d07 Support SSHClient.authPassword on FreeBSD (#815)
* Support SSHClient.authPassword on FreeBSD

FreeBSD "keyboard-interactive" prompt is "Password for user@host:"

* Add test for PasswordResponseProvider
2022-09-19 13:16:56 +02:00
kegelh
d5d6096d5d Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and… (#813)
* Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and a Transport.write call

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.

* Add integration test for #805
2022-09-17 07:11:11 +02:00
exceptionfactory
2551f8e559 Add Transport.isKeyExchangeRequired() to avoid unnecessary KEXINIT (#811)
* Added Transport.isKeyExchangeRequired() to avoid unnecessary KEXINIT

- Updated SSHClient.onConnect() to check isKeyExchangeRequired() before calling doKex()
- Added started timestamp in ThreadNameProvider for improved tracking

* Moved KeepAliveThread State check after authentication to avoid test timing issues
2022-09-16 15:04:26 +02:00
kegelh
430cbfcf13 Make all tests runnable on Windows (#814) 2022-09-16 12:25:28 +02:00
Jeroen van Erp
ec467a3875 Prepare release notes for 0.34.0 2022-08-10 10:23:18 +02:00
Geoffrey Thomas
1b258f0677 AuthGssApiWithMic: Use default client creds instead of remote username (#743)
Previously, AuthGssApiWithMic used params.getUsername() to create the
local client credential object. However, at least when using the native
GSS libraries (sun.security.jgss.native=true), the username would need 
to be something like "user@EXAMPLE.COM", not "user", or the library is 
unable to find credentials. Also, your remote username might not be your
local username.

Instead, and more simply, call the GSSManager#createCredential variant
that just uses default credentials, which should handle both of these 
cases.

Tested on Windows using SSPI. I haven't tested this patch on Linux but I
have confirmed that this form of call to createCredential works as I 
expect when using the native GSS/Kerberos library there too.

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-08-08 14:16:18 +02:00
Alex Heneveld
559384ac91 restore the interrupt flag whenever we catch InterruptedException (#801)
Co-authored-by: Alex Heneveld <alex@cloudsoft.io>
2022-08-08 14:09:18 +02:00
exceptionfactory
5674072666 Replace PKCS5 Key File Class with PKCS8 (#793)
* Replaced PKCS5 parsing with PKCS8

- Moved tests for PEM-encoded PKCS1 files to PKCS8
- Removed PKCS5 Key File implementation

* Added PKCS8 test to retry password after initial failure

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-07-14 11:36:42 +02:00
exceptionfactory
f33bfecbf5 Upgraded SLF4J to 1.7.36 and Logback to 1.2.11 (#792) 2022-07-13 08:06:03 +02:00
exceptionfactory
c0f6000ff5 Updated KeepAlive and RemotePF examples (#791)
- Set KeepAlive interval before connecting
2022-07-12 17:01:35 +02:00
Brent Tyler
3de0302c84 Added SFTP file transfer resume support on both PUT and GET. (#775)
* Added SFTP file transfer resume support on both PUT and GET. Internally SFTPFileTransfer has a few sanity checks to fall back to full replacement even if the resume flag is set. 

SCP file transfers have not been changed to support this at this time.

* Added JUnit tests for issue-700

* Throw SCPException when attempting to resume SCP transfers.

* Licensing

* Small bug resuming a completed file was restarting since the bytes were equal.

* Enhanced test cases to validate the expected bytes transferred for each scenario are the actual bytes transferred.

* Removed author info which was pre-filled from company IDE template

* Added "fall through" comment for switch

* Changed the API for requesting a resume from a boolean flag with some internal decisions to be a user-specified long byte offset. This is cleaner but puts the onus on the caller to know exactly what they're asking for in their circumstance, which is ultimately better for a library like sshj.

* Reverted some now-unnecessary changes to SFTPFileTransfer.Uploader.prepareFile()

* Fix gradle exclude path for test files

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-05-27 13:05:41 +02:00
Jeroen van Erp
d7e402c557 Prepare release notes for 0.33.0 2022-04-20 12:54:04 +02:00
Vladimir Lagunov
8ef996b406 Fix #777: Don't request excessive read-ahead packets in RemoteFile (#778)
Due to a bug in logic introduced by #769, RemoteFile.ReadAheadRemoteFileInputStream started to send new read ahead requests for file parts that had already been requested.

Every call to read() asked the server to send parts of the file from the point which is already downloaded. Instead, it should have asked to send parts after the last requested part. This commit adds exactly this logic.

The bug didn't cause content corruption. It only affected performance, both on servers and on clients.
2022-04-07 13:10:02 +02:00
Vladimir Lagunov
e9cb90901c Throw IOE instead of NPE if OpenSSHKeyV1KeyFile reads an empty file (#773)
There is a contract that FileKeyProvider.readKey throws an IOException if something goes wrong. Throwing an NPE is not expected by API users. Also, it is much more difficult to find out if the NPE is thrown due to a broken key file, or due to an internal bug.
2022-04-01 09:41:48 +02:00
Raymond Lai
69812e9a81 Add support for JuiceSSH generated ed25519 keys (#770)
Reported from https://github.com/TeamAmaze/AmazeFileManager/issues/2976, it was found the key uses aes-128-cbc which is currently not supported by sshj. This change adds support for it.

To enable support for this, also eliminated hardcoding byte array size for key and IV, as a result of BCrypt.pbkdf().
2022-03-07 10:13:15 +01:00
Vladimir Lagunov
9a939d029b Fix ReadAheadRemoteFileInputStream not reading the whole file if a buffer is too big (#769)
If an instance of ReadAheadRemoteFileInputStream before this change is wrapped into a BufferedInputStream with a big buffer, the SSH client requests big packets from the server. It turned out that if the server had sent a response smaller than requested, the client wouldn't have adjusted to decreased window size, and would have read the file incorrectly.

This change detects cases when the server is not able to fulfil client's requests. Since this change, the client adjusts the maximum request length, sends new read-ahead requests, and starts to ignore all read-ahead requests sent earlier.

Just specifying some allegedly small constant buffer size wouldn't have helped in all possible cases. There is no way to explicitly get the maximum request length inside a client. All that limits differ from server to server. For instance, OpenSSH defines SFTP_MAX_MSG_LENGTH as 256 * 1024. Apache SSHD defines MAX_READDATA_PACKET_LENGTH as 63 * 1024, and it allows to redefine that size.

Interestingly, a similar issue #183 was fixed many years ago, but the bug was actually in the code introduced for that fix.
2022-03-04 21:07:18 +01:00
exceptionfactory
50efeb6519 Remove deprecated proxy connect methods from SocketClient (#756)
* Removed deprecated proxy connect methods from SocketClient

- Removed custom Jdk7HttpProxySocket class

* Reverted removal of Jdk7HttpProxySocket to retain JDK 7 support for HTTP CONNECT

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2022-02-04 17:29:26 +01:00
Yves Langisch
aabb1be52e Try all public key algorithms available for a specific key type in SSH_MSG_USERAUTH_REQUEST. (#763) 2022-02-04 09:08:30 +01:00
exceptionfactory
32329e547e Add Codecov to GitHub workflow (#759)
* Added Codecov to GitHub workflow

* Added Codecov to GitHub workflow
2022-01-03 20:24:45 +01:00
David Kocher
8cf63a96a9 Add parameter to limit read ahead to maximum length. Allows to use mu… (#724)
* Add parameter to limit read ahead to maximum length. Allows to use multiple concurrent threads reading from the same file with an offset without reading too much ahead for a single segment.

* Review and add tests.

Signed-off-by: David Kocher <dkocher@iterate.ch>

Co-authored-by: Yves Langisch <yves@langisch.ch>
2021-12-23 22:24:52 +01:00
exceptionfactory
cab7731928 Added Thread naming based on remote socket address (#738) (#753)
- Added ThreadNameProvider to set name based on Thread Class and remote socket address
- Added RemoteAddressProvider to abstract access to Remote Socket Address
- Set Reader Thread name in TransportImpl
- Set SFTP PacketReader Thread name in SFTPEngine
- Set KeepAlive Thread name in SSHClient

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2021-12-23 15:42:23 +01:00
Damiano Albani
50073db6c1 Bump version to latest release in POM excerpt (#736)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2021-12-22 17:44:18 +01:00
exceptionfactory
90099bbf5e Updated SSHClient to interrupt KeepAlive Thread when disconnecting (#506) (#752)
- Changed KeepAlive.setKeepAliveInterval() to avoid starting Thread
- Updated SSHClient.onConnect() to start KeepAlive Thread when enabled
- Updated SSHClient.disconnect() to interrupt KeepAlive Thread
- Updated KeepAliveThreadTerminationTest to verify state of KeepAlive Thread

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2021-12-22 16:55:09 +01:00
exceptionfactory
ce0a7d5193 Avoid setting SFTP rename flags below version 5 (#751) (#754)
Fixes #750 
Fixes #751 

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2021-12-22 12:18:28 +01:00
exceptionfactory
ced27fc898 Upgraded Bouncy Castle to 1.70 and upgraded test dependencies (#755)
- Adjusted test classes to work with Apache SSHD 2.8.0
- Upgraded Bouncy Castle from 1.69 to 1.70
- Upgraded Apache SSHD from 2.1.0 to 2.8.0
- Upgraded JUnit from 4.12 to 4.13.2
- Upgraded Mockito from 2.28.2 to 4.2.0
- Upgraded Logback from 1.2.6 to 1.2.9
- Upgraded Apache HTTP Client from 4.5.9 to 4.5.14
2021-12-22 10:37:36 +01:00
Vladimir Lagunov
624747c527 Lean on Config.keyAlgorithms choosing between rsa-sha2-* and ssh-rsa (#742)
* Improve SshdContainer: log `docker build` to stdout, don't wait too long if container exited

* Fix #740: Lean on Config.keyAlgorithms choosing between rsa-sha2-* and ssh-rsa

Previously, there was a heuristic that was choosing rsa-sha2-512 after receiving a host key of type RSA. It didn't work well when a server doesn't have an RSA host key.

OpenSSH 8.8 introduced a breaking change: it removed ssh-rsa from the default list of supported public key signature algorithms. SSHJ was unable to connect to OpenSSH 8.8 server if the server has an EcDSA or Ed25519 host key.

Current behaviour behaves the same as OpenSSH 8.8 client does. SSHJ doesn't try to determine rsa-sha2-* support on the fly. Instead, it looks only on `Config.getKeyAlgorithms()`, which may or may not contain ssh-rsa and rsa-sha2-* in any order.

Sorry, this commit mostly reverts changes from #607.

* Introduce ConfigImpl.prioritizeSshRsaKeyAlgorithm to deal with broken backward compatibility

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2021-12-06 12:14:04 +01:00
David Kocher
d8697c2228 ByteBuffer.array() must not be used as it does not take the real buffer size into account and returns the whole buffer up to its capacity. Fixes #745. (#746)
Co-authored-by: Yves Langisch <yla@iterate.ch>
2021-11-22 09:51:15 +01:00
Vladimir Lagunov
7c14098f7d Fix: if the client knows CA key, it should send host key algo proposal for certificates (#733)
* Fix: if the client knows CA key, it should send host key algo proposal for certificates

* Run specific SSH server in KeyWithCertificateSpec

Required to verify the case with wrong host key algorithm proposals. See #733

* Split KeyWithCertificateSpec into HostKeyWithCertificateSpec and PublicKeyAuthWithCertificateSpec

Prevents from starting unnecessary SSHD containers, making the tests run a bit faster when they are launched separately.
2021-11-10 23:06:07 +01:00
Vladimir Lagunov
d5805a6c64 Use testcontainers (#741)
* Replace abstract class IntegrationBaseSpec with composition through IntegrationTestUtil

* Switch to testcontainers in integration tests

It allows running different SSH servers with different configurations in tests, giving ability to cover more bugs, like mentioned in #733.
2021-11-10 14:30:35 +01:00
Torbjørn Søiland
8a66dc5336 Close client connection when remote closes connection + testing (#686) (#687) 2021-10-19 16:34:59 +02:00
Henning Pöttker
a5c10ab50f Fix issue urls in release notes (#732) 2021-10-12 20:11:48 +02:00
101 changed files with 2468 additions and 1332 deletions

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
*.bat text eol=crlf *.bat text eol=crlf
src/itest/docker-image/** eol=lf

View File

@@ -24,6 +24,8 @@ jobs:
run: chmod +x gradlew run: chmod +x gradlew
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew check run: ./gradlew check
- name: Codecov
uses: codecov/codecov-action@v2
integration: integration:
name: Integration test name: Integration test

View File

@@ -1,7 +1,7 @@
= sshj - SSHv2 library for Java = sshj - SSHv2 library for Java
Jeroen van Erp Jeroen van Erp
:sshj_groupid: com.hierynomus :sshj_groupid: com.hierynomus
:sshj_version: 0.31.0 :sshj_version: 0.32.0
:source-highlighter: pygments :source-highlighter: pygments
image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"] image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"]
@@ -104,8 +104,32 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
Fork away! Fork away!
== Release history == Release history
SSHJ 0.34.0 (2022-08-10)::
* Merged https://github.com/hierynomus/sshj/pull/743[#743]: Use default client credentials for AuthGssApiWithMic
* Merged https://github.com/hierynomus/sshj/pull/801[#801]: Restore thread interrupt status after catching InterruptedException
* Merged https://github.com/hierynomus/sshj/pull/793[#793]: Merge PKCS5 and PKCS8 classes
* Upgraded dependencies SLF4J (1.7.36) and Logback (1.2.11)
* Merged https://github.com/hierynomus/sshj/pull/791[#791]: Update KeepAlive examples
* Merged https://github.com/hierynomus/sshj/pull/775[#775]: Add SFTP resume support
SSHJ 0.33.0 (2022-04-22)::
* Upgraded dependencies BouncyCastle (1.70)
* Merged https://github.com/hierynomus/sshj/pull/687[#687]: Correctly close connection when remote closes connection.
* Merged https://github.com/hierynomus/sshj/pull/741[#741]: Add support for testcontainers in test setup to test more scenarios
* Merged https://github.com/hierynomus/sshj/pull/733[#733]: Send correct key proposal if client knows CA key
* Merged https://github.com/hierynomus/sshj/pull/746[#746]: Fix bug in reading Putty private key file with passphrase
* Merged https://github.com/hierynomus/sshj/pull/742[#742]: Use Config.keyAlgorithms to determine rsa-sha2 support
* Merged https://github.com/hierynomus/sshj/pull/754[#754]: Use SFTP protocol version to set FXP rename flags conditionally
* Merged https://github.com/hierynomus/sshj/pull/752[#752]: Correctly start and terminate KeepAlive thread
* Merged https://github.com/hierynomus/sshj/pull/753[#753]: Provide better thread names
* Merged https://github.com/hierynomus/sshj/pull/724[#724]: Add parameter to limit read ahead length
* Merged https://github.com/hierynomus/sshj/pull/763[#763]: Try all public key algorithms for a specific key type
* Merged https://github.com/hierynomus/sshj/pull/756[#756]: Remove deprecated proxy connect methods
* Merged https://github.com/hierynomus/sshj/pull/770[#770]: Add support for `ed25519` `aes-128-cbc` keys
* Merged https://github.com/hierynomus/sshj/pull/773[#773]: Fix NPE when reading empty OpenSSHKeyV1KeyFile
* Merged https://github.com/hierynomus/sshj/pull/777[#777]: Don't request too many read-ahead packets
SSHJ 0.32.0 (2021-10-12):: SSHJ 0.32.0 (2021-10-12)::
* Send EOF on channel close (Fixes https://github.com/hierynomus/sshj/issue/143[#143], https://github.com/hierynomus/sshj/issue/496[#496], https://github.com/hierynomus/sshj/issue/553[#553], https://github.com/hierynomus/sshj/issue/554[#554]) * Send EOF on channel close (Fixes https://github.com/hierynomus/sshj/issues/143[#143], https://github.com/hierynomus/sshj/issues/496[#496], https://github.com/hierynomus/sshj/issues/553[#553], https://github.com/hierynomus/sshj/issues/554[#554])
* Merged https://github.com/hierynomus/sshj/pull/726[#726]: Parse OpenSSH v1 keys with full CRT information present * Merged https://github.com/hierynomus/sshj/pull/726[#726]: Parse OpenSSH v1 keys with full CRT information present
* Merged https://github.com/hierynomus/sshj/pull/721[#721]: Prefer known host key algorithm for host key verification * Merged https://github.com/hierynomus/sshj/pull/721[#721]: Prefer known host key algorithm for host key verification
* Merged https://github.com/hierynomus/sshj/pull/716[#716], https://github.com/hierynomus/sshj/pull/729[#729] and https://github.com/hierynomus/sshj/pull/730[#730]: Add full support for PuTTY v3 key files. * Merged https://github.com/hierynomus/sshj/pull/716[#716], https://github.com/hierynomus/sshj/pull/729[#729] and https://github.com/hierynomus/sshj/pull/730[#730]: Add full support for PuTTY v3 key files.

View File

@@ -1,7 +1,3 @@
import java.text.SimpleDateFormat
import com.bmuschko.gradle.docker.tasks.container.*
import com.bmuschko.gradle.docker.tasks.image.*
plugins { plugins {
id "java" id "java"
id "groovy" id "groovy"
@@ -39,11 +35,11 @@ project.version = scmVersion.version
configurations.implementation.transitive = false configurations.implementation.transitive = false
def bouncycastleVersion = "1.69" def bouncycastleVersion = "1.70"
def sshdVersion = "2.1.0" def sshdVersion = "2.8.0"
dependencies { dependencies {
implementation "org.slf4j:slf4j-api:1.7.32" implementation "org.slf4j:slf4j-api:1.7.36"
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
implementation "com.jcraft:jzlib:1.1.3" implementation "com.jcraft:jzlib:1.1.3"
@@ -51,16 +47,16 @@ dependencies {
implementation "net.i2p.crypto:eddsa:0.3.0" implementation "net.i2p.crypto:eddsa:0.3.0"
testImplementation "junit:junit:4.12" testImplementation "junit:junit:4.13.2"
testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4' testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
testImplementation "org.mockito:mockito-core:2.28.2" testImplementation "org.mockito:mockito-core:4.2.0"
testImplementation "org.apache.sshd:sshd-core:$sshdVersion" testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion" testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion" testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
testRuntimeOnly "ch.qos.logback:logback-classic:1.2.6" testImplementation "ch.qos.logback:logback-classic:1.2.11"
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4' testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
testImplementation 'org.apache.httpcomponents:httpclient:4.5.9' testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
testImplementation 'org.testcontainers:testcontainers:1.16.2'
} }
license { license {
@@ -69,7 +65,12 @@ license {
mapping { mapping {
java = 'SLASHSTAR_STYLE' java = 'SLASHSTAR_STYLE'
} }
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java']) excludes([
'**/djb/Curve25519.java',
'**/sshj/common/Base64.java',
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
'**/files/test_file_*.txt',
])
} }
if (!JavaVersion.current().isJava9Compatible()) { if (!JavaVersion.current().isJava9Compatible()) {
@@ -276,48 +277,11 @@ jacocoTestReport {
} }
} }
task buildItestImage(type: DockerBuildImage) {
inputDir = file('src/itest/docker-image')
images.add('sshj/sshd-itest:latest')
}
task createItestContainer(type: DockerCreateContainer) {
dependsOn buildItestImage
targetImageId buildItestImage.getImageId()
hostConfig.portBindings = ['2222:22']
hostConfig.autoRemove = true
}
task startItestContainer(type: DockerStartContainer) {
dependsOn createItestContainer
targetContainerId createItestContainer.getContainerId()
}
task logItestContainer(type: DockerLogsContainer) {
dependsOn createItestContainer
targetContainerId createItestContainer.getContainerId()
showTimestamps = true
stdErr = true
stdOut = true
tailAll = true
}
task stopItestContainer(type: DockerStopContainer) {
targetContainerId createItestContainer.getContainerId()
}
task forkedUploadRelease(type: GradleBuild) { task forkedUploadRelease(type: GradleBuild) {
buildFile = project.buildFile buildFile = project.buildFile
tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"] tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"]
} }
project.tasks.integrationTest.dependsOn(startItestContainer)
project.tasks.integrationTest.finalizedBy(stopItestContainer)
// Being enabled, it pollutes logs on CI. Uncomment when debugging some test to get sshd logs.
// project.tasks.stopItestContainer.dependsOn(logItestContainer)
project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build]) project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build])
project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease) project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease)
project.tasks.jacocoTestReport.dependsOn(project.tasks.test) project.tasks.jacocoTestReport.dependsOn(project.tasks.test)

View File

@@ -24,7 +24,7 @@
<groupId>com.hierynomus</groupId> <groupId>com.hierynomus</groupId>
<artifactId>sshj-examples</artifactId> <artifactId>sshj-examples</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>0.19.1</version> <version>0.33.0</version>
<name>sshj-examples</name> <name>sshj-examples</name>
<description>Examples for SSHv2 library for Java</description> <description>Examples for SSHv2 library for Java</description>
@@ -55,7 +55,7 @@
<dependency> <dependency>
<groupId>com.hierynomus</groupId> <groupId>com.hierynomus</groupId>
<artifactId>sshj</artifactId> <artifactId>sshj</artifactId>
<version>0.31.0</version> <version>0.33.0</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -19,8 +19,9 @@ public class KeepAlive {
final SSHClient ssh = new SSHClient(defaultConfig); final SSHClient ssh = new SSHClient(defaultConfig);
try { try {
ssh.addHostKeyVerifier(new PromiscuousVerifier()); ssh.addHostKeyVerifier(new PromiscuousVerifier());
// Set interval to enable keep-alive before connecting
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
ssh.connect(args[0]); ssh.connect(args[0]);
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5); //every 60sec
ssh.authPassword(args[1], args[2]); ssh.authPassword(args[1], args[2]);
Session session = ssh.startSession(); Session session = ssh.startSession();
session.allocateDefaultPTY(); session.allocateDefaultPTY();

View File

@@ -19,6 +19,7 @@ public class RemotePF {
client.loadKnownHosts(); client.loadKnownHosts();
client.connect("localhost"); client.connect("localhost");
client.getConnection().getKeepAlive().setKeepAliveInterval(5);
try { try {
client.authPublickey(System.getProperty("user.name")); client.authPublickey(System.getProperty("user.name"));
@@ -33,8 +34,6 @@ public class RemotePF {
// what we do with incoming connections that are forwarded to us // what we do with incoming connections that are forwarded to us
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80))); new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
client.getTransport().setHeartbeatInterval(30);
// Something to hang on to so that the forwarding stays // Something to hang on to so that the forwarding stays
client.getTransport().join(); client.getTransport().join();

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

269
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,78 +17,113 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@@ -105,79 +140,95 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

22
gradlew.bat vendored
View File

@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -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"]

View File

@@ -3,6 +3,7 @@ ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHQiZm0w
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaWrwt3drIOjeBq2LSHRavxAT7ja2f+5soOUJl/zKSI ajvanerp@Heimdall.xebialabs.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaWrwt3drIOjeBq2LSHRavxAT7ja2f+5soOUJl/zKSI ajvanerp@Heimdall.xebialabs.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBi sshjtest@TranceLove
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKRyZAtOJJfAhPU6xE6ZXY564vwErAI3n3Yn4lTHL9bxev9Ily6eCqPLcV0WbSV04pztngFn9MjT7yb8mcXheHpIaWEH569sMpmpOtyfn4p68SceuXBGyyPGMIcfOTknkASd1JYSD4EPkd9rZmCzcx3vEnLu8ChnA/G221xSVQ5VC/jD/c/CgNUayhQ+xbn57qHKKtZwfTa21QmwIabGYJNwlVjlKTCdddeVnZfKqKrG7cxHQApsxd21rhM9IT/C/f4Y/Tx3WUUVeam0iZ265oiPHoPALqJIWSQIUheRYAxYAQqJwSQ0Or9MM8XXun2Iy3RUSGk6eIvrCsFbNURsHNs7Pu0UnpYv6FZ3vCkFep/1pAT6fQvY7pDOOWDHKXArD4watc9gIWaQBH73wDW/KgBcnMRSoGWgQjsYqIamP4oV1+HqUI3lRAsXZaX+eiBGt3+3A5KebP27UJ1YUwhwlzs7wzTKaCu0OaL+hOsP1F2AxAa995bgFksMd23645ux3YCJKXG4sGpJ1Z/Hs49K72gv+QjLZVxXqY623c8+3OUhlixqoEFd4iG7UMc5a552ch/VA+jaspmLZoFhPz99aBRVb1oCSPxSwLw+Q/wxv6pZmT+14rqTzY2farjU53hM+CsUPh7dnWXhGG7RuA5wCdeOXOYjuksfzAoHIZhPqTgQ== ajvanerp@Heimdall.local ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKRyZAtOJJfAhPU6xE6ZXY564vwErAI3n3Yn4lTHL9bxev9Ily6eCqPLcV0WbSV04pztngFn9MjT7yb8mcXheHpIaWEH569sMpmpOtyfn4p68SceuXBGyyPGMIcfOTknkASd1JYSD4EPkd9rZmCzcx3vEnLu8ChnA/G221xSVQ5VC/jD/c/CgNUayhQ+xbn57qHKKtZwfTa21QmwIabGYJNwlVjlKTCdddeVnZfKqKrG7cxHQApsxd21rhM9IT/C/f4Y/Tx3WUUVeam0iZ265oiPHoPALqJIWSQIUheRYAxYAQqJwSQ0Or9MM8XXun2Iy3RUSGk6eIvrCsFbNURsHNs7Pu0UnpYv6FZ3vCkFep/1pAT6fQvY7pDOOWDHKXArD4watc9gIWaQBH73wDW/KgBcnMRSoGWgQjsYqIamP4oV1+HqUI3lRAsXZaX+eiBGt3+3A5KebP27UJ1YUwhwlzs7wzTKaCu0OaL+hOsP1F2AxAa995bgFksMd23645ux3YCJKXG4sGpJ1Z/Hs49K72gv+QjLZVxXqY623c8+3OUhlixqoEFd4iG7UMc5a552ch/VA+jaspmLZoFhPz99aBRVb1oCSPxSwLw+Q/wxv6pZmT+14rqTzY2farjU53hM+CsUPh7dnWXhGG7RuA5wCdeOXOYjuksfzAoHIZhPqTgQ== ajvanerp@Heimdall.local
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMvfRYSe44VQGwxexOMibcM3+fWeUP1jrBofOxFDRRrzRF8dK/vll2svqTPXMRnITnT1UoemEcB5OHtvH4hzfh/HFeDxJ5S7UncYxoClTSa8MeMFG2Zj9CoUZs1SHbwSGg== root@sshj ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMvfRYSe44VQGwxexOMibcM3+fWeUP1jrBofOxFDRRrzRF8dK/vll2svqTPXMRnITnT1UoemEcB5OHtvH4hzfh/HFeDxJ5S7UncYxoClTSa8MeMFG2Zj9CoUZs1SHbwSGg== root@sshj

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -20,9 +20,15 @@ import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.transport.TransportException import net.schmizz.sshj.transport.TransportException
import net.schmizz.sshj.userauth.UserAuthException import net.schmizz.sshj.userauth.UserAuthException
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class IntegrationSpec extends IntegrationBaseSpec { class IntegrationSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should accept correct key for #signatureName"() { def "should accept correct key for #signatureName"() {
@@ -33,7 +39,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint
when: when:
sshClient.connect(SERVER_IP, DOCKER_PORT) sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
then: then:
sshClient.isConnected() sshClient.isConnected()
@@ -50,7 +56,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3") sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3")
when: when:
sshClient.connect(SERVER_IP, DOCKER_PORT) sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
then: then:
thrown(TransportException.class) thrown(TransportException.class)
@@ -59,11 +65,11 @@ class IntegrationSpec extends IntegrationBaseSpec {
@Unroll @Unroll
def "should authenticate with key #key"() { def "should authenticate with key #key"() {
given: given:
SSHClient client = getConnectedClient() SSHClient client = sshd.getConnectedClient()
when: when:
def keyProvider = passphrase != null ? client.loadKeys("src/itest/resources/keyfiles/$key", passphrase) : client.loadKeys("src/itest/resources/keyfiles/$key") def keyProvider = passphrase != null ? client.loadKeys("src/itest/resources/keyfiles/$key", passphrase) : client.loadKeys("src/itest/resources/keyfiles/$key")
client.authPublickey(USERNAME, keyProvider) client.authPublickey(IntegrationTestUtil.USERNAME, keyProvider)
then: then:
client.isAuthenticated() client.isAuthenticated()
@@ -74,6 +80,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
"id_ecdsa_opensshv1" | null "id_ecdsa_opensshv1" | null
"id_ed25519_opensshv1" | null "id_ed25519_opensshv1" | null
"id_ed25519_opensshv1_aes256cbc.pem" | "foobar" "id_ed25519_opensshv1_aes256cbc.pem" | "foobar"
"id_ed25519_opensshv1_aes128cbc.pem" | "sshjtest"
"id_ed25519_opensshv1_protected" | "sshjtest" "id_ed25519_opensshv1_protected" | "sshjtest"
"id_rsa" | null "id_rsa" | null
"id_rsa_opensshv1" | null "id_rsa_opensshv1" | null
@@ -83,7 +90,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
def "should not authenticate with wrong key"() { def "should not authenticate with wrong key"() {
given: given:
SSHClient client = getConnectedClient() SSHClient client = sshd.getConnectedClient()
when: when:
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key") client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")

View File

@@ -13,14 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.hierynomus.sshj.backport; package com.hierynomus.sshj
public class JavaVersion {
public static boolean isJava7OrEarlier() {
String property = System.getProperty("java.specification.version");
float diff = Float.parseFloat(property) - 1.7f;
return diff < 0.01;
}
class IntegrationTestUtil {
static final String USERNAME = "sshj"
static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
} }

View 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
}
}

View File

@@ -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()
}
}

View File

@@ -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;
}
}

View 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());
}
}

View File

@@ -15,21 +15,28 @@
*/ */
package com.hierynomus.sshj.sftp package com.hierynomus.sshj.sftp
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.SSHClient import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.OpenMode import net.schmizz.sshj.sftp.OpenMode
import net.schmizz.sshj.sftp.RemoteFile import net.schmizz.sshj.sftp.RemoteFile
import net.schmizz.sshj.sftp.SFTPClient import net.schmizz.sshj.sftp.SFTPClient
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
class FileWriteSpec extends IntegrationBaseSpec { class FileWriteSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
def "should append to file (GH issue #390)"() { def "should append to file (GH issue #390)"() {
given: given:
SSHClient client = getConnectedClient() SSHClient client = sshd.getConnectedClient()
client.authPublickey("sshj", "src/test/resources/id_rsa") client.authPublickey("sshj", "src/test/resources/id_rsa")
SFTPClient sftp = client.newSFTPClient() SFTPClient sftp = client.newSFTPClient()
def file = "/home/sshj/test.txt" def file = "/home/sshj/test.txt"

View File

@@ -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",
]
}
}

View File

@@ -15,29 +15,34 @@
*/ */
package com.hierynomus.sshj.signature package com.hierynomus.sshj.signature
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts import net.schmizz.sshj.transport.verification.PromiscuousVerifier
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
import java.nio.file.Files
import java.util.stream.Collectors
/** /**
* This is a brief test for verifying connection to a server using keys with certificates. * This is a brief test for verifying connection to a server using keys with certificates.
* *
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}. * Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
*/ */
class KeyWithCertificateSpec extends IntegrationBaseSpec { class PublicKeyAuthWithCertificateSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "authorising with a signed public key #keyName"() { def "authorising with a signed public key #keyName"() {
given: given:
def client = getConnectedClient() SSHClient client = new SSHClient(new DefaultConfig())
client.addHostKeyVerifier(new PromiscuousVerifier())
client.connect("127.0.0.1", sshd.firstMappedPort)
when: when:
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/certificates/$keyName") client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/$keyName")
then: then:
client.authenticated client.authenticated
@@ -73,43 +78,4 @@ class KeyWithCertificateSpec extends IntegrationBaseSpec {
"id_ed25519_384_rfc4716_signed_by_rsa", "id_ed25519_384_rfc4716_signed_by_rsa",
] ]
} }
@Unroll
def "accepting a signed host public key with type #hostKeyAlgo"() {
given:
File knownHosts = Files.createTempFile("known_hosts", "").toFile()
knownHosts.deleteOnExit()
and:
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub")
String knownHostsFileContents = "" +
"@cert-authority $SERVER_IP ${caPubKey.text}" +
"\n@cert-authority [$SERVER_IP]:$DOCKER_PORT ${caPubKey.text}"
knownHosts.write(knownHostsFileContents)
and:
def config = new DefaultConfig()
config.keyAlgorithms = config.keyAlgorithms.stream()
.filter { it.name == hostKeyAlgo }
.collect(Collectors.toList())
SSHClient sshClient = new SSHClient(config)
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(knownHosts))
sshClient.connect(SERVER_IP, DOCKER_PORT)
when:
sshClient.authPassword("sshj", "ultrapassword")
then:
sshClient.authenticated
and:
knownHosts.getText() == knownHostsFileContents
where:
hostKeyAlgo << [
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ssh-ed25519-cert-v01@openssh.com",
"ssh-rsa-cert-v01@openssh.com",
]
}
} }

View File

@@ -15,18 +15,25 @@
*/ */
package com.hierynomus.sshj.signature package com.hierynomus.sshj.signature
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import net.schmizz.sshj.DefaultConfig import com.hierynomus.sshj.SshdContainer
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class RsaSignatureClientKeySpec extends IntegrationBaseSpec { class RsaSignatureClientKeySpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should correctly connect using publickey auth with RSA key with signature"() { def "should correctly connect using publickey auth with RSA key with signature"() {
given: given:
def client = getConnectedClient(new DefaultConfig()) def client = sshd.getConnectedClient()
when: when:
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/id_rsa2") client.authPublickey(IntegrationTestUtil.USERNAME, "src/itest/resources/keyfiles/id_rsa2")
then: then:
client.authenticated client.authenticated

View File

@@ -15,22 +15,29 @@
*/ */
package com.hierynomus.sshj.signature package com.hierynomus.sshj.signature
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import com.hierynomus.sshj.key.KeyAlgorithms import com.hierynomus.sshj.key.KeyAlgorithms
import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class SignatureSpec extends IntegrationBaseSpec { class SignatureSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should correctly connect with #sig Signature"() { def "should correctly connect with #sig Signature"() {
given: given:
def cfg = new DefaultConfig() def cfg = new DefaultConfig()
cfg.setKeyAlgorithms(Collections.singletonList(sigFactory)) cfg.setKeyAlgorithms(Collections.singletonList(sigFactory))
def client = getConnectedClient(cfg) def client = sshd.getConnectedClient(cfg)
when: when:
client.authPublickey(USERNAME, KEYFILE) client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then: then:
client.authenticated client.authenticated

View File

@@ -15,21 +15,28 @@
*/ */
package com.hierynomus.sshj.transport.cipher package com.hierynomus.sshj.transport.cipher
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class CipherSpec extends IntegrationBaseSpec { class CipherSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should correctly connect with #cipher Cipher"() { def "should correctly connect with #cipher Cipher"() {
given: given:
def cfg = new DefaultConfig() def cfg = new DefaultConfig()
cfg.setCipherFactories(cipherFactory) cfg.setCipherFactories(cipherFactory)
def client = getConnectedClient(cfg) def client = sshd.getConnectedClient(cfg)
when: when:
client.authPublickey(USERNAME, KEYFILE) client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then: then:
client.authenticated client.authenticated

View File

@@ -15,29 +15,32 @@
*/ */
package com.hierynomus.sshj.transport.kex package com.hierynomus.sshj.transport.kex
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.transport.mac.Macs import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.transport.kex.Curve25519DH
import net.schmizz.sshj.transport.kex.Curve25519SHA256 import net.schmizz.sshj.transport.kex.Curve25519SHA256
import net.schmizz.sshj.transport.kex.DH
import net.schmizz.sshj.transport.kex.DHGexSHA1 import net.schmizz.sshj.transport.kex.DHGexSHA1
import net.schmizz.sshj.transport.kex.DHGexSHA256 import net.schmizz.sshj.transport.kex.DHGexSHA256
import net.schmizz.sshj.transport.kex.ECDH
import net.schmizz.sshj.transport.kex.ECDHNistP import net.schmizz.sshj.transport.kex.ECDHNistP
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class KexSpec extends IntegrationBaseSpec { class KexSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should correctly connect with #kex Key Exchange"() { def "should correctly connect with #kex Key Exchange"() {
given: given:
def cfg = new DefaultConfig() def cfg = new DefaultConfig()
cfg.setKeyExchangeFactories(kexFactory) cfg.setKeyExchangeFactories(kexFactory)
def client = getConnectedClient(cfg) def client = sshd.getConnectedClient(cfg)
when: when:
client.authPublickey(USERNAME, KEYFILE) client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then: then:
client.authenticated client.authenticated

View File

@@ -15,21 +15,28 @@
*/ */
package com.hierynomus.sshj.transport.mac package com.hierynomus.sshj.transport.mac
import com.hierynomus.sshj.IntegrationBaseSpec import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
class MacSpec extends IntegrationBaseSpec { class MacSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll @Unroll
def "should correctly connect with #mac MAC"() { def "should correctly connect with #mac MAC"() {
given: given:
def cfg = new DefaultConfig() def cfg = new DefaultConfig()
cfg.setMACFactories(macFactory) cfg.setMACFactories(macFactory)
def client = getConnectedClient(cfg) def client = sshd.getConnectedClient(cfg)
when: when:
client.authPublickey(USERNAME, KEYFILE) client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then: then:
client.authenticated client.authenticated
@@ -47,10 +54,10 @@ class MacSpec extends IntegrationBaseSpec {
given: given:
def cfg = new DefaultConfig() def cfg = new DefaultConfig()
cfg.setMACFactories(macFactory) cfg.setMACFactories(macFactory)
def client = getConnectedClient(cfg) def client = sshd.getConnectedClient(cfg)
when: when:
client.authPublickey(USERNAME, KEYFILE) client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then: then:
client.authenticated client.authenticated

View File

@@ -0,0 +1,3 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABDfrL8SxDyrkNlsJdAmc7Z0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBiAAAAkLVqaDIfs+sPBNyy7ytdLnP/xH7Nt5FIXx3Upw6wKuMGdBzbFQLcvu60Le+SFP3uUfXE8TcHramXbH0n+UBMW6raCAKOkHUU1BtrKxPG1eKU/LBx3Bk5FxyKm7fo0XsCUmqSVK25EHOJfYq1QwIbWICkvQUNu+2Hg8/MQKoFJMentI+GqjdaG76f6Wf+aj9UwA==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -27,8 +27,6 @@ import java.util.List;
public class KeyAlgorithms { public class KeyAlgorithms {
public static List<String> SSH_RSA_SHA2_ALGORITHMS = Arrays.asList("rsa-sha2-512", "rsa-sha2-256");
public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); } public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); }
public static Factory SSHRSACertV01() { return new Factory("ssh-rsa-cert-v01@openssh.com", new SignatureRSA.FactoryCERT(), KeyType.RSA_CERT); } public static Factory SSHRSACertV01() { return new Factory("ssh-rsa-cert-v01@openssh.com", new SignatureRSA.FactoryCERT(), KeyType.RSA_CERT); }
public static Factory RSASHA256() { return new Factory("rsa-sha2-256", new SignatureRSA.FactoryRSASHA256(), KeyType.RSA); } public static Factory RSASHA256() { return new Factory("rsa-sha2-256", new SignatureRSA.FactoryRSASHA256(), KeyType.RSA); }
@@ -61,6 +59,10 @@ public class KeyAlgorithms {
return algorithmName; return algorithmName;
} }
public KeyType getKeyType() {
return keyType;
}
@Override @Override
public KeyAlgorithm create() { public KeyAlgorithm create() {
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType); return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);

View File

@@ -177,11 +177,11 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
Arrays.fill(charBuffer.array(), '\u0000'); Arrays.fill(charBuffer.array(), '\u0000');
Arrays.fill(byteBuffer.array(), (byte) 0); Arrays.fill(byteBuffer.array(), (byte) 0);
} }
byte[] keyiv = new byte[48]; byte[] keyiv = new byte[cipher.getIVSize()+ cipher.getBlockSize()];
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv); new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
Arrays.fill(passphrase, (byte) 0); Arrays.fill(passphrase, (byte) 0);
byte[] key = Arrays.copyOfRange(keyiv, 0, 32); byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize());
byte[] iv = Arrays.copyOfRange(keyiv, 32, 48); byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize());
cipher.init(Cipher.Mode.Decrypt, key, iv); cipher.init(Cipher.Mode.Decrypt, key, iv);
} else { } else {
throw new IllegalStateException("No support for KDF '" + kdfName + "'."); throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
@@ -193,6 +193,8 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
return BlockCiphers.AES256CTR().create(); return BlockCiphers.AES256CTR().create();
} else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) { } else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) {
return BlockCiphers.AES256CBC().create(); return BlockCiphers.AES256CBC().create();
} else if (cipherName.equals(BlockCiphers.AES128CBC().getName())) {
return BlockCiphers.AES128CBC().create();
} }
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format"); throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
} }
@@ -216,6 +218,9 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
while (line != null && !line.startsWith(BEGIN)) { while (line != null && !line.startsWith(BEGIN)) {
line = reader.readLine(); line = reader.readLine();
} }
if (line == null) {
return false;
}
line = line.substring(BEGIN.length()); line = line.substring(BEGIN.length());
return line.startsWith(OPENSSH_PRIVATE_KEY); return line.startsWith(OPENSSH_PRIVATE_KEY);
} }

View File

@@ -176,6 +176,7 @@ public class Promise<V, T extends Throwable> {
} }
return val; return val;
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw chainer.chain(ie); throw chainer.chain(ie);
} finally { } finally {
lock.unlock(); lock.unlock();

View File

@@ -24,7 +24,7 @@ final class Heartbeater
extends KeepAlive { extends KeepAlive {
Heartbeater(ConnectionImpl conn) { Heartbeater(ConnectionImpl conn) {
super(conn, "heartbeater"); super(conn, "sshj-Heartbeater");
} }
@Override @Override

View File

@@ -20,6 +20,8 @@ import net.schmizz.sshj.connection.ConnectionImpl;
import net.schmizz.sshj.transport.TransportException; import net.schmizz.sshj.transport.TransportException;
import org.slf4j.Logger; import org.slf4j.Logger;
import java.util.concurrent.TimeUnit;
public abstract class KeepAlive extends Thread { public abstract class KeepAlive extends Thread {
protected final Logger log; protected final Logger log;
protected final ConnectionImpl conn; protected final ConnectionImpl conn;
@@ -33,40 +35,49 @@ public abstract class KeepAlive extends Thread {
setDaemon(true); setDaemon(true);
} }
/**
* KeepAlive enabled based on KeepAlive interval
*
* @return Enabled when KeepInterval is greater than 0
*/
public boolean isEnabled() {
return keepAliveInterval > 0;
}
/**
* Get KeepAlive interval in seconds
*
* @return KeepAlive interval in seconds defaults to 0
*/
public synchronized int getKeepAliveInterval() { public synchronized int getKeepAliveInterval() {
return keepAliveInterval; return keepAliveInterval;
} }
/**
* Set KeepAlive interval in seconds
*
* @param keepAliveInterval KeepAlive interval in seconds
*/
public synchronized void setKeepAliveInterval(int keepAliveInterval) { public synchronized void setKeepAliveInterval(int keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval; this.keepAliveInterval = keepAliveInterval;
if (keepAliveInterval > 0 && getState() == State.NEW) {
start();
}
notify();
}
synchronized protected int getPositiveInterval()
throws InterruptedException {
while (keepAliveInterval <= 0) {
wait();
}
return keepAliveInterval;
} }
@Override @Override
public void run() { public void run() {
log.debug("Starting {}, sending keep-alive every {} seconds", getClass().getSimpleName(), keepAliveInterval); log.debug("{} Started with interval [{} seconds]", getClass().getSimpleName(), keepAliveInterval);
try { try {
while (!isInterrupted()) { while (!isInterrupted()) {
final int hi = getPositiveInterval(); final int interval = getKeepAliveInterval();
if (conn.getTransport().isRunning()) { if (conn.getTransport().isRunning()) {
log.debug("Sending keep-alive since {} seconds elapsed", hi); log.debug("{} Sending after interval [{} seconds]", getClass().getSimpleName(), interval);
doKeepAlive(); doKeepAlive();
} }
Thread.sleep(hi * 1000); TimeUnit.SECONDS.sleep(interval);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Interrupt signal may be catched when sleeping. // this is almost certainly a planned interruption, but even so, no harm in setting the interrupt flag
Thread.currentThread().interrupt();
log.trace("{} Interrupted while sleeping", getClass().getSimpleName());
} catch (Exception e) { } catch (Exception e) {
// If we weren't interrupted, kill the transport, then this exception was unexpected. // If we weren't interrupted, kill the transport, then this exception was unexpected.
// Else we're in shutdown-mode already, so don't forcibly kill the transport. // Else we're in shutdown-mode already, so don't forcibly kill the transport.
@@ -74,9 +85,7 @@ public abstract class KeepAlive extends Thread {
conn.getTransport().die(e); conn.getTransport().die(e);
} }
} }
log.debug("{} Stopped", getClass().getSimpleName());
log.debug("Stopping {}", getClass().getSimpleName());
} }
protected abstract void doKeepAlive() throws TransportException, ConnectionException; protected abstract void doKeepAlive() throws TransportException, ConnectionException;

View File

@@ -37,7 +37,7 @@ public class KeepAliveRunner extends KeepAlive {
new LinkedList<Promise<SSHPacket, ConnectionException>>(); new LinkedList<Promise<SSHPacket, ConnectionException>>();
KeepAliveRunner(ConnectionImpl conn) { KeepAliveRunner(ConnectionImpl conn) {
super(conn, "keep-alive"); super(conn, "sshj-KeepAliveRunner");
} }
synchronized public int getMaxAliveCount() { synchronized public int getMaxAliveCount() {

View File

@@ -26,6 +26,7 @@ import net.schmizz.sshj.transport.mac.MAC;
import net.schmizz.sshj.transport.random.Random; import net.schmizz.sshj.transport.random.Random;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -188,4 +189,30 @@ public class ConfigImpl
public void setVerifyHostKeyCertificates(boolean value) { public void setVerifyHostKeyCertificates(boolean value) {
verifyHostKeyCertificates = value; verifyHostKeyCertificates = value;
} }
/**
* Modern servers neglect the key algorithm ssh-rsa. OpenSSH 8.8 even dropped its support by default in favour
* of rsa-sha2-*. However, there are legacy servers like Apache SSHD that don't support the newer replacements
* for ssh-rsa.
*
* If ssh-rsa factory is in {@link #getKeyAlgorithms()}, this methods makes ssh-rsa key algorithm more preferred
* than any of rsa-sha2-*. Otherwise, nothing happens.
*/
public void prioritizeSshRsaKeyAlgorithm() {
List<Factory.Named<KeyAlgorithm>> keyAlgorithms = getKeyAlgorithms();
for (int sshRsaIndex = 0; sshRsaIndex < keyAlgorithms.size(); ++ sshRsaIndex) {
if ("ssh-rsa".equals(keyAlgorithms.get(sshRsaIndex).getName())) {
for (int i = 0; i < sshRsaIndex; ++i) {
final String algo = keyAlgorithms.get(i).getName();
if ("rsa-sha2-256".equals(algo) || "rsa-sha2-512".equals(algo)) {
keyAlgorithms = new ArrayList<>(keyAlgorithms);
keyAlgorithms.add(i, keyAlgorithms.remove(sshRsaIndex));
setKeyAlgorithms(keyAlgorithms);
break;
}
}
break;
}
}
}
} }

View File

@@ -36,11 +36,9 @@ import net.schmizz.sshj.transport.kex.Curve25519SHA256;
import net.schmizz.sshj.transport.kex.DHGexSHA1; import net.schmizz.sshj.transport.kex.DHGexSHA1;
import net.schmizz.sshj.transport.kex.DHGexSHA256; import net.schmizz.sshj.transport.kex.DHGexSHA256;
import net.schmizz.sshj.transport.kex.ECDHNistP; import net.schmizz.sshj.transport.kex.ECDHNistP;
import net.schmizz.sshj.transport.random.BouncyCastleRandom;
import net.schmizz.sshj.transport.random.JCERandom; import net.schmizz.sshj.transport.random.JCERandom;
import net.schmizz.sshj.transport.random.SingletonRandomFactory; import net.schmizz.sshj.transport.random.SingletonRandomFactory;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile; import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -162,7 +160,6 @@ public class DefaultConfig
setFileKeyProviderFactories( setFileKeyProviderFactories(
new OpenSSHKeyV1KeyFile.Factory(), new OpenSSHKeyV1KeyFile.Factory(),
new PKCS8KeyFile.Factory(), new PKCS8KeyFile.Factory(),
new PKCS5KeyFile.Factory(),
new OpenSSHKeyFile.Factory(), new OpenSSHKeyFile.Factory(),
new PuTTYKeyFile.Factory()); new PuTTYKeyFile.Factory());
} }

View File

@@ -15,6 +15,8 @@
*/ */
package net.schmizz.sshj; package net.schmizz.sshj;
import net.schmizz.keepalive.KeepAlive;
import com.hierynomus.sshj.common.ThreadNameProvider;
import net.schmizz.sshj.common.*; import net.schmizz.sshj.common.*;
import net.schmizz.sshj.connection.Connection; import net.schmizz.sshj.connection.Connection;
import net.schmizz.sshj.connection.ConnectionException; import net.schmizz.sshj.connection.ConnectionException;
@@ -38,6 +40,7 @@ import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
import net.schmizz.sshj.transport.verification.FingerprintVerifier; import net.schmizz.sshj.transport.verification.FingerprintVerifier;
import net.schmizz.sshj.transport.verification.HostKeyVerifier; import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts; import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import net.schmizz.sshj.userauth.AuthResult;
import net.schmizz.sshj.userauth.UserAuth; import net.schmizz.sshj.userauth.UserAuth;
import net.schmizz.sshj.userauth.UserAuthException; import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.UserAuthImpl; import net.schmizz.sshj.userauth.UserAuthImpl;
@@ -55,6 +58,7 @@ import javax.security.auth.login.LoginContext;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.KeyPair; import java.security.KeyPair;
@@ -215,13 +219,30 @@ public class SSHClient
throws UserAuthException, TransportException { throws UserAuthException, TransportException {
checkConnected(); checkConnected();
final Deque<UserAuthException> savedEx = new LinkedList<UserAuthException>(); final Deque<UserAuthException> savedEx = new LinkedList<UserAuthException>();
for (AuthMethod method: methods) { final List<AuthMethod> tried = new LinkedList<AuthMethod>();
for (Iterator<AuthMethod> it = methods.iterator(); it.hasNext();) {
AuthMethod method = it.next();
method.setLoggerFactory(loggerFactory); method.setLoggerFactory(loggerFactory);
try { try {
if (auth.authenticate(username, (Service) conn, method, trans.getTimeoutMs())) AuthResult result = auth.authenticate(username, (Service) conn, method, trans.getTimeoutMs());
if (result == AuthResult.SUCCESS) {
return; return;
} else if (result == AuthResult.PARTIAL) {
// Put all remaining methods in the tried list, so that we can try them for the second round of authentication
while (it.hasNext()) {
tried.add(it.next());
}
auth(username, tried);
return;
}
tried.add(method);
} catch (UserAuthException e) { } catch (UserAuthException e) {
savedEx.push(e); savedEx.push(e);
tried.add(method);
} }
} }
throw new UserAuthException("Exhausted available authentication methods", savedEx.peek()); throw new UserAuthException("Exhausted available authentication methods", savedEx.peek());
@@ -424,6 +445,7 @@ public class SSHClient
@Override @Override
public void disconnect() public void disconnect()
throws IOException { throws IOException {
conn.getKeepAlive().interrupt();
for (LocalPortForwarder forwarder : forwarders) { for (LocalPortForwarder forwarder : forwarders) {
try { try {
forwarder.close(); forwarder.close();
@@ -441,6 +463,16 @@ public class SSHClient
return conn; return conn;
} }
/**
* Get Remote Socket Address from Transport
*
* @return Remote Socket Address or null when not connected
*/
@Override
public InetSocketAddress getRemoteSocketAddress() {
return trans.getRemoteSocketAddress();
}
/** /**
* Returns the character set used to communicate with the remote machine for certain strings (like paths). * Returns the character set used to communicate with the remote machine for certain strings (like paths).
* *
@@ -537,7 +569,7 @@ public class SSHClient
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported: * Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
* <ul> * <ul>
* <li>PKCS8 (OpenSSH uses this format)</li> * <li>PKCS8 (OpenSSH uses this format)</li>
* <li>PKCS5</li> * <li>PEM-encoded PKCS1</li>
* <li>Putty keyfile</li> * <li>Putty keyfile</li>
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li> * <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
* </ul> * </ul>
@@ -791,7 +823,17 @@ public class SSHClient
throws IOException { throws IOException {
super.onConnect(); super.onConnect();
trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream()); trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream());
doKex(); final KeepAlive keepAliveThread = conn.getKeepAlive();
if (keepAliveThread.isEnabled()) {
ThreadNameProvider.setThreadName(conn.getKeepAlive(), trans);
keepAliveThread.start();
}
if (trans.isKeyExchangeRequired()) {
log.debug("Initiating Key Exchange for new connection");
doKex();
} else {
log.debug("Key Exchange already completed for new connection");
}
} }
/** /**

View File

@@ -15,8 +15,6 @@
*/ */
package net.schmizz.sshj; package net.schmizz.sshj;
import com.hierynomus.sshj.backport.JavaVersion;
import com.hierynomus.sshj.backport.Jdk7HttpProxySocket;
import net.schmizz.sshj.connection.channel.Channel; import net.schmizz.sshj.connection.channel.Channel;
import net.schmizz.sshj.connection.channel.direct.DirectConnection; import net.schmizz.sshj.connection.channel.direct.DirectConnection;
@@ -26,7 +24,6 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket; import java.net.Socket;
public abstract class SocketClient { public abstract class SocketClient {
@@ -57,73 +54,6 @@ public abstract class SocketClient {
return new InetSocketAddress(hostname, port); return new InetSocketAddress(hostname, port);
} }
/**
* Connect to a host via a proxy.
* @param hostname The host name to connect to.
* @param proxy The proxy to connect via.
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
*/
@Deprecated
public void connect(String hostname, Proxy proxy) throws IOException {
connect(hostname, defaultPort, proxy);
}
/**
* Connect to a host via a proxy.
* @param hostname The host name to connect to.
* @param port The port to connect to.
* @param proxy The proxy to connect via.
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
*/
@Deprecated
public void connect(String hostname, int port, Proxy proxy) throws IOException {
this.hostname = hostname;
this.port = port;
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
socket = new Jdk7HttpProxySocket(proxy);
} else {
socket = new Socket(proxy);
}
socket.connect(makeInetSocketAddress(hostname, port), connectTimeout);
onConnect();
}
/**
* Connect to a host via a proxy.
* @param host The host address to connect to.
* @param proxy The proxy to connect via.
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
*/
@Deprecated
public void connect(InetAddress host, Proxy proxy) throws IOException {
connect(host, defaultPort, proxy);
}
/**
* Connect to a host via a proxy.
* @param host The host address to connect to.
* @param port The port to connect to.
* @param proxy The proxy to connect via.
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
*/
@Deprecated
public void connect(InetAddress host, int port, Proxy proxy) throws IOException {
this.port = port;
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
socket = new Jdk7HttpProxySocket(proxy);
} else {
socket = new Socket(proxy);
}
socket.connect(new InetSocketAddress(host, port), connectTimeout);
onConnect();
}
public void connect(String hostname) throws IOException { public void connect(String hostname) throws IOException {
connect(hostname, defaultPort); connect(hostname, defaultPort);
} }

View File

@@ -145,8 +145,14 @@ public class StreamCopier {
final double sizeKiB = count / 1024.0; final double sizeKiB = count / 1024.0;
log.debug(String.format("%1$,.1f KiB transferred in %2$,.1f seconds (%3$,.2f KiB/s)", sizeKiB, timeSeconds, (sizeKiB / timeSeconds))); log.debug(String.format("%1$,.1f KiB transferred in %2$,.1f seconds (%3$,.2f KiB/s)", sizeKiB, timeSeconds, (sizeKiB / timeSeconds)));
if (length != -1 && read == -1) // Did we encounter EOF?
throw new IOException("Encountered EOF, could not transfer " + length + " bytes"); if (read == -1) {
// If InputStream was closed we should also close OutputStream
out.close();
if (length != -1)
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
}
return count; return count;
} }

View File

@@ -304,6 +304,25 @@ public abstract class AbstractChannel
} }
} }
// Prevent CHANNEL_CLOSE to be sent between isOpen and a Transport.write call in the runnable, otherwise
// a disconnect with a "packet referred to nonexistent channel" message can occur.
//
// This particularly happens when the transport.Reader thread passes an eof from the server to the
// ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the
// transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.
boolean whileOpen(TransportRunnable runnable) throws TransportException, ConnectionException {
openCloseLock.lock();
try {
if (isOpen()) {
runnable.run();
return true;
}
} finally {
openCloseLock.unlock();
}
return false;
}
private void gotChannelRequest(SSHPacket buf) private void gotChannelRequest(SSHPacket buf)
throws ConnectionException, TransportException { throws ConnectionException, TransportException {
final String reqType; final String reqType;
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
+ rwin + " >"; + rwin + " >";
} }
public interface TransportRunnable {
void run() throws TransportException, ConnectionException;
}
} }

View File

@@ -105,6 +105,7 @@ public final class ChannelInputStream
try { try {
buf.wait(); buf.wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw (IOException) new InterruptedIOException().initCause(e); throw (IOException) new InterruptedIOException().initCause(e);
} }
} }

View File

@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/ */
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable { public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
private final Channel chan; private final AbstractChannel chan;
private final Transport trans; private final Transport trans;
private final Window.Remote win; private final Window.Remote win;
@@ -47,6 +47,12 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA); private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer(); private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
private final AbstractChannel.TransportRunnable packetWriteRunnable = new AbstractChannel.TransportRunnable() {
@Override
public void run() throws TransportException {
trans.write(packet);
}
};
DataBuffer() { DataBuffer() {
headerOffset = packet.rpos(); headerOffset = packet.rpos();
@@ -99,8 +105,9 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
if (leftOverBytes > 0) { if (leftOverBytes > 0) {
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes); leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
} }
if (!chan.whileOpen(packetWriteRunnable)) {
trans.write(packet); throwStreamClosed();
}
win.consume(writeNow); win.consume(writeNow);
packet.rpos(headerOffset); packet.rpos(headerOffset);
@@ -119,7 +126,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
} }
public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) { public ChannelOutputStream(AbstractChannel chan, Transport trans, Window.Remote win) {
this.chan = chan; this.chan = chan;
this.trans = trans; this.trans = trans;
this.win = win; this.win = win;
@@ -157,7 +164,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
if (error != null) { if (error != null) {
throw error; throw error;
} else { } else {
throw new ConnectionException("Stream closed"); throwStreamClosed();
} }
} }
} }
@@ -165,9 +172,14 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
@Override @Override
public synchronized void close() throws IOException { public synchronized void close() throws IOException {
// Not closed yet, and underlying channel is open to flush the data to. // Not closed yet, and underlying channel is open to flush the data to.
if (!closed.getAndSet(true) && chan.isOpen()) { if (!closed.getAndSet(true)) {
buffer.flush(false); chan.whileOpen(new AbstractChannel.TransportRunnable() {
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient())); @Override
public void run() throws TransportException, ConnectionException {
buffer.flush(false);
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
}
});
} }
} }
@@ -188,4 +200,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
return "< ChannelOutputStream for Channel #" + chan.getID() + " >"; return "< ChannelOutputStream for Channel #" + chan.getID() + " >";
} }
private static void throwStreamClosed() throws ConnectionException {
throw new ConnectionException("Stream closed");
}
} }

View File

@@ -93,6 +93,7 @@ public abstract class Window {
throw new ConnectionException("Timeout when trying to expand the window size"); throw new ConnectionException("Timeout when trying to expand the window size");
} }
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new ConnectionException(ie); throw new ConnectionException(ie);
} }
} }

View File

@@ -15,10 +15,11 @@
*/ */
package net.schmizz.sshj.connection.channel.direct; package net.schmizz.sshj.connection.channel.direct;
import com.hierynomus.sshj.common.RemoteAddressProvider;
import net.schmizz.sshj.common.SSHException; import net.schmizz.sshj.common.SSHException;
/** A factory interface for creating SSH {@link Session session channels}. */ /** A factory interface for creating SSH {@link Session session channels}. */
public interface SessionFactory { public interface SessionFactory extends RemoteAddressProvider {
/** /**
* Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String) * Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String)
@@ -27,7 +28,7 @@ public interface SessionFactory {
* *
* @return the opened {@code session} channel * @return the opened {@code session} channel
* *
* @throws SSHException * @throws SSHException Thrown on session initialization failures
* @see Session * @see Session
*/ */
Session startSession() Session startSession()

View File

@@ -142,6 +142,10 @@ public class RemotePortForwarder
// Listen on all IPv4 // Listen on all IPv4
return true; return true;
} }
if ("0.0.0.0".equals(address) && "0:0:0:0:0:0:0:0".equals(channelForward.address)) {
// Handle IPv4 requests on IPv6 channel forward
return true;
}
return false; return false;
} }

View File

@@ -41,7 +41,7 @@ public class PacketReader extends Thread {
this.engine = engine; this.engine = engine;
log = engine.getLoggerFactory().getLogger(getClass()); log = engine.getLoggerFactory().getLogger(getClass());
this.in = engine.getSubsystem().getInputStream(); this.in = engine.getSubsystem().getInputStream();
setName("sftp reader"); setName("sshj-PacketReader");
setDaemon(true); setDaemon(true);
} }

View File

@@ -23,6 +23,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -220,49 +222,104 @@ public class RemoteFile
public class ReadAheadRemoteFileInputStream public class ReadAheadRemoteFileInputStream
extends InputStream { extends InputStream {
private class UnconfirmedRead {
private final long offset;
private final Promise<Response, SFTPException> promise;
private final int length;
private UnconfirmedRead(long offset, int length, Promise<Response, SFTPException> promise) {
this.offset = offset;
this.length = length;
this.promise = promise;
}
UnconfirmedRead(long offset, int length) throws IOException {
this(offset, length, RemoteFile.this.asyncRead(offset, length));
}
public long getOffset() {
return offset;
}
public Promise<Response, SFTPException> getPromise() {
return promise;
}
public int getLength() {
return length;
}
}
private final byte[] b = new byte[1]; private final byte[] b = new byte[1];
private final int maxUnconfirmedReads; private final int maxUnconfirmedReads;
private final Queue<Promise<Response, SFTPException>> unconfirmedReads = new LinkedList<Promise<Response, SFTPException>>(); private final long readAheadLimit;
private final Queue<Long> unconfirmedReadOffsets = new LinkedList<Long>(); private final Deque<UnconfirmedRead> unconfirmedReads = new ArrayDeque<>();
private long requestOffset; private long currentOffset;
private long responseOffset; private int maxReadLength = Integer.MAX_VALUE;
private boolean eof; private boolean eof;
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) { public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
assert 0 <= maxUnconfirmedReads; this(maxUnconfirmedReads, 0L);
}
this.maxUnconfirmedReads = maxUnconfirmedReads;
/**
*
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
* @param fileOffset Initial offset in file to read from
*/
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
this(maxUnconfirmedReads, fileOffset, -1L);
} }
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) { /**
*
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
* @param fileOffset Initial offset in file to read from
* @param readAheadLimit Read ahead is disabled after this limit has been reached
*/
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset, long readAheadLimit) {
assert 0 <= maxUnconfirmedReads; assert 0 <= maxUnconfirmedReads;
assert 0 <= fileOffset; assert 0 <= fileOffset;
this.maxUnconfirmedReads = maxUnconfirmedReads; this.maxUnconfirmedReads = maxUnconfirmedReads;
this.requestOffset = this.responseOffset = fileOffset; this.currentOffset = fileOffset;
this.readAheadLimit = readAheadLimit > 0 ? fileOffset + readAheadLimit : Long.MAX_VALUE;
} }
private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]); private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]);
private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException { private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException {
if (unconfirmedReads.size() <= 0) { final UnconfirmedRead unconfirmedRead = unconfirmedReads.peek();
if (unconfirmedRead == null || !blocking && !unconfirmedRead.getPromise().isDelivered()) {
return false; return false;
} }
unconfirmedReads.remove(unconfirmedRead);
if (!blocking && !unconfirmedReads.peek().isDelivered()) { final Response res = unconfirmedRead.promise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
return false;
}
unconfirmedReadOffsets.remove();
final Response res = unconfirmedReads.remove().retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
switch (res.getType()) { switch (res.getType()) {
case DATA: case DATA:
int recvLen = res.readUInt32AsInt(); int recvLen = res.readUInt32AsInt();
responseOffset += recvLen; if (unconfirmedRead.offset == currentOffset) {
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen); currentOffset += recvLen;
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
if (recvLen < unconfirmedRead.length) {
// The server returned a packet smaller than the client had requested.
// It can be caused by at least one of the following:
// * The file has been read fully. Then, few futile read requests can be sent during
// the next read(), but the file will be downloaded correctly anyway.
// * The server shapes the request length. Then, the read window will be adjusted,
// and all further read-ahead requests won't be shaped.
// * The file on the server is not a regular file, it is something like fifo.
// Then, the window will shrink, and the client will start reading the file slower than it
// hypothetically can. It must be a rare case, and it is not worth implementing a sort of
// congestion control algorithm here.
maxReadLength = recvLen;
unconfirmedReads.clear();
}
}
break; break;
case STATUS: case STATUS:
@@ -290,40 +347,31 @@ public class RemoteFile
// we also need to go here for len <= 0, because pending may be at // we also need to go here for len <= 0, because pending may be at
// EOF in which case it would return -1 instead of 0 // EOF in which case it would return -1 instead of 0
long requestOffset;
if (unconfirmedReads.isEmpty()) {
requestOffset = currentOffset;
}
else {
final UnconfirmedRead lastRequest = unconfirmedReads.getLast();
requestOffset = lastRequest.offset + lastRequest.length;
}
while (unconfirmedReads.size() <= maxUnconfirmedReads) { while (unconfirmedReads.size() <= maxUnconfirmedReads) {
// Send read requests as long as there is no EOF and we have not reached the maximum parallelism // Send read requests as long as there is no EOF and we have not reached the maximum parallelism
int reqLen = Math.max(1024, len); // don't be shy! int reqLen = Math.min(Math.max(1024, len), maxReadLength);
unconfirmedReads.add(RemoteFile.this.asyncRead(requestOffset, reqLen)); if (readAheadLimit > requestOffset) {
unconfirmedReadOffsets.add(requestOffset); long remaining = readAheadLimit - requestOffset;
if (reqLen > remaining) {
reqLen = (int) remaining;
}
}
unconfirmedReads.add(new UnconfirmedRead(requestOffset, reqLen));
requestOffset += reqLen; requestOffset += reqLen;
if (requestOffset >= readAheadLimit) {
break;
}
} }
long nextOffset = unconfirmedReadOffsets.peek(); if (!retrieveUnconfirmedRead(true /*blocking*/)) {
if (responseOffset != nextOffset) {
// the server could not give us all the data we needed, so
// we try to fill the gap synchronously
assert responseOffset < nextOffset;
assert 0 < (nextOffset - responseOffset);
assert (nextOffset - responseOffset) <= Integer.MAX_VALUE;
byte[] buf = new byte[(int) (nextOffset - responseOffset)];
int recvLen = RemoteFile.this.read(responseOffset, buf, 0, buf.length);
if (recvLen < 0) {
eof = true;
return -1;
}
if (0 == recvLen) {
// avoid infinite loops
throw new SFTPException("Unexpected response size (0), bailing out");
}
responseOffset += recvLen;
pending = new ByteArrayInputStream(buf, 0, recvLen);
} else if (!retrieveUnconfirmedRead(true /*blocking*/)) {
// this may happen if we change prefetch strategy // this may happen if we change prefetch strategy
// currently, we should never get here... // currently, we should never get here...

View File

@@ -232,21 +232,41 @@ public class SFTPClient
throws IOException { throws IOException {
xfer.download(source, dest); xfer.download(source, dest);
} }
public void get(String source, String dest, long byteOffset)
throws IOException {
xfer.download(source, dest, byteOffset);
}
public void put(String source, String dest) public void put(String source, String dest)
throws IOException { throws IOException {
xfer.upload(source, dest); xfer.upload(source, dest);
} }
public void put(String source, String dest, long byteOffset)
throws IOException {
xfer.upload(source, dest, byteOffset);
}
public void get(String source, LocalDestFile dest) public void get(String source, LocalDestFile dest)
throws IOException { throws IOException {
xfer.download(source, dest); xfer.download(source, dest);
} }
public void get(String source, LocalDestFile dest, long byteOffset)
throws IOException {
xfer.download(source, dest, byteOffset);
}
public void put(LocalSourceFile source, String dest) public void put(LocalSourceFile source, String dest)
throws IOException { throws IOException {
xfer.upload(source, dest); xfer.upload(source, dest);
} }
public void put(LocalSourceFile source, String dest, long byteOffset)
throws IOException {
xfer.upload(source, dest, byteOffset);
}
@Override @Override
public void close() public void close()

View File

@@ -15,6 +15,7 @@
*/ */
package net.schmizz.sshj.sftp; package net.schmizz.sshj.sftp;
import com.hierynomus.sshj.common.ThreadNameProvider;
import net.schmizz.concurrent.Promise; import net.schmizz.concurrent.Promise;
import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.LoggerFactory; import net.schmizz.sshj.common.LoggerFactory;
@@ -68,6 +69,7 @@ public class SFTPEngine
sub = session.startSubsystem("sftp"); sub = session.startSubsystem("sftp");
out = sub.getOutputStream(); out = sub.getOutputStream();
reader = new PacketReader(this); reader = new PacketReader(this);
ThreadNameProvider.setThreadName(reader, ssh);
pathHelper = new PathHelper(new PathHelper.Canonicalizer() { pathHelper = new PathHelper(new PathHelper.Canonicalizer() {
@Override @Override
public String canonicalize(String path) public String canonicalize(String path)
@@ -235,14 +237,17 @@ public class SFTPEngine
if (operativeVersion < 1) if (operativeVersion < 1)
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion); throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
long renameFlagMask = 0L; final Request request = newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset());
for (RenameFlags flag : flags) { // SFTP Version 5 introduced rename flags according to Section 6.5 of the specification
renameFlagMask = renameFlagMask | flag.longValue(); if (operativeVersion >= 5) {
long renameFlagMask = 0L;
for (RenameFlags flag : flags) {
renameFlagMask = renameFlagMask | flag.longValue();
}
request.putUInt32(renameFlagMask);
} }
doRequest( doRequest(request).ensureStatusPacketIsOK();
newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset()).putUInt32(renameFlagMask)
).ensureStatusPacketIsOK();
} }
public String canonicalize(String path) public String canonicalize(String path)

View File

@@ -50,25 +50,47 @@ public class SFTPFileTransfer
@Override @Override
public void upload(String source, String dest) public void upload(String source, String dest)
throws IOException { throws IOException {
upload(new FileSystemFile(source), dest); upload(source, dest, 0);
}
@Override
public void upload(String source, String dest, long byteOffset)
throws IOException {
upload(new FileSystemFile(source), dest, byteOffset);
} }
@Override @Override
public void download(String source, String dest) public void download(String source, String dest)
throws IOException { throws IOException {
download(source, new FileSystemFile(dest)); download(source, dest, 0);
}
@Override
public void download(String source, String dest, long byteOffset)
throws IOException {
download(source, new FileSystemFile(dest), byteOffset);
} }
@Override @Override
public void upload(LocalSourceFile localFile, String remotePath) throws IOException { public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
new Uploader(localFile, remotePath).upload(getTransferListener()); upload(localFile, remotePath, 0);
}
@Override
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException {
new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset);
} }
@Override @Override
public void download(String source, LocalDestFile dest) throws IOException { public void download(String source, LocalDestFile dest) throws IOException {
download(source, dest, 0);
}
@Override
public void download(String source, LocalDestFile dest, long byteOffset) throws IOException {
final PathComponents pathComponents = engine.getPathHelper().getComponents(source); final PathComponents pathComponents = engine.getPathHelper().getComponents(source);
final FileAttributes attributes = engine.stat(source); final FileAttributes attributes = engine.stat(source);
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest); new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest, byteOffset);
} }
public void setUploadFilter(LocalFileFilter uploadFilter) { public void setUploadFilter(LocalFileFilter uploadFilter) {
@@ -92,7 +114,8 @@ public class SFTPFileTransfer
@SuppressWarnings("PMD.MissingBreakInSwitch") @SuppressWarnings("PMD.MissingBreakInSwitch")
private void download(final TransferListener listener, private void download(final TransferListener listener,
final RemoteResourceInfo remote, final RemoteResourceInfo remote,
final LocalDestFile local) throws IOException { final LocalDestFile local,
final long byteOffset) throws IOException {
final LocalDestFile adjustedFile; final LocalDestFile adjustedFile;
switch (remote.getAttributes().getType()) { switch (remote.getAttributes().getType()) {
case DIRECTORY: case DIRECTORY:
@@ -101,8 +124,9 @@ public class SFTPFileTransfer
case UNKNOWN: case UNKNOWN:
log.warn("Server did not supply information about the type of file at `{}` " + log.warn("Server did not supply information about the type of file at `{}` " +
"-- assuming it is a regular file!", remote.getPath()); "-- assuming it is a regular file!", remote.getPath());
// fall through
case REGULAR: case REGULAR:
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local); adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local, byteOffset);
break; break;
default: default:
throw new IOException(remote + " is not a regular file or directory"); throw new IOException(remote + " is not a regular file or directory");
@@ -119,7 +143,7 @@ public class SFTPFileTransfer
final RemoteDirectory rd = engine.openDir(remote.getPath()); final RemoteDirectory rd = engine.openDir(remote.getPath());
try { try {
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter())) for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
download(listener, rri, adjusted.getChild(rri.getName())); download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files
} finally { } finally {
rd.close(); rd.close();
} }
@@ -128,13 +152,15 @@ public class SFTPFileTransfer
private LocalDestFile downloadFile(final StreamCopier.Listener listener, private LocalDestFile downloadFile(final StreamCopier.Listener listener,
final RemoteResourceInfo remote, final RemoteResourceInfo remote,
final LocalDestFile local) final LocalDestFile local,
final long byteOffset)
throws IOException { throws IOException {
final LocalDestFile adjusted = local.getTargetFile(remote.getName()); final LocalDestFile adjusted = local.getTargetFile(remote.getName());
final RemoteFile rf = engine.open(remote.getPath()); final RemoteFile rf = engine.open(remote.getPath());
try { try {
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16); log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
final OutputStream os = adjusted.getOutputStream(); final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
final OutputStream os = adjusted.getOutputStream(byteOffset != 0);
try { try {
new StreamCopier(rfis, os, engine.getLoggerFactory()) new StreamCopier(rfis, os, engine.getLoggerFactory())
.bufSize(engine.getSubsystem().getLocalMaxPacketSize()) .bufSize(engine.getSubsystem().getLocalMaxPacketSize())
@@ -173,17 +199,17 @@ public class SFTPFileTransfer
this.remote = remote; this.remote = remote;
} }
private void upload(final TransferListener listener) throws IOException { private void upload(final TransferListener listener, long byteOffset) throws IOException {
if (source.isDirectory()) { if (source.isDirectory()) {
makeDirIfNotExists(remote); // Ensure that the directory exists makeDirIfNotExists(remote); // Ensure that the directory exists
uploadDir(listener.directory(source.getName()), source, remote); uploadDir(listener.directory(source.getName()), source, remote);
setAttributes(source, remote); setAttributes(source, remote);
} else if (source.isFile() && isDirectory(remote)) { } else if (source.isFile() && isDirectory(remote)) {
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName()); String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote); uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote, byteOffset);
setAttributes(source, adjustedRemote); setAttributes(source, adjustedRemote);
} else if (source.isFile()) { } else if (source.isFile()) {
uploadFile(listener.file(source.getName(), source.getLength()), source, remote); uploadFile(listener.file(source.getName(), source.getLength()), source, remote, byteOffset);
setAttributes(source, remote); setAttributes(source, remote);
} else { } else {
throw new IOException(source + " is not a file or directory"); throw new IOException(source + " is not a file or directory");
@@ -192,13 +218,14 @@ public class SFTPFileTransfer
private void upload(final TransferListener listener, private void upload(final TransferListener listener,
final LocalSourceFile local, final LocalSourceFile local,
final String remote) final String remote,
final long byteOffset)
throws IOException { throws IOException {
final String adjustedPath; final String adjustedPath;
if (local.isDirectory()) { if (local.isDirectory()) {
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote); adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
} else if (local.isFile()) { } else if (local.isFile()) {
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote); adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote, byteOffset);
} else { } else {
throw new IOException(local + " is not a file or directory"); throw new IOException(local + " is not a file or directory");
} }
@@ -217,22 +244,34 @@ public class SFTPFileTransfer
throws IOException { throws IOException {
makeDirIfNotExists(remote); makeDirIfNotExists(remote);
for (LocalSourceFile f : local.getChildren(getUploadFilter())) for (LocalSourceFile f : local.getChildren(getUploadFilter()))
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName())); upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()), 0); // not supporting individual byte offsets for these files
return remote; return remote;
} }
private String uploadFile(final StreamCopier.Listener listener, private String uploadFile(final StreamCopier.Listener listener,
final LocalSourceFile local, final LocalSourceFile local,
final String remote) final String remote,
final long byteOffset)
throws IOException { throws IOException {
final String adjusted = prepareFile(local, remote); final String adjusted = prepareFile(local, remote, byteOffset);
RemoteFile rf = null; RemoteFile rf = null;
InputStream fis = null; InputStream fis = null;
RemoteFile.RemoteFileOutputStream rfos = null; RemoteFile.RemoteFileOutputStream rfos = null;
EnumSet<OpenMode> modes;
try { try {
rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC)); if (byteOffset == 0) {
// Starting at the beginning, overwrite/create
modes = EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC);
} else {
// Starting at some offset, append
modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND);
}
log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset);
rf = engine.open(adjusted, modes);
fis = local.getInputStream(); fis = local.getInputStream();
rfos = rf.new RemoteFileOutputStream(0, 16); fis.skip(byteOffset);
rfos = rf.new RemoteFileOutputStream(byteOffset, 16);
new StreamCopier(fis, rfos, engine.getLoggerFactory()) new StreamCopier(fis, rfos, engine.getLoggerFactory())
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead()) .bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
.keepFlushing(false) .keepFlushing(false)
@@ -294,7 +333,7 @@ public class SFTPFileTransfer
} }
} }
private String prepareFile(final LocalSourceFile local, final String remote) private String prepareFile(final LocalSourceFile local, final String remote, final long byteOffset)
throws IOException { throws IOException {
final FileAttributes attrs; final FileAttributes attrs;
try { try {
@@ -309,7 +348,7 @@ public class SFTPFileTransfer
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) { if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory"); throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
} else { } else {
log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType()); log.debug("probeFile: {} is a {} file that will be {}", remote, attrs.getMode().getType(), byteOffset > 0 ? "resumed" : "replaced");
return remote; return remote;
} }
} }

View File

@@ -243,7 +243,6 @@ final class KeyExchanger
negotiatedAlgs.getKeyExchangeAlgorithm()); negotiatedAlgs.getKeyExchangeAlgorithm());
transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(), transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(),
negotiatedAlgs.getSignatureAlgorithm())); negotiatedAlgs.getSignatureAlgorithm()));
transport.setRSASHA2Support(negotiatedAlgs.getRSASHA2Support());
try { try {
kex.init(transport, kex.init(transport,

View File

@@ -26,10 +26,8 @@ public final class NegotiatedAlgorithms {
private final String c2sComp; private final String c2sComp;
private final String s2cComp; private final String s2cComp;
private final boolean rsaSHA2Support;
NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC, NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC,
String c2sComp, String s2cComp, boolean rsaSHA2Support) { String c2sComp, String s2cComp) {
this.kex = kex; this.kex = kex;
this.sig = sig; this.sig = sig;
this.c2sCipher = c2sCipher; this.c2sCipher = c2sCipher;
@@ -38,7 +36,6 @@ public final class NegotiatedAlgorithms {
this.s2cMAC = s2cMAC; this.s2cMAC = s2cMAC;
this.c2sComp = c2sComp; this.c2sComp = c2sComp;
this.s2cComp = s2cComp; this.s2cComp = s2cComp;
this.rsaSHA2Support = rsaSHA2Support;
} }
public String getKeyExchangeAlgorithm() { public String getKeyExchangeAlgorithm() {
@@ -73,10 +70,6 @@ public final class NegotiatedAlgorithms {
return s2cComp; return s2cComp;
} }
public boolean getRSASHA2Support() {
return rsaSHA2Support;
}
@Override @Override
public String toString() { public String toString() {
return ("[ " + return ("[ " +
@@ -88,7 +81,6 @@ public final class NegotiatedAlgorithms {
"s2cMAC=" + s2cMAC + "; " + "s2cMAC=" + s2cMAC + "; " +
"c2sComp=" + c2sComp + "; " + "c2sComp=" + c2sComp + "; " +
"s2cComp=" + s2cComp + "; " + "s2cComp=" + s2cComp + "; " +
"rsaSHA2Support=" + rsaSHA2Support +
" ]"); " ]");
} }

View File

@@ -15,7 +15,6 @@
*/ */
package net.schmizz.sshj.transport; package net.schmizz.sshj.transport;
import com.hierynomus.sshj.key.KeyAlgorithms;
import net.schmizz.sshj.Config; import net.schmizz.sshj.Config;
import net.schmizz.sshj.common.Buffer; import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.Factory; import net.schmizz.sshj.common.Factory;
@@ -140,8 +139,8 @@ class Proposal {
firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(), firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(),
other.getClient2ServerCompressionAlgorithms()), other.getClient2ServerCompressionAlgorithms()),
firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(), firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(),
other.getServer2ClientCompressionAlgorithms()), other.getServer2ClientCompressionAlgorithms())
other.getHostKeyAlgorithms().containsAll(KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS)); );
} }
private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) { private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) {

View File

@@ -29,7 +29,7 @@ public final class Reader
public Reader(TransportImpl trans) { public Reader(TransportImpl trans) {
this.trans = trans; this.trans = trans;
log = trans.getConfig().getLoggerFactory().getLogger(getClass()); log = trans.getConfig().getLoggerFactory().getLogger(getClass());
setName("reader"); setName("sshj-Reader");
setDaemon(true); setDaemon(true);
} }

View File

@@ -15,6 +15,7 @@
*/ */
package net.schmizz.sshj.transport; package net.schmizz.sshj.transport;
import com.hierynomus.sshj.common.RemoteAddressProvider;
import com.hierynomus.sshj.key.KeyAlgorithm; import com.hierynomus.sshj.key.KeyAlgorithm;
import net.schmizz.sshj.Config; import net.schmizz.sshj.Config;
import net.schmizz.sshj.Service; import net.schmizz.sshj.Service;
@@ -27,11 +28,12 @@ import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** Transport layer of the SSH protocol. */ /** Transport layer of the SSH protocol. */
public interface Transport public interface Transport
extends SSHPacketHandler { extends SSHPacketHandler, RemoteAddressProvider {
/** /**
* Sets the host information and the streams to be used by this transport. Identification information is exchanged * Sets the host information and the streams to be used by this transport. Identification information is exchanged
@@ -69,6 +71,13 @@ public interface Transport
void doKex() void doKex()
throws TransportException; throws TransportException;
/**
* Is Key Exchange required based on current transport status
*
* @return Key Exchange required status
*/
boolean isKeyExchangeRequired();
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */ /** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
String getClientVersion(); String getClientVersion();
@@ -208,7 +217,7 @@ public interface Transport
/** /**
* Specify a {@code listener} that will be notified upon disconnection. * Specify a {@code listener} that will be notified upon disconnection.
* *
* @param listener * @param listener Disconnect Listener to be configured
*/ */
void setDisconnectListener(DisconnectListener listener); void setDisconnectListener(DisconnectListener listener);
@@ -223,5 +232,5 @@ public interface Transport
void die(Exception e); void die(Exception e);
KeyAlgorithm getHostKeyAlgorithm(); KeyAlgorithm getHostKeyAlgorithm();
KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException; List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException;
} }

View File

@@ -15,6 +15,7 @@
*/ */
package net.schmizz.sshj.transport; package net.schmizz.sshj.transport;
import com.hierynomus.sshj.common.ThreadNameProvider;
import com.hierynomus.sshj.key.KeyAlgorithm; import com.hierynomus.sshj.key.KeyAlgorithm;
import com.hierynomus.sshj.key.KeyAlgorithms; import com.hierynomus.sshj.key.KeyAlgorithms;
import com.hierynomus.sshj.transport.IdentificationStringParser; import com.hierynomus.sshj.transport.IdentificationStringParser;
@@ -22,7 +23,6 @@ import net.schmizz.concurrent.ErrorDeliveryUtil;
import net.schmizz.concurrent.Event; import net.schmizz.concurrent.Event;
import net.schmizz.sshj.AbstractService; import net.schmizz.sshj.AbstractService;
import net.schmizz.sshj.Config; import net.schmizz.sshj.Config;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.Service; import net.schmizz.sshj.Service;
import net.schmizz.sshj.common.*; import net.schmizz.sshj.common.*;
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier; import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
@@ -32,6 +32,8 @@ import org.slf4j.Logger;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -86,8 +88,6 @@ public final class TransportImpl
private KeyAlgorithm hostKeyAlgorithm; private KeyAlgorithm hostKeyAlgorithm;
private boolean rsaSHA2Support;
private final Event<TransportException> serviceAccept; private final Event<TransportException> serviceAccept;
private final Event<TransportException> close; private final Event<TransportException> close;
@@ -130,8 +130,8 @@ public final class TransportImpl
public TransportImpl(Config config) { public TransportImpl(Config config) {
this.config = config; this.config = config;
this.loggerFactory = config.getLoggerFactory(); this.loggerFactory = config.getLoggerFactory();
this.serviceAccept = new Event<TransportException>("service accept", TransportException.chainer, loggerFactory); this.serviceAccept = new Event<>("service accept", TransportException.chainer, loggerFactory);
this.close = new Event<TransportException>("transport close", TransportException.chainer, loggerFactory); this.close = new Event<>("transport close", TransportException.chainer, loggerFactory);
this.nullService = new NullService(this); this.nullService = new NullService(this);
this.service = nullService; this.service = nullService;
this.log = loggerFactory.getLogger(getClass()); this.log = loggerFactory.getLogger(getClass());
@@ -165,9 +165,20 @@ public final class TransportImpl
throw new TransportException(e); throw new TransportException(e);
} }
ThreadNameProvider.setThreadName(reader, this);
reader.start(); reader.start();
} }
/**
* Get Remote Socket Address using Connection Information
*
* @return Remote Socket Address or null when not connected
*/
@Override
public InetSocketAddress getRemoteSocketAddress() {
return connInfo == null ? null : new InetSocketAddress(getRemoteHost(), getRemotePort());
}
/** /**
* TransportImpl implements its own default DisconnectListener. * TransportImpl implements its own default DisconnectListener.
*/ */
@@ -211,7 +222,7 @@ public final class TransportImpl
* *
* @param buffer The buffer to read from. * @param buffer The buffer to read from.
* @return empty string if full ident string has not yet been received * @return empty string if full ident string has not yet been received
* @throws IOException * @throws IOException Thrown when protocol version is not supported
*/ */
private String readIdentification(Buffer.PlainBuffer buffer) private String readIdentification(Buffer.PlainBuffer buffer)
throws IOException { throws IOException {
@@ -243,6 +254,16 @@ public final class TransportImpl
kexer.startKex(true); kexer.startKex(true);
} }
/**
* Is Key Exchange required returns true when Key Exchange is not done and when Key Exchange is not ongoing
*
* @return Key Exchange required status
*/
@Override
public boolean isKeyExchangeRequired() {
return !kexer.isKexDone() && !kexer.isKexOngoing();
}
public boolean isKexDone() { public boolean isKexDone() {
return kexer.isKexDone(); return kexer.isKexDone();
} }
@@ -544,7 +565,7 @@ public final class TransportImpl
* Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly. * Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly.
* *
* @param packet The 'unimplemented' packet received * @param packet The 'unimplemented' packet received
* @throws TransportException * @throws TransportException Thrown when key exchange is ongoing
*/ */
private void gotUnimplemented(SSHPacket packet) private void gotUnimplemented(SSHPacket packet)
throws SSHException { throws SSHException {
@@ -626,21 +647,19 @@ public final class TransportImpl
return this.hostKeyAlgorithm; return this.hostKeyAlgorithm;
} }
public void setRSASHA2Support(boolean rsaSHA2Support) {
this.rsaSHA2Support = rsaSHA2Support;
}
@Override @Override
public KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException { public List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException {
if (keyType != KeyType.RSA || !rsaSHA2Support) {
return Factory.Named.Util.create(getConfig().getKeyAlgorithms(), keyType.toString());
}
List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms(); List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms();
List<KeyAlgorithm> available = new ArrayList<>();
if (factories != null) if (factories != null)
for (Factory.Named<KeyAlgorithm> f : factories) for (Factory.Named<KeyAlgorithm> f : factories)
if (f.getName().equals("ssh-rsa") || KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS.contains(f.getName())) if (
return f.create(); f instanceof KeyAlgorithms.Factory && ((KeyAlgorithms.Factory) f).getKeyType().equals(keyType)
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType); || !(f instanceof KeyAlgorithms.Factory) && f.getName().equals(keyType.toString())
)
available.add(f.create());
if (available.isEmpty())
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
return available;
} }
} }

View File

@@ -138,7 +138,19 @@ public class OpenSSHKnownHosts
for (KnownHostEntry e : entries) { for (KnownHostEntry e : entries) {
try { try {
if (e.appliesTo(adjustedHostname)) { if (e.appliesTo(adjustedHostname)) {
knownHostAlgorithms.add(e.getType().toString()); final KeyType type = e.getType();
if (e instanceof HostEntry && ((HostEntry) e).marker == Marker.CA_CERT) {
// Only the CA key type is known, but the type of the host key is not.
// Adding all supported types for keys with certificates.
for (final KeyType candidate : KeyType.values()) {
if (candidate.getParent() != null) {
knownHostAlgorithms.add(candidate.toString());
}
}
}
else {
knownHostAlgorithms.add(type.toString());
}
} }
} catch (IOException ioe) { } catch (IOException ioe) {
} }

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.schmizz.sshj.userauth;
public enum AuthResult {
SUCCESS,
FAILURE,
PARTIAL
}

View File

@@ -37,12 +37,12 @@ public interface UserAuth {
* @param nextService the service to set on successful authentication * @param nextService the service to set on successful authentication
* @param methods the {@link AuthMethod}'s to try * @param methods the {@link AuthMethod}'s to try
* *
* @return whether authentication was successful * @return whether authentication was successful, failed, or partially successful
* *
* @throws UserAuthException in case of authentication failure * @throws UserAuthException in case of authentication failure
* @throws TransportException if there was a transport-layer error * @throws TransportException if there was a transport-layer error
*/ */
boolean authenticate(String username, Service nextService, AuthMethod methods, int timeoutMs) AuthResult authenticate(String username, Service nextService, AuthMethod methods, int timeoutMs)
throws UserAuthException, TransportException; throws UserAuthException, TransportException;
/** /**

View File

@@ -40,7 +40,7 @@ public class UserAuthImpl
extends AbstractService extends AbstractService
implements UserAuth { implements UserAuth {
private final Promise<Boolean, UserAuthException> authenticated; private final Promise<AuthResult, UserAuthException> authenticated;
// Externally available // Externally available
private volatile String banner = ""; private volatile String banner = "";
@@ -53,13 +53,13 @@ public class UserAuthImpl
public UserAuthImpl(Transport trans) { public UserAuthImpl(Transport trans) {
super("ssh-userauth", trans); super("ssh-userauth", trans);
authenticated = new Promise<Boolean, UserAuthException>("authenticated", UserAuthException.chainer, trans.getConfig().getLoggerFactory()); authenticated = new Promise<AuthResult, UserAuthException>("authenticated", UserAuthException.chainer, trans.getConfig().getLoggerFactory());
} }
@Override @Override
public boolean authenticate(String username, Service nextService, AuthMethod method, int timeoutMs) public AuthResult authenticate(String username, Service nextService, AuthMethod method, int timeoutMs)
throws UserAuthException, TransportException { throws UserAuthException, TransportException {
final boolean outcome; final AuthResult outcome;
authenticated.lock(); authenticated.lock();
try { try {
@@ -73,8 +73,10 @@ public class UserAuthImpl
currentMethod.request(); currentMethod.request();
outcome = authenticated.retrieve(timeoutMs, TimeUnit.MILLISECONDS); outcome = authenticated.retrieve(timeoutMs, TimeUnit.MILLISECONDS);
if (outcome) { if (outcome == AuthResult.SUCCESS) {
log.debug("`{}` auth successful", method.getName()); log.debug("`{}` auth successful", method.getName());
} else if (outcome == AuthResult.PARTIAL) {
log.debug("`{}` auth partially successful", method.getName());
} else { } else {
log.debug("`{}` auth failed", method.getName()); log.debug("`{}` auth failed", method.getName());
} }
@@ -124,7 +126,7 @@ public class UserAuthImpl
// Should fix https://github.com/hierynomus/sshj/issues/237 // Should fix https://github.com/hierynomus/sshj/issues/237
trans.setAuthenticated(); // So it can put delayed compression into force if applicable trans.setAuthenticated(); // So it can put delayed compression into force if applicable
trans.setService(nextService); // We aren't in charge anymore, next service is trans.setService(nextService); // We aren't in charge anymore, next service is
authenticated.deliver(true); authenticated.deliver(AuthResult.SUCCESS);
break; break;
case USERAUTH_FAILURE: case USERAUTH_FAILURE:
@@ -133,7 +135,7 @@ public class UserAuthImpl
if (allowedMethods.contains(currentMethod.getName()) && currentMethod.shouldRetry()) { if (allowedMethods.contains(currentMethod.getName()) && currentMethod.shouldRetry()) {
currentMethod.request(); currentMethod.request();
} else { } else {
authenticated.deliver(false); authenticated.deliver(partialSuccess ? AuthResult.PARTIAL : AuthResult.FAILURE);
} }
break; break;

View File

@@ -16,10 +16,9 @@
package net.schmizz.sshj.userauth.keyprovider; package net.schmizz.sshj.userauth.keyprovider;
/** /**
* @version $Id:$ * Key File Formats
*/ */
public enum KeyFormat { public enum KeyFormat {
PKCS5,
PKCS8, PKCS8,
OpenSSH, OpenSSH,
OpenSSHv1, OpenSSHv1,

View File

@@ -27,9 +27,9 @@ public class KeyProviderUtil {
* <p/> * <p/>
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package. * Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
* *
* @param location * @param location File Path to key
* @return name of the key file format * @return name of the key file format
* @throws java.io.IOException * @throws java.io.IOException Thrown on file processing failures
*/ */
public static KeyFormat detectKeyFileFormat(File location) public static KeyFormat detectKeyFileFormat(File location)
throws IOException { throws IOException {
@@ -45,7 +45,7 @@ public class KeyProviderUtil {
* @param privateKey Private key stored in a string * @param privateKey Private key stored in a string
* @param separatePubKey Is the public key stored separately from the private key * @param separatePubKey Is the public key stored separately from the private key
* @return name of the key file format * @return name of the key file format
* @throws java.io.IOException * @throws java.io.IOException Thrown on file processing failures
*/ */
public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey) public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey)
throws IOException { throws IOException {
@@ -60,7 +60,7 @@ public class KeyProviderUtil {
* @param privateKey Private key accessible through a {@code Reader} * @param privateKey Private key accessible through a {@code Reader}
* @param separatePubKey Is the public key stored separately from the private key * @param separatePubKey Is the public key stored separately from the private key
* @return name of the key file format * @return name of the key file format
* @throws java.io.IOException * @throws java.io.IOException Thrown on file processing failures
*/ */
public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey) public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey)
throws IOException { throws IOException {
@@ -94,10 +94,8 @@ public class KeyProviderUtil {
} else if (separatePubKey) { } else if (separatePubKey) {
// Can delay asking for password since have unencrypted pubkey // Can delay asking for password since have unencrypted pubkey
return KeyFormat.OpenSSH; return KeyFormat.OpenSSH;
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
return KeyFormat.PKCS8;
} else { } else {
return KeyFormat.PKCS5; return KeyFormat.PKCS8;
} }
} else if (header.startsWith("PuTTY-User-Key-File-")) { } else if (header.startsWith("PuTTY-User-Key-File-")) {
return KeyFormat.PuTTY; return KeyFormat.PuTTY;

View File

@@ -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);
}
}
}

View File

@@ -39,7 +39,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.security.KeyPair; import java.security.KeyPair;
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */ /**
* Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption
*/
public class PKCS8KeyFile extends BaseFileKeyProvider { public class PKCS8KeyFile extends BaseFileKeyProvider {
public static class Factory public static class Factory

View File

@@ -84,8 +84,7 @@ public class AuthGssApiWithMic
@Override @Override
public GSSContext run() throws GSSException { public GSSContext run() throws GSSException {
GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME); GSSCredential clientCreds = manager.createCredential(GSSCredential.INITIATE_ONLY);
GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE); GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME); GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);

View File

@@ -27,17 +27,36 @@ import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import java.io.IOException; import java.io.IOException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.LinkedList;
import java.util.Queue;
public abstract class KeyedAuthMethod public abstract class KeyedAuthMethod
extends AbstractAuthMethod { extends AbstractAuthMethod {
protected final KeyProvider kProv; protected final KeyProvider kProv;
private Queue<KeyAlgorithm> available;
public KeyedAuthMethod(String name, KeyProvider kProv) { public KeyedAuthMethod(String name, KeyProvider kProv) {
super(name); super(name);
this.kProv = kProv; this.kProv = kProv;
} }
private KeyAlgorithm getPublicKeyAlgorithm(KeyType keyType) throws TransportException {
if (available == null) {
available = new LinkedList<>(params.getTransport().getClientKeyAlgorithms(keyType));
}
return available.peek();
}
@Override
public boolean shouldRetry() {
if (available != null) {
available.poll();
return !available.isEmpty();
}
return false;
}
protected SSHPacket putPubKey(SSHPacket reqBuf) protected SSHPacket putPubKey(SSHPacket reqBuf)
throws UserAuthException { throws UserAuthException {
PublicKey key; PublicKey key;
@@ -50,13 +69,16 @@ public abstract class KeyedAuthMethod
// public key as 2 strings: [ key type | key blob ] // public key as 2 strings: [ key type | key blob ]
KeyType keyType = KeyType.fromKey(key); KeyType keyType = KeyType.fromKey(key);
try { try {
KeyAlgorithm ka = params.getTransport().getClientKeyAlgorithm(keyType); KeyAlgorithm ka = getPublicKeyAlgorithm(keyType);
reqBuf.putString(ka.getKeyAlgorithm()) if (ka != null) {
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData()); reqBuf.putString(ka.getKeyAlgorithm())
return reqBuf; .putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
return reqBuf;
}
} catch (IOException ioe) { } catch (IOException ioe) {
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType); throw new UserAuthException("No KeyAlgorithm configured for key " + keyType, ioe);
} }
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType);
} }
protected SSHPacket putSig(SSHPacket reqBuf) protected SSHPacket putSig(SSHPacket reqBuf)
@@ -71,7 +93,7 @@ public abstract class KeyedAuthMethod
final KeyType kt = KeyType.fromKey(key); final KeyType kt = KeyType.fromKey(key);
Signature signature; Signature signature;
try { try {
signature = params.getTransport().getClientKeyAlgorithm(kt).newSignature(); signature = getPublicKeyAlgorithm(kt).newSignature();
} catch (TransportException e) { } catch (TransportException e) {
throw new UserAuthException("No KeyAlgorithm configured for key " + kt); throw new UserAuthException("No KeyAlgorithm configured for key " + kt);
} }

View File

@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
public class PasswordResponseProvider public class PasswordResponseProvider
implements ChallengeResponseProvider { implements ChallengeResponseProvider {
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword:\\s?\\z", Pattern.DOTALL); // FreeBSD prompt is "Password for user@host:"
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword(?: for .*)?:\\s?\\z", Pattern.DOTALL);
private static final char[] EMPTY_RESPONSE = new char[0]; private static final char[] EMPTY_RESPONSE = new char[0];

View File

@@ -15,6 +15,7 @@
*/ */
package net.schmizz.sshj.userauth.password; package net.schmizz.sshj.userauth.password;
import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
@@ -64,6 +65,9 @@ public class PasswordUtils {
*/ */
public static byte[] toByteArray(char[] password) { public static byte[] toByteArray(char[] password) {
CharBuffer charBuffer = CharBuffer.wrap(password); CharBuffer charBuffer = CharBuffer.wrap(password);
return StandardCharsets.UTF_8.encode(charBuffer).array(); final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, bytes.length);
return bytes;
} }
} }

View File

@@ -71,7 +71,13 @@ public class FileSystemFile
@Override @Override
public OutputStream getOutputStream() public OutputStream getOutputStream()
throws IOException { throws IOException {
return new FileOutputStream(file); return getOutputStream(false);
}
@Override
public OutputStream getOutputStream(boolean append)
throws IOException {
return new FileOutputStream(file, append);
} }
@Override @Override

View File

@@ -31,6 +31,19 @@ public interface FileTransfer {
void upload(String localPath, String remotePath) void upload(String localPath, String remotePath)
throws IOException; throws IOException;
/**
* This is meant to delegate to {@link #upload(LocalSourceFile, String)} with the {@code localPath} wrapped as e.g.
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} &gt; 0.
*
* @param localPath
* @param remotePath
* @param byteOffset
*
* @throws IOException
*/
void upload(String localPath, String remotePath, long byteOffset)
throws IOException;
/** /**
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g. * This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
* a {@link FileSystemFile}. * a {@link FileSystemFile}.
@@ -43,6 +56,19 @@ public interface FileTransfer {
void download(String remotePath, String localPath) void download(String remotePath, String localPath)
throws IOException; throws IOException;
/**
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} &gt; 0.
*
* @param localPath
* @param remotePath
* @param byteOffset
*
* @throws IOException
*/
void download(String remotePath, String localPath, long byteOffset)
throws IOException;
/** /**
* Upload {@code localFile} to {@code remotePath}. * Upload {@code localFile} to {@code remotePath}.
* *
@@ -54,6 +80,18 @@ public interface FileTransfer {
void upload(LocalSourceFile localFile, String remotePath) void upload(LocalSourceFile localFile, String remotePath)
throws IOException; throws IOException;
/**
* Upload {@code localFile} to {@code remotePath}. Appends to existing if {@code byteOffset} &gt; 0.
*
* @param localFile
* @param remotePath
* @param byteOffset
*
* @throws IOException
*/
void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
throws IOException;
/** /**
* Download {@code remotePath} to {@code localFile}. * Download {@code remotePath} to {@code localFile}.
* *
@@ -65,6 +103,18 @@ public interface FileTransfer {
void download(String remotePath, LocalDestFile localFile) void download(String remotePath, LocalDestFile localFile)
throws IOException; throws IOException;
/**
* Download {@code remotePath} to {@code localFile}. Appends to existing if {@code byteOffset} &gt; 0.
*
* @param localFile
* @param remotePath
* @param byteOffset
*
* @throws IOException
*/
void download(String remotePath, LocalDestFile localFile, long byteOffset)
throws IOException;
TransferListener getTransferListener(); TransferListener getTransferListener();
void setTransferListener(TransferListener listener); void setTransferListener(TransferListener listener);

View File

@@ -20,8 +20,13 @@ import java.io.OutputStream;
public interface LocalDestFile { public interface LocalDestFile {
long getLength();
OutputStream getOutputStream() OutputStream getOutputStream()
throws IOException; throws IOException;
OutputStream getOutputStream(boolean append)
throws IOException;
/** @return A child file/directory of this directory with given {@code name}. */ /** @return A child file/directory of this directory with given {@code name}. */
LocalDestFile getChild(String name); LocalDestFile getChild(String name);

View File

@@ -52,24 +52,49 @@ public class SCPFileTransfer
@Override @Override
public void upload(String localPath, String remotePath) public void upload(String localPath, String remotePath)
throws IOException { throws IOException {
newSCPUploadClient().copy(new FileSystemFile(localPath), remotePath); upload(localPath, remotePath, 0);
}
@Override
public void upload(String localFile, String remotePath, long byteOffset)
throws IOException {
upload(new FileSystemFile(localFile), remotePath, byteOffset);
} }
@Override @Override
public void download(String remotePath, String localPath) public void download(String remotePath, String localPath)
throws IOException { throws IOException {
download(remotePath, new FileSystemFile(localPath)); download(remotePath, localPath, 0);
}
@Override
public void download(String remotePath, String localPath, long byteOffset) throws IOException {
download(remotePath, new FileSystemFile(localPath), byteOffset);
} }
@Override @Override
public void download(String remotePath, LocalDestFile localFile) public void download(String remotePath, LocalDestFile localFile)
throws IOException { throws IOException {
download(remotePath, localFile, 0);
}
@Override
public void download(String remotePath, LocalDestFile localFile, long byteOffset)
throws IOException {
checkByteOffsetSupport(byteOffset);
newSCPDownloadClient().copy(remotePath, localFile); newSCPDownloadClient().copy(remotePath, localFile);
} }
@Override @Override
public void upload(LocalSourceFile localFile, String remotePath) public void upload(LocalSourceFile localFile, String remotePath)
throws IOException { throws IOException {
upload(localFile, remotePath, 0);
}
@Override
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
throws IOException {
checkByteOffsetSupport(byteOffset);
newSCPUploadClient().copy(localFile, remotePath); newSCPUploadClient().copy(localFile, remotePath);
} }
@@ -79,4 +104,11 @@ public class SCPFileTransfer
} }
return this; return this;
} }
private void checkByteOffsetSupport(long byteOffset) throws IOException {
// TODO - implement byte offsets on SCP, if possible.
if (byteOffset > 0) {
throw new SCPException("Byte offset on SCP file transfers is not supported.");
}
}
} }

View File

@@ -16,6 +16,7 @@
package com.hierynomus.sshj.userauth.keyprovider package com.hierynomus.sshj.userauth.keyprovider
import com.hierynomus.sshj.test.SshFixture import com.hierynomus.sshj.test.SshFixture
import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.userauth.keyprovider.KeyFormat import net.schmizz.sshj.userauth.keyprovider.KeyFormat
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
@@ -39,7 +40,14 @@ class FileKeyProviderSpec extends Specification {
@Unroll @Unroll
def "should have #format FileKeyProvider enabled by default"() { def "should have #format FileKeyProvider enabled by default"() {
given: given:
SSHClient client = fixture.setupConnectedDefaultClient() // `fixture` is backed by Apache SSHD server. Looks like it doesn't support rsa-sha2-512 public key signature.
// Changing the default config to prioritize the former default implementation of RSA signature.
def config = new DefaultConfig()
config.prioritizeSshRsaKeyAlgorithm()
and:
SSHClient client = fixture.setupClient(config)
fixture.connectClient(client)
when: when:
client.authPublickey("jeroen", keyfile) client.authPublickey("jeroen", keyfile)
@@ -48,11 +56,11 @@ class FileKeyProviderSpec extends Specification {
client.isAuthenticated() client.isAuthenticated()
cleanup: cleanup:
client.disconnect() client?.disconnect()
where: where:
format | keyfile format | keyfile
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5" KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh" KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
} }
} }

View 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]
}
}

View File

@@ -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;
}
}

View File

@@ -37,14 +37,14 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class RemotePortForwarderTest { public class RemotePortForwarderTest {
private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class); private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class);
private static final PortRange RANGE = new PortRange(9000, 9999); private static final PortRange RANGE = new PortRange(9000, 9999);
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress("127.0.0.1", 8080); private static final String LOCALHOST = "127.0.0.1";
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress(LOCALHOST, 8080);
@Rule @Rule
public SshFixture fixture = new SshFixture(); public SshFixture fixture = new SshFixture();
@@ -62,61 +62,63 @@ public class RemotePortForwarderTest {
@Test @Test
public void shouldHaveWorkingHttpServer() throws IOException { public void shouldHaveWorkingHttpServer() throws IOException {
// Just to check that we have a working http server... // Just to check that we have a working http server...
assertThat(httpGet("127.0.0.1", 8080), equalTo(200)); assertEquals(200, httpGet( 8080));
} }
@Test @Test
public void shouldDynamicallyForwardPortForLocalhost() throws IOException { public void shouldDynamicallyForwardPortForLocalhost() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0)); RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0));
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
} }
@Test @Test
public void shouldDynamicallyForwardPortForAllIPv4() throws IOException { public void shouldDynamicallyForwardPortForAllIPv4() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", new SinglePort(0)); RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", new SinglePort(0));
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
} }
@Test @Test
public void shouldDynamicallyForwardPortForAllProtocols() throws IOException { public void shouldDynamicallyForwardPortForAllProtocols() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0)); RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0));
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
} }
@Test @Test
public void shouldForwardPortForLocalhost() throws IOException { public void shouldForwardPortForLocalhost() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE); RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE);
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
} }
@Test @Test
public void shouldForwardPortForAllIPv4() throws IOException { public void shouldForwardPortForAllIPv4() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE); RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE);
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
} }
@Test @Test
public void shouldForwardPortForAllProtocols() throws IOException { public void shouldForwardPortForAllProtocols() throws IOException {
SSHClient sshClient = getFixtureClient(); SSHClient sshClient = getFixtureClient();
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", RANGE); RemotePortForwarder.Forward bind = forwardPort(sshClient, "", RANGE);
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200)); assertHttpGetSuccess(bind);
}
private void assertHttpGetSuccess(final RemotePortForwarder.Forward bind) throws IOException {
assertEquals(200, httpGet(bind.getPort()));
} }
private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException { private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException {
while (true) { while (true) {
try { try {
RemotePortForwarder.Forward forward = sshClient.getRemotePortForwarder().bind( return sshClient.getRemotePortForwarder().bind(
// where the server should listen // where the server should listen
new RemotePortForwarder.Forward(address, portRange.nextPort()), new RemotePortForwarder.Forward(address, portRange.nextPort()),
// what we do with incoming connections that are forwarded to us // what we do with incoming connections that are forwarded to us
new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR)); new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR));
return forward;
} catch (ConnectionException ce) { } catch (ConnectionException ce) {
if (!portRange.hasNext()) { if (!portRange.hasNext()) {
throw ce; throw ce;
@@ -125,9 +127,9 @@ public class RemotePortForwarderTest {
} }
} }
private int httpGet(String server, int port) throws IOException { private int httpGet(int port) throws IOException {
HttpClient client = HttpClientBuilder.create().build(); HttpClient client = HttpClientBuilder.create().build();
String urlString = "http://" + server + ":" + port; String urlString = "http://" + LOCALHOST + ":" + port;
log.info("Trying: GET " + urlString); log.info("Trying: GET " + urlString);
HttpResponse execute = client.execute(new HttpGet(urlString)); HttpResponse execute = client.execute(new HttpGet(urlString));
return execute.getStatusLine().getStatusCode(); return execute.getStatusLine().getStatusCode();
@@ -140,7 +142,7 @@ public class RemotePortForwarderTest {
} }
private static class PortRange { private static class PortRange {
private int upper; private final int upper;
private int current; private int current;
public PortRange(int lower, int upper) { public PortRange(int lower, int upper) {

View File

@@ -15,54 +15,67 @@
*/ */
package com.hierynomus.sshj.keepalive; package com.hierynomus.sshj.keepalive;
import com.hierynomus.sshj.test.KnownFailingTests;
import com.hierynomus.sshj.test.SlowTests;
import com.hierynomus.sshj.test.SshFixture; import com.hierynomus.sshj.test.SshFixture;
import net.schmizz.keepalive.KeepAlive;
import net.schmizz.keepalive.KeepAliveProvider; import net.schmizz.keepalive.KeepAliveProvider;
import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.userauth.UserAuthException; import net.schmizz.sshj.userauth.UserAuthException;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.experimental.categories.Category;
import java.io.IOException; import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import static org.junit.Assert.fail; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class KeepAliveThreadTerminationTest { public class KeepAliveThreadTerminationTest {
private static final int KEEP_ALIVE_SECONDS = 1;
private static final long STOP_SLEEP = 1500;
@Rule @Rule
public SshFixture fixture = new SshFixture(); public SshFixture fixture = new SshFixture();
@Test @Test
@Category({SlowTests.class, KnownFailingTests.class}) public void shouldNotStartThreadOnSetKeepAliveInterval() {
public void shouldCorrectlyTerminateThreadOnDisconnect() throws IOException, InterruptedException { final SSHClient sshClient = setupClient();
DefaultConfig defaultConfig = new DefaultConfig();
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
for (int i = 0; i < 10; i++) {
SSHClient sshClient = fixture.setupClient(defaultConfig);
fixture.connectClient(sshClient);
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(1);
try {
sshClient.authPassword("bad", "credentials");
fail("Should not auth.");
} catch (UserAuthException e) {
// OK
}
fixture.stopClient();
Thread.sleep(2000);
}
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
for (long l : threadMXBean.getAllThreadIds()) { assertTrue(keepAlive.isDaemon());
ThreadInfo threadInfo = threadMXBean.getThreadInfo(l); assertFalse(keepAlive.isAlive());
if (threadInfo.getThreadName().equals("keep-alive") && threadInfo.getThreadState() != Thread.State.TERMINATED) { assertEquals(Thread.State.NEW, keepAlive.getState());
fail("Found alive keep-alive thread in state " + threadInfo.getThreadState()); }
}
} @Test
public void shouldStartThreadOnConnectAndInterruptOnDisconnect() throws IOException, InterruptedException {
final SSHClient sshClient = setupClient();
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
assertTrue(keepAlive.isDaemon());
assertEquals(Thread.State.NEW, keepAlive.getState());
fixture.connectClient(sshClient);
assertThrows(UserAuthException.class, () -> sshClient.authPassword("bad", "credentials"));
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
fixture.stopClient();
Thread.sleep(STOP_SLEEP);
assertFalse(keepAlive.isAlive());
assertEquals(Thread.State.TERMINATED, keepAlive.getState());
}
private SSHClient setupClient() {
final DefaultConfig defaultConfig = new DefaultConfig();
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
final SSHClient sshClient = fixture.setupClient(defaultConfig);
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(KEEP_ALIVE_SECONDS);
return sshClient;
} }
} }

View File

@@ -17,21 +17,25 @@ package com.hierynomus.sshj.sftp;
import com.hierynomus.sshj.test.SshFixture; import com.hierynomus.sshj.test.SshFixture;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.ByteArrayUtils;
import net.schmizz.sshj.sftp.OpenMode; import net.schmizz.sshj.sftp.OpenMode;
import net.schmizz.sshj.sftp.RemoteFile; import net.schmizz.sshj.sftp.RemoteFile;
import net.schmizz.sshj.sftp.SFTPEngine; import net.schmizz.sshj.sftp.SFTPEngine;
import net.schmizz.sshj.sftp.SFTPException;
import org.apache.sshd.common.util.io.IoUtils;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import java.io.File; import java.io.*;
import java.io.IOException; import java.security.SecureRandom;
import java.io.InputStream;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Random; import java.util.Random;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class RemoteFileTest { public class RemoteFileTest {
@Rule @Rule
@@ -84,4 +88,141 @@ public class RemoteFileTest {
assertThat("The written and received data should match", data, equalTo(test2)); assertThat("The written and received data should match", data, equalTo(test2));
} }
@Test
public void shouldNotReadAheadAfterLimitInputStream() throws IOException {
SSHClient ssh = fixture.setupConnectedDefaultClient();
ssh.authPassword("test", "test");
SFTPEngine sftp = new SFTPEngine(ssh).init();
RemoteFile rf;
File file = temp.newFile("SftpReadAheadLimitTest.bin");
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
byte[] data = new byte[8192];
new Random(53).nextBytes(data);
data[3072] = 1;
rf.write(0, data, 0, data.length);
rf.close();
assertThat("The file should exist", file.exists());
rf = sftp.open(file.getPath());
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
byte[] test = new byte[4097];
int n = 0;
while (n < 2048) {
n += rs.read(test, n, 2048 - n);
}
rf.close();
while (n < 3072) {
n += rs.read(test, n, 3072 - n);
}
assertThat("buffer overrun", test[3072] == 0);
try {
rs.read(test, n, test.length - n);
fail("Content must not be buffered");
} catch (SFTPException e){
// expected
}
}
@Test
public void limitedReadAheadInputStream() throws IOException {
SSHClient ssh = fixture.setupConnectedDefaultClient();
ssh.authPassword("test", "test");
SFTPEngine sftp = new SFTPEngine(ssh).init();
RemoteFile rf;
File file = temp.newFile("SftpReadAheadLimitedTest.bin");
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
byte[] data = new byte[8192];
new Random(53).nextBytes(data);
data[3072] = 1;
rf.write(0, data, 0, data.length);
rf.close();
assertThat("The file should exist", file.exists());
rf = sftp.open(file.getPath());
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
byte[] test = new byte[4097];
int n = 0;
while (n < 2048) {
n += rs.read(test, n, 2048 - n);
}
while (n < 3072) {
n += rs.read(test, n, 3072 - n);
}
assertThat("buffer overrun", test[3072] == 0);
n += rs.read(test, n, test.length - n); // --> ArrayIndexOutOfBoundsException
byte[] test2 = new byte[data.length];
System.arraycopy(test, 0, test2, 0, test.length);
while (n < data.length) {
n += rs.read(test2, n, data.length - n);
}
assertThat("The written and received data should match", data, equalTo(test2));
}
@Test
public void shouldReadCorrectlyWhenWrappedInBufferedStream_FullSizeBuffer() throws IOException {
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024 * 1024);
}
@Test
public void shouldReadCorrectlyWhenWrappedInBufferedStream_HalfSizeBuffer() throws IOException {
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 512 * 1024);
}
@Test
public void shouldReadCorrectlyWhenWrappedInBufferedStream_QuarterSizeBuffer() throws IOException {
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 256 * 1024);
}
@Test
public void shouldReadCorrectlyWhenWrappedInBufferedStream_SmallSizeBuffer() throws IOException {
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024);
}
private void doTestShouldReadCorrectlyWhenWrappedInBufferedStream(int fileSize, int bufferSize) throws IOException {
SSHClient ssh = fixture.setupConnectedDefaultClient();
ssh.authPassword("test", "test");
SFTPEngine sftp = new SFTPEngine(ssh).init();
final byte[] expected = new byte[fileSize];
new SecureRandom(new byte[] { 31 }).nextBytes(expected);
File file = temp.newFile("shouldReadCorrectlyWhenWrappedInBufferedStream.bin");
try (OutputStream fStream = new FileOutputStream(file)) {
IoUtils.copy(new ByteArrayInputStream(expected), fStream);
}
RemoteFile rf = sftp.open(file.getPath());
final byte[] actual;
try (InputStream inputStream = new BufferedInputStream(
rf.new ReadAheadRemoteFileInputStream(10),
bufferSize)
) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IoUtils.copy(inputStream, baos, expected.length);
actual = baos.toByteArray();
}
assertEquals("The file should be fully read", expected.length, actual.length);
assertThat("The file should be read correctly",
ByteArrayUtils.equals(expected, 0, actual, 0, expected.length));
}
} }

View File

@@ -19,21 +19,18 @@ import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator; import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider; import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.scp.server.ScpCommandFactory;
import org.apache.sshd.server.SshServer; import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.command.CommandFactory;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.shell.ProcessShellFactory; import org.apache.sshd.server.shell.ProcessShellFactory;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.Arrays; import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
@@ -42,10 +39,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class SshFixture extends ExternalResource { public class SshFixture extends ExternalResource {
public static final String hostkey = "hostkey.pem"; public static final String hostkey = "hostkey.pem";
public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db"; public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db";
public static final String listCommand = OsUtils.isWin32() ? "cmd.exe /C dir" : "ls";
private SshServer server = defaultSshServer(); private final SshServer server = defaultSshServer();
private SSHClient client = null; private SSHClient client = null;
private AtomicBoolean started = new AtomicBoolean(false); private final AtomicBoolean started = new AtomicBoolean(false);
private boolean autoStart = true; private boolean autoStart = true;
public SshFixture(boolean autoStart) { public SshFixture(boolean autoStart) {
@@ -108,38 +106,23 @@ public class SshFixture extends ExternalResource {
sshServer.setPort(randomPort()); sshServer.setPort(randomPort());
ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey); ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey);
sshServer.setKeyPairProvider(fileKeyPairProvider); sshServer.setKeyPairProvider(fileKeyPairProvider);
sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { sshServer.setPasswordAuthenticator((username, password, session) -> username.equals(password));
@Override
public boolean authenticate(String username, String password, ServerSession session) {
return username.equals(password);
}
});
sshServer.setGSSAuthenticator(new BogusGSSAuthenticator()); sshServer.setGSSAuthenticator(new BogusGSSAuthenticator());
sshServer.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory())); sshServer.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
ScpCommandFactory commandFactory = new ScpCommandFactory(); ScpCommandFactory commandFactory = new ScpCommandFactory();
commandFactory.setDelegateCommandFactory(new CommandFactory() { commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
@Override
public Command createCommand(String command) {
return new ProcessShellFactory(command.split(" ")).create();
}
});
sshServer.setCommandFactory(commandFactory); sshServer.setCommandFactory(commandFactory);
sshServer.setShellFactory(new ProcessShellFactory("ls")); sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
return sshServer; return sshServer;
} }
private int randomPort() { private int randomPort() {
try { try {
ServerSocket s = null; try (final ServerSocket s = new ServerSocket(0)) {
try {
s = new ServerSocket(0);
return s.getLocalPort(); return s.getLocalPort();
} finally {
if (s != null)
s.close();
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new UncheckedIOException(e);
} }
} }

View File

@@ -39,4 +39,8 @@ public class FileUtil {
IOUtils.closeQuietly(fileInputStream); IOUtils.closeQuietly(fileInputStream);
} }
} }
public static boolean compareFileContents(File f1, File f2) throws IOException {
return readFromFile(f1).equals(readFromFile(f2));
}
} }

View File

@@ -22,9 +22,8 @@ import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.transport.kex.DHGexSHA1; import net.schmizz.sshj.transport.kex.DHGexSHA1;
import net.schmizz.sshj.transport.kex.DHGexSHA256; import net.schmizz.sshj.transport.kex.DHGexSHA256;
import net.schmizz.sshj.transport.kex.ECDHNistP; import net.schmizz.sshj.transport.kex.ECDHNistP;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.kex.BuiltinDHFactories; import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.common.kex.KeyExchange; import org.apache.sshd.common.kex.KeyExchangeFactory;
import org.apache.sshd.server.SshServer; import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.kex.DHGEXServer; import org.apache.sshd.server.kex.DHGEXServer;
import org.apache.sshd.server.kex.DHGServer; import org.apache.sshd.server.kex.DHGServer;
@@ -38,6 +37,7 @@ import java.util.Collections;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class KeyExchangeTest extends BaseAlgorithmTest { public class KeyExchangeTest extends BaseAlgorithmTest {
@SuppressWarnings("deprecation")
@Parameterized.Parameters(name = "algorithm={0}") @Parameterized.Parameters(name = "algorithm={0}")
public static Collection<Object[]> getParameters() { public static Collection<Object[]> getParameters() {
return Arrays.asList(new Object[][]{ return Arrays.asList(new Object[][]{
@@ -56,10 +56,10 @@ public class KeyExchangeTest extends BaseAlgorithmTest {
}); });
} }
private Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory; private final Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
private NamedFactory<KeyExchange> serverFactory; private final KeyExchangeFactory serverFactory;
public KeyExchangeTest(NamedFactory<KeyExchange> serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) { public KeyExchangeTest(KeyExchangeFactory serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) {
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
this.serverFactory = serverFactory; this.serverFactory = serverFactory;
} }

View File

@@ -20,12 +20,8 @@ import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive; import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
import net.schmizz.sshj.userauth.method.ChallengeResponseProvider; import net.schmizz.sshj.userauth.method.ChallengeResponseProvider;
import net.schmizz.sshj.userauth.password.Resource; import net.schmizz.sshj.userauth.password.Resource;
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.apache.sshd.common.NamedFactory; import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
import org.apache.sshd.server.auth.UserAuth;
import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractive;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -43,28 +39,8 @@ public class AuthKeyboardInteractiveTest {
@Before @Before
public void setKeyboardInteractiveAuthenticator() throws IOException { public void setKeyboardInteractiveAuthenticator() throws IOException {
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() { fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthKeyboardInteractiveFactory()));
@Override fixture.getServer().setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
public String getName() {
return UserAuthKeyboardInteractiveFactory.NAME;
}
@Override
public UserAuth get() {
return new UserAuthKeyboardInteractive();
}
@Override
public UserAuth create() {
return get();
}
}));
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
@Override
public boolean authenticate(String username, String password, ServerSession session) {
return password.equals(username);
}
});
fixture.getServer().start(); fixture.getServer().start();
} }
@@ -75,7 +51,7 @@ public class AuthKeyboardInteractiveTest {
sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() { sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() {
@Override @Override
public List<String> getSubmethods() { public List<String> getSubmethods() {
return new ArrayList<String>(); return new ArrayList<>();
} }
@Override @Override

View File

@@ -21,56 +21,29 @@ import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider; import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
import net.schmizz.sshj.userauth.password.Resource; import net.schmizz.sshj.userauth.password.Resource;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.server.auth.UserAuth;
import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.password.PasswordChangeRequiredException; import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
import org.apache.sshd.server.auth.password.UserAuthPassword;
import org.apache.sshd.server.auth.password.UserAuthPasswordFactory; import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.session.ServerSession;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Stack; import java.util.Stack;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;
public class AuthPasswordTest { public class AuthPasswordTest {
@Rule @Rule
public SshFixture fixture = new SshFixture(false); public SshFixture fixture = new SshFixture(false);
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before @Before
public void setPasswordAuthenticator() throws IOException { public void setPasswordAuthenticator() throws IOException {
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() { fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthPasswordFactory()));
@Override
public String getName() {
return UserAuthPasswordFactory.NAME;
}
@Override
public UserAuth get() {
return new UserAuthPassword() {
@Override
protected Boolean handleClientPasswordChangeRequest(Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) throws Exception {
return session.getPasswordAuthenticator().authenticate(username, newPassword, session);
}
};
}
@Override
public UserAuth create() {
return get();
}
}));
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() { fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
@Override @Override
public boolean authenticate(String username, String password, ServerSession session) { public boolean authenticate(String username, String password, ServerSession session) {
@@ -80,6 +53,12 @@ public class AuthPasswordTest {
return password.equals(username); return password.equals(username);
} }
} }
@Override
public boolean handleClientPasswordChangeRequest(
ServerSession session, String username, String oldPassword, String newPassword) {
return username.equals(newPassword);
}
}); });
fixture.getServer().start(); fixture.getServer().start();
} }
@@ -87,8 +66,7 @@ public class AuthPasswordTest {
@Test @Test
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException { public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient(); SSHClient sshClient = fixture.setupConnectedDefaultClient();
expectedException.expect(UserAuthException.class); assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", "changeme"));
sshClient.authPassword("jeroen", "changeme");
assertThat("Should not have authenticated", !sshClient.isAuthenticated()); assertThat("Should not have authenticated", !sshClient.isAuthenticated());
} }
@@ -112,8 +90,7 @@ public class AuthPasswordTest {
@Test @Test
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException { public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient(); SSHClient sshClient = fixture.setupConnectedDefaultClient();
expectedException.expect(UserAuthException.class); assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", new PasswordFinder() {
sshClient.authPassword("jeroen", new PasswordFinder() {
@Override @Override
public char[] reqPassword(Resource<?> resource) { public char[] reqPassword(Resource<?> resource) {
return "changeme".toCharArray(); return "changeme".toCharArray();
@@ -123,7 +100,7 @@ public class AuthPasswordTest {
public boolean shouldRetry(Resource<?> resource) { public boolean shouldRetry(Resource<?> resource) {
return false; return false;
} }
}, new StaticPasswordUpdateProvider("bad")); }, new StaticPasswordUpdateProvider("bad")));
assertThat("Should not have authenticated", !sshClient.isAuthenticated()); assertThat("Should not have authenticated", !sshClient.isAuthenticated());
} }
@@ -153,7 +130,7 @@ public class AuthPasswordTest {
} }
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider { private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
private Stack<String> newPasswords = new Stack<String>(); private final Stack<String> newPasswords = new Stack<>();
public StaticPasswordUpdateProvider(String... newPasswords) { public StaticPasswordUpdateProvider(String... newPasswords) {
for (int i = newPasswords.length - 1; i >= 0; i--) { for (int i = newPasswords.length - 1; i >= 0; i--) {

View File

@@ -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;
}
}
}

View File

@@ -40,7 +40,7 @@ public class LoadsOfConnects {
SSHClient client = fixture.setupConnectedDefaultClient(); SSHClient client = fixture.setupConnectedDefaultClient();
client.authPassword("test", "test"); client.authPassword("test", "test");
Session s = client.startSession(); Session s = client.startSession();
Session.Command c = s.exec("ls"); Session.Command c = s.exec(SshFixture.listCommand);
IOUtils.readFully(c.getErrorStream()); IOUtils.readFully(c.getErrorStream());
IOUtils.readFully(c.getInputStream()); IOUtils.readFully(c.getInputStream());
c.close(); c.close();

View File

@@ -35,9 +35,9 @@ public class KeyProviderUtilTest {
} }
@Test @Test
public void testPkcs5() throws IOException { public void testPkcs1Rsa() throws IOException {
KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs5")); KeyFormat format = KeyProviderUtil.detectKeyFileFormat(new File(ROOT, "pkcs1-rsa"));
assertEquals(KeyFormat.PKCS5, format); assertEquals(KeyFormat.PKCS8, format);
} }
@Test @Test

View File

@@ -39,6 +39,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.PrivateKey; import java.security.PrivateKey;
@@ -247,6 +248,12 @@ public class OpenSSHKeyFileTest {
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", true); checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", true);
} }
@Test
public void shouldLoadProtectedED25519PrivateKeyAes128CBC() throws IOException {
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes128cbc.pem", "sshjtest", false);
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes128cbc.pem", "sshjtest", true);
}
@Test(expected = KeyDecryptionFailedException.class) @Test(expected = KeyDecryptionFailedException.class)
public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException { public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException {
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile(); OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
@@ -437,6 +444,14 @@ public class OpenSSHKeyFileTest {
corruptedKeyFile.getPublic()); corruptedKeyFile.getPublic());
} }
@Test
public void emptyPrivateKey() {
FileKeyProvider keyProvider = new OpenSSHKeyV1KeyFile();
keyProvider.init(new StringReader(""));
assertThrows("This key is not in 'openssh-key-v1' format", IOException.class, keyProvider::getPrivate);
}
@Before @Before
public void checkBCRegistration() { public void checkBCRegistration() {
if (!SecurityUtils.isBouncyCastleRegistered()) { if (!SecurityUtils.isBouncyCastleRegistered()) {

View File

@@ -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());
}
}

View File

@@ -15,110 +15,135 @@
*/ */
package net.schmizz.sshj.keyprovider; package net.schmizz.sshj.keyprovider;
import com.hierynomus.sshj.common.KeyAlgorithm;
import com.hierynomus.sshj.common.KeyDecryptionFailedException; import com.hierynomus.sshj.common.KeyDecryptionFailedException;
import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.password.PasswordFinder; import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUtils; import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.userauth.password.Resource;
import net.schmizz.sshj.util.KeyUtil; import net.schmizz.sshj.util.KeyUtil;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.net.URL;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class PKCS8KeyFileTest { public class PKCS8KeyFileTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
static final FileKeyProvider rsa = new PKCS8KeyFile();
static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469"; static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469";
static final String pubExp = "23"; static final String pubExp = "23";
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";
static final String KEY_PASSPHRASE = "passphrase";
static final String INCORRECT_PASSPHRASE = String.class.getSimpleName();
@Before @Test
public void setUp() public void testKeys() throws GeneralSecurityException, IOException {
throws UnsupportedEncodingException, GeneralSecurityException { final PKCS8KeyFile provider = new PKCS8KeyFile();
if (!SecurityUtils.isBouncyCastleRegistered()) provider.init(new File("src/test/resources/id_rsa"));
throw new AssertionError("bouncy castle needed"); assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
rsa.init(new File("src/test/resources/id_rsa")); assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
assertEquals(provider.getType(), KeyType.RSA);
} }
@Test @Test
public void testKeys() public void testPkcs1Rsa() throws IOException {
throws IOException, GeneralSecurityException { final PKCS8KeyFile provider = new PKCS8KeyFile();
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic()); provider.init(getFile("pkcs1-rsa"));
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate()); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
} }
@Test @Test
public void testType() public void testPkcs1Encrypted() throws IOException, GeneralSecurityException {
throws IOException { final PKCS8KeyFile provider = new PKCS8KeyFile();
assertEquals(rsa.getType(), KeyType.RSA); provider.init(getFile("pkcs1-rsa-encrypted"), PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray()));
assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), provider.getPublic());
assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), provider.getPrivate());
} }
@Test @Test
public void testPkcs8Rsa() throws IOException { public void testPkcs8Rsa() throws IOException {
final PKCS8KeyFile provider = new PKCS8KeyFile(); final PKCS8KeyFile provider = new PKCS8KeyFile();
provider.init(getReader("pkcs8-rsa-2048")); provider.init(getFile("pkcs8-rsa-2048"));
assertEquals("RSA", provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
assertEquals("RSA", provider.getPrivate().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
} }
@Test @Test
public void testPkcs8RsaEncrypted() throws IOException { public void testPkcs8RsaEncrypted() throws IOException {
final PKCS8KeyFile provider = new PKCS8KeyFile(); final PKCS8KeyFile provider = new PKCS8KeyFile();
final PasswordFinder passwordFinder = PasswordUtils.createOneOff("passphrase".toCharArray()); final PasswordFinder passwordFinder = PasswordUtils.createOneOff(KEY_PASSPHRASE.toCharArray());
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
assertEquals("RSA", provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
assertEquals("RSA", provider.getPrivate().getAlgorithm()); assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
} }
@Test @Test
public void testPkcs8RsaEncryptedIncorrectPassword() throws IOException { public void testPkcs8RsaEncryptedIncorrectPassword() {
expectedException.expect(KeyDecryptionFailedException.class);
final PKCS8KeyFile provider = new PKCS8KeyFile(); final PKCS8KeyFile provider = new PKCS8KeyFile();
final PasswordFinder passwordFinder = PasswordUtils.createOneOff(String.class.getSimpleName().toCharArray()); final PasswordFinder passwordFinder = PasswordUtils.createOneOff(INCORRECT_PASSPHRASE.toCharArray());
provider.init(getReader("pkcs8-rsa-2048-encrypted"), passwordFinder); provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
provider.getPrivate(); assertThrows(KeyDecryptionFailedException.class, provider::getPrivate);
}
@Test
public void testPkcs8RsaEncryptedRetryPassword() throws IOException {
final PKCS8KeyFile provider = new PKCS8KeyFile();
final PasswordFinder passwordFinder = new PasswordFinder() {
private boolean retryEnabled = true;
@Override
public char[] reqPassword(Resource<?> resource) {
final char[] password;
if (retryEnabled) {
password = INCORRECT_PASSPHRASE.toCharArray();
} else {
password = KEY_PASSPHRASE.toCharArray();
}
return password;
}
@Override
public boolean shouldRetry(Resource<?> resource) {
final boolean shouldRetry = retryEnabled;
if (retryEnabled) {
retryEnabled = false;
}
return shouldRetry;
}
};
provider.init(getFile("pkcs8-rsa-2048-encrypted"), passwordFinder);
assertEquals(KeyAlgorithm.RSA, provider.getPublic().getAlgorithm());
assertEquals(KeyAlgorithm.RSA, provider.getPrivate().getAlgorithm());
} }
@Test @Test
public void testPkcs8Ecdsa() throws IOException { public void testPkcs8Ecdsa() throws IOException {
final PKCS8KeyFile provider = new PKCS8KeyFile(); final PKCS8KeyFile provider = new PKCS8KeyFile();
provider.init(getReader("pkcs8-ecdsa")); provider.init(getFile("pkcs8-ecdsa"));
assertEquals("ECDSA", provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.ECDSA, provider.getPublic().getAlgorithm());
assertEquals("ECDSA", provider.getPrivate().getAlgorithm()); assertEquals(KeyAlgorithm.ECDSA, provider.getPrivate().getAlgorithm());
} }
@Test @Test
public void testPkcs8Dsa() throws IOException { public void testPkcs8Dsa() throws IOException {
final PKCS8KeyFile provider = new PKCS8KeyFile(); final PKCS8KeyFile provider = new PKCS8KeyFile();
provider.init(getReader("pkcs8-dsa")); provider.init(getFile("pkcs8-dsa"));
assertEquals("DSA", provider.getPublic().getAlgorithm()); assertEquals(KeyAlgorithm.DSA, provider.getPublic().getAlgorithm());
assertEquals("DSA", provider.getPrivate().getAlgorithm()); assertEquals(KeyAlgorithm.DSA, provider.getPrivate().getAlgorithm());
} }
private Reader getReader(final String filename) { private File getFile(final String filename) {
final String path = String.format("/keyformats/%s", filename); final String path = String.format("/keyformats/%s", filename);
final InputStream inputStream = getClass().getResourceAsStream(path); final URL resource = getClass().getResource(path);
if (inputStream == null) { if (resource == null) {
throw new IllegalArgumentException(String.format("Key File [%s] not found", path)); throw new IllegalArgumentException(String.format("Key File [%s] not found", path));
} }
return new InputStreamReader(inputStream); return new File(resource.getPath());
} }
} }

View File

@@ -209,6 +209,25 @@ public class PuTTYKeyFileTest {
"oYhmT2+0DKBuBVCAM4qRdA==\n" + "oYhmT2+0DKBuBVCAM4qRdA==\n" +
"Private-MAC: 40ccc8b9a7291ec64e5be0c99badbc8a012bf220\n"; "Private-MAC: 40ccc8b9a7291ec64e5be0c99badbc8a012bf220\n";
final static String ppk1024_umlaut_passphrase = "PuTTY-User-Key-File-2: ssh-rsa\n" +
"Encryption: aes256-cbc\n" +
"Comment: user@host\n" +
"Public-Lines: 4\n" +
"AAAAB3NzaC1yc2EAAAADAQABAAAAgQDsQv60HaW0301hX/xV3AUcutbDDAJp7KWc\n" +
"6swL+H6jhwe3N7FK/SA4492bK5oHwU3ea3X6moLuapTMawMQbRy1kfQm99wcYc7C\n" +
"6PJO3uouzjDatc/aByDejbo5OL9kK4Vy7qm6tw1hC0JIM+TCvItKu+t6Myl7xzv4\n" +
"KbSHiMzulQ==\n" +
"Private-Lines: 8\n" +
"hPS6HYs4t8WChglZzo5G/B0ohnw2DQS19HMPllyVr9XfDyT2Xk8ZSTye84r5CtMP\n" +
"xF4Qc0nkoStyw9p9Tm762FhkM0iGghLWeCdTyqXVlAA9l3sr0BMJ9AoMvjQBqqns\n" +
"gjfPvmtNPFn8sfApHVOv1qSLSGOMZFm/q6KtGuR+IyTnMuZ71b/cQYYHbsAQxt09\n" +
"96I7jDhup/4uoi/tcPYhe998wRFSSldkAtcmYGUnDWCiivlP+gZsXvOI2zs2gCxx\n" +
"ECEwZNTR/j3G0muRUMf91iZSMBije+41j345F+ZHJ43gYXW6lxjFtI5jr9LRGWF1\n" +
"hTeY6IlLt4EBBGNrO8Rn0oGVuQdFQAZaredlt1V5FsgcSaMgg3rlScoz0IHHD66Q\n" +
"Hglp/IYN6Sx6OEGjh3oLGImag+Mz9/9WWGXPLhZ4MUpFAWqcTD4qPK0jYxTCM6QC\n" +
"TybFqMeCSEKiHSOiOGf2oQ==\n" +
"Private-MAC: 6aec23b6267edcb87b05ddef52a80894e3a246c4";
final static String ppkdsa_passphrase = "PuTTY-User-Key-File-2: ssh-dss\n" + final static String ppkdsa_passphrase = "PuTTY-User-Key-File-2: ssh-dss\n" +
"Encryption: aes256-cbc\n" + "Encryption: aes256-cbc\n" +
"Comment: dsa-key-20140507\n" + "Comment: dsa-key-20140507\n" +
@@ -502,6 +521,15 @@ public class PuTTYKeyFileTest {
assertNotNull(key.getPublic()); assertNotNull(key.getPublic());
} }
@Test
public void testCorrectPassphraseUmlautRsa() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new StringReader(ppk1024_umlaut_passphrase), new UnitTestPasswordFinder("äöü"));
// Install JCE Unlimited Strength Jurisdiction Policy Files if we get java.security.InvalidKeyException: Illegal key size
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());
}
@Test(expected = IOException.class) @Test(expected = IOException.class)
public void testWrongPassphraseRsa() throws Exception { public void testWrongPassphraseRsa() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile(); PuTTYKeyFile key = new PuTTYKeyFile();

View 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;
}
}
}

View File

@@ -0,0 +1,10 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?

View File

@@ -0,0 +1,6 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(),.;'[]/?
abcdefghijklmnopqrstuvwxyzABCDEF

View File

@@ -0,0 +1,3 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABDfrL8SxDyrkNlsJdAmc7Z0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBiAAAAkLVqaDIfs+sPBNyy7ytdLnP/xH7Nt5FIXx3Upw6wKuMGdBzbFQLcvu60Le+SFP3uUfXE8TcHramXbH0n+UBMW6raCAKOkHUU1BtrKxPG1eKU/LBx3Bk5FxyKm7fo0XsCUmqSVK25EHOJfYq1QwIbWICkvQUNu+2Hg8/MQKoFJMentI+GqjdaG76f6Wf+aj9UwA==
-----END OPENSSH PRIVATE KEY-----

Some files were not shown because too many files have changed in this diff Show More