Compare commits

..

13 Commits

Author SHA1 Message Date
Jeroen van Erp
830a39dc24 Prepare release notes for 0.35.0 2023-01-30 14:27:03 +01:00
Jan S
dcfa1833d7 TimeoutException message improved (#835) 2023-01-06 10:05:33 +01: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
45 changed files with 805 additions and 482 deletions

1
.gitattributes vendored
View File

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

View File

@@ -104,6 +104,18 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
Fork away!
== Release history
SSHJ 0.35.0 (2023-01-30)::
* Merged https://github.com/hierynomus/sshj/pull/835[#835]: TimeoutException message improved
* Merged https://github.com/hierynomus/sshj/pull/815[#815]: Support authPassword on FreeBSD
* Merged https://github.com/hierynomus/sshj/pull/813[#813]: Prevent `CHANNEL_CLOSE` between isOpen and write call.
* Merged https://github.com/hierynomus/sshj/pull/811[#811]: Add `Transport.isKeyExchangeREquired` to prevent unnecessary KEXINIT
SSHJ 0.34.0 (2022-08-10)::
* Merged https://github.com/hierynomus/sshj/pull/743[#743]: Use default client credentials for AuthGssApiWithMic
* Merged https://github.com/hierynomus/sshj/pull/801[#801]: Restore thread interrupt status after catching InterruptedException
* Merged https://github.com/hierynomus/sshj/pull/793[#793]: Merge PKCS5 and PKCS8 classes
* Upgraded dependencies SLF4J (1.7.36) and Logback (1.2.11)
* Merged https://github.com/hierynomus/sshj/pull/791[#791]: Update KeepAlive examples
* Merged https://github.com/hierynomus/sshj/pull/775[#775]: Add SFTP resume support
SSHJ 0.33.0 (2022-04-22)::
* Upgraded dependencies BouncyCastle (1.70)
* Merged https://github.com/hierynomus/sshj/pull/687[#687]: Correctly close connection when remote closes connection.

View File

@@ -39,7 +39,7 @@ def bouncycastleVersion = "1.70"
def sshdVersion = "2.8.0"
dependencies {
implementation "org.slf4j:slf4j-api:1.7.32"
implementation "org.slf4j:slf4j-api:1.7.36"
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
implementation "com.jcraft:jzlib:1.1.3"
@@ -53,7 +53,7 @@ dependencies {
testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
testImplementation "ch.qos.logback:logback-classic:1.2.9"
testImplementation "ch.qos.logback:logback-classic:1.2.11"
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
testImplementation 'org.testcontainers:testcontainers:1.16.2'
@@ -65,7 +65,12 @@ license {
mapping {
java = 'SLASHSTAR_STYLE'
}
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java'])
excludes([
'**/djb/Curve25519.java',
'**/sshj/common/Base64.java',
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
'**/files/test_file_*.txt',
])
}
if (!JavaVersion.current().isJava9Compatible()) {

View File

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

View File

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

View File

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

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

@@ -29,7 +29,8 @@ public class ThreadNameProvider {
public static void setThreadName(final Thread thread, final RemoteAddressProvider remoteAddressProvider) {
final InetSocketAddress remoteSocketAddress = remoteAddressProvider.getRemoteSocketAddress();
final String address = remoteSocketAddress == null ? DISCONNECTED : remoteSocketAddress.toString();
final String threadName = String.format("sshj-%s-%s", thread.getClass().getSimpleName(), address);
final long started = System.currentTimeMillis();
final String threadName = String.format("sshj-%s-%s-%d", thread.getClass().getSimpleName(), address, started);
thread.setName(threadName);
}
}

View File

@@ -136,7 +136,7 @@ public class Promise<V, T extends Throwable> {
throws T {
final V value = tryRetrieve(timeout, unit);
if (value == null)
throw chainer.chain(new TimeoutException("Timeout expired"));
throw chainer.chain(new TimeoutException("Timeout expired: " + timeout + " " + unit));
else
return value;
}
@@ -176,6 +176,7 @@ public class Promise<V, T extends Throwable> {
}
return val;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw chainer.chain(ie);
} finally {
lock.unlock();

View File

@@ -35,14 +35,29 @@ public abstract class KeepAlive extends Thread {
setDaemon(true);
}
/**
* KeepAlive enabled based on KeepAlive interval
*
* @return Enabled when KeepInterval is greater than 0
*/
public boolean isEnabled() {
return keepAliveInterval > 0;
}
/**
* Get KeepAlive interval in seconds
*
* @return KeepAlive interval in seconds defaults to 0
*/
public synchronized int getKeepAliveInterval() {
return keepAliveInterval;
}
/**
* Set KeepAlive interval in seconds
*
* @param keepAliveInterval KeepAlive interval in seconds
*/
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval;
}
@@ -60,6 +75,8 @@ public abstract class KeepAlive extends Thread {
TimeUnit.SECONDS.sleep(interval);
}
} catch (InterruptedException e) {
// this is almost certainly a planned interruption, but even so, no harm in setting the interrupt flag
Thread.currentThread().interrupt();
log.trace("{} Interrupted while sleeping", getClass().getSimpleName());
} catch (Exception e) {
// If we weren't interrupted, kill the transport, then this exception was unexpected.

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.DHGexSHA256;
import net.schmizz.sshj.transport.kex.ECDHNistP;
import net.schmizz.sshj.transport.random.BouncyCastleRandom;
import net.schmizz.sshj.transport.random.JCERandom;
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import org.slf4j.Logger;
@@ -162,7 +160,6 @@ public class DefaultConfig
setFileKeyProviderFactories(
new OpenSSHKeyV1KeyFile.Factory(),
new PKCS8KeyFile.Factory(),
new PKCS5KeyFile.Factory(),
new OpenSSHKeyFile.Factory(),
new PuTTYKeyFile.Factory());
}

View File

@@ -551,7 +551,7 @@ public class SSHClient
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
* <ul>
* <li>PKCS8 (OpenSSH uses this format)</li>
* <li>PKCS5</li>
* <li>PEM-encoded PKCS1</li>
* <li>Putty keyfile</li>
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
* </ul>
@@ -810,7 +810,12 @@ public class SSHClient
ThreadNameProvider.setThreadName(conn.getKeepAlive(), trans);
keepAliveThread.start();
}
doKex();
if (trans.isKeyExchangeRequired()) {
log.debug("Initiating Key Exchange for new connection");
doKex();
} else {
log.debug("Key Exchange already completed for new connection");
}
}
/**

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)
throws ConnectionException, TransportException {
final String reqType;
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
+ rwin + " >";
}
public interface TransportRunnable {
void run() throws TransportException, ConnectionException;
}
}

View File

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

View File

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

View File

@@ -261,7 +261,16 @@ public class RemoteFile
private boolean eof;
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
this(maxUnconfirmedReads, 0L, -1L);
this(maxUnconfirmedReads, 0L);
}
/**
*
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
* @param fileOffset Initial offset in file to read from
*/
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
this(maxUnconfirmedReads, fileOffset, -1L);
}
/**

View File

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

View File

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

View File

@@ -71,6 +71,13 @@ public interface Transport
void doKex()
throws TransportException;
/**
* Is Key Exchange required based on current transport status
*
* @return Key Exchange required status
*/
boolean isKeyExchangeRequired();
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
String getClientVersion();

