update to OpenJDK 21, update to Grdle 8.4, add key parameter for loading private key by classloader
This commit is contained in:
parent
4eff2cd434
commit
2b879107d7
35 changed files with 139 additions and 174 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,6 +15,7 @@ logs
|
||||||
*~
|
*~
|
||||||
.secret
|
.secret
|
||||||
build
|
build
|
||||||
|
**/*.key
|
||||||
**/*.crt
|
**/*.crt
|
||||||
**/*.pkcs8
|
**/*.pkcs8
|
||||||
**/*.gz
|
**/*.gz
|
||||||
|
|
11
build.gradle
11
build.gradle
|
@ -4,9 +4,10 @@ plugins {
|
||||||
id "pmd"
|
id "pmd"
|
||||||
id 'maven-publish'
|
id 'maven-publish'
|
||||||
id 'signing'
|
id 'signing'
|
||||||
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
|
id "io.github.gradle-nexus.publish-plugin" version "2.0.0-rc-1"
|
||||||
id "com.github.spotbugs" version "5.0.14"
|
id "com.github.spotbugs" version "6.0.0-beta.3"
|
||||||
id "org.cyclonedx.bom" version "1.7.2"
|
id "org.cyclonedx.bom" version "1.7.4"
|
||||||
|
id "org.xbib.gradle.plugin.asciidoctor" version "3.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
|
@ -30,14 +31,14 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
apply from: rootProject.file('gradle/ide/idea.gradle')
|
//apply from: rootProject.file('gradle/ide/idea.gradle')
|
||||||
apply from: rootProject.file('gradle/repositories/maven.gradle')
|
apply from: rootProject.file('gradle/repositories/maven.gradle')
|
||||||
apply from: rootProject.file('gradle/compile/java.gradle')
|
apply from: rootProject.file('gradle/compile/java.gradle')
|
||||||
apply from: rootProject.file('gradle/test/junit5.gradle')
|
apply from: rootProject.file('gradle/test/junit5.gradle')
|
||||||
apply from: rootProject.file('gradle/publish/maven.gradle')
|
apply from: rootProject.file('gradle/publish/maven.gradle')
|
||||||
apply from: rootProject.file('gradle/quality/checkstyle.gradle')
|
apply from: rootProject.file('gradle/quality/checkstyle.gradle')
|
||||||
apply from: rootProject.file('gradle/quality/pmd.gradle')
|
apply from: rootProject.file('gradle/quality/pmd.gradle')
|
||||||
apply from: rootProject.file('gradle/quality/spotbugs.gradle')
|
//apply from: rootProject.file('gradle/quality/spotbugs.gradle')
|
||||||
}
|
}
|
||||||
apply from: rootProject.file('gradle/publish/sonatype.gradle')
|
apply from: rootProject.file('gradle/publish/sonatype.gradle')
|
||||||
apply from: rootProject.file('gradle/publish/forgejo.gradle')
|
apply from: rootProject.file('gradle/publish/forgejo.gradle')
|
||||||
|
|
|
@ -49,11 +49,11 @@ import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Matchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.Matchers.anyCollectionOf;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.Matchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -330,7 +330,7 @@ public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
|
||||||
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo/bar"), options)) {
|
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo/bar"), options)) {
|
||||||
// don't do anything with the stream, there's a separate test for that
|
// don't do anything with the stream, there's a separate test for that
|
||||||
} finally {
|
} finally {
|
||||||
verify(getExceptionFactory()).createNewOutputStreamException(eq("/foo/bar"), eq(553), anyString(), anyCollectionOf(OpenOption.class));
|
verify(getExceptionFactory()).createNewOutputStreamException(eq("/foo/bar"), eq(553), anyString(), anyList());
|
||||||
assertSame(foo, getFileSystemEntry("/foo"));
|
assertSame(foo, getFileSystemEntry("/foo"));
|
||||||
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
||||||
}
|
}
|
||||||
|
@ -347,7 +347,7 @@ public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
|
||||||
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo/bar"), options)) {
|
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo/bar"), options)) {
|
||||||
// don't do anything with the stream, there's a separate test for that
|
// don't do anything with the stream, there's a separate test for that
|
||||||
} finally {
|
} finally {
|
||||||
verify(getExceptionFactory()).createNewOutputStreamException(eq("/foo/bar"), eq(553), anyString(), anyCollectionOf(OpenOption.class));
|
verify(getExceptionFactory()).createNewOutputStreamException(eq("/foo/bar"), eq(553), anyString(), anyList());
|
||||||
assertSame(foo, getFileSystemEntry("/foo"));
|
assertSame(foo, getFileSystemEntry("/foo"));
|
||||||
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
||||||
}
|
}
|
||||||
|
@ -427,8 +427,7 @@ public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
|
||||||
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo"), options)) {
|
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo"), options)) {
|
||||||
// don't do anything with the stream, there's a separate test for that
|
// don't do anything with the stream, there's a separate test for that
|
||||||
} finally {
|
} finally {
|
||||||
verify(getExceptionFactory(), never()).createNewOutputStreamException(anyString(), anyInt(), anyString(),
|
verify(getExceptionFactory(), never()).createNewOutputStreamException(anyString(), anyInt(), anyString(), anyList());
|
||||||
anyCollectionOf(OpenOption.class));
|
|
||||||
assertSame(foo, getFileSystemEntry("/foo"));
|
assertSame(foo, getFileSystemEntry("/foo"));
|
||||||
assertEquals(0, getChildCount("/foo"));
|
assertEquals(0, getChildCount("/foo"));
|
||||||
}
|
}
|
||||||
|
@ -443,8 +442,7 @@ public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
|
||||||
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo"), options)) {
|
try (OutputStream input = getFileSystem().newOutputStream(createPath("/foo"), options)) {
|
||||||
// don't do anything with the stream, there's a separate test for that
|
// don't do anything with the stream, there's a separate test for that
|
||||||
} finally {
|
} finally {
|
||||||
verify(getExceptionFactory(), never()).createNewOutputStreamException(anyString(), anyInt(), anyString(),
|
verify(getExceptionFactory(), never()).createNewOutputStreamException(anyString(), anyInt(), anyString(), anyList());
|
||||||
anyCollectionOf(OpenOption.class));
|
|
||||||
assertSame(foo, getFileSystemEntry("/foo"));
|
assertSame(foo, getFileSystemEntry("/foo"));
|
||||||
assertEquals(0, getChildCount("/foo"));
|
assertEquals(0, getChildCount("/foo"));
|
||||||
}
|
}
|
||||||
|
@ -693,7 +691,7 @@ public class FTPFileSystemTest extends AbstractFTPFileSystemTest {
|
||||||
try {
|
try {
|
||||||
getFileSystem().copy(createPath("/foo/bar"), createPath("/baz/bar"), options);
|
getFileSystem().copy(createPath("/foo/bar"), createPath("/baz/bar"), options);
|
||||||
} finally {
|
} finally {
|
||||||
verify(getExceptionFactory()).createNewOutputStreamException(eq("/baz/bar"), eq(553), anyString(), anyCollectionOf(OpenOption.class));
|
verify(getExceptionFactory()).createNewOutputStreamException(eq("/baz/bar"), eq(553), anyString(), anyList());
|
||||||
assertSame(foo, getFileSystemEntry("/foo"));
|
assertSame(foo, getFileSystemEntry("/foo"));
|
||||||
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
assertSame(bar, getFileSystemEntry("/foo/bar"));
|
||||||
assertNull(getFileSystemEntry("/baz"));
|
assertNull(getFileSystemEntry("/baz"));
|
||||||
|
|
|
@ -2755,7 +2755,7 @@ public class FTPClient extends FTP implements Configurable {
|
||||||
* happen when parserKey is neither
|
* happen when parserKey is neither
|
||||||
* the fully qualified class name of a class
|
* the fully qualified class name of a class
|
||||||
* implementing the interface
|
* implementing the interface
|
||||||
* {@link }FTPFileEntryParser}
|
* {@link FTPFileEntryParser}
|
||||||
* nor a string containing one of the recognized keys
|
* nor a string containing one of the recognized keys
|
||||||
* mapping to such a parser or if class loader
|
* mapping to such a parser or if class loader
|
||||||
* security issues prevent its being loaded.
|
* security issues prevent its being loaded.
|
||||||
|
|
|
@ -7,7 +7,7 @@ module org.xbib.files.sftp.fs {
|
||||||
exports org.apache.sshd.fs;
|
exports org.apache.sshd.fs;
|
||||||
exports org.apache.sshd.fs.spi;
|
exports org.apache.sshd.fs.spi;
|
||||||
requires org.xbib.files;
|
requires org.xbib.files;
|
||||||
requires transitive org.xbib.files.sftp;
|
requires org.xbib.files.sftp;
|
||||||
provides FileSystemProvider with SftpFileSystemProvider;
|
provides FileSystemProvider with SftpFileSystemProvider;
|
||||||
provides FileServiceProvider with SFTPFileServiceProvider;
|
provides FileServiceProvider with SFTPFileServiceProvider;
|
||||||
requires java.logging;
|
requires java.logging;
|
||||||
|
|
|
@ -83,6 +83,7 @@ import org.apache.sshd.common.auth.BasicCredentialsImpl;
|
||||||
import org.apache.sshd.common.auth.BasicCredentialsProvider;
|
import org.apache.sshd.common.auth.BasicCredentialsProvider;
|
||||||
import org.apache.sshd.common.auth.MutableBasicCredentials;
|
import org.apache.sshd.common.auth.MutableBasicCredentials;
|
||||||
import org.apache.sshd.common.io.IoSession;
|
import org.apache.sshd.common.io.IoSession;
|
||||||
|
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
|
||||||
import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
|
import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
|
||||||
import org.apache.sshd.common.util.GenericUtils;
|
import org.apache.sshd.common.util.GenericUtils;
|
||||||
import org.apache.sshd.common.util.NumberUtils;
|
import org.apache.sshd.common.util.NumberUtils;
|
||||||
|
@ -197,12 +198,13 @@ public class SftpFileSystemProvider extends FileSystemProvider {
|
||||||
if (port <= 0) {
|
if (port <= 0) {
|
||||||
port = SshConstants.DEFAULT_PORT;
|
port = SshConstants.DEFAULT_PORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object o = env.get("username");
|
Object o = env.get("username");
|
||||||
String username = o instanceof String ? (String) o : o != null ? o.toString() : null;
|
String username = o instanceof String ? (String) o : o != null ? o.toString() : null;
|
||||||
o = env.get("password");
|
o = env.get("password");
|
||||||
char[] password = o instanceof char[] ? (char[]) o : o instanceof String ? ((String)o).toCharArray() : null;
|
char[] password = o instanceof char[] ? (char[]) o : o instanceof String ? ((String)o).toCharArray() : null;
|
||||||
|
if (env.containsKey("key")) {
|
||||||
|
clientInstance.setKeyIdentityProvider(new ClassLoadableResourceKeyPairProvider(env.get("key").toString()));
|
||||||
|
}
|
||||||
boolean disableServerKeys = "true".equals(env.get("disableServerKeys"));
|
boolean disableServerKeys = "true".equals(env.get("disableServerKeys"));
|
||||||
if (disableServerKeys) {
|
if (disableServerKeys) {
|
||||||
clientInstance.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
|
clientInstance.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
|
||||||
|
@ -280,7 +282,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
try {
|
try {
|
||||||
session.close();
|
session.close();
|
||||||
} catch (IOException t) {
|
} catch (Exception t) {
|
||||||
e.addSuppressed(t);
|
e.addSuppressed(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,11 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.xbib.files.FileService;
|
import org.xbib.files.FileService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.ServiceLoader;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class FileServiceProviderTest {
|
public class FileServiceProviderTest {
|
||||||
|
|
||||||
static {
|
|
||||||
// load bouncy castle provider (and other security providers)
|
|
||||||
for (Provider p : ServiceLoader.load(Provider.class)) {
|
|
||||||
if (Security.getProvider(p.getName()) == null) {
|
|
||||||
Security.addProvider(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSFTP() throws IOException {
|
public void testSFTP() throws IOException {
|
||||||
Map<String, ?> env = Map.of("username", "joerg");
|
Map<String, ?> env = Map.of("username", "joerg");
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.apache.sshd.fs.test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.xbib.files.FileService;
|
||||||
|
|
||||||
|
public class SftpClientTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSFTP() throws IOException {
|
||||||
|
String targetURLString = "sftp://fernleihe-test.hbz-nrw.de:22";
|
||||||
|
String targetIdString = "malva";
|
||||||
|
URI uri = URI.create(targetURLString);
|
||||||
|
FileService fs = FileService.newInstance(targetURLString,
|
||||||
|
Map.of("username", targetIdString,
|
||||||
|
"key", uri.getHost() + "/" + targetIdString + ".key"));
|
||||||
|
fs.list(".").forEach(p -> Logger.getAnonymousLogger().info(p.toString()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,7 +75,6 @@ import org.apache.sshd.common.Factory;
|
||||||
import org.apache.sshd.common.NamedResource;
|
import org.apache.sshd.common.NamedResource;
|
||||||
import org.apache.sshd.common.ServiceFactory;
|
import org.apache.sshd.common.ServiceFactory;
|
||||||
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
|
||||||
import org.apache.sshd.common.config.keys.PublicKeyEntry;
|
import org.apache.sshd.common.config.keys.PublicKeyEntry;
|
||||||
import org.apache.sshd.common.future.SshFutureListener;
|
import org.apache.sshd.common.future.SshFutureListener;
|
||||||
import org.apache.sshd.common.helpers.AbstractFactoryManager;
|
import org.apache.sshd.common.helpers.AbstractFactoryManager;
|
||||||
|
@ -732,7 +731,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
|
||||||
} else if (id instanceof KeyPair) {
|
} else if (id instanceof KeyPair) {
|
||||||
KeyPair kp = (KeyPair) id;
|
KeyPair kp = (KeyPair) id;
|
||||||
session.addPublicKeyIdentity(kp);
|
session.addPublicKeyIdentity(kp);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import org.apache.sshd.common.util.GenericUtils;
|
||||||
public interface PasswordIdentityProvider {
|
public interface PasswordIdentityProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An "empty" implementation of {@link PasswordIdentityProvider} that returns and empty group of passwords
|
* An "empty" implementation of {@link PasswordIdentityProvider} that returns and empty group of passwords
|
||||||
*/
|
*/
|
||||||
PasswordIdentityProvider EMPTY_PASSWORDS_PROVIDER = new PasswordIdentityProvider() {
|
PasswordIdentityProvider EMPTY_PASSWORDS_PROVIDER = new PasswordIdentityProvider() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
package org.apache.sshd.client.impl;
|
package org.apache.sshd.client.impl;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
@ -82,12 +83,16 @@ public class SimpleSftpClientImpl implements SimpleSftpClient {
|
||||||
SimpleClient client = getClient();
|
SimpleClient client = getClient();
|
||||||
ClientSession session = sessionProvider.apply(client);
|
ClientSession session = sessionProvider.apply(client);
|
||||||
try {
|
try {
|
||||||
SftpClient sftp = createSftpClient(session);
|
SftpClient sftpClient = createSftpClient(session);
|
||||||
session = null; // disable auto-close at finally block
|
session = null;
|
||||||
return sftp;
|
return sftpClient;
|
||||||
} finally {
|
} finally {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.close();
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ import org.apache.sshd.common.CoreModuleProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a way to implement proxied connections where some metadata about the client is sent <U>before</U> the actual
|
* Provides a way to implement proxied connections where some metadata about the client is sent <U>before</U> the actual
|
||||||
* SSH protocol is executed - e.g., the <A HREF=@http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt">PROXY
|
* SSH protocol is executed - e.g., the http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt PROXY
|
||||||
* protocol</A>. The implementor should use the {@code IoSession#write(Buffer)} method to send any packets with the
|
* protocol. The implementor should use the {@code IoSession#write(Buffer)} method to send any packets with the
|
||||||
* meta-data.
|
* meta-data.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
||||||
|
|
|
@ -18,13 +18,8 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.sshd.client.session;
|
package org.apache.sshd.client.session;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
|
@ -125,8 +125,8 @@ public abstract class AbstractSimpleClientSessionCreator extends AbstractSimpleC
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
try {
|
try {
|
||||||
session.close();
|
session.close();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
err = GenericUtils.accumulateException(err, e);
|
err = GenericUtils.accumulateException(err, new IOException(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.apache.sshd.common.future.SshFutureListener;
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
||||||
*/
|
*/
|
||||||
public interface Closeable extends Channel {
|
public interface Closeable extends AutoCloseable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close this resource asynchronously and return a future. Resources support two closing modes: a graceful mode
|
* Close this resource asynchronously and return a future. Resources support two closing modes: a graceful mode
|
||||||
|
@ -76,13 +76,12 @@ public interface Closeable extends Channel {
|
||||||
*/
|
*/
|
||||||
boolean isClosing();
|
boolean isClosing();
|
||||||
|
|
||||||
@Override
|
|
||||||
default boolean isOpen() {
|
default boolean isOpen() {
|
||||||
return !(isClosed() || isClosing());
|
return !(isClosed() || isClosing());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void close() throws IOException {
|
default void close() throws Exception {
|
||||||
Closeable.close(this);
|
Closeable.close(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,9 +44,6 @@ public final class CoreModuleProperties {
|
||||||
public static final Property<String> PROXY_AUTH_CHANNEL_TYPE
|
public static final Property<String> PROXY_AUTH_CHANNEL_TYPE
|
||||||
= Property.string("ssh-agent-factory-proxy-auth-channel-type", "auth-agent-req@openssh.com");
|
= Property.string("ssh-agent-factory-proxy-auth-channel-type", "auth-agent-req@openssh.com");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.agent.local.ProxyAgentFactory#getChannelForwardingFactories}
|
|
||||||
*/
|
|
||||||
public static final Property<Boolean> PREFER_UNIX_AGENT
|
public static final Property<Boolean> PREFER_UNIX_AGENT
|
||||||
= Property.bool("ssh-prefer-unix-agent", OsUtils.isUNIX());
|
= Property.bool("ssh-prefer-unix-agent", OsUtils.isUNIX());
|
||||||
|
|
||||||
|
@ -147,8 +144,6 @@ public final class CoreModuleProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to ignore invalid identities files when pre-initializing the client session
|
* Whether to ignore invalid identities files when pre-initializing the client session
|
||||||
*
|
|
||||||
* @see ClientIdentityLoader#isValidLocation(NamedResource)
|
|
||||||
*/
|
*/
|
||||||
public static final Property<Boolean> IGNORE_INVALID_IDENTITIES
|
public static final Property<Boolean> IGNORE_INVALID_IDENTITIES
|
||||||
= Property.bool("ignore-invalid-identities", true);
|
= Property.bool("ignore-invalid-identities", true);
|
||||||
|
@ -579,105 +574,84 @@ public final class CoreModuleProperties {
|
||||||
public static final Property<String> MODULI_URL
|
public static final Property<String> MODULI_URL
|
||||||
= Property.string("moduli-url");
|
= Property.string("moduli-url");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator}.
|
|
||||||
*/
|
|
||||||
public static final Property<String> KB_SERVER_INTERACTIVE_NAME
|
public static final Property<String> KB_SERVER_INTERACTIVE_NAME
|
||||||
= Property.string("kb-server-interactive-name", "Password authentication");
|
= Property.string("kb-server-interactive-name", "Password authentication");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator}.
|
|
||||||
*/
|
|
||||||
public static final Property<String> KB_SERVER_INTERACTIVE_INSTRUCTION
|
public static final Property<String> KB_SERVER_INTERACTIVE_INSTRUCTION
|
||||||
= Property.string("kb-server-interactive-instruction", "");
|
= Property.string("kb-server-interactive-instruction", "");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator}.
|
|
||||||
*/
|
|
||||||
public static final Property<String> KB_SERVER_INTERACTIVE_LANG
|
public static final Property<String> KB_SERVER_INTERACTIVE_LANG
|
||||||
= Property.string("kb-server-interactive-language", "en-US");
|
= Property.string("kb-server-interactive-language", "en-US");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator}.
|
|
||||||
*/
|
|
||||||
public static final Property<String> KB_SERVER_INTERACTIVE_PROMPT
|
public static final Property<String> KB_SERVER_INTERACTIVE_PROMPT
|
||||||
= Property.string("kb-server-interactive-prompt", "Password: ");
|
= Property.string("kb-server-interactive-prompt", "Password: ");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator}.
|
|
||||||
*/
|
|
||||||
public static final Property<Boolean> KB_SERVER_INTERACTIVE_ECHO_PROMPT
|
public static final Property<Boolean> KB_SERVER_INTERACTIVE_ECHO_PROMPT
|
||||||
= Property.bool("kb-server-interactive-echo-prompt", false);
|
= Property.bool("kb-server-interactive-echo-prompt", false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum amount of extended (a.k.a. STDERR) data allowed to be accumulated until a {@link ChannelDataReceiver} for
|
* Maximum amount of extended (a.k.a. STDERR) data allowed to be accumulated until a ChannelDataReceiver for
|
||||||
* the data is registered
|
* the data is registered
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> MAX_EXTDATA_BUFSIZE
|
public static final Property<Integer> MAX_EXTDATA_BUFSIZE
|
||||||
= Property.integer("channel-session-max-extdata-bufsize", 0);
|
= Property.integer("channel-session-max-extdata-bufsize", 0);
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.kex.DHGEXServer}.
|
|
||||||
*/
|
|
||||||
public static final Property<Integer> PROP_DHGEX_SERVER_MIN_KEY
|
public static final Property<Integer> PROP_DHGEX_SERVER_MIN_KEY
|
||||||
= Property.integer("dhgex-server-min");
|
= Property.integer("dhgex-server-min");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.server.kex.DHGEXServer}.
|
|
||||||
*/
|
|
||||||
public static final Property<Integer> PROP_DHGEX_SERVER_MAX_KEY
|
public static final Property<Integer> PROP_DHGEX_SERVER_MAX_KEY
|
||||||
= Property.integer("dhgex-server-max");
|
= Property.integer("dhgex-server-max");
|
||||||
/**
|
/**
|
||||||
* Value used by the {@link org.apache.sshd.server.shell.InvertedShellWrapper} to control the "busy-wait"
|
* Value used to control the "busy-wait"
|
||||||
* sleep time (millis) on the pumping loop if nothing was pumped - must be <U>positive</U>.
|
* sleep time (millis) on the pumping loop if nothing was pumped - must be <U>positive</U>.
|
||||||
*/
|
*/
|
||||||
public static final Property<Duration> PUMP_SLEEP_TIME
|
public static final Property<Duration> PUMP_SLEEP_TIME
|
||||||
= Property.duration("inverted-shell-wrapper-pump-sleep", Duration.ofMillis(1));
|
= Property.duration("inverted-shell-wrapper-pump-sleep", Duration.ofMillis(1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value used by the {@link org.apache.sshd.server.shell.InvertedShellWrapper} to control copy buffer size.
|
* Value used to control copy buffer size.
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> BUFFER_SIZE
|
public static final Property<Integer> BUFFER_SIZE
|
||||||
= Property.integer("inverted-shell-wrapper-buffer-size", IoUtils.DEFAULT_COPY_SIZE);
|
= Property.integer("inverted-shell-wrapper-buffer-size", IoUtils.DEFAULT_COPY_SIZE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.x11.X11ForwardSupport} to control the channel open
|
* Configuration value to control the channel open
|
||||||
* timeout.
|
* timeout.
|
||||||
*/
|
*/
|
||||||
public static final Property<Duration> X11_OPEN_TIMEOUT
|
public static final Property<Duration> X11_OPEN_TIMEOUT
|
||||||
= Property.duration("x11-fwd-open-timeout", Duration.ofSeconds(30L));
|
= Property.duration("x11-fwd-open-timeout", Duration.ofSeconds(30L));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.x11.X11ForwardSupport} to control from which X11
|
* Configuration value to control from which X11
|
||||||
* display number to start looking for a free value.
|
* display number to start looking for a free value.
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> X11_DISPLAY_OFFSET
|
public static final Property<Integer> X11_DISPLAY_OFFSET
|
||||||
= Property.integer("x11-fwd-display-offset", 10);
|
= Property.integer("x11-fwd-display-offset", 10);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.x11.X11ForwardSupport} to control up to which (but not
|
* Configuration value to control up to which (but not
|
||||||
* including) X11 display number to look or a free value.
|
* including) X11 display number to look or a free value.
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> X11_MAX_DISPLAYS
|
public static final Property<Integer> X11_MAX_DISPLAYS
|
||||||
= Property.integer("x11-fwd-max-display", 1000);
|
= Property.integer("x11-fwd-max-display", 1000);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.x11.X11ForwardSupport} to control the base port number
|
* Configuration value to control the base port number
|
||||||
* for the X11 display number socket binding.
|
* for the X11 display number socket binding.
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> X11_BASE_PORT
|
public static final Property<Integer> X11_BASE_PORT
|
||||||
= Property.integer("x11-fwd-base-port", 6000);
|
= Property.integer("x11-fwd-base-port", 6000);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.x11.X11ForwardSupport} to control the host used to bind
|
* Configuration value to control the host used to bind
|
||||||
* to for the X11 display when looking for a free port.
|
* to for the X11 display when looking for a free port.
|
||||||
*/
|
*/
|
||||||
public static final Property<String> X11_BIND_HOST
|
public static final Property<String> X11_BIND_HOST
|
||||||
= Property.string("x11-fwd-bind-host", SshdSocketAddress.LOCALHOST_IPV4);
|
= Property.string("x11-fwd-bind-host", SshdSocketAddress.LOCALHOST_IPV4);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration value for the {@link org.apache.sshd.server.forward.TcpipServerChannel} to control the higher
|
* Configuration value to control the higher
|
||||||
* theshold for the data to be buffered waiting to be sent. If the buffered data size reaches this value, the
|
* threshold for the data to be buffered waiting to be sent. If the buffered data size reaches this value, the
|
||||||
* session will pause reading until the data length goes below the
|
* session will pause reading until the data length goes below the
|
||||||
* {@link #TCPIP_SERVER_CHANNEL_BUFFER_SIZE_THRESHOLD_LOW} threshold.
|
* {@link #TCPIP_SERVER_CHANNEL_BUFFER_SIZE_THRESHOLD_LOW} threshold.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -23,9 +23,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.apache.sshd.common.Property;
|
|
||||||
import org.apache.sshd.common.PropertyResolver;
|
|
||||||
import org.apache.sshd.common.SshConstants;
|
|
||||||
import org.apache.sshd.common.util.ValidateUtils;
|
import org.apache.sshd.common.util.ValidateUtils;
|
||||||
import org.apache.sshd.common.util.buffer.Buffer;
|
import org.apache.sshd.common.util.buffer.Buffer;
|
||||||
import org.apache.sshd.common.util.io.IoUtils;
|
import org.apache.sshd.common.util.io.IoUtils;
|
||||||
|
@ -41,8 +38,6 @@ public final class SftpModuleProperties {
|
||||||
* Used to indicate the {@link Charset} (or its name) for decoding referenced files/folders names - extracted from
|
* Used to indicate the {@link Charset} (or its name) for decoding referenced files/folders names - extracted from
|
||||||
* the client session when 1st initialized.
|
* the client session when 1st initialized.
|
||||||
*
|
*
|
||||||
* @see SftpClient#getNameDecodingCharset()
|
|
||||||
* @see SftpClient#setNameDecodingCharset(Charset)
|
|
||||||
*/
|
*/
|
||||||
public static final Property<Charset> NAME_DECODING_CHARSET
|
public static final Property<Charset> NAME_DECODING_CHARSET
|
||||||
= Property.charset("sftp-name-decoding-charset", StandardCharsets.UTF_8);
|
= Property.charset("sftp-name-decoding-charset", StandardCharsets.UTF_8);
|
||||||
|
@ -54,45 +49,26 @@ public final class SftpModuleProperties {
|
||||||
public static final Property<Duration> SFTP_CHANNEL_OPEN_TIMEOUT
|
public static final Property<Duration> SFTP_CHANNEL_OPEN_TIMEOUT
|
||||||
= Property.duration("sftp-channel-open-timeout", Duration.ofSeconds(15L));
|
= Property.duration("sftp-channel-open-timeout", Duration.ofSeconds(15L));
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystem}.
|
|
||||||
*/
|
|
||||||
public static final Property<Integer> POOL_SIZE
|
public static final Property<Integer> POOL_SIZE
|
||||||
= Property.integer("sftp-fs-pool-size", 8);
|
= Property.integer("sftp-fs-pool-size", 8);
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystemProvider}.
|
|
||||||
*/
|
|
||||||
public static final Property<Integer> READ_BUFFER_SIZE
|
public static final Property<Integer> READ_BUFFER_SIZE
|
||||||
= Property.integer("sftp-fs-read-buffer-size");
|
= Property.integer("sftp-fs-read-buffer-size");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystemProvider}.
|
|
||||||
*/
|
|
||||||
public static final Property<Integer> WRITE_BUFFER_SIZE
|
public static final Property<Integer> WRITE_BUFFER_SIZE
|
||||||
= Property.integer("sftp-fs-write-buffer-size");
|
= Property.integer("sftp-fs-write-buffer-size");
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystemProvider}.
|
|
||||||
*/
|
|
||||||
public static final Property<Duration> CONNECT_TIME
|
public static final Property<Duration> CONNECT_TIME
|
||||||
= Property.duration("sftp-fs-connect-time", Duration.ofSeconds(15L));
|
= Property.duration("sftp-fs-connect-time", Duration.ofSeconds(15L));
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystemProvider}.
|
|
||||||
*/
|
|
||||||
public static final Property<Duration> AUTH_TIME
|
public static final Property<Duration> AUTH_TIME
|
||||||
= Property.duration("sftp-fs-auth-time", Duration.ofSeconds(15L));
|
= Property.duration("sftp-fs-auth-time", Duration.ofSeconds(15L));
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link org.apache.sshd.sftp.client.fs.SftpFileSystemProvider}.
|
|
||||||
*/
|
|
||||||
public static final Property<Charset> NAME_DECODER_CHARSET
|
public static final Property<Charset> NAME_DECODER_CHARSET
|
||||||
= Property.charset("sftp-fs-name-decoder-charset", StandardCharsets.UTF_8);
|
= Property.charset("sftp-fs-name-decoder-charset", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property used to avoid large buffers when
|
* Property used to avoid large buffers when
|
||||||
* {@link org.apache.sshd.sftp.client.impl.AbstractSftpClient#write(SftpClient.Handle, long, byte[], int, int)} is
|
|
||||||
* invoked with a large buffer size.
|
* invoked with a large buffer size.
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> WRITE_CHUNK_SIZE
|
public static final Property<Integer> WRITE_CHUNK_SIZE
|
||||||
|
@ -122,25 +98,22 @@ public final class SftpModuleProperties {
|
||||||
/**
|
/**
|
||||||
* Allows controlling reports of which client extensions are supported (and reported via "support" and
|
* Allows controlling reports of which client extensions are supported (and reported via "support" and
|
||||||
* "support2" server extensions) as a comma-separate list of names. <B>Note:</B> requires overriding the
|
* "support2" server extensions) as a comma-separate list of names. <B>Note:</B> requires overriding the
|
||||||
* {@link AbstractSftpSubsystemHelper#executeExtendedCommand(Buffer, int, String)} command accordingly. If empty
|
* command accordingly. If empty
|
||||||
* string is set then no server extensions are reported
|
* string is set then no server extensions are reported
|
||||||
*
|
*
|
||||||
* @see AbstractSftpSubsystemHelper#DEFAULT_SUPPORTED_CLIENT_EXTENSIONS
|
|
||||||
*/
|
*/
|
||||||
public static final Property<String> CLIENT_EXTENSIONS
|
public static final Property<String> CLIENT_EXTENSIONS
|
||||||
= Property.string("sftp-client-extensions");
|
= Property.string("sftp-client-extensions");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comma-separated list of which {@code OpenSSH} extensions are reported and what version is reported for each -
|
* Comma-separated list of which {@code OpenSSH} extensions are reported and what version is reported for each -
|
||||||
* format: {@code name=version}. If empty value set, then no such extensions are reported. Otherwise, the
|
* format: {@code name=version}. If empty value set, then no such extensions are reported.
|
||||||
* {@link AbstractSftpSubsystemHelper#DEFAULT_OPEN_SSH_EXTENSIONS} are used
|
|
||||||
*/
|
*/
|
||||||
public static final Property<String> OPENSSH_EXTENSIONS
|
public static final Property<String> OPENSSH_EXTENSIONS
|
||||||
= Property.string("sftp-openssh-extensions");
|
= Property.string("sftp-openssh-extensions");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comma separate list of {@code SSH_ACL_CAP_xxx} names - where name can be without the prefix. If not defined then
|
* Comma separate list of {@code SSH_ACL_CAP_xxx} names - where name can be without the prefix.
|
||||||
* {@link AbstractSftpSubsystemHelper#DEFAULT_ACL_SUPPORTED_MASK} is used
|
|
||||||
*/
|
*/
|
||||||
public static final Property<String> ACL_SUPPORTED_MASK
|
public static final Property<String> ACL_SUPPORTED_MASK
|
||||||
= Property.string("sftp-acl-supported-mask");
|
= Property.string("sftp-acl-supported-mask");
|
||||||
|
@ -152,7 +125,7 @@ public final class SftpModuleProperties {
|
||||||
= Property.string("sftp-newline", IoUtils.EOL);
|
= Property.string("sftp-newline", IoUtils.EOL);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the use of a max. packet length for {@link AbstractSftpSubsystemHelper#doRead(Buffer, int)} protection
|
* Force the use of a max. packet length for protection
|
||||||
* against malicious packets
|
* against malicious packets
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> MAX_READDATA_PACKET_LENGTH
|
public static final Property<Integer> MAX_READDATA_PACKET_LENGTH
|
||||||
|
@ -187,9 +160,6 @@ public final class SftpModuleProperties {
|
||||||
/**
|
/**
|
||||||
* Max. rounds to attempt to create a unique file handle - if all handles already in use after these many rounds,
|
* Max. rounds to attempt to create a unique file handle - if all handles already in use after these many rounds,
|
||||||
* then an exception is thrown
|
* then an exception is thrown
|
||||||
*
|
|
||||||
* @see SftpSubsystem#generateFileHandle(Path)
|
|
||||||
* @see #DEFAULT_FILE_HANDLE_ROUNDS
|
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> MAX_FILE_HANDLE_RAND_ROUNDS
|
public static final Property<Integer> MAX_FILE_HANDLE_RAND_ROUNDS
|
||||||
= Property.validating(
|
= Property.validating(
|
||||||
|
@ -200,8 +170,7 @@ public final class SftpModuleProperties {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum amount of data allocated for listing the contents of a directory in any single invocation of
|
* Maximum amount of data allocated for listing the contents of a directory in any single invocation
|
||||||
* {@link SftpSubsystem#doReadDir(Buffer, int)}
|
|
||||||
*/
|
*/
|
||||||
public static final Property<Integer> MAX_READDIR_DATA_SIZE
|
public static final Property<Integer> MAX_READDIR_DATA_SIZE
|
||||||
= Property.integer("sftp-max-readdir-data-size", 16 * 1024);
|
= Property.integer("sftp-max-readdir-data-size", 16 * 1024);
|
||||||
|
|
|
@ -36,7 +36,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.IntUnaryOperator;
|
import java.util.function.IntUnaryOperator;
|
||||||
|
|
||||||
import org.apache.sshd.common.AttributeRepository;
|
|
||||||
import org.apache.sshd.common.Closeable;
|
import org.apache.sshd.common.Closeable;
|
||||||
import org.apache.sshd.common.FactoryManager;
|
import org.apache.sshd.common.FactoryManager;
|
||||||
import org.apache.sshd.common.PropertyResolver;
|
import org.apache.sshd.common.PropertyResolver;
|
||||||
|
@ -100,7 +99,7 @@ public abstract class AbstractChannel extends AbstractInnerCloseable implements
|
||||||
private int id = -1;
|
private int id = -1;
|
||||||
private int recipient = -1;
|
private int recipient = -1;
|
||||||
private Session sessionInstance;
|
private Session sessionInstance;
|
||||||
private CloseableExecutorService executor;
|
private final CloseableExecutorService executor;
|
||||||
private final List<RequestHandler<Channel>> requestHandlers = new CopyOnWriteArrayList<>();
|
private final List<RequestHandler<Channel>> requestHandlers = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private final Window localWindow;
|
private final Window localWindow;
|
||||||
|
|
|
@ -704,7 +704,7 @@ public final class KeyUtils {
|
||||||
* @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default
|
* @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default
|
||||||
* fingerprint.
|
* fingerprint.
|
||||||
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
||||||
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
|
* @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint,
|
||||||
* {@code null} if no key.
|
* {@code null} if no key.
|
||||||
* @see #getDefaultFingerPrintFactory()
|
* @see #getDefaultFingerPrintFactory()
|
||||||
* @see #checkFingerPrint(String, Factory, PublicKey)
|
* @see #checkFingerPrint(String, Factory, PublicKey)
|
||||||
|
@ -718,7 +718,7 @@ public final class KeyUtils {
|
||||||
* fingerprint.
|
* fingerprint.
|
||||||
* @param f The {@link Factory} to be used to generate the default {@link Digest} for the key
|
* @param f The {@link Factory} to be used to generate the default {@link Digest} for the key
|
||||||
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
||||||
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
|
* @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint,
|
||||||
* {@code null} if no key.
|
* {@code null} if no key.
|
||||||
*/
|
*/
|
||||||
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(
|
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(
|
||||||
|
@ -731,7 +731,7 @@ public final class KeyUtils {
|
||||||
* fingerprint.
|
* fingerprint.
|
||||||
* @param d The {@link Digest} to be used to generate the default fingerprint for the key
|
* @param d The {@link Digest} to be used to generate the default fingerprint for the key
|
||||||
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
* @param key the {@link PublicKey} - if {@code null} then returns null.
|
||||||
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
|
* @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint,
|
||||||
* {@code null} if no key.
|
* {@code null} if no key.
|
||||||
*/
|
*/
|
||||||
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, Digest d, PublicKey key) {
|
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, Digest d, PublicKey key) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
|
||||||
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractResourceKeyPairProvider<R> extends AbstractKeyPairProvider {
|
public abstract class AbstractResourceKeyPairProvider<R> extends AbstractKeyPairProvider {
|
||||||
|
|
||||||
private FilePasswordProvider passwordFinder;
|
private FilePasswordProvider passwordFinder;
|
||||||
/*
|
/*
|
||||||
* NOTE: the map is case insensitive even for Linux, as it is (very) bad practice to have 2 key files that differ
|
* NOTE: the map is case insensitive even for Linux, as it is (very) bad practice to have 2 key files that differ
|
||||||
|
|
|
@ -165,7 +165,7 @@ public final class OsUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove {@code Windows} domain and/or group prefix as well as "(User);" suffix
|
* Remove {@code Windows} domain and/or group prefix as well as "(User);" suffix
|
||||||
*
|
*
|
||||||
* @param user The original username - ignored if {@code null}/empty
|
* @param user The original username - ignored if {@code null}/empty
|
||||||
* @return The canonical user - unchanged if {@code Unix} O/S
|
* @return The canonical user - unchanged if {@code Unix} O/S
|
||||||
|
|
|
@ -63,13 +63,13 @@ public interface BufferPublicKeyParser<PUB extends PublicKey> {
|
||||||
OpenSSHCertPublicKeyParser.INSTANCE));
|
OpenSSHCertPublicKeyParser.INSTANCE));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param keyType The key type - e.g., "ssh-rsa", "ssh-dss"
|
* @param keyType The key type - e.g., ssh-rsa, ssh-dss
|
||||||
* @return {@code true} if this key type is supported by the parser
|
* @return {@code true} if this key type is supported by the parser
|
||||||
*/
|
*/
|
||||||
boolean isKeyTypeSupported(String keyType);
|
boolean isKeyTypeSupported(String keyType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param keyType The key type - e.g., "ssh-rsa", "ssh-dss"
|
* @param keyType The key type - e.g., ssh-rsa, ssh-dss
|
||||||
* @param buffer The {@link Buffer} containing the encoded raw public key
|
* @param buffer The {@link Buffer} containing the encoded raw public key
|
||||||
* @return The decoded {@link PublicKey}
|
* @return The decoded {@link PublicKey}
|
||||||
* @throws GeneralSecurityException If failed to generate the key
|
* @throws GeneralSecurityException If failed to generate the key
|
||||||
|
|
|
@ -78,10 +78,6 @@ public final class Builder implements ObjectBuilder<Closeable> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder sequential(Object id, Iterable<Closeable> closeables) {
|
|
||||||
return close(new SequentialCloseable(id, lock, closeables));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder parallel(Closeable... closeables) {
|
public Builder parallel(Closeable... closeables) {
|
||||||
if (closeables.length == 1) {
|
if (closeables.length == 1) {
|
||||||
close(closeables[0]);
|
close(closeables[0]);
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.sshd.common.util.closeable;
|
package org.apache.sshd.common.util.closeable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import org.apache.sshd.common.Closeable;
|
import org.apache.sshd.common.Closeable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,4 +27,12 @@ import org.apache.sshd.common.Closeable;
|
||||||
*/
|
*/
|
||||||
public abstract class IoBaseCloseable implements Closeable {
|
public abstract class IoBaseCloseable implements Closeable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
Closeable.close(this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class SequentialCloseable extends SimpleCloseable {
|
||||||
@Override
|
@Override
|
||||||
protected void doClose(boolean immediately) {
|
protected void doClose(boolean immediately) {
|
||||||
Iterator<? extends Closeable> iterator = closeables.iterator();
|
Iterator<? extends Closeable> iterator = closeables.iterator();
|
||||||
SshFutureListener<CloseFuture> listener = new SshFutureListener<CloseFuture>() {
|
SshFutureListener<CloseFuture> listener = new SshFutureListener<>() {
|
||||||
@SuppressWarnings("synthetic-access")
|
@SuppressWarnings("synthetic-access")
|
||||||
@Override
|
@Override
|
||||||
public void operationComplete(CloseFuture previousFuture) {
|
public void operationComplete(CloseFuture previousFuture) {
|
||||||
|
|
|
@ -76,7 +76,7 @@ public class DERWriter extends FilterOutputStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The integer is always considered to be positive, so if the first byte is < 0, we pad with a zero to make it
|
* The integer is always considered to be positive, so if the first byte is < 0, we pad with a zero to make it
|
||||||
* positive
|
* positive
|
||||||
*
|
*
|
||||||
* @param bytes {@link BigInteger} bytes
|
* @param bytes {@link BigInteger} bytes
|
||||||
|
@ -87,7 +87,7 @@ public class DERWriter extends FilterOutputStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The integer is always considered to be positive, so if the first byte is < 0, we pad with a zero to make it
|
* The integer is always considered to be positive, so if the first byte is < 0, we pad with a zero to make it
|
||||||
* positive
|
* positive
|
||||||
*
|
*
|
||||||
* @param bytes {@link BigInteger} bytes
|
* @param bytes {@link BigInteger} bytes
|
||||||
|
|
|
@ -111,7 +111,7 @@ public final class SecurityUtils {
|
||||||
/**
|
/**
|
||||||
* The min. key size value used for testing whether Diffie-Hellman Group Exchange is supported or not. According to
|
* The min. key size value used for testing whether Diffie-Hellman Group Exchange is supported or not. According to
|
||||||
* <A HREF="https://tools.ietf.org/html/rfc4419">RFC 4419</A> section 3: "Servers and clients SHOULD support
|
* <A HREF="https://tools.ietf.org/html/rfc4419">RFC 4419</A> section 3: "Servers and clients SHOULD support
|
||||||
* groups with a modulus length of k bits, where 1024 <= k <= 8192". </code>
|
* groups with a modulus length of k bits, where 1024 <= k <= 8192". </code>
|
||||||
*
|
*
|
||||||
* <B>Note: this has been amended by <A HREF="https://tools.ietf.org/html/rfc8270">RFC 8270</A>
|
* <B>Note: this has been amended by <A HREF="https://tools.ietf.org/html/rfc8270">RFC 8270</A>
|
||||||
*/
|
*/
|
||||||
|
@ -384,7 +384,6 @@ public final class SecurityUtils {
|
||||||
if (REGISTRATION_STATE_HOLDER.get()) {
|
if (REGISTRATION_STATE_HOLDER.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String regsList = System.getProperty(SECURITY_PROVIDER_REGISTRARS,
|
String regsList = System.getProperty(SECURITY_PROVIDER_REGISTRARS,
|
||||||
GenericUtils.join(DEFAULT_SECURITY_PROVIDER_REGISTRARS, ','));
|
GenericUtils.join(DEFAULT_SECURITY_PROVIDER_REGISTRARS, ','));
|
||||||
boolean bouncyCastleRegistered = false;
|
boolean bouncyCastleRegistered = false;
|
||||||
|
@ -475,13 +474,11 @@ public final class SecurityUtils {
|
||||||
if (parser == null) {
|
if (parser == null) {
|
||||||
throw new NoSuchProviderException("No registered key-pair resource parser");
|
throw new NoSuchProviderException("No registered key-pair resource parser");
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<KeyPair> ids = parser.loadKeyPairs(session, resourceKey, provider, inputStream);
|
Collection<KeyPair> ids = parser.loadKeyPairs(session, resourceKey, provider, inputStream);
|
||||||
int numLoaded = GenericUtils.size(ids);
|
int numLoaded = GenericUtils.size(ids);
|
||||||
if (numLoaded <= 0) {
|
if (numLoaded <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,27 @@
|
||||||
|
|
||||||
package org.apache.sshd.common.util.threads;
|
package org.apache.sshd.common.util.threads;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.sshd.common.Closeable;
|
import org.apache.sshd.common.Closeable;
|
||||||
|
|
||||||
public interface CloseableExecutorService extends ExecutorService, Closeable {
|
public interface CloseableExecutorService extends ExecutorService, Closeable {
|
||||||
default boolean awaitTermination(Duration timeout) throws InterruptedException {
|
|
||||||
|
default boolean awaitTermination(Duration timeout) throws InterruptedException {
|
||||||
Objects.requireNonNull(timeout, "No timeout specified");
|
Objects.requireNonNull(timeout, "No timeout specified");
|
||||||
return awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
return awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() {
|
||||||
|
try {
|
||||||
|
Closeable.close(this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,6 @@ public class DavPropertyNameSet extends PropContainer
|
||||||
* in order to successfully add the given entry.
|
* in order to successfully add the given entry.
|
||||||
* @return true if contentEntry is an instance of <code>DavPropertyName</code>
|
* @return true if contentEntry is an instance of <code>DavPropertyName</code>
|
||||||
* that could be added to this set. False otherwise.
|
* that could be added to this set. False otherwise.
|
||||||
* @see PropContainer#addContent(Object)
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean addContent(PropEntry contentEntry) {
|
public boolean addContent(PropEntry contentEntry) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
group = org.xbib
|
group = org.xbib
|
||||||
name = files
|
name = files
|
||||||
version = 4.0.0
|
version = 4.1.0
|
||||||
|
|
||||||
org.gradle.warning.mode = ALL
|
org.gradle.warning.mode = ALL
|
||||||
|
|
|
@ -2,21 +2,14 @@
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
modularity.inferModulePath.set(true)
|
modularity.inferModulePath.set(true)
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
compileTestJava {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes('Implementation-Version': project.version)
|
attributes('Implementation-Version': project.version)
|
||||||
|
@ -24,7 +17,9 @@ jar {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
options.compilerArgs.add('-Xlint:all,-exports')
|
options.fork = true
|
||||||
|
options.forkOptions.jvmArgs += ['-Duser.language=en','-Duser.country=US']
|
||||||
|
options.compilerArgs.add('-Xlint:all')
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
22
gradlew
vendored
22
gradlew
vendored
|
@ -83,7 +83,8 @@ done
|
||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -130,10 +131,13 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
|
@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -198,11 +202,11 @@ fi
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
|
@ -15,10 +15,10 @@ pluginManagement {
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
libs {
|
libs {
|
||||||
version('gradle', '8.1.1')
|
version('gradle', '8.4')
|
||||||
version('groovy', '4.0.12')
|
version('groovy', '4.0.12')
|
||||||
version('junit', '5.9.3')
|
version('junit', '5.10.0')
|
||||||
version('net', '3.2.0')
|
version('net', '4.0.0')
|
||||||
library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit')
|
library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit')
|
||||||
library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit')
|
library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit')
|
||||||
library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit')
|
library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit')
|
||||||
|
@ -27,8 +27,8 @@ dependencyResolutionManagement {
|
||||||
library('junit4', 'junit', 'junit').version('4.13.2')
|
library('junit4', 'junit', 'junit').version('4.13.2')
|
||||||
library('net-security', 'org.xbib', 'net-security').versionRef('net')
|
library('net-security', 'org.xbib', 'net-security').versionRef('net')
|
||||||
library('mockftpserver', 'org.mockftpserver', 'MockFtpServer').version('2.7.1')
|
library('mockftpserver', 'org.mockftpserver', 'MockFtpServer').version('2.7.1')
|
||||||
library('mockito-core', 'org.mockito', 'mockito-core').version('3.7.7')
|
library('mockito-core', 'org.mockito', 'mockito-core').version('5.6.0')
|
||||||
library('mockito-junit-jupiter', 'org.mockito', 'mockito-junit-jupiter').version('3.7.7')
|
library('mockito-junit-jupiter', 'org.mockito', 'mockito-junit-jupiter').version('5.6.0')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue