Reintroduce CMS SSH and rename CMS SQL.
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 25 May 2022 07:51:42 +0000 (09:51 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 25 May 2022 07:51:42 +0000 (09:51 +0200)
23 files changed:
Makefile
org.argeo.cms.pgsql/.classpath [deleted file]
org.argeo.cms.pgsql/.project [deleted file]
org.argeo.cms.pgsql/bnd.bnd [deleted file]
org.argeo.cms.pgsql/build.properties [deleted file]
org.argeo.cms.pgsql/src/org/argeo/cms/pgsql/util/CheckPg.java [deleted file]
org.argeo.cms.sql/.classpath [new file with mode: 0644]
org.argeo.cms.sql/.project [new file with mode: 0644]
org.argeo.cms.sql/bnd.bnd [new file with mode: 0644]
org.argeo.cms.sql/build.properties [new file with mode: 0644]
org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java [new file with mode: 0644]
org.argeo.cms.ssh/.classpath [new file with mode: 0644]
org.argeo.cms.ssh/.project [new file with mode: 0644]
org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms.ssh/bnd.bnd [new file with mode: 0644]
org.argeo.cms.ssh/build.properties [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java [new file with mode: 0644]
org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java [new file with mode: 0644]

index af16906ccffc62dafadb8b94c353b9d423eb83ae..41cbb631549fb99d1778109d12d52451072e39b3 100644 (file)
--- 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 (file)
index e801ebf..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
-       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
-       <classpathentry kind="src" path="src"/>
-       <classpathentry kind="output" path="bin"/>
-</classpath>
diff --git a/org.argeo.cms.pgsql/.project b/org.argeo.cms.pgsql/.project
deleted file mode 100644 (file)
index ff01ad9..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-       <name>org.argeo.cms.pgsql</name>
-       <comment></comment>
-       <projects>
-       </projects>
-       <buildSpec>
-               <buildCommand>
-                       <name>org.eclipse.jdt.core.javabuilder</name>
-                       <arguments>
-                       </arguments>
-               </buildCommand>
-               <buildCommand>
-                       <name>org.eclipse.pde.ManifestBuilder</name>
-                       <arguments>
-                       </arguments>
-               </buildCommand>
-               <buildCommand>
-                       <name>org.eclipse.pde.SchemaBuilder</name>
-                       <arguments>
-                       </arguments>
-               </buildCommand>
-       </buildSpec>
-       <natures>
-               <nature>org.eclipse.pde.PluginNature</nature>
-               <nature>org.eclipse.jdt.core.javanature</nature>
-       </natures>
-</projectDescription>
diff --git a/org.argeo.cms.pgsql/bnd.bnd b/org.argeo.cms.pgsql/bnd.bnd
deleted file mode 100644 (file)
index 9c73009..0000000
+++ /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 (file)
index 34d2e4d..0000000
+++ /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 (file)
index 9db43df..0000000
+++ /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<String> 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<String> 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 (file)
index 0000000..e801ebf
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.sql/.project b/org.argeo.cms.sql/.project
new file mode 100644 (file)
index 0000000..b96a541
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.sql</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.sql/bnd.bnd b/org.argeo.cms.sql/bnd.bnd
new file mode 100644 (file)
index 0000000..9c73009
--- /dev/null
@@ -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 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -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 (file)
index 0000000..bc002a6
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..81fe078
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.ssh/.project b/org.argeo.cms.ssh/.project
new file mode 100644 (file)
index 0000000..6b0c9dd
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.ssh</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
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 (file)
index 0000000..997d664
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/org.argeo.cms.ssh/build.properties b/org.argeo.cms.ssh/build.properties
new file mode 100644 (file)
index 0000000..17273d1
--- /dev/null
@@ -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 (file)
index 0000000..cb53f7c
--- /dev/null
@@ -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<ClientChannelEvent> 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 (file)
index 0000000..3e2f9aa
--- /dev/null
@@ -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 (file)
index 0000000..b272547
--- /dev/null
@@ -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 (file)
index 0000000..6e7b6ef
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..ed1818d
--- /dev/null
@@ -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 (file)
index 0000000..8c048f7
--- /dev/null
@@ -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<? extends Map.Entry<PublicKey, String>> identities = sshAgent.getIdentities();
+//                     for (Map.Entry<PublicKey, String> 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<Path> 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<String, Object> 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 (file)
index 0000000..9555662
--- /dev/null
@@ -0,0 +1,2 @@
+/** SSH support. */
+package org.argeo.cms.ssh;
\ No newline at end of file