View File

@@ -254,6 +254,16 @@ public final class TransportImpl
kexer.startKex(true);
}
/**
* Is Key Exchange required returns true when Key Exchange is not done and when Key Exchange is not ongoing
*
* @return Key Exchange required status
*/
@Override
public boolean isKeyExchangeRequired() {
return !kexer.isKexDone() && !kexer.isKexOngoing();
}
public boolean isKexDone() {
return kexer.isKexDone();
}

View File

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

View File

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

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.security.KeyPair;
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
/**
* Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption
*/
public class PKCS8KeyFile extends BaseFileKeyProvider {
public static class Factory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ class FileKeyProviderSpec extends Specification {
where:
format | keyfile
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5"
KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
}
}

View File

@@ -59,10 +59,11 @@ public class KeepAliveThreadTerminationTest {
assertEquals(Thread.State.NEW, keepAlive.getState());
fixture.connectClient(sshClient);
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
assertThrows(UserAuthException.class, () -> sshClient.authPassword("bad", "credentials"));
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
fixture.stopClient();
Thread.sleep(STOP_SLEEP);

View File

@@ -20,6 +20,7 @@ import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.scp.server.ScpCommandFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.shell.ProcessShellFactory;
@@ -38,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class SshFixture extends ExternalResource {
public static final String hostkey = "hostkey.pem";
public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db";
public static final String listCommand = OsUtils.isWin32() ? "cmd.exe /C dir" : "ls";
private final SshServer server = defaultSshServer();
private SSHClient client = null;
@@ -110,7 +112,7 @@ public class SshFixture extends ExternalResource {
ScpCommandFactory commandFactory = new ScpCommandFactory();
commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
sshServer.setCommandFactory(commandFactory);
sshServer.setShellFactory(new ProcessShellFactory("ls", "ls"));
sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
return sshServer;
}

View File

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

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();
client.authPassword("test", "test");
Session s = client.startSession();
Session.Command c = s.exec("ls");
Session.Command c = s.exec(SshFixture.listCommand);
IOUtils.readFully(c.getErrorStream());
IOUtils.readFully(c.getInputStream());
c.close();

View File

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

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

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