From: Mathieu Baudier Date: Sun, 10 Jul 2022 10:17:08 +0000 (+0200) Subject: Rename CMS EE4J X-Git-Tag: v2.3.10~125 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=00753f77ac3f41f7dbfe281eeab886ef4bdc0ce5;p=lgpl%2Fargeo-commons.git Rename CMS EE4J --- diff --git a/Makefile b/Makefile index 4de934b22..ab2d62548 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ org.argeo.api.cli \ org.argeo.api.cms \ org.argeo.cms \ org.argeo.cms.ux \ -org.argeo.cms.ee4j \ +org.argeo.cms.ee \ org.argeo.cms.lib.jetty \ org.argeo.cms.lib.equinox \ org.argeo.cms.lib.sshd \ diff --git a/org.argeo.cms.ee/.classpath b/org.argeo.cms.ee/.classpath new file mode 100644 index 000000000..e801ebfb4 --- /dev/null +++ b/org.argeo.cms.ee/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.ee/.project b/org.argeo.cms.ee/.project new file mode 100644 index 000000000..4b68cdd92 --- /dev/null +++ b/org.argeo.cms.ee/.project @@ -0,0 +1,33 @@ + + + org.argeo.cms.ee + + + + + + 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.ee/OSGI-INF/pkgServlet.xml b/org.argeo.cms.ee/OSGI-INF/pkgServlet.xml new file mode 100644 index 000000000..00fcaff99 --- /dev/null +++ b/org.argeo.cms.ee/OSGI-INF/pkgServlet.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.ee/OSGI-INF/pkgServletContext.xml b/org.argeo.cms.ee/OSGI-INF/pkgServletContext.xml new file mode 100644 index 000000000..7540a2cdb --- /dev/null +++ b/org.argeo.cms.ee/OSGI-INF/pkgServletContext.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.ee/bnd.bnd b/org.argeo.cms.ee/bnd.bnd new file mode 100644 index 000000000..6fae1ea24 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/build.properties b/org.argeo.cms.ee/build.properties new file mode 100644 index 000000000..ee94f53be --- /dev/null +++ b/org.argeo.cms.ee/build.properties @@ -0,0 +1,5 @@ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/jettyServiceFactory.xml +source.. = src/ diff --git a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java new file mode 100644 index 000000000..fb289c18e --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/CmsLoginServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsLoginServlet.java new file mode 100644 index 000000000..29a3137bb --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/CmsLogoutServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsLogoutServlet.java new file mode 100644 index 000000000..0628eae36 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsPrivateServletContext.java new file mode 100644 index 000000000..cec04d230 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsSessionDescriptor.java new file mode 100644 index 000000000..30de616a2 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java new file mode 100644 index 000000000..983202ad2 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/TokenDescriptor.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/TokenDescriptor.java new file mode 100644 index 000000000..1541b4f29 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/integration/package-info.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/package-info.java new file mode 100644 index 000000000..1405737ee --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/CmsServletContext.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java new file mode 100644 index 000000000..9cb48b212 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java new file mode 100644 index 000000000..3bea0b4de --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/ServletHttpRequest.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/ServletHttpRequest.java new file mode 100644 index 000000000..54c880435 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/ServletHttpResponse.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/ServletHttpResponse.java new file mode 100644 index 000000000..de47365ca --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/ServletHttpSession.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/ServletHttpSession.java new file mode 100644 index 000000000..8d087daa7 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/internal/HttpUtils.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/HttpUtils.java new file mode 100644 index 000000000..70f2cc6b0 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java new file mode 100644 index 000000000..c762b67ec --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/servlet/internal/RobotServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/RobotServlet.java new file mode 100644 index 000000000..288ee268c --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java new file mode 100644 index 000000000..46dabc28e --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java new file mode 100644 index 000000000..e01f6f721 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java new file mode 100644 index 000000000..819837b49 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/websocket/javax/server/WebSocketView.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/javax/server/WebSocketView.java new file mode 100644 index 000000000..a5da88be9 --- /dev/null +++ b/org.argeo.cms.ee/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.ee/src/org/argeo/cms/websocket/javax/server/package-info.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/javax/server/package-info.java new file mode 100644 index 000000000..564c881bc --- /dev/null +++ b/org.argeo.cms.ee/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.ee4j/.classpath b/org.argeo.cms.ee4j/.classpath deleted file mode 100644 index e801ebfb4..000000000 --- a/org.argeo.cms.ee4j/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.argeo.cms.ee4j/.project b/org.argeo.cms.ee4j/.project deleted file mode 100644 index 9140addc8..000000000 --- a/org.argeo.cms.ee4j/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - 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 deleted file mode 100644 index 00fcaff99..000000000 --- a/org.argeo.cms.ee4j/OSGI-INF/pkgServlet.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml b/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml deleted file mode 100644 index 7540a2cdb..000000000 --- a/org.argeo.cms.ee4j/OSGI-INF/pkgServletContext.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/org.argeo.cms.ee4j/bnd.bnd b/org.argeo.cms.ee4j/bnd.bnd deleted file mode 100644 index 6fae1ea24..000000000 --- a/org.argeo.cms.ee4j/bnd.bnd +++ /dev/null @@ -1,11 +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/pkgServletContext.xml,\ -OSGI-INF/pkgServlet.xml diff --git a/org.argeo.cms.ee4j/build.properties b/org.argeo.cms.ee4j/build.properties deleted file mode 100644 index ee94f53be..000000000 --- a/org.argeo.cms.ee4j/build.properties +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index fb289c18e..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLoginServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLoginServlet.java deleted file mode 100644 index 29a3137bb..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLogoutServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsLogoutServlet.java deleted file mode 100644 index 0628eae36..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsPrivateServletContext.java deleted file mode 100644 index cec04d230..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsSessionDescriptor.java deleted file mode 100644 index 30de616a2..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/CmsTokenServlet.java deleted file mode 100644 index 983202ad2..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/integration/TokenDescriptor.java b/org.argeo.cms.ee4j/src/org/argeo/cms/integration/TokenDescriptor.java deleted file mode 100644 index 1541b4f29..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 1405737ee..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/CmsServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/CmsServletContext.java deleted file mode 100644 index 9cb48b212..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/PrivateWwwAuthServletContext.java deleted file mode 100644 index 3bea0b4de..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpRequest.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpRequest.java deleted file mode 100644 index 54c880435..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpResponse.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpResponse.java deleted file mode 100644 index de47365ca..000000000 --- a/org.argeo.cms.ee4j/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/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpSession.java b/org.argeo.cms.ee4j/src/org/argeo/cms/servlet/ServletHttpSession.java deleted file mode 100644 index 8d087daa7..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 70f2cc6b0..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index c762b67ec..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 288ee268c..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 46dabc28e..000000000 --- a/org.argeo.cms.ee4j/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; - -/** - * 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 deleted file mode 100644 index e01f6f721..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 819837b49..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index a5da88be9..000000000 --- a/org.argeo.cms.ee4j/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/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 deleted file mode 100644 index 564c881bc..000000000 --- a/org.argeo.cms.ee4j/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