From: Mathieu Baudier Date: Sun, 10 Jul 2022 06:57:09 +0000 (+0200) Subject: Refactor non-SWT projects. X-Git-Tag: v2.3.10~129 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=4e548693acc16f97b74eaaa95d6841054a172b85;p=lgpl%2Fargeo-commons.git Refactor non-SWT projects. --- diff --git a/Makefile b/Makefile index ee0604630..3b432b849 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,12 @@ org.argeo.api.acr \ org.argeo.api.cli \ org.argeo.api.cms \ org.argeo.cms \ -org.argeo.cms.sql \ -org.argeo.cms.ssh \ org.argeo.cms.ux \ -eclipse/org.argeo.ext.equinox.jetty \ -eclipse/org.argeo.cms.servlet \ +org.argeo.cms.ee4j \ +org.argeo.cms.lib.jetty \ +org.argeo.cms.lib.equinox \ +org.argeo.cms.lib.sshd \ +org.argeo.cms.lib.pgsql \ swt/org.argeo.cms.swt \ swt/org.argeo.cms.e4 \ swt/rap/org.argeo.swt.specific.rap \ diff --git a/eclipse/org.argeo.cms.servlet/.classpath b/eclipse/org.argeo.cms.servlet/.classpath deleted file mode 100644 index e801ebfb4..000000000 --- a/eclipse/org.argeo.cms.servlet/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/eclipse/org.argeo.cms.servlet/.project b/eclipse/org.argeo.cms.servlet/.project deleted file mode 100644 index d39f97472..000000000 --- a/eclipse/org.argeo.cms.servlet/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - org.argeo.cms.servlet - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - org.eclipse.pde.ds.core.builder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/eclipse/org.argeo.cms.servlet/OSGI-INF/jettyServiceFactory.xml b/eclipse/org.argeo.cms.servlet/OSGI-INF/jettyServiceFactory.xml deleted file mode 100644 index c2cf1c73b..000000000 --- a/eclipse/org.argeo.cms.servlet/OSGI-INF/jettyServiceFactory.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServlet.xml b/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServlet.xml deleted file mode 100644 index 00fcaff99..000000000 --- a/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServlet.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServletContext.xml b/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServletContext.xml deleted file mode 100644 index 7540a2cdb..000000000 --- a/eclipse/org.argeo.cms.servlet/OSGI-INF/pkgServletContext.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/eclipse/org.argeo.cms.servlet/bnd.bnd b/eclipse/org.argeo.cms.servlet/bnd.bnd deleted file mode 100644 index 7c537ba6b..000000000 --- a/eclipse/org.argeo.cms.servlet/bnd.bnd +++ /dev/null @@ -1,12 +0,0 @@ -Import-Package:\ -org.osgi.service.http;version=0.0.0,\ -org.osgi.service.http.whiteboard;version=0.0.0,\ -org.osgi.framework.namespace;version=0.0.0,\ -org.argeo.cms.osgi,\ -javax.servlet.*;version="[3,5)",\ -* - -Service-Component:\ -OSGI-INF/jettyServiceFactory.xml,\ -OSGI-INF/pkgServletContext.xml,\ -OSGI-INF/pkgServlet.xml diff --git a/eclipse/org.argeo.cms.servlet/build.properties b/eclipse/org.argeo.cms.servlet/build.properties deleted file mode 100644 index ee94f53be..000000000 --- a/eclipse/org.argeo.cms.servlet/build.properties +++ /dev/null @@ -1,5 +0,0 @@ -output.. = bin/ -bin.includes = META-INF/,\ - .,\ - OSGI-INF/jettyServiceFactory.xml -source.. = src/ diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java deleted file mode 100644 index fb289c18e..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java +++ /dev/null @@ -1,154 +0,0 @@ -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.argeo.api.cms.CmsLog; - -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 CmsLog log = CmsLog.getLog(CmsExceptionsChain.class); - - private List 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 getExceptions() { - return exceptions; - } - - public void setExceptions(List exceptions) { - this.exceptions = exceptions; - } - - /** An exception in the chain. */ - public static class SystemException { - private String type; - private String message; - private List 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 getStackTrace() { - return stackTrace; - } - - public void setStackTrace(List 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"); -// } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java deleted file mode 100644 index 29a3137bb..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java +++ /dev/null @@ -1,112 +0,0 @@ -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.cms.CmsAuth; -import org.argeo.api.cms.CmsSessionId; -import org.argeo.cms.auth.RemoteAuthCallback; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.servlet.ServletHttpRequest; -import org.argeo.cms.servlet.ServletHttpResponse; -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 req, HttpServletResponse resp) throws ServletException, IOException { - LoginContext lc = null; - String username = req.getParameter(PARAM_USERNAME); - String password = req.getParameter(PARAM_PASSWORD); - ServletHttpRequest request = new ServletHttpRequest(req); - ServletHttpResponse response = new ServletHttpResponse(resp); - try { - lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(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 RemoteAuthCallback) { - ((RemoteAuthCallback) callback).setRequest(request); - ((RemoteAuthCallback) callback).setResponse(response); - } - } - } - }); - lc.login(); - - Subject subject = lc.getSubject(); - CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class)); - if (cmsSessionId == null) { - resp.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); - - resp.setContentType("application/json"); - JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); - jg.writeObject(cmsSessionDescriptor); - - String redirectTo = redirectTo(req); - if (redirectTo != null) - resp.sendRedirect(redirectTo); - } catch (LoginException e) { - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - } - - protected T extractFrom(Set 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; - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java deleted file mode 100644 index 0628eae36..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java +++ /dev/null @@ -1,79 +0,0 @@ -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.cms.CmsAuth; -import org.argeo.api.cms.CmsSessionId; -import org.argeo.cms.auth.CurrentUser; -import org.argeo.cms.auth.RemoteAuthCallback; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.servlet.ServletHttpRequest; -import org.argeo.cms.servlet.ServletHttpResponse; - -/** 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 { - ServletHttpRequest httpRequest = new ServletHttpRequest(request); - ServletHttpResponse httpResponse = new ServletHttpResponse(response); - LoginContext lc = null; - try { - lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, - new RemoteAuthCallbackHandler(httpRequest, httpResponse) { - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof RemoteAuthCallback) { - ((RemoteAuthCallback) callback).setRequest(httpRequest); - ((RemoteAuthCallback) callback).setResponse(httpResponse); - } - } - } - }); - 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 extractFrom(Set creds) { - if (creds.size() > 0) - return creds.iterator().next(); - else - return null; - } - - protected String redirectTo(HttpServletRequest request) { - return null; - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java deleted file mode 100644 index cec04d230..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.argeo.cms.integration; - -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.api.cms.CmsAuth; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.auth.RemoteAuthUtils; -import org.argeo.cms.servlet.ServletHttpRequest; -import org.argeo.cms.servlet.ServletHttpResponse; -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 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 req, HttpServletResponse resp) throws IOException { - LoginContext lc = null; - ServletHttpRequest request = new ServletHttpRequest(req); - ServletHttpResponse response = new ServletHttpResponse(resp); - - String pathInfo = req.getPathInfo(); - String servletPath = req.getServletPath(); - if ((pathInfo != null && (servletPath + pathInfo).equals(loginPage)) || servletPath.contentEquals(loginServlet)) - return true; - try { - lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response)); - lc.login(); - } catch (LoginException e) { - lc = processUnauthorized(req, resp); - if (lc == null) - return false; - } - Subject.doAs(lc.getSubject(), new PrivilegedAction() { - - @Override - public Void run() { - // TODO also set login context in order to log out ? - RemoteAuthUtils.configureRequestSecurity(request); - return null; - } - - }); - - return true; - } - - @Override - public void finishSecurity(HttpServletRequest req, HttpServletResponse resp) { - RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(req)); - } - - 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; - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java deleted file mode 100644 index 30de616a2..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java +++ /dev/null @@ -1,96 +0,0 @@ -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.api.cms.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 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(); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java deleted file mode 100644 index 983202ad2..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java +++ /dev/null @@ -1,117 +0,0 @@ -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.cms.CmsAuth; -import org.argeo.cms.CmsUserManager; -import org.argeo.cms.auth.RemoteAuthCallback; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.servlet.ServletHttpRequest; -import org.argeo.cms.servlet.ServletHttpResponse; -import org.argeo.util.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 req, HttpServletResponse resp) throws ServletException, IOException { - ServletHttpRequest request = new ServletHttpRequest(req); - ServletHttpResponse response = new ServletHttpResponse(resp); - LoginContext lc = null; - try { - lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) { - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof RemoteAuthCallback) { - ((RemoteAuthCallback) callback).setRequest(request); - ((RemoteAuthCallback) 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 = req.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)))); - - resp.setContentType("application/json"); - JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); - jg.writeObject(tokenDescriptor); - } catch (Exception e) { - new CmsExceptionsChain(e).writeAsJson(objectMapper, resp); - } - } - - @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 extractFrom(Set creds) { - if (creds.size() > 0) - return creds.iterator().next(); - else - return null; - } - - public void setUserManager(CmsUserManager userManager) { - this.userManager = userManager; - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java deleted file mode 100644 index 1541b4f29..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java +++ /dev/null @@ -1,49 +0,0 @@ -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 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 getRoles() { -// return roles; -// } -// -// public void setRoles(Set roles) { -// this.roles = roles; -// } - - public String getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(String expiryDate) { - this.expiryDate = expiryDate; - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java deleted file mode 100644 index 1405737ee..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Argeo CMS integration (JSON, web services). */ -package org.argeo.cms.integration; \ No newline at end of file diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/CmsServletContext.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/CmsServletContext.java deleted file mode 100644 index 9cb48b212..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/CmsServletContext.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.cms.servlet; - -import java.io.IOException; -import java.net.URL; -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.api.cms.CmsAuth; -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.auth.RemoteAuthUtils; -import org.argeo.cms.servlet.internal.HttpUtils; -import org.osgi.framework.Bundle; -import org.osgi.framework.FrameworkUtil; -import org.osgi.service.http.context.ServletContextHelper; - -/** - * Default servlet context degrading to anonymous if the the session is not - * pre-authenticated. - */ -public class CmsServletContext extends ServletContextHelper { - private final static CmsLog log = CmsLog.getLog(CmsServletContext.class); - // use CMS bundle for resources - private Bundle bundle = FrameworkUtil.getBundle(getClass()); - - public void init(Map properties) { - - } - - public void destroy() { - - } - - @Override - public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException { - if (log.isTraceEnabled()) - HttpUtils.logRequestHeaders(log, request); - ClassLoader currentThreadContextClassLoader = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(CmsServletContext.class.getClassLoader()); - LoginContext lc; - try { - lc = CmsAuth.USER.newLoginContext( - new RemoteAuthCallbackHandler(new ServletHttpRequest(request), new ServletHttpResponse(response))); - lc.login(); - } catch (LoginException e) { - lc = processUnauthorized(request, response); - if (log.isTraceEnabled()) - HttpUtils.logResponseHeaders(log, response); - if (lc == null) - return false; - } finally { - Thread.currentThread().setContextClassLoader(currentThreadContextClassLoader); - } - - Subject subject = lc.getSubject(); - // log.debug("SERVLET CONTEXT: "+subject); - Subject.doAs(subject, new PrivilegedAction() { - - @Override - public Void run() { - // TODO also set login context in order to log out ? - RemoteAuthUtils.configureRequestSecurity(new ServletHttpRequest(request)); - return null; - } - - }); - return true; - } - - @Override - public void finishSecurity(HttpServletRequest request, HttpServletResponse response) { - RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(request)); - } - - protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) { - // anonymous - ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(CmsServletContext.class.getClassLoader()); - LoginContext lc = CmsAuth.ANONYMOUS.newLoginContext( - new RemoteAuthCallbackHandler(new ServletHttpRequest(request), new ServletHttpResponse(response))); - lc.login(); - return lc; - } catch (LoginException e1) { - if (log.isDebugEnabled()) - log.error("Cannot log in as anonymous", e1); - return null; - } finally { - Thread.currentThread().setContextClassLoader(currentContextClassLoader); - } - } - - @Override - public URL getResource(String name) { - // TODO make it more robust and versatile - // if used directly it can only load from within this bundle - return bundle.getResource(name); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java deleted file mode 100644 index 3bea0b4de..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.argeo.cms.servlet; - -import javax.security.auth.login.LoginContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.argeo.cms.auth.SpnegoLoginModule; -import org.argeo.cms.servlet.internal.HttpUtils; - -/** Servlet context forcing authentication. */ -public class PrivateWwwAuthServletContext extends CmsServletContext { - // TODO make it configurable - private final String httpAuthRealm = "Argeo"; - private final boolean forceBasic = false; - - @Override - protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) { - askForWwwAuth(request, response); - return null; - } - - protected void askForWwwAuth(HttpServletRequest request, HttpServletResponse response) { - // response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "basic - // realm=\"" + httpAuthRealm + "\""); - if (SpnegoLoginModule.hasAcceptorCredentials() && !forceBasic)// SPNEGO - response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Negotiate"); - else - response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + httpAuthRealm + "\""); - - // response.setDateHeader("Date", System.currentTimeMillis()); - // response.setDateHeader("Expires", System.currentTimeMillis() + (24 * - // 60 * 60 * 1000)); - // response.setHeader("Accept-Ranges", "bytes"); - // response.setHeader("Connection", "Keep-Alive"); - // response.setHeader("Keep-Alive", "timeout=5, max=97"); - // response.setContentType("text/html; charset=UTF-8"); - response.setStatus(401); - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpRequest.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpRequest.java deleted file mode 100644 index 54c880435..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpRequest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.argeo.cms.servlet; - -import java.util.Locale; -import java.util.Objects; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import org.argeo.cms.auth.RemoteAuthRequest; -import org.argeo.cms.auth.RemoteAuthSession; - -public class ServletHttpRequest implements RemoteAuthRequest { - private final HttpServletRequest request; - - public ServletHttpRequest(HttpServletRequest request) { - Objects.requireNonNull(request); - this.request = request; - } - - @Override - public RemoteAuthSession getSession() { - HttpSession httpSession = request.getSession(false); - if (httpSession == null) - return null; - return new ServletHttpSession(httpSession); - } - - @Override - public RemoteAuthSession createSession() { - return new ServletHttpSession(request.getSession(true)); - } - - @Override - public Locale getLocale() { - return request.getLocale(); - } - - @Override - public Object getAttribute(String key) { - return request.getAttribute(key); - } - - @Override - public void setAttribute(String key, Object object) { - request.setAttribute(key, object); - } - - @Override - public String getHeader(String key) { - return request.getHeader(key); - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public int getLocalPort() { - return request.getLocalPort(); - } - - @Override - public int getRemotePort() { - return request.getRemotePort(); - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpResponse.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpResponse.java deleted file mode 100644 index de47365ca..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.argeo.cms.servlet; - -import java.util.Objects; - -import javax.servlet.http.HttpServletResponse; - -import org.argeo.cms.auth.RemoteAuthResponse; - -public class ServletHttpResponse implements RemoteAuthResponse { - private final HttpServletResponse response; - - public ServletHttpResponse(HttpServletResponse response) { - Objects.requireNonNull(response); - this.response = response; - } - - @Override - public void setHeader(String keys, String value) { - response.setHeader(keys, value); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpSession.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpSession.java deleted file mode 100644 index 8d087daa7..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/ServletHttpSession.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.argeo.cms.servlet; - -import org.argeo.cms.auth.RemoteAuthSession; - -public class ServletHttpSession implements RemoteAuthSession { - private javax.servlet.http.HttpSession session; - - public ServletHttpSession(javax.servlet.http.HttpSession session) { - super(); - this.session = session; - } - - @Override - public boolean isValid() { - try {// test http session - session.getCreationTime(); - return true; - } catch (IllegalStateException ise) { - return false; - } - } - - @Override - public String getId() { - return session.getId(); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/HttpUtils.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/HttpUtils.java deleted file mode 100644 index 70f2cc6b0..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/HttpUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.argeo.cms.servlet.internal; - -import java.util.Enumeration; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.argeo.api.cms.CmsLog; - -public class HttpUtils { - public final static String HEADER_AUTHORIZATION = "Authorization"; - public final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; - - static boolean isBrowser(String userAgent) { - return userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox") - || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium") - || userAgent.contains("opera") || userAgent.contains("browser"); - } - - public static void logResponseHeaders(CmsLog log, HttpServletResponse response) { - if (!log.isDebugEnabled()) - return; - for (String headerName : response.getHeaderNames()) { - Object headerValue = response.getHeader(headerName); - log.debug(headerName + ": " + headerValue); - } - } - - public static void logRequestHeaders(CmsLog log, HttpServletRequest request) { - if (!log.isDebugEnabled()) - return; - for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) { - String headerName = headerNames.nextElement(); - Object headerValue = request.getHeader(headerName); - log.debug(headerName + ": " + headerValue); - } - log.debug(request.getRequestURI() + "\n"); - } - - public static void logRequest(CmsLog log, HttpServletRequest request) { - log.debug("contextPath=" + request.getContextPath()); - log.debug("servletPath=" + request.getServletPath()); - log.debug("requestURI=" + request.getRequestURI()); - log.debug("queryString=" + request.getQueryString()); - StringBuilder buf = new StringBuilder(); - // headers - Enumeration en = request.getHeaderNames(); - while (en.hasMoreElements()) { - String header = en.nextElement(); - Enumeration values = request.getHeaders(header); - while (values.hasMoreElements()) - buf.append(" " + header + ": " + values.nextElement()); - buf.append('\n'); - } - - // attributed - Enumeration an = request.getAttributeNames(); - while (an.hasMoreElements()) { - String attr = an.nextElement(); - Object value = request.getAttribute(attr); - buf.append(" " + attr + ": " + value); - buf.append('\n'); - } - log.debug("\n" + buf); - } - - private HttpUtils() { - - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/PkgServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/PkgServlet.java deleted file mode 100644 index c762b67ec..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/PkgServlet.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.argeo.cms.servlet.internal; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.Collection; -import java.util.SortedMap; -import java.util.TreeMap; - -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.argeo.cms.osgi.PublishNamespace; -import org.argeo.osgi.util.FilterRequirement; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.Version; -import org.osgi.framework.VersionRange; -import org.osgi.framework.namespace.PackageNamespace; -import org.osgi.framework.wiring.BundleCapability; -import org.osgi.framework.wiring.BundleWiring; -import org.osgi.framework.wiring.FrameworkWiring; -import org.osgi.resource.Requirement; - -public class PkgServlet extends HttpServlet { - private static final long serialVersionUID = 7660824185145214324L; - - private BundleContext bundleContext = FrameworkUtil.getBundle(PkgServlet.class).getBundleContext(); - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String pathInfo = req.getPathInfo(); - - String pkg, versionStr, file; - String[] parts = pathInfo.split("/"); - // first is always empty - if (parts.length == 4) { - pkg = parts[1]; - versionStr = parts[2]; - file = parts[3]; - } else if (parts.length == 3) { - pkg = parts[1]; - versionStr = null; - file = parts[2]; - } else { - throw new IllegalArgumentException("Unsupported path length " + pathInfo); - } - - FrameworkWiring frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); - String filter; - if (versionStr == null) { - filter = "(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")"; - } else { - if (versionStr.startsWith("[") || versionStr.startsWith("(")) {// range - VersionRange versionRange = new VersionRange(versionStr); - filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")" - + versionRange.toFilterString(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + ")"; - - } else { - Version version = new Version(versionStr); - filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")(" - + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + version + "))"; - } - } - Requirement requirement = new FilterRequirement(PackageNamespace.PACKAGE_NAMESPACE, filter); - Collection packages = frameworkWiring.findProviders(requirement); - if (packages.isEmpty()) { - resp.sendError(404); - return; - } - - // TODO verify that it works with multiple versions - SortedMap sorted = new TreeMap<>(); - for (BundleCapability capability : packages) { - sorted.put(capability.getRevision().getVersion(), capability); - } - - Bundle bundle = sorted.get(sorted.firstKey()).getRevision().getBundle(); - String entryPath = '/' + pkg.replace('.', '/') + '/' + file; - URL internalURL = bundle.getResource(entryPath); - if (internalURL == null) { - resp.sendError(404); - return; - } - - // Resource found, we now check whether it can be published - boolean publish = false; - BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); - capabilities: for (BundleCapability bundleCapability : bundleWiring - .getCapabilities(PublishNamespace.CMS_PUBLISH_NAMESPACE)) { - Object publishedPkg = bundleCapability.getAttributes().get(PublishNamespace.PKG); - if (publishedPkg != null) { - if (publishedPkg.equals("*") || publishedPkg.equals(pkg)) { - Object publishedFile = bundleCapability.getAttributes().get(PublishNamespace.FILE); - if (publishedFile == null) { - publish = true; - break capabilities; - } else { - String[] publishedFiles = publishedFile.toString().split(","); - for (String pattern : publishedFiles) { - if (pattern.startsWith("*.")) { - String ext = pattern.substring(1); - if (file.endsWith(ext)) { - publish = true; - break capabilities; - } - } else { - if (publishedFile.equals(file)) { - publish = true; - break capabilities; - } - } - } - } - } - } - } - - if (!publish) { - resp.sendError(404); - return; - } - - try (InputStream in = internalURL.openStream()) { - IOUtils.copy(in, resp.getOutputStream()); - } - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/RobotServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/RobotServlet.java deleted file mode 100644 index 288ee268c..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/RobotServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.argeo.cms.servlet.internal; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class RobotServlet extends HttpServlet { - private static final long serialVersionUID = 7935661175336419089L; - - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - PrintWriter writer = response.getWriter(); - writer.append("User-agent: *\n"); - writer.append("Disallow:\n"); - response.setHeader("Content-Type", "text/plain"); - writer.flush(); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java deleted file mode 100644 index 97321915a..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.argeo.cms.servlet.internal.jetty; - -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.Map; -import java.util.concurrent.ForkJoinPool; - -import javax.websocket.DeploymentException; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpointConfig; - -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.argeo.api.cms.CmsState; -import org.argeo.cms.CmsDeployProperty; -import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator; -import org.argeo.cms.websocket.javax.server.TestEndpoint; -import org.argeo.util.LangUtils; -import org.eclipse.equinox.http.jetty.JettyConfigurator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; - -public class JettyConfig { - private final static CmsLog log = CmsLog.getLog(JettyConfig.class); - - final static String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer"; - - private CmsState cmsState; - - private final BundleContext bc = FrameworkUtil.getBundle(JettyConfig.class).getBundleContext(); - - // private static final String JETTY_PROPERTY_PREFIX = - // "org.eclipse.equinox.http.jetty."; - - public void start() { - // We need to start asynchronously so that Jetty bundle get started by lazy init - // due to the non-configurable behaviour of its activator - ForkJoinPool.commonPool().execute(() -> { - Dictionary properties = getHttpServerConfig(); - startServer(properties); - }); - - ServiceTracker serverSt = new ServiceTracker( - bc, ServerContainer.class, null) { - - @Override - public ServerContainer addingService(ServiceReference reference) { - ServerContainer serverContainer = super.addingService(reference); - - BundleContext bc = reference.getBundle().getBundleContext(); - ServiceReference srConfigurator = bc - .getServiceReference(ServerEndpointConfig.Configurator.class); - ServerEndpointConfig.Configurator endpointConfigurator = bc.getService(srConfigurator); - ServerEndpointConfig config = ServerEndpointConfig.Builder - .create(TestEndpoint.class, "/ws/test/events/").configurator(endpointConfigurator).build(); - try { - serverContainer.addEndpoint(config); - } catch (DeploymentException e) { - throw new IllegalStateException("Cannot initalise the WebSocket server runtime.", e); - } - return serverContainer; - } - - }; - serverSt.open(); - - // check initialisation -// ServiceTracker httpSt = new ServiceTracker(bc, HttpService.class, null) { -// -// @Override -// public HttpService addingService(ServiceReference sr) { -// Object httpPort = sr.getProperty("http.port"); -// Object httpsPort = sr.getProperty("https.port"); -// log.info(httpPortsMsg(httpPort, httpsPort)); -// close(); -// return super.addingService(sr); -// } -// }; -// httpSt.open(); - } - - public void stop() { - try { - JettyConfigurator.stopServer(CmsConstants.DEFAULT); - } catch (Exception e) { - log.error("Cannot stop default Jetty server.", e); - } - - } - - public void startServer(Dictionary properties) { - // Explicitly configures Jetty so that the default server is not started by the - // activator of the Equinox Jetty bundle. - Map config = LangUtils.dictToStringMap(properties); - if (!config.isEmpty()) { - config.put("customizer.class", CMS_JETTY_CUSTOMIZER_CLASS); - - // TODO centralise with Jetty extender - Object webSocketEnabled = config.get(CmsDeployProperty.WEBSOCKET_ENABLED.getProperty()); - if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { - bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null); - // config.put(WEBSOCKET_ENABLED, "true"); - } - } - - long begin = System.currentTimeMillis(); - int tryCount = 60; - try { - while (tryCount > 0) { - try { - // FIXME deal with multiple ids - JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config)); - - Object httpPort = config.get(JettyHttpConstants.HTTP_PORT); - Object httpsPort = config.get(JettyHttpConstants.HTTPS_PORT); - log.info(httpPortsMsg(httpPort, httpsPort)); - - // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi - // configuration is not cleaned - FrameworkUtil.getBundle(JettyConfigurator.class).start(); - return; - } catch (IllegalStateException e) { - // e.printStackTrace(); - // Jetty may not be ready - try { - Thread.sleep(1000); - } catch (Exception e1) { - // silent - } - tryCount--; - } - } - long duration = System.currentTimeMillis() - begin; - log.error("Gave up with starting Jetty server after " + (duration / 1000) + " s"); - } catch (Exception e) { - log.error("Cannot start default Jetty server with config " + properties, e); - } - - } - - private String httpPortsMsg(Object httpPort, Object httpsPort) { - return (httpPort != null ? "HTTP " + httpPort + " " : " ") + (httpsPort != null ? "HTTPS " + httpsPort : ""); - } - - /** Override the provided config with the framework properties */ - public Dictionary getHttpServerConfig() { - String httpPort = getFrameworkProp(CmsDeployProperty.HTTP_PORT); - String httpsPort = getFrameworkProp(CmsDeployProperty.HTTPS_PORT); - /// TODO make it more generic - String httpHost = getFrameworkProp(CmsDeployProperty.HOST); -// String httpsHost = getFrameworkProp( -// JettyConfig.JETTY_PROPERTY_PREFIX + CmsHttpConstants.HTTPS_HOST); - String webSocketEnabled = getFrameworkProp(CmsDeployProperty.WEBSOCKET_ENABLED); - - final Hashtable props = new Hashtable(); - // try { - if (httpPort != null || httpsPort != null) { - boolean httpEnabled = httpPort != null; - props.put(JettyHttpConstants.HTTP_ENABLED, httpEnabled); - boolean httpsEnabled = httpsPort != null; - props.put(JettyHttpConstants.HTTPS_ENABLED, httpsEnabled); - - if (httpEnabled) { - props.put(JettyHttpConstants.HTTP_PORT, httpPort); - if (httpHost != null) - props.put(JettyHttpConstants.HTTP_HOST, httpHost); - } - - if (httpsEnabled) { - props.put(JettyHttpConstants.HTTPS_PORT, httpsPort); - if (httpHost != null) - props.put(JettyHttpConstants.HTTPS_HOST, httpHost); - - // keystore - props.put(JettyHttpConstants.SSL_KEYSTORETYPE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORETYPE)); - props.put(JettyHttpConstants.SSL_KEYSTORE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORE)); - props.put(JettyHttpConstants.SSL_PASSWORD, getFrameworkProp(CmsDeployProperty.SSL_PASSWORD)); - - // truststore - props.put(JettyHttpConstants.SSL_TRUSTSTORETYPE, - getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORETYPE)); - props.put(JettyHttpConstants.SSL_TRUSTSTORE, getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORE)); - props.put(JettyHttpConstants.SSL_TRUSTSTOREPASSWORD, - getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD)); - - // client certificate authentication - String wantClientAuth = getFrameworkProp(CmsDeployProperty.SSL_WANTCLIENTAUTH); - if (wantClientAuth != null) - props.put(JettyHttpConstants.SSL_WANTCLIENTAUTH, Boolean.parseBoolean(wantClientAuth)); - String needClientAuth = getFrameworkProp(CmsDeployProperty.SSL_NEEDCLIENTAUTH); - if (needClientAuth != null) - props.put(JettyHttpConstants.SSL_NEEDCLIENTAUTH, Boolean.parseBoolean(needClientAuth)); - } - - // web socket - if (webSocketEnabled != null && webSocketEnabled.equals("true")) - props.put(CmsDeployProperty.WEBSOCKET_ENABLED.getProperty(), true); - - props.put(CmsConstants.CN, CmsConstants.DEFAULT); - } - return props; - } - - private String getFrameworkProp(CmsDeployProperty deployProperty) { - return cmsState.getDeployProperty(deployProperty.getProperty()); - } - - public void setCmsState(CmsState cmsState) { - this.cmsState = cmsState; - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java deleted file mode 100644 index 8ceb358dd..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.argeo.cms.servlet.internal.jetty; - -/** Compatible with Jetty. */ -interface JettyHttpConstants { - static final String HTTP_ENABLED = "http.enabled"; - static final String HTTP_PORT = "http.port"; - static final String HTTP_HOST = "http.host"; - static final String HTTPS_ENABLED = "https.enabled"; - static final String HTTPS_HOST = "https.host"; - static final String HTTPS_PORT = "https.port"; - static final String SSL_KEYSTORE = "ssl.keystore"; - static final String SSL_PASSWORD = "ssl.password"; - static final String SSL_KEYPASSWORD = "ssl.keypassword"; - static final String SSL_NEEDCLIENTAUTH = "ssl.needclientauth"; - static final String SSL_WANTCLIENTAUTH = "ssl.wantclientauth"; - static final String SSL_PROTOCOL = "ssl.protocol"; - static final String SSL_ALGORITHM = "ssl.algorithm"; - static final String SSL_KEYSTORETYPE = "ssl.keystoretype"; - - // Argeo - static final String SSL_TRUSTSTORE = "ssl.truststore"; - static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword"; - static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype"; - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java deleted file mode 100644 index 005af743c..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.argeo.cms.servlet.internal.jetty; - -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.Map; - -import javax.websocket.DeploymentException; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpointConfig; - -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator; -import org.argeo.cms.websocket.javax.server.TestEndpoint; -import org.argeo.util.LangUtils; -import org.eclipse.equinox.http.jetty.JettyConfigurator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.ServiceReference; -import org.osgi.service.cm.ConfigurationException; -import org.osgi.service.cm.ManagedServiceFactory; -import org.osgi.util.tracker.ServiceTracker; - -@Deprecated -public class JettyServiceFactory implements ManagedServiceFactory { - private final static CmsLog log = CmsLog.getLog(JettyServiceFactory.class); - - final static String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer"; - // Argeo specific - final static String WEBSOCKET_ENABLED = "websocket.enabled"; - - private final BundleContext bc = FrameworkUtil.getBundle(JettyServiceFactory.class).getBundleContext(); - - public void start() { - ServiceTracker serverSt = new ServiceTracker( - bc, ServerContainer.class, null) { - - @Override - public ServerContainer addingService(ServiceReference reference) { - ServerContainer serverContainer = super.addingService(reference); - - BundleContext bc = reference.getBundle().getBundleContext(); - ServiceReference srConfigurator = bc - .getServiceReference(ServerEndpointConfig.Configurator.class); - ServerEndpointConfig.Configurator endpointConfigurator = bc.getService(srConfigurator); - ServerEndpointConfig config = ServerEndpointConfig.Builder - .create(TestEndpoint.class, "/ws/test/events/").configurator(endpointConfigurator).build(); - try { - serverContainer.addEndpoint(config); - } catch (DeploymentException e) { - throw new IllegalStateException("Cannot initalise the WebSocket server runtime.", e); - } - return serverContainer; - } - - }; - serverSt.open(); - } - - @Override - public String getName() { - return "Jetty Service Factory"; - } - - @Override - public void updated(String pid, Dictionary properties) throws ConfigurationException { - // Explicitly configures Jetty so that the default server is not started by the - // activator of the Equinox Jetty bundle. - Map config = LangUtils.dictToStringMap(properties); - if (!config.isEmpty()) { - config.put("customizer.class", CMS_JETTY_CUSTOMIZER_CLASS); - - // TODO centralise with Jetty extender - Object webSocketEnabled = config.get(WEBSOCKET_ENABLED); - if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { - bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null); - config.put(WEBSOCKET_ENABLED, "true"); - } - } - - int tryCount = 60; - try { - tryGettyJetty: while (tryCount > 0) { - try { - // FIXME deal with multiple ids - JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config)); - // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi - // configuration is not cleaned - FrameworkUtil.getBundle(JettyConfigurator.class).start(); - break tryGettyJetty; - } catch (IllegalStateException e) { - // Jetty may not be ready - try { - Thread.sleep(1000); - } catch (Exception e1) { - // silent - } - tryCount--; - } - } - } catch (Exception e) { - log.error("Cannot start default Jetty server with config " + properties, e); - } - - } - - @Override - public void deleted(String pid) { - } - - public void stop() { - try { - JettyConfigurator.stopServer(CmsConstants.DEFAULT); - } catch (Exception e) { - log.error("Cannot stop default Jetty server.", e); - } - - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java deleted file mode 100644 index 8cc165591..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.argeo.cms.websocket.javax.server; - -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.websocket.Extension; -import javax.websocket.HandshakeResponse; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; -import javax.websocket.server.ServerEndpointConfig.Configurator; - -import org.argeo.api.cms.CmsAuth; -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.auth.RemoteAuthCallbackHandler; -import org.argeo.cms.auth.RemoteAuthSession; -import org.argeo.cms.servlet.ServletHttpSession; -import org.osgi.service.http.context.ServletContextHelper; - -/** - * Disabled until third party issues are solved.. 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 CmsLog log = CmsLog.getLog(CmsWebSocketConfigurator.class); - final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; - - @Override - public boolean checkOrigin(String originHeaderValue) { - return true; - } - - @Override - public T getEndpointInstance(Class endpointClass) throws InstantiationException { - try { - return endpointClass.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new IllegalArgumentException("Cannot get endpoint instance", e); - } - } - - @Override - public List getNegotiatedExtensions(List installed, List requested) { - return requested; - } - - @Override - public String getNegotiatedSubprotocol(List supported, List 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) { - if(true) - return; - - RemoteAuthSession httpSession = new ServletHttpSession( - (javax.servlet.http.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(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(httpSession)); - lc.login(); - if (log.isDebugEnabled()) - log.debug("Web socket logged-in as " + lc.getSubject()); - Subject.doAs(lc.getSubject(), new PrivilegedAction() { - - @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"); - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java deleted file mode 100644 index e01f6f721..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java +++ /dev/null @@ -1,178 +0,0 @@ -package org.argeo.cms.websocket.javax.server; - -import java.io.IOException; -import java.security.AccessControlContext; -import java.util.Hashtable; -import java.util.Map; - -import javax.security.auth.Subject; -import javax.websocket.CloseReason; -import javax.websocket.OnClose; -import javax.websocket.OnError; -import javax.websocket.OnMessage; -import javax.websocket.OnOpen; -import javax.websocket.RemoteEndpoint; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; - -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.integration.CmsExceptionsChain; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventConstants; -import org.osgi.service.event.EventHandler; -import org.osgi.service.http.context.ServletContextHelper; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** Provides WebSocket access. */ -@ServerEndpoint(value = "/ws/test/events/") -public class TestEndpoint implements EventHandler { - private final static CmsLog log = CmsLog.getLog(TestEndpoint.class); - - final static String TOPICS_BASE = "/test"; - final static String INPUT = "input"; - final static String TOPIC = "topic"; - final static String VIEW_UID = "viewUid"; - final static String COMPUTATION_UID = "computationUid"; - final static String MESSAGES = "messages"; - final static String ERRORS = "errors"; - - final static String EXCEPTION = "exception"; - final static String MESSAGE = "message"; - - private BundleContext bc = FrameworkUtil.getBundle(TestEndpoint.class).getBundleContext(); - - private String wsSessionId; - private RemoteEndpoint.Basic remote; - private ServiceRegistration eventHandlerSr; - - // json - private ObjectMapper objectMapper = new ObjectMapper(); - - private WebSocketView view; - - @OnOpen - public void onWebSocketConnect(Session session) { - wsSessionId = session.getId(); - - // 24h timeout - session.setMaxIdleTimeout(1000 * 60 * 60 * 24); - - Map userProperties = session.getUserProperties(); - Subject subject = null; -// AccessControlContext accessControlContext = (AccessControlContext) userProperties -// .get(ServletContextHelper.REMOTE_USER); -// Subject subject = Subject.getSubject(accessControlContext); -// // Deal with authentication failure -// if (subject == null) { -// try { -// CloseReason.CloseCode closeCode = new CloseReason.CloseCode() { -// -// @Override -// public int getCode() { -// return 4001; -// } -// }; -// session.close(new CloseReason(closeCode, "Unauthorized")); -// if (log.isTraceEnabled()) -// log.trace("Unauthorized web socket " + wsSessionId + ". Closing with code " + closeCode.getCode() -// + "."); -// return; -// } catch (IOException e) { -// // silent -// } -// return;// ignore -// } - - if (log.isDebugEnabled()) - log.debug("WS#" + wsSessionId + " open for: " + subject); - remote = session.getBasicRemote(); - view = new WebSocketView(subject); - - // OSGi events - String[] topics = new String[] { TOPICS_BASE + "/*" }; - Hashtable ht = new Hashtable<>(); - ht.put(EventConstants.EVENT_TOPIC, topics); - ht.put(EventConstants.EVENT_FILTER, "(" + VIEW_UID + "=" + view.getUid() + ")"); - eventHandlerSr = bc.registerService(EventHandler.class, this, ht); - - if (log.isDebugEnabled()) - log.debug("New view " + view.getUid() + " opened, via web socket."); - } - - @OnMessage - public void onWebSocketText(Session session, String message) throws JsonMappingException, JsonProcessingException { - try { - if (log.isTraceEnabled()) - log.trace("WS#" + view.getUid() + " received:\n" + message + "\n"); -// JsonNode jsonNode = objectMapper.readTree(message); -// String topic = jsonNode.get(TOPIC).textValue(); - - final String computationUid = null; -// if (MY_TOPIC.equals(topic)) { -// view.checkRole(SPECIFIC_ROLE); -// computationUid= process(); -// } - remote.sendText("ACK"); - } catch (Exception e) { - log.error("Error when receiving web socket message", e); - sendSystemErrorMessage(e); - } - } - - @OnClose - public void onWebSocketClose(CloseReason reason) { - if (eventHandlerSr != null) - eventHandlerSr.unregister(); - if (view != null && log.isDebugEnabled()) - log.debug("WS#" + view.getUid() + " closed: " + reason); - } - - @OnError - public void onWebSocketError(Throwable cause) { - if (view != null) { - log.error("WS#" + view.getUid() + " ERROR", cause); - } else { - if (log.isTraceEnabled()) - log.error("Error in web socket session " + wsSessionId, cause); - } - } - - @Override - public void handleEvent(Event event) { - try { - Object uid = event.getProperty(COMPUTATION_UID); - Exception exception = (Exception) event.getProperty(EXCEPTION); - if (exception != null) { - CmsExceptionsChain systemErrors = new CmsExceptionsChain(exception); - String sent = systemErrors.toJsonString(objectMapper); - remote.sendText(sent); - return; - } - String topic = event.getTopic(); - if (log.isTraceEnabled()) - log.trace("WS#" + view.getUid() + " " + topic + ": notify event " + topic + "#" + uid + ", " + event); - } catch (Exception e) { - log.error("Error when handling event for WebSocket", e); - sendSystemErrorMessage(e); - } - - } - - /** Sends an error message in JSON format. */ - protected void sendSystemErrorMessage(Exception e) { - CmsExceptionsChain systemErrors = new CmsExceptionsChain(e); - try { - if (remote != null) - remote.sendText(systemErrors.toJsonString(objectMapper)); - } catch (Exception e1) { - log.error("Cannot send WebSocket system error messages " + systemErrors, e1); - } - } -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java deleted file mode 100644 index 819837b49..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.argeo.cms.websocket.javax.server; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; - -/** Tests connectivity to the web socket server. */ -public class WebSocketTest { - - public static void main(String[] args) throws Exception { - CompletableFuture received = new CompletableFuture<>(); - WebSocket.Listener listener = new WebSocket.Listener() { - - public CompletionStage onText(WebSocket webSocket, CharSequence message, boolean last) { - System.out.println(message); - CompletionStage res = CompletableFuture.completedStage(message.toString()); - received.complete(true); - return res; - } - }; - - HttpClient client = HttpClient.newHttpClient(); - CompletableFuture ws = client.newWebSocketBuilder() - .buildAsync(URI.create("ws://localhost:7070/ws/test/events/"), listener); - WebSocket webSocket = ws.get(); - webSocket.sendText("TEST", true); - - received.get(10, TimeUnit.SECONDS); - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, ""); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java deleted file mode 100644 index a5da88be9..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.argeo.cms.websocket.javax.server; - -import java.security.Principal; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - -import javax.security.auth.Subject; -import javax.security.auth.x500.X500Principal; - -import org.osgi.service.useradmin.Role; - -/** - * Abstraction of a single Frontend view, that is a web browser page. There can - * be multiple views within one single authenticated HTTP session. - */ -public class WebSocketView { - private final String uid; - private Subject subject; - - public WebSocketView(Subject subject) { - this.uid = UUID.randomUUID().toString(); - this.subject = subject; - } - - public String getUid() { - return uid; - } - - public Set getRoles() { - return roles(subject); - } - - public boolean isInRole(String role) { - return getRoles().contains(role); - } - - public void checkRole(String role) { - checkRole(subject, role); - } - - public final static Set roles(Subject subject) { - Set roles = new HashSet(); - X500Principal principal = subject.getPrincipals(X500Principal.class).iterator().next(); - String username = principal.getName(); - roles.add(username); - for (Principal group : subject.getPrincipals()) { - if (group instanceof Role) - roles.add(group.getName()); - } - return roles; - } - - public static void checkRole(Subject subject, String role) { - Set roles = roles(subject); - if (!roles.contains(role)) - throw new IllegalStateException("User is not in role " + role); - } - -} diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java deleted file mode 100644 index 564c881bc..000000000 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Argeo CMS websocket integration. */ -package org.argeo.cms.websocket.javax.server; \ No newline at end of file diff --git a/eclipse/org.argeo.ext.equinox.jetty/.classpath b/eclipse/org.argeo.ext.equinox.jetty/.classpath deleted file mode 100644 index eca7bdba8..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/eclipse/org.argeo.ext.equinox.jetty/.gitignore b/eclipse/org.argeo.ext.equinox.jetty/.gitignore deleted file mode 100644 index 09e3bc9b2..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/bin/ -/target/ diff --git a/eclipse/org.argeo.ext.equinox.jetty/.project b/eclipse/org.argeo.ext.equinox.jetty/.project deleted file mode 100644 index 0b9700dd6..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - org.argeo.ext.equinox.jetty - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore b/eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore deleted file mode 100644 index 4854a41b9..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/MANIFEST.MF diff --git a/eclipse/org.argeo.ext.equinox.jetty/bnd.bnd b/eclipse/org.argeo.ext.equinox.jetty/bnd.bnd deleted file mode 100644 index 7fca53671..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/bnd.bnd +++ /dev/null @@ -1 +0,0 @@ -Fragment-Host: org.eclipse.equinox.http.jetty diff --git a/eclipse/org.argeo.ext.equinox.jetty/build.properties b/eclipse/org.argeo.ext.equinox.jetty/build.properties deleted file mode 100644 index 34d2e4d2d..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/build.properties +++ /dev/null @@ -1,4 +0,0 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - . diff --git a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java deleted file mode 100644 index ab291b545..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.equinox.jetty; - -import java.util.Dictionary; - -import javax.servlet.ServletContext; -import javax.websocket.DeploymentException; -import javax.websocket.server.ServerContainer; - -import org.eclipse.equinox.http.jetty.JettyCustomizer; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.Configurator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; - -/** Customises the Jetty HTTP server. */ -public class CmsJettyCustomizer extends JettyCustomizer { - static final String SSL_TRUSTSTORE = "ssl.truststore"; - static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword"; - static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype"; - - private BundleContext bc = FrameworkUtil.getBundle(CmsJettyCustomizer.class).getBundleContext(); - - public final static String WEBSOCKET_ENABLED = "argeo.websocket.enabled"; - - @Override - public Object customizeContext(Object context, Dictionary settings) { - // WebSocket - Object webSocketEnabled = settings.get(WEBSOCKET_ENABLED); - if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { - ServletContextHandler servletContextHandler = (ServletContextHandler) context; - JavaxWebSocketServletContainerInitializer.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); - - } - - @Override - public Object customizeHttpsConnector(Object connector, Dictionary settings) { - ServerConnector httpsConnector = (ServerConnector) connector; - if (httpsConnector != null) - for (ConnectionFactory connectionFactory : httpsConnector.getConnectionFactories()) { - if (connectionFactory instanceof SslConnectionFactory) { - SslContextFactory.Server sslConnectionFactory = ((SslConnectionFactory) connectionFactory) - .getSslContextFactory(); - sslConnectionFactory.setTrustStorePath((String) settings.get(SSL_TRUSTSTORE)); - sslConnectionFactory.setTrustStoreType((String) settings.get(SSL_TRUSTSTORETYPE)); - sslConnectionFactory.setTrustStorePassword((String) settings.get(SSL_TRUSTSTOREPASSWORD)); - } - } - return super.customizeHttpsConnector(connector, settings); - } - -} diff --git a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java deleted file mode 100644 index 41c8ce9b0..000000000 --- a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Equinox Jetty extensions. */ -package org.argeo.equinox.jetty; \ No newline at end of file diff --git a/org.argeo.cms.ee4j/.classpath b/org.argeo.cms.ee4j/.classpath new file mode 100644 index 000000000..e801ebfb4 --- /dev/null +++ b/org.argeo.cms.ee4j/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.ee4j/.project b/org.argeo.cms.ee4j/.project new file mode 100644 index 000000000..9140addc8 --- /dev/null +++ b/org.argeo.cms.ee4j/.project @@ -0,0 +1,33 @@ + + + org.argeo.cms.ee4j + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.ee4j/OSGI-INF/pkgServlet.xml b/org.argeo.cms.ee4j/OSGI-INF/pkgServlet.xml new file mode 100644 index 000000000..00fcaff99 --- /dev/null +++ b/org.argeo.cms.ee4j/OSGI-INF/pkgServlet.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml b/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml new file mode 100644 index 000000000..7540a2cdb --- /dev/null +++ b/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.ee4j/bnd.bnd b/org.argeo.cms.ee4j/bnd.bnd new file mode 100644 index 000000000..6fae1ea24 --- /dev/null +++ b/org.argeo.cms.ee4j/bnd.bnd @@ -0,0 +1,11 @@ +Import-Package:\ +org.osgi.service.http;version=0.0.0,\ +org.osgi.service.http.whiteboard;version=0.0.0,\ +org.osgi.framework.namespace;version=0.0.0,\ +org.argeo.cms.osgi,\ +javax.servlet.*;version="[3,5)",\ +* + +Service-Component:\ +OSGI-INF/pkgServletContext.xml,\ +OSGI-INF/pkgServlet.xml diff --git a/org.argeo.cms.ee4j/build.properties b/org.argeo.cms.ee4j/build.properties new file mode 100644 index 000000000..ee94f53be --- /dev/null +++ b/org.argeo.cms.ee4j/build.properties @@ -0,0 +1,5 @@ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/jettyServiceFactory.xml +source.. = src/ diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsExceptionsChain.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsExceptionsChain.java new file mode 100644 index 000000000..fb289c18e --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsExceptionsChain.java @@ -0,0 +1,154 @@ +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.argeo.api.cms.CmsLog; + +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 CmsLog log = CmsLog.getLog(CmsExceptionsChain.class); + + private List 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 getExceptions() { + return exceptions; + } + + public void setExceptions(List exceptions) { + this.exceptions = exceptions; + } + + /** An exception in the chain. */ + public static class SystemException { + private String type; + private String message; + private List 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 getStackTrace() { + return stackTrace; + } + + public void setStackTrace(List 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"); +// } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLoginServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLoginServlet.java new file mode 100644 index 000000000..29a3137bb --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLoginServlet.java @@ -0,0 +1,112 @@ +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.cms.CmsAuth; +import org.argeo.api.cms.CmsSessionId; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +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 req, HttpServletResponse resp) throws ServletException, IOException { + LoginContext lc = null; + String username = req.getParameter(PARAM_USERNAME); + String password = req.getParameter(PARAM_PASSWORD); + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(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 RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(request); + ((RemoteAuthCallback) callback).setResponse(response); + } + } + } + }); + lc.login(); + + Subject subject = lc.getSubject(); + CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class)); + if (cmsSessionId == null) { + resp.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); + + resp.setContentType("application/json"); + JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); + jg.writeObject(cmsSessionDescriptor); + + String redirectTo = redirectTo(req); + if (redirectTo != null) + resp.sendRedirect(redirectTo); + } catch (LoginException e) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + protected T extractFrom(Set 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; + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLogoutServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLogoutServlet.java new file mode 100644 index 000000000..0628eae36 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLogoutServlet.java @@ -0,0 +1,79 @@ +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.cms.CmsAuth; +import org.argeo.api.cms.CmsSessionId; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; + +/** 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 { + ServletHttpRequest httpRequest = new ServletHttpRequest(request); + ServletHttpResponse httpResponse = new ServletHttpResponse(response); + LoginContext lc = null; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, + new RemoteAuthCallbackHandler(httpRequest, httpResponse) { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(httpRequest); + ((RemoteAuthCallback) callback).setResponse(httpResponse); + } + } + } + }); + 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 extractFrom(Set creds) { + if (creds.size() > 0) + return creds.iterator().next(); + else + return null; + } + + protected String redirectTo(HttpServletRequest request) { + return null; + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsPrivateServletContext.java new file mode 100644 index 000000000..cec04d230 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsPrivateServletContext.java @@ -0,0 +1,82 @@ +package org.argeo.cms.integration; + +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.api.cms.CmsAuth; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.auth.RemoteAuthUtils; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +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 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 req, HttpServletResponse resp) throws IOException { + LoginContext lc = null; + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + + String pathInfo = req.getPathInfo(); + String servletPath = req.getServletPath(); + if ((pathInfo != null && (servletPath + pathInfo).equals(loginPage)) || servletPath.contentEquals(loginServlet)) + return true; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response)); + lc.login(); + } catch (LoginException e) { + lc = processUnauthorized(req, resp); + if (lc == null) + return false; + } + Subject.doAs(lc.getSubject(), new PrivilegedAction() { + + @Override + public Void run() { + // TODO also set login context in order to log out ? + RemoteAuthUtils.configureRequestSecurity(request); + return null; + } + + }); + + return true; + } + + @Override + public void finishSecurity(HttpServletRequest req, HttpServletResponse resp) { + RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(req)); + } + + 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; + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsSessionDescriptor.java new file mode 100644 index 000000000..30de616a2 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsSessionDescriptor.java @@ -0,0 +1,96 @@ +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.api.cms.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 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(); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsTokenServlet.java new file mode 100644 index 000000000..983202ad2 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsTokenServlet.java @@ -0,0 +1,117 @@ +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.cms.CmsAuth; +import org.argeo.cms.CmsUserManager; +import org.argeo.cms.auth.RemoteAuthCallback; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.servlet.ServletHttpRequest; +import org.argeo.cms.servlet.ServletHttpResponse; +import org.argeo.util.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 req, HttpServletResponse resp) throws ServletException, IOException { + ServletHttpRequest request = new ServletHttpRequest(req); + ServletHttpResponse response = new ServletHttpResponse(resp); + LoginContext lc = null; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof RemoteAuthCallback) { + ((RemoteAuthCallback) callback).setRequest(request); + ((RemoteAuthCallback) 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 = req.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)))); + + resp.setContentType("application/json"); + JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter()); + jg.writeObject(tokenDescriptor); + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(objectMapper, resp); + } + } + + @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 extractFrom(Set creds) { + if (creds.size() > 0) + return creds.iterator().next(); + else + return null; + } + + public void setUserManager(CmsUserManager userManager) { + this.userManager = userManager; + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/TokenDescriptor.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/TokenDescriptor.java new file mode 100644 index 000000000..1541b4f29 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/TokenDescriptor.java @@ -0,0 +1,49 @@ +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 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 getRoles() { +// return roles; +// } +// +// public void setRoles(Set roles) { +// this.roles = roles; +// } + + public String getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(String expiryDate) { + this.expiryDate = expiryDate; + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/integration/package-info.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/package-info.java new file mode 100644 index 000000000..1405737ee --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS integration (JSON, web services). */ +package org.argeo.cms.integration; \ No newline at end of file diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/CmsServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/CmsServletContext.java new file mode 100644 index 000000000..9cb48b212 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/CmsServletContext.java @@ -0,0 +1,106 @@ +package org.argeo.cms.servlet; + +import java.io.IOException; +import java.net.URL; +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.api.cms.CmsAuth; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.auth.RemoteAuthUtils; +import org.argeo.cms.servlet.internal.HttpUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.http.context.ServletContextHelper; + +/** + * Default servlet context degrading to anonymous if the the session is not + * pre-authenticated. + */ +public class CmsServletContext extends ServletContextHelper { + private final static CmsLog log = CmsLog.getLog(CmsServletContext.class); + // use CMS bundle for resources + private Bundle bundle = FrameworkUtil.getBundle(getClass()); + + public void init(Map properties) { + + } + + public void destroy() { + + } + + @Override + public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException { + if (log.isTraceEnabled()) + HttpUtils.logRequestHeaders(log, request); + ClassLoader currentThreadContextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(CmsServletContext.class.getClassLoader()); + LoginContext lc; + try { + lc = CmsAuth.USER.newLoginContext( + new RemoteAuthCallbackHandler(new ServletHttpRequest(request), new ServletHttpResponse(response))); + lc.login(); + } catch (LoginException e) { + lc = processUnauthorized(request, response); + if (log.isTraceEnabled()) + HttpUtils.logResponseHeaders(log, response); + if (lc == null) + return false; + } finally { + Thread.currentThread().setContextClassLoader(currentThreadContextClassLoader); + } + + Subject subject = lc.getSubject(); + // log.debug("SERVLET CONTEXT: "+subject); + Subject.doAs(subject, new PrivilegedAction() { + + @Override + public Void run() { + // TODO also set login context in order to log out ? + RemoteAuthUtils.configureRequestSecurity(new ServletHttpRequest(request)); + return null; + } + + }); + return true; + } + + @Override + public void finishSecurity(HttpServletRequest request, HttpServletResponse response) { + RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(request)); + } + + protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) { + // anonymous + ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(CmsServletContext.class.getClassLoader()); + LoginContext lc = CmsAuth.ANONYMOUS.newLoginContext( + new RemoteAuthCallbackHandler(new ServletHttpRequest(request), new ServletHttpResponse(response))); + lc.login(); + return lc; + } catch (LoginException e1) { + if (log.isDebugEnabled()) + log.error("Cannot log in as anonymous", e1); + return null; + } finally { + Thread.currentThread().setContextClassLoader(currentContextClassLoader); + } + } + + @Override + public URL getResource(String name) { + // TODO make it more robust and versatile + // if used directly it can only load from within this bundle + return bundle.getResource(name); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java new file mode 100644 index 000000000..3bea0b4de --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java @@ -0,0 +1,39 @@ +package org.argeo.cms.servlet; + +import javax.security.auth.login.LoginContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.cms.auth.SpnegoLoginModule; +import org.argeo.cms.servlet.internal.HttpUtils; + +/** Servlet context forcing authentication. */ +public class PrivateWwwAuthServletContext extends CmsServletContext { + // TODO make it configurable + private final String httpAuthRealm = "Argeo"; + private final boolean forceBasic = false; + + @Override + protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) { + askForWwwAuth(request, response); + return null; + } + + protected void askForWwwAuth(HttpServletRequest request, HttpServletResponse response) { + // response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "basic + // realm=\"" + httpAuthRealm + "\""); + if (SpnegoLoginModule.hasAcceptorCredentials() && !forceBasic)// SPNEGO + response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Negotiate"); + else + response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + httpAuthRealm + "\""); + + // response.setDateHeader("Date", System.currentTimeMillis()); + // response.setDateHeader("Expires", System.currentTimeMillis() + (24 * + // 60 * 60 * 1000)); + // response.setHeader("Accept-Ranges", "bytes"); + // response.setHeader("Connection", "Keep-Alive"); + // response.setHeader("Keep-Alive", "timeout=5, max=97"); + // response.setContentType("text/html; charset=UTF-8"); + response.setStatus(401); + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpRequest.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpRequest.java new file mode 100644 index 000000000..54c880435 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpRequest.java @@ -0,0 +1,67 @@ +package org.argeo.cms.servlet; + +import java.util.Locale; +import java.util.Objects; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.argeo.cms.auth.RemoteAuthRequest; +import org.argeo.cms.auth.RemoteAuthSession; + +public class ServletHttpRequest implements RemoteAuthRequest { + private final HttpServletRequest request; + + public ServletHttpRequest(HttpServletRequest request) { + Objects.requireNonNull(request); + this.request = request; + } + + @Override + public RemoteAuthSession getSession() { + HttpSession httpSession = request.getSession(false); + if (httpSession == null) + return null; + return new ServletHttpSession(httpSession); + } + + @Override + public RemoteAuthSession createSession() { + return new ServletHttpSession(request.getSession(true)); + } + + @Override + public Locale getLocale() { + return request.getLocale(); + } + + @Override + public Object getAttribute(String key) { + return request.getAttribute(key); + } + + @Override + public void setAttribute(String key, Object object) { + request.setAttribute(key, object); + } + + @Override + public String getHeader(String key) { + return request.getHeader(key); + } + + @Override + public String getRemoteAddr() { + return request.getRemoteAddr(); + } + + @Override + public int getLocalPort() { + return request.getLocalPort(); + } + + @Override + public int getRemotePort() { + return request.getRemotePort(); + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpResponse.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpResponse.java new file mode 100644 index 000000000..de47365ca --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpResponse.java @@ -0,0 +1,22 @@ +package org.argeo.cms.servlet; + +import java.util.Objects; + +import javax.servlet.http.HttpServletResponse; + +import org.argeo.cms.auth.RemoteAuthResponse; + +public class ServletHttpResponse implements RemoteAuthResponse { + private final HttpServletResponse response; + + public ServletHttpResponse(HttpServletResponse response) { + Objects.requireNonNull(response); + this.response = response; + } + + @Override + public void setHeader(String keys, String value) { + response.setHeader(keys, value); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpSession.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpSession.java new file mode 100644 index 000000000..8d087daa7 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpSession.java @@ -0,0 +1,28 @@ +package org.argeo.cms.servlet; + +import org.argeo.cms.auth.RemoteAuthSession; + +public class ServletHttpSession implements RemoteAuthSession { + private javax.servlet.http.HttpSession session; + + public ServletHttpSession(javax.servlet.http.HttpSession session) { + super(); + this.session = session; + } + + @Override + public boolean isValid() { + try {// test http session + session.getCreationTime(); + return true; + } catch (IllegalStateException ise) { + return false; + } + } + + @Override + public String getId() { + return session.getId(); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/HttpUtils.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/HttpUtils.java new file mode 100644 index 000000000..70f2cc6b0 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/HttpUtils.java @@ -0,0 +1,70 @@ +package org.argeo.cms.servlet.internal; + +import java.util.Enumeration; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsLog; + +public class HttpUtils { + public final static String HEADER_AUTHORIZATION = "Authorization"; + public final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + + static boolean isBrowser(String userAgent) { + return userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox") + || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium") + || userAgent.contains("opera") || userAgent.contains("browser"); + } + + public static void logResponseHeaders(CmsLog log, HttpServletResponse response) { + if (!log.isDebugEnabled()) + return; + for (String headerName : response.getHeaderNames()) { + Object headerValue = response.getHeader(headerName); + log.debug(headerName + ": " + headerValue); + } + } + + public static void logRequestHeaders(CmsLog log, HttpServletRequest request) { + if (!log.isDebugEnabled()) + return; + for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) { + String headerName = headerNames.nextElement(); + Object headerValue = request.getHeader(headerName); + log.debug(headerName + ": " + headerValue); + } + log.debug(request.getRequestURI() + "\n"); + } + + public static void logRequest(CmsLog log, HttpServletRequest request) { + log.debug("contextPath=" + request.getContextPath()); + log.debug("servletPath=" + request.getServletPath()); + log.debug("requestURI=" + request.getRequestURI()); + log.debug("queryString=" + request.getQueryString()); + StringBuilder buf = new StringBuilder(); + // headers + Enumeration en = request.getHeaderNames(); + while (en.hasMoreElements()) { + String header = en.nextElement(); + Enumeration values = request.getHeaders(header); + while (values.hasMoreElements()) + buf.append(" " + header + ": " + values.nextElement()); + buf.append('\n'); + } + + // attributed + Enumeration an = request.getAttributeNames(); + while (an.hasMoreElements()) { + String attr = an.nextElement(); + Object value = request.getAttribute(attr); + buf.append(" " + attr + ": " + value); + buf.append('\n'); + } + log.debug("\n" + buf); + } + + private HttpUtils() { + + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/PkgServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/PkgServlet.java new file mode 100644 index 000000000..c762b67ec --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/PkgServlet.java @@ -0,0 +1,133 @@ +package org.argeo.cms.servlet.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collection; +import java.util.SortedMap; +import java.util.TreeMap; + +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.argeo.cms.osgi.PublishNamespace; +import org.argeo.osgi.util.FilterRequirement; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.resource.Requirement; + +public class PkgServlet extends HttpServlet { + private static final long serialVersionUID = 7660824185145214324L; + + private BundleContext bundleContext = FrameworkUtil.getBundle(PkgServlet.class).getBundleContext(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String pathInfo = req.getPathInfo(); + + String pkg, versionStr, file; + String[] parts = pathInfo.split("/"); + // first is always empty + if (parts.length == 4) { + pkg = parts[1]; + versionStr = parts[2]; + file = parts[3]; + } else if (parts.length == 3) { + pkg = parts[1]; + versionStr = null; + file = parts[2]; + } else { + throw new IllegalArgumentException("Unsupported path length " + pathInfo); + } + + FrameworkWiring frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); + String filter; + if (versionStr == null) { + filter = "(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")"; + } else { + if (versionStr.startsWith("[") || versionStr.startsWith("(")) {// range + VersionRange versionRange = new VersionRange(versionStr); + filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")" + + versionRange.toFilterString(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + ")"; + + } else { + Version version = new Version(versionStr); + filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")(" + + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + version + "))"; + } + } + Requirement requirement = new FilterRequirement(PackageNamespace.PACKAGE_NAMESPACE, filter); + Collection packages = frameworkWiring.findProviders(requirement); + if (packages.isEmpty()) { + resp.sendError(404); + return; + } + + // TODO verify that it works with multiple versions + SortedMap sorted = new TreeMap<>(); + for (BundleCapability capability : packages) { + sorted.put(capability.getRevision().getVersion(), capability); + } + + Bundle bundle = sorted.get(sorted.firstKey()).getRevision().getBundle(); + String entryPath = '/' + pkg.replace('.', '/') + '/' + file; + URL internalURL = bundle.getResource(entryPath); + if (internalURL == null) { + resp.sendError(404); + return; + } + + // Resource found, we now check whether it can be published + boolean publish = false; + BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); + capabilities: for (BundleCapability bundleCapability : bundleWiring + .getCapabilities(PublishNamespace.CMS_PUBLISH_NAMESPACE)) { + Object publishedPkg = bundleCapability.getAttributes().get(PublishNamespace.PKG); + if (publishedPkg != null) { + if (publishedPkg.equals("*") || publishedPkg.equals(pkg)) { + Object publishedFile = bundleCapability.getAttributes().get(PublishNamespace.FILE); + if (publishedFile == null) { + publish = true; + break capabilities; + } else { + String[] publishedFiles = publishedFile.toString().split(","); + for (String pattern : publishedFiles) { + if (pattern.startsWith("*.")) { + String ext = pattern.substring(1); + if (file.endsWith(ext)) { + publish = true; + break capabilities; + } + } else { + if (publishedFile.equals(file)) { + publish = true; + break capabilities; + } + } + } + } + } + } + } + + if (!publish) { + resp.sendError(404); + return; + } + + try (InputStream in = internalURL.openStream()) { + IOUtils.copy(in, resp.getOutputStream()); + } + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/RobotServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/RobotServlet.java new file mode 100644 index 000000000..288ee268c --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/internal/RobotServlet.java @@ -0,0 +1,24 @@ +package org.argeo.cms.servlet.internal; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class RobotServlet extends HttpServlet { + private static final long serialVersionUID = 7935661175336419089L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + PrintWriter writer = response.getWriter(); + writer.append("User-agent: *\n"); + writer.append("Disallow:\n"); + response.setHeader("Content-Type", "text/plain"); + writer.flush(); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java new file mode 100644 index 000000000..46dabc28e --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java @@ -0,0 +1,109 @@ +package org.argeo.cms.websocket.javax.server; + +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.websocket.Extension; +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import javax.websocket.server.ServerEndpointConfig.Configurator; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.auth.RemoteAuthSession; +import org.argeo.cms.servlet.ServletHttpSession; + +/** + * Disabled until third party issues are solved.. Customises + * the initialisation of a new web socket. + */ +public class CmsWebSocketConfigurator extends Configurator { + public final static String WEBSOCKET_SUBJECT = "org.argeo.cms.websocket.subject"; + public final static String REMOTE_USER = "org.osgi.service.http.authentication.remote.user"; + + private final static CmsLog log = CmsLog.getLog(CmsWebSocketConfigurator.class); + final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + + @Override + public boolean checkOrigin(String originHeaderValue) { + return true; + } + + @Override + public T getEndpointInstance(Class endpointClass) throws InstantiationException { + try { + return endpointClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot get endpoint instance", e); + } + } + + @Override + public List getNegotiatedExtensions(List installed, List requested) { + return requested; + } + + @Override + public String getNegotiatedSubprotocol(List supported, List 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) { + if (true) + return; + + RemoteAuthSession httpSession = new ServletHttpSession( + (javax.servlet.http.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(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(httpSession)); + lc.login(); + if (log.isDebugEnabled()) + log.debug("Web socket logged-in as " + lc.getSubject()); + Subject.doAs(lc.getSubject(), new PrivilegedAction() { + + @Override + public Void run() { + sec.getUserProperties().put(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"); + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java new file mode 100644 index 000000000..e01f6f721 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java @@ -0,0 +1,178 @@ +package org.argeo.cms.websocket.javax.server; + +import java.io.IOException; +import java.security.AccessControlContext; +import java.util.Hashtable; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.integration.CmsExceptionsChain; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; +import org.osgi.service.http.context.ServletContextHelper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Provides WebSocket access. */ +@ServerEndpoint(value = "/ws/test/events/") +public class TestEndpoint implements EventHandler { + private final static CmsLog log = CmsLog.getLog(TestEndpoint.class); + + final static String TOPICS_BASE = "/test"; + final static String INPUT = "input"; + final static String TOPIC = "topic"; + final static String VIEW_UID = "viewUid"; + final static String COMPUTATION_UID = "computationUid"; + final static String MESSAGES = "messages"; + final static String ERRORS = "errors"; + + final static String EXCEPTION = "exception"; + final static String MESSAGE = "message"; + + private BundleContext bc = FrameworkUtil.getBundle(TestEndpoint.class).getBundleContext(); + + private String wsSessionId; + private RemoteEndpoint.Basic remote; + private ServiceRegistration eventHandlerSr; + + // json + private ObjectMapper objectMapper = new ObjectMapper(); + + private WebSocketView view; + + @OnOpen + public void onWebSocketConnect(Session session) { + wsSessionId = session.getId(); + + // 24h timeout + session.setMaxIdleTimeout(1000 * 60 * 60 * 24); + + Map userProperties = session.getUserProperties(); + Subject subject = null; +// AccessControlContext accessControlContext = (AccessControlContext) userProperties +// .get(ServletContextHelper.REMOTE_USER); +// Subject subject = Subject.getSubject(accessControlContext); +// // Deal with authentication failure +// if (subject == null) { +// try { +// CloseReason.CloseCode closeCode = new CloseReason.CloseCode() { +// +// @Override +// public int getCode() { +// return 4001; +// } +// }; +// session.close(new CloseReason(closeCode, "Unauthorized")); +// if (log.isTraceEnabled()) +// log.trace("Unauthorized web socket " + wsSessionId + ". Closing with code " + closeCode.getCode() +// + "."); +// return; +// } catch (IOException e) { +// // silent +// } +// return;// ignore +// } + + if (log.isDebugEnabled()) + log.debug("WS#" + wsSessionId + " open for: " + subject); + remote = session.getBasicRemote(); + view = new WebSocketView(subject); + + // OSGi events + String[] topics = new String[] { TOPICS_BASE + "/*" }; + Hashtable ht = new Hashtable<>(); + ht.put(EventConstants.EVENT_TOPIC, topics); + ht.put(EventConstants.EVENT_FILTER, "(" + VIEW_UID + "=" + view.getUid() + ")"); + eventHandlerSr = bc.registerService(EventHandler.class, this, ht); + + if (log.isDebugEnabled()) + log.debug("New view " + view.getUid() + " opened, via web socket."); + } + + @OnMessage + public void onWebSocketText(Session session, String message) throws JsonMappingException, JsonProcessingException { + try { + if (log.isTraceEnabled()) + log.trace("WS#" + view.getUid() + " received:\n" + message + "\n"); +// JsonNode jsonNode = objectMapper.readTree(message); +// String topic = jsonNode.get(TOPIC).textValue(); + + final String computationUid = null; +// if (MY_TOPIC.equals(topic)) { +// view.checkRole(SPECIFIC_ROLE); +// computationUid= process(); +// } + remote.sendText("ACK"); + } catch (Exception e) { + log.error("Error when receiving web socket message", e); + sendSystemErrorMessage(e); + } + } + + @OnClose + public void onWebSocketClose(CloseReason reason) { + if (eventHandlerSr != null) + eventHandlerSr.unregister(); + if (view != null && log.isDebugEnabled()) + log.debug("WS#" + view.getUid() + " closed: " + reason); + } + + @OnError + public void onWebSocketError(Throwable cause) { + if (view != null) { + log.error("WS#" + view.getUid() + " ERROR", cause); + } else { + if (log.isTraceEnabled()) + log.error("Error in web socket session " + wsSessionId, cause); + } + } + + @Override + public void handleEvent(Event event) { + try { + Object uid = event.getProperty(COMPUTATION_UID); + Exception exception = (Exception) event.getProperty(EXCEPTION); + if (exception != null) { + CmsExceptionsChain systemErrors = new CmsExceptionsChain(exception); + String sent = systemErrors.toJsonString(objectMapper); + remote.sendText(sent); + return; + } + String topic = event.getTopic(); + if (log.isTraceEnabled()) + log.trace("WS#" + view.getUid() + " " + topic + ": notify event " + topic + "#" + uid + ", " + event); + } catch (Exception e) { + log.error("Error when handling event for WebSocket", e); + sendSystemErrorMessage(e); + } + + } + + /** Sends an error message in JSON format. */ + protected void sendSystemErrorMessage(Exception e) { + CmsExceptionsChain systemErrors = new CmsExceptionsChain(e); + try { + if (remote != null) + remote.sendText(systemErrors.toJsonString(objectMapper)); + } catch (Exception e1) { + log.error("Cannot send WebSocket system error messages " + systemErrors, e1); + } + } +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java new file mode 100644 index 000000000..819837b49 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java @@ -0,0 +1,35 @@ +package org.argeo.cms.websocket.javax.server; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +/** Tests connectivity to the web socket server. */ +public class WebSocketTest { + + public static void main(String[] args) throws Exception { + CompletableFuture received = new CompletableFuture<>(); + WebSocket.Listener listener = new WebSocket.Listener() { + + public CompletionStage onText(WebSocket webSocket, CharSequence message, boolean last) { + System.out.println(message); + CompletionStage res = CompletableFuture.completedStage(message.toString()); + received.complete(true); + return res; + } + }; + + HttpClient client = HttpClient.newHttpClient(); + CompletableFuture ws = client.newWebSocketBuilder() + .buildAsync(URI.create("ws://localhost:7070/ws/test/events/"), listener); + WebSocket webSocket = ws.get(); + webSocket.sendText("TEST", true); + + received.get(10, TimeUnit.SECONDS); + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, ""); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketView.java b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketView.java new file mode 100644 index 000000000..a5da88be9 --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/WebSocketView.java @@ -0,0 +1,60 @@ +package org.argeo.cms.websocket.javax.server; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import org.osgi.service.useradmin.Role; + +/** + * Abstraction of a single Frontend view, that is a web browser page. There can + * be multiple views within one single authenticated HTTP session. + */ +public class WebSocketView { + private final String uid; + private Subject subject; + + public WebSocketView(Subject subject) { + this.uid = UUID.randomUUID().toString(); + this.subject = subject; + } + + public String getUid() { + return uid; + } + + public Set getRoles() { + return roles(subject); + } + + public boolean isInRole(String role) { + return getRoles().contains(role); + } + + public void checkRole(String role) { + checkRole(subject, role); + } + + public final static Set roles(Subject subject) { + Set roles = new HashSet(); + X500Principal principal = subject.getPrincipals(X500Principal.class).iterator().next(); + String username = principal.getName(); + roles.add(username); + for (Principal group : subject.getPrincipals()) { + if (group instanceof Role) + roles.add(group.getName()); + } + return roles; + } + + public static void checkRole(Subject subject, String role) { + Set roles = roles(subject); + if (!roles.contains(role)) + throw new IllegalStateException("User is not in role " + role); + } + +} diff --git a/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/package-info.java b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/package-info.java new file mode 100644 index 000000000..564c881bc --- /dev/null +++ b/org.argeo.cms.ee4j/src/org/argeo/cms/websocket/javax/server/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS websocket integration. */ +package org.argeo.cms.websocket.javax.server; \ No newline at end of file diff --git a/org.argeo.cms.lib.equinox/.classpath b/org.argeo.cms.lib.equinox/.classpath new file mode 100644 index 000000000..eca7bdba8 --- /dev/null +++ b/org.argeo.cms.lib.equinox/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.lib.equinox/.gitignore b/org.argeo.cms.lib.equinox/.gitignore new file mode 100644 index 000000000..09e3bc9b2 --- /dev/null +++ b/org.argeo.cms.lib.equinox/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/target/ diff --git a/org.argeo.cms.lib.equinox/.project b/org.argeo.cms.lib.equinox/.project new file mode 100644 index 000000000..3be5eb6c2 --- /dev/null +++ b/org.argeo.cms.lib.equinox/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.lib.equinox + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.lib.equinox/META-INF/.gitignore b/org.argeo.cms.lib.equinox/META-INF/.gitignore new file mode 100644 index 000000000..4854a41b9 --- /dev/null +++ b/org.argeo.cms.lib.equinox/META-INF/.gitignore @@ -0,0 +1 @@ +/MANIFEST.MF diff --git a/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml b/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml new file mode 100644 index 000000000..c2cf1c73b --- /dev/null +++ b/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.argeo.cms.lib.equinox/bnd.bnd b/org.argeo.cms.lib.equinox/bnd.bnd new file mode 100644 index 000000000..3d836dfb6 --- /dev/null +++ b/org.argeo.cms.lib.equinox/bnd.bnd @@ -0,0 +1,4 @@ +Fragment-Host: org.eclipse.equinox.http.jetty + +Service-Component: \ +OSGI-INF/jettyServiceFactory.xml,\ diff --git a/org.argeo.cms.lib.equinox/build.properties b/org.argeo.cms.lib.equinox/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms.lib.equinox/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java new file mode 100644 index 000000000..97321915a --- /dev/null +++ b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java @@ -0,0 +1,214 @@ +package org.argeo.cms.servlet.internal.jetty; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ForkJoinPool; + +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.CmsState; +import org.argeo.cms.CmsDeployProperty; +import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator; +import org.argeo.cms.websocket.javax.server.TestEndpoint; +import org.argeo.util.LangUtils; +import org.eclipse.equinox.http.jetty.JettyConfigurator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class JettyConfig { + private final static CmsLog log = CmsLog.getLog(JettyConfig.class); + + final static String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer"; + + private CmsState cmsState; + + private final BundleContext bc = FrameworkUtil.getBundle(JettyConfig.class).getBundleContext(); + + // private static final String JETTY_PROPERTY_PREFIX = + // "org.eclipse.equinox.http.jetty."; + + public void start() { + // We need to start asynchronously so that Jetty bundle get started by lazy init + // due to the non-configurable behaviour of its activator + ForkJoinPool.commonPool().execute(() -> { + Dictionary properties = getHttpServerConfig(); + startServer(properties); + }); + + ServiceTracker serverSt = new ServiceTracker( + bc, ServerContainer.class, null) { + + @Override + public ServerContainer addingService(ServiceReference reference) { + ServerContainer serverContainer = super.addingService(reference); + + BundleContext bc = reference.getBundle().getBundleContext(); + ServiceReference srConfigurator = bc + .getServiceReference(ServerEndpointConfig.Configurator.class); + ServerEndpointConfig.Configurator endpointConfigurator = bc.getService(srConfigurator); + ServerEndpointConfig config = ServerEndpointConfig.Builder + .create(TestEndpoint.class, "/ws/test/events/").configurator(endpointConfigurator).build(); + try { + serverContainer.addEndpoint(config); + } catch (DeploymentException e) { + throw new IllegalStateException("Cannot initalise the WebSocket server runtime.", e); + } + return serverContainer; + } + + }; + serverSt.open(); + + // check initialisation +// ServiceTracker httpSt = new ServiceTracker(bc, HttpService.class, null) { +// +// @Override +// public HttpService addingService(ServiceReference sr) { +// Object httpPort = sr.getProperty("http.port"); +// Object httpsPort = sr.getProperty("https.port"); +// log.info(httpPortsMsg(httpPort, httpsPort)); +// close(); +// return super.addingService(sr); +// } +// }; +// httpSt.open(); + } + + public void stop() { + try { + JettyConfigurator.stopServer(CmsConstants.DEFAULT); + } catch (Exception e) { + log.error("Cannot stop default Jetty server.", e); + } + + } + + public void startServer(Dictionary properties) { + // Explicitly configures Jetty so that the default server is not started by the + // activator of the Equinox Jetty bundle. + Map config = LangUtils.dictToStringMap(properties); + if (!config.isEmpty()) { + config.put("customizer.class", CMS_JETTY_CUSTOMIZER_CLASS); + + // TODO centralise with Jetty extender + Object webSocketEnabled = config.get(CmsDeployProperty.WEBSOCKET_ENABLED.getProperty()); + if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { + bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null); + // config.put(WEBSOCKET_ENABLED, "true"); + } + } + + long begin = System.currentTimeMillis(); + int tryCount = 60; + try { + while (tryCount > 0) { + try { + // FIXME deal with multiple ids + JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config)); + + Object httpPort = config.get(JettyHttpConstants.HTTP_PORT); + Object httpsPort = config.get(JettyHttpConstants.HTTPS_PORT); + log.info(httpPortsMsg(httpPort, httpsPort)); + + // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi + // configuration is not cleaned + FrameworkUtil.getBundle(JettyConfigurator.class).start(); + return; + } catch (IllegalStateException e) { + // e.printStackTrace(); + // Jetty may not be ready + try { + Thread.sleep(1000); + } catch (Exception e1) { + // silent + } + tryCount--; + } + } + long duration = System.currentTimeMillis() - begin; + log.error("Gave up with starting Jetty server after " + (duration / 1000) + " s"); + } catch (Exception e) { + log.error("Cannot start default Jetty server with config " + properties, e); + } + + } + + private String httpPortsMsg(Object httpPort, Object httpsPort) { + return (httpPort != null ? "HTTP " + httpPort + " " : " ") + (httpsPort != null ? "HTTPS " + httpsPort : ""); + } + + /** Override the provided config with the framework properties */ + public Dictionary getHttpServerConfig() { + String httpPort = getFrameworkProp(CmsDeployProperty.HTTP_PORT); + String httpsPort = getFrameworkProp(CmsDeployProperty.HTTPS_PORT); + /// TODO make it more generic + String httpHost = getFrameworkProp(CmsDeployProperty.HOST); +// String httpsHost = getFrameworkProp( +// JettyConfig.JETTY_PROPERTY_PREFIX + CmsHttpConstants.HTTPS_HOST); + String webSocketEnabled = getFrameworkProp(CmsDeployProperty.WEBSOCKET_ENABLED); + + final Hashtable props = new Hashtable(); + // try { + if (httpPort != null || httpsPort != null) { + boolean httpEnabled = httpPort != null; + props.put(JettyHttpConstants.HTTP_ENABLED, httpEnabled); + boolean httpsEnabled = httpsPort != null; + props.put(JettyHttpConstants.HTTPS_ENABLED, httpsEnabled); + + if (httpEnabled) { + props.put(JettyHttpConstants.HTTP_PORT, httpPort); + if (httpHost != null) + props.put(JettyHttpConstants.HTTP_HOST, httpHost); + } + + if (httpsEnabled) { + props.put(JettyHttpConstants.HTTPS_PORT, httpsPort); + if (httpHost != null) + props.put(JettyHttpConstants.HTTPS_HOST, httpHost); + + // keystore + props.put(JettyHttpConstants.SSL_KEYSTORETYPE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORETYPE)); + props.put(JettyHttpConstants.SSL_KEYSTORE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORE)); + props.put(JettyHttpConstants.SSL_PASSWORD, getFrameworkProp(CmsDeployProperty.SSL_PASSWORD)); + + // truststore + props.put(JettyHttpConstants.SSL_TRUSTSTORETYPE, + getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORETYPE)); + props.put(JettyHttpConstants.SSL_TRUSTSTORE, getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORE)); + props.put(JettyHttpConstants.SSL_TRUSTSTOREPASSWORD, + getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD)); + + // client certificate authentication + String wantClientAuth = getFrameworkProp(CmsDeployProperty.SSL_WANTCLIENTAUTH); + if (wantClientAuth != null) + props.put(JettyHttpConstants.SSL_WANTCLIENTAUTH, Boolean.parseBoolean(wantClientAuth)); + String needClientAuth = getFrameworkProp(CmsDeployProperty.SSL_NEEDCLIENTAUTH); + if (needClientAuth != null) + props.put(JettyHttpConstants.SSL_NEEDCLIENTAUTH, Boolean.parseBoolean(needClientAuth)); + } + + // web socket + if (webSocketEnabled != null && webSocketEnabled.equals("true")) + props.put(CmsDeployProperty.WEBSOCKET_ENABLED.getProperty(), true); + + props.put(CmsConstants.CN, CmsConstants.DEFAULT); + } + return props; + } + + private String getFrameworkProp(CmsDeployProperty deployProperty) { + return cmsState.getDeployProperty(deployProperty.getProperty()); + } + + public void setCmsState(CmsState cmsState) { + this.cmsState = cmsState; + } + +} diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java new file mode 100644 index 000000000..8ceb358dd --- /dev/null +++ b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java @@ -0,0 +1,25 @@ +package org.argeo.cms.servlet.internal.jetty; + +/** Compatible with Jetty. */ +interface JettyHttpConstants { + static final String HTTP_ENABLED = "http.enabled"; + static final String HTTP_PORT = "http.port"; + static final String HTTP_HOST = "http.host"; + static final String HTTPS_ENABLED = "https.enabled"; + static final String HTTPS_HOST = "https.host"; + static final String HTTPS_PORT = "https.port"; + static final String SSL_KEYSTORE = "ssl.keystore"; + static final String SSL_PASSWORD = "ssl.password"; + static final String SSL_KEYPASSWORD = "ssl.keypassword"; + static final String SSL_NEEDCLIENTAUTH = "ssl.needclientauth"; + static final String SSL_WANTCLIENTAUTH = "ssl.wantclientauth"; + static final String SSL_PROTOCOL = "ssl.protocol"; + static final String SSL_ALGORITHM = "ssl.algorithm"; + static final String SSL_KEYSTORETYPE = "ssl.keystoretype"; + + // Argeo + static final String SSL_TRUSTSTORE = "ssl.truststore"; + static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword"; + static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype"; + +} diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java new file mode 100644 index 000000000..005af743c --- /dev/null +++ b/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java @@ -0,0 +1,120 @@ +package org.argeo.cms.servlet.internal.jetty; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator; +import org.argeo.cms.websocket.javax.server.TestEndpoint; +import org.argeo.util.LangUtils; +import org.eclipse.equinox.http.jetty.JettyConfigurator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.util.tracker.ServiceTracker; + +@Deprecated +public class JettyServiceFactory implements ManagedServiceFactory { + private final static CmsLog log = CmsLog.getLog(JettyServiceFactory.class); + + final static String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer"; + // Argeo specific + final static String WEBSOCKET_ENABLED = "websocket.enabled"; + + private final BundleContext bc = FrameworkUtil.getBundle(JettyServiceFactory.class).getBundleContext(); + + public void start() { + ServiceTracker serverSt = new ServiceTracker( + bc, ServerContainer.class, null) { + + @Override + public ServerContainer addingService(ServiceReference reference) { + ServerContainer serverContainer = super.addingService(reference); + + BundleContext bc = reference.getBundle().getBundleContext(); + ServiceReference srConfigurator = bc + .getServiceReference(ServerEndpointConfig.Configurator.class); + ServerEndpointConfig.Configurator endpointConfigurator = bc.getService(srConfigurator); + ServerEndpointConfig config = ServerEndpointConfig.Builder + .create(TestEndpoint.class, "/ws/test/events/").configurator(endpointConfigurator).build(); + try { + serverContainer.addEndpoint(config); + } catch (DeploymentException e) { + throw new IllegalStateException("Cannot initalise the WebSocket server runtime.", e); + } + return serverContainer; + } + + }; + serverSt.open(); + } + + @Override + public String getName() { + return "Jetty Service Factory"; + } + + @Override + public void updated(String pid, Dictionary properties) throws ConfigurationException { + // Explicitly configures Jetty so that the default server is not started by the + // activator of the Equinox Jetty bundle. + Map config = LangUtils.dictToStringMap(properties); + if (!config.isEmpty()) { + config.put("customizer.class", CMS_JETTY_CUSTOMIZER_CLASS); + + // TODO centralise with Jetty extender + Object webSocketEnabled = config.get(WEBSOCKET_ENABLED); + if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { + bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null); + config.put(WEBSOCKET_ENABLED, "true"); + } + } + + int tryCount = 60; + try { + tryGettyJetty: while (tryCount > 0) { + try { + // FIXME deal with multiple ids + JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config)); + // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi + // configuration is not cleaned + FrameworkUtil.getBundle(JettyConfigurator.class).start(); + break tryGettyJetty; + } catch (IllegalStateException e) { + // Jetty may not be ready + try { + Thread.sleep(1000); + } catch (Exception e1) { + // silent + } + tryCount--; + } + } + } catch (Exception e) { + log.error("Cannot start default Jetty server with config " + properties, e); + } + + } + + @Override + public void deleted(String pid) { + } + + public void stop() { + try { + JettyConfigurator.stopServer(CmsConstants.DEFAULT); + } catch (Exception e) { + log.error("Cannot stop default Jetty server.", e); + } + + } + +} diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java b/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java new file mode 100644 index 000000000..ab291b545 --- /dev/null +++ b/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java @@ -0,0 +1,65 @@ +package org.argeo.equinox.jetty; + +import java.util.Dictionary; + +import javax.servlet.ServletContext; +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerContainer; + +import org.eclipse.equinox.http.jetty.JettyCustomizer; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.Configurator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** Customises the Jetty HTTP server. */ +public class CmsJettyCustomizer extends JettyCustomizer { + static final String SSL_TRUSTSTORE = "ssl.truststore"; + static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword"; + static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype"; + + private BundleContext bc = FrameworkUtil.getBundle(CmsJettyCustomizer.class).getBundleContext(); + + public final static String WEBSOCKET_ENABLED = "argeo.websocket.enabled"; + + @Override + public Object customizeContext(Object context, Dictionary settings) { + // WebSocket + Object webSocketEnabled = settings.get(WEBSOCKET_ENABLED); + if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) { + ServletContextHandler servletContextHandler = (ServletContextHandler) context; + JavaxWebSocketServletContainerInitializer.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); + + } + + @Override + public Object customizeHttpsConnector(Object connector, Dictionary settings) { + ServerConnector httpsConnector = (ServerConnector) connector; + if (httpsConnector != null) + for (ConnectionFactory connectionFactory : httpsConnector.getConnectionFactories()) { + if (connectionFactory instanceof SslConnectionFactory) { + SslContextFactory.Server sslConnectionFactory = ((SslConnectionFactory) connectionFactory) + .getSslContextFactory(); + sslConnectionFactory.setTrustStorePath((String) settings.get(SSL_TRUSTSTORE)); + sslConnectionFactory.setTrustStoreType((String) settings.get(SSL_TRUSTSTORETYPE)); + sslConnectionFactory.setTrustStorePassword((String) settings.get(SSL_TRUSTSTOREPASSWORD)); + } + } + return super.customizeHttpsConnector(connector, settings); + } + +} diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/package-info.java b/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/package-info.java new file mode 100644 index 000000000..41c8ce9b0 --- /dev/null +++ b/org.argeo.cms.lib.equinox/src/org/argeo/equinox/jetty/package-info.java @@ -0,0 +1,2 @@ +/** Equinox Jetty extensions. */ +package org.argeo.equinox.jetty; \ No newline at end of file diff --git a/org.argeo.cms.lib.jetty/.classpath b/org.argeo.cms.lib.jetty/.classpath new file mode 100644 index 000000000..81fe078c2 --- /dev/null +++ b/org.argeo.cms.lib.jetty/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.lib.jetty/.project b/org.argeo.cms.lib.jetty/.project new file mode 100644 index 000000000..132df7ff4 --- /dev/null +++ b/org.argeo.cms.lib.jetty/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.lib.jetty + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.lib.jetty/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.lib.jetty/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..62ef3488c --- /dev/null +++ b/org.argeo.cms.lib.jetty/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/org.argeo.cms.lib.jetty/.settings/org.eclipse.pde.core.prefs b/org.argeo.cms.lib.jetty/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 000000000..e8ff8be0b --- /dev/null +++ b/org.argeo.cms.lib.jetty/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +pluginProject.equinox=false +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/org.argeo.cms.lib.jetty/bnd.bnd b/org.argeo.cms.lib.jetty/bnd.bnd new file mode 100644 index 000000000..d92601739 --- /dev/null +++ b/org.argeo.cms.lib.jetty/bnd.bnd @@ -0,0 +1,9 @@ +Import-Package: \ +javax.servlet.http,\ +org.eclipse.jetty.util.component;version="[9.4,12)";resolution:=optional,\ +org.eclipse.jetty.http;version="[9.4,12)";resolution:=optional,\ +org.eclipse.jetty.io;version="[9.4,12)";resolution:=optional,\ +org.eclipse.jetty.security;version="[9.4,12)";resolution:=optional,\ +org.eclipse.jetty.server.handler;version="[9.4,12)";resolution:=optional,\ +org.eclipse.jetty.*;version="[9.4,12)";resolution:=optional,\ +* \ No newline at end of file diff --git a/org.argeo.cms.lib.jetty/build.properties b/org.argeo.cms.lib.jetty/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms.lib.jetty/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/lib/jetty/CmsJettyServer.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/lib/jetty/CmsJettyServer.java new file mode 100644 index 000000000..d197a00f7 --- /dev/null +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/lib/jetty/CmsJettyServer.java @@ -0,0 +1,52 @@ +package org.argeo.cms.lib.jetty; + +import java.nio.file.Path; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +public class CmsJettyServer { + private Server server; + private ServerConnector serverConnector; + private Path tempDir; + + public void start() { + server = new Server(new QueuedThreadPool(10, 1)); + serverConnector = new ServerConnector(server); + serverConnector.setPort(0); + server.setConnectors(new Connector[] { serverConnector }); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + server.setHandler(context); + + //context.addServlet(new ServletHolder(new RWTServlet()), "/" + entryPoint); + + // Required to serve rwt-resources. It is important that this is last. + ServletHolder holderPwd = new ServletHolder("default", DefaultServlet.class); + context.addServlet(holderPwd, "/"); + + try { + server.start(); + } catch (Exception e) { + throw new IllegalStateException("Cannot start Jetty server", e); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown")); + } + + public void stop() { + try { + serverConnector.close(); + server.stop(); + // TODO delete temp dir + } catch (Exception e) { + e.printStackTrace(); + } + + } +} diff --git a/org.argeo.cms.lib.pgsql/.classpath b/org.argeo.cms.lib.pgsql/.classpath new file mode 100644 index 000000000..e801ebfb4 --- /dev/null +++ b/org.argeo.cms.lib.pgsql/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.lib.pgsql/.project b/org.argeo.cms.lib.pgsql/.project new file mode 100644 index 000000000..3cd5f6fb4 --- /dev/null +++ b/org.argeo.cms.lib.pgsql/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.lib.pgsql + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.lib.pgsql/bnd.bnd b/org.argeo.cms.lib.pgsql/bnd.bnd new file mode 100644 index 000000000..9c7300926 --- /dev/null +++ b/org.argeo.cms.lib.pgsql/bnd.bnd @@ -0,0 +1 @@ +Import-Package: org.postgresql;version="[42,43)" diff --git a/org.argeo.cms.lib.pgsql/build.properties b/org.argeo.cms.lib.pgsql/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms.lib.pgsql/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.argeo.cms.lib.pgsql/src/org/argeo/cms/sql/postgres/CheckPg.java b/org.argeo.cms.lib.pgsql/src/org/argeo/cms/sql/postgres/CheckPg.java new file mode 100644 index 000000000..bc002a6a5 --- /dev/null +++ b/org.argeo.cms.lib.pgsql/src/org/argeo/cms/sql/postgres/CheckPg.java @@ -0,0 +1,42 @@ +package org.argeo.cms.sql.postgres; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.postgresql.Driver; + +/** Simple PostgreSQL check. */ +public class CheckPg { + + public List listTables() { + String osUser = System.getProperty("user.name"); + + String url = "jdbc:postgresql://localhost/" + osUser; + Properties props = new Properties(); + props.setProperty("user", osUser); + props.setProperty("password", "changeit"); + List result = new ArrayList<>(); + + Driver driver = new Driver(); + try (Connection conn = driver.connect(url, props); Statement s = conn.createStatement();) { + s.execute("SELECT * FROM pg_catalog.pg_tables"); + ResultSet rs = s.getResultSet(); + while (rs.next()) { + result.add(rs.getString("tablename")); + } + return result; + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + new CheckPg().listTables().forEach(System.out::println); + } + +} diff --git a/org.argeo.cms.lib.sshd/.classpath b/org.argeo.cms.lib.sshd/.classpath new file mode 100644 index 000000000..81fe078c2 --- /dev/null +++ b/org.argeo.cms.lib.sshd/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.lib.sshd/.gitignore b/org.argeo.cms.lib.sshd/.gitignore new file mode 100644 index 000000000..7fb0c180c --- /dev/null +++ b/org.argeo.cms.lib.sshd/.gitignore @@ -0,0 +1,3 @@ +/hostkey.ser +/id_rsa +/id_rsa.pub diff --git a/org.argeo.cms.lib.sshd/.project b/org.argeo.cms.lib.sshd/.project new file mode 100644 index 000000000..588b82996 --- /dev/null +++ b/org.argeo.cms.lib.sshd/.project @@ -0,0 +1,33 @@ + + + org.argeo.cms.lib.sshd + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.lib.sshd/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.lib.sshd/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..997d6645b --- /dev/null +++ b/org.argeo.cms.lib.sshd/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,104 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml b/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml new file mode 100644 index 000000000..aa2d8db2d --- /dev/null +++ b/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/org.argeo.cms.lib.sshd/bnd.bnd b/org.argeo.cms.lib.sshd/bnd.bnd new file mode 100644 index 000000000..e708391a7 --- /dev/null +++ b/org.argeo.cms.lib.sshd/bnd.bnd @@ -0,0 +1,7 @@ +Import-Package: \ +org.apache.sshd.server.forward,\ +org.apache.sshd.common.forward,\ +* + +Service-Component: \ +OSGI-INF/cmsSshServer.xml diff --git a/org.argeo.cms.lib.sshd/build.properties b/org.argeo.cms.lib.sshd/build.properties new file mode 100644 index 000000000..1e3132aac --- /dev/null +++ b/org.argeo.cms.lib.sshd/build.properties @@ -0,0 +1,9 @@ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/ +source.. = src/ +additional.bundles = org.apache.sshd.common,\ + org.apache.sshd.core,\ + org.slf4j.api,\ + org.argeo.ext.slf4j diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/AbstractSsh.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/AbstractSsh.java new file mode 100644 index 000000000..f2525bff8 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/AbstractSsh.java @@ -0,0 +1,211 @@ +package org.argeo.cms.ssh; + +import java.io.Console; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.io.input.NoCloseInputStream; +import org.apache.sshd.common.util.io.output.NoCloseOutputStream; +import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider; +import org.argeo.api.cms.CmsLog; + +@SuppressWarnings("restriction") +public abstract class AbstractSsh { + private final static CmsLog log = CmsLog.getLog(AbstractSsh.class); + + private static SshClient sshClient; + private static SftpFileSystemProvider sftpFileSystemProvider; + + private boolean passwordSet = false; + private ClientSession session; + + private SshKeyPair sshKeyPair; + + public 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; + } + + public void authenticate() { + 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; + } + } + verifyAuth(); + } + + public void verifyAuth() { + try { + session.auth().verify(1000l); + } catch (IOException e) { + throw new IllegalStateException("Cannot verify auth", e); + } + } + + public static char[] readPassword() { + Console console = System.console(); + if (console == null) {// IDE + System.out.print("Password: "); + try (Scanner s = new Scanner(System.in)) { + String password = s.next(); + return password.toCharArray(); + } + } else { + console.printf("Password: "); + char[] pwd = console.readPassword(); + return pwd; + } + } + + 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); + } + } + + public 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(AbstractSsh ssh) { + openShell(ssh.getSession()); + } + + public static void openShell(ClientSession session) { + try (ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_SHELL)) { + channel.setIn(new NoCloseInputStream(System.in)); + channel.setOut(new NoCloseOutputStream(System.out)); + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open(); + + Set events = new HashSet<>(); + events.add(ClientChannelEvent.CLOSED); + channel.waitFor(events, 0); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + session.close(false); + } + } + + static URI toUri(String username, String host, int port) { + try { + if (username == null) + username = "root"; + return new URI("ssh://" + username + "@" + host + ":" + port); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot generate SSH URI to " + host + ":" + port + " for " + username, + e); + } + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java new file mode 100644 index 000000000..9e9389342 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java @@ -0,0 +1,108 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.sshd.scp.server.ScpCommandFactory; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.shell.ProcessShellFactory; +import org.argeo.util.OS; + +/** A simple SSH server with some defaults. Supports SCP. */ +@SuppressWarnings("restriction") +public class BasicSshServer { + private Integer port; + private Path hostKeyPath; + + private SshServer sshd = null; + + public BasicSshServer(Integer port, Path hostKeyPath) { + this.port = port; + this.hostKeyPath = hostKeyPath; + } + + public void init() { + try { + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + if (hostKeyPath == null) + throw new IllegalStateException("An SSH server key must be set"); + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath)); + // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", + // "-l" })); + String[] shellCommand = OS.LOCAL.getDefaultShellCommand(); + // FIXME transfer args +// sshd.setShellFactory(new ProcessShellFactory(shellCommand)); + sshd.setShellFactory(new ProcessShellFactory(shellCommand[0], shellCommand)); + sshd.setCommandFactory(new ScpCommandFactory()); + + sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true)); + sshd.start(); + } catch (Exception e) { + throw new RuntimeException("Cannot start SSH server on port " + port, e); + } + } + + public void destroy() { + try { + sshd.stop(); + } catch (IOException e) { + throw new RuntimeException("Cannot stop SSH server on port " + port, e); + } + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Path getHostKeyPath() { + return hostKeyPath; + } + + public void setHostKeyPath(Path hostKeyPath) { + this.hostKeyPath = hostKeyPath; + } + + public static void main(String[] args) { + int port = 2222; + Path hostKeyPath = Paths.get("hostkey.ser"); + try { + if (args.length > 0) + port = Integer.parseInt(args[0]); + if (args.length > 1) + hostKeyPath = Paths.get(args[1]); + } catch (Exception e1) { + printUsage(); + } + + BasicSshServer sshServer = new BasicSshServer(port, hostKeyPath); + sshServer.init(); + Runtime.getRuntime().addShutdownHook(new Thread("Shutdown SSH server") { + + @Override + public void run() { + sshServer.destroy(); + } + }); + try { + synchronized (sshServer) { + sshServer.wait(); + } + } catch (InterruptedException e) { + sshServer.destroy(); + } + + } + + public static void printUsage() { + System.out.println("java " + BasicSshServer.class.getName() + " [port] [server key path]"); + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java new file mode 100644 index 000000000..7246d603d --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java @@ -0,0 +1,142 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.sshd.common.forward.PortForwardingEventListener; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.scp.server.ScpCommandFactory; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.gss.GSSAuthenticator; +import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; +import org.apache.sshd.server.forward.AcceptAllForwardingFilter; +import org.apache.sshd.server.jaas.JaasPasswordAuthenticator; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.shell.InteractiveProcessShellFactory; +import org.apache.sshd.sftp.server.SftpSubsystemFactory; +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.CmsState; +import org.argeo.cms.CmsDeployProperty; + +public class CmsSshServer { + private final static CmsLog log = CmsLog.getLog(CmsSshServer.class); + private static final String DEFAULT_SSH_HOST_KEY_PATH = CmsConstants.NODE + '/' + CmsConstants.NODE + ".ser"; + + private CmsState cmsState; + private SshServer sshd = null; + + private int port; + private String host; + + public void start() { + String portStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_PORT.getProperty()); + if (portStr == null) + return; // ignore + port = Integer.parseInt(portStr); + + host = cmsState.getDeployProperty(CmsDeployProperty.HOST.getProperty()); + + Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH); + + try { + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + if (host != null) + sshd.setHost(host); + if (hostKeyPath == null) + throw new IllegalStateException("An SSH server key must be set"); + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath)); + // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", + // "-l" })); +// String[] shellCommand = OS.LOCAL.getDefaultShellCommand(); + // FIXME transfer args +// sshd.setShellFactory(new ProcessShellFactory(shellCommand)); + sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE); + sshd.setCommandFactory(new ScpCommandFactory()); + + // tunnels + sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE); + // sshd.setForwardingFilter(ForwardingFilter.asForwardingFilter(null, null, + // TcpForwardingFilter.DEFAULT)); + // sshd.setForwarderFactory(DefaultForwarderFactory.INSTANCE); +// TcpForwardingFilter tcpForwardingFilter = sshd.getTcpForwardingFilter(); + sshd.addPortForwardingEventListener(new PortForwardingEventListener() { + + @Override + public void establishingExplicitTunnel(Session session, SshdSocketAddress local, + SshdSocketAddress remote, boolean localForwarding) throws IOException { + log.debug("Establishing tunnel " + local + ", " + remote); + } + + @Override + public void establishedExplicitTunnel(Session session, SshdSocketAddress local, + SshdSocketAddress remote, boolean localForwarding, SshdSocketAddress boundAddress, + Throwable reason) throws IOException { + log.debug("Established tunnel " + local + ", " + remote + ", " + boundAddress); + } + + @Override + public void establishingDynamicTunnel(Session session, SshdSocketAddress local) throws IOException { + log.debug("Establishing dynamic tunnel " + local); + } + + @Override + public void establishedDynamicTunnel(Session session, SshdSocketAddress local, + SshdSocketAddress boundAddress, Throwable reason) throws IOException { + log.debug("Established dynamic tunnel " + local); + } + + }); + + // Authentication + //sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true)); + sshd.setPublickeyAuthenticator(null); + // sshd.setKeyboardInteractiveAuthenticator(null); + JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator(); + jaasPasswordAuthenticator.setDomain(CmsAuth.NODE.getLoginContextName()); + sshd.setPasswordAuthenticator(jaasPasswordAuthenticator); + + Path krb5keyTab = cmsState.getDataPath("node/krb5.keytab"); + if (Files.exists(krb5keyTab)) { + // FIXME experimental + GSSAuthenticator gssAuthenticator = new GSSAuthenticator(); + gssAuthenticator.setKeytabFile(cmsState.getDataPath("node/krb5.keytab").toString()); + gssAuthenticator.setServicePrincipalName("HTTP@" + host); + sshd.setGSSAuthenticator(gssAuthenticator); + } + + // SFTP + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + + // start + sshd.start(); + + log.debug(() -> "CMS SSH server started on port " + port + (host != null ? " of host " + host : "")); + } catch (IOException e) { + throw new RuntimeException("Cannot start SSH server on port " + port, e); + } + + } + + public void stop() { + if (sshd == null) + return; + try { + sshd.stop(); + log.debug(() -> "CMS SSH server stopped on port " + port + (host != null ? " of host " + host : "")); + } catch (IOException e) { + throw new RuntimeException("Cannot stop SSH server", e); + } + + } + + public void setCmsState(CmsState cmsState) { + this.cmsState = cmsState; + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Sftp.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Sftp.java new file mode 100644 index 000000000..f6f447418 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Sftp.java @@ -0,0 +1,42 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Path; + +import org.apache.sshd.sftp.client.fs.SftpFileSystem; + +/** Create an SFTP {@link FileSystem}. */ +public class Sftp extends AbstractSsh { + private URI uri; + + private SftpFileSystem fileSystem; + + public Sftp(String username, String host, int port) { + this(AbstractSsh.toUri(username, host, port)); + } + + public Sftp(URI uri) { + this.uri = uri; + openSession(uri); + } + + public FileSystem getFileSystem() { + if (fileSystem == null) { + try { + authenticate(); + fileSystem = getSftpFileSystemProvider().newFileSystem(getSession()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + return fileSystem; + } + + public Path getBasePath() { + String p = uri.getPath() != null ? uri.getPath() : "/"; + return getFileSystem().getPath(p); + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Ssh.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Ssh.java new file mode 100644 index 000000000..6e7b6ef2a --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/Ssh.java @@ -0,0 +1,81 @@ +package org.argeo.cms.ssh; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +/** Create an SSH shell. */ +public class Ssh extends AbstractSsh { + private final URI uri; + + public Ssh(String username, String host, int port) { + this(AbstractSsh.toUri(username, host, port)); + } + + public Ssh(URI uri) { + this.uri = uri; + openSession(uri); + } + + public static void main(String[] args) { + Options options = getOptions(); + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse(options, args); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + System.err.println("There must be at least one argument"); + printHelp(options); + System.exit(1); + } + URI uri = new URI("ssh://" + remaining.get(0)); + List command = new ArrayList<>(); + if (remaining.size() > 1) { + for (int i = 1; i < remaining.size(); i++) { + command.add(remaining.get(i)); + } + } + + // auth + Ssh ssh = new Ssh(uri); + ssh.authenticate(); + + if (command.size() == 0) {// shell + AbstractSsh.openShell(ssh.getSession()); + } else {// execute command + + } + ssh.closeSession(); + } catch (Exception exp) { + exp.printStackTrace(); + printHelp(options); + System.exit(1); + } finally { + + } + } + + public URI getUri() { + return uri; + } + + public static Options getOptions() { + Options options = new Options(); +// options.addOption("p", true, "port"); + options.addOption(Option.builder("p").hasArg().argName("port").desc("port of the SSH server").build()); + + return options; + } + + public static void printHelp(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("ssh [username@]hostname", options, true); + } +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshKeyPair.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshKeyPair.java new file mode 100644 index 000000000..f5cbb0450 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshKeyPair.java @@ -0,0 +1,205 @@ +package org.argeo.cms.ssh; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.RSAPublicKeySpec; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider; +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 loadDefault(char[] password) { + Path privateKeyPath = Paths.get(System.getProperty("user.home") + "/.ssh/id_rsa"); + // TODO try other formats + return load(privateKeyPath, password); + } + + public static SshKeyPair load(Path privateKeyPath, char[] password) { + try (Reader reader = Files.newBufferedReader(privateKeyPath)) { + return load(reader, password); + } catch (IOException e) { + throw new IllegalStateException("Cannot load private key from " + privateKeyPath, 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 PEMEncryptedKeyPair) { + PEMEncryptedKeyPair ekp = (PEMEncryptedKeyPair) object; + PEMDecryptorProvider decryptorProvider = new BcPEMDecryptorProvider(password); + PEMKeyPair pemKp = ekp.decryptKeyPair(decryptorProvider); + kp = converter.getKeyPair(pemKp); + } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) { + // Encrypted key - we will use provided password + PKCS8EncryptedPrivateKeyInfo ckp = (PKCS8EncryptedPrivateKeyInfo) object; +// PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password); + InputDecryptorProvider inputDecryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder() + .build(password); + PrivateKeyInfo pkInfo = ckp.decryptPrivateKeyInfo(inputDecryptorProvider); + PrivateKey privateKey = converter.getPrivateKey(pkInfo); + + // generate public key + RSAPrivateCrtKey privk = (RSAPrivateCrtKey) privateKey; + RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(privk.getModulus(), + privk.getPublicExponent()); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + kp = new KeyPair(publicKey, privateKey); + } else { + // Unencrypted key - no password needed +// PKCS8EncryptedPrivateKeyInfo ukp = (PKCS8EncryptedPrivateKeyInfo) object; + PEMKeyPair pemKp = (PEMKeyPair) object; + kp = converter.getKeyPair(pemKp); + } + return new SshKeyPair(kp); + } catch (Exception e) { + throw new RuntimeException("Cannot load private key", e); + } + } + + public static void main(String args[]) { + Path privateKeyPath = Paths.get(System.getProperty("user.dir") + "/id_rsa"); + SshKeyPair skp = SshKeyPair.loadOrGenerate(privateKeyPath, 1024, null); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + + StringReader reader = new StringReader(skp.getPrivateKeyAsPemString(null)); + skp = SshKeyPair.load(reader, null); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + + reader = new StringReader(skp.getPrivateKeyAsPemString("demo".toCharArray())); + skp = SshKeyPair.load(reader, "demo".toCharArray()); + System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); + System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); + System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java new file mode 100644 index 000000000..71b636576 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java @@ -0,0 +1,154 @@ +package org.argeo.cms.ssh; + +import java.io.Console; +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.security.KeyPair; +import java.util.Map; +import java.util.Scanner; + +import org.apache.commons.io.IOUtils; +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.local.LocalAgentFactory; +import org.apache.sshd.agent.unix.UnixAgentFactory; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.config.keys.ClientIdentityLoader; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.sftp.client.fs.SftpFileSystem; +import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider; +import org.argeo.api.cms.CmsLog; + +public class SshSync { + private final static CmsLog log = CmsLog.getLog(SshSync.class); + + public static void main(String[] args) { + + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + boolean osAgent = false; + SshAgentFactory agentFactory = osAgent ? new UnixAgentFactory() : new LocalAgentFactory(); + // SshAgentFactory agentFactory = new LocalAgentFactory(); + client.setAgentFactory(agentFactory); + SshAgent sshAgent = agentFactory.createClient(null, client); + + String login = System.getProperty("user.name"); + String host = "localhost"; + int port = 22; + + if (!osAgent) { + String keyPath = "/home/" + login + "/.ssh/id_rsa"; + + String password; + Console console = System.console(); + if (console != null) { + password = new String(console.readPassword(keyPath + ": ")); + } else { + System.out.print(keyPath + ": "); + try (Scanner s = new Scanner(System.in)) { + password = s.next(); + } + } + NamedResource namedResource = new NamedResource() { + + @Override + public String getName() { + return keyPath; + } + }; + KeyPair keyPair = ClientIdentityLoader.DEFAULT + .loadClientIdentities(null, namedResource, FilePasswordProvider.of(password)).iterator().next(); + sshAgent.addIdentity(keyPair, "NO COMMENT"); + } + +// List> identities = sshAgent.getIdentities(); +// for (Map.Entry entry : identities) { +// System.out.println(entry.getValue() + " : " + entry.getKey()); +// } + + ConnectFuture connectFuture = client.connect(login, host, port); + connectFuture.await(); + ClientSession session = connectFuture.getSession(); + + try { + +// session.addPasswordIdentity(new String(password)); + session.auth().verify(1000l); + + SftpFileSystemProvider fsProvider = new SftpFileSystemProvider(client); + + SftpFileSystem fs = fsProvider.newFileSystem(session); + Path testPath = fs.getPath("/home/" + login + "/tmp"); + Files.list(testPath).forEach(System.out::println); + test(testPath); + + } finally { + client.stop(); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + static void test(Path testBase) { + try { + Path testPath = testBase.resolve("ssh-test.txt"); + Files.createFile(testPath); + log.debug("Created file " + testPath); + Files.delete(testPath); + log.debug("Deleted " + testPath); + String txt = "TEST\nTEST2\n"; + byte[] arr = txt.getBytes(); + Files.write(testPath, arr); + log.debug("Wrote " + testPath); + byte[] read = Files.readAllBytes(testPath); + log.debug("Read " + testPath); + Path testDir = testBase.resolve("testDir"); + log.debug("Resolved " + testDir); + // Copy + Files.createDirectory(testDir); + log.debug("Created directory " + testDir); + Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); + log.debug("Created sub directories " + subsubdir); + Path copiedFile = testDir.resolve("copiedFile.txt"); + log.debug("Resolved " + copiedFile); + Path relativeCopiedFile = testDir.relativize(copiedFile); + log.debug("Relative copied file " + relativeCopiedFile); + try (OutputStream out = Files.newOutputStream(copiedFile); + InputStream in = Files.newInputStream(testPath)) { + IOUtils.copy(in, out); + } + log.debug("Copied " + testPath + " to " + copiedFile); + Files.delete(testPath); + log.debug("Deleted " + testPath); + byte[] copiedRead = Files.readAllBytes(copiedFile); + log.debug("Read " + copiedFile); + // Browse directories + DirectoryStream files = Files.newDirectoryStream(testDir); + int fileCount = 0; + Path listedFile = null; + for (Path file : files) { + fileCount++; + if (!Files.isDirectory(file)) + listedFile = file; + } + log.debug("Listed " + testDir); + // Generic attributes + Map attrs = Files.readAttributes(copiedFile, "*"); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshCli.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshCli.java new file mode 100644 index 000000000..12b4d5e1d --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshCli.java @@ -0,0 +1,17 @@ +package org.argeo.cms.ssh.cli; + +import org.argeo.api.cli.CommandsCli; + +public class SshCli extends CommandsCli { + public SshCli(String commandName) { + super(commandName); + addCommand("shell", new SshShell()); + } + + @Override + public String getDescription() { + return "SSH utilities."; + } + + +} diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshShell.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshShell.java new file mode 100644 index 000000000..78903a779 --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/cli/SshShell.java @@ -0,0 +1,115 @@ +package org.argeo.cms.ssh.cli; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +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.config.keys.ClientIdentityLoader; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.argeo.api.cli.DescribedCommand; +import org.argeo.cms.ssh.AbstractSsh; +import org.argeo.cms.ssh.Ssh; + +public class SshShell implements DescribedCommand { + private Option portOption; + + @Override + public Options getOptions() { + Options options = new Options(); + portOption = Option.builder().option("p").longOpt("port").hasArg().desc("port to connect to").build(); + options.addOption(portOption); + return options; + } + + @Override + public String apply(List args) { + CommandLine cl = toCommandLine(args); + String portStr = cl.getOptionValue(portOption); + if (portStr == null) + portStr = "22"; + + String host = cl.getArgList().get(0); + + String uriStr = "ssh://" + host + ":" + portStr + "/"; + // System.out.println(uriStr); + URI uri = URI.create(uriStr); + + Ssh ssh = null; + try { + ssh = new Ssh(uri); + boolean osAgent; + SshAgent sshAgent; + try { + String sshAuthSockentEnv = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME); + if (sshAuthSockentEnv != null) { + ssh.getSshClient().getProperties().put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, sshAuthSockentEnv); + SshAgentFactory agentFactory = new UnixAgentFactory(); + ssh.getSshClient().setAgentFactory(agentFactory); + sshAgent = agentFactory.createClient(null, ssh.getSshClient()); + osAgent = true; + } else { + osAgent = false; + } + } catch (Exception e) { + e.printStackTrace(); + osAgent = false; + } + + if (!osAgent) { + SshAgentFactory agentFactory = new LocalAgentFactory(); + ssh.getSshClient().setAgentFactory(agentFactory); + sshAgent = agentFactory.createClient(null, ssh.getSshClient()); + String keyPath = System.getProperty("user.home") + "/.ssh/id_rsa"; + + char[] keyPassword = AbstractSsh.readPassword(); + NamedResource namedResource = new NamedResource() { + + @Override + public String getName() { + return keyPath; + } + }; + KeyPair keyPair = ClientIdentityLoader.DEFAULT + .loadClientIdentities(null, namedResource, FilePasswordProvider.of(new String(keyPassword))) + .iterator().next(); + sshAgent.addIdentity(keyPair, "NO COMMENT"); + } + +// char[] keyPassword = AbstractSsh.readPassword(); +// SshKeyPair keyPair = SshKeyPair.loadDefault(keyPassword); +// Arrays.fill(keyPassword, '*'); +// ssh.setSshKeyPair(keyPair); +// ssh.authenticate(); + ssh.verifyAuth(); + + long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime(); + System.out.println("Ssh available in " + jvmUptime + " ms."); + + AbstractSsh.openShell(ssh); + } catch (IOException | GeneralSecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + if (ssh != null) + ssh.closeSession(); + } + return null; + } + + @Override + public String getDescription() { + return "Launch a static CMS."; + } + + } \ No newline at end of file diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/package-info.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/package-info.java new file mode 100644 index 000000000..9555662ad --- /dev/null +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/package-info.java @@ -0,0 +1,2 @@ +/** SSH support. */ +package org.argeo.cms.ssh; \ No newline at end of file diff --git a/org.argeo.cms.sql/.classpath b/org.argeo.cms.sql/.classpath deleted file mode 100644 index e801ebfb4..000000000 --- a/org.argeo.cms.sql/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.argeo.cms.sql/.project b/org.argeo.cms.sql/.project deleted file mode 100644 index b96a541b2..000000000 --- a/org.argeo.cms.sql/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - org.argeo.cms.sql - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/org.argeo.cms.sql/bnd.bnd b/org.argeo.cms.sql/bnd.bnd deleted file mode 100644 index 9c7300926..000000000 --- a/org.argeo.cms.sql/bnd.bnd +++ /dev/null @@ -1 +0,0 @@ -Import-Package: org.postgresql;version="[42,43)" diff --git a/org.argeo.cms.sql/build.properties b/org.argeo.cms.sql/build.properties deleted file mode 100644 index 34d2e4d2d..000000000 --- a/org.argeo.cms.sql/build.properties +++ /dev/null @@ -1,4 +0,0 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - . diff --git a/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java b/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java deleted file mode 100644 index bc002a6a5..000000000 --- a/org.argeo.cms.sql/src/org/argeo/cms/sql/postgres/CheckPg.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.cms.sql.postgres; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import org.postgresql.Driver; - -/** Simple PostgreSQL check. */ -public class CheckPg { - - public List listTables() { - String osUser = System.getProperty("user.name"); - - String url = "jdbc:postgresql://localhost/" + osUser; - Properties props = new Properties(); - props.setProperty("user", osUser); - props.setProperty("password", "changeit"); - List result = new ArrayList<>(); - - Driver driver = new Driver(); - try (Connection conn = driver.connect(url, props); Statement s = conn.createStatement();) { - s.execute("SELECT * FROM pg_catalog.pg_tables"); - ResultSet rs = s.getResultSet(); - while (rs.next()) { - result.add(rs.getString("tablename")); - } - return result; - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - public static void main(String[] args) { - new CheckPg().listTables().forEach(System.out::println); - } - -} diff --git a/org.argeo.cms.ssh/.classpath b/org.argeo.cms.ssh/.classpath deleted file mode 100644 index 81fe078c2..000000000 --- a/org.argeo.cms.ssh/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.argeo.cms.ssh/.gitignore b/org.argeo.cms.ssh/.gitignore deleted file mode 100644 index 7fb0c180c..000000000 --- a/org.argeo.cms.ssh/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/hostkey.ser -/id_rsa -/id_rsa.pub diff --git a/org.argeo.cms.ssh/.project b/org.argeo.cms.ssh/.project deleted file mode 100644 index d46b3afef..000000000 --- a/org.argeo.cms.ssh/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - org.argeo.cms.ssh - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - org.eclipse.pde.ds.core.builder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 997d6645b..000000000 --- a/org.argeo.cms.ssh/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,104 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled -org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= -org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable -org.eclipse.jdt.core.compiler.annotation.nullable.secondary= -org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled -org.eclipse.jdt.core.compiler.problem.APILeak=warning -org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning -org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info -org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled -org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.cms.ssh/OSGI-INF/cmsSshServer.xml b/org.argeo.cms.ssh/OSGI-INF/cmsSshServer.xml deleted file mode 100644 index aa2d8db2d..000000000 --- a/org.argeo.cms.ssh/OSGI-INF/cmsSshServer.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/org.argeo.cms.ssh/bnd.bnd b/org.argeo.cms.ssh/bnd.bnd deleted file mode 100644 index e708391a7..000000000 --- a/org.argeo.cms.ssh/bnd.bnd +++ /dev/null @@ -1,7 +0,0 @@ -Import-Package: \ -org.apache.sshd.server.forward,\ -org.apache.sshd.common.forward,\ -* - -Service-Component: \ -OSGI-INF/cmsSshServer.xml diff --git a/org.argeo.cms.ssh/build.properties b/org.argeo.cms.ssh/build.properties deleted file mode 100644 index 1e3132aac..000000000 --- a/org.argeo.cms.ssh/build.properties +++ /dev/null @@ -1,9 +0,0 @@ -output.. = bin/ -bin.includes = META-INF/,\ - .,\ - OSGI-INF/ -source.. = src/ -additional.bundles = org.apache.sshd.common,\ - org.apache.sshd.core,\ - org.slf4j.api,\ - org.argeo.ext.slf4j diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java deleted file mode 100644 index f2525bff8..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/AbstractSsh.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.Console; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Scanner; -import java.util.Set; - -import org.apache.sshd.client.SshClient; -import org.apache.sshd.client.channel.ClientChannel; -import org.apache.sshd.client.channel.ClientChannelEvent; -import org.apache.sshd.client.future.ConnectFuture; -import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.common.util.io.input.NoCloseInputStream; -import org.apache.sshd.common.util.io.output.NoCloseOutputStream; -import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider; -import org.argeo.api.cms.CmsLog; - -@SuppressWarnings("restriction") -public abstract class AbstractSsh { - private final static CmsLog log = CmsLog.getLog(AbstractSsh.class); - - private static SshClient sshClient; - private static SftpFileSystemProvider sftpFileSystemProvider; - - private boolean passwordSet = false; - private ClientSession session; - - private SshKeyPair sshKeyPair; - - public 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; - } - - public void authenticate() { - 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; - } - } - verifyAuth(); - } - - public void verifyAuth() { - try { - session.auth().verify(1000l); - } catch (IOException e) { - throw new IllegalStateException("Cannot verify auth", e); - } - } - - public static char[] readPassword() { - Console console = System.console(); - if (console == null) {// IDE - System.out.print("Password: "); - try (Scanner s = new Scanner(System.in)) { - String password = s.next(); - return password.toCharArray(); - } - } else { - console.printf("Password: "); - char[] pwd = console.readPassword(); - return pwd; - } - } - - 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); - } - } - - public 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(AbstractSsh ssh) { - openShell(ssh.getSession()); - } - - public static void openShell(ClientSession session) { - try (ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_SHELL)) { - channel.setIn(new NoCloseInputStream(System.in)); - channel.setOut(new NoCloseOutputStream(System.out)); - channel.setErr(new NoCloseOutputStream(System.err)); - channel.open(); - - Set events = new HashSet<>(); - events.add(ClientChannelEvent.CLOSED); - channel.waitFor(events, 0); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - session.close(false); - } - } - - static URI toUri(String username, String host, int port) { - try { - if (username == null) - username = "root"; - return new URI("ssh://" + username + "@" + host + ":" + port); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot generate SSH URI to " + host + ":" + port + " for " + username, - e); - } - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java deleted file mode 100644 index 9e9389342..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/BasicSshServer.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.apache.sshd.scp.server.ScpCommandFactory; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.apache.sshd.server.shell.ProcessShellFactory; -import org.argeo.util.OS; - -/** A simple SSH server with some defaults. Supports SCP. */ -@SuppressWarnings("restriction") -public class BasicSshServer { - private Integer port; - private Path hostKeyPath; - - private SshServer sshd = null; - - public BasicSshServer(Integer port, Path hostKeyPath) { - this.port = port; - this.hostKeyPath = hostKeyPath; - } - - public void init() { - try { - sshd = SshServer.setUpDefaultServer(); - sshd.setPort(port); - if (hostKeyPath == null) - throw new IllegalStateException("An SSH server key must be set"); - sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath)); - // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", - // "-l" })); - String[] shellCommand = OS.LOCAL.getDefaultShellCommand(); - // FIXME transfer args -// sshd.setShellFactory(new ProcessShellFactory(shellCommand)); - sshd.setShellFactory(new ProcessShellFactory(shellCommand[0], shellCommand)); - sshd.setCommandFactory(new ScpCommandFactory()); - - sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true)); - sshd.start(); - } catch (Exception e) { - throw new RuntimeException("Cannot start SSH server on port " + port, e); - } - } - - public void destroy() { - try { - sshd.stop(); - } catch (IOException e) { - throw new RuntimeException("Cannot stop SSH server on port " + port, e); - } - } - - public Integer getPort() { - return port; - } - - public void setPort(Integer port) { - this.port = port; - } - - public Path getHostKeyPath() { - return hostKeyPath; - } - - public void setHostKeyPath(Path hostKeyPath) { - this.hostKeyPath = hostKeyPath; - } - - public static void main(String[] args) { - int port = 2222; - Path hostKeyPath = Paths.get("hostkey.ser"); - try { - if (args.length > 0) - port = Integer.parseInt(args[0]); - if (args.length > 1) - hostKeyPath = Paths.get(args[1]); - } catch (Exception e1) { - printUsage(); - } - - BasicSshServer sshServer = new BasicSshServer(port, hostKeyPath); - sshServer.init(); - Runtime.getRuntime().addShutdownHook(new Thread("Shutdown SSH server") { - - @Override - public void run() { - sshServer.destroy(); - } - }); - try { - synchronized (sshServer) { - sshServer.wait(); - } - } catch (InterruptedException e) { - sshServer.destroy(); - } - - } - - public static void printUsage() { - System.out.println("java " + BasicSshServer.class.getName() + " [port] [server key path]"); - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/CmsSshServer.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/CmsSshServer.java deleted file mode 100644 index 7246d603d..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/CmsSshServer.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; - -import org.apache.sshd.common.forward.PortForwardingEventListener; -import org.apache.sshd.common.session.Session; -import org.apache.sshd.common.util.net.SshdSocketAddress; -import org.apache.sshd.scp.server.ScpCommandFactory; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.auth.gss.GSSAuthenticator; -import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; -import org.apache.sshd.server.forward.AcceptAllForwardingFilter; -import org.apache.sshd.server.jaas.JaasPasswordAuthenticator; -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.apache.sshd.server.shell.InteractiveProcessShellFactory; -import org.apache.sshd.sftp.server.SftpSubsystemFactory; -import org.argeo.api.cms.CmsAuth; -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.argeo.api.cms.CmsState; -import org.argeo.cms.CmsDeployProperty; - -public class CmsSshServer { - private final static CmsLog log = CmsLog.getLog(CmsSshServer.class); - private static final String DEFAULT_SSH_HOST_KEY_PATH = CmsConstants.NODE + '/' + CmsConstants.NODE + ".ser"; - - private CmsState cmsState; - private SshServer sshd = null; - - private int port; - private String host; - - public void start() { - String portStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_PORT.getProperty()); - if (portStr == null) - return; // ignore - port = Integer.parseInt(portStr); - - host = cmsState.getDeployProperty(CmsDeployProperty.HOST.getProperty()); - - Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH); - - try { - sshd = SshServer.setUpDefaultServer(); - sshd.setPort(port); - if (host != null) - sshd.setHost(host); - if (hostKeyPath == null) - throw new IllegalStateException("An SSH server key must be set"); - sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath)); - // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", - // "-l" })); -// String[] shellCommand = OS.LOCAL.getDefaultShellCommand(); - // FIXME transfer args -// sshd.setShellFactory(new ProcessShellFactory(shellCommand)); - sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE); - sshd.setCommandFactory(new ScpCommandFactory()); - - // tunnels - sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE); - // sshd.setForwardingFilter(ForwardingFilter.asForwardingFilter(null, null, - // TcpForwardingFilter.DEFAULT)); - // sshd.setForwarderFactory(DefaultForwarderFactory.INSTANCE); -// TcpForwardingFilter tcpForwardingFilter = sshd.getTcpForwardingFilter(); - sshd.addPortForwardingEventListener(new PortForwardingEventListener() { - - @Override - public void establishingExplicitTunnel(Session session, SshdSocketAddress local, - SshdSocketAddress remote, boolean localForwarding) throws IOException { - log.debug("Establishing tunnel " + local + ", " + remote); - } - - @Override - public void establishedExplicitTunnel(Session session, SshdSocketAddress local, - SshdSocketAddress remote, boolean localForwarding, SshdSocketAddress boundAddress, - Throwable reason) throws IOException { - log.debug("Established tunnel " + local + ", " + remote + ", " + boundAddress); - } - - @Override - public void establishingDynamicTunnel(Session session, SshdSocketAddress local) throws IOException { - log.debug("Establishing dynamic tunnel " + local); - } - - @Override - public void establishedDynamicTunnel(Session session, SshdSocketAddress local, - SshdSocketAddress boundAddress, Throwable reason) throws IOException { - log.debug("Established dynamic tunnel " + local); - } - - }); - - // Authentication - //sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true)); - sshd.setPublickeyAuthenticator(null); - // sshd.setKeyboardInteractiveAuthenticator(null); - JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator(); - jaasPasswordAuthenticator.setDomain(CmsAuth.NODE.getLoginContextName()); - sshd.setPasswordAuthenticator(jaasPasswordAuthenticator); - - Path krb5keyTab = cmsState.getDataPath("node/krb5.keytab"); - if (Files.exists(krb5keyTab)) { - // FIXME experimental - GSSAuthenticator gssAuthenticator = new GSSAuthenticator(); - gssAuthenticator.setKeytabFile(cmsState.getDataPath("node/krb5.keytab").toString()); - gssAuthenticator.setServicePrincipalName("HTTP@" + host); - sshd.setGSSAuthenticator(gssAuthenticator); - } - - // SFTP - sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); - - // start - sshd.start(); - - log.debug(() -> "CMS SSH server started on port " + port + (host != null ? " of host " + host : "")); - } catch (IOException e) { - throw new RuntimeException("Cannot start SSH server on port " + port, e); - } - - } - - public void stop() { - if (sshd == null) - return; - try { - sshd.stop(); - log.debug(() -> "CMS SSH server stopped on port " + port + (host != null ? " of host " + host : "")); - } catch (IOException e) { - throw new RuntimeException("Cannot stop SSH server", e); - } - - } - - public void setCmsState(CmsState cmsState) { - this.cmsState = cmsState; - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java deleted file mode 100644 index f6f447418..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Sftp.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.FileSystem; -import java.nio.file.Path; - -import org.apache.sshd.sftp.client.fs.SftpFileSystem; - -/** Create an SFTP {@link FileSystem}. */ -public class Sftp extends AbstractSsh { - private URI uri; - - private SftpFileSystem fileSystem; - - public Sftp(String username, String host, int port) { - this(AbstractSsh.toUri(username, host, port)); - } - - public Sftp(URI uri) { - this.uri = uri; - openSession(uri); - } - - public FileSystem getFileSystem() { - if (fileSystem == null) { - try { - authenticate(); - fileSystem = getSftpFileSystemProvider().newFileSystem(getSession()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - return fileSystem; - } - - public Path getBasePath() { - String p = uri.getPath() != null ? uri.getPath() : "/"; - return getFileSystem().getPath(p); - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java deleted file mode 100644 index 6e7b6ef2a..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/Ssh.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.argeo.cms.ssh; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; - -/** Create an SSH shell. */ -public class Ssh extends AbstractSsh { - private final URI uri; - - public Ssh(String username, String host, int port) { - this(AbstractSsh.toUri(username, host, port)); - } - - public Ssh(URI uri) { - this.uri = uri; - openSession(uri); - } - - public static void main(String[] args) { - Options options = getOptions(); - CommandLineParser parser = new DefaultParser(); - try { - CommandLine line = parser.parse(options, args); - List remaining = line.getArgList(); - if (remaining.size() == 0) { - System.err.println("There must be at least one argument"); - printHelp(options); - System.exit(1); - } - URI uri = new URI("ssh://" + remaining.get(0)); - List command = new ArrayList<>(); - if (remaining.size() > 1) { - for (int i = 1; i < remaining.size(); i++) { - command.add(remaining.get(i)); - } - } - - // auth - Ssh ssh = new Ssh(uri); - ssh.authenticate(); - - if (command.size() == 0) {// shell - AbstractSsh.openShell(ssh.getSession()); - } else {// execute command - - } - ssh.closeSession(); - } catch (Exception exp) { - exp.printStackTrace(); - printHelp(options); - System.exit(1); - } finally { - - } - } - - public URI getUri() { - return uri; - } - - public static Options getOptions() { - Options options = new Options(); -// options.addOption("p", true, "port"); - options.addOption(Option.builder("p").hasArg().argName("port").desc("port of the SSH server").build()); - - return options; - } - - public static void printHelp(Options options) { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("ssh [username@]hostname", options, true); - } -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java deleted file mode 100644 index f5cbb0450..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshKeyPair.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.RSAPrivateCrtKey; -import java.security.spec.RSAPublicKeySpec; - -import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.config.keys.PublicKeyEntry; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.openssl.PEMDecryptorProvider; -import org.bouncycastle.openssl.PEMEncryptedKeyPair; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.PKCS8Generator; -import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider; -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 loadDefault(char[] password) { - Path privateKeyPath = Paths.get(System.getProperty("user.home") + "/.ssh/id_rsa"); - // TODO try other formats - return load(privateKeyPath, password); - } - - public static SshKeyPair load(Path privateKeyPath, char[] password) { - try (Reader reader = Files.newBufferedReader(privateKeyPath)) { - return load(reader, password); - } catch (IOException e) { - throw new IllegalStateException("Cannot load private key from " + privateKeyPath, 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 PEMEncryptedKeyPair) { - PEMEncryptedKeyPair ekp = (PEMEncryptedKeyPair) object; - PEMDecryptorProvider decryptorProvider = new BcPEMDecryptorProvider(password); - PEMKeyPair pemKp = ekp.decryptKeyPair(decryptorProvider); - kp = converter.getKeyPair(pemKp); - } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) { - // Encrypted key - we will use provided password - PKCS8EncryptedPrivateKeyInfo ckp = (PKCS8EncryptedPrivateKeyInfo) object; -// PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password); - InputDecryptorProvider inputDecryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder() - .build(password); - PrivateKeyInfo pkInfo = ckp.decryptPrivateKeyInfo(inputDecryptorProvider); - PrivateKey privateKey = converter.getPrivateKey(pkInfo); - - // generate public key - RSAPrivateCrtKey privk = (RSAPrivateCrtKey) privateKey; - RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(privk.getModulus(), - privk.getPublicExponent()); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - - kp = new KeyPair(publicKey, privateKey); - } else { - // Unencrypted key - no password needed -// PKCS8EncryptedPrivateKeyInfo ukp = (PKCS8EncryptedPrivateKeyInfo) object; - PEMKeyPair pemKp = (PEMKeyPair) object; - kp = converter.getKeyPair(pemKp); - } - return new SshKeyPair(kp); - } catch (Exception e) { - throw new RuntimeException("Cannot load private key", e); - } - } - - public static void main(String args[]) { - Path privateKeyPath = Paths.get(System.getProperty("user.dir") + "/id_rsa"); - SshKeyPair skp = SshKeyPair.loadOrGenerate(privateKeyPath, 1024, null); - System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); - System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); - System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); - - StringReader reader = new StringReader(skp.getPrivateKeyAsPemString(null)); - skp = SshKeyPair.load(reader, null); - System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); - System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); - System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); - - reader = new StringReader(skp.getPrivateKeyAsPemString("demo".toCharArray())); - skp = SshKeyPair.load(reader, "demo".toCharArray()); - System.out.println("Public:\n" + skp.getPublicKeyAsOpenSshString()); - System.out.println("Private (plain):\n" + skp.getPrivateKeyAsPemString(null)); - System.out.println("Private (encrypted):\n" + skp.getPrivateKeyAsPemString("demo".toCharArray())); - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java deleted file mode 100644 index 71b636576..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/SshSync.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.argeo.cms.ssh; - -import java.io.Console; -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.security.KeyPair; -import java.util.Map; -import java.util.Scanner; - -import org.apache.commons.io.IOUtils; -import org.apache.sshd.agent.SshAgent; -import org.apache.sshd.agent.SshAgentFactory; -import org.apache.sshd.agent.local.LocalAgentFactory; -import org.apache.sshd.agent.unix.UnixAgentFactory; -import org.apache.sshd.client.SshClient; -import org.apache.sshd.client.config.keys.ClientIdentityLoader; -import org.apache.sshd.client.future.ConnectFuture; -import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.config.keys.FilePasswordProvider; -import org.apache.sshd.sftp.client.fs.SftpFileSystem; -import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider; -import org.argeo.api.cms.CmsLog; - -public class SshSync { - private final static CmsLog log = CmsLog.getLog(SshSync.class); - - public static void main(String[] args) { - - try (SshClient client = SshClient.setUpDefaultClient()) { - client.start(); - boolean osAgent = false; - SshAgentFactory agentFactory = osAgent ? new UnixAgentFactory() : new LocalAgentFactory(); - // SshAgentFactory agentFactory = new LocalAgentFactory(); - client.setAgentFactory(agentFactory); - SshAgent sshAgent = agentFactory.createClient(null, client); - - String login = System.getProperty("user.name"); - String host = "localhost"; - int port = 22; - - if (!osAgent) { - String keyPath = "/home/" + login + "/.ssh/id_rsa"; - - String password; - Console console = System.console(); - if (console != null) { - password = new String(console.readPassword(keyPath + ": ")); - } else { - System.out.print(keyPath + ": "); - try (Scanner s = new Scanner(System.in)) { - password = s.next(); - } - } - NamedResource namedResource = new NamedResource() { - - @Override - public String getName() { - return keyPath; - } - }; - KeyPair keyPair = ClientIdentityLoader.DEFAULT - .loadClientIdentities(null, namedResource, FilePasswordProvider.of(password)).iterator().next(); - sshAgent.addIdentity(keyPair, "NO COMMENT"); - } - -// List> identities = sshAgent.getIdentities(); -// for (Map.Entry entry : identities) { -// System.out.println(entry.getValue() + " : " + entry.getKey()); -// } - - ConnectFuture connectFuture = client.connect(login, host, port); - connectFuture.await(); - ClientSession session = connectFuture.getSession(); - - try { - -// session.addPasswordIdentity(new String(password)); - session.auth().verify(1000l); - - SftpFileSystemProvider fsProvider = new SftpFileSystemProvider(client); - - SftpFileSystem fs = fsProvider.newFileSystem(session); - Path testPath = fs.getPath("/home/" + login + "/tmp"); - Files.list(testPath).forEach(System.out::println); - test(testPath); - - } finally { - client.stop(); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - static void test(Path testBase) { - try { - Path testPath = testBase.resolve("ssh-test.txt"); - Files.createFile(testPath); - log.debug("Created file " + testPath); - Files.delete(testPath); - log.debug("Deleted " + testPath); - String txt = "TEST\nTEST2\n"; - byte[] arr = txt.getBytes(); - Files.write(testPath, arr); - log.debug("Wrote " + testPath); - byte[] read = Files.readAllBytes(testPath); - log.debug("Read " + testPath); - Path testDir = testBase.resolve("testDir"); - log.debug("Resolved " + testDir); - // Copy - Files.createDirectory(testDir); - log.debug("Created directory " + testDir); - Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); - log.debug("Created sub directories " + subsubdir); - Path copiedFile = testDir.resolve("copiedFile.txt"); - log.debug("Resolved " + copiedFile); - Path relativeCopiedFile = testDir.relativize(copiedFile); - log.debug("Relative copied file " + relativeCopiedFile); - try (OutputStream out = Files.newOutputStream(copiedFile); - InputStream in = Files.newInputStream(testPath)) { - IOUtils.copy(in, out); - } - log.debug("Copied " + testPath + " to " + copiedFile); - Files.delete(testPath); - log.debug("Deleted " + testPath); - byte[] copiedRead = Files.readAllBytes(copiedFile); - log.debug("Read " + copiedFile); - // Browse directories - DirectoryStream files = Files.newDirectoryStream(testDir); - int fileCount = 0; - Path listedFile = null; - for (Path file : files) { - fileCount++; - if (!Files.isDirectory(file)) - listedFile = file; - } - log.debug("Listed " + testDir); - // Generic attributes - Map attrs = Files.readAttributes(copiedFile, "*"); - log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshCli.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshCli.java deleted file mode 100644 index 12b4d5e1d..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshCli.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.argeo.cms.ssh.cli; - -import org.argeo.api.cli.CommandsCli; - -public class SshCli extends CommandsCli { - public SshCli(String commandName) { - super(commandName); - addCommand("shell", new SshShell()); - } - - @Override - public String getDescription() { - return "SSH utilities."; - } - - -} diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshShell.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshShell.java deleted file mode 100644 index 78903a779..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/cli/SshShell.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.argeo.cms.ssh.cli; - -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.net.URI; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.util.List; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -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.config.keys.ClientIdentityLoader; -import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.config.keys.FilePasswordProvider; -import org.argeo.api.cli.DescribedCommand; -import org.argeo.cms.ssh.AbstractSsh; -import org.argeo.cms.ssh.Ssh; - -public class SshShell implements DescribedCommand { - private Option portOption; - - @Override - public Options getOptions() { - Options options = new Options(); - portOption = Option.builder().option("p").longOpt("port").hasArg().desc("port to connect to").build(); - options.addOption(portOption); - return options; - } - - @Override - public String apply(List args) { - CommandLine cl = toCommandLine(args); - String portStr = cl.getOptionValue(portOption); - if (portStr == null) - portStr = "22"; - - String host = cl.getArgList().get(0); - - String uriStr = "ssh://" + host + ":" + portStr + "/"; - // System.out.println(uriStr); - URI uri = URI.create(uriStr); - - Ssh ssh = null; - try { - ssh = new Ssh(uri); - boolean osAgent; - SshAgent sshAgent; - try { - String sshAuthSockentEnv = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME); - if (sshAuthSockentEnv != null) { - ssh.getSshClient().getProperties().put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, sshAuthSockentEnv); - SshAgentFactory agentFactory = new UnixAgentFactory(); - ssh.getSshClient().setAgentFactory(agentFactory); - sshAgent = agentFactory.createClient(null, ssh.getSshClient()); - osAgent = true; - } else { - osAgent = false; - } - } catch (Exception e) { - e.printStackTrace(); - osAgent = false; - } - - if (!osAgent) { - SshAgentFactory agentFactory = new LocalAgentFactory(); - ssh.getSshClient().setAgentFactory(agentFactory); - sshAgent = agentFactory.createClient(null, ssh.getSshClient()); - String keyPath = System.getProperty("user.home") + "/.ssh/id_rsa"; - - char[] keyPassword = AbstractSsh.readPassword(); - NamedResource namedResource = new NamedResource() { - - @Override - public String getName() { - return keyPath; - } - }; - KeyPair keyPair = ClientIdentityLoader.DEFAULT - .loadClientIdentities(null, namedResource, FilePasswordProvider.of(new String(keyPassword))) - .iterator().next(); - sshAgent.addIdentity(keyPair, "NO COMMENT"); - } - -// char[] keyPassword = AbstractSsh.readPassword(); -// SshKeyPair keyPair = SshKeyPair.loadDefault(keyPassword); -// Arrays.fill(keyPassword, '*'); -// ssh.setSshKeyPair(keyPair); -// ssh.authenticate(); - ssh.verifyAuth(); - - long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime(); - System.out.println("Ssh available in " + jvmUptime + " ms."); - - AbstractSsh.openShell(ssh); - } catch (IOException | GeneralSecurityException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - if (ssh != null) - ssh.closeSession(); - } - return null; - } - - @Override - public String getDescription() { - return "Launch a static CMS."; - } - - } \ No newline at end of file diff --git a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java b/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java deleted file mode 100644 index 9555662ad..000000000 --- a/org.argeo.cms.ssh/src/org/argeo/cms/ssh/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** SSH support. */ -package org.argeo.cms.ssh; \ No newline at end of file diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FileSync.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FileSync.java new file mode 100644 index 000000000..91279c53d --- /dev/null +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FileSync.java @@ -0,0 +1,103 @@ +package org.argeo.cms.ux.cli; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.argeo.api.cli.CommandArgsException; +import org.argeo.api.cli.DescribedCommand; +import org.argeo.cms.acr.fs.PathSync; +import org.argeo.cms.acr.fs.SyncResult; + +public class FileSync implements DescribedCommand> { + final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build(); + final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories") + .build(); + final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress") + .build(); + + @Override + public SyncResult apply(List t) { + try { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + URI sourceUri = new URI(remaining.get(0)); + URI targetUri; + if (remaining.size() == 1) { + targetUri = Paths.get(System.getProperty("user.dir")).toUri(); + } else { + targetUri = new URI(remaining.get(1)); + } + boolean delete = line.hasOption(deleteOption.getLongOpt()); + boolean recursive = line.hasOption(recursiveOption.getLongOpt()); + PathSync pathSync = new PathSync(sourceUri, targetUri, delete, recursive); + return pathSync.call(); + } catch (URISyntaxException e) { + throw new CommandArgsException(e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(recursiveOption); + options.addOption(deleteOption); + options.addOption(progressOption); + return options; + } + + @Override + public String getUsage() { + return "[source URI] [target URI]"; + } + + public static void main(String[] args) { + DescribedCommand.mainImpl(new FileSync(), args); +// Options options = new Options(); +// options.addOption("r", "recursive", false, "recurse into directories"); +// options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); +// +// CommandLineParser parser = new DefaultParser(); +// try { +// CommandLine line = parser.parse(options, args); +// List remaining = line.getArgList(); +// if (remaining.size() == 0) { +// System.err.println("There must be at least one argument"); +// printHelp(options); +// System.exit(1); +// } +// URI sourceUri = new URI(remaining.get(0)); +// URI targetUri; +// if (remaining.size() == 1) { +// targetUri = Paths.get(System.getProperty("user.dir")).toUri(); +// } else { +// targetUri = new URI(remaining.get(1)); +// } +// PathSync pathSync = new PathSync(sourceUri, targetUri); +// pathSync.run(); +// } catch (Exception exp) { +// exp.printStackTrace(); +// printHelp(options); +// System.exit(1); +// } + } + +// public static void printHelp(Options options) { +// HelpFormatter formatter = new HelpFormatter(); +// formatter.printHelp("sync SRC [DEST]", options, true); +// } + + @Override + public String getDescription() { + return "Synchronises files"; + } + +} diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FsCommands.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FsCommands.java new file mode 100644 index 000000000..97d8c85f0 --- /dev/null +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/cli/FsCommands.java @@ -0,0 +1,18 @@ +package org.argeo.cms.ux.cli; + +import org.argeo.api.cli.CommandsCli; + +/** File utilities. */ +public class FsCommands extends CommandsCli { + + public FsCommands(String commandName) { + super(commandName); + addCommand("sync", new FileSync()); + } + + @Override + public String getDescription() { + return "Utilities around files and file systems"; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java deleted file mode 100644 index 397caea34..000000000 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.argeo.cms.acr.fs; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.argeo.api.cli.CommandArgsException; -import org.argeo.api.cli.DescribedCommand; - -public class FileSync implements DescribedCommand> { - final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build(); - final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories") - .build(); - final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress") - .build(); - - @Override - public SyncResult apply(List t) { - try { - CommandLine line = toCommandLine(t); - List remaining = line.getArgList(); - if (remaining.size() == 0) { - throw new CommandArgsException("There must be at least one argument"); - } - URI sourceUri = new URI(remaining.get(0)); - URI targetUri; - if (remaining.size() == 1) { - targetUri = Paths.get(System.getProperty("user.dir")).toUri(); - } else { - targetUri = new URI(remaining.get(1)); - } - boolean delete = line.hasOption(deleteOption.getLongOpt()); - boolean recursive = line.hasOption(recursiveOption.getLongOpt()); - PathSync pathSync = new PathSync(sourceUri, targetUri, delete, recursive); - return pathSync.call(); - } catch (URISyntaxException e) { - throw new CommandArgsException(e); - } - } - - @Override - public Options getOptions() { - Options options = new Options(); - options.addOption(recursiveOption); - options.addOption(deleteOption); - options.addOption(progressOption); - return options; - } - - @Override - public String getUsage() { - return "[source URI] [target URI]"; - } - - public static void main(String[] args) { - DescribedCommand.mainImpl(new FileSync(), args); -// Options options = new Options(); -// options.addOption("r", "recursive", false, "recurse into directories"); -// options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); -// -// CommandLineParser parser = new DefaultParser(); -// try { -// CommandLine line = parser.parse(options, args); -// List remaining = line.getArgList(); -// if (remaining.size() == 0) { -// System.err.println("There must be at least one argument"); -// printHelp(options); -// System.exit(1); -// } -// URI sourceUri = new URI(remaining.get(0)); -// URI targetUri; -// if (remaining.size() == 1) { -// targetUri = Paths.get(System.getProperty("user.dir")).toUri(); -// } else { -// targetUri = new URI(remaining.get(1)); -// } -// PathSync pathSync = new PathSync(sourceUri, targetUri); -// pathSync.run(); -// } catch (Exception exp) { -// exp.printStackTrace(); -// printHelp(options); -// System.exit(1); -// } - } - -// public static void printHelp(Options options) { -// HelpFormatter formatter = new HelpFormatter(); -// formatter.printHelp("sync SRC [DEST]", options, true); -// } - - @Override - public String getDescription() { - return "Synchronises files"; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java deleted file mode 100644 index 088c1c352..000000000 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.argeo.cms.acr.fs; - -import org.argeo.api.cli.CommandsCli; - -/** File utilities. */ -public class FsCommands extends CommandsCli { - - public FsCommands(String commandName) { - super(commandName); - addCommand("sync", new FileSync()); - } - - @Override - public String getDescription() { - return "Utilities around files and file systems"; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index e8a0dc2b7..9e00ed458 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -27,7 +27,6 @@ import org.argeo.cms.internal.auth.ImpliedByPrincipal; import org.argeo.cms.internal.http.WebCmsSessionImpl; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.osgi.useradmin.AuthenticatingUser; -import org.osgi.service.http.HttpContext; import org.osgi.service.useradmin.Authorization; /** Centralises security related registrations. */ @@ -140,8 +139,8 @@ class CmsAuthUtils { String httpSessId = httpSession.getId(); boolean anonymous = authorization.getName() == null; String remoteUser = !anonymous ? authorization.getName() : CmsConstants.ROLE_ANONYMOUS; - request.setAttribute(HttpContext.REMOTE_USER, remoteUser); - request.setAttribute(HttpContext.AUTHORIZATION, authorization); + request.setAttribute(RemoteAuthRequest.REMOTE_USER, remoteUser); + request.setAttribute(RemoteAuthRequest.AUTHORIZATION, authorization); CmsSessionImpl cmsSession; CmsSessionImpl currentLocalSession = CmsContextImpl.getCmsContext().getCmsSessionByLocalId(httpSessId); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthRequest.java b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthRequest.java index 2d1d14b4e..be5d0e15e 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthRequest.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthRequest.java @@ -4,6 +4,9 @@ import java.util.Locale; /** Transitional interface to decouple from the Servlet API. */ public interface RemoteAuthRequest { + final static String REMOTE_USER = "org.osgi.service.http.authentication.remote.user"; + final static String AUTHORIZATION = "org.osgi.service.useradmin.authorization"; + RemoteAuthSession getSession(); RemoteAuthSession createSession(); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java index 3abcf8c94..8f0509690 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java @@ -14,13 +14,11 @@ import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; -import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.cms.CmsDeployProperty; import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.cms.internal.runtime.CmsStateImpl; -import org.osgi.service.http.HttpContext; import org.osgi.service.useradmin.Authorization; /** Use the HTTP session as the basis for authentication. */ @@ -79,7 +77,7 @@ public class RemoteSessionLoginModule implements LoginModule { log.trace("Retrieved authorization from " + cmsSession); } } else { - authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION); + authorization = (Authorization) request.getAttribute(RemoteAuthRequest.AUTHORIZATION); if (authorization == null) {// search by session ID RemoteAuthSession httpSession = request.getSession(); if (httpSession == null) { @@ -110,7 +108,7 @@ public class RemoteSessionLoginModule implements LoginModule { } else { if (log.isTraceEnabled()) log.trace("HTTP login: " + true); - request.setAttribute(HttpContext.AUTHORIZATION, authorization); + request.setAttribute(RemoteAuthRequest.AUTHORIZATION, authorization); return true; } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java index 2353250c5..1b94bdad1 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java @@ -4,7 +4,6 @@ import org.argeo.api.cms.CmsDeployment; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.cms.CmsDeployProperty; -import org.osgi.service.http.HttpService; /** Implementation of a CMS deployment. */ public class CmsDeploymentImpl implements CmsDeployment { @@ -12,7 +11,7 @@ public class CmsDeploymentImpl implements CmsDeployment { // Readiness private boolean httpExpected = false; - private HttpService httpService; +// private HttpService httpService; private CmsState cmsState; // private DeployConfig deployConfig; @@ -40,9 +39,9 @@ public class CmsDeploymentImpl implements CmsDeployment { // return deployConfig.getProps(factoryPid, cn); // } - public boolean isHttpAvailableOrNotExpected() { - return (httpExpected ? httpService != null : true); - } +// public boolean isHttpAvailableOrNotExpected() { +// return (httpExpected ? httpService != null : true); +// } // private void loadIpaJaasConfiguration() { // if (System.getProperty(KernelConstants.JAAS_CONFIG_PROP) == null) { @@ -70,8 +69,8 @@ public class CmsDeploymentImpl implements CmsDeployment { httpExpected = httpPort != null || httpsPort != null; } - public void setHttpService(HttpService httpService) { - this.httpService = httpService; - } +// public void setHttpService(HttpService httpService) { +// this.httpService = httpService; +// } }