From: Mathieu Baudier Date: Mon, 26 Jan 2015 21:26:25 +0000 (+0000) Subject: Remoting working X-Git-Tag: argeo-commons-2.1.30~432 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=c322e016196139f7f4fb9192e5b1e773999143d0;p=lgpl%2Fargeo-commons.git Remoting working git-svn-id: https://svn.argeo.org/commons/trunk@7707 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- diff --git a/dep/org.argeo.security.dep.node/pom.xml b/dep/org.argeo.security.dep.node/pom.xml index 27efb4fa1..acef7d17c 100644 --- a/dep/org.argeo.security.dep.node/pom.xml +++ b/dep/org.argeo.security.dep.node/pom.xml @@ -45,11 +45,11 @@ - - org.argeo.commons - org.argeo.node.repo.jackrabbit - 2.1.13-SNAPSHOT - + + + + + @@ -78,11 +78,11 @@ - - org.argeo.commons - org.argeo.security.dao.jackrabbit - 2.1.13-SNAPSHOT - + + + + + diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/HttpFilter.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/HttpFilter.java new file mode 100644 index 000000000..7c2151ebb --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/HttpFilter.java @@ -0,0 +1,38 @@ +package org.argeo.cms.internal.kernel; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** Abstract base class for http filters. */ +abstract class HttpFilter implements Filter { + protected abstract void doFilter(HttpSession httpSession, + HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException; + + @Override + public void doFilter(ServletRequest servletRequest, + ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + doFilter(request.getSession(), request, + (HttpServletResponse) servletResponse, filterChain); + } + + @Override + public void destroy() { + } + + @Override + public void init(FilterConfig arg0) throws ServletException { + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Kernel.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Kernel.java index c813a9a37..c38a4b29a 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Kernel.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Kernel.java @@ -27,9 +27,9 @@ final class Kernel { private final BundleContext bundleContext; private JackrabbitNode node; - private OsgiJackrabbitRepositoryFactory repositoryFactory; + private RepositoryFactory repositoryFactory; private NodeSecurity nodeSecurity; - private NodeHttpFilter httpFilter; + private NodeHttp nodeHttp; Kernel(BundleContext bundleContext) { this.bundleContext = bundleContext; @@ -45,14 +45,14 @@ final class Kernel { node = new JackrabbitNode(bundleContext); repositoryFactory = new OsgiJackrabbitRepositoryFactory(); nodeSecurity = new NodeSecurity(bundleContext, node); - httpFilter = new NodeHttpFilter(bundleContext, nodeSecurity); + nodeHttp = new NodeHttp(bundleContext, node, nodeSecurity); // Publish services to OSGi register nodeSecurity.publish(); node.publish(); bundleContext.registerService(RepositoryFactory.class, repositoryFactory, null); - httpFilter.publish(); + nodeHttp.publish(); } catch (Exception e) { log.error("Cannot initialize Argeo CMS", e); throw new ArgeoException("Cannot initialize", e); @@ -67,7 +67,7 @@ final class Kernel { void destroy() { long begin = System.currentTimeMillis(); - httpFilter = null; + nodeHttp = null; nodeSecurity.destroy(); node.destroy(); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java index d36f7a51d..4d53917b4 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java @@ -12,7 +12,22 @@ interface KernelConstants { final static String[] DEFAULT_CNDS = { "/org/argeo/jcr/argeo.cnd", "/org/argeo/cms/cms.cnd" }; - + // Security final static String DEFAULT_SECURITY_KEY = "argeo"; + final static String ANONYMOUS_USER = "anonymous"; + final static String ADMIN_USER = "root"; + + // Roles + final static String ROLE_USER = "ROLE_USER"; + final static String ROLE_ADMIN = "ROLE_ADMIN"; + final static String ROLE_ANONYMOUS = "ROLE_ANONYMOUS"; + + // DAV + final static String WEBDAV_CONFIG = "/org/argeo/cms/internal/kernel/webdav-config.xml"; + final static String PATH_WEBDAV_PUBLIC = "/data/public"; + final static String PATH_WEBDAV_PRIVATE = "/data/files"; + final static String PATH_REMOTING_PUBLIC = "/data/pub"; + final static String PATH_REMOTING_PRIVATE = "/data/jcr"; + final static String PATH_WORKBENCH_PUBLIC = "/ui/public"; } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java index c3297cba8..ba2a352a7 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java @@ -2,14 +2,27 @@ package org.argeo.cms.internal.kernel; import java.io.File; import java.io.IOException; +import java.util.Collections; import java.util.Dictionary; +import java.util.Enumeration; import java.util.Hashtable; +import java.util.List; import java.util.Properties; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; import org.argeo.cms.CmsException; import org.osgi.framework.BundleContext; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; -class KernelUtils { +class KernelUtils implements KernelConstants { final static String OSGI_INSTANCE_AREA = "osgi.instance.area"; static Dictionary asDictionary(Properties props) { @@ -20,8 +33,7 @@ class KernelUtils { return hashtable; } - static Dictionary asDictionary(ClassLoader cl, - String resource) { + static Dictionary asDictionary(ClassLoader cl, String resource) { Properties props = new Properties(); try { props.load(cl.getResourceAsStream(resource)); @@ -37,6 +49,36 @@ class KernelUtils { .substring("file:".length())).getAbsoluteFile(); } + // Security + static void anonymousLogin(AuthenticationManager authenticationManager) { + try { + List anonAuthorities = Collections + .singletonList(new SimpleGrantedAuthority(ROLE_ANONYMOUS)); + UserDetails anonUser = new User(ANONYMOUS_USER, "", true, true, + true, true, anonAuthorities); + AnonymousAuthenticationToken anonToken = new AnonymousAuthenticationToken( + DEFAULT_SECURITY_KEY, anonUser, anonAuthorities); + Authentication authentication = authenticationManager + .authenticate(anonToken); + SecurityContextHolder.getContext() + .setAuthentication(authentication); + } catch (Exception e) { + throw new CmsException("Cannot authenticate", e); + } + } + + // HTTP + static void logRequestHeaders(Log 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); + } + } + private KernelUtils() { } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java new file mode 100644 index 000000000..c0383f543 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java @@ -0,0 +1,289 @@ +package org.argeo.cms.internal.kernel; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; + +import javax.servlet.FilterChain; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsException; +import org.argeo.cms.internal.kernel.NodeHttp.AnonymousFilter; +import org.argeo.cms.internal.kernel.NodeHttp.DavFilter; +import org.argeo.jackrabbit.servlet.OpenInViewSessionProvider; +import org.argeo.jackrabbit.servlet.RemotingServlet; +import org.argeo.jackrabbit.servlet.WebdavServlet; +import org.argeo.jcr.ArgeoJcrConstants; +import org.eclipse.equinox.http.servlet.ExtendedHttpService; +import org.osgi.framework.BundleContext; +import org.osgi.service.http.NamespaceException; +import org.osgi.util.tracker.ServiceTracker; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Intercepts and enriches http access, mainly focusing on security and + * transactionality. + */ +class NodeHttp implements KernelConstants, ArgeoJcrConstants { + private final static Log log = LogFactory.getLog(NodeHttp.class); + + private final static String ATTR_AUTH = "auth"; + private final static String HEADER_AUTHORIZATION = "Authorization"; + private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + + static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final AuthenticationManager authenticationManager; + private final BundleContext bundleContext; + private ExtendedHttpService httpService; + + // FIXME Make it more unique + private String httpAuthRealm = "Argeo"; + + // Filters + private final RootFilter rootFilter; + + // remoting + private OpenInViewSessionProvider sessionProvider; + private WebdavServlet publicWebdavServlet; + private WebdavServlet privateWebdavServlet; + private RemotingServlet publicRemotingServlet; + private RemotingServlet privateRemotingServlet; + + NodeHttp(BundleContext bundleContext, JackrabbitNode node, + NodeSecurity authenticationManager) { + this.bundleContext = bundleContext; + this.authenticationManager = authenticationManager; + + // Equinox dependency + ServiceTracker st = new ServiceTracker( + bundleContext, ExtendedHttpService.class, null); + st.open(); + try { + httpService = st.waitForService(1000); + } catch (InterruptedException e) { + httpService = null; + } + + if (httpService == null) + throw new CmsException("Could not find " + + ExtendedHttpService.class + " service."); + + // Filters + rootFilter = new RootFilter(); + + // DAV + sessionProvider = new OpenInViewSessionProvider(); + publicWebdavServlet = new WebdavServlet(node, sessionProvider); + privateWebdavServlet = new WebdavServlet(node, sessionProvider); + publicRemotingServlet = new RemotingServlet(node, sessionProvider); + privateRemotingServlet = new RemotingServlet(node, sessionProvider); + } + + void publish() { + try { + registerWebdavServlet(PATH_WEBDAV_PUBLIC, ALIAS_NODE, true, + publicWebdavServlet); + registerWebdavServlet(PATH_WEBDAV_PRIVATE, ALIAS_NODE, false, + privateWebdavServlet); + registerRemotingServlet(PATH_REMOTING_PUBLIC, ALIAS_NODE, true, + publicRemotingServlet); + registerRemotingServlet(PATH_REMOTING_PRIVATE, ALIAS_NODE, false, + privateRemotingServlet); + + // httpService.registerFilter("/", rootFilter, null, null); + } catch (Exception e) { + throw new CmsException("Cannot publish HTTP services to OSGi", e); + } + } + + private void registerWebdavServlet(String pathPrefix, String alias, + Boolean anonymous, WebdavServlet webdavServlet) + throws NamespaceException, ServletException { + String path = pathPrefix + "/" + alias; + Properties initParameters = new Properties(); + initParameters.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, + KernelConstants.WEBDAV_CONFIG); + initParameters.setProperty( + WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); + httpService.registerFilter(path, anonymous ? new AnonymousFilter() + : new DavFilter(), null, null); + // Cast to servlet because of a weird behaviour in Eclipse + httpService.registerServlet(path, (Servlet) webdavServlet, + initParameters, null); + } + + private void registerRemotingServlet(String pathPrefix, String alias, + Boolean anonymous, RemotingServlet remotingServlet) + throws NamespaceException, ServletException { + String path = pathPrefix + "/" + alias; + Properties initParameters = new Properties(); + initParameters.setProperty( + RemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); + + // Looks like a bug in Jackrabbit remoting init + initParameters.setProperty(RemotingServlet.INIT_PARAM_HOME, + KernelUtils.getOsgiInstanceDir(bundleContext) + + "/tmp/jackrabbit"); + initParameters.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, + "remoting"); + // Cast to servlet because of a weird behaviour in Eclipse + httpService.registerFilter(path, anonymous ? new AnonymousFilter() + : new DavFilter(), null, null); + httpService.registerServlet(path, (Servlet) remotingServlet, + initParameters, null); + } + + private Boolean isSessionAuthenticated(HttpSession httpSession) { + SecurityContext contextFromSession = (SecurityContext) httpSession + .getAttribute(SPRING_SECURITY_CONTEXT_KEY); + return contextFromSession != null; + } + + private void requestBasicAuth(HttpSession httpSession, + HttpServletResponse response) { + response.setStatus(401); + response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" + + httpAuthRealm + "\""); + httpSession.setAttribute(ATTR_AUTH, Boolean.TRUE); + } + + private UsernamePasswordAuthenticationToken basicAuth(String authHeader) { + if (authHeader != null) { + StringTokenizer st = new StringTokenizer(authHeader); + if (st.hasMoreTokens()) { + String basic = st.nextToken(); + if (basic.equalsIgnoreCase("Basic")) { + try { + String credentials = new String(Base64.decodeBase64(st + .nextToken()), "UTF-8"); + log.debug("Credentials: " + credentials); + int p = credentials.indexOf(":"); + if (p != -1) { + String login = credentials.substring(0, p).trim(); + String password = credentials.substring(p + 1) + .trim(); + + return new UsernamePasswordAuthenticationToken( + login, password); + } else { + throw new CmsException( + "Invalid authentication token"); + } + } catch (Exception e) { + throw new CmsException( + "Couldn't retrieve authentication", e); + } + } + } + } + throw new CmsException("Couldn't retrieve authentication"); + } + + /** Intercepts all requests. Authenticates. */ + class RootFilter extends HttpFilter { + + @Override + public void doFilter(HttpSession httpSession, + HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + // Authenticate from session + if (isSessionAuthenticated(httpSession)) { + filterChain.doFilter(request, response); + return; + } + + // TODO Kerberos + + // TODO Certificate + + // Process basic auth + String basicAuth = request.getHeader(HEADER_AUTHORIZATION); + if (basicAuth != null) { + UsernamePasswordAuthenticationToken token = basicAuth(basicAuth); + Authentication auth = authenticationManager.authenticate(token); + SecurityContextHolder.getContext().setAuthentication(auth); + httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, + SecurityContextHolder.getContext()); + httpSession.setAttribute(ATTR_AUTH, Boolean.FALSE); + filterChain.doFilter(request, response); + return; + } + + Boolean doBasicAuth = true; + if (doBasicAuth) { + requestBasicAuth(httpSession, response); + // skip filter chain + return; + } + + // TODO Login page + + // Anonymous + KernelUtils.anonymousLogin(authenticationManager); + filterChain.doFilter(request, response); + } + } + + /** Intercepts all requests. Authenticates. */ + class AnonymousFilter extends HttpFilter { + @Override + public void doFilter(HttpSession httpSession, + HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + // Authenticate from session + if (isSessionAuthenticated(httpSession)) { + filterChain.doFilter(request, response); + return; + } + + KernelUtils.anonymousLogin(authenticationManager); + filterChain.doFilter(request, response); + } + } + + /** Intercepts all requests. Authenticates. */ + class DavFilter extends HttpFilter { + + @Override + public void doFilter(HttpSession httpSession, + HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + // Authenticate from session + if (isSessionAuthenticated(httpSession)) { + filterChain.doFilter(request, response); + return; + } + + // Process basic auth + String basicAuth = request.getHeader(HEADER_AUTHORIZATION); + if (basicAuth != null) { + UsernamePasswordAuthenticationToken token = basicAuth(basicAuth); + Authentication auth = authenticationManager.authenticate(token); + SecurityContextHolder.getContext().setAuthentication(auth); + httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, + SecurityContextHolder.getContext()); + httpSession.setAttribute(ATTR_AUTH, Boolean.FALSE); + filterChain.doFilter(request, response); + return; + } + + requestBasicAuth(httpSession, response); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttpFilter.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttpFilter.java deleted file mode 100644 index a32228a73..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttpFilter.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.argeo.cms.internal.kernel; - -import java.io.IOException; -import java.util.StringTokenizer; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.cms.CmsException; -import org.eclipse.equinox.http.servlet.ExtendedHttpService; -import org.osgi.framework.BundleContext; -import org.osgi.service.http.HttpContext; -import org.osgi.util.tracker.ServiceTracker; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -class NodeHttpFilter implements Filter { - private final static Log log = LogFactory.getLog(NodeHttpFilter.class); - - private final static String ATTR_AUTH = "auth"; - private final static String HEADER_AUTHORIZATION = "Authorization"; - - static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private ExtendedHttpService httpService; - private final AuthenticationManager authenticationManager; - - private Boolean basicAuthEnabled = false; - - NodeHttpFilter(BundleContext bundleContext, - AuthenticationManager authenticationManager) { - this.authenticationManager = authenticationManager; - - // Equinox dependency - ServiceTracker st = new ServiceTracker( - bundleContext, ExtendedHttpService.class, null); - st.open(); - try { - httpService = st.waitForService(1000); - } catch (InterruptedException e) { - httpService = null; - } - - if (httpService == null) - throw new CmsException("Could not find " - + ExtendedHttpService.class + " service."); - } - - void publish() { - try { - HttpContext httpContext = httpService.createDefaultHttpContext(); - httpService.registerFilter("/", this, null, httpContext); - } catch (Exception e) { - throw new CmsException("Cannot register HTTP filter", e); - } - } - - @Override - public void doFilter(ServletRequest servletRequest, - ServletResponse servletResponse, FilterChain filterChain) - throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpSession httpSession = request.getSession(); - - // Authenticate from session - SecurityContext contextFromSession = (SecurityContext) httpSession - .getAttribute(SPRING_SECURITY_CONTEXT_KEY); - if (contextFromSession != null) { - filterChain.doFilter(servletRequest, servletResponse); - return; - } - - if (basicAuthEnabled) { - // Basic auth - String basicAuth = request.getHeader(HEADER_AUTHORIZATION); - - // for (Enumeration headerNames = request.getHeaderNames(); - // headerNames - // .hasMoreElements();) { - // String headerName = headerNames.nextElement(); - // Object headerValue = request.getHeader(headerName); - // log.debug(headerName + ": " + headerValue); - // } - - if (basicAuth == null) { - HttpServletResponse response = (HttpServletResponse) servletResponse; - response.setStatus(401); - response.setHeader("WWW-Authenticate", "basic realm=\"Auth (" - + httpSession.getCreationTime() + ")\""); - httpSession.setAttribute(ATTR_AUTH, Boolean.TRUE); - return; - } else { - UsernamePasswordAuthenticationToken token = basicAuth(basicAuth); - Authentication auth = authenticationManager.authenticate(token); - SecurityContextHolder.getContext().setAuthentication(auth); - httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - httpSession.setAttribute(ATTR_AUTH, Boolean.FALSE); - } - } - // Assume authentication has been done and continue - filterChain.doFilter(servletRequest, servletResponse); - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - } - - @Override - public void destroy() { - } - - private UsernamePasswordAuthenticationToken basicAuth(String authHeader) { - if (authHeader != null) { - StringTokenizer st = new StringTokenizer(authHeader); - if (st.hasMoreTokens()) { - String basic = st.nextToken(); - if (basic.equalsIgnoreCase("Basic")) { - try { - String credentials = new String(Base64.decodeBase64(st - .nextToken()), "UTF-8"); - log.debug("Credentials: " + credentials); - int p = credentials.indexOf(":"); - if (p != -1) { - String login = credentials.substring(0, p).trim(); - String password = credentials.substring(p + 1) - .trim(); - - return new UsernamePasswordAuthenticationToken( - login, password); - } else { - throw new CmsException( - "Invalid authentication token"); - } - } catch (Exception e) { - throw new CmsException( - "Couldn't retrieve authentication", e); - } - } - } - } - throw new CmsException("Couldn't retrieve authentication"); - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/webdav-config.xml b/org.argeo.cms/src/org/argeo/cms/internal/kernel/webdav-config.xml new file mode 100644 index 000000000..da4e18b11 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/webdav-config.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nt:file + nt:resource + + + + + + + + + + + + + rep + jcr + + + + + + + diff --git a/org.argeo.security.core/src/org/argeo/security/jcr/jackrabbit/ScopedSessionProvider.java b/org.argeo.security.core/src/org/argeo/security/jcr/jackrabbit/ScopedSessionProvider.java new file mode 100644 index 000000000..635f71ee2 --- /dev/null +++ b/org.argeo.security.core/src/org/argeo/security/jcr/jackrabbit/ScopedSessionProvider.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.jcr.jackrabbit; + +import java.io.Serializable; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.server.SessionProvider; +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoJcrConstants; +import org.argeo.jcr.JcrUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Session provider assuming a single workspace and a short life cycle, + * typically a Spring bean of scope (web) 'session'. + */ +public class ScopedSessionProvider implements SessionProvider, Serializable { + private static final long serialVersionUID = 6589775984177317058L; + private static final Log log = LogFactory + .getLog(ScopedSessionProvider.class); + private transient HttpSession httpSession = null; + private transient Session jcrSession = null; + + private transient String currentRepositoryName = null; + private transient String currentWorkspaceName = null; + private transient String currentJcrUser = null; + + // private transient String anonymousUserId = "anonymous"; + + public Session getSession(HttpServletRequest request, Repository rep, + String workspace) throws LoginException, ServletException, + RepositoryException { + + Authentication authentication = SecurityContextHolder.getContext() + .getAuthentication(); + if (authentication == null) + throw new ArgeoException( + "Request not authenticated by Spring Security"); + String springUser = authentication.getName(); + + // HTTP + String requestJcrRepository = (String) request + .getAttribute(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS); + + // HTTP session + if (httpSession != null + && !httpSession.getId().equals(request.getSession().getId())) + throw new ArgeoException( + "Only session scope is supported in this mode"); + if (httpSession == null) + httpSession = request.getSession(); + + // Initializes current values + if (currentRepositoryName == null) + currentRepositoryName = requestJcrRepository; + if (currentWorkspaceName == null) + currentWorkspaceName = workspace; + if (currentJcrUser == null) + currentJcrUser = springUser; + + // logout if there was a change in session coordinates + if (jcrSession != null) + if (!currentRepositoryName.equals(requestJcrRepository)) { + if (log.isDebugEnabled()) + log.debug(getHttpSessionId() + " Changed from repository '" + + currentRepositoryName + "' to '" + + requestJcrRepository + + "', logging out cached JCR session."); + logout(); + } else if (!currentWorkspaceName.equals(workspace)) { + if (log.isDebugEnabled()) + log.debug(getHttpSessionId() + " Changed from workspace '" + + currentWorkspaceName + "' to '" + workspace + + "', logging out cached JCR session."); + logout(); + } else if (!currentJcrUser.equals(springUser)) { + if (log.isDebugEnabled()) + log.debug(getHttpSessionId() + " Changed from user '" + + currentJcrUser + "' to '" + springUser + + "', logging out cached JCR session."); + logout(); + } + + // login if needed + if (jcrSession == null) + try { + Session session = login(rep, workspace); + if (!session.getUserID().equals(springUser)) { + JcrUtils.logoutQuietly(session); + throw new ArgeoException("Spring Security user '" + + springUser + "' not in line with JCR user '" + + session.getUserID() + "'"); + } + currentRepositoryName = requestJcrRepository; + // do not use workspace variable which may be null + currentWorkspaceName = session.getWorkspace().getName(); + currentJcrUser = session.getUserID(); + + jcrSession = session; + return jcrSession; + } catch (RepositoryException e) { + throw new ArgeoException("Cannot open session to workspace " + + workspace, e); + } + + // returns cached session + return jcrSession; + } + + protected Session login(Repository repository, String workspace) + throws RepositoryException { + Session session = repository.login(workspace); + if (log.isDebugEnabled()) + log.debug(getHttpSessionId() + " User '" + session.getUserID() + + "' logged in workspace '" + + session.getWorkspace().getName() + "' of repository '" + + currentRepositoryName + "'"); + return session; + } + + public void releaseSession(Session session) { + if (log.isTraceEnabled()) + log.trace(getHttpSessionId() + " Releasing JCR session " + session); + } + + protected void logout() { + JcrUtils.logoutQuietly(jcrSession); + jcrSession = null; + } + + protected final String getHttpSessionId() { + return httpSession != null ? httpSession.getId() : ""; + } + + public void init() { + } + + public void destroy() { + logout(); + if (getHttpSessionId() != null) + if (log.isDebugEnabled()) + log.debug(getHttpSessionId() + + " Cleaned up provider for web session "); + httpSession = null; + } + +} diff --git a/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java new file mode 100644 index 000000000..519a21840 --- /dev/null +++ b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.jackrabbit.servlet; + +import java.io.Serializable; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.server.SessionProvider; +import org.argeo.jcr.JcrUtils; + +/** + * Implements an open session in view patter: a new JCR session is created for + * each request + */ +public class OpenInViewSessionProvider implements SessionProvider, Serializable { + private static final long serialVersionUID = 2270957712453841368L; + + private final static Log log = LogFactory + .getLog(OpenInViewSessionProvider.class); + + public Session getSession(HttpServletRequest request, Repository rep, + String workspace) throws LoginException, ServletException, + RepositoryException { + return login(request, rep, workspace); + } + + protected Session login(HttpServletRequest request, Repository repository, + String workspace) throws RepositoryException { + if (log.isTraceEnabled()) + log.trace("Login to workspace " + + (workspace == null ? "" : workspace) + + " in web session " + request.getSession().getId()); + return repository.login(workspace); + } + + public void releaseSession(Session session) { + JcrUtils.logoutQuietly(session); + if (log.isTraceEnabled()) + log.trace("Logged out remote JCR session " + session); + } + + public void init() { + } + + public void destroy() { + } + +} diff --git a/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/RemotingServlet.java b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/RemotingServlet.java new file mode 100644 index 000000000..01bbb355d --- /dev/null +++ b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/RemotingServlet.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.jackrabbit.servlet; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet; + +/** Provides remote access to a JCR repository */ +public class RemotingServlet extends JcrRemotingServlet { + public final static String INIT_PARAM_RESOURCE_PATH_PREFIX = JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX; + public final static String INIT_PARAM_HOME = JcrRemotingServlet.INIT_PARAM_HOME; + public final static String INIT_PARAM_TMP_DIRECTORY = JcrRemotingServlet.INIT_PARAM_TMP_DIRECTORY; + + private static final long serialVersionUID = 3131835511468341309L; + + private final Repository repository; + private final SessionProvider sessionProvider; + + public RemotingServlet(Repository repository, + SessionProvider sessionProvider) { + this.repository = repository; + this.sessionProvider = sessionProvider; + } + + @Override + protected Repository getRepository() { + return repository; + } + + @Override + protected SessionProvider getSessionProvider() { + return sessionProvider; + } + +} diff --git a/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/WebdavServlet.java b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/WebdavServlet.java new file mode 100644 index 000000000..c2346f00e --- /dev/null +++ b/org.argeo.server.jackrabbit/src/org/argeo/jackrabbit/servlet/WebdavServlet.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.jackrabbit.servlet; + +import java.io.IOException; + +import javax.jcr.Repository; +import javax.servlet.ServletException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.WebdavResponse; +import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet; + +/** WebDav servlet whose repository is injected */ +public class WebdavServlet extends SimpleWebdavServlet { + public final static String INIT_PARAM_RESOURCE_CONFIG = SimpleWebdavServlet.INIT_PARAM_RESOURCE_CONFIG; + public final static String INIT_PARAM_RESOURCE_PATH_PREFIX = SimpleWebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX; + + private static final long serialVersionUID = -369787931175177080L; + + private final static Log log = LogFactory.getLog(WebdavServlet.class); + + private final Repository repository; + + public WebdavServlet(Repository repository, SessionProvider sessionProvider) { + this.repository = repository; + setSessionProvider(sessionProvider); + } + + public Repository getRepository() { + return repository; + } + + @Override + protected boolean execute(WebdavRequest request, WebdavResponse response, + int method, DavResource resource) throws ServletException, + IOException, DavException { + if (log.isTraceEnabled()) + log.trace(request.getMethod() + "\t" + request.getPathInfo()); + boolean res = super.execute(request, response, method, resource); + return res; + } + +} diff --git a/org.argeo.server.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.server.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java new file mode 100644 index 000000000..61854131b --- /dev/null +++ b/org.argeo.server.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.jcr.proxy; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; +import org.argeo.jcr.JcrUtils; + +/** Wraps a proxy via HTTP */ +public class ResourceProxyServlet extends HttpServlet { + private static final long serialVersionUID = -8886549549223155801L; + + private final static Log log = LogFactory + .getLog(ResourceProxyServlet.class); + + private ResourceProxy proxy; + + private String contentTypeCharset = "UTF-8"; + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + String path = request.getPathInfo(); + + if (log.isTraceEnabled()) { + log.trace("path=" + path); + log.trace("UserPrincipal = " + request.getUserPrincipal().getName()); + log.trace("SessionID = " + request.getSession().getId()); + log.trace("ContextPath = " + request.getContextPath()); + log.trace("ServletPath = " + request.getServletPath()); + log.trace("PathInfo = " + request.getPathInfo()); + log.trace("Method = " + request.getMethod()); + log.trace("User-Agent = " + request.getHeader("User-Agent")); + } + + Node node = null; + try { + node = proxy.proxy(path); + if (node == null) + response.sendError(404); + else + processResponse(node, response); + } finally { + if (node != null) + try { + JcrUtils.logoutQuietly(node.getSession()); + } catch (RepositoryException e) { + // silent + } + } + + } + + /** Retrieve the content of the node. */ + protected void processResponse(Node node, HttpServletResponse response) { + Binary binary = null; + InputStream in = null; + try { + String fileName = node.getName(); + String ext = FilenameUtils.getExtension(fileName); + + // TODO use a more generic / standard approach + // see http://svn.apache.org/viewvc/tomcat/trunk/conf/web.xml + String contentType; + if ("xml".equals(ext)) + contentType = "text/xml;charset=" + contentTypeCharset; + else if ("jar".equals(ext)) + contentType = "application/java-archive"; + else if ("zip".equals(ext)) + contentType = "application/zip"; + else if ("gz".equals(ext)) + contentType = "application/x-gzip"; + else if ("bz2".equals(ext)) + contentType = "application/x-bzip2"; + else if ("tar".equals(ext)) + contentType = "application/x-tar"; + else if ("rpm".equals(ext)) + contentType = "application/x-redhat-package-manager"; + else + contentType = "application/octet-stream"; + contentType = contentType + ";name=\"" + fileName + "\""; + response.setHeader("Content-Disposition", "attachment; filename=\"" + + fileName + "\""); + response.setHeader("Expires", "0"); + response.setHeader("Cache-Control", "no-cache, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + + response.setContentType(contentType); + + try { + binary = node.getNode(Property.JCR_CONTENT) + .getProperty(Property.JCR_DATA).getBinary(); + } catch (PathNotFoundException e) { + log.error("Node " + node + " as no data under content"); + throw e; + } + in = binary.getStream(); + IOUtils.copy(in, response.getOutputStream()); + } catch (Exception e) { + throw new ArgeoException("Cannot download " + node, e); + } finally { + IOUtils.closeQuietly(in); + JcrUtils.closeQuietly(binary); + } + } + + public void setProxy(ResourceProxy resourceProxy) { + this.proxy = resourceProxy; + } + +} diff --git a/pom.xml b/pom.xml index 33c70f142..6f0d352ed 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.argeo.commons argeo-commons @@ -30,16 +31,16 @@ org.argeo.security.auth.ldap org.argeo.security.dao.ldap org.argeo.security.dao.cli - org.argeo.security.dao.jackrabbit + org.argeo.server.core - org.argeo.server.jcr.mvc + org.argeo.server.jackrabbit org.argeo.eclipse.ui org.argeo.eclipse.ui.rap - org.argeo.node.repo.jackrabbit + org.argeo.cms org.argeo.eclipse.ui.workbench