--- /dev/null
+<?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>
--- /dev/null
+/bin/
+/target/
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.cms.integration</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>
--- /dev/null
+eclipse.preferences.version=1
+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
--- /dev/null
+/MANIFEST.MF
--- /dev/null
+Import-Package:\
+org.argeo.api,\
+javax.jcr.nodetype,\
+*
\ No newline at end of file
--- /dev/null
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .
+additional.bundles = org.apache.sshd.common,\
+org.apache.sshd.core
--- /dev/null
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.argeo.slc</groupId>
+ <artifactId>argeo-slc-cms</artifactId>
+ <version>2.1.17-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+ <artifactId>org.argeo.cms.integration</artifactId>
+ <name>CMS Integration</name>
+ <dependencies>
+ <dependency>
+ <groupId>org.argeo.commons</groupId>
+ <artifactId>org.argeo.cms</artifactId>
+ <version>${version.argeo-commons}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.commons</groupId>
+ <artifactId>org.argeo.maintenance</artifactId>
+ <version>${version.argeo-commons}</version>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Serialisable wrapper of a {@link Throwable}. */
+public class CmsExceptionsChain {
+ public final static Log log = LogFactory.getLog(CmsExceptionsChain.class);
+
+ private List<SystemException> exceptions = new ArrayList<>();
+
+ public CmsExceptionsChain() {
+ super();
+ }
+
+ public CmsExceptionsChain(Throwable exception) {
+ writeException(exception);
+ if (log.isDebugEnabled())
+ log.error("Exception chain", exception);
+ }
+
+ public String toJsonString(ObjectMapper objectMapper) {
+ try {
+ return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+ }
+ }
+
+ public void writeAsJson(ObjectMapper objectMapper, Writer writer) {
+ try {
+ JsonGenerator jg = objectMapper.writerWithDefaultPrettyPrinter().getFactory().createGenerator(writer);
+ jg.writeObject(this);
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+ }
+ }
+
+ public void writeAsJson(ObjectMapper objectMapper, HttpServletResponse resp) {
+ try {
+ resp.setContentType("application/json");
+ resp.setStatus(500);
+ writeAsJson(objectMapper, resp.getWriter());
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+ }
+ }
+
+ /** recursive */
+ protected void writeException(Throwable exception) {
+ SystemException systemException = new SystemException(exception);
+ exceptions.add(systemException);
+ Throwable cause = exception.getCause();
+ if (cause != null)
+ writeException(cause);
+ }
+
+ public List<SystemException> getExceptions() {
+ return exceptions;
+ }
+
+ public void setExceptions(List<SystemException> exceptions) {
+ this.exceptions = exceptions;
+ }
+
+ /** An exception in the chain. */
+ public static class SystemException {
+ private String type;
+ private String message;
+ private List<String> stackTrace;
+
+ public SystemException() {
+ }
+
+ public SystemException(Throwable exception) {
+ this.type = exception.getClass().getName();
+ this.message = exception.getMessage();
+ this.stackTrace = new ArrayList<>();
+ StackTraceElement[] elems = exception.getStackTrace();
+ for (int i = 0; i < elems.length; i++)
+ stackTrace.add("at " + elems[i].toString());
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public List<String> getStackTrace() {
+ return stackTrace;
+ }
+
+ public void setStackTrace(List<String> stackTrace) {
+ this.stackTrace = stackTrace;
+ }
+
+ @Override
+ public String toString() {
+ return "System exception: " + type + ", " + message + ", " + stackTrace;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return exceptions.toString();
+ }
+
+// public static void main(String[] args) throws Exception {
+// try {
+// try {
+// try {
+// testDeeper();
+// } catch (Exception e) {
+// throw new Exception("Less deep exception", e);
+// }
+// } catch (Exception e) {
+// throw new RuntimeException("Top exception", e);
+// }
+// } catch (Exception e) {
+// CmsExceptionsChain vjeSystemErrors = new CmsExceptionsChain(e);
+// ObjectMapper objectMapper = new ObjectMapper();
+// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(vjeSystemErrors));
+// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(e));
+// e.printStackTrace();
+// }
+// }
+//
+// static void testDeeper() throws Exception {
+// throw new IllegalStateException("Deep exception");
+// }
+
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.NodeConstants;
+import org.argeo.cms.auth.CmsSessionId;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Externally authenticate an http session. */
+public class CmsLoginServlet extends HttpServlet {
+ public final static String PARAM_USERNAME = "username";
+ public final static String PARAM_PASSWORD = "password";
+
+ private static final long serialVersionUID = 2478080654328751539L;
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ doPost(request, response);
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ LoginContext lc = null;
+ String username = request.getParameter(PARAM_USERNAME);
+ String password = request.getParameter(PARAM_PASSWORD);
+ try {
+ lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response) {
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback && username != null)
+ ((NameCallback) callback).setName(username);
+ else if (callback instanceof PasswordCallback && password != null)
+ ((PasswordCallback) callback).setPassword(password.toCharArray());
+ else if (callback instanceof HttpRequestCallback) {
+ ((HttpRequestCallback) callback).setRequest(request);
+ ((HttpRequestCallback) callback).setResponse(response);
+ }
+ }
+ }
+ });
+ lc.login();
+
+ Subject subject = lc.getSubject();
+ CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class));
+ if (cmsSessionId == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+ Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class));
+ Locale locale = extractFrom(subject.getPublicCredentials(Locale.class));
+
+ CmsSessionDescriptor cmsSessionDescriptor = new CmsSessionDescriptor(authorization.getName(),
+ cmsSessionId.getUuid().toString(), authorization.getRoles(), authorization.toString(),
+ locale != null ? locale.toString() : null);
+
+ response.setContentType("application/json");
+ JsonGenerator jg = objectMapper.getFactory().createGenerator(response.getWriter());
+ jg.writeObject(cmsSessionDescriptor);
+
+ String redirectTo = redirectTo(request);
+ if (redirectTo != null)
+ response.sendRedirect(redirectTo);
+ } catch (LoginException e) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+ }
+
+ protected <T> T extractFrom(Set<T> creds) {
+ if (creds.size() > 0)
+ return creds.iterator().next();
+ else
+ return null;
+ }
+
+ /**
+ * To be overridden in order to return a richer {@link CmsSessionDescriptor} to
+ * be serialized.
+ */
+ protected CmsSessionDescriptor enrichJson(CmsSessionDescriptor cmsSessionDescriptor) {
+ return cmsSessionDescriptor;
+ }
+
+ protected String redirectTo(HttpServletRequest request) {
+ return null;
+ }
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.NodeConstants;
+import org.argeo.cms.auth.CmsSessionId;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+
+/** Externally authenticate an http session. */
+public class CmsLogoutServlet extends HttpServlet {
+ private static final long serialVersionUID = 2478080654328751539L;
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ doPost(request, response);
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ LoginContext lc = null;
+ try {
+ lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response) {
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof HttpRequestCallback) {
+ ((HttpRequestCallback) callback).setRequest(request);
+ ((HttpRequestCallback) callback).setResponse(response);
+ }
+ }
+ }
+ });
+ lc.login();
+
+ Subject subject = lc.getSubject();
+ CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class));
+ if (cmsSessionId != null) {// logged in
+ CurrentUser.logoutCmsSession(subject);
+ }
+
+ } catch (LoginException e) {
+ // ignore
+ }
+
+ String redirectTo = redirectTo(request);
+ if (redirectTo != null)
+ response.sendRedirect(redirectTo);
+ }
+
+ protected <T> T extractFrom(Set<T> creds) {
+ if (creds.size() > 0)
+ return creds.iterator().next();
+ else
+ return null;
+ }
+
+ protected String redirectTo(HttpServletRequest request) {
+ return null;
+ }
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import static org.argeo.api.NodeConstants.LOGIN_CONTEXT_USER;
+
+import java.io.IOException;
+import java.security.AccessControlContext;
+import java.security.PrivilegedAction;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.argeo.cms.servlet.ServletAuthUtils;
+import org.osgi.service.http.context.ServletContextHelper;
+
+/** Manages security access to servlets. */
+public class CmsPrivateServletContext extends ServletContextHelper {
+ public final static String LOGIN_PAGE = "argeo.cms.integration.loginPage";
+ public final static String LOGIN_SERVLET = "argeo.cms.integration.loginServlet";
+ private String loginPage;
+ private String loginServlet;
+
+ public void init(Map<String, String> properties) {
+ loginPage = properties.get(LOGIN_PAGE);
+ loginServlet = properties.get(LOGIN_SERVLET);
+ }
+
+ /**
+ * Add the {@link AccessControlContext} as a request attribute, or redirect to
+ * the login page.
+ */
+ @Override
+ public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response) throws IOException {
+ LoginContext lc = null;
+
+ String pathInfo = request.getPathInfo();
+ String servletPath = request.getServletPath();
+ if ((pathInfo != null && (servletPath + pathInfo).equals(loginPage)) || servletPath.contentEquals(loginServlet))
+ return true;
+ try {
+ lc = new LoginContext(LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response));
+ lc.login();
+ } catch (LoginException e) {
+ lc = processUnauthorized(request, response);
+ if (lc == null)
+ return false;
+ }
+ Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+ @Override
+ public Void run() {
+ // TODO also set login context in order to log out ?
+ ServletAuthUtils.configureRequestSecurity(request);
+ return null;
+ }
+
+ });
+
+ return true;
+ }
+
+ @Override
+ public void finishSecurity(HttpServletRequest request, HttpServletResponse response) {
+ ServletAuthUtils.clearRequestSecurity(request);
+ }
+
+ protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) {
+ try {
+ response.sendRedirect(loginPage);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot redirect to login page", e);
+ }
+ return null;
+ }
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.argeo.cms.auth.CmsSession;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** A serializable descriptor of an internal {@link CmsSession}. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CmsSessionDescriptor implements Serializable, Authorization {
+ private static final long serialVersionUID = 8592162323372641462L;
+
+ private String name;
+ private String cmsSessionId;
+ private String displayName;
+ private String locale;
+ private Set<String> roles;
+
+ public CmsSessionDescriptor() {
+ }
+
+ public CmsSessionDescriptor(String name, String cmsSessionId, String[] roles, String displayName, String locale) {
+ this.name = name;
+ this.displayName = displayName;
+ this.cmsSessionId = cmsSessionId;
+ this.locale = locale;
+ this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles)));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getCmsSessionId() {
+ return cmsSessionId;
+ }
+
+ public void setCmsSessionId(String cmsSessionId) {
+ this.cmsSessionId = cmsSessionId;
+ }
+
+ public Boolean isAnonymous() {
+ return name == null;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ public boolean hasRole(String name) {
+ return roles.contains(name);
+ }
+
+ @Override
+ public String[] getRoles() {
+ return roles.toArray(new String[roles.size()]);
+ }
+
+ public void setRoles(String[] roles) {
+ this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles)));
+ }
+
+ @Override
+ public int hashCode() {
+ return cmsSessionId != null ? cmsSessionId.hashCode() : super.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return displayName != null ? displayName : name != null ? name : super.toString();
+ }
+
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.NodeConstants;
+import org.argeo.cms.CmsUserManager;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.argeo.naming.NamingUtils;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Provides access to tokens. */
+public class CmsTokenServlet extends HttpServlet {
+ private static final long serialVersionUID = 302918711430864140L;
+
+ public final static String PARAM_EXPIRY_DATE = "expiryDate";
+ public final static String PARAM_TOKEN = "token";
+
+ private final static int DEFAULT_HOURS = 24;
+
+ private CmsUserManager userManager;
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ LoginContext lc = null;
+ try {
+ lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response) {
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof HttpRequestCallback) {
+ ((HttpRequestCallback) callback).setRequest(request);
+ ((HttpRequestCallback) callback).setResponse(response);
+ }
+ }
+ }
+ });
+ lc.login();
+ } catch (LoginException e) {
+ // ignore
+ }
+
+ try {
+ Subject subject = lc.getSubject();
+ Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class));
+ String token = UUID.randomUUID().toString();
+ String expiryDateStr = request.getParameter(PARAM_EXPIRY_DATE);
+ ZonedDateTime expiryDate;
+ if (expiryDateStr != null) {
+ expiryDate = NamingUtils.ldapDateToZonedDateTime(expiryDateStr);
+ } else {
+ expiryDate = ZonedDateTime.now().plusHours(DEFAULT_HOURS);
+ expiryDateStr = NamingUtils.instantToLdapDate(expiryDate);
+ }
+ userManager.addAuthToken(authorization.getName(), token, expiryDate);
+
+ TokenDescriptor tokenDescriptor = new TokenDescriptor();
+ tokenDescriptor.setUsername(authorization.getName());
+ tokenDescriptor.setToken(token);
+ tokenDescriptor.setExpiryDate(expiryDateStr);
+// tokenDescriptor.setRoles(Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles))));
+
+ response.setContentType("application/json");
+ JsonGenerator jg = objectMapper.getFactory().createGenerator(response.getWriter());
+ jg.writeObject(tokenDescriptor);
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(objectMapper, response);
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ // temporarily wrap POST for ease of testing
+ doPost(req, resp);
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String token = req.getParameter(PARAM_TOKEN);
+ userManager.expireAuthToken(token);
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(objectMapper, resp);
+ }
+ }
+
+ protected <T> T extractFrom(Set<T> creds) {
+ if (creds.size() > 0)
+ return creds.iterator().next();
+ else
+ return null;
+ }
+
+ public void setUserManager(CmsUserManager userManager) {
+ this.userManager = userManager;
+ }
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessControlContext;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.security.auth.Subject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.api.JackrabbitNode;
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.service.http.context.ServletContextHelper;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Access a JCR repository via web services. */
+public class JcrReadServlet extends HttpServlet {
+ private static final long serialVersionUID = 6536175260540484539L;
+ private final static Log log = LogFactory.getLog(JcrReadServlet.class);
+
+ protected final static String ACCEPT_HTTP_HEADER = "Accept";
+ protected final static String CONTENT_DISPOSITION_HTTP_HEADER = "Content-Disposition";
+
+ protected final static String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
+ protected final static String XML_CONTENT_TYPE = "application/xml";
+ protected final static String JSON_CONTENT_TYPE = "application/json";
+
+ private final static String PARAM_VERBOSE = "verbose";
+ private final static String PARAM_DEPTH = "depth";
+
+ protected final static String JCR_NODES = "jcr:nodes";
+ // cf. javax.jcr.Property
+ protected final static String JCR_PATH = "path";
+ protected final static String JCR_NAME = "name";
+
+ protected final static String _JCR = "_jcr";
+ protected final static String JCR_PREFIX = "jcr:";
+ protected final static String REP_PREFIX = "rep:";
+
+ private Repository repository;
+ private Integer maxDepth = 8;
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isTraceEnabled())
+ log.trace("Data service: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ boolean verbose = req.getParameter(PARAM_VERBOSE) != null && !req.getParameter(PARAM_VERBOSE).equals("false");
+ int depth = 1;
+ if (req.getParameter(PARAM_DEPTH) != null) {
+ depth = Integer.parseInt(req.getParameter(PARAM_DEPTH));
+ if (depth > maxDepth)
+ throw new RuntimeException("Depth " + depth + " is higher than maximum " + maxDepth);
+ }
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+ if (!session.itemExists(jcrPath))
+ throw new RuntimeException("JCR node " + jcrPath + " does not exist");
+ Node node = session.getNode(jcrPath);
+
+ List<String> acceptHeader = readAcceptHeader(req);
+ if (!acceptHeader.isEmpty() && node.isNodeType(NodeType.NT_FILE)) {
+ resp.setContentType(OCTET_STREAM_CONTENT_TYPE);
+ resp.addHeader(CONTENT_DISPOSITION_HTTP_HEADER, "attachment; filename='" + node.getName() + "'");
+ IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
+ resp.flushBuffer();
+ } else {
+ if (!acceptHeader.isEmpty() && acceptHeader.get(0).equals(XML_CONTENT_TYPE)) {
+ // TODO Use req.startAsync(); ?
+ resp.setContentType(XML_CONTENT_TYPE);
+ session.exportSystemView(node.getPath(), resp.getOutputStream(), false, depth <= 1);
+ return;
+ }
+ if (!acceptHeader.isEmpty() && !acceptHeader.contains(JSON_CONTENT_TYPE)) {
+ if (log.isTraceEnabled())
+ log.warn("Content type " + acceptHeader + " in Accept header is not supported. Supported: "
+ + JSON_CONTENT_TYPE + " (default), " + XML_CONTENT_TYPE);
+ }
+ resp.setContentType(JSON_CONTENT_TYPE);
+ JsonGenerator jsonGenerator = getObjectMapper().getFactory().createGenerator(resp.getWriter());
+ jsonGenerator.writeStartObject();
+ writeNodeChildren(node, jsonGenerator, depth, verbose);
+ writeNodeProperties(node, jsonGenerator, verbose);
+ jsonGenerator.writeEndObject();
+ jsonGenerator.flush();
+ }
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ protected Session openJcrSession(HttpServletRequest req, HttpServletResponse resp, Repository repository,
+ String workspace) throws RepositoryException {
+ AccessControlContext acc = (AccessControlContext) req.getAttribute(ServletContextHelper.REMOTE_USER);
+ Subject subject = Subject.getSubject(acc);
+ try {
+ return Subject.doAs(subject, new PrivilegedExceptionAction<Session>() {
+
+ @Override
+ public Session run() throws RepositoryException {
+ return repository.login(workspace);
+ }
+
+ });
+ } catch (PrivilegedActionException e) {
+ if (e.getException() instanceof RepositoryException)
+ throw (RepositoryException) e.getException();
+ else
+ throw new RuntimeException(e.getException());
+ }
+// return workspace != null ? repository.login(workspace) : repository.login();
+ }
+
+ protected String getWorkspace(HttpServletRequest req) {
+ String path = req.getPathInfo();
+ try {
+ path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String[] pathTokens = path.split("/");
+ return pathTokens[1];
+ }
+
+ protected String getJcrPath(HttpServletRequest req) {
+ String path = req.getPathInfo();
+ try {
+ path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String[] pathTokens = path.split("/");
+ String domain = pathTokens[1];
+ String jcrPath = path.substring(domain.length() + 1);
+ return jcrPath;
+ }
+
+ protected List<String> readAcceptHeader(HttpServletRequest req) {
+ List<String> lst = new ArrayList<>();
+ String acceptHeader = req.getHeader(ACCEPT_HTTP_HEADER);
+ if (acceptHeader == null)
+ return lst;
+// Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER);
+// while (acceptHeader.hasMoreElements()) {
+ String[] arr = acceptHeader.split("\\.");
+ for (int i = 0; i < arr.length; i++) {
+ String str = arr[i].trim();
+ if (!"".equals(str))
+ lst.add(str);
+ }
+// }
+ return lst;
+ }
+
+ protected void writeNodeProperties(Node node, JsonGenerator jsonGenerator, boolean verbose)
+ throws RepositoryException, IOException {
+ String jcrPath = node.getPath();
+ Map<String, Map<String, Property>> namespaces = new TreeMap<>();
+
+ PropertyIterator pit = node.getProperties();
+ properties: while (pit.hasNext()) {
+ Property property = pit.nextProperty();
+
+ final String propertyName = property.getName();
+ int columnIndex = propertyName.indexOf(':');
+ if (columnIndex > 0) {
+ // mark prefix with a '_' before the name of the object, according to JSON
+ // conventions to indicate a special value
+ String prefix = "_" + propertyName.substring(0, columnIndex);
+ String unqualifiedName = propertyName.substring(columnIndex + 1);
+ if (!namespaces.containsKey(prefix))
+ namespaces.put(prefix, new LinkedHashMap<String, Property>());
+ Map<String, Property> map = namespaces.get(prefix);
+ assert !map.containsKey(unqualifiedName);
+ map.put(unqualifiedName, property);
+ continue properties;
+ }
+
+ if (property.getType() == PropertyType.BINARY) {
+ if (!(node instanceof JackrabbitNode)) {
+ continue properties;// skip
+ }
+ }
+
+ writeProperty(propertyName, property, jsonGenerator);
+ }
+
+ for (String prefix : namespaces.keySet()) {
+ Map<String, Property> map = namespaces.get(prefix);
+ jsonGenerator.writeFieldName(prefix);
+ jsonGenerator.writeStartObject();
+ if (_JCR.equals(prefix)) {
+ jsonGenerator.writeStringField(JCR_NAME, node.getName());
+ jsonGenerator.writeStringField(JCR_PATH, jcrPath);
+ }
+ properties: for (String unqualifiedName : map.keySet()) {
+ Property property = map.get(unqualifiedName);
+ if (property.getType() == PropertyType.BINARY) {
+ if (!(node instanceof JackrabbitNode)) {
+ continue properties;// skip
+ }
+ }
+ writeProperty(unqualifiedName, property, jsonGenerator);
+ }
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ protected void writeProperty(String fieldName, Property property, JsonGenerator jsonGenerator)
+ throws RepositoryException, IOException {
+ if (!property.isMultiple()) {
+ jsonGenerator.writeFieldName(fieldName);
+ writePropertyValue(property.getType(), property.getValue(), jsonGenerator);
+ } else {
+ jsonGenerator.writeFieldName(fieldName);
+ jsonGenerator.writeStartArray();
+ Value[] values = property.getValues();
+ for (Value value : values) {
+ writePropertyValue(property.getType(), value, jsonGenerator);
+ }
+ jsonGenerator.writeEndArray();
+ }
+ }
+
+ protected void writePropertyValue(int type, Value value, JsonGenerator jsonGenerator)
+ throws RepositoryException, IOException {
+ if (type == PropertyType.DOUBLE)
+ jsonGenerator.writeNumber(value.getDouble());
+ else if (type == PropertyType.LONG)
+ jsonGenerator.writeNumber(value.getLong());
+ else if (type == PropertyType.BINARY) {
+ if (value instanceof JackrabbitValue) {
+ String contentIdentity = ((JackrabbitValue) value).getContentIdentity();
+ jsonGenerator.writeString("SHA256:" + contentIdentity);
+ } else {
+ // TODO write Base64 ?
+ jsonGenerator.writeNull();
+ }
+ } else
+ jsonGenerator.writeString(value.getString());
+ }
+
+ protected void writeNodeChildren(Node node, JsonGenerator jsonGenerator, int depth, boolean verbose)
+ throws RepositoryException, IOException {
+ if (!node.hasNodes())
+ return;
+ if (depth <= 0)
+ return;
+ NodeIterator nit;
+
+ nit = node.getNodes();
+ children: while (nit.hasNext()) {
+ Node child = nit.nextNode();
+ if (!verbose && child.getName().startsWith(REP_PREFIX)) {
+ continue children;// skip Jackrabbit auth metadata
+ }
+
+ jsonGenerator.writeFieldName(child.getName());
+ jsonGenerator.writeStartObject();
+ writeNodeChildren(child, jsonGenerator, depth - 1, verbose);
+ writeNodeProperties(child, jsonGenerator, verbose);
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ public void setMaxDepth(Integer maxDepth) {
+ this.maxDepth = maxDepth;
+ }
+
+ protected Repository getRepository() {
+ return repository;
+ }
+
+ protected ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jcr.JcrUtils;
+
+/** Access a JCR repository via web services. */
+public class JcrWriteServlet extends JcrReadServlet {
+ private static final long serialVersionUID = 17272653843085492L;
+ private final static Log log = LogFactory.getLog(JcrWriteServlet.class);
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isDebugEnabled())
+ log.debug("Data service POST: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+
+ if (req.getContentType() != null && req.getContentType().equals(XML_CONTENT_TYPE)) {
+// resp.setContentType(XML_CONTENT_TYPE);
+ session.getWorkspace().importXML(jcrPath, req.getInputStream(),
+ ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
+ return;
+ }
+
+ if (!session.itemExists(jcrPath)) {
+ String parentPath = FilenameUtils.getFullPathNoEndSeparator(jcrPath);
+ String fileName = FilenameUtils.getName(jcrPath);
+ Node folderNode = JcrUtils.mkfolders(session, parentPath);
+ byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+ JcrUtils.copyBytesAsFile(folderNode, fileName, bytes);
+ } else {
+ Node node = session.getNode(jcrPath);
+ if (!node.isNodeType(NodeType.NT_FILE))
+ throw new IllegalArgumentException("Node " + jcrPath + " exists but is not a file");
+ byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+ JcrUtils.copyBytesAsFile(node.getParent(), node.getName(), bytes);
+ }
+ session.save();
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isDebugEnabled())
+ log.debug("Data service DELETE: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+ if (!session.itemExists(jcrPath)) {
+ // ignore
+ return;
+ } else {
+ Node node = session.getNode(jcrPath);
+ node.remove();
+ }
+ session.save();
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** A serializable descriptor of a token. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TokenDescriptor implements Serializable {
+ private static final long serialVersionUID = -6607393871416803324L;
+
+ private String token;
+ private String username;
+ private String expiryDate;
+// private Set<String> roles;
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+// public Set<String> getRoles() {
+// return roles;
+// }
+//
+// public void setRoles(Set<String> roles) {
+// this.roles = roles;
+// }
+
+ public String getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(String expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.integration;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+public class XslTemplate {
+ private Transformer transformer;
+
+ public XslTemplate(InputStream in, String systemId) {
+ this(loadTransformer(in, systemId));
+ }
+
+ public XslTemplate(Transformer transformer) {
+ this.transformer = transformer;
+ }
+
+ private static Transformer loadTransformer(InputStream in, String systemId) {
+ try {
+ TransformerFactory tFactory = TransformerFactory.newInstance();
+ StreamSource stylesource = new StreamSource(in, systemId);
+ return tFactory.newTransformer(stylesource);
+ } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
+ throw new IllegalArgumentException("Cannot initialise stylesheet with systemId " + systemId, e);
+ }
+ }
+
+ public synchronized void apply(Node node, OutputStream out) {
+ // TODO use a pool of Transformer instead of synchronized
+ try (ByteArrayOutputStream xml = new ByteArrayOutputStream()) {
+ node.getSession().exportDocumentView(node.getPath(), xml, true, false);
+ try (ByteArrayInputStream xmlIn = new ByteArrayInputStream(xml.toByteArray())) {
+ Source source = new StreamSource(xmlIn);
+ Result results = new StreamResult(out);
+ transformer.transform(source, results);
+ }
+ } catch (IOException | RepositoryException | TransformerException e) {
+ throw new RuntimeException("Cannot process XSL template on " + node, e);
+ }
+ }
+}
--- /dev/null
+/** Argeo CMS integration (JSON, web services). */
+package org.argeo.cms.integration;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.websocket;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.List;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.servlet.http.HttpSession;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.api.NodeConstants;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.osgi.service.http.context.ServletContextHelper;
+
+/** Customises the initialisation of a new web socket. */
+public class CmsWebSocketConfigurator extends Configurator {
+ public final static String WEBSOCKET_SUBJECT = "org.argeo.cms.websocket.subject";
+
+ private final static Log log = LogFactory.getLog(CmsWebSocketConfigurator.class);
+ final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ @Override
+ public boolean checkOrigin(String originHeaderValue) {
+ return true;
+ }
+
+ @Override
+ public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
+ try {
+ return endpointClass.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot get endpoint instance", e);
+ }
+ }
+
+ @Override
+ public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
+ return requested;
+ }
+
+ @Override
+ public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
+ if ((requested == null) || (requested.size() == 0))
+ return "";
+ if ((supported == null) || (supported.isEmpty()))
+ return "";
+ for (String possible : requested) {
+ if (possible == null)
+ continue;
+ if (supported.contains(possible))
+ return possible;
+ }
+ return "";
+ }
+
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+ HttpSession httpSession = (HttpSession) request.getHttpSession();
+ if (log.isDebugEnabled() && httpSession != null)
+ log.debug("Web socket HTTP session id: " + httpSession.getId());
+
+ if (httpSession == null) {
+ rejectResponse(response, null);
+ }
+ try {
+ LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
+ new HttpRequestCallbackHandler(httpSession));
+ lc.login();
+ if (log.isDebugEnabled())
+ log.debug("Web socket logged-in as " + lc.getSubject());
+ Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+ @Override
+ public Void run() {
+ sec.getUserProperties().put(ServletContextHelper.REMOTE_USER, AccessController.getContext());
+ return null;
+ }
+
+ });
+ } catch (Exception e) {
+ rejectResponse(response, e);
+ }
+ }
+
+ /**
+ * Behaviour when the web socket could not be authenticated. Throws an
+ * {@link IllegalStateException} by default.
+ *
+ * @param e can be null
+ */
+ protected void rejectResponse(HandshakeResponse response, Exception e) {
+ // violent implementation, as suggested in
+ // https://stackoverflow.com/questions/21763829/jsr-356-how-to-abort-a-websocket-connection-during-the-handshake
+// throw new IllegalStateException("Web socket cannot be authenticated");
+ }
+}
+
+//if (!webServerConfig.isEmpty()) {
+//webServerConfig.put("customizer.class", KernelConstants.CMS_JETTY_CUSTOMIZER_CLASS);
+//
+//// TODO centralise with Jetty extender
+//Object webSocketEnabled = webServerConfig.get(InternalHttpConstants.WEBSOCKET_ENABLED);
+//if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) {
+// bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null);
+// webServerConfig.put(InternalHttpConstants.WEBSOCKET_ENABLED, "true");
+//}
+//}
+
--- /dev/null
+/** Argeo CMS websocket integration. */
+package org.argeo.cms.websocket;
\ No newline at end of file
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Simplify atomic backups implementation, especially by managing VFS.
+ */
+public abstract class AbstractAtomicBackup implements AtomicBackup {
+ private String name;
+ private String compression = "bz2";
+
+ protected abstract void writeBackup(FileObject targetFo);
+
+ public AbstractAtomicBackup() {
+ }
+
+ public AbstractAtomicBackup(String name) {
+ this.name = name;
+ }
+
+ public void init() {
+ if (name == null)
+ throw new MaintenanceException("Atomic backup name must be set");
+ }
+
+ public void destroy() {
+
+ }
+
+ @Override
+ public String backup(FileSystemManager fileSystemManager,
+ String backupsBase, BackupContext backupContext,
+ FileSystemOptions opts) {
+ if (name == null)
+ throw new MaintenanceException("Atomic backup name must be set");
+
+ FileObject targetFo = null;
+ try {
+ if (backupsBase.startsWith("sftp:"))
+ SftpFileSystemConfigBuilder.getInstance()
+ .setStrictHostKeyChecking(opts, "no");
+ if (compression == null || compression.equals("none"))
+ targetFo = fileSystemManager.resolveFile(backupsBase + '/'
+ + backupContext.getRelativeFolder() + '/' + name, opts);
+ else if (compression.equals("bz2"))
+ targetFo = fileSystemManager.resolveFile("bz2:" + backupsBase
+ + '/' + backupContext.getRelativeFolder() + '/' + name
+ + ".bz2" + "!" + name, opts);
+ else if (compression.equals("gz"))
+ targetFo = fileSystemManager.resolveFile("gz:" + backupsBase
+ + '/' + backupContext.getRelativeFolder() + '/' + name
+ + ".gz" + "!" + name, opts);
+ else
+ throw new MaintenanceException("Unsupported compression "
+ + compression);
+
+ writeBackup(targetFo);
+
+ return targetFo.toString();
+ } catch (Exception e) {
+ throw new MaintenanceException("Cannot backup " + name + " to "
+ + targetFo, e);
+ } finally {
+ BackupUtils.closeFOQuietly(targetFo);
+ }
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setCompression(String compression) {
+ this.compression = compression;
+ }
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+
+/** Performs the backup of a single component, typically a database dump */
+public interface AtomicBackup {
+ /** Name identifiying this backup */
+ public String getName();
+
+ /**
+ * Retrieves the data of the component in a format that allows to restore
+ * the component
+ *
+ * @param backupContext
+ * the context of this backup
+ * @return the VFS URI of the generated file or directory
+ */
+ public String backup(FileSystemManager fileSystemManager,
+ String backupsBase, BackupContext backupContext,
+ FileSystemOptions opts);
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Transient information of a given backup, centralizing common information such
+ * as timestamp and location.
+ */
+public interface BackupContext {
+ /** Backup date */
+ public Date getTimestamp();
+
+ /** Formatted backup date */
+ public String getTimestampAsString();
+
+ /** System name */
+ public String getSystemName();
+
+ /** Local base */
+ public String getRelativeFolder();
+
+ /** Date format */
+ public DateFormat getDateFormat();
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider;
+import org.apache.commons.vfs2.provider.ftp.FtpFileProvider;
+import org.apache.commons.vfs2.provider.gzip.GzipFileProvider;
+import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider;
+import org.apache.commons.vfs2.provider.ram.RamFileProvider;
+import org.apache.commons.vfs2.provider.sftp.SftpFileProvider;
+import org.apache.commons.vfs2.provider.url.UrlFileProvider;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Programatically configured VFS file system manager which can be declared as a
+ * bean and associated with a life cycle (methods
+ * {@link DefaultFileSystemManager#init()} and
+ * {@link DefaultFileSystemManager#close()}). Supports bz2, file, ram, gzip,
+ * ftp, sftp
+ */
+public class BackupFileSystemManager extends DefaultFileSystemManager {
+
+ public BackupFileSystemManager() {
+ super();
+ try {
+ addProvider("file", new DefaultLocalFileProvider());
+ addProvider("bz2", new Bzip2FileProvider());
+ addProvider("ftp", new FtpFileProvider());
+ addProvider("sftp", new SftpFileProvider());
+ addProvider("gzip", new GzipFileProvider());
+ addProvider("ram", new RamFileProvider());
+ setDefaultProvider(new UrlFileProvider());
+ } catch (FileSystemException e) {
+ throw new MaintenanceException("Cannot configure backup file provider", e);
+ }
+ }
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+
+/** Purges previous backups */
+public interface BackupPurge {
+ /**
+ * Purge the backups identified by these arguments. Although these are the
+ * same fields as a {@link BackupContext} we don't pass it as argument since
+ * we want to use this interface to purge remote backups as well (that is,
+ * with a different base), or outside the scope of a running backup.
+ */
+ public void purge(FileSystemManager fileSystemManager, String base,
+ String name, DateFormat dateFormat, FileSystemOptions opts);
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backup utilities */
+public class BackupUtils {
+ /** Close a file object quietly even if it is null or throws an exception. */
+ public static void closeFOQuietly(FileObject fo) {
+ if (fo != null) {
+ try {
+ fo.close();
+ } catch (Exception e) {
+ // silent
+ }
+ }
+ }
+
+ /** Prevents instantiation */
+ private BackupUtils() {
+ }
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a MySQL database using mysqldump. */
+public class MySqlBackup extends OsCallBackup {
+ private String mysqldumpLocation = "/usr/bin/mysqldump";
+
+ private String dbUser;
+ private String dbPassword;
+ private String dbName;
+
+ public MySqlBackup() {
+ }
+
+ public MySqlBackup(String dbUser, String dbPassword, String dbName) {
+ this.dbUser = dbUser;
+ this.dbPassword = dbPassword;
+ this.dbName = dbName;
+ init();
+ }
+
+ @Override
+ public void init() {
+ if (getName() == null)
+ setName(dbName + ".mysql");
+ super.init();
+ }
+
+ @Override
+ public void writeBackup(FileObject targetFo) {
+ if (getCommand() == null)
+ setCommand(mysqldumpLocation
+ + " --lock-tables --add-locks --add-drop-table"
+ + " -u ${dbUser} --password=${dbPassword} --databases ${dbName}");
+ getVariables().put("dbUser", dbUser);
+ getVariables().put("dbPassword", dbPassword);
+ getVariables().put("dbName", dbName);
+
+ super.writeBackup(targetFo);
+ }
+
+ public void setDbUser(String dbUser) {
+ this.dbUser = dbUser;
+ }
+
+ public void setDbPassword(String dbPassword) {
+ this.dbPassword = dbPassword;
+ }
+
+ public void setDbName(String dbName) {
+ this.dbName = dbName;
+ }
+
+ public void setMysqldumpLocation(String mysqldumpLocation) {
+ this.mysqldumpLocation = mysqldumpLocation;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+import org.argeo.maintenance.MaintenanceException;
+
+/** Backups an OpenLDAP server using slapcat */
+public class OpenLdapBackup extends OsCallBackup {
+ private String slapcatLocation = "/usr/sbin/slapcat";
+ private String slapdConfLocation = "/etc/openldap/slapd.conf";
+ private String baseDn;
+
+ public OpenLdapBackup() {
+ super();
+ }
+
+ public OpenLdapBackup(String baseDn) {
+ super();
+ this.baseDn = baseDn;
+ }
+
+ @Override
+ public void writeBackup(FileObject targetFo) {
+ if (baseDn == null)
+ throw new MaintenanceException("Base DN must be set");
+
+ if (getCommand() == null)
+ setCommand(slapcatLocation
+ + " -f ${slapdConfLocation} -b '${baseDn}'");
+ getVariables().put("slapdConfLocation", slapdConfLocation);
+ getVariables().put("baseDn", baseDn);
+
+ super.writeBackup(targetFo);
+ }
+
+ public void setSlapcatLocation(String slapcatLocation) {
+ this.slapcatLocation = slapcatLocation;
+ }
+
+ public void setSlapdConfLocation(String slapdConfLocation) {
+ this.slapdConfLocation = slapdConfLocation;
+ }
+
+ public void setBaseDn(String baseDn) {
+ this.baseDn = baseDn;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+import org.apache.commons.exec.ExecuteStreamHandler;
+import org.apache.commons.exec.Executor;
+import org.apache.commons.exec.PumpStreamHandler;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileContent;
+import org.apache.commons.vfs2.FileObject;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Runs an OS command and save its standard output as a file. Typically used for
+ * MySQL or OpenLDAP dumps.
+ */
+public class OsCallBackup extends AbstractAtomicBackup {
+ private final static Log log = LogFactory.getLog(OsCallBackup.class);
+
+ private String command;
+ private Map<String, String> variables = new HashMap<String, String>();
+ private Executor executor = new DefaultExecutor();
+
+ private Map<String, String> environment = new HashMap<String, String>();
+
+ /** Name of the sudo user, root if "", not sudo if null */
+ private String sudo = null;
+
+ public OsCallBackup() {
+ }
+
+ public OsCallBackup(String name) {
+ super(name);
+ }
+
+ public OsCallBackup(String name, String command) {
+ super(name);
+ this.command = command;
+ }
+
+ @Override
+ public void writeBackup(FileObject targetFo) {
+ String commandToUse = command;
+
+ // sudo
+ if (sudo != null) {
+ if (sudo.equals(""))
+ commandToUse = "sudo " + commandToUse;
+ else
+ commandToUse = "sudo -u " + sudo + " " + commandToUse;
+ }
+
+ CommandLine commandLine = CommandLine.parse(commandToUse, variables);
+ ByteArrayOutputStream errBos = new ByteArrayOutputStream();
+ if (log.isTraceEnabled())
+ log.trace(commandLine.toString());
+
+ try {
+ // stdout
+ FileContent targetContent = targetFo.getContent();
+ // stderr
+ ExecuteStreamHandler streamHandler = new PumpStreamHandler(targetContent.getOutputStream(), errBos);
+ executor.setStreamHandler(streamHandler);
+ executor.execute(commandLine, environment);
+ } catch (ExecuteException e) {
+ byte[] err = errBos.toByteArray();
+ String errStr = new String(err);
+ throw new MaintenanceException("Process " + commandLine + " failed (" + e.getExitValue() + "): " + errStr, e);
+ } catch (Exception e) {
+ byte[] err = errBos.toByteArray();
+ String errStr = new String(err);
+ throw new MaintenanceException("Process " + commandLine + " failed: " + errStr, e);
+ } finally {
+ IOUtils.closeQuietly(errBos);
+ }
+ }
+
+ public void setCommand(String command) {
+ this.command = command;
+ }
+
+ protected String getCommand() {
+ return command;
+ }
+
+ /**
+ * A reference to the environment variables that will be passed to the
+ * process. Empty by default.
+ */
+ protected Map<String, String> getEnvironment() {
+ return environment;
+ }
+
+ protected Map<String, String> getVariables() {
+ return variables;
+ }
+
+ public void setVariables(Map<String, String> variables) {
+ this.variables = variables;
+ }
+
+ public void setExecutor(Executor executor) {
+ this.executor = executor;
+ }
+
+ public void setSudo(String sudo) {
+ this.sudo = sudo;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a PostgreSQL database using pg_dump. */
+public class PostgreSqlBackup extends OsCallBackup {
+ /**
+ * PostgreSQL password environment variable (see
+ * http://stackoverflow.com/questions
+ * /2893954/how-to-pass-in-password-to-pg-dump)
+ */
+ protected final static String PGPASSWORD = "PGPASSWORD";
+
+ private String pgDumpLocation = "/usr/bin/pg_dump";
+
+ private String dbUser;
+ private String dbPassword;
+ private String dbName;
+
+ public PostgreSqlBackup() {
+ super();
+ }
+
+ public PostgreSqlBackup(String dbUser, String dbPassword, String dbName) {
+ this.dbUser = dbUser;
+ this.dbPassword = dbPassword;
+ this.dbName = dbName;
+ init();
+ }
+
+ @Override
+ public void init() {
+ // disable compression since pg_dump is used with -Fc option
+ setCompression(null);
+
+ if (getName() == null)
+ setName(dbName + ".pgdump");
+ super.init();
+ }
+
+ @Override
+ public void writeBackup(FileObject targetFo) {
+ if (getCommand() == null) {
+ getEnvironment().put(PGPASSWORD, dbPassword);
+ setCommand(pgDumpLocation + " -Fc" + " -U ${dbUser} ${dbName}");
+ }
+ getVariables().put("dbUser", dbUser);
+ getVariables().put("dbPassword", dbPassword);
+ getVariables().put("dbName", dbName);
+
+ super.writeBackup(targetFo);
+ }
+
+ public void setDbUser(String dbUser) {
+ this.dbUser = dbUser;
+ }
+
+ public void setDbPassword(String dbPassword) {
+ this.dbPassword = dbPassword;
+ }
+
+ public void setDbName(String dbName) {
+ this.dbName = dbName;
+ }
+
+ public void setPgDumpLocation(String mysqldumpLocation) {
+ this.pgDumpLocation = mysqldumpLocation;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.commons.vfs2.FileSystemManager;
+
+/** Simple implementation of a backup context */
+public class SimpleBackupContext implements BackupContext {
+ private DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmm");
+ private final Date timestamp;
+ private final String name;
+
+ private final FileSystemManager fileSystemManager;
+
+ public SimpleBackupContext(FileSystemManager fileSystemManager,
+ String backupsBase, String name) {
+ this.name = name;
+ this.timestamp = new Date();
+ this.fileSystemManager = fileSystemManager;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public String getTimestampAsString() {
+ return dateFormat.format(timestamp);
+ }
+
+ public String getSystemName() {
+ return name;
+ }
+
+ public String getRelativeFolder() {
+ return name + '/' + getTimestampAsString();
+ }
+
+ public DateFormat getDateFormat() {
+ return dateFormat;
+ }
+
+ public FileSystemManager getFileSystemManager() {
+ return fileSystemManager;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.Selectors;
+import org.argeo.maintenance.MaintenanceException;
+
+/** Simple backup purge which keeps backups only for a given number of days */
+public class SimpleBackupPurge implements BackupPurge {
+ private final static Log log = LogFactory.getLog(SimpleBackupPurge.class);
+
+ private Integer daysKept = 30;
+
+ @Override
+ public void purge(FileSystemManager fileSystemManager, String base, String name, DateFormat dateFormat,
+ FileSystemOptions opts) {
+ try {
+ ZonedDateTime nowDt = ZonedDateTime.now();
+ FileObject baseFo = fileSystemManager.resolveFile(base + '/' + name, opts);
+
+ SortedMap<ZonedDateTime, FileObject> toDelete = new TreeMap<ZonedDateTime, FileObject>();
+ int backupCount = 0;
+
+ // make sure base dir exists
+ baseFo.createFolder();
+
+ // scan backups and list those which should be deleted
+ for (FileObject backupFo : baseFo.getChildren()) {
+ String backupName = backupFo.getName().getBaseName();
+ Date backupDate = dateFormat.parse(backupName);
+ backupCount++;
+ ZonedDateTime backupDt = ZonedDateTime.ofInstant(backupDate.toInstant(), ZoneId.systemDefault());
+ Period sinceThen = Period.between(backupDt.toLocalDate(), nowDt.toLocalDate());
+ // new Period(backupDt, nowDt);
+ int days = sinceThen.getDays();
+ // int days = sinceThen.getMinutes();
+ if (days > daysKept) {
+ toDelete.put(backupDt, backupFo);
+ }
+ }
+
+ if (toDelete.size() != 0 && toDelete.size() == backupCount) {
+ // all backups would be deleted
+ // but we want to keep at least one
+ ZonedDateTime lastBackupDt = toDelete.firstKey();
+ FileObject keptFo = toDelete.remove(lastBackupDt);
+ log.warn("Backup " + keptFo + " kept although it is older than " + daysKept + " days.");
+ }
+
+ // delete old backups
+ for (FileObject backupFo : toDelete.values()) {
+ backupFo.delete(Selectors.SELECT_ALL);
+ if (log.isDebugEnabled())
+ log.debug("Deleted backup " + backupFo);
+ }
+ } catch (Exception e) {
+ throw new MaintenanceException("Could not purge previous backups", e);
+ }
+
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.io.File;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a Subversion repository using svnadmin. */
+public class SvnBackup extends OsCallBackup {
+ private String svnadminLocation = "/usr/bin/svnadmin";
+
+ private String repoLocation;
+ private String repoName;
+
+ public SvnBackup() {
+ }
+
+ public SvnBackup(String repoLocation) {
+ this.repoLocation = repoLocation;
+ init();
+ }
+
+ @Override
+ public void init() {
+ // use directory as repo name
+ if (repoName == null)
+ repoName = new File(repoLocation).getName();
+
+ if (getName() == null)
+ setName(repoName + ".svndump");
+ super.init();
+ }
+
+ @Override
+ public void writeBackup(FileObject targetFo) {
+ if (getCommand() == null) {
+ setCommand(svnadminLocation + " dump " + " ${repoLocation}");
+ }
+ getVariables().put("repoLocation", repoLocation);
+
+ super.writeBackup(targetFo);
+ }
+
+ public void setRepoLocation(String repoLocation) {
+ this.repoLocation = repoLocation;
+ }
+
+ public void setRepoName(String repoName) {
+ this.repoName = repoName;
+ }
+
+ public void setSvnadminLocation(String mysqldumpLocation) {
+ this.svnadminLocation = mysqldumpLocation;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup.vfs;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.Selectors;
+import org.apache.commons.vfs2.UserAuthenticator;
+import org.apache.commons.vfs2.impl.DefaultFileSystemConfigBuilder;
+import org.argeo.maintenance.MaintenanceException;
+import org.argeo.util.LangUtils;
+
+/**
+ * Combines multiple backups and transfer them to a remote location. Purges
+ * remote and local data based on certain criteria.
+ */
+public class SystemBackup implements Runnable {
+ private final static Log log = LogFactory.getLog(SystemBackup.class);
+
+ private FileSystemManager fileSystemManager;
+ private UserAuthenticator userAuthenticator = null;
+
+ private String backupsBase;
+ private String systemName;
+
+ private List<AtomicBackup> atomicBackups = new ArrayList<AtomicBackup>();
+ private BackupPurge backupPurge = new SimpleBackupPurge();
+
+ private Map<String, UserAuthenticator> remoteBases = new HashMap<String, UserAuthenticator>();
+
+ @Override
+ public void run() {
+ if (atomicBackups.size() == 0)
+ throw new MaintenanceException("No atomic backup listed");
+ List<String> failures = new ArrayList<String>();
+
+ SimpleBackupContext backupContext = new SimpleBackupContext(fileSystemManager, backupsBase, systemName);
+
+ // purge older backups
+ FileSystemOptions opts = new FileSystemOptions();
+ try {
+ DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(opts, userAuthenticator);
+ } catch (FileSystemException e) {
+ throw new MaintenanceException("Cannot create authentication", e);
+ }
+
+ try {
+
+ backupPurge.purge(fileSystemManager, backupsBase, systemName, backupContext.getDateFormat(), opts);
+ } catch (Exception e) {
+ failures.add("Purge " + backupsBase + " failed: " + e.getMessage());
+ log.error("Purge of " + backupsBase + " failed", e);
+ }
+
+ // perform backup
+ for (AtomicBackup atomickBackup : atomicBackups) {
+ try {
+ String target = atomickBackup.backup(fileSystemManager, backupsBase, backupContext, opts);
+ if (log.isDebugEnabled())
+ log.debug("Performed backup " + target);
+ } catch (Exception e) {
+ String msg = "Atomic backup " + atomickBackup.getName() + " failed: "
+ + LangUtils.chainCausesMessages(e);
+ failures.add(msg);
+ log.error(msg);
+ if (log.isTraceEnabled())
+ log.trace("Stacktrace of atomic backup " + atomickBackup.getName() + " failure.", e);
+ }
+ }
+
+ // dispatch to remote
+ for (String remoteBase : remoteBases.keySet()) {
+ FileObject localBaseFo = null;
+ FileObject remoteBaseFo = null;
+ UserAuthenticator auth = remoteBases.get(remoteBase);
+
+ // authentication
+ FileSystemOptions remoteOpts = new FileSystemOptions();
+ try {
+ DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(remoteOpts, auth);
+ backupPurge.purge(fileSystemManager, remoteBase, systemName, backupContext.getDateFormat(), remoteOpts);
+ } catch (Exception e) {
+ failures.add("Purge " + remoteBase + " failed: " + e.getMessage());
+ log.error("Cannot purge " + remoteBase, e);
+ }
+
+ try {
+ localBaseFo = fileSystemManager.resolveFile(backupsBase + '/' + backupContext.getRelativeFolder(),
+ opts);
+ remoteBaseFo = fileSystemManager.resolveFile(remoteBase + '/' + backupContext.getRelativeFolder(),
+ remoteOpts);
+ remoteBaseFo.copyFrom(localBaseFo, Selectors.SELECT_ALL);
+ if (log.isDebugEnabled())
+ log.debug("Copied backup to " + remoteBaseFo + " from " + localBaseFo);
+ // }
+ } catch (Exception e) {
+ failures.add("Dispatch to " + remoteBase + " failed: " + e.getMessage());
+ log.error("Cannot dispatch backups from " + backupContext.getRelativeFolder() + " to " + remoteBase, e);
+ }
+ BackupUtils.closeFOQuietly(localBaseFo);
+ BackupUtils.closeFOQuietly(remoteBaseFo);
+ }
+
+ int failureCount = 0;
+ if (failures.size() > 0) {
+ StringBuffer buf = new StringBuffer();
+ for (String failure : failures) {
+ buf.append('\n').append(failureCount).append(" - ").append(failure);
+ failureCount++;
+ }
+ throw new MaintenanceException(failureCount + " error(s) when running the backup,"
+ + " check the logs and the backups as soon as possible." + buf);
+ }
+ }
+
+ public void setFileSystemManager(FileSystemManager fileSystemManager) {
+ this.fileSystemManager = fileSystemManager;
+ }
+
+ public void setBackupsBase(String backupsBase) {
+ this.backupsBase = backupsBase;
+ }
+
+ public void setSystemName(String name) {
+ this.systemName = name;
+ }
+
+ public void setAtomicBackups(List<AtomicBackup> atomicBackups) {
+ this.atomicBackups = atomicBackups;
+ }
+
+ public void setBackupPurge(BackupPurge backupPurge) {
+ this.backupPurge = backupPurge;
+ }
+
+ public void setUserAuthenticator(UserAuthenticator userAuthenticator) {
+ this.userAuthenticator = userAuthenticator;
+ }
+
+ public void setRemoteBases(Map<String, UserAuthenticator> remoteBases) {
+ this.remoteBases = remoteBases;
+ }
+
+ // public static void main(String args[]) {
+ // while (true) {
+ // try {
+ // StandardFileSystemManager fsm = new StandardFileSystemManager();
+ // fsm.init();
+ //
+ // SystemBackup systemBackup = new SystemBackup();
+ // systemBackup.setSystemName("mySystem");
+ // systemBackup
+ // .setBackupsBase("/home/mbaudier/dev/src/commons/server/runtime/org.argeo.server.core/target");
+ // systemBackup.setFileSystemManager(fsm);
+ //
+ // List<AtomicBackup> atomicBackups = new ArrayList<AtomicBackup>();
+ //
+ // MySqlBackup mySqlBackup = new MySqlBackup("root", "", "test");
+ // atomicBackups.add(mySqlBackup);
+ // PostgreSqlBackup postgreSqlBackup = new PostgreSqlBackup(
+ // "argeo", "argeo", "gis_template");
+ // atomicBackups.add(postgreSqlBackup);
+ // SvnBackup svnBackup = new SvnBackup(
+ // "/home/mbaudier/tmp/testsvnrepo");
+ // atomicBackups.add(svnBackup);
+ //
+ // systemBackup.setAtomicBackups(atomicBackups);
+ //
+ // Map<String, UserAuthenticator> remoteBases = new HashMap<String,
+ // UserAuthenticator>();
+ // StaticUserAuthenticator userAuthenticator = new StaticUserAuthenticator(
+ // null, "demo", "demo");
+ // remoteBases.put("sftp://localhost/home/mbaudier/test",
+ // userAuthenticator);
+ // systemBackup.setRemoteBases(remoteBases);
+ //
+ // systemBackup.run();
+ //
+ // fsm.close();
+ // } catch (FileSystemException e) {
+ // // TODO Auto-generated catch block
+ // e.printStackTrace();
+ // System.exit(1);
+ // }
+ //
+ // // wait
+ // try {
+ // Thread.sleep(120 * 1000);
+ // } catch (InterruptedException e) {
+ // e.printStackTrace();
+ // }
+ // }
+ // }
+}
--- /dev/null
+/** Argeo Node backup utilities based on Apache Commons VFS. */
+package org.argeo.maintenance.backup.vfs;
\ No newline at end of file
--- /dev/null
+package org.argeo.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.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+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;
+
+@SuppressWarnings("restriction")
+abstract class AbstractSsh {
+ private final static Log log = LogFactory.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);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.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.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();
+ sshd.setShellFactory(new ProcessShellFactory(shellCommand));
+ sshd.setCommandFactory(new ScpCommandFactory());
+ 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]");
+ }
+
+}
--- /dev/null
+package org.argeo.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);
+ }
+
+}
--- /dev/null
+package org.argeo.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);
+ }
+}
--- /dev/null
+package org.argeo.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()));
+ }
+
+}
--- /dev/null
+package org.argeo.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.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+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;
+
+public class SshSync {
+ private final static Log log = LogFactory.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();
+ }
+
+ }
+
+}
--- /dev/null
+/** SSH support. */
+package org.argeo.ssh;
\ No newline at end of file
--- /dev/null
+<?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-1.8"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+/bin/
+/target/
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.ext.equinox.jetty</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>
--- /dev/null
+/MANIFEST.MF
--- /dev/null
+Fragment-Host: org.eclipse.equinox.http.jetty
+
+Import-Package: org.eclipse.jetty.websocket.jsr356,\
+org.eclipse.jetty.websocket.api,\
+org.eclipse.jetty.websocket.common,\
+org.osgi.service.http,\
+*
\ No newline at end of file
--- /dev/null
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.argeo.slc</groupId>
+ <artifactId>argeo-slc-cms</artifactId>
+ <version>2.1.17-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+ <artifactId>org.argeo.ext.equinox.jetty</artifactId>
+ <name>Extension of Equinox Jetty Integration</name>
+ <dependencies>
+ <dependency>
+ <groupId>org.argeo.commons</groupId>
+ <artifactId>org.argeo.cms</artifactId>
+ <version>${version.argeo-commons}</version>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
--- /dev/null
+package org.argeo.equinox.jetty;
+
+import java.util.Dictionary;
+
+import javax.servlet.ServletContext;
+import javax.websocket.DeploymentException;
+
+import org.eclipse.equinox.http.jetty.JettyCustomizer;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.websocket.jsr356.server.ServerContainer;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer.Configurator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/** Customises the Jetty HTTP server. */
+public class CmsJettyCustomizer extends JettyCustomizer {
+ private BundleContext bc = FrameworkUtil.getBundle(CmsJettyCustomizer.class).getBundleContext();
+
+ public final static String WEBSOCKET_ENABLED = "websocket.enabled";
+
+ @Override
+ public Object customizeContext(Object context, Dictionary<String, ?> settings) {
+ // WebSocket
+ Object webSocketEnabled = settings.get(WEBSOCKET_ENABLED);
+ if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) {
+ ServletContextHandler servletContextHandler = (ServletContextHandler) context;
+ WebSocketServerContainerInitializer.configure(servletContextHandler, new Configurator() {
+
+ @Override
+ public void accept(ServletContext servletContext, ServerContainer serverContainer)
+ throws DeploymentException {
+ bc.registerService(javax.websocket.server.ServerContainer.class, serverContainer, null);
+ }
+ });
+ }
+ return super.customizeContext(context, settings);
+
+ }
+}
--- /dev/null
+/** Equinox Jetty extensions. */
+package org.argeo.equinox.jetty;
\ No newline at end of file
<version.argeo-tp>2.1.27</version.argeo-tp>
<version.argeo-tp-extras>2.1.13</version.argeo-tp-extras>
<version.argeo-commons>2.1.89-SNAPSHOT</version.argeo-commons>
-
+
<version.slc>2.1.17-SNAPSHOT</version.slc>
<developmentCycle.startDate>2015-02-12</developmentCycle.startDate>
<developmentCycle.slc>2.1</developmentCycle.slc>
<argeo.rpm.stagingRepository>/srv/rpmfactory/argeo-osgi-2/el7</argeo.rpm.stagingRepository>
</properties>
<modules>
+ <!-- Argeo CMS extensions -->
+ <module>org.argeo.cms.integration</module>
+ <module>org.argeo.ext.equinox.jetty</module>
+
+ <!-- SLC framework -->
<module>org.argeo.slc.api</module>
<module>org.argeo.slc.runtime</module>
<module>org.argeo.slc.jcr</module>
<type>pom</type>
</dependency>
+ <!-- CMS extensions -->
+ <dependency>
+ <groupId>org.argeo.slc</groupId>
+ <artifactId>org.argeo.cms.integration</artifactId>
+ <version>2.1.17-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.slc</groupId>
+ <artifactId>org.argeo.ext.equinox.jetty</artifactId>
+ <version>2.1.17-SNAPSHOT</version>
+ </dependency>
+ <!-- Misc -->
+ <dependency>
+ <groupId>org.argeo.tp.apache.commons</groupId>
+ <artifactId>org.apache.commons.vfs</artifactId>
+ </dependency>
+ <!-- Jackson JSON processor -->
+ <dependency>
+ <groupId>org.argeo.tp.jackson</groupId>
+ <artifactId>com.fasterxml.jackson.core.jackson-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jackson</groupId>
+ <artifactId>com.fasterxml.jackson.core.jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jackson</groupId>
+ <artifactId>com.fasterxml.jackson.core.jackson-annotations</artifactId>
+ </dependency>
+ <!-- HTTP2 -->
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.alpn.api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.alpn.client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.alpn.server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.http2.common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.http2.client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.http2.client.http</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.http2.server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.http2.hpack</artifactId>
+ </dependency>
+
+ <!-- Async -->
+ <dependency>
+ <groupId>org.argeo.tp.javax</groupId>
+ <artifactId>javax.websocket</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.servlet</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.javax.websocket</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.jetty</groupId>
+ <artifactId>org.eclipse.jetty.websocket.javax.websocket.server</artifactId>
+ </dependency>
+
+ <!-- SSH -->
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.mina.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.tomcat.jni</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.sftp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.scp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.cli</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.apache</groupId>
+ <artifactId>org.apache.sshd.putty</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.argeo.tp.misc</groupId>
+ <artifactId>net.i2p.crypto.eddsa</artifactId>
+ </dependency>
+
+
+
<dependency>
<groupId>org.argeo.slc</groupId>
<version>${version.argeo-commons}</version>
<type>pom</type>
</dependency>
-
+
<!-- Absolutely minimal SLC Agent -->
<dependency>
<groupId>org.argeo.slc</groupId>