From: Mathieu Baudier Date: Wed, 25 May 2022 07:51:42 +0000 (+0200) Subject: Reintroduce CMS SSH and rename CMS SQL. X-Git-Tag: v2.3.10~220 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=27ce3504c4058f5ba9f73eb6c5aa5dac5ed2421c Reintroduce CMS SSH and rename CMS SQL. --- diff --git a/Makefile b/Makefile index af16906cc..41cbb6315 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,8 @@ org.argeo.api.uuid \ org.argeo.api.acr \ org.argeo.api.cms \ org.argeo.cms \ -org.argeo.cms.pgsql \ +org.argeo.cms.sql \ +org.argeo.cms.ssh \ org.argeo.cms.ux \ eclipse/org.argeo.ext.equinox.jetty \ eclipse/org.argeo.cms.servlet \ diff --git a/org.argeo.cms.pgsql/.classpath b/org.argeo.cms.pgsql/.classpath deleted file mode 100644 index e801ebfb4..000000000 --- a/org.argeo.cms.pgsql/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.argeo.cms.pgsql/.project b/org.argeo.cms.pgsql/.project deleted file mode 100644 index ff01ad98d..000000000 --- a/org.argeo.cms.pgsql/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - org.argeo.cms.pgsql - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/org.argeo.cms.pgsql/bnd.bnd b/org.argeo.cms.pgsql/bnd.bnd deleted file mode 100644 index 9c7300926..000000000 --- a/org.argeo.cms.pgsql/bnd.bnd +++ /dev/null @@ -1 +0,0 @@ -Import-Package: org.postgresql;version="[42,43)" diff --git a/org.argeo.cms.pgsql/build.properties b/org.argeo.cms.pgsql/build.properties deleted file mode 100644 index 34d2e4d2d..000000000 --- a/org.argeo.cms.pgsql/build.properties +++ /dev/null @@ -1,4 +0,0 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - . diff --git a/org.argeo.cms.pgsql/src/org/argeo/cms/pgsql/util/CheckPg.java b/org.argeo.cms.pgsql/src/org/argeo/cms/pgsql/util/CheckPg.java deleted file mode 100644 index 9db43df25..000000000 --- a/org.argeo.cms.pgsql/src/org/argeo/cms/pgsql/util/CheckPg.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.cms.pgsql.util; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import org.postgresql.Driver; - -/** Simple PostgreSQL check. */ -public class CheckPg { - - public List listTables() { - String osUser = System.getProperty("user.name"); - - String url = "jdbc:postgresql://localhost/" + osUser; - Properties props = new Properties(); - props.setProperty("user", osUser); - props.setProperty("password", "changeit"); - List result = new ArrayList<>(); - - Driver driver = new Driver(); - try (Connection conn = driver.connect(url, props); Statement s = conn.createStatement();) { - s.execute("SELECT * FROM pg_catalog.pg_tables"); - ResultSet rs = s.getResultSet(); - while (rs.next()) { - result.add(rs.getString("tablename")); - } - return result; - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - public static void main(String[] args) { - new CheckPg().listTables().forEach(System.out::println); - } - -} diff --git a/org.argeo.cms.sql/.classpath b/org.argeo.cms.sql/.classpath new file mode 100644 index 000000000..e801ebfb4 --- /dev/null +++ b/org.argeo.cms.sql/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.sql/.project b/org.argeo.cms.sql/.project new file mode 100644 index 000000000..b96a541b2 --- /dev/null +++ b/org.argeo.cms.sql/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.sql + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.sql/bnd.bnd b/org.argeo.cms.sql/bnd.bnd new file mode 100644 index 000000000..9c7300926 --- /dev/null +++ b/org.argeo.cms.sql/bnd.bnd @@ -0,0 +1 @@ +Import-Package: org.postgresql;version="[42,43)" diff --git a/org.argeo.cms.sql/build.properties b/org.argeo.cms.sql/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms.sql/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java b/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java new file mode 100644 index 000000000..bc002a6a5 --- /dev/null +++ b/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java @@ -0,0 +1,42 @@ +package org.argeo.cms.sql.postgres; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.postgresql.Driver; + +/** Simple PostgreSQL check. */ +public class CheckPg { + + public List listTables() { + String osUser = System.getProperty("user.name"); + + String url = "jdbc:postgresql://localhost/" + osUser; + Properties props = new Properties(); + props.setProperty("user", osUser); + props.setProperty("password", "changeit"); + List result = new ArrayList<>(); + + Driver driver = new Driver(); + try (Connection conn = driver.connect(url, props); Statement s = conn.createStatement();) { + s.execute("SELECT * FROM pg_catalog.pg_tables"); + ResultSet rs = s.getResultSet(); + while (rs.next()) { + result.add(rs.getString("tablename")); + } + return result; + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + new CheckPg().listTables().forEach(System.out::println); + } + +} diff --git a/org.argeo.cms.ssh/.classpath b/org.argeo.cms.ssh/.classpath new file mode 100644 index 000000000..81fe078c2 --- /dev/null +++ b/org.argeo.cms.ssh/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.ssh/.project b/org.argeo.cms.ssh/.project new file mode 100644 index 000000000..6b0c9dd49 --- /dev/null +++ b/org.argeo.cms.ssh/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.ssh + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..997d6645b --- /dev/null +++ b/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,104 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.cms.ssh/bnd.bnd b/org.argeo.cms.ssh/bnd.bnd new file mode 100644 index 000000000..e69de29bb diff --git a/org.argeo.cms.ssh/build.properties b/org.argeo.cms.ssh/build.properties new file mode 100644 index 000000000..17273d1ff --- /dev/null +++ b/org.argeo.cms.ssh/build.properties @@ -0,0 +1,9 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . +additional.bundles = org.apache.sshd.common,\ + org.apache.sshd.core,\ + org.slf4j.api,\ + org.argeo.ext.slf4j + \ No newline at end of file diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java new file mode 100644 index 000000000..cb53f7c1e --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java @@ -0,0 +1,188 @@ +package org.argeo.cms.ssh; + +import java.io.Console; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.NoCloseOutputStream; +import org.argeo.api.cms.CmsLog; + +@SuppressWarnings("restriction") +abstract class AbstractSsh { + private final static CmsLog log = CmsLog.getLog(AbstractSsh.class); + + private static SshClient sshClient; + private static SftpFileSystemProvider sftpFileSystemProvider; + + private boolean passwordSet = false; + private ClientSession session; + + private SshKeyPair sshKeyPair; + + synchronized SshClient getSshClient() { + if (sshClient == null) { + long begin = System.currentTimeMillis(); + sshClient = SshClient.setUpDefaultClient(); + sshClient.start(); + long duration = System.currentTimeMillis() - begin; + if (log.isDebugEnabled()) + log.debug("SSH client started in " + duration + " ms"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> sshClient.stop(), "Stop SSH client")); + } + return sshClient; + } + + synchronized SftpFileSystemProvider getSftpFileSystemProvider() { + if (sftpFileSystemProvider == null) { + sftpFileSystemProvider = new SftpFileSystemProvider(sshClient); + } + return sftpFileSystemProvider; + } + + void authenticate() { + try { + if (sshKeyPair != null) { + session.addPublicKeyIdentity(sshKeyPair.asKeyPair()); + } else { + + if (!passwordSet) { + String password; + Console console = System.console(); + if (console == null) {// IDE + System.out.print("Password: "); + try (Scanner s = new Scanner(System.in)) { + password = s.next(); + } + } else { + console.printf("Password: "); + char[] pwd = console.readPassword(); + password = new String(pwd); + Arrays.fill(pwd, ' '); + } + session.addPasswordIdentity(password); + passwordSet = true; + } + } + session.auth().verify(1000l); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + void addPassword(String password) { + session.addPasswordIdentity(password); + } + + void loadKey(String password) { + loadKey(password, System.getProperty("user.home") + "/.ssh/id_rsa"); + } + + void loadKey(String password, String keyPath) { +// try { +// KeyPair keyPair = ClientIdentityLoader.DEFAULT.loadClientIdentity(keyPath, +// FilePasswordProvider.of(password)); +// session.addPublicKeyIdentity(keyPair); +// } catch (IOException | GeneralSecurityException e) { +// throw new IllegalStateException(e); +// } + } + + void openSession(URI uri) { + openSession(uri.getUserInfo(), uri.getHost(), uri.getPort() > 0 ? uri.getPort() : null); + } + + void openSession(String login, String host, Integer port) { + if (session != null) + throw new IllegalStateException("Session is already open"); + + if (host == null) + host = "localhost"; + if (port == null) + port = 22; + if (login == null) + login = System.getProperty("user.name"); + String password = null; + int sepIndex = login.indexOf(':'); + if (sepIndex > 0) + if (sepIndex + 1 < login.length()) { + password = login.substring(sepIndex + 1); + login = login.substring(0, sepIndex); + } else { + throw new IllegalArgumentException("Illegal authority: " + login); + } + try { + ConnectFuture connectFuture = getSshClient().connect(login, host, port); + connectFuture.await(); + ClientSession session = connectFuture.getSession(); + if (password != null) { + session.addPasswordIdentity(password); + passwordSet = true; + } + this.session = session; + } catch (IOException e) { + throw new IllegalStateException("Cannot connect to " + host + ":" + port); + } + } + + void closeSession() { + if (session == null) + throw new IllegalStateException("No session is open"); + try { + session.close(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + session = null; + } + } + + ClientSession getSession() { + return session; + } + + public void setSshKeyPair(SshKeyPair sshKeyPair) { + this.sshKeyPair = sshKeyPair; + } + + public static void openShell(ClientSession session) { + try (ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_SHELL)) { + channel.setIn(new NoCloseInputStream(System.in)); + channel.setOut(new NoCloseOutputStream(System.out)); + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open(); + + Set events = new HashSet<>(); + events.add(ClientChannelEvent.CLOSED); + channel.waitFor(events, 0); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + session.close(false); + } + } + + static URI toUri(String username, String host, int port) { + try { + if (username == null) + username = "root"; + return new URI("ssh://" + username + "@" + host + ":" + port); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot generate SSH URI to " + host + ":" + port + " for " + username, + e); + } + } + +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java new file mode 100644 index 000000000..3e2f9aafb --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java @@ -0,0 +1,108 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.shell.ProcessShellFactory; +import org.argeo.util.OS; + +/** A simple SSH server with some defaults. Supports SCP. */ +@SuppressWarnings("restriction") +public class BasicSshServer { + private Integer port; + private Path hostKeyPath; + + private SshServer sshd = null; + + public BasicSshServer(Integer port, Path hostKeyPath) { + this.port = port; + this.hostKeyPath = hostKeyPath; + } + + public void init() { + try { + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + if (hostKeyPath == null) + throw new IllegalStateException("An SSH server key must be set"); + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath)); + // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", + // "-l" })); + String[] shellCommand = OS.LOCAL.getDefaultShellCommand(); + // FIXME transfer args +// sshd.setShellFactory(new ProcessShellFactory(shellCommand)); + sshd.setShellFactory(new ProcessShellFactory(shellCommand[0], shellCommand)); + sshd.setCommandFactory(new ScpCommandFactory()); + + sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true)); + sshd.start(); + } catch (Exception e) { + throw new RuntimeException("Cannot start SSH server on port " + port, e); + } + } + + public void destroy() { + try { + sshd.stop(); + } catch (IOException e) { + throw new RuntimeException("Cannot stop SSH server on port " + port, e); + } + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Path getHostKeyPath() { + return hostKeyPath; + } + + public void setHostKeyPath(Path hostKeyPath) { + this.hostKeyPath = hostKeyPath; + } + + public static void main(String[] args) { + int port = 2222; + Path hostKeyPath = Paths.get("hostkey.ser"); + try { + if (args.length > 0) + port = Integer.parseInt(args[0]); + if (args.length > 1) + hostKeyPath = Paths.get(args[1]); + } catch (Exception e1) { + printUsage(); + } + + BasicSshServer sshServer = new BasicSshServer(port, hostKeyPath); + sshServer.init(); + Runtime.getRuntime().addShutdownHook(new Thread("Shutdown SSH server") { + + @Override + public void run() { + sshServer.destroy(); + } + }); + try { + synchronized (sshServer) { + sshServer.wait(); + } + } catch (InterruptedException e) { + sshServer.destroy(); + } + + } + + public static void printUsage() { + System.out.println("java " + BasicSshServer.class.getName() + " [port] [server key path]"); + } + +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java new file mode 100644 index 000000000..b27254713 --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java @@ -0,0 +1,42 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Path; + +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem; + +/** Create an SFTP {@link FileSystem}. */ +public class Sftp extends AbstractSsh { + private URI uri; + + private SftpFileSystem fileSystem; + + public Sftp(String username, String host, int port) { + this(AbstractSsh.toUri(username, host, port)); + } + + public Sftp(URI uri) { + this.uri = uri; + openSession(uri); + } + + public FileSystem getFileSystem() { + if (fileSystem == null) { + try { + authenticate(); + fileSystem = getSftpFileSystemProvider().newFileSystem(getSession()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + return fileSystem; + } + + public Path getBasePath() { + String p = uri.getPath() != null ? uri.getPath() : "/"; + return getFileSystem().getPath(p); + } + +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java new file mode 100644 index 000000000..6e7b6ef2a --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java @@ -0,0 +1,81 @@ +package org.argeo.cms.ssh; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +/** Create an SSH shell. */ +public class Ssh extends AbstractSsh { + private final URI uri; + + public Ssh(String username, String host, int port) { + this(AbstractSsh.toUri(username, host, port)); + } + + public Ssh(URI uri) { + this.uri = uri; + openSession(uri); + } + + public static void main(String[] args) { + Options options = getOptions(); + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse(options, args); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + System.err.println("There must be at least one argument"); + printHelp(options); + System.exit(1); + } + URI uri = new URI("ssh://" + remaining.get(0)); + List command = new ArrayList<>(); + if (remaining.size() > 1) { + for (int i = 1; i < remaining.size(); i++) { + command.add(remaining.get(i)); + } + } + + // auth + Ssh ssh = new Ssh(uri); + ssh.authenticate(); + + if (command.size() == 0) {// shell + AbstractSsh.openShell(ssh.getSession()); + } else {// execute command + + } + ssh.closeSession(); + } catch (Exception exp) { + exp.printStackTrace(); + printHelp(options); + System.exit(1); + } finally { + + } + } + + public URI getUri() { + return uri; + } + + public static Options getOptions() { + Options options = new Options(); +// options.addOption("p", true, "port"); + options.addOption(Option.builder("p").hasArg().argName("port").desc("port of the SSH server").build()); + + return options; + } + + public static void printHelp(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("ssh [username@]hostname", options, true); + } +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java new file mode 100644 index 000000000..ed1818d40 --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java @@ -0,0 +1,182 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.RSAPublicKeySpec; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.operator.InputDecryptorProvider; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; + +@SuppressWarnings("restriction") +public class SshKeyPair { + public final static String RSA_KEY_TYPE = "ssh-rsa"; + + private PublicKey publicKey; + private PrivateKey privateKey; + private KeyPair keyPair; + + public SshKeyPair(KeyPair keyPair) { + super(); + this.publicKey = keyPair.getPublic(); + this.privateKey = keyPair.getPrivate(); + this.keyPair = keyPair; + } + + public SshKeyPair(PublicKey publicKey, PrivateKey privateKey) { + super(); + this.publicKey = publicKey; + this.privateKey = privateKey; + this.keyPair = new KeyPair(publicKey, privateKey); + } + + public KeyPair asKeyPair() { + return keyPair; + } + + public String getPublicKeyAsOpenSshString() { + return PublicKeyEntry.toString(publicKey); + } + + public String getPrivateKeyAsPemString(char[] password) { + try { + Object obj; + + if (password != null) { + JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder( + PKCS8Generator.PBE_SHA1_3DES); + encryptorBuilder.setPasssword(password); + OutputEncryptor oe = encryptorBuilder.build(); + JcaPKCS8Generator gen = new JcaPKCS8Generator(privateKey, oe); + obj = gen.generate(); + } else { + obj = privateKey; + } + + StringWriter sw = new StringWriter(); + JcaPEMWriter pemWrt = new JcaPEMWriter(sw); + pemWrt.writeObject(obj); + pemWrt.close(); + return sw.toString(); + } catch (Exception e) { + throw new RuntimeException("Cannot convert private key", e); + } + } + + public static SshKeyPair loadOrGenerate(Path privateKeyPath, int size, char[] password) { + try { + SshKeyPair sshKeyPair; + if (Files.exists(privateKeyPath)) { +// String privateKeyStr = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.US_ASCII); + sshKeyPair = load( + new InputStreamReader(Files.newInputStream(privateKeyPath), StandardCharsets.US_ASCII), + password); + // TOD make sure public key is consistemt + } else { + sshKeyPair = generate(size); + Files.write(privateKeyPath, + sshKeyPair.getPrivateKeyAsPemString(password).getBytes(StandardCharsets.US_ASCII)); + Path publicKeyPath = privateKeyPath.resolveSibling(privateKeyPath.getFileName() + ".pub"); + Files.write(publicKeyPath, + sshKeyPair.getPublicKeyAsOpenSshString().getBytes(StandardCharsets.US_ASCII)); + } + return sshKeyPair; + } catch (IOException e) { + throw new RuntimeException("Cannot read or write private key " + privateKeyPath, e); + } + } + + public static SshKeyPair generate(int size) { + return generate(RSA_KEY_TYPE, size); + } + + public static SshKeyPair generate(String keyType, int size) { + try { + KeyPair keyPair = KeyUtils.generateKeyPair(keyType, size); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + return new SshKeyPair(publicKey, privateKey); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Cannot generate SSH key", e); + } + } + + public static SshKeyPair load(Reader reader, char[] password) { + try (PEMParser pemParser = new PEMParser(reader)) { + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter();// .setProvider("BC"); + KeyPair kp; + if (object instanceof PKCS8EncryptedPrivateKeyInfo) { + // Encrypted key - we will use provided password + PKCS8EncryptedPrivateKeyInfo ckp = (PKCS8EncryptedPrivateKeyInfo) object; +// PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password); + InputDecryptorProvider inputDecryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder() + .build(password); + PrivateKeyInfo pkInfo = ckp.decryptPrivateKeyInfo(inputDecryptorProvider); + PrivateKey privateKey = converter.getPrivateKey(pkInfo); + + // generate public key + RSAPrivateCrtKey privk = (RSAPrivateCrtKey) privateKey; + RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(privk.getModulus(), + privk.getPublicExponent()); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + kp = new KeyPair(publicKey, privateKey); + } else { + // Unencrypted key - no password needed +// PKCS8EncryptedPrivateKeyInfo ukp = (PKCS8EncryptedPrivateKeyInfo) object; + PEMKeyPair pemKp = (PEMKeyPair) object; + kp = converter.getKeyPair(pemKp); + } + return new SshKeyPair(kp); + } catch (Exception e) { + throw new RuntimeException("Cannot load private key", e); + } + } + + public static void main(String args[]) { + Path privateKeyPath = Paths.get(System.getProperty("user.dir") + "/id_rsa"); + SshKeyPair skp = SshKeyPair.loadOrGenerate(privateKeyPath, 1024, null); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + + StringReader reader = new StringReader(skp.getPrivateKeyAsPemString(null)); + skp = SshKeyPair.load(reader, null); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + + reader = new StringReader(skp.getPrivateKeyAsPemString("demo".toCharArray())); + skp = SshKeyPair.load(reader, "demo".toCharArray()); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + } + +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java new file mode 100644 index 000000000..8c048f76f --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java @@ -0,0 +1,134 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Scanner; + +import org.apache.commons.io.IOUtils; +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.local.LocalAgentFactory; +import org.apache.sshd.agent.unix.UnixAgentFactory; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem; +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider; +import org.argeo.api.cms.CmsLog; + +public class SshSync { + private final static CmsLog log = CmsLog.getLog(SshSync.class); + + public static void main(String[] args) { + + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + boolean osAgent = true; + SshAgentFactory agentFactory = osAgent ? new UnixAgentFactory() : new LocalAgentFactory(); + // SshAgentFactory agentFactory = new LocalAgentFactory(); + client.setAgentFactory(agentFactory); + SshAgent sshAgent = agentFactory.createClient(client); + + String login = System.getProperty("user.name"); + String host = "localhost"; + int port = 22; + + if (!osAgent) { + String keyPath = "/home/" + login + "/.ssh/id_rsa"; + System.out.print(keyPath + ": "); + Scanner s = new Scanner(System.in); + String password = s.next(); +// KeyPair keyPair = ClientIdentityLoader.DEFAULT.loadClientIdentity(keyPath, +// FilePasswordProvider.of(password)); +// sshAgent.addIdentity(keyPair, "NO COMMENT"); + } + +// List> identities = sshAgent.getIdentities(); +// for (Map.Entry entry : identities) { +// System.out.println(entry.getValue() + " : " + entry.getKey()); +// } + + ConnectFuture connectFuture = client.connect(login, host, port); + connectFuture.await(); + ClientSession session = connectFuture.getSession(); + + try { + +// session.addPasswordIdentity(new String(password)); + session.auth().verify(1000l); + + SftpFileSystemProvider fsProvider = new SftpFileSystemProvider(client); + + SftpFileSystem fs = fsProvider.newFileSystem(session); + Path testPath = fs.getPath("/home/" + login + "/tmp"); + Files.list(testPath).forEach(System.out::println); + test(testPath); + + } finally { + client.stop(); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + static void test(Path testBase) { + try { + Path testPath = testBase.resolve("ssh-test.txt"); + Files.createFile(testPath); + log.debug("Created file " + testPath); + Files.delete(testPath); + log.debug("Deleted " + testPath); + String txt = "TEST\nTEST2\n"; + byte[] arr = txt.getBytes(); + Files.write(testPath, arr); + log.debug("Wrote " + testPath); + byte[] read = Files.readAllBytes(testPath); + log.debug("Read " + testPath); + Path testDir = testBase.resolve("testDir"); + log.debug("Resolved " + testDir); + // Copy + Files.createDirectory(testDir); + log.debug("Created directory " + testDir); + Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); + log.debug("Created sub directories " + subsubdir); + Path copiedFile = testDir.resolve("copiedFile.txt"); + log.debug("Resolved " + copiedFile); + Path relativeCopiedFile = testDir.relativize(copiedFile); + log.debug("Relative copied file " + relativeCopiedFile); + try (OutputStream out = Files.newOutputStream(copiedFile); + InputStream in = Files.newInputStream(testPath)) { + IOUtils.copy(in, out); + } + log.debug("Copied " + testPath + " to " + copiedFile); + Files.delete(testPath); + log.debug("Deleted " + testPath); + byte[] copiedRead = Files.readAllBytes(copiedFile); + log.debug("Read " + copiedFile); + // Browse directories + DirectoryStream files = Files.newDirectoryStream(testDir); + int fileCount = 0; + Path listedFile = null; + for (Path file : files) { + fileCount++; + if (!Files.isDirectory(file)) + listedFile = file; + } + log.debug("Listed " + testDir); + // Generic attributes + Map attrs = Files.readAttributes(copiedFile, "*"); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + +} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java new file mode 100644 index 000000000..9555662ad --- /dev/null +++ b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java @@ -0,0 +1,2 @@ +/** SSH support. */ +package org.argeo.cms.ssh; \ No newline at end of file