diff --git a/maverick-base-tests/pom.xml b/maverick-base-tests/pom.xml index 5f472ec3..26cda7c7 100644 --- a/maverick-base-tests/pom.xml +++ b/maverick-base-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 jar maverick-base-tests diff --git a/maverick-base/pom.xml b/maverick-base/pom.xml index 0eb3a479..e5c3888b 100644 --- a/maverick-base/pom.xml +++ b/maverick-base/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-base Base API diff --git a/maverick-base/src/main/java/com/sshtools/common/auth/PasswordKeyboardInteractiveProvider.java b/maverick-base/src/main/java/com/sshtools/common/auth/PasswordKeyboardInteractiveProvider.java index 60f4e882..fff1c41c 100644 --- a/maverick-base/src/main/java/com/sshtools/common/auth/PasswordKeyboardInteractiveProvider.java +++ b/maverick-base/src/main/java/com/sshtools/common/auth/PasswordKeyboardInteractiveProvider.java @@ -106,10 +106,10 @@ public boolean setResponse(String[] answers, Collection additionalPro else instruction = e.getMessage(); - return true; } catch(IOException ex) { con.disconnect(TransportProtocolSpecification.BY_APPLICATION, ex.getMessage()); } + return true; case CHANGING_PASSWORD: if(answers.length < 2) { throw new RuntimeException("Not enough answers!"); diff --git a/maverick-base/src/main/java/com/sshtools/common/events/Event.java b/maverick-base/src/main/java/com/sshtools/common/events/Event.java index 96cd6e39..f6ab1554 100644 --- a/maverick-base/src/main/java/com/sshtools/common/events/Event.java +++ b/maverick-base/src/main/java/com/sshtools/common/events/Event.java @@ -107,6 +107,21 @@ public String getAllAttributes() { return buff.toString(); } + + public String logAttributes() { + StringBuffer buff = new StringBuffer(); + for (String key : eventAttributes.keySet()) { + Object value = eventAttributes.get(key); + if(buff.length() > 0) { + buff.append(" "); + } + buff.append(key); + buff.append("="); + buff.append(String.valueOf(value)); + } + + return buff.toString(); + } /** * Add an attribute to the event diff --git a/maverick-base/src/main/java/com/sshtools/common/events/EventServiceImplementation.java b/maverick-base/src/main/java/com/sshtools/common/events/EventServiceImplementation.java index 70c1d5c1..308f4bf3 100644 --- a/maverick-base/src/main/java/com/sshtools/common/events/EventServiceImplementation.java +++ b/maverick-base/src/main/java/com/sshtools/common/events/EventServiceImplementation.java @@ -141,7 +141,7 @@ public void fireEvent(final Event evt) { } if(Log.isDebugEnabled()) { - Log.debug("Firing {} success={}", getEventName(evt.getId()), evt.getState() ? "true" : "false"); + Log.debug("Firing {} success={} {}", getEventName(evt.getId()), evt.getState() ? "true" : "false", evt.logAttributes()); } Object obj = (Object) evt.getAttribute(EventCodes.ATTRIBUTE_CONNECTION); @@ -174,7 +174,7 @@ public void fireEvent(final Event evt) { } } - public void setProcessAllEventsOnEventException(boolean processAllEventsOnEventException) { + public void setProcessAllEventsOnEventException(boolean processAllEventsOnEventException) { this.processAllEventsOnEventException = processAllEventsOnEventException; } @@ -186,4 +186,5 @@ public void addListener(EventListener listener) { public void removeListener(EventListener listener) { globalListeners.remove(listener); } + } diff --git a/maverick-base/src/main/java/com/sshtools/common/files/direct/NioFile.java b/maverick-base/src/main/java/com/sshtools/common/files/direct/NioFile.java index 8ce7ea92..b401c313 100644 --- a/maverick-base/src/main/java/com/sshtools/common/files/direct/NioFile.java +++ b/maverick-base/src/main/java/com/sshtools/common/files/direct/NioFile.java @@ -452,7 +452,7 @@ protected SftpFileAttributes doGetAttributes() throws FileNotFoundException, IOE permsBldr.withAllWrite(); } var filename = path.getFileName(); - if (filename != null && filename.toString().endsWith(".exe") || filename.toString().endsWith(".com") || filename.toString().endsWith(".cmd")) { + if (filename != null && ( filename.toString().endsWith(".exe") || filename.toString().endsWith(".com") || filename.toString().endsWith(".cmd"))) { permsBldr.withAllExecute(); } bldr.withPermissions(permsBldr.build()); diff --git a/maverick-base/src/main/java/com/sshtools/common/permissions/IPPolicy.java b/maverick-base/src/main/java/com/sshtools/common/permissions/IPPolicy.java index 9f1a0930..a4da4e89 100644 --- a/maverick-base/src/main/java/com/sshtools/common/permissions/IPPolicy.java +++ b/maverick-base/src/main/java/com/sshtools/common/permissions/IPPolicy.java @@ -136,17 +136,25 @@ public void flagAddress(String addr) { public void flagAddress(InetAddress addr) { + if(check(DISABLE_BAN)) { + return; + } + Integer count = flaggedAddressCounts.getOrDefault(addr, 0); if(count >= failedAuthenticationThreshold) { + if(Log.isInfoEnabled()) { Log.info("Temporarily banning IP address {} due to failed authentication count of {}", addr.getHostAddress(), count); + } temporaryBans.put(addr, true); return; } ++count; - Log.info("Flagging IP address {} with failed authentication count of {}", addr.getHostAddress(), count); + if(Log.isInfoEnabled()) { + Log.info("Flagging IP address {} with failed authentication count of {}", addr.getHostAddress(), count); + } flaggedAddressCounts.put(addr, count); } diff --git a/maverick-base/src/main/java/com/sshtools/common/policy/SignaturePolicy.java b/maverick-base/src/main/java/com/sshtools/common/policy/SignaturePolicy.java index e29f2761..ce92e64d 100644 --- a/maverick-base/src/main/java/com/sshtools/common/policy/SignaturePolicy.java +++ b/maverick-base/src/main/java/com/sshtools/common/policy/SignaturePolicy.java @@ -30,16 +30,34 @@ public class SignaturePolicy { Set supportedSignatures = new TreeSet<>(); - - public SignaturePolicy() { + boolean strictMode; + + public SignaturePolicy() { + } + public SignaturePolicy(Collection supportedSignatures) { + this.supportedSignatures.addAll(supportedSignatures); + this.strictMode = false; } - public SignaturePolicy(Collection supportedSignatures) { + public SignaturePolicy(Collection supportedSignatures, boolean strictMode) { this.supportedSignatures.addAll(supportedSignatures); + this.strictMode = strictMode; } public Set getSupportedSignatures() { return Collections.unmodifiableSet(supportedSignatures); } + + public void setSupportedSignatures(Collection supportedSignatures) { + this.supportedSignatures = new TreeSet<>(supportedSignatures); + } + + public boolean isStrictMode() { + return strictMode; + } + + public void setStrictMode(boolean strictMode) { + this.strictMode = strictMode; + } } diff --git a/maverick-base/src/main/java/com/sshtools/common/publickey/SshCertificateAuthority.java b/maverick-base/src/main/java/com/sshtools/common/publickey/SshCertificateAuthority.java index d6c694f1..e066e45e 100644 --- a/maverick-base/src/main/java/com/sshtools/common/publickey/SshCertificateAuthority.java +++ b/maverick-base/src/main/java/com/sshtools/common/publickey/SshCertificateAuthority.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.TimeZone; import com.sshtools.common.ssh.SshException; import com.sshtools.common.ssh.components.SshCertificate; @@ -94,6 +95,22 @@ public static SshCertificate generateCertificate(SshKeyPair key, List criticalOptions, List extensions, SshKeyPair signedBy) throws SshException, IOException { + return generateCertificate(key, serial, type, keyId, validPrincipals, + validityDays, Calendar.DAY_OF_MONTH, TimeZone.getTimeZone("UTC"), + criticalOptions, extensions, signedBy); + } + + public static SshCertificate generateCertificate(SshKeyPair key, + long serial, + int type, + String keyId, + List validPrincipals, + int validity, + int validityTimeUnit, + TimeZone timeZone, + List criticalOptions, + List extensions, + SshKeyPair signedBy) throws SshException, IOException { switch(type) { case SshCertificate.SSH_CERT_TYPE_HOST: @@ -111,9 +128,11 @@ public static SshCertificate generateCertificate(SshKeyPair key, c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); + c.setTimeZone(timeZone); + UnsignedInteger64 validAfter = new UnsignedInteger64(c.getTimeInMillis() / 1000); - c.add(Calendar.DAY_OF_MONTH, validityDays); + c.add(Calendar.DAY_OF_MONTH, validity); UnsignedInteger64 validBefore = new UnsignedInteger64(c.getTimeInMillis() / 1000); @SuppressWarnings("unused") diff --git a/maverick-base/src/main/java/com/sshtools/common/sftp/SftpFileAttributes.java b/maverick-base/src/main/java/com/sshtools/common/sftp/SftpFileAttributes.java index 5d6fdf1f..f9367e5c 100644 --- a/maverick-base/src/main/java/com/sshtools/common/sftp/SftpFileAttributes.java +++ b/maverick-base/src/main/java/com/sshtools/common/sftp/SftpFileAttributes.java @@ -1976,7 +1976,7 @@ public String toPermissionsString() { str.append('-'); break; } - str.append(permissions.map(PosixPermissions::asFileModesString).orElse("")); + str.append(permissions.map(PosixPermissions::asFileModesString).orElse("---------")); return str.toString(); } diff --git a/maverick-base/src/main/java/com/sshtools/common/sftp/SftpStatusException.java b/maverick-base/src/main/java/com/sshtools/common/sftp/SftpStatusException.java index fc505a42..c5a68a13 100644 --- a/maverick-base/src/main/java/com/sshtools/common/sftp/SftpStatusException.java +++ b/maverick-base/src/main/java/com/sshtools/common/sftp/SftpStatusException.java @@ -203,5 +203,83 @@ public static String getStatusText(int status) { return "Unknown status type " + String.valueOf(status); } } + + public static String getStatusMessage(int status) { + switch (status) { + case SSH_FX_OK: + return "SSH_FX_OK"; + case SSH_FX_EOF: + return "SSH_FX_EOF"; + case SSH_FX_NO_SUCH_FILE: + return "SSH_FX_NO_SUCH_FILE"; + case SSH_FX_PERMISSION_DENIED: + return "SSH_FX_PERMISSION_DENIED"; + case SSH_FX_FAILURE: + return "SSH_FX_FAILURE"; + case SSH_FX_BAD_MESSAGE: + return "SSH_FX_BAD_MESSAGE"; + case SSH_FX_NO_CONNECTION: + return "SSH_FX_NO_CONNECTION"; + case SSH_FX_CONNECTION_LOST: + return "SSH_FX_CONNECTION_LOST"; + case SSH_FX_OP_UNSUPPORTED: + return "SSH_FX_OP_UNSUPPORTED"; + case SSH_FX_INVALID_HANDLE: + case INVALID_HANDLE: + return "SSH_FX_INVALID_HANDLE"; + case SSH_FX_NO_SUCH_PATH: + return "SSH_FX_NO_SUCH_PATH"; + case SSH_FX_FILE_ALREADY_EXISTS: + return "SSH_FX_FILE_ALREADY_EXISTS"; + case SSH_FX_WRITE_PROTECT: + return "SSH_FX_WRITE_PROTECT"; + case SSH_FX_NO_MEDIA: + return "SSH_FX_NO_MEDIA"; + case SSH_FX_NO_SPACE_ON_FILESYSTEM: + return "SSH_FX_NO_SPACE_ON_FILESYSTEM"; + case SSH_FX_QUOTA_EXCEEDED: + return "SSH_FX_QUOTA_EXCEEDED"; + case SSH_FX_UNKNOWN_PRINCIPAL: + return "SSH_FX_UNKNOWN_PRINCIPAL"; + case SSH_FX_LOCK_CONFLICT: + return "SSH_FX_LOCK_CONFLICT"; + case SSH_FX_DIR_NOT_EMPTY: + return "SSH_FX_DIR_NOT_EMPTY"; + case SSH_FX_NOT_A_DIRECTORY: + return "SSH_FX_NOT_A_DIRECTORY"; + case SSH_FX_INVALID_FILENAME: + return "SSH_FX_INVALID_FILENAME"; + case SSH_FX_LINK_LOOP: + return "SSH_FX_LINK_LOOP"; + case SSH_FX_CANNOT_DELETE: + return "SSH_FX_CANNOT_DELETE"; + case SSH_FX_INVALID_PARAMETER: + return "SSH_FX_INVALID_PARAMETER"; + case SSH_FX_FILE_IS_A_DIRECTORY: + return "SSH_FX_FILE_IS_A_DIRECTORY"; + case SSH_FX_BYTE_RANGE_LOCK_CONFLICT: + return "SSH_FX_BYTE_RANGE_LOCK_CONFLICT"; + case SSH_FX_BYTE_RANGE_LOCK_REFUSED: + return "SSH_FX_BYTE_RANGE_LOCK_REFUSED"; + case SSH_FX_DELETE_PENDING: + return "SSH_FX_DELETE_PENDING"; + case SSH_FX_FILE_CORRUPT: + return "SSH_FX_FILE_CORRUPT"; + case SSH_FX_OWNER_INVALID: + return "SSH_FX_OWNER_INVALID"; + case SSH_FX_GROUP_INVALID: + return "SSH_FX_GROUP_INVALID"; + case SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK: + return "SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"; + case INVALID_RESUME_STATE: + return "INVALID_RESUME_STATE"; + case ATTRIBUTE_BITS_NOT_AVAILABLE: + return "ATTRIBUTE_BITS_NOT_AVAILABLE"; + case BAD_API_USAGE: + return "BAD_API_USAGE"; + default: + return "SSH_FX_UNKNOWN/" + String.valueOf(status); + } + } } diff --git a/maverick-base/src/main/java/com/sshtools/common/ssh/Channel.java b/maverick-base/src/main/java/com/sshtools/common/ssh/Channel.java index 259fed27..df3a9522 100644 --- a/maverick-base/src/main/java/com/sshtools/common/ssh/Channel.java +++ b/maverick-base/src/main/java/com/sshtools/common/ssh/Channel.java @@ -44,6 +44,8 @@ public interface Channel { void addEventListener(ChannelEventListener listener); + void removeEventListener(ChannelEventListener listener); + void sendChannelRequest(String requestName, boolean wantReply, byte[] data); void sendChannelRequest(String type, boolean wantreply, diff --git a/maverick-base/src/main/java/com/sshtools/common/ssh/SessionChannelServer.java b/maverick-base/src/main/java/com/sshtools/common/ssh/SessionChannelServer.java index 616e26b4..abbd1c6d 100644 --- a/maverick-base/src/main/java/com/sshtools/common/ssh/SessionChannelServer.java +++ b/maverick-base/src/main/java/com/sshtools/common/ssh/SessionChannelServer.java @@ -28,9 +28,9 @@ public interface SessionChannelServer extends SessionChannel { OutputStream getErrorStream(); - void enableRawMode(); + default void pauseDataCaching() { }; - void disableRawMode(); + default void resumeDataCaching() { }; boolean setEnvironmentVariable(String name, String value); } diff --git a/maverick-bc-fips-tests/pom.xml b/maverick-bc-fips-tests/pom.xml index 06e7767b..4a39c661 100644 --- a/maverick-bc-fips-tests/pom.xml +++ b/maverick-bc-fips-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-bc-fips-tests BouncyCastle FIPS Tests diff --git a/maverick-bc-fips/pom.xml b/maverick-bc-fips/pom.xml index ccc9a60f..bff19f97 100644 --- a/maverick-bc-fips/pom.xml +++ b/maverick-bc-fips/pom.xml @@ -6,7 +6,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-bc-fips BouncyCastle FIPS diff --git a/maverick-bc-tests/pom.xml b/maverick-bc-tests/pom.xml index 95cdd7e3..2e66a580 100644 --- a/maverick-bc-tests/pom.xml +++ b/maverick-bc-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-bc-tests BouncyCastle JCE Support Tests diff --git a/maverick-bc/pom.xml b/maverick-bc/pom.xml index f932df7d..ae5c0d8f 100644 --- a/maverick-bc/pom.xml +++ b/maverick-bc/pom.xml @@ -6,7 +6,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-bc BouncyCastle JCE Support diff --git a/maverick-logging/pom.xml b/maverick-logging/pom.xml index 05f23a2b..b08bfaad 100644 --- a/maverick-logging/pom.xml +++ b/maverick-logging/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-logging Logging API diff --git a/maverick-putty/pom.xml b/maverick-putty/pom.xml index 1b311c27..5068f42f 100644 --- a/maverick-putty/pom.xml +++ b/maverick-putty/pom.xml @@ -6,7 +6,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-putty PuTTY Key Support diff --git a/maverick-sshagent-jdk16-sockets/pom.xml b/maverick-sshagent-jdk16-sockets/pom.xml index e037ca8d..03270441 100644 --- a/maverick-sshagent-jdk16-sockets/pom.xml +++ b/maverick-sshagent-jdk16-sockets/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-sshagent-jdk16-sockets JDK16+ Unix Domain Socket Agent Provider diff --git a/maverick-sshagent-jni-sockets/pom.xml b/maverick-sshagent-jni-sockets/pom.xml index f78fcd82..53c1b8df 100644 --- a/maverick-sshagent-jni-sockets/pom.xml +++ b/maverick-sshagent-jni-sockets/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-sshagent-jni-sockets JNI Unix Domain Sockets Key Agent Provider diff --git a/maverick-sshagent-named-pipes/pom.xml b/maverick-sshagent-named-pipes/pom.xml index 3499d4cc..30b56471 100644 --- a/maverick-sshagent-named-pipes/pom.xml +++ b/maverick-sshagent-named-pipes/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-sshagent-named-pipes Named Pipes Key Agent Provider diff --git a/maverick-sshagent/pom.xml b/maverick-sshagent/pom.xml index e36be81e..a9f2ebab 100644 --- a/maverick-sshagent/pom.xml +++ b/maverick-sshagent/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-sshagent Key Agent diff --git a/maverick-synergy-assembly/notes/CHANGES b/maverick-synergy-assembly/notes/CHANGES index ef7ce2a0..0e9f75ec 100644 --- a/maverick-synergy-assembly/notes/CHANGES +++ b/maverick-synergy-assembly/notes/CHANGES @@ -1,12 +1,30 @@ -Maverick Synergy 3.1.1 - 3 Apr, 2024 - -Bug Fixes - o Fixed issue with Terrapin strict kex mode and messages after SSH_MSG_NEWKEYS. +Maverick Synergy 3.1.2 - Sep 20, 2024 + +Bug Fixes + o Added strict mode option to SignaturePolicy for clients to require declaration of signature support in SSH_MSG_EXT_INFO when provided. + o Specifically calculate dates with UTC time zone set to avoid local time zone defaults. + o User and group information can now be defaulted on an AbstractMount. + o Changed default public key preference from ecdsa to ed25519. + o Incorrect parent mount passed to VirtualMountFile when resolving children. + o CommandTaskBulder missing withConnection method. + o Fixes regression found in callback service where `osshell` would fail to send keyboard input. + o Logging indicates that an IP address will be temporarily banned even if banning has been disabled. + o ScpClient and SftpClient have differing strategies for the default sandbox value. By default sandboxing is now turned off for both SFTP and SCP clients. + o Client would hang if 'none' authentication succeeded. + o Ensure attributes for SftpHandle are requested after file has been opened to maintain messaging strict sequence. ================= = HISTORY = ================= +Maverick Synergy 3.1.1 - 3 Apr, 2024 + +Bug Fixes + o SshClient put file throws No connection or client supplied. + o Fixed issue with Terrapin strict kex mode and messages after SSH_MSG_NEWKEYS. + +----------------- + Maverick Synergy 3.1.0 - Feb 20, 2024 Features diff --git a/maverick-synergy-assembly/pom.xml b/maverick-synergy-assembly/pom.xml index 52a104b2..e70c3137 100644 --- a/maverick-synergy-assembly/pom.xml +++ b/maverick-synergy-assembly/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-assembly diff --git a/maverick-synergy-callback-client/pom.xml b/maverick-synergy-callback-client/pom.xml index e6dc7248..cd013efe 100644 --- a/maverick-synergy-callback-client/pom.xml +++ b/maverick-synergy-callback-client/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-callback-client Callback Client API diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackAuthenticationMechanismFactory.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackAuthenticationMechanismFactory.java index 2c3c274b..a7c07469 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackAuthenticationMechanismFactory.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackAuthenticationMechanismFactory.java @@ -31,9 +31,8 @@ import com.sshtools.common.sshd.AbstractServerTransport; public class CallbackAuthenticationMechanismFactory extends DefaultAuthenticationMechanismFactory { - - MutualCallbackAuthenticationProvider provider; + private final MutualCallbackAuthenticationProvider provider; public CallbackAuthenticationMechanismFactory(MutualCallbackAuthenticationProvider provider) { this.provider = provider; diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackClient.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackClient.java index e07593d4..bc8b7f9b 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackClient.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackClient.java @@ -23,9 +23,11 @@ */ import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -57,18 +59,23 @@ public class CallbackClient implements ChannelFactoryListener public static final String CALLBACK_CLIENT = "callbackClient"; - SshEngine ssh = new SshEngine(); - Set clients = new HashSet(); - ExecutorService executor; - List hostKeys = new ArrayList<>(); - ChannelFactory channelFactory; - List defaultPolicies = new ArrayList<>(); - FileFactory fileFactory; + private SshEngine ssh = new SshEngine(); + private ExecutorService executor; + private List hostKeys = new ArrayList<>(); + private ChannelFactory channelFactory; + private List defaultPolicies = new ArrayList<>(); + private FileFactory fileFactory; + private Set clients = Collections.synchronizedSet(new HashSet()); public CallbackClient() { executor = getExecutorService(); EventServiceImplementation.getInstance().addListener(new DisconnectionListener()); channelFactory = new DefaultServerChannelFactory(); + try { + ssh.startup(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } public SshEngine getSshEngine() { @@ -94,6 +101,22 @@ public synchronized CallbackSession start(CallbackConfiguration config, String h return session; } + public void updateMemo(String memo) throws IOException { + synchronized(clients) { + IOException exception = null; + for(var clnt : clients) { + try { + clnt.updateMemo(memo); + } catch (IOException e) { + if(exception == null) + exception = e; + } + } + if(exception != null) + throw exception; + } + } + public synchronized void start(CallbackSession client) { if(Log.isInfoEnabled()) { @@ -166,49 +189,21 @@ class DisconnectionListener implements EventListener { @Override public void processEvent(Event evt) { - switch(evt.getId()) { case EventCodes.EVENT_DISCONNECTED: - final SshConnection con = (SshConnection)evt.getAttribute(EventCodes.ATTRIBUTE_CONNECTION); - if(!executor.isShutdown()) { executor.execute(new Runnable() { public void run() { if(con.containsProperty(CALLBACK_CLIENT)) { CallbackSession client = (CallbackSession) con.getProperty(CALLBACK_CLIENT); - - if(Log.isInfoEnabled()) { + if(client != null) {if(Log.isInfoEnabled()) { Log.info("Disconnected from {}:{}" , - client.getConfig().getServerHost(), - client.getConfig().getServerPort()); - } - - onClientStop(client, con); - con.removeProperty(CALLBACK_CLIENT); - clients.remove(client); - - if(!client.isStopped() && client.getConfig().isReconnect()) { - while(getSshEngine().isStarted()) { - - if(Log.isInfoEnabled()) { - Log.info("Will connect again to {}:{} in {} seconds" , - client.getConfig().getServerHost(), - client.getConfig().getServerPort(), - client.getConfig().getReconnectIntervalMs() / 1000); - } - try { - try { - Thread.sleep(client.getConfig().getReconnectIntervalMs()); - } catch (InterruptedException e1) { - } - client.connect(); - break; - } catch (IOException e) { - } + client.getConfig().getServerHost(), + client.getConfig().getServerPort()); } - } else { - stop(); + con.removeProperty(CALLBACK_CLIENT); + clients.remove(client); } } } @@ -229,6 +224,7 @@ public SshServerContext createContext(SshEngineContext daemonContext, CallbackCo sshContext.setIdleConnectionTimeoutSeconds(0); sshContext.setExtendedIdentificationSanitization(false); + for(SshKeyPair key : hostKeys) { sshContext.addHostKey(key); } diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackConfiguration.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackConfiguration.java index 67172a02..f09b9d65 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackConfiguration.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackConfiguration.java @@ -32,17 +32,15 @@ public class CallbackConfiguration { public static final String DEFAULT_CALLBACK_ID = "CallbackClient"; - String agentName; - String serverHost; - int serverPort = 22; - String remoteUUID; - String localUUID; - Long reconnectIntervalMs; - SshKeyPair privateKey; - SshPublicKey publicKey; - String memo; - String callbackIdentifier = DEFAULT_CALLBACK_ID; - boolean reconnect = true; + private String agentName; + private String serverHost; + private int serverPort = 22; + private Long reconnectIntervalMs; + private SshKeyPair privateKey; + private SshPublicKey publicKey; + private String memo; + private String callbackIdentifier = DEFAULT_CALLBACK_ID; + private boolean reconnect = true; Map properties = new HashMap<>(); diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackServerAuthentication.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackServerAuthentication.java index 79de3b96..1ff5da3d 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackServerAuthentication.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackServerAuthentication.java @@ -31,7 +31,7 @@ public class CallbackServerAuthentication extends AbstractPublicKeyAuthenticationProvider { - Set serverKeys; + private Set serverKeys; CallbackServerAuthentication(Set serverKeys) throws IOException { diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackSession.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackSession.java index 94aa287b..e1072d12 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackSession.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/CallbackSession.java @@ -25,18 +25,15 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import com.sshtools.common.logger.Log; import com.sshtools.common.ssh.GlobalRequest; -import com.sshtools.common.ssh.SshConnection; import com.sshtools.common.ssh.SshException; import com.sshtools.common.util.ByteArrayWriter; -import com.sshtools.server.ServerConnectionStateListener; -import com.sshtools.server.SshServerContext; import com.sshtools.synergy.nio.ConnectRequestFuture; import com.sshtools.synergy.nio.DisconnectRequestFuture; import com.sshtools.synergy.nio.ProtocolContext; +import com.sshtools.synergy.ssh.Connection; import com.sshtools.synergy.ssh.TransportProtocol; /** @@ -46,15 +43,17 @@ */ public class CallbackSession implements Runnable { - CallbackConfiguration config; - CallbackClient app; - ConnectRequestFuture future; - - boolean isStopped = false; - String hostname; - int port; - Map attributes = new HashMap(); - int numberOfAuthenticationErrors = 0; + private final CallbackClient app; + private final String hostname; + private final int port; + + private CallbackConfiguration config; + private ConnectRequestFuture future; + private boolean isStopped = false; + private Map attributes = new HashMap(); + private long reconnectStartedAt = -1; + private Throwable exception; + private Connection con; public CallbackSession(CallbackConfiguration config, CallbackClient app, String hostname, int port) throws IOException { this.config = config; @@ -64,119 +63,130 @@ public CallbackSession(CallbackConfiguration config, CallbackClient app, String } public void run() { - try { - connect(); - } catch (IOException e) { - Log.error("Failed to startup", e); + while(app.getSshEngine().isStarted()) { + + if(isStopped) { + Log.info("Callback to {}:{} has been stopped", hostname, port); + break; + } + + try { + connect(); + } catch (IOException | SshException e) { + exception = e; + Log.error("Connection failed to {}:{}", hostname, port); + } + + if(Log.isInfoEnabled()) { + Log.info("Connection disconnected from {}:{}", hostname, port); + } + + if(!config.isReconnect()) { + break; + } + reconnectStartedAt = System.currentTimeMillis(); + + try { + long interval = config.getReconnectIntervalMs(); + + if(Log.isInfoEnabled()) { + Log.info("Will reconnect to {}:{} in {} seconds", hostname, port, interval / 1000); + } + Thread.sleep(interval); + } catch (InterruptedException e) { + } finally { + reconnectStartedAt = -1; + } + } + } + + public Throwable getLastError() { + return exception; + } + + public long getTimeRemainingUntilReconnect() { + if(reconnectStartedAt == -1) + return -1; + else { + return Math.min(config.getReconnectIntervalMs(), Math.max(0, config.getReconnectIntervalMs() - ( System.currentTimeMillis() - reconnectStartedAt ))); } } + + public void updateMemo(String memo) throws IOException { + GlobalRequest req = new GlobalRequest("memo@jadaptive.com", + con, ByteArrayWriter.encodeString(config.getMemo())); + con.sendGlobalRequest(req, false); + } - public void connect() throws IOException { - - if(isStopped) { - throw new IOException("Client has been stopped"); - } + public void connect() throws IOException, SshException { if(Log.isInfoEnabled()) { Log.info("Connecting to {}:{}", hostname, port); } - synchronized(app) { - if(!app.getSshEngine().isStarted() && !app.getSshEngine().isStarting()) { - if(!app.getSshEngine().startup()) { - throw new IOException("SSH Engine failed to start"); + future = app.getSshEngine().connect( + hostname, + port, + createContext(config)); + + future.waitFor(30000L); + if(future.isDone() && future.isSuccess()) { + + con = future.getConnection(); + + if(!con.isConnected() || con.isDisconnecting()) { + Throwable exception = app.getSshEngine().getLastError(); + if(exception == null) { + throw new IOException("Failed to connect."); } - } - } - - while(app.getSshEngine().isStarted()) { - SshConnection currentConnection = null; - try { - future = app.getSshEngine().connect( - hostname, - port, - createContext(config)); - - future.waitFor(30000L); - if(future.isDone() && future.isSuccess()) { - - currentConnection = future.getConnection(); - currentConnection.getAuthenticatedFuture().waitFor(30000L); - - if(currentConnection.getAuthenticatedFuture().isDone() - && currentConnection.getAuthenticatedFuture().isSuccess()) { - - if(Log.isInfoEnabled()) { - Log.info("Callback {} registering with memo {}", currentConnection.getUUID(), config.getMemo()); - } - GlobalRequest req = new GlobalRequest("memo@jadaptive.com", - currentConnection, ByteArrayWriter.encodeString(config.getMemo())); - currentConnection.sendGlobalRequest(req, false); - app.onClientConnected(this, currentConnection); - if(Log.isInfoEnabled()) { - Log.info("Client is connected to {}:{}", hostname, port); - } - break; - } else { - if(Log.isInfoEnabled()) { - Log.info("Could not authenticate to {}:{}", hostname, port); - } - currentConnection.disconnect(); - } - + else if(exception instanceof IOException) { + throw (IOException)exception; } - - if(Objects.isNull(currentConnection)) { - - if(Log.isInfoEnabled()) { - Log.info("Connection did not complete to {}:{}", hostname, port); - } - - if(!config.isReconnect()) { - break; - } - - try { - long interval = config.getReconnectIntervalMs(); - - if(Log.isInfoEnabled()) { - Log.info("Will reconnect to {}:{} in {} seconds", hostname, port, interval / 1000); - } - Thread.sleep(interval); - } catch (InterruptedException e) { - } + else if(exception instanceof SshException) { + throw (SshException)exception; } - } catch(Throwable e) { - Log.error("{} on {}:{}", - e, - e.getMessage(), - config.getServerHost(), - config.getServerPort()); - - if(Objects.nonNull(currentConnection)) { - currentConnection.disconnect(); + else { + throw new IOException("Failed to connect.", exception); } - - long interval = config.getReconnectIntervalMs(); + } + + con.setProperty(CallbackClient.CALLBACK_CLIENT, CallbackSession.this); + con.getAuthenticatedFuture().waitFor(30000L); + + if(con.getAuthenticatedFuture().isDone() + && con.getAuthenticatedFuture().isSuccess()) { + + if(Log.isInfoEnabled()) { + Log.info("Callback {} registering with memo {}", con.getUUID(), config.getMemo()); + } + updateMemo(config.getMemo()); + app.onClientConnected(this, con); if(Log.isInfoEnabled()) { - Log.info("Reconnecting to {}:{} in {} seconds", hostname, port, interval / 1000); + Log.info("Client is connected to {}:{}", hostname, port); } - try { - Thread.sleep(interval); - } catch (InterruptedException e1) { + + exception = null; + + con.getDisconnectFuture().waitForever(); + } else { + if(Log.isInfoEnabled()) { + Log.info("Could not authenticate to {}:{}", hostname, port); } + + exception = new IOException("Authentication failed."); + con.disconnect(); } + + app.onClientStop(this, con); + con.removeProperty(CallbackClient.CALLBACK_CLIENT); + app.getClients() .remove(this); } + + } protected ProtocolContext createContext(CallbackConfiguration config) throws IOException, SshException { - SshServerContext ctx = app.createContext(app.getSshEngine().getContext(), config); - ctx.addStateListener(new ServerConnectionStateListener() { - public void connected(SshConnection con) { - con.setProperty(CallbackClient.CALLBACK_CLIENT, CallbackSession.this); - } - }); - return ctx; + return app.createContext(app.getSshEngine().getContext(), config); } public void disconnect() { diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthentication.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthentication.java index 7c67ac28..4207a113 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthentication.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthentication.java @@ -45,10 +45,10 @@ public class MutualCallbackAuthentication implements Authenti public static final int SSH_MSG_USERAUTH_SIGNED_CHALLENGE = 60; - AbstractServerTransport transport; - AbstractAuthenticationProtocol authentication; - SshConnection con; - MutualCallbackAuthenticationProvider provider; + private AbstractServerTransport transport; + private AbstractAuthenticationProtocol authentication; + private SshConnection con; + private MutualCallbackAuthenticationProvider provider; public static final String AUTHENTICATION_METHOD = "publickey"; diff --git a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthenticationProvider.java b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthenticationProvider.java index 742b50eb..0f2a44bb 100644 --- a/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthenticationProvider.java +++ b/maverick-synergy-callback-client/src/main/java/com/sshtools/callback/client/MutualCallbackAuthenticationProvider.java @@ -31,7 +31,8 @@ public class MutualCallbackAuthenticationProvider implements Authenticator { public static final String MUTUAL_KEY_AUTHENTICATION = "mutual-key-auth@sshtools.com"; - MutualKeyAuthenticatonStore authenticationStore; + + private final MutualKeyAuthenticatonStore authenticationStore; public MutualCallbackAuthenticationProvider(MutualKeyAuthenticatonStore authenticationStore) { this.authenticationStore = authenticationStore; diff --git a/maverick-synergy-callback-server/pom.xml b/maverick-synergy-callback-server/pom.xml index 34109641..58cb34cb 100644 --- a/maverick-synergy-callback-server/pom.xml +++ b/maverick-synergy-callback-server/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-callback-server Callback Server API diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackContextFactory.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackContextFactory.java index 18396ab9..de7f8beb 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackContextFactory.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackContextFactory.java @@ -86,15 +86,13 @@ public SshServerContext createContext(SshEngineContext daemonContext, SocketChan clientContext.addAuthenticator(new MutualCallbackAuthenticator(store)); clientContext.addStateListener(new ClientStateListener() { - @Override - public void connected(SshConnection con) { - Log.info("Callback client {} connected", con.getUsername()); - callbacks.registerCallbackClient(con); - } - @Override public void disconnected(SshConnection con) { - callbacks.unregisterCallbackClient(con.getUUID()); + if(con.getRemoteIdentification().substring(8).startsWith(callbackIdentifier)) { + Log.info("Callback client {} disconnected", con.getUsername()); + callbacks.unregisterCallbackClient(con.getUUID()); + } + } }); @@ -112,8 +110,9 @@ public boolean processGlobalRequest(GlobalRequest request, ConnectionProtocol extends ForwardingChannel { + static final String DIRECT_STREAM_LOCAL_CHANNEL = "direct-streamlocal@openssh.com"; CallbackForwardingChannel channel; SshConnection callbackClient; final static Integer CHANNEL_QUEUE = ExecutorOperationQueues.generateUniqueQueue("callbackDataQueue"); public CallbackForwardingChannel(Context ctx, SshConnection callbackClient) { - super(LocalForwardingChannel.LOCAL_FORWARDING_CHANNEL_TYPE, + this(LocalForwardingChannel.LOCAL_FORWARDING_CHANNEL_TYPE, ctx, callbackClient); + this.callbackClient = callbackClient; + } + + public CallbackForwardingChannel(String channel, Context ctx, SshConnection callbackClient) { + super(channel, ctx.getPolicy(ForwardingPolicy.class).getForwardingMaxPacketSize(), ctx.getPolicy(ForwardingPolicy.class).getForwardingMaxWindowSize(), ctx.getPolicy(ForwardingPolicy.class).getForwardingMaxWindowSize(), @@ -92,17 +98,44 @@ public CallbackForwardingChannel(String channelType, SshConnection con, String h this.hostToConnect = hostToConnect; this.portToConnect = portToConnect; } + + /** + * Constructs a forwarding channel of the type "forwarded-tcpip" + * + * @param addressToBind + * String + * @param portToBind + * int + * @param socketChannel + * SocketChannel + */ + public CallbackForwardingChannel(String channelType, Context ctx, SshConnection con, String hostToConnect, int portToConnect) { + super(channelType, con.getContext().getPolicy(ForwardingPolicy.class).getForwardingMaxPacketSize(), + ctx.getPolicy(ForwardingPolicy.class).getForwardingMaxWindowSize(), + ctx.getPolicy(ForwardingPolicy.class).getForwardingMaxWindowSize(), + ctx.getPolicy(ForwardingPolicy.class).getForwardingMinWindowSize(), + true); + this.hostToConnect = hostToConnect; + this.portToConnect = portToConnect; + } + /** * Create the forwarding channel. * * @return byte[] */ protected byte[] createChannel() throws IOException { - - - ByteArrayWriter baw = new ByteArrayWriter(); + if(getChannelType().equals(DIRECT_STREAM_LOCAL_CHANNEL)) { + try(var baw = new ByteArrayWriter()) { + baw.writeString(hostToConnect); + baw.writeString(""); // Reserved + baw.writeInt(0); // Reserved + return baw.toByteArray(); + + } + } - try { + try(var baw = new ByteArrayWriter()) { baw.writeString(hostToConnect); baw.writeInt(portToConnect); baw.writeString(originatingHost = con.getRemoteIPAddress()); @@ -110,9 +143,7 @@ protected byte[] createChannel() throws IOException { return baw.toByteArray(); - } finally { - baw.close(); - } + } } /** @@ -130,10 +161,17 @@ protected byte[] openChannel(byte[] requestdata) ByteArrayReader bar = new ByteArrayReader(requestdata); try { - hostToConnect = bar.readString(); - portToConnect = (int) bar.readInt(); - originatingHost = bar.readString(); - originatingPort = (int) bar.readInt(); + if(getChannelType().equals(DIRECT_STREAM_LOCAL_CHANNEL)) { + hostToConnect = bar.readString(); // socket path + bar.readString(); // reserved + bar.readInt(); // reserved + } + else { + hostToConnect = bar.readString(); + portToConnect = (int) bar.readInt(); + originatingHost = bar.readString(); + originatingPort = (int) bar.readInt(); + } boolean success = checkPermissions(); @@ -162,7 +200,7 @@ protected byte[] openChannel(byte[] requestdata) @Override protected void doTask() throws Throwable { - channel = new CallbackForwardingChannel( + channel = new CallbackForwardingChannel(getChannelType(), connection.getContext(), callbackClient, hostToConnect, portToConnect); channel.setBoundChannel(CallbackForwardingChannel.this); diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackRegistrationService.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackRegistrationService.java index b6d7cad1..cb0e56a4 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackRegistrationService.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackRegistrationService.java @@ -33,7 +33,7 @@ public interface CallbackRegistrationService extends SshOptionsResolver { Callback getCallbackByUUID(String uuid); - void registerCallbackClient(SshConnection con); + void registerCallbackClient(SshConnection con, String memo); void unregisterCallbackClient(String uuid); diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackServer.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackServer.java index 90b41228..07e4fc25 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackServer.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/CallbackServer.java @@ -37,6 +37,7 @@ import com.sshtools.common.ssh.components.SshKeyPair; import com.sshtools.common.ssh.components.SshPublicKey; import com.sshtools.server.AbstractSshServer; +import com.sshtools.server.InMemoryPasswordAuthenticator; import com.sshtools.server.SshServerContext; import com.sshtools.server.callback.commands.CallbackCommandFactory; import com.sshtools.server.vsession.VirtualChannelFactory; @@ -57,7 +58,8 @@ public class CallbackServer extends AbstractSshServer { InMemoryMutualKeyAuthenticationStore store = new InMemoryMutualKeyAuthenticationStore(); InMemoryCallbackRegistrationService callbacks = new InMemoryCallbackRegistrationService(); - + InMemoryPasswordAuthenticator users = new InMemoryPasswordAuthenticator(); + public CallbackServer() { super(); } @@ -76,6 +78,7 @@ public CallbackServer(String addressToBind, int port) throws UnknownHostExceptio @Override public ProtocolContextFactory getDefaultContextFactory() { + addAuthenticator(users); return new CallbackContextFactory(store, callbacks, this); } @@ -98,8 +101,14 @@ public ChannelFactory getChannelFactory() { new SshClientsCommandFactory()); } - public void addAgentKey(String username, SshKeyPair privateKey, SshPublicKey publicKey) { + public CallbackServer addAgentKey(String username, SshKeyPair privateKey, SshPublicKey publicKey) { store.addKey(username, privateKey, publicKey); + return this; + } + + public CallbackServer addUser(String username, String password) { + users.addUser(username, password.toCharArray()); + return this; } } diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/DefaultCallback.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/DefaultCallback.java index dee439cd..5dda55ca 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/DefaultCallback.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/DefaultCallback.java @@ -27,10 +27,11 @@ public class DefaultCallback implements Callback { SshConnection con; - String memo = "Undefined"; + String memo; - DefaultCallback(SshConnection con) { + DefaultCallback(SshConnection con, String memo) { this.con = con; + this.memo = memo; } @Override diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/InMemoryCallbackRegistrationService.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/InMemoryCallbackRegistrationService.java index c0223e8b..feb5e1d0 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/InMemoryCallbackRegistrationService.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/InMemoryCallbackRegistrationService.java @@ -120,8 +120,8 @@ public Callback getCallbackByUUID(String uuid) { } @Override - public void registerCallbackClient(SshConnection con) { - callbacks.put(con.getUUID(), new DefaultCallback(con)); + public void registerCallbackClient(SshConnection con, String memo) { + callbacks.put(con.getUUID(), new DefaultCallback(con, memo)); } @Override diff --git a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/commands/CallbackShell.java b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/commands/CallbackShell.java index 9757fc44..fd61d05c 100644 --- a/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/commands/CallbackShell.java +++ b/maverick-synergy-callback-server/src/main/java/com/sshtools/server/callback/commands/CallbackShell.java @@ -68,7 +68,7 @@ public void run(String[] args, VirtualConsole console) withColumns(console.getTerminal().getWidth()). withRows(console.getTerminal().getHeight()). onBeforeTask((task, session) -> { - console.getSessionChannel().enableRawMode(); + console.getSessionChannel().pauseDataCaching(); listener.session = session; ((VirtualShellNG)console.getSessionChannel()).addWindowSizeChangeListener(listener); con.addTask(Task.ofRunnable(con.getConnection(), (c) -> IOUtils.copy(console.getSessionChannel().getInputStream(), session.getOutputStream()))); @@ -77,7 +77,7 @@ public void run(String[] args, VirtualConsole console) onClose((task, session) -> ((VirtualShellNG)console.getSessionChannel()).removeWindowSizeChangeListener(listener)). build()).waitForever(); - console.getSessionChannel().disableRawMode(); + console.getSessionChannel().resumeDataCaching(); console.println(); console.println(String.format("---- Exited shell on %s", clientName)); } diff --git a/maverick-synergy-client-tests/pom.xml b/maverick-synergy-client-tests/pom.xml index f62110f3..b230342a 100644 --- a/maverick-synergy-client-tests/pom.xml +++ b/maverick-synergy-client-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-client-tests Client API Tests diff --git a/maverick-synergy-client/pom.xml b/maverick-synergy-client/pom.xml index a71cc628..6a29176d 100644 --- a/maverick-synergy-client/pom.xml +++ b/maverick-synergy-client/pom.xml @@ -6,7 +6,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-client Client API diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/AbstractSessionChannel.java b/maverick-synergy-client/src/main/java/com/sshtools/client/AbstractSessionChannel.java index e2fec2aa..93ba2c77 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/AbstractSessionChannel.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/AbstractSessionChannel.java @@ -32,6 +32,7 @@ import com.sshtools.common.util.ByteArrayWriter; import com.sshtools.common.util.UnsignedInteger32; import com.sshtools.synergy.ssh.ChannelNG; +import com.sshtools.synergy.ssh.TerminalModes; /** * Implements the client side of the SSH Connection protocol session channel @@ -108,7 +109,7 @@ public RequestFuture allocatePseudoTerminal(String type) { } public RequestFuture allocatePseudoTerminal(String type, int cols, int rows) { - return allocatePseudoTerminal(type, cols, rows, 0, 0, null); + return allocatePseudoTerminal(type, cols, rows, 0, 0, (TerminalModes)null); } public RequestFuture allocatePseudoTerminal(String type, int cols, int rows, PseudoTerminalModes modes) { @@ -175,8 +176,14 @@ public RequestFuture signal(String signal) { } } + @Deprecated(since = "3.1.2", forRemoval = true) public RequestFuture allocatePseudoTerminal(String type, int cols, int rows, int width, int height, PseudoTerminalModes modes) { + return allocatePseudoTerminal(type, cols, rows, width, height, TerminalModes.TerminalModesBuilder.create().fromBytes(modes.toByteArray()).build()); + } + + public RequestFuture allocatePseudoTerminal(String type, int cols, int rows, int width, int height, + TerminalModes modes) { ByteArrayWriter request = new ByteArrayWriter(); diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/AuthenticationProtocolClient.java b/maverick-synergy-client/src/main/java/com/sshtools/client/AuthenticationProtocolClient.java index 45da84c3..0482d5f8 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/AuthenticationProtocolClient.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/AuthenticationProtocolClient.java @@ -151,6 +151,11 @@ public boolean processMessage(byte[] msg) throws IOException, SshException { completedAuthentications.add(currentAuthenticator.getName()); + + if(currentAuthenticator.getName().equals("none")) { + transport.getConnectFuture().connected(transport, transport.getConnection()); + } + currentAuthenticator.success(); diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/PseudoTerminalModes.java b/maverick-synergy-client/src/main/java/com/sshtools/client/PseudoTerminalModes.java index 318c7934..1b934c4d 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/PseudoTerminalModes.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/PseudoTerminalModes.java @@ -29,6 +29,7 @@ import com.sshtools.common.ssh.SshException; import com.sshtools.common.util.ByteArrayWriter; +import com.sshtools.synergy.ssh.TerminalModes; /** * @@ -52,9 +53,12 @@ * build()); * * + *

Deprecated, to be replaced by {@link TerminalModes} that allows the same + * object to be used by the client and the server.

* * @author Lee David Painter */ +@Deprecated(since = "3.1.2", forRemoval = true) public class PseudoTerminalModes { /** diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/PublicKeyAuthenticator.java b/maverick-synergy-client/src/main/java/com/sshtools/client/PublicKeyAuthenticator.java index d3bf82fc..c91ddfb5 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/PublicKeyAuthenticator.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/PublicKeyAuthenticator.java @@ -105,13 +105,7 @@ private boolean setupNextKey() throws IOException, SshException { if(Log.isDebugEnabled()) { Log.debug("Upgrading certificate {} to use {} signature", currentKey.getAlgorithm(), signingAlgorithm); } - } - - - - - - else if(!policy.getSupportedSignatures().contains(signingAlgorithm)) { + } else if(policy.isStrictMode() && !policy.getSupportedSignatures().contains(signingAlgorithm)) { Log.debug("Server does not support {} signature for key {}", currentKey.getSigningAlgorithm(), SshKeyUtils.getOpenSSHFormattedKey(currentKey)); diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/scp/ScpClient.java b/maverick-synergy-client/src/main/java/com/sshtools/client/scp/ScpClient.java index 1f030153..082fda53 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/scp/ScpClient.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/scp/ScpClient.java @@ -67,7 +67,9 @@ public class ScpClient extends ScpClientIO { * @throws PermissionDeniedException */ public ScpClient(SshClient ssh) throws PermissionDeniedException, IOException { - this(NioFileFactoryBuilder.create().build(), ssh); + this(NioFileFactoryBuilder.create() + .withoutSandbox() + .build(), ssh); } /** @@ -78,7 +80,9 @@ public ScpClient(SshClient ssh) throws PermissionDeniedException, IOException { * @throws IOException */ public ScpClient(File cwd, SshClient ssh) throws PermissionDeniedException, IOException { - this(NioFileFactoryBuilder.create().withHome(cwd).build(), ssh); + this(NioFileFactoryBuilder.create() + .withoutSandbox() + .withHome(cwd).build(), ssh); } /** diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpChannel.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpChannel.java index 28c4c6f2..6e16fbf4 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpChannel.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpChannel.java @@ -56,6 +56,7 @@ import com.sshtools.common.ssh.SshConnection; import com.sshtools.common.ssh.SshException; import com.sshtools.common.ssh.SshIOException; +import com.sshtools.common.util.Base64; import com.sshtools.common.util.ByteArrayReader; import com.sshtools.common.util.UnsignedInteger32; import com.sshtools.common.util.UnsignedInteger64; @@ -163,7 +164,7 @@ public class SftpChannel extends AbstractSubsystem { int version = MAX_VERSION; int serverVersion = -1; - UnsignedInteger32 requestId = new UnsignedInteger32(0); + UnsignedInteger32 nextRequestId = new UnsignedInteger32(0); Map responses = new ConcurrentHashMap(); SftpThreadSynchronizer sync = new SftpThreadSynchronizer(); Map extensions = new HashMap(); @@ -518,8 +519,8 @@ public byte[] getExtension(String name) { } UnsignedInteger32 nextRequestId() { - requestId = UnsignedInteger32.add(requestId, 1); - return requestId; + nextRequestId = UnsignedInteger32.add(nextRequestId, 1); + return nextRequestId; } public void close() { @@ -555,7 +556,9 @@ public SftpMessage getResponse(UnsignedInteger32 requestId) throws SshException } } - return (SftpMessage) responses.remove(requestId); + SftpMessage m = (SftpMessage) responses.remove(requestId); + + return m; } @@ -664,23 +667,14 @@ public void changePermissions(String filename, String permissions) * @throws SftpStatusException * , SshException */ - public void getOKRequestStatus(UnsignedInteger32 requestId) + public void getOKRequestStatus(UnsignedInteger32 requestId, String path) throws SftpStatusException, SshException { SftpMessage bar = getResponse(requestId); try { if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - if (status == SftpStatusException.SSH_FX_OK) { - return; - } - - if (version >= 3) { - String msg = bar.readString(); - throw new SftpStatusException(status, msg); - } - throw new SftpStatusException(status); - + processStatusResponse(bar, path, requestId); + return; } close(); throw new SshException( @@ -736,9 +730,13 @@ public void setAttributes(String path, SftpFileAttributes attrs) msg.writeString(path, CHARSET_ENCODING); msg.write(attrs.toByteArray(getVersion())); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_SETSTAT for {}", path); + } + sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, path); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -808,8 +806,9 @@ public UnsignedInteger32 postWriteRequest(byte[] handle, long position, public void writeFile(byte[] handle, UnsignedInteger64 offset, byte[] data, int off, int len) throws SftpStatusException, SshException { - getOKRequestStatus(getBestHandle(handle).postWriteRequest(offset.longValue(), data, - off, len)); + SftpHandle h = getBestHandle(handle); + getOKRequestStatus(h.postWriteRequest(offset.longValue(), data, off, len), + h.getFile() == null ? "" : h.getFile().getAbsolutePath()); } /** @@ -1073,6 +1072,9 @@ public void lockFile(byte[] handle, long offset, long length, int lockFlags) thr "Locks are not supported by the server SFTP version " + String.valueOf(version)); } + + SftpHandle h = getBestHandle(handle); + try { UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); @@ -1083,9 +1085,12 @@ public void lockFile(byte[] handle, long offset, long length, int lockFlags) thr msg.writeUINT64(length); msg.writeInt(lockFlags); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_BLOCK for {}", h.getFile().getAbsolutePath()); + } sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, h.getFile().getAbsolutePath()); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1113,6 +1118,9 @@ public void unlockFile(byte[] handle, long offset, long length) throws SftpStatu "Locks are not supported by the server SFTP version " + String.valueOf(version)); } + + SftpHandle h = getBestHandle(handle); + try { UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); @@ -1122,9 +1130,12 @@ public void unlockFile(byte[] handle, long offset, long length) throws SftpStatu msg.writeUINT64(offset); msg.writeUINT64(length); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_UNBLOCK for {}", h.getFile().getAbsolutePath()); + } sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, h.getFile().getAbsolutePath()); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1152,6 +1163,7 @@ public void createSymbolicLink(String targetpath, String linkpath) "Symbolic links are not supported by the server SFTP version " + String.valueOf(version)); } + try { UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); @@ -1162,10 +1174,16 @@ public void createSymbolicLink(String targetpath, String linkpath) if(version >= 6) { msg.writeBoolean(true); } + + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_SYMLINK to link {} to {}", + linkpath, + targetpath); + } sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, linkpath); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1209,9 +1227,14 @@ public void createLink(String targetpath, String linkpath, boolean symbolic) msg.writeString(targetpath, CHARSET_ENCODING); msg.writeBoolean(symbolic); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_LINK to link {} to {}", + linkpath, + targetpath); + } sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, linkpath); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1247,22 +1270,23 @@ public String getSymbolicLinkTarget(String linkpath) msg.writeInt(requestId.longValue()); msg.writeString(linkpath, CHARSET_ENCODING); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_READLINK for {} requestId={}", linkpath, requestId); + } + sendMessage(msg); SftpMessage fileMsg = getResponse(requestId); if (fileMsg.getType() == SSH_FXP_STATUS) { - int status = (int) fileMsg.readInt(); - if (version >= 3) { - throw new SftpStatusException(status, fileMsg.readString()); + processStatusResponse(fileMsg, linkpath, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); + } else { + try { + SftpFile[] files = extractFiles(fileMsg, null); + return files[0].getAbsolutePath(); + } finally { + fileMsg.release(); } - throw new SftpStatusException(status); - - } - try { - SftpFile[] files = extractFiles(fileMsg, null); - return files[0].getAbsolutePath(); - } finally { - fileMsg.release(); } } catch (SshIOException ex) { throw ex.getRealException(); @@ -1298,9 +1322,14 @@ public String getAbsolutePath(String path) throws SftpStatusException, msg.write(SSH_FXP_REALPATH); msg.writeInt(requestId.longValue()); msg.writeString(path, CHARSET_ENCODING); + + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_REALPATH for {}", path); + } + sendMessage(msg); - return getSingleFileResponse(getResponse(requestId), "SSH_FXP_REALPATH").getAbsolutePath(); + return getSingleFileResponse(getResponse(requestId), "SSH_FXP_REALPATH", path, requestId).getAbsolutePath(); } catch (SshIOException ex) { throw ex.getRealException(); @@ -1318,7 +1347,7 @@ public String getAbsolutePath(String path) throws SftpStatusException, * @throws SshException * @throws SftpStatusException */ - public SftpFile getSingleFileResponse(SftpMessage bar, String messageName) throws SshException, SftpStatusException { + public SftpFile getSingleFileResponse(SftpMessage bar, String messageName, String path, UnsignedInteger32 requestId) throws SshException, SftpStatusException { try { if (bar.getType() == SSH_FXP_NAME) { SftpFile[] files = extractFiles(bar, null); @@ -1331,14 +1360,13 @@ public SftpFile getSingleFileResponse(SftpMessage bar, String messageName) throw SshException.CHANNEL_FAILURE); } + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_NAME with value {}", files[0].getAbsolutePath()); + } return files[0]; } else if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - if (version >= 3) { - String desc = bar.readString(); - throw new SftpStatusException(status, desc); - } - throw new SftpStatusException(status); + processStatusResponse(bar, path, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { close(); throw new SshException( @@ -1508,14 +1536,14 @@ public SftpHandle openFile(String absolutePath, int flags) /** * Open a file. * - * @param absolutePath + * @param path * @param flags * @param attrs * @return SftpFile * @throws SftpStatusException * , SshException */ - public SftpHandle openFile(String absolutePath, int flags, + public SftpHandle openFile(String path, int flags, SftpFileAttributes attrs) throws SftpStatusException, SshException { if (version >= 5) { @@ -1585,7 +1613,7 @@ else if((flags & OPEN_CREATE)==OPEN_CREATE) { } } - return openFileVersion5(absolutePath, newFlags, accessFlags, attrs); + return openFileVersion5(path, newFlags, accessFlags, attrs); } else { if (attrs == null) { attrs = SftpFileAttributesBuilder.ofType( @@ -1594,19 +1622,26 @@ else if((flags & OPEN_CREATE)==OPEN_CREATE) { } try { + + try { + attrs = getAttributes(path); + } catch(SftpStatusException e) { } UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); msg.write(SSH_FXP_OPEN); msg.writeInt(requestId.longValue()); - msg.writeString(absolutePath, CHARSET_ENCODING); + msg.writeString(path, CHARSET_ENCODING); msg.writeInt(flags); msg.write(attrs.toByteArray(getVersion())); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_OPEN for {}", path); + } + sendMessage(msg); - - - SftpFile file = new SftpFile(absolutePath, getAttributes(absolutePath), this, null); - SftpHandle handle = file.handle(getHandleResponse(requestId)); + + SftpFile file = new SftpFile(path, attrs, this, null); + SftpHandle handle = getHandle(requestId, file); EventServiceImplementation.getInstance().fireEvent( (new Event(this, @@ -1623,7 +1658,7 @@ else if((flags & OPEN_CREATE)==OPEN_CREATE) { } } - public SftpHandle openFileVersion5(String absolutePath, int flags, + public SftpHandle openFileVersion5(String path, int flags, int accessFlags, SftpFileAttributes attrs) throws SftpStatusException, SshException { @@ -1634,19 +1669,28 @@ public SftpHandle openFileVersion5(String absolutePath, int flags, } try { + + try { + attrs = getAttributes(path); + } catch(SftpStatusException e) { } + UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); msg.write(SSH_FXP_OPEN); msg.writeInt(requestId.longValue()); - msg.writeString(absolutePath, CHARSET_ENCODING); + msg.writeString(path, CHARSET_ENCODING); msg.writeInt(accessFlags); msg.writeInt(flags); msg.write(attrs.toByteArray(getVersion())); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_OPEN for {}", path); + } + sendMessage(msg); - SftpFile file = new SftpFile(absolutePath, getAttributes(absolutePath), this, null); - SftpHandle handle = file.handle(getHandleResponse(requestId)); + SftpFile file = new SftpFile(path, attrs, this, null); + SftpHandle handle = getHandle(requestId, file); EventServiceImplementation.getInstance().fireEvent( (new Event(this, EventCodes.EVENT_SFTP_FILE_OPENED, @@ -1687,9 +1731,14 @@ public SftpHandle openDirectory(String path) throws SftpStatusException, msg.write(SSH_FXP_OPENDIR); msg.writeInt(requestId.longValue()); msg.writeString(absolutePath, CHARSET_ENCODING); + + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_OPENDIR for {}", path); + } + sendMessage(msg); - return new SftpFile(absolutePath, attrs, this, null).handle(getHandleResponse(requestId)); + return getHandle(requestId, new SftpFile(path, attrs, this, "")); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1717,7 +1766,7 @@ public void closeHandle(byte[] handle) throws SftpStatusException, SshException } } - private SftpHandle getBestHandle(byte[] handle) { + SftpHandle getBestHandle(byte[] handle) { synchronized(handles) { var h = handles.get(handle); if(h != null) { @@ -1759,9 +1808,13 @@ public void removeDirectory(String path) throws SftpStatusException, msg.writeInt(requestId.longValue()); msg.writeString(path, CHARSET_ENCODING); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_RMDIR for {}", path); + } + sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, path); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1776,22 +1829,26 @@ public void removeDirectory(String path) throws SftpStatusException, /** * Remove a file. * - * @param filename + * @param path * @throws SftpStatusException * , SshException */ - public void removeFile(String filename) throws SftpStatusException, + public void removeFile(String path) throws SftpStatusException, SshException { try { UnsignedInteger32 requestId = nextRequestId(); Packet msg = createPacket(); msg.write(SSH_FXP_REMOVE); msg.writeInt(requestId.longValue()); - msg.writeString(filename, CHARSET_ENCODING); + msg.writeString(path, CHARSET_ENCODING); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_REMOVE for {}", path); + } + sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, path); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1803,7 +1860,7 @@ public void removeFile(String filename) throws SftpStatusException, EventCodes.EVENT_SFTP_FILE_DELETED, true)) .addAttribute( EventCodes.ATTRIBUTE_FILE_NAME, - filename)); + path)); } /** @@ -1838,9 +1895,13 @@ public void renameFile(String oldpath, String newpath, int flags) if(version >= 5) { msg.writeInt(flags); } + + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_RENAME from {} to {}", oldpath, newpath); + } sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, newpath); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -1868,7 +1929,7 @@ public void renameFile(String oldpath, String newpath, int flags) */ public SftpFileAttributes getAttributes(String path) throws SftpStatusException, SshException { - return getAttributes(path, SSH_FXP_STAT); + return getAttributes(path, SSH_FXP_STAT, "SSH_FXP_STAT"); } /** @@ -1882,10 +1943,10 @@ public SftpFileAttributes getAttributes(String path) */ public SftpFileAttributes getLinkAttributes(String path) throws SftpStatusException, SshException { - return getAttributes(path, SSH_FXP_LSTAT); + return getAttributes(path, SSH_FXP_LSTAT, "SSH_FXP_LSTAT"); } - protected SftpFileAttributes getAttributes(String path, int messageId) + protected SftpFileAttributes getAttributes(String path, int messageId, String messageName) throws SftpStatusException, SshException { try { UnsignedInteger32 requestId = nextRequestId(); @@ -1911,12 +1972,18 @@ protected SftpFileAttributes getAttributes(String path, int messageId) msg.writeInt(flags); } + + if(Log.isDebugEnabled()) { + Log.debug("Sending {} for {}", + messageName, + path); + } sendMessage(msg); SftpMessage bar = getResponse(requestId); try { - return extractAttributes(bar); + return extractAttributes(bar, path, requestId); } finally { bar.release(); } @@ -1927,20 +1994,17 @@ protected SftpFileAttributes getAttributes(String path, int messageId) } } - SftpFileAttributes extractAttributes(SftpMessage bar) + SftpFileAttributes extractAttributes(SftpMessage bar, String path, UnsignedInteger32 requestId) throws SftpStatusException, SshException { try { if (bar.getType() == SSH_FXP_ATTRS) { + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_ATTRS for {}", path); + } return SftpFileAttributesBuilder.of(bar, getVersion(), getCharsetEncoding()).build(); } else if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - - // Only read the message string if the version is >= 3 - if (version >= 3) { - String msg = bar.readString(); - throw new SftpStatusException(status, msg); - } - throw new SftpStatusException(status); + processStatusResponse(bar, path, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { close(); throw new SshException( @@ -1954,6 +2018,35 @@ SftpFileAttributes extractAttributes(SftpMessage bar) } } + void processStatusResponse(SftpMessage bar, String path, UnsignedInteger32 requestId) throws SftpStatusException, IOException { + + int status = (int) bar.readInt(); + + if(status == SftpStatusException.SSH_FX_OK) { + + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FX_OK for {} requestId={}", + path, requestId); + } + return; + } + + if (version >= 3) { + String desc = bar.readString(); + if(Log.isDebugEnabled()) { + Log.debug("Received {} with message {} for {}", + SftpStatusException.getStatusMessage(status), desc, path); + } + throw new SftpStatusException(status, desc); + } + + if(Log.isDebugEnabled()) { + Log.debug("Received {} for {}", SftpStatusException.getStatusMessage(status), path); + } + + throw new SftpStatusException(status); + } + /** * Get the attributes of a file. * @@ -2005,9 +2098,14 @@ public void makeDirectory(String path, SftpFileAttributes attrs) msg.writeString(path, CHARSET_ENCODING); msg.write(attrs.toByteArray(getVersion())); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_MKDIR for {}", + path); + } + sendMessage(msg); - getOKRequestStatus(requestId); + getOKRequestStatus(requestId, path); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -2015,28 +2113,26 @@ public void makeDirectory(String path, SftpFileAttributes attrs) } } - SftpHandle getHandle(SftpMessage bar, SftpFile file) + SftpHandle getHandle(SftpMessage bar, SftpFile file, UnsignedInteger32 requestId) throws SftpStatusException, SshException { - var response = getHandleResponse(bar); + var response = getHandleResponse(bar, file, requestId); return new SftpHandle(response, this, file); } - public SftpMessage getExtendedReply(UnsignedInteger32 requestId) throws SftpStatusException, SshException { - return getExtendedReply(getResponse(requestId)); + public SftpMessage getExtendedReply(UnsignedInteger32 requestId, String path) throws SftpStatusException, SshException { + return getExtendedReply(getResponse(requestId), path, requestId); } - public SftpMessage getExtendedReply(SftpMessage bar) throws SftpStatusException, SshException { + public SftpMessage getExtendedReply(SftpMessage bar, String path, UnsignedInteger32 requestId) throws SftpStatusException, SshException { try { if (bar.getType() == SSH_FXP_EXTENDED_REPLY) { + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FX_EXTENDED_REPLY"); + } return bar; } else if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - - if (version >= 3) { - String msg = bar.readString(); - throw new SftpStatusException(status, msg); - } - throw new SftpStatusException(status); + processStatusResponse(bar, path, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { close(); throw new SshException( @@ -2050,30 +2146,30 @@ public SftpMessage getExtendedReply(SftpMessage bar) throws SftpStatusException, } } - public byte[] getHandleResponse(UnsignedInteger32 requestId) - throws SftpStatusException, SshException { - return getHandleResponse(getResponse(requestId)); - } - +// public byte[] getHandleResponse(UnsignedInteger32 requestId, String path) +// throws SftpStatusException, SshException { +// return getHandleResponse(getResponse(requestId), path, requestId); +// } +// public SftpHandle getHandle(UnsignedInteger32 requestId, SftpFile file) throws SftpStatusException, SshException { - return new SftpHandle(getHandleResponse(getResponse(requestId)), this, file); + return new SftpHandle(getHandleResponse(getResponse(requestId), file, requestId), this, file); } - public byte[] getHandleResponse(SftpMessage bar) + public byte[] getHandleResponse(SftpMessage bar, SftpFile file, UnsignedInteger32 requestId) throws SftpStatusException, SshException { try { if (bar.getType() == SSH_FXP_HANDLE) { - return bar.readBinaryString(); - } else if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - - if (version >= 3) { - String msg = bar.readString(); - throw new SftpStatusException(status, msg); + byte[] handle = bar.readBinaryString(); + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_HANDLE for {} handle={}", file.getAbsolutePath(), Base64.encodeBytes(handle, true)); } - throw new SftpStatusException(status); + handles.put(handle, new SftpHandle(handle, this, file)); + return handle; + } else if (bar.getType() == SSH_FXP_STATUS) { + processStatusResponse(bar, file.getAbsolutePath(), requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { close(); throw new SshException( @@ -2087,21 +2183,19 @@ public byte[] getHandleResponse(SftpMessage bar) } } - SftpMessage getExtensionResponse(UnsignedInteger32 requestId) + SftpMessage getExtensionResponse(UnsignedInteger32 requestId, String path) throws SftpStatusException, SshException { - + SftpMessage bar = getResponse(requestId); try { if (bar.getType() == SSH_FXP_EXTENDED_REPLY) { + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_EXTENDED_REPLY"); + } return bar; } else if (bar.getType() == SSH_FXP_STATUS) { - int status = (int) bar.readInt(); - - if (version >= 3) { - String msg = bar.readString(); - throw new SftpStatusException(status, msg); - } - throw new SftpStatusException(status); + processStatusResponse(bar, path, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { close(); throw new SshException( diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpClient.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpClient.java index 6b5361c6..526c77d1 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpClient.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpClient.java @@ -770,7 +770,7 @@ private String resolveRemotePath(String path) throws SftpStatusException, SshExc actual = path; } - if (!actual.equals("/") && actual.endsWith("/")) { + if (!Boolean.getBoolean("maverick.disableSlashRemoval") && !actual.equals("/") && actual.endsWith("/")) { return actual.substring(0, actual.length() - 1); } else { return actual; @@ -1038,17 +1038,18 @@ public SftpFile[] ls(String path, String filter, boolean regexFilter, int maximu } } - private SftpHandle openDirectoryHandle(String actual, ByteArrayWriter msg) throws SshException, SftpStatusException { - SftpFile file = new SftpFile(actual, sftp.getAttributes(actual), sftp, null); + private SftpHandle openDirectoryHandle(String path, ByteArrayWriter msg) throws SshException, SftpStatusException { + SftpFile file = new SftpFile(path, sftp.getAttributes(path), sftp, null); try { - return file.handle(sftp.getHandleResponse( - sftp.sendExtensionMessage("open-directory-with-filter@sshtools.com", msg.toByteArray()))); + return sftp.getHandle( + sftp.sendExtensionMessage("open-directory-with-filter@sshtools.com", msg.toByteArray()), + file); } catch (SftpStatusException e) { if (Boolean.getBoolean("maverick.disableLocalFiltering")) { throw new SshException("Remote server does not support server side filtering", SshException.UNSUPPORTED_OPERATION); } - return file.handle(sftp.openDirectory(actual).getHandle()); + return file.handle(sftp.openDirectory(path).getHandle()); } } @@ -2479,7 +2480,9 @@ public void rename(String oldpath, String newpath, boolean posix) msg.writeString(resolveRemotePath(oldpath)); msg.writeString(resolveRemotePath(newpath)); - sftp.getOKRequestStatus(sftp.sendExtensionMessage("posix-rename@openssh.com", msg.toByteArray())); + sftp.getOKRequestStatus( + sftp.sendExtensionMessage("posix-rename@openssh.com", msg.toByteArray()), + newpath); } finally { msg.close(); @@ -2499,7 +2502,8 @@ public void copyRemoteFile(String sourceFile, String destinationFile, boolean ov msg.writeString(resolveRemotePath(destinationFile)); msg.writeBoolean(overwriteDestination); - sftp.getOKRequestStatus(sftp.sendExtensionMessage("copy-file", msg.toByteArray())); + sftp.getOKRequestStatus(sftp.sendExtensionMessage("copy-file", msg.toByteArray()), + destinationFile); } finally { msg.close(); @@ -2866,7 +2870,9 @@ public byte[] getRemoteHash(String remoteFile, long offset, long length, byte[] msg.writeUINT64(length); msg.writeBinaryString(quickCheck); - SftpMessage resp = sftp.getExtensionResponse(sftp.sendExtensionMessage("md5-hash", msg.toByteArray())); + SftpMessage resp = sftp.getExtensionResponse( + sftp.sendExtensionMessage("md5-hash", msg.toByteArray()), + remoteFile); resp.readString(); return resp.readBinaryString(); @@ -2924,8 +2930,10 @@ protected byte[] doCheckHashHandle(byte[] handle, long offset, long length, Remo msg.writeUINT64(length); msg.writeInt(0L); + SftpHandle h = sftp.getBestHandle(handle); return processCheckFileResponse( - sftp.getExtensionResponse(sftp.sendExtensionMessage("check-file-handle", msg.toByteArray())), + sftp.getExtensionResponse(sftp.sendExtensionMessage("check-file-handle", msg.toByteArray()), + h.getFile().getAbsolutePath()), algorithm); } finally { @@ -2933,20 +2941,21 @@ protected byte[] doCheckHashHandle(byte[] handle, long offset, long length, Remo } } - protected byte[] doCheckFileHandle(String filename, long offset, long length, RemoteHash algorithm) + protected byte[] doCheckFileHandle(String path, long offset, long length, RemoteHash algorithm) throws IOException, SftpStatusException, SshException { ByteArrayWriter msg = new ByteArrayWriter(); try { - msg.writeString(filename); + msg.writeString(path); msg.writeString(algorithm.name()); msg.writeUINT64(offset); msg.writeUINT64(length); msg.writeInt(0L); return processCheckFileResponse( - sftp.getExtensionResponse(sftp.sendExtensionMessage("check-file-name", msg.toByteArray())), + sftp.getExtensionResponse(sftp.sendExtensionMessage("check-file-name", msg.toByteArray()), + path), algorithm); } finally { @@ -2999,8 +3008,11 @@ protected byte[] doMD5HashHandle(byte[] handle, long offset, long length, byte[] msg.writeUINT64(length); msg.writeBinaryString(quickCheck); + SftpHandle h = sftp.getBestHandle(handle); + SftpMessage resp = sftp - .getExtensionResponse(sftp.sendExtensionMessage("md5-hash-handle", msg.toByteArray())); + .getExtensionResponse(sftp.sendExtensionMessage("md5-hash-handle", msg.toByteArray()), + h.getFile().getAbsolutePath()); resp.readString(); return resp.readBinaryString(); @@ -3869,7 +3881,7 @@ public void hardlink(String src, String dst) throws SshException, SftpStatusExce msg.writeString(dst); SftpChannel channel = getSubsystemChannel(); UnsignedInteger32 requestId = channel.sendExtensionMessage("hardlink@openssh.com", msg.toByteArray()); - channel.getOKRequestStatus(requestId); + channel.getOKRequestStatus(requestId, dst); } catch (IOException e) { throw new SshException(e); } @@ -3881,7 +3893,7 @@ public String getHomeDirectory(String username) throws SshException, SftpStatusE msg.writeString(username); SftpChannel channel = getSubsystemChannel(); UnsignedInteger32 requestId = channel.sendExtensionMessage("home-directory", msg.toByteArray()); - return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME").getAbsolutePath(); + return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME", "", requestId).getAbsolutePath(); } catch (IOException e) { throw new SshException(e); } @@ -3892,7 +3904,7 @@ public String makeTemporaryFolder() throws SshException, SftpStatusException { SftpChannel channel = getSubsystemChannel(); UnsignedInteger32 requestId = channel.sendExtensionMessage("make-temp-folder", null); - return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME").getAbsolutePath(); + return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME", "", requestId).getAbsolutePath(); } @@ -3900,7 +3912,7 @@ public String getTemporaryFolder() throws SshException, SftpStatusException { SftpChannel channel = getSubsystemChannel(); UnsignedInteger32 requestId = channel.sendExtensionMessage("get-temp-folder", null); - return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME").getAbsolutePath(); + return channel.getSingleFileResponse(channel.getResponse(requestId), "SSH_FXP_NAME", "", requestId).getAbsolutePath(); } public StatVfs statVFS(String path) throws SshException, SftpStatusException { @@ -3911,12 +3923,8 @@ public StatVfs statVFS(String path) throws SshException, SftpStatusException { UnsignedInteger32 requestId = channel.sendExtensionMessage("statvfs@openssh.com", msg.toByteArray()); SftpMessage response = channel.getResponse(requestId); if (response.getType() == SftpChannel.SSH_FXP_STATUS) { - int status = (int) response.readInt(); - if (sftp.version >= 3) { - String errmsg = response.readString(); - throw new SftpStatusException(status, errmsg); - } - throw new SftpStatusException(status); + sftp.processStatusResponse(response, path, requestId); + throw new IllegalStateException("Received unexpected SSH_FX_OK in status response!"); } else { return new StatVfs(response); } diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFile.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFile.java index 16a87773..3867f0f3 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFile.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFile.java @@ -58,7 +58,7 @@ public final class SftpFile { } else { //Remove trailing forward slash - if (absolutePath.endsWith("/")) { + if (!Boolean.getBoolean("maverick.disableSlashRemoval") && absolutePath.endsWith("/")) { absolutePath = absolutePath.substring(0, absolutePath.length() - 1); } diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileInputStream.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileInputStream.java index aaf1634f..60bff7df 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileInputStream.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileInputStream.java @@ -26,10 +26,12 @@ import java.io.InputStream; import java.util.Vector; +import com.sshtools.common.logger.Log; import com.sshtools.common.sftp.SftpStatusException; import com.sshtools.common.ssh.SshException; import com.sshtools.common.ssh.SshIOException; import com.sshtools.common.util.UnsignedInteger32; +import com.sshtools.common.util.UnsignedInteger64; /** * An InputStream to read the contents of a remote file. @@ -42,8 +44,10 @@ public class SftpFileInputStream extends InputStream { private long position; private SftpMessage currentMessage; private int currentMessageRemaining; + private long readPosition = 0; private boolean isEOF = false; private boolean error = false; + private UnsignedInteger64 length; /** * @@ -75,19 +79,10 @@ public SftpFileInputStream(SftpFile file) throws SftpStatusException, @Deprecated(since = "3.1.0", forRemoval = true) public SftpFileInputStream(SftpFile file, long position) throws SftpStatusException, SshException { this.sftp = file.getSFTPChannel(); + this.length = file.attributes().size(); this.handle = file.openFile(SftpChannel.OPEN_READ); this.position = position; - } - - /** - * - * @param handle handle - * @throws SftpStatusException - * @throws SshException - */ - SftpFileInputStream(SftpHandle handle) throws SftpStatusException, - SshException { - this(handle, 0); + } /** @@ -99,10 +94,11 @@ public SftpFileInputStream(SftpFile file, long position) throws SftpStatusExcept * @throws SftpStatusException * @throws SshException */ - SftpFileInputStream(SftpHandle handle, long position) { + SftpFileInputStream(SftpHandle handle, long position) throws SftpStatusException, SshException { this.handle = handle; this.position = position; this.sftp = handle.getSFTPChannel(); + this.length = handle.getFile().attributes().size(); } /* @@ -139,6 +135,7 @@ public int read(byte[] buffer, int offset, int len) throws IOException { System.arraycopy(currentMessage.array(), currentMessage.getPosition(), buffer, offset, count); + readPosition += count; currentMessageRemaining -= count; currentMessage.skip(count); @@ -174,19 +171,44 @@ private void bufferNextMessage() throws SshException, IOException, currentMessage = sftp.getResponse(requestid); if (currentMessage.getType() == SftpChannel.SSH_FXP_DATA) { + currentMessageRemaining = (int) currentMessage.readInt(); + + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_DATA with {} bytes at position {} for {} requestId={}", + currentMessageRemaining, + readPosition, + handle.getFile().getFilename(), + requestid); + } + + } else if (currentMessage.getType() == SftpChannel.SSH_FXP_STATUS) { try { int status = (int) currentMessage.readInt(); if (status == SftpStatusException.SSH_FX_EOF) { + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FX_EOF for {}", handle.getFile().getFilename()); + } isEOF = true; return; } if (sftp.getVersion() >= 3) { String desc = currentMessage.readString(); + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_STATUS {}/{} for {}", + status, + desc, + handle.getFile().getFilename()); + } + throw new IOException(desc); } + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_STATUS {} for {}", + status, handle.getFile().getFilename()); + } throw new IOException("Unexpected status " + status); } finally { currentMessage.release(); @@ -207,7 +229,20 @@ private void bufferNextMessage() throws SshException, IOException, } private void bufferMoreData() throws SftpStatusException, SshException { - while (outstandingRequests.size() < 100) { + + /** + * Read up to length of file + */ + while (outstandingRequests.size() < 100 && length.longValue() > position) { + outstandingRequests.addElement(handle.postReadRequest(position, 32768)); + position += 32768; + } + + /** + * If there are no requests then add one to ensure we are still reading if file size + * has changed. + */ + if(outstandingRequests.isEmpty()) { outstandingRequests.addElement(handle.postReadRequest(position, 32768)); position += 32768; } @@ -237,12 +272,18 @@ public void close() throws IOException { try { handle.close(); - UnsignedInteger32 requestid; - while (!error && outstandingRequests.size() > 0) { - requestid = (UnsignedInteger32) outstandingRequests - .elementAt(0); - outstandingRequests.remove(0); - sftp.getResponse(requestid).release(); + if(!error && outstandingRequests.size() > 0) { + if(Log.isWarnEnabled()) { + Log.warn("Discarding {} data messages through premature closing of InputStream for file {}", outstandingRequests.size(), handle.getFile().getFilename()); + } + UnsignedInteger32 requestid; + while (!error && outstandingRequests.size() > 0) { + + + requestid = (UnsignedInteger32) outstandingRequests.elementAt(0); + outstandingRequests.remove(0); + sftp.getResponse(requestid).release(); + } } } catch (SshException ex) { throw new SshIOException(ex); diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileOutputStream.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileOutputStream.java index 6e0549c1..84217d8a 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileOutputStream.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpFileOutputStream.java @@ -131,7 +131,7 @@ private boolean processNextResponse(int numOutstandingRequests) throws SftpStatu // Maybe look for a response if (outstandingRequests.size() > numOutstandingRequests) { UnsignedInteger32 requestid = (UnsignedInteger32) outstandingRequests.elementAt(0); - sftp.getOKRequestStatus(requestid); + sftp.getOKRequestStatus(requestid, handle.getFile().getAbsolutePath()); outstandingRequests.removeElementAt(0); } diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpHandle.java b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpHandle.java index 9e10ec6a..9ae66a7b 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpHandle.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/sftp/SftpHandle.java @@ -29,6 +29,7 @@ import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Vector; @@ -56,7 +57,7 @@ public final class SftpHandle implements Closeable { private volatile boolean closed; private volatile boolean performVerification = false; - + SftpHandle(byte[] handle, SftpChannel sftp, SftpFile file) { super(); this.handle = handle; @@ -162,7 +163,9 @@ public void copyTo(SftpHandle destinationHandle, UnsignedInteger64 fromOffset, U msg.writeBinaryString(destinationHandle.getHandle()); msg.writeUINT64(toOffset); - sftp.getOKRequestStatus(sftp.sendExtensionMessage("copy-data", msg.toByteArray())); + sftp.getOKRequestStatus( + sftp.sendExtensionMessage("copy-data", msg.toByteArray()), + file.getAbsolutePath()); } } @@ -243,9 +246,13 @@ public void close() throws IOException { msg.writeInt(requestId.longValue()); msg.writeBinaryString(handle); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_CLOSE for {} requestId={}", file.getFilename(), requestId); + } + sftp.sendMessage(msg); - sftp.getOKRequestStatus(requestId); + sftp.getOKRequestStatus(requestId, file.getAbsolutePath()); } catch (SshException | SshIOException | SftpStatusException ex) { throw new IOException("Failed to close handle.", ex); } @@ -299,9 +306,10 @@ public int listChildren(List children) throws SftpStatusException, Ssh sftp.sendMessage(msg); - if (Log.isDebugEnabled()) { - Log.debug("Sending list children request"); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_READDIR for {} requestId={}", file.getFilename(), requestId); } + SftpMessage bar = sftp.getResponse(requestId); try { @@ -314,7 +322,7 @@ public int listChildren(List children) throws SftpStatusException, Ssh SftpFile[] files = sftp.extractFiles(bar, file.getAbsolutePath()); if (Log.isDebugEnabled()) { - Log.debug("THere are {} results in this packet", files.length); + Log.debug("There are {} results in this packet", files.length); } for (int i = 0; i < files.length; i++) { @@ -373,9 +381,13 @@ public void setAttributes(SftpFileAttributes attrs) throws SftpStatusException, msg.writeBinaryString(handle); msg.write(attrs.toByteArray(sftp.getVersion())); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_FSETSTAT for {} requestId=", file.getFilename(), requestId); + } + sftp.sendMessage(msg); - sftp.getOKRequestStatus(requestId); + sftp.getOKRequestStatus(requestId, file.getAbsolutePath()); } catch (SshIOException ex) { throw ex.getRealException(); } catch (IOException ex) { @@ -410,11 +422,17 @@ public SftpFileAttributes getAttributes() throws SftpStatusException, SshExcepti | SftpFileAttributes.SSH_FILEXFER_ATTR_SUBSECOND_TIMES | SftpFileAttributes.SSH_FILEXFER_ATTR_EXTENDED); } + + + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_FSTAT for {} requestId=", file.getFilename(), requestId); + } + sftp.sendMessage(msg); SftpMessage attrMessage = sftp.getResponse(requestId); try { - return sftp.extractAttributes(attrMessage); + return sftp.extractAttributes(attrMessage, getFile().getFilename(), requestId); } finally { attrMessage.release(); } @@ -515,7 +533,7 @@ public void performOptimizedWrite(String filename, int blocksize, int maxAsyncRe progress.progressed(transfered); } - Vector requests = new Vector(); + List requests = new ArrayList(); // BufferedInputStream is not in J2ME, whatever type of input stream // has been passed in can be used in conjunction with the abstract // InputStream class. @@ -527,7 +545,7 @@ public void performOptimizedWrite(String filename, int blocksize, int maxAsyncRe if (buffered == -1) break; - requests.addElement(postWriteRequest(transfered, buf, 0, buffered)); + requests.add(postWriteRequest(transfered, buf, 0, buffered)); transfered += buffered; @@ -540,16 +558,14 @@ public void performOptimizedWrite(String filename, int blocksize, int maxAsyncRe } if (requests.size() > maxAsyncRequests) { - sftp.requestId = (UnsignedInteger32) requests.elementAt(0); - requests.removeElementAt(0); - sftp.getOKRequestStatus(sftp.requestId); - + UnsignedInteger32 requestId = (UnsignedInteger32) requests.remove(0); + sftp.getOKRequestStatus(requestId, file.getAbsolutePath()); } } while (requests.size() > 0) { - sftp.getOKRequestStatus(requests.remove(0)); + sftp.getOKRequestStatus(requests.remove(0), file.getAbsolutePath()); } } @@ -604,6 +620,10 @@ public int readFile(UnsignedInteger64 offset, byte[] output, int off, int len) msg.write(offset.toByteArray()); msg.writeInt(len); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_READ for {} bytes at position {} for {} requestId={}", len, offset.toString(), file.getFilename(), requestId); + } + sftp.sendMessage(msg); SftpMessage bar = sftp.getResponse(requestId); @@ -613,10 +633,10 @@ public int readFile(UnsignedInteger64 offset, byte[] output, int off, int len) byte[] msgdata = bar.readBinaryString(); System.arraycopy(msgdata, 0, output, off, msgdata.length); - if (Log.isDebugEnabled()) { - Log.debug("Received SSH_FXP_DATA channel={} requestId={} offset={} blocksize={}", - sftp.getSession().getLocalId(), requestId.toString(), offset.toString(), msgdata.length); + if(Log.isDebugEnabled()) { + Log.debug("Received SSH_FXP_DATA with {} bytes at position {} for {} requestId={}", msgdata.length, offset.toString(), file.getFilename(), requestId); } + return msgdata.length; } else if (bar.getType() == SftpChannel.SSH_FXP_STATUS) { int status = (int) bar.readInt(); @@ -668,9 +688,8 @@ public UnsignedInteger32 postReadRequest(long offset, int len) throws SftpStatus msg.writeUINT64(offset); msg.writeInt(len); - if (Log.isDebugEnabled()) { - Log.debug("Sending SSH_FXP_READ channel={} requestId={} offset={} blocksize={}", sftp.getSession().getLocalId(), - requestId.toString(), offset, len); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_READ for {} bytes at position {} for {} requestId={}", len, offset, file.getFilename(), requestId); } sftp.sendMessage(msg); @@ -852,6 +871,7 @@ public void performOptimizedRead(long length, int blocksize, OutputStream out, } catch (IOException e) { throw new TransferCancelledException(); } + transfered += dataLen; if (progress != null) { progress.progressed(transfered); @@ -926,7 +946,8 @@ public void performOptimizedRead(long length, int blocksize, OutputStream out, baw.writeBinaryString(new byte[0]); SftpMessage reply = sftp.getExtensionResponse( - sftp.sendExtensionMessage("md5-hash-handle", baw.toByteArray())); + sftp.sendExtensionMessage("md5-hash-handle", baw.toByteArray()), + file.getAbsolutePath()); reply.readString(); byte[] remoteDigest = reply.readBinaryString(); @@ -1076,6 +1097,10 @@ public UnsignedInteger32 postWriteRequest(long position, byte[] data, int off, i msg.writeUINT64(position); msg.writeBinaryString(data, off, len); + if(Log.isDebugEnabled()) { + Log.debug("Sending SSH_FXP_WRITE with {} bytes at position {} for {} requestId={}", len, position, file.getFilename(), requestId); + } + sftp.sendMessage(msg); return requestId; diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/AbstractConnectionTask.java b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/AbstractConnectionTask.java index ac2ae567..b1e4231a 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/AbstractConnectionTask.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/AbstractConnectionTask.java @@ -81,7 +81,7 @@ public B withClients(Function clientSupplier) { protected final Optional> clientSupplier; protected AbstractConnectionTask(AbstractConnectionTaskBuilder builder) { - super(builder.connection.orElse(builder.clientSupplier.orElseThrow(() -> new IllegalArgumentException("No connection or client supplied.")).apply(0).getConnection())); + super(builder.connection.orElseGet(() -> builder.clientSupplier.orElseThrow(() -> new IllegalArgumentException("No connection or client supplied.")).apply(0).getConnection())); this.clientSupplier = builder.clientSupplier; } diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/PushTask.java b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/PushTask.java index 4d18ae5f..6625dc58 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/PushTask.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/PushTask.java @@ -390,7 +390,7 @@ private Collection sendChunks(AbstractFile localFile, String remoteFo requestId = primarySftpClient.getSubsystemChannel().sendExtensionMessage("create-multipart-file@sshtools.com", msg.toByteArray()); - SftpMessage ext = primarySftpClient.getSubsystemChannel().getExtendedReply(requestId); + SftpMessage ext = primarySftpClient.getSubsystemChannel().getExtendedReply(requestId, remotePath); byte[] handle = ext.readBinaryString(); var blocksize = ext.readInt(); diff --git a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/ShellTask.java b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/ShellTask.java index df6a338c..a621b1bc 100644 --- a/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/ShellTask.java +++ b/maverick-synergy-client/src/main/java/com/sshtools/client/tasks/ShellTask.java @@ -33,6 +33,7 @@ import com.sshtools.common.shell.ShellPolicy; import com.sshtools.common.ssh.SshConnection; import com.sshtools.common.ssh.SshException; +import com.sshtools.synergy.ssh.TerminalModes; /** * A {@link Task} that starts a remote shell with an allocated PTY. @@ -78,7 +79,7 @@ public final static class ShellTaskBuilder extends AbstractSessionTaskBuilder modes = Optional.empty(); + private Optional modes = Optional.empty(); private boolean autoConsume; private ShellTaskBuilder() { @@ -169,7 +170,19 @@ public final ShellTaskBuilder withRows(int rows) { * @param modes modes * @return builder for chaining */ + @Deprecated(since = "3.1.2", forRemoval = true) public final ShellTaskBuilder withModes(PseudoTerminalModes modes) { + return withModes(TerminalModes.TerminalModesBuilder.create().fromBytes(modes.toByteArray()).build()); + } + + /** + * Set the terminal modes to use when allocating a PTY. Note, this + * will have no effect if {{@link #withPty} is set to false. + * + * @param modes modes + * @return builder for chaining + */ + public final ShellTaskBuilder withModes(TerminalModes modes) { this.modes = Optional.of(modes); return this; } @@ -272,7 +285,7 @@ public ShellTaskBuilder withoutPty() { private final int rows; private final int cols; private final boolean withPty; - private final Optional modes; + private final Optional modes; private final boolean autoConsume; private ShellTask(ShellTaskBuilder builder) { diff --git a/maverick-synergy-common-tests/pom.xml b/maverick-synergy-common-tests/pom.xml index e064a98e..e5b2514b 100644 --- a/maverick-synergy-common-tests/pom.xml +++ b/maverick-synergy-common-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-common-tests Common API Tests diff --git a/maverick-synergy-common/pom.xml b/maverick-synergy-common/pom.xml index faeaad2f..d9ddd1e3 100644 --- a/maverick-synergy-common/pom.xml +++ b/maverick-synergy-common/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-common Common API diff --git a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/ChannelNG.java b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/ChannelNG.java index 8732a05b..821a9e45 100644 --- a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/ChannelNG.java +++ b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/ChannelNG.java @@ -29,6 +29,7 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -98,9 +99,9 @@ public abstract class ChannelNG implements Channel { protected SshConnection con; private ChannelInputStream channelIn; private ChannelOutputStream channelOut = new ChannelOutputStream(this); - private final boolean autoConsume; + private boolean autoConsume; + protected boolean paused; - @Deprecated(forRemoval = true, since = "3.1.0") public ChannelNG(String channelType, int maximumPacketSize, int initialWindowSize, int maximumWindowSpace, int minimumWindowSpace, ChannelRequestFuture closeFuture, boolean autoConsume) { this(channelType, maximumPacketSize, new UnsignedInteger32(initialWindowSize), new UnsignedInteger32(maximumWindowSpace), new UnsignedInteger32(minimumWindowSpace), closeFuture, autoConsume); @@ -142,6 +143,10 @@ public final boolean isAutoConsume() { return autoConsume; } + public void setAutoconsume(boolean autoConsume) { + this.autoConsume = autoConsume; + } + public InputStream getInputStream() { if(Objects.nonNull(channelIn)) { return channelIn; @@ -242,6 +247,17 @@ public void addEventListener(ChannelEventListener listener) { } } + /** + * Stop listening for channel events + * + * @param listener + */ + public void removeEventListener(ChannelEventListener listener) { + if (listener != null) { + eventListeners.remove(listener); + } + } + /** * The name of this channel. * @@ -354,7 +370,7 @@ void confirmOpen() { openFuture.done(true); onChannelOpenConfirmation(); - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelOpen(this); } @@ -385,7 +401,7 @@ void adjustWindow(UnsignedInteger32 count) { onWindowAdjust(count); - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onWindowAdjust(this, remoteWindow.getWindowSpace().longValue()); } @@ -444,14 +460,14 @@ void consumeWindowSpace(int length) throws IOException { } protected void onChannelData(ByteBuffer data) { - for (ChannelEventListener listener : eventListeners) { - listener.onChannelDataIn(this, data); + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { + listener.onChannelDataIn(this, data.asReadOnlyBuffer()); } - if(Objects.nonNull(cache)) { + if(!paused && !autoConsume && Objects.nonNull(cache)) { try { cache.put(data); } catch (EOFException e) { - Log.error("Attempt to write data to channel cache failed because the cache is closed"); + Log.error("Attempt to write data to channel cache failed because the cache is closed", e); close(); } } else { @@ -577,7 +593,7 @@ public void sendChannelDataAndBlock(ByteBuffer buf, int type, Runnable r) throws processedBuffer.remaining(), processedBuffer.position(), processedBuffer.limit(), processedBuffer.capacity()); } - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelDataOut(this, processedBuffer); } connection.sendMessage(new ChannelData(processedBuffer, type, window)); @@ -587,7 +603,7 @@ public void sendChannelDataAndBlock(ByteBuffer buf, int type, Runnable r) throws Log.trace("Final Buffer rem={} pos={} limit={}, capacity={}", buf.remaining(), buf.position(), buf.limit(), buf.capacity()); } - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelDataOut(this, buf); } connection.sendMessage(lastMessage = new ChannelData(buf, type, window)); @@ -678,7 +694,7 @@ void processExtendedData(int type, ByteBuffer data) throws IOException { * @param data */ protected void onExtendedData(ByteBuffer data, int type) { - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelExtendedData(this, data, type); } if(Objects.isNull(cache)) { @@ -688,7 +704,7 @@ protected void onExtendedData(ByteBuffer data, int type) { void processChannelEOF() { - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelEOF(this); } @@ -812,7 +828,7 @@ protected void close(boolean forceClose) { sentClose.set(true); doSend = true; - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelClosing(this); } @@ -843,7 +859,7 @@ protected void close(boolean forceClose) { if(forceClose) { this.forcedClose = true; - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelError(this, closingError != null ? closingError : new IOException("Channel has been forced to close")); } @@ -879,7 +895,7 @@ public void run() { if(Log.isTraceEnabled()) { log("Completing", "the close operation"); } - for (ChannelEventListener listener : eventListeners) { + for (ChannelEventListener listener : new ArrayList<>(eventListeners)) { listener.onChannelClose(ChannelNG.this); } eventListeners.clear(); @@ -1443,6 +1459,7 @@ public int read(byte[] b, int off, int len) throws IOException { try { streamCache.waitFor(1000); } catch (InterruptedException e) { + throw new InterruptedIOException("The thread was interrupted"); } } diff --git a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/Connection.java b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/Connection.java index d9c9dfda..32de50d3 100644 --- a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/Connection.java +++ b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/Connection.java @@ -434,4 +434,11 @@ public void setRemoteAddress(InetSocketAddress remoteAddress) { this.remoteAddress = remoteAddress; } + @Override + public String toString() { + return transport.getUUID(); + } + + + } diff --git a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/SshContext.java b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/SshContext.java index 5c57b033..f3798052 100644 --- a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/SshContext.java +++ b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/SshContext.java @@ -165,6 +165,7 @@ public abstract class SshContext extends ProtocolContext implements public static final String KEX_DIFFIE_HELLMAN_ECDH_NISTP_521 = "ecdh-sha2-nistp521"; /** SSH2 DSA Public Key **/ + @Deprecated public static final String PUBLIC_KEY_SSHDSS = "ssh-dss"; /** ED25519 Public key */ @@ -226,7 +227,8 @@ public abstract class SshContext extends ProtocolContext implements protected String prefCompressionSC = COMPRESSION_NONE; protected String prefKeyExchange = KEX_DIFFIE_HELLMAN_GROUP_EXCHANGE_SHA256; - protected String prefPublicKey = PUBLIC_KEY_ECDSA_SHA2_NISPTP_256; + + protected String prefPublicKey = PUBLIC_KEY_ED25519; protected int maxChannels = 100; diff --git a/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/TerminalModes.java b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/TerminalModes.java new file mode 100644 index 00000000..37211a11 --- /dev/null +++ b/maverick-synergy-common/src/main/java/com/sshtools/synergy/ssh/TerminalModes.java @@ -0,0 +1,839 @@ +package com.sshtools.synergy.ssh; + +/*- + * #%L + * Client API + * %% + * Copyright (C) 2002 - 2024 JADAPTIVE Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.sshtools.common.util.ByteArrayReader; +import com.sshtools.common.util.ByteArrayWriter; + +/** + *

When a client requests a pseudo terminal it informs the server of + * any terminal modes that it knows of. This is typically used in + * situations where advance terminal configuration is required but + * it can also be used to perform simple configuration such as turning + * off character echo.

+ * + *

NOTE: the server may ignore some of the modes set if it does not + * support them.

+ * + *
+ * var session = ssh.openSessionChannel();
+ * session.requestPseudoTerminal("vt100", 80, 24, 0, 0, TerminalModesBuilder.create().
+ * 			// Turning off echo
+ * 			withMode(TerminalModes.Mode.ECHO, false).
+ * 			// Setting the Input/Output baud rate
+ * 			withMode(TerminalModes.Mode.TTY_OP_ISPEED, 38400).
+ * 			withMode(TerminalModes.Mode.TTY_OP_OSPEED, 38400).
+ * 			build());
+ * 
+ * + * @author Brett Smith + * @author Lee David Painter + */ +public final class TerminalModes { + + /** + * Enumeration of supported modes. + */ + public enum Mode { + + /** + * Interrupt character; 255 if none. + */ + VINTR, + + /** + * The quit character (sends SIGQUIT signal on POSIX systems). + */ + VQUIT, + + /** + * Erase the character to left of the cursor. + */ + VERASE, + + /** + * Kill the current input line. + */ + VKILL, + + /** + * End-of-file character (sends EOF from the terminal). + */ + VEOF, + + /** + * End-of-line character in addition to carriage return and/or linefeed. + */ + VEOL, + + /** + * Additional end-of-line character. + */ + VEOL2, + + /** + * Continues paused output (normally control-Q). + */ + VSTART, + + /** + * Pauses output (normally control-S).. + */ + VSTOP, + + /** + * Suspends the current program. + */ + VSUSP, + + /** + * Another suspend character. + */ + VDSUSP, + + /** + * Reprints the current input line. + */ + VREPRINT, + + /** + * Erases a word left of cursor. + */ + VWERASE, + + /** + * Enter the next character typed literally, even if it is a special character + */ + VLNEXT, + + /** + * Character to flush output. + */ + VFLUSH, + + /** + * Switch to a different shell layer. + */ + VSWITCH, + + /** + * Prints system status line (load, command, pid, etc). + */ + VSTATUS, + + /** + * Toggles the flushing of terminal output. + */ + VDISCARD, + + /** + * The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, + * and 1 if it is TRUE. + */ + IGNPAR, + + /** + * Mark parity and framing errors. + */ + PARMRK, + + /** + * Enable checking of parity errors. + */ + INPCK, + + /** + * Strip 8th bit off characters. + */ + ISTRIP, + + /** + * Map NL into CR on input. + */ + INLCR, + + /** + * Ignore CR on input. + */ + IGNCR, + + /** + * Map CR to NL on input. + */ + ICRNL, + + /** + * Translate uppercase characters to lowercase. + */ + IUCLC, + + /** + * Enable output flow control. + */ + IXON, + + /** + * Any char will restart after stop. + */ + IXANY, + + /** + * Enable input flow control. + */ + IXOFF, + + /** + * Ring bell on input queue full. + */ + IMAXBEL, + + /** + * Output is assumed to be UTF-8 + */ + IUTF8, + + /** + * Enable signals INTR, QUIT, [D]SUSP. + */ + ISIG, + + /** + * Canonicalize input lines. + */ + ICANON, + + /** + * Enable input and output of uppercase characters by preceding their lowercase + * equivalents with "\". + */ + XCASE, + + /** + * Enable echoing. + */ + ECHO, + + /** + * Visually erase chars. + */ + ECHOE, + + /** + * Kill character discards current line. + */ + ECHOK, + + /** + * Echo NL even if ECHO is off. + */ + ECHONL, + + /** + * Don't flush after interrupt. + */ + NOFLSH, + + /** + * Stop background jobs from output.TerminalModes + */ + TOSTOP, + + + /** + * Enable extensions. + */ + IEXTEN, + + + /** + * Echo control characters as ^(Char). + */ + ECHOCTL, + + + /** + * Visual erase for line kill. + */ + ECHOKE, + + + /** + * Retype pending input. + */ + PENDIN, + + + /** + * Enable output processing. + */ + OPOST, + + + /** + * Convert lowercase to uppercase. + */ + OLCUC, + + /** + * Map NL to CR-NL. + */ + ONLCR, + + /** + * Translate carriage return to newline (output). + */ + OCRNL, + + /** + * Translate newline to carriage return-newline (output). + */ + ONOCR, + + /** + * Newline performs a carriage return (output). + */ + ONLRET, + + /** + * 7 bit mode. + */ + CS7, + + /** + * 8 bit mode. + */ + CS8, + + + /** + * Parity enable. + */ + PARENB, + + /** + * Odd parity, else even. + */ + PARODD, + + + /** + * Specifies the input baud rate in bits per second. + */ + TTY_OP_ISPEED, + + /** + * Specifies the output baud rate in bits per second. + */ + TTY_OP_OSPEED; + + public int toMode() { + switch(this) { + case VINTR: + return 1; + case VQUIT: + return 2; + case VERASE: + return 3; + case VKILL: + return 4; + case VEOF: + return 5; + case VEOL: + return 6; + case VEOL2: + return 7; + case VSTART: + return 8; + case VSTOP: + return 9; + case VSUSP: + return 10; + case VDSUSP: + return 11; + case VREPRINT: + return 12; + case VWERASE: + return 13; + case VLNEXT: + return 14; + case VFLUSH: + return 15; + case VSWITCH: + return 16; + case VSTATUS: + return 17; + case VDISCARD: + return 18; + case IGNPAR: + return 30; + case PARMRK: + return 31; + case INPCK: + return 32; + case ISTRIP: + return 33; + case INLCR: + return 34; + case IGNCR: + return 35; + case ICRNL: + return 36; + case IUCLC: + return 37; + case IXON: + return 38; + case IXANY: + return 39; + case IXOFF: + return 40; + case IMAXBEL: + return 41; + case IUTF8: + return 42; + case ISIG: + return 50; + case ICANON: + return 51; + case XCASE: + return 52; + case ECHO: + return 53; + case ECHOE: + return 54; + case ECHOK: + return 55; + case ECHONL: + return 56; + case NOFLSH: + return 57; + case TOSTOP: + return 58; + case IEXTEN: + return 59; + case ECHOCTL: + return 60; + case ECHOKE: + return 61; + case PENDIN: + return 62; + case OPOST: + return 70; + case OLCUC: + return 71; + case ONLCR: + return 72; + case OCRNL: + return 73; + case ONOCR: + return 74; + case ONLRET: + return 75; + case CS7: + return 90; + case CS8: + return 91; + case PARENB: + return 92; + case PARODD: + return 93; + case TTY_OP_ISPEED: + return 128; + case TTY_OP_OSPEED: + return 129; + } + throw new IllegalStateException(); + } + + public static Mode fromMode(int mode) { + switch(mode) { + case 1: + return VINTR; + case 2: + return VQUIT; + case 3: + return VERASE; + case 4: + return VKILL; + case 5: + return VEOF; + case 6: + return VEOL; + case 7: + return VEOL2; + case 8: + return VSTART; + case 9: + return VSTOP; + case 10: + return VSUSP; + case 11: + return VDSUSP; + case 12: + return VREPRINT; + case 13: + return VWERASE; + case 14: + return VLNEXT; + case 15: + return VFLUSH; + case 16: + return VSWITCH; + case 17: + return VSTATUS; + case 18: + return VDISCARD; + case 30: + return IGNPAR; + case 31: + return PARMRK; + case 32: + return INPCK; + case 33: + return ISTRIP; + case 34: + return INLCR; + case 35: + return INLCR; + case 36: + return ICRNL; + case 37: + return IUCLC; + case 38: + return IXON; + case 39: + return IXANY; + case 40: + return IXOFF; + case 41: + return IMAXBEL; + case 42: + return IUTF8; + case 50: + return ISIG; + case 51: + return ICANON; + case 52: + return XCASE; + case 53: + return ECHO; + case 54: + return ECHOE; + case 55: + return ECHOK; + case 56: + return ECHONL; + case 57: + return NOFLSH; + case 58: + return TOSTOP; + case 59: + return IEXTEN; + case 60: + return ECHOCTL; + case 61: + return ECHOKE; + case 62: + return PENDIN; + case 70: + return OPOST; + case 71: + return OLCUC; + case 72: + return ONLCR; + case 73: + return OCRNL; + case 74: + return ONOCR; + case 75: + return ONLRET; + case 90: + return CS7; + case 91: + return CS8; + case 92: + return PARENB; + case 93: + return PARODD; + case 128: + return TTY_OP_ISPEED; + case 129: + return TTY_OP_OSPEED; + } + throw new IllegalStateException("" + mode); + } + } + + /** + * Builds {@link TerminalModes}. + * + *

+ * You can reuse an instance of this class providing that you do not want to + * change any of the modes. If you do want to change modes you can call the + * reset method to clear out old modes. + *

+ */ + public final static class TerminalModesBuilder { + private final Map codes = new LinkedHashMap<>(); + + /** + * Clear all modes set in this builder. + * + * @return this for chaining + */ + public TerminalModesBuilder reset() { + codes.clear(); + return this; + } + + /** + * Set one or more modes to true, i.e. a value of 1. + * + * @param modes modes + * @return this for chaining + */ + public TerminalModesBuilder withModes(Mode... modes) { + Arrays.asList(modes).forEach(m -> withMode(m, true)); + return this; + } + + /** + * Set one or more modes to false, i.e. a value of 0. + * + * @param modes modes + * @return this for chaining + */ + public TerminalModesBuilder withoutModes(Mode... modes) { + Arrays.asList(modes).forEach(m -> withMode(m, false)); + return this; + } + + /** + * Set a boolean mode. + * + * @param mode mode + * @param value value to set + * @return this for chaining + */ + public TerminalModesBuilder withMode(int mode, boolean value) { + return withMode(Mode.fromMode(mode), value); + } + + /** + * Set a boolean mode. + * + * @param mode mode + * @param value value to set + * @return this for chaining + */ + public TerminalModesBuilder withMode(Mode mode, boolean value) { + return withMode(mode, value ? 1 : 0); + } + + /** + * Set an integer mode. + * + * @param mode mode + * @param value value to set + * @return this for chaining + */ + public TerminalModesBuilder withMode(int mode, int value) { + return withMode(Mode.fromMode(mode), value); + } + + /** + * Set an integer mode. + * + * @param mode mode + * @param value value to set + * @return this for chaining + */ + public TerminalModesBuilder withMode(Mode mode, int value) { + codes.put(mode, value); + return this; + } + + /** + * Set a boolean>code> mode to true. + * + * @param mode mode + * @return this for chaining + */ + public TerminalModesBuilder withMode(int mode) { + return withMode(mode, true); + } + + /** + * Set a boolean>code> mode to false. + * + * @param mode mode + * @return this for chaining + */ + public TerminalModesBuilder withoutMode(int mode) { + return withMode(mode, false); + } + + /** + * Create a new {@link TerminalModesBuilder} + * + * @return builder + */ + public static TerminalModesBuilder create() { + return new TerminalModesBuilder(); + } + + /** + * Build a new {@link TerminalModes}. + * + * @return modes + * @throws IOException + */ + public TerminalModes build() { + return new TerminalModes(this); + } + + public TerminalModesBuilder fromBytes(byte[] modes) { + return read(new ByteArrayReader(modes)); + } + + public TerminalModesBuilder read(ByteArrayReader reader) { + while(true) { + /* Hrm why does ByteArrayWriter throw IOExceptions? */ + try { + var mode = reader.read(); + if(mode < 1) + break; + withMode(Mode.fromMode(mode), (int)reader.readInt()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return this; + } + } + + private final Map modes; + + private TerminalModes(TerminalModesBuilder builder) { + this.modes = Collections.unmodifiableMap(new LinkedHashMap<>(builder.codes)); + } + + /** + * Get all of the modes and their values as a map. + * + * @return modes + */ + public Map modes() { + return modes; + } + + /** + * Returns the encoded modes for use by the {@link SshSession}. + * @return byte[] + */ + public byte[] toByteArray() { + var baw = new ByteArrayWriter(); + write(baw); + return baw.toByteArray(); + } + + /** + * Get the value of a numeric node, or zero if it does not exist + * + * @param mode + * @param default + * @return value + */ + public int get(Mode mode) { + return get(mode, 0); + } + + /** + * Get the value of a number node, or a default if it does not exist. + * + * @param mode + * @param default + * @return value + */ + public int get(Mode mode, int defaultValue) { + return modes.getOrDefault(mode, defaultValue); + } + + /** + * Get if a mode is set (i.e. is non-zero). If the mode is not in the set, + * false will be returned. + * + * @param mode mode + * @return mode is preent and non-zero + */ + public boolean is(Mode mode) { + return is(mode, false); + } + + /** + * Get if a mode is set (i.e. is non-zero) or a default if it does not exist. + * + * @param mode mode + * @param defaultValue default value + * @return mode is non-zero + */ + public boolean is(Mode mode, boolean defaultValue) { + var m = modes.get(mode); + return m == null ? defaultValue : m > 0; + } + + /** + * Get if a mode is present in the set. + * + * @param mode mode + * @return mode is present and non-zero + */ + public boolean present(Mode mode) { + return modes.containsKey(mode); + } + + /** + * Write the modes to the writer. + * + * @param writer + */ + public void write(ByteArrayWriter writer) { + modes.forEach((k, v) -> { + writer.write(k.toMode()); + try { + writer.writeInt(v); + } catch (IOException e) { + /* Hrm why does ByteArrayWriter throw IOExceptions? */ + throw new UncheckedIOException(e); + } + }); + } + + +} diff --git a/maverick-synergy-common/src/main/java/module-info.java b/maverick-synergy-common/src/main/java/module-info.java index 9b79cb68..37e6b47c 100644 --- a/maverick-synergy-common/src/main/java/module-info.java +++ b/maverick-synergy-common/src/main/java/module-info.java @@ -33,6 +33,7 @@ exports com.sshtools.synergy.ssh.components; exports com.sshtools.synergy.ssh.components.jce; exports com.sshtools.synergy.util; + opens com.sshtools.synergy.ssh; uses SshCompressionFactory; } diff --git a/maverick-synergy-jdk16-client/pom.xml b/maverick-synergy-jdk16-client/pom.xml index c46815a3..e64b377c 100644 --- a/maverick-synergy-jdk16-client/pom.xml +++ b/maverick-synergy-jdk16-client/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-jdk16-client Client implementation Unix Domain Socket forwarding diff --git a/maverick-synergy-jdk16-common/pom.xml b/maverick-synergy-jdk16-common/pom.xml index 059f153e..2c939df7 100644 --- a/maverick-synergy-jdk16-common/pom.xml +++ b/maverick-synergy-jdk16-common/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-jdk16-common Common code for Unix Domain Socket support diff --git a/maverick-synergy-jdk16-server/pom.xml b/maverick-synergy-jdk16-server/pom.xml index cf240210..b16474c3 100644 --- a/maverick-synergy-jdk16-server/pom.xml +++ b/maverick-synergy-jdk16-server/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-jdk16-server Server implementation Unix Domain Socket forwarding diff --git a/maverick-synergy-jdk16-server/src/main/java/module-info.java b/maverick-synergy-jdk16-server/src/main/java/module-info.java index 92ad1370..364c1f68 100644 --- a/maverick-synergy-jdk16-server/src/main/java/module-info.java +++ b/maverick-synergy-jdk16-server/src/main/java/module-info.java @@ -23,4 +23,5 @@ module com.sshtools.synergy.jdk16.server { requires transitive com.sshtools.synergy.jdk16.common; requires transitive com.sshtools.synergy.server; + exports com.sshtools.server.jdk16; } diff --git a/maverick-synergy-s3/pom.xml b/maverick-synergy-s3/pom.xml index 394c950e..2c934e1f 100644 --- a/maverick-synergy-s3/pom.xml +++ b/maverick-synergy-s3/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-s3 S3 File System @@ -12,7 +12,7 @@ com.sshtools maverick-base - 3.1.1 + 3.1.2 diff --git a/maverick-synergy-server/pom.xml b/maverick-synergy-server/pom.xml index 552d7615..f1d94643 100644 --- a/maverick-synergy-server/pom.xml +++ b/maverick-synergy-server/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy-server Server API diff --git a/maverick-synergy-server/src/main/java/com/sshtools/server/SessionChannelNG.java b/maverick-synergy-server/src/main/java/com/sshtools/server/SessionChannelNG.java index 01e8d889..e35566a1 100644 --- a/maverick-synergy-server/src/main/java/com/sshtools/server/SessionChannelNG.java +++ b/maverick-synergy-server/src/main/java/com/sshtools/server/SessionChannelNG.java @@ -50,6 +50,7 @@ import com.sshtools.common.util.Utils; import com.sshtools.synergy.ssh.ChannelNG; import com.sshtools.synergy.ssh.ChannelOutputStream; +import com.sshtools.synergy.ssh.TerminalModes; /** *

@@ -109,7 +110,6 @@ public abstract class SessionChannelNG extends ChannelNG imple boolean haltIncomingData = false; long lastActivity = System.currentTimeMillis(); boolean agentForwardingRequested; - boolean rawMode = false; boolean singleSession = false; ChannelOutputStream stderrOutputStream = new ChannelOutputStream(this, SSH_EXTENDED_DATA_STDERR); @@ -141,14 +141,6 @@ protected void onChannelClosed() { } } - public void enableRawMode() { - rawMode = true; - } - - public void disableRawMode() { - rawMode = false; - } - public Subsystem getSubsystem() { return subsystem; } @@ -166,6 +158,13 @@ public boolean isAgentForwardingRequested() { return agentForwardingRequested; } + public void pauseDataCaching() { + paused = true; + } + + public void resumeDataCaching() { + paused = false; + } /** * Implement this method to support agent forwarding. * @@ -178,6 +177,9 @@ protected boolean requestAgentForwarding(String requestType) { /** * If the client requests a pseudo terminal for the session this method will * be invoked before the shell, exec or subsystem is started. + *

+ * Deprecated, at version 3.2.0 {@link #allocatePseudoTerminal(String, int, int, int, int, TerminalModes)}. + * will be made abstract and this method will be removed. * * @param term * @param cols @@ -187,8 +189,28 @@ protected boolean requestAgentForwarding(String requestType) { * @param modes * @return boolean */ - protected abstract boolean allocatePseudoTerminal(String term, int cols, - int rows, int width, int height, byte[] modes); + @Deprecated(forRemoval = true, since = "3.1.2") + protected boolean allocatePseudoTerminal(String term, int cols, + int rows, int width, int height, byte[] modes) { + throw new UnsupportedOperationException("No longer used, instead call allocatePseudoTerminal() with TerminalModes."); + } + + /** + * If the client requests a pseudo terminal for the session this method will + * be invoked before the shell, exec or subsystem is started. + * + * @param term + * @param cols + * @param rows + * @param width + * @param height + * @param modes + * @return boolean + */ + protected boolean allocatePseudoTerminal(String term, int cols, + int rows, int width, int height, TerminalModes modes) { + return allocatePseudoTerminal(term, cols, width, width, height, modes.toByteArray()); + } /** * When the window (terminal) size changes on the client side, it MAY send @@ -314,10 +336,10 @@ protected void onChannelRequest(String type, boolean wantreply, int rows = (int) bar.readInt(); int width = (int) bar.readInt(); int height = (int) bar.readInt(); - byte[] modes = bar.readBinaryString(); success = allocatePseudoTerminal(term, cols, rows, width, - height, modes); + height, new TerminalModes.TerminalModesBuilder().fromBytes(bar.readBinaryString()).build()); + if(Log.isDebugEnabled()) Log.debug(term + " pseudo terminal requested"); if(Log.isDebugEnabled()) diff --git a/maverick-synergy-server/src/main/java/com/sshtools/server/UnsupportedSession.java b/maverick-synergy-server/src/main/java/com/sshtools/server/UnsupportedSession.java index 61bb2e04..e659a3e2 100644 --- a/maverick-synergy-server/src/main/java/com/sshtools/server/UnsupportedSession.java +++ b/maverick-synergy-server/src/main/java/com/sshtools/server/UnsupportedSession.java @@ -10,12 +10,12 @@ * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Lesser Public License for more details. - * + * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * . @@ -27,19 +27,20 @@ import com.sshtools.common.logger.Log; import com.sshtools.common.ssh.ConnectionAwareTask; import com.sshtools.common.ssh.SshConnection; +import com.sshtools.synergy.ssh.TerminalModes; /** * This is a basic session that provides a message to the user to inform them * that a shell or command cannot be executed because the server does not * support an interactive session. - * - * + * + * */ public class UnsupportedSession extends SessionChannelNG { String message = "This server does not support an interactive session.\r\nGoodbye.\r\n"; - + public UnsupportedSession(SshConnection con) { super(con); } @@ -48,10 +49,11 @@ protected boolean executeCommand(String cmd) { return false; } - protected boolean startShell() { - + @Override + protected boolean startShell() { + con.executeTask(new ConnectionAwareTask(con) { - + @Override protected void doTask() { try { @@ -66,17 +68,19 @@ protected void doTask() { } } }); - - + + return true; } - protected boolean allocatePseudoTerminal(String parm1, int parm2, int parm3, int parm4, int parm5, byte[] parm6) { + @Override + protected boolean allocatePseudoTerminal(String parm1, int parm2, int parm3, int parm4, int parm5, TerminalModes parm6) { return true; } - - public boolean setEnvironmentVariable(String name, String value) { + + @Override + public boolean setEnvironmentVariable(String name, String value) { return false; } @@ -92,6 +96,6 @@ protected void onLocalEOF() { @Override protected void processSignal(String signal) { - + } } diff --git a/maverick-synergy/pom.xml b/maverick-synergy/pom.xml index 797f877a..74648a73 100644 --- a/maverick-synergy/pom.xml +++ b/maverick-synergy/pom.xml @@ -6,7 +6,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-synergy Unified API diff --git a/maverick-utils/pom.xml b/maverick-utils/pom.xml index 7657da5f..84a3d92e 100644 --- a/maverick-utils/pom.xml +++ b/maverick-utils/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-utils Utils diff --git a/maverick-virtual-filesystem-tests/pom.xml b/maverick-virtual-filesystem-tests/pom.xml index 0d21deba..0cb82935 100644 --- a/maverick-virtual-filesystem-tests/pom.xml +++ b/maverick-virtual-filesystem-tests/pom.xml @@ -3,7 +3,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-virtual-filesystem-tests Virtual File System Tests diff --git a/maverick-virtual-filesystem/pom.xml b/maverick-virtual-filesystem/pom.xml index 85a8fe53..21a8bfeb 100644 --- a/maverick-virtual-filesystem/pom.xml +++ b/maverick-virtual-filesystem/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-virtual-filesystem Virtual File System diff --git a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/AbstractMount.java b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/AbstractMount.java index b8a0c556..274c728d 100644 --- a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/AbstractMount.java +++ b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/AbstractMount.java @@ -51,7 +51,11 @@ public int hashCode() { private boolean filesystemRoot; private boolean isDefault; protected boolean isImaginary; - + protected String username = "unknown"; + protected String group = "unknown"; + protected int uid = 0; + protected int gid = 0; + protected AbstractMount(String mount, String path) { this(mount, path, false, false); } @@ -121,4 +125,20 @@ public void setAttribute(String key, Object value) { public String toString() { return getMount() + " on " + getRoot(); } + + public String getUsername() { + return username; + } + + public String getGroup() { + return group; + } + + public int getUid() { + return uid; + } + + public int getGid() { + return gid; + } } diff --git a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualFileFactory.java b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualFileFactory.java index e87e7538..851c02bc 100644 --- a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualFileFactory.java +++ b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualFileFactory.java @@ -249,7 +249,7 @@ public Map resolveChildren(VirtualFile parent) throws Permis if(intermediate = !childPaths.isEmpty()) { childPath = FileUtils.checkEndsWithNoSlash(childPaths.get(0)); } - files.put(childPath, new VirtualMountFile(currentPath + childPath, parent.getMount(), this, intermediate)); + files.put(childPath, new VirtualMountFile(currentPath + childPath, m, this, intermediate)); } } diff --git a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMount.java b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMount.java index a77699f3..350e118d 100644 --- a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMount.java +++ b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMount.java @@ -60,6 +60,11 @@ public class VirtualMount extends AbstractMount { AbstractFile f = actualFileFactory.getFile(path); this.path = f.getAbsolutePath(); } + + this.uid = mountTemplate.getUid(); + this.gid = mountTemplate.getGid(); + this.username = mountTemplate.getUsername(); + this.group = mountTemplate.getGroup(); } diff --git a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountFile.java b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountFile.java index b60cb574..d9f8abb7 100644 --- a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountFile.java +++ b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountFile.java @@ -123,10 +123,10 @@ public SftpFileAttributes getAttributes() throws FileNotFoundException, bldr.withPermissions(builder.build()); - bldr.withUid(0); - bldr.withGid(0); - bldr.withUsername(System.getProperty("maverick.unknownUsername", "unknown")); - bldr.withGroup(System.getProperty("maverick.unknownUsername", "unknown")); + bldr.withUid(parentMount.getUid()); + bldr.withGid(parentMount.getGid()); + bldr.withUsername(System.getProperty("maverick.unknownUsername", parentMount.getUsername())); + bldr.withGroup(System.getProperty("maverick.unknownUsername", parentMount.getGroup())); bldr.withLastAccessTime(parentMount.lastModified()); bldr.withLastModifiedTime(parentMount.lastModified()); bldr.withCreateTime(parentMount.lastModified()); diff --git a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountTemplate.java b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountTemplate.java index 8e36fca6..ecd128b5 100644 --- a/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountTemplate.java +++ b/maverick-virtual-filesystem/src/main/java/com/sshtools/common/files/vfs/VirtualMountTemplate.java @@ -77,4 +77,24 @@ public boolean isChildOf(VirtualMountTemplate o2) { public long lastModified() { return lastModified; } + + public VirtualMountTemplate setUsername(String username) { + this.username = username; + return this; + } + + public VirtualMountTemplate setGroup(String group) { + this.group = group; + return this; + } + + public VirtualMountTemplate setUid(int uid) { + this.uid = uid; + return this; + } + + public VirtualMountTemplate setGid(int gid) { + this.gid = gid; + return this; + } } diff --git a/maverick-virtual-session-tests/pom.xml b/maverick-virtual-session-tests/pom.xml index c1f0a8d3..932c46a0 100644 --- a/maverick-virtual-session-tests/pom.xml +++ b/maverick-virtual-session-tests/pom.xml @@ -4,7 +4,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-virtual-session-tests Virtual Sessions Tests diff --git a/maverick-virtual-session/pom.xml b/maverick-virtual-session/pom.xml index ce782c45..b62a5e28 100644 --- a/maverick-virtual-session/pom.xml +++ b/maverick-virtual-session/pom.xml @@ -7,7 +7,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-virtual-session Virtual Sessions diff --git a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualConsole.java b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualConsole.java index d274dff5..0139f36e 100644 --- a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualConsole.java +++ b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualConsole.java @@ -38,6 +38,7 @@ import com.sshtools.common.ssh.Context; import com.sshtools.common.ssh.SessionChannelServer; import com.sshtools.common.ssh.SshConnection; +import com.sshtools.synergy.ssh.TerminalModes; public class VirtualConsole { @@ -49,10 +50,11 @@ public class VirtualConsole { Msh shell; AbstractFile cwd; AbstractFileFactory fileFactory; + TerminalModes modes; static ThreadLocal threadConsoles = new ThreadLocal<>(); - public VirtualConsole(SessionChannelServer channel, Environment env, Terminal terminal, LineReader reader, Msh shell) throws IOException, PermissionDeniedException { + public VirtualConsole(SessionChannelServer channel, Environment env, Terminal terminal, LineReader reader, Msh shell, TerminalModes modes) throws IOException, PermissionDeniedException { this.channel = channel; this.con = channel.getConnection(); this.env = env; @@ -61,6 +63,11 @@ public VirtualConsole(SessionChannelServer channel, Environment env, Terminal te this.shell = shell; this.fileFactory = getContext().getPolicy(FileSystemPolicy.class) .getFileFactory().getFileFactory(con); + this.modes = modes; + } + + public TerminalModes getPseudoTerminalModes() { + return modes; } public SshConnection getConnection() { diff --git a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualSessionPolicy.java b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualSessionPolicy.java index 6d435962..8fb1c43f 100644 --- a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualSessionPolicy.java +++ b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualSessionPolicy.java @@ -68,6 +68,10 @@ public Map getShellEnvironment() { return shellEnvironment; } + public void setShellEnvironment(Map shellEnvironment) { + this.shellEnvironment = shellEnvironment; + } + public File getShellDirectory() { return shellDirectory; } diff --git a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualShellNG.java b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualShellNG.java index 1a06a346..92e5f0f3 100644 --- a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualShellNG.java +++ b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/VirtualShellNG.java @@ -10,12 +10,12 @@ * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Lesser Public License for more details. - * + * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * . @@ -24,7 +24,6 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.file.FileSystem; import java.nio.file.FileSystems; @@ -47,33 +46,30 @@ import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; -import org.jline.terminal.impl.AbstractPosixTerminal; -import org.jline.terminal.impl.ExternalTerminal; import com.sshtools.common.files.nio.AbstractFileURI; import com.sshtools.common.logger.Log; import com.sshtools.common.permissions.PermissionDeniedException; import com.sshtools.common.policy.ClassLoaderPolicy; -import com.sshtools.common.ssh.Channel; -import com.sshtools.common.ssh.ChannelEventListener; import com.sshtools.common.ssh.SshConnection; import com.sshtools.common.util.Utils; import com.sshtools.server.AgentForwardingChannel; import com.sshtools.server.SessionChannelNG; +import com.sshtools.synergy.ssh.TerminalModes; public class VirtualShellNG extends SessionChannelNG { String shellCommand = null; Environment env = new Environment(); Set protectedEnvironmentVars = new HashSet<>(); - + public VirtualShellNG(SshConnection con, - ShellCommandFactory commandFactory, + ShellCommandFactory commandFactory, String shellCommand) { this(con, commandFactory); this.shellCommand = shellCommand; } - + public interface WindowSizeChangeListener { void newSize(int rows, int cols); } @@ -82,37 +78,15 @@ public interface WindowSizeChangeListener { protected VirtualConsole console; protected ShellCommandFactory commandFactory; - boolean rawMode = false; - - List listeners = new ArrayList(); + List listeners = new ArrayList<>(); + private Terminal terminal; - + private TerminalModes modes; + public VirtualShellNG(SshConnection con, ShellCommandFactory commandFactory) { super(con); this.commandFactory = commandFactory; - addEventListener(new ChannelEventListener() { - @Override - public void onChannelDataIn(Channel channel, ByteBuffer data) { - - byte[] tmp = new byte[data.remaining()]; - data.get(tmp); - - try { - if(terminal instanceof AbstractPosixTerminal) { - ((AbstractPosixTerminal)terminal).getPty().getMasterOutput().write(tmp); - ((AbstractPosixTerminal)terminal).getPty().getMasterOutput().flush(); - } - else { - ((ExternalTerminal)terminal).processInputBytes(tmp, 0, tmp.length); - } - evaluateWindowSpace(); - } catch (Exception e) { - Log.error("Failed to send input to terminal.", e); - close(); - } - } - }); } public void addWindowSizeChangeListener(WindowSizeChangeListener listener) { @@ -122,26 +96,28 @@ public void addWindowSizeChangeListener(WindowSizeChangeListener listener) { public void removeWindowSizeChangeListener(WindowSizeChangeListener listener) { listeners.remove(listener); } - + public void addProtectedEnvironmentVar(String name) { protectedEnvironmentVars.add(name.toUpperCase()); } protected boolean executeCommand(String cmd) { - + try { shell = commandFactory.createShell(con); shell.execCommand(getInputStream(), console = createConsole(), cmd); return true; } catch (Exception e) { - if(Log.isErrorEnabled()) + if(Log.isErrorEnabled()) { Log.error("Failed to execute command " + cmd, e); - } + } + } return false; } + @Override protected void changeWindowDimensions(int cols, int rows, int width, int height) { - + console.getTerminal().setSize(new Size(cols, rows)); for (WindowSizeChangeListener l : listeners) { @@ -149,33 +125,35 @@ protected void changeWindowDimensions(int cols, int rows, int width, int height) } } + @Override public void onSessionOpen() { - + shell.start(); } + @Override protected boolean startShell() { - + if(Utils.isNotBlank(shellCommand)) { return executeCommand(shellCommand); } - + try { - shell = createShell(con); + shell = createShell(con); shell.startShell(null, console = createConsole()); return true; } catch (Throwable t) { Log.warn("Failed to start shell.", t); - } + } return false; } protected RootShell createShell(SshConnection con) throws PermissionDeniedException, IOException { return commandFactory.createShell(con); } - + private VirtualConsole createConsole() throws IOException, PermissionDeniedException { - + Attributes attrs = new Attributes(); attrs.setInputFlag(InputFlag.ICRNL, true); terminal = TerminalBuilder.builder(). @@ -185,47 +163,50 @@ private VirtualConsole createConsole() throws IOException, PermissionDeniedExcep size(new Size(env.getOrDefault("COLS", 80), env.getOrDefault("ROWS", 80))). encoding(Charset.forName("UTF-8")). attributes(attrs).build(); - + Map env = new HashMap<>(); env.put("connection", getConnection()); FileSystem fs = FileSystems.newFileSystem( - AbstractFileURI.create(getConnection(), ""), + AbstractFileURI.create(getConnection(), ""), env, getContext().getPolicy(ClassLoaderPolicy.class).getClassLoader()); - + final LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder() .terminal(terminal) .completer(new VirtualShellCompletor()) .variable(LineReader.HISTORY_SIZE, 1000) .variable(LineReader.HISTORY_FILE, fs.getPath(".history")); - return new VirtualConsole(this, this.env, terminal, lineReaderBuilder.build(), shell); + return new VirtualConsole(this, this.env, terminal, lineReaderBuilder.build(), shell, modes); } @Override protected boolean requestAgentForwarding(String requestType) { - + try { if(!getConnection().containsProperty(AgentForwardingChannel.SSH_AGENT_CLIENT)) { connection.openChannel(new AgentForwardingChannel(requestType, this)); - } + } return true; } catch (IOException e) { return false; } - + } - protected boolean allocatePseudoTerminal(String term, int cols, int rows, int width, int height, byte[] modes) { - + @Override + protected boolean allocatePseudoTerminal(String term, int cols, int rows, int width, int height, TerminalModes modes) { + env.put("TERM", term); env.put("COLS", cols); env.put("ROWS", rows); env.put("PTYMODES", modes); - + this.modes = modes; + return true; } + @Override public boolean setEnvironmentVariable(String name, String value) { if(protectedEnvironmentVars.contains(name.toUpperCase())) { return false; @@ -234,6 +215,7 @@ public boolean setEnvironmentVariable(String name, String value) { return true; } + @Override protected void onChannelOpenFailure() { } @@ -241,25 +223,25 @@ protected void onChannelOpenFailure() { @Override protected void processSignal(String signal) { - + } @Override protected void onLocalEOF() { - + } @Override - public void enableRawMode() { + public void pauseDataCaching() { console.getTerminal().pause(); - super.enableRawMode(); - + super.pauseDataCaching(); } @Override - public void disableRawMode() { + public void resumeDataCaching() { + super.resumeDataCaching(); console.getTerminal().resume(); - super.disableRawMode(); + } class VirtualShellCompletor implements Completer, MshListener { @@ -269,17 +251,17 @@ class VirtualShellCompletor implements Completer, MshListener { VirtualShellCompletor() { shell.addListener(this); } - + @Override public void complete(LineReader reader, ParsedLine line, List candidates) { - + if(!inCommand.get()) { processShellCompletion(reader, line, candidates); } else { processInCommandCompletion(reader, line, candidates); } } - + private void processInCommandCompletion(LineReader reader, ParsedLine line, List candidates) { @SuppressWarnings("unchecked") List tmp = (List) console.getEnvironment().get("_COMPLETIONS"); @@ -289,7 +271,7 @@ private void processInCommandCompletion(LineReader reader, ParsedLine line, List } private void processShellCompletion(LineReader reader, ParsedLine line, List candidates) { - + switch(line.wordIndex()) { case 0: /** @@ -318,12 +300,12 @@ public void commandStarted(Command cmd, String[] args, VirtualConsole console) { inCommand.set(true); this.currentCommand = cmd; } - + @Override public void commandFinished(Command cmd, String[] args, VirtualConsole console) { inCommand.set(false); this.currentCommand = null; } - + } } diff --git a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/commands/os/AbstractOSCommand.java b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/commands/os/AbstractOSCommand.java index ab582c6b..5ae1f441 100644 --- a/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/commands/os/AbstractOSCommand.java +++ b/maverick-virtual-session/src/main/java/com/sshtools/server/vsession/commands/os/AbstractOSCommand.java @@ -100,7 +100,7 @@ private void runCommand(String cmd, List cmdArgs, if(directory != null) builder.setDirectory(directory.getAbsolutePath()); builder.setConsole(false); - builder.setEnvironment(env); + builder.setEnvironment(penv); pty = builder.start(); final InputStream in = pty.getInputStream(); @@ -119,13 +119,13 @@ public void newSize(int rows, int cols) { shell.addWindowSizeChangeListener(listener); - console.getSessionChannel().enableRawMode(); + console.getSessionChannel().pauseDataCaching(); - console.getSessionChannel().addEventListener(new ChannelEventListener() { + ChannelEventListener l = new ChannelEventListener() { @Override public void onChannelDataIn(Channel channel, ByteBuffer buffer) { - + byte[] tmp = new byte[buffer.remaining()]; buffer.get(tmp); @@ -138,7 +138,8 @@ public void onChannelDataIn(Channel channel, ByteBuffer buffer) { IOUtils.closeStream(in); } } - }); + }; + console.getSessionChannel().addEventListener(l); try { IOUtils.copy(in, console.getSessionChannel().getOutputStream()); @@ -150,7 +151,12 @@ public void onChannelDataIn(Channel channel, ByteBuffer buffer) { } } catch (Exception e) { } finally { - console.getSessionChannel().disableRawMode(); + try { + console.getSessionChannel().resumeDataCaching(); + } + finally { + console.getSessionChannel().removeEventListener(l); + } } } diff --git a/maverick-virtual-session/src/main/java/com/sshtools/vsession/commands/ssh/SshClientCommand.java b/maverick-virtual-session/src/main/java/com/sshtools/vsession/commands/ssh/SshClientCommand.java index ca9bf5ca..57c9780a 100644 --- a/maverick-virtual-session/src/main/java/com/sshtools/vsession/commands/ssh/SshClientCommand.java +++ b/maverick-virtual-session/src/main/java/com/sshtools/vsession/commands/ssh/SshClientCommand.java @@ -23,10 +23,12 @@ */ import java.io.IOException; +import java.nio.ByteBuffer; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; +import com.sshtools.client.PseudoTerminalModes.PseudoTerminalModesBuilder; import com.sshtools.client.SessionChannelNG; import com.sshtools.client.SshClient; import com.sshtools.client.SshClientContext; @@ -34,7 +36,10 @@ import com.sshtools.client.tasks.CommandTask.CommandTaskBuilder; import com.sshtools.client.tasks.ShellTask.ShellTaskBuilder; import com.sshtools.client.tasks.Task; +import com.sshtools.common.logger.Log; import com.sshtools.common.permissions.PermissionDeniedException; +import com.sshtools.common.ssh.Channel; +import com.sshtools.common.ssh.ChannelEventListener; import com.sshtools.common.util.IOUtils; import com.sshtools.server.vsession.VirtualConsole; import com.sshtools.server.vsession.VirtualShellNG; @@ -54,10 +59,11 @@ public SshClientCommand() { @Override public void runCommand(SshClient sshClient, SshClientArguments arguments, VirtualConsole console) { - - console.getSessionChannel().enableRawMode(); - + try { + + console.getSessionChannel().pauseDataCaching(); + Connection connection = sshClient.getConnection(); AbstractSessionTask task; @@ -65,7 +71,8 @@ public void runCommand(SshClient sshClient, SshClientArguments arguments, Virtua if (CommandUtil.isNotEmpty(arguments.getCommand())) { String command = arguments.getCommand(); - task = CommandTaskBuilder.create() + var builder = CommandTaskBuilder.create() + .withConnection(connection) .withCommand(command) .withTermType(console.getTerminal().getType()) .withColumns(console.getTerminal().getWidth()) @@ -79,12 +86,18 @@ public void runCommand(SshClient sshClient, SshClientArguments arguments, Virtua IOUtils.copy(session.getInputStream(), console.getSessionChannel().getOutputStream()); }) - .onClose((t, session) -> ((VirtualShellNG)console.getSessionChannel()).removeWindowSizeChangeListener(listener)) - .build(); + .onClose((t, session) -> ((VirtualShellNG)console.getSessionChannel()).removeWindowSizeChangeListener(listener)); + if(console.getPseudoTerminalModes() != null) { + try { + builder.withModes(PseudoTerminalModesBuilder.create().build()); + } catch (IOException e) { + } + } + task = builder.build(); } else { - task = ShellTaskBuilder.create(). + var builder = ShellTaskBuilder.create(). withConnection(connection). withTermType(console.getTerminal().getType()). withColumns(console.getTerminal().getWidth()). @@ -92,12 +105,44 @@ public void runCommand(SshClient sshClient, SshClientArguments arguments, Virtua onBeforeTask((t, session) -> { listener.session = session; ((VirtualShellNG)console.getSessionChannel()).addWindowSizeChangeListener(listener); + + ChannelEventListener l = new ChannelEventListener() { + + @Override + public void onChannelDataIn(Channel channel, ByteBuffer buffer) { + + byte[] tmp = new byte[buffer.remaining()]; + buffer.get(tmp); + + try { + session.getOutputStream().write(tmp); + session.getOutputStream().flush(); + } catch (IOException e) { + Log.error("Error writing data from console", e); + } + } + }; + console.getSessionChannel().addEventListener(l); + + session.addEventListener(new ChannelEventListener() { + @Override + public void onChannelClose(Channel channel) { + if(Log.isDebugEnabled()) { + Log.debug("Detected close of child command so removing channel data listeners"); + } + console.getSessionChannel().removeEventListener(l); + session.removeEventListener(this); + } + }); }) .onTask((t, session)-> { - connection.addTask(Task.ofRunnable(connection, (c) -> IOUtils.copy(console.getSessionChannel().getInputStream(), session.getOutputStream()))); IOUtils.copy(session.getInputStream(), console.getSessionChannel().getOutputStream()); }). - onClose((t, session) -> ((VirtualShellNG)console.getSessionChannel()).removeWindowSizeChangeListener(listener)). + onClose((t, session) -> ((VirtualShellNG)console.getSessionChannel()).removeWindowSizeChangeListener(listener)); + if(console.getPseudoTerminalModes() != null) { + builder.withModes(console.getPseudoTerminalModes()); + } + task = builder. build(); } @@ -105,7 +150,8 @@ public void runCommand(SshClient sshClient, SshClientArguments arguments, Virtua task.waitForever(); } finally { - console.getSessionChannel().disableRawMode(); + + console.getSessionChannel().resumeDataCaching(); console.println(); } diff --git a/maverick-x509/pom.xml b/maverick-x509/pom.xml index 3964b1f5..37a1156f 100644 --- a/maverick-x509/pom.xml +++ b/maverick-x509/pom.xml @@ -5,7 +5,7 @@ com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 maverick-x509 X509 Certificate Support diff --git a/pom.xml b/pom.xml index e2657c0a..c020b3a3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.sshtools maverick-synergy-group - 3.1.1 + 3.1.2 Maverick Synergy Open source Java SSH API http://www.jadaptive.com