X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Finternal%2Fkernel%2FNodeHttp.java;h=3174285dbfdf93658a53014f744d66407449e302;hb=2afabd9e7b225f80b341063e25188314394c9aef;hp=0510b3c837116053f1ac5c8265e5c2213b96885d;hpb=61780b581925666edd4bd7743a00dca7170f1d35;p=lgpl%2Fargeo-commons.git 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 index 0510b3c83..3174285db 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java @@ -1,327 +1,312 @@ package org.argeo.cms.internal.kernel; -import static javax.jcr.Property.JCR_DESCRIPTION; -import static javax.jcr.Property.JCR_LAST_MODIFIED; -import static javax.jcr.Property.JCR_TITLE; -import static org.argeo.cms.CmsTypes.CMS_IMAGE; - import java.io.IOException; -import java.io.PrintWriter; -import java.security.PrivilegedExceptionAction; -import java.security.cert.X509Certificate; -import java.util.Calendar; -import java.util.Collection; -import java.util.Enumeration; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.security.auth.Subject; -import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -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.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet; +import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet; import org.argeo.cms.CmsException; -import org.argeo.jcr.JcrUtils; +import org.argeo.cms.internal.http.CmsSessionProvider; +import org.argeo.cms.internal.http.DataHttpContext; +import org.argeo.cms.internal.http.HttpUtils; +import org.argeo.cms.internal.http.LinkServlet; +import org.argeo.cms.internal.http.PrivateHttpContext; +import org.argeo.cms.internal.http.RobotServlet; import org.argeo.node.NodeConstants; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceReference; import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.osgi.util.tracker.ServiceTracker; /** * Intercepts and enriches http access, mainly focusing on security and * transactionality. */ -class NodeHttp implements KernelConstants { +public class NodeHttp implements KernelConstants { private final static Log log = LogFactory.getLog(NodeHttp.class); - // Filters - // private final RootFilter rootFilter; + public final static String DEFAULT_SERVICE = "HTTP"; + + private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext(); + + private ServiceTracker repositories; + private final ServiceTracker httpServiceTracker; - // private final DoSFilter dosFilter; - // private final QoSFilter qosFilter; + private String httpRealm = "Argeo"; + private String webDavConfig = HttpUtils.WEBDAV_CONFIG; + private final boolean cleanState; - private BundleContext bc; + public NodeHttp(boolean cleanState) { + this.cleanState = cleanState; + httpServiceTracker = new PrepareHttpStc(); + // httpServiceTracker.open(); + KernelUtils.asyncOpen(httpServiceTracker); + } - NodeHttp(HttpService httpService, BundleContext bc) { - this.bc = bc; - // rootFilter = new RootFilter(); - // dosFilter = new CustomDosFilter(); - // qosFilter = new QoSFilter(); + public void destroy() { + if (repositories != null) + repositories.close(); + } + public void registerRepositoryServlets(HttpService httpService, String alias, Repository repository) { + if (httpService == null) + throw new CmsException("No HTTP service available"); try { - httpService.registerServlet("/!", new LinkServlet(), null, null); - httpService.registerServlet("/robots.txt", new RobotServlet(), null, null); + registerWebdavServlet(httpService, alias, repository); + registerRemotingServlet(httpService, alias, repository); + if (NodeConstants.EGO.equals(alias)) + registerFilesServlet(httpService, alias, repository); + if (log.isTraceEnabled()) + log.trace("Registered servlets for repository '" + alias + "'"); } catch (Exception e) { - throw new CmsException("Cannot register filters", e); + throw new CmsException("Could not register servlets for repository '" + alias + "'", e); } } - public void destroy() { + public static void unregisterRepositoryServlets(HttpService httpService, String alias) { + if (httpService == null) + return; + try { + httpService.unregister(webdavPath(alias)); + httpService.unregister(remotingPath(alias)); + if (NodeConstants.EGO.equals(alias)) + httpService.unregister(filesPath(alias)); + if (log.isTraceEnabled()) + log.trace("Unregistered servlets for repository '" + alias + "'"); + } catch (Exception e) { + log.error("Could not unregister servlets for repository '" + alias + "'", e); + } } - class LinkServlet extends HttpServlet { - private static final long serialVersionUID = 3749990143146845708L; + void registerWebdavServlet(HttpService httpService, String alias, Repository repository) + throws NamespaceException, ServletException { + // WebdavServlet webdavServlet = new WebdavServlet(repository, new + // OpenInViewSessionProvider(alias)); + WebdavServlet webdavServlet = new WebdavServlet(repository, new CmsSessionProvider(alias)); + String path = webdavPath(alias); + Properties ip = new Properties(); + ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, webDavConfig); + ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); + httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext(httpRealm)); + } - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - String path = request.getPathInfo(); - String userAgent = request.getHeader("User-Agent").toLowerCase(); - boolean isBot = false; - boolean isCompatibleBrowser = false; - if (userAgent.contains("bot") || userAgent.contains("facebook") || userAgent.contains("twitter")) { - isBot = true; - } else if (userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox") - || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium") - || userAgent.contains("opera") || userAgent.contains("browser")) { - isCompatibleBrowser = true; - } + void registerFilesServlet(HttpService httpService, String alias, Repository repository) + throws NamespaceException, ServletException { + WebdavServlet filesServlet = new WebdavServlet(repository, new CmsSessionProvider(alias)); + String path = filesPath(alias); + Properties ip = new Properties(); + ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, webDavConfig); + ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); + httpService.registerServlet(path, filesServlet, ip, new PrivateHttpContext(httpRealm, true)); + } - if (isBot) { - log.warn("# BOT " + request.getHeader("User-Agent")); - canonicalAnswer(request, response, path); - return; - } + void registerRemotingServlet(HttpService httpService, String alias, Repository repository) + throws NamespaceException, ServletException { + RemotingServlet remotingServlet = new RemotingServlet(repository, new CmsSessionProvider(alias)); + String path = remotingPath(alias); + Properties ip = new Properties(); + ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); + ip.setProperty(JcrRemotingServlet.INIT_PARAM_AUTHENTICATE_HEADER, "Negotiate"); - if (isCompatibleBrowser && log.isTraceEnabled()) - log.trace("# BWS " + request.getHeader("User-Agent")); - redirectTo(response, "/#" + path); + // Looks like a bug in Jackrabbit remoting init + Path tmpDir; + try { + tmpDir = Files.createTempDirectory("remoting_" + alias); + } catch (IOException e) { + throw new CmsException("Cannot create temp directory for remoting servlet", e); } + ip.setProperty(RemotingServlet.INIT_PARAM_HOME, tmpDir.toString()); + ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting_" + alias); + ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, HttpUtils.DEFAULT_PROTECTED_HANDLERS); + ip.setProperty(RemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false"); + httpService.registerServlet(path, remotingServlet, ip, new PrivateHttpContext(httpRealm)); + } - private void redirectTo(HttpServletResponse response, String location) { - response.setHeader("Location", location); - response.setStatus(HttpServletResponse.SC_FOUND); - } + static String webdavPath(String alias) { + return NodeConstants.PATH_DATA + "/" + alias; + } - // private boolean canonicalAnswerNeededBy(HttpServletRequest request) { - // String userAgent = request.getHeader("User-Agent").toLowerCase(); - // return userAgent.startsWith("facebookexternalhit/"); - // } + static String remotingPath(String alias) { + return NodeConstants.PATH_JCR + "/" + alias; + } - /** For bots which don't understand RWT. */ - private void canonicalAnswer(HttpServletRequest request, HttpServletResponse response, String path) { - Session session = null; - try { - PrintWriter writer = response.getWriter(); - session = Subject.doAs(KernelUtils.anonymousLogin(), new PrivilegedExceptionAction() { - - @Override - public Session run() throws Exception { - Collection> srs = bc.getServiceReferences(Repository.class, "(" - + NodeConstants.JCR_REPOSITORY_ALIAS + "=" + NodeConstants.ALIAS_NODE + ")"); - Repository repository = bc.getService(srs.iterator().next()); - return repository.login(); - } - - }); - Node node = session.getNode(path); - String title = node.hasProperty(JCR_TITLE) ? node.getProperty(JCR_TITLE).getString() : node.getName(); - String desc = node.hasProperty(JCR_DESCRIPTION) ? node.getProperty(JCR_DESCRIPTION).getString() : null; - Calendar lastUpdate = node.hasProperty(JCR_LAST_MODIFIED) - ? node.getProperty(JCR_LAST_MODIFIED).getDate() : null; - String url = KernelUtils.getCanonicalUrl(node, request); - String imgUrl = null; - loop: for (NodeIterator it = node.getNodes(); it.hasNext();) { - // Takes the first found cms:image - Node child = it.nextNode(); - if (child.isNodeType(CMS_IMAGE)) { - imgUrl = KernelUtils.getDataUrl(child, request); - break loop; - } - } - StringBuilder buf = new StringBuilder(); - buf.append(""); - buf.append(""); - writeMeta(buf, "og:title", escapeHTML(title)); - writeMeta(buf, "og:type", "website"); - buf.append(""); - buf.append(""); - writeMeta(buf, "og:url", url); - if (desc != null) - writeMeta(buf, "og:description", escapeHTML(desc)); - if (imgUrl != null) - writeMeta(buf, "og:image", imgUrl); - if (lastUpdate != null) - writeMeta(buf, "og:updated_time", Long.toString(lastUpdate.getTime().getTime())); - buf.append(""); - buf.append(""); - buf.append( - "

!! This page is meant for indexing robots, not for real people," + " visit ").append(escapeHTML(title)).append(" instead.

"); - writeCanonical(buf, node); - buf.append(""); - buf.append(""); - writer.print(buf.toString()); - - response.setHeader("Content-Type", "text/html"); - writer.flush(); - } catch (Exception e) { - throw new CmsException("Cannot write canonical answer", e); - } finally { - JcrUtils.logoutQuietly(session); - } + static String filesPath(String alias) { + return NodeConstants.PATH_FILES; + } + + class RepositoriesStc extends ServiceTracker { + private final HttpService httpService; + + private final BundleContext bc; + + public RepositoriesStc(BundleContext bc, HttpService httpService) { + super(bc, Repository.class, null); + this.httpService = httpService; + this.bc = bc; } - /** - * From - * http://stackoverflow.com/questions/1265282/recommended-method-for- - * escaping-html-in-java (+ escaping '). TODO Use - * org.apache.commons.lang.StringEscapeUtils - */ - private String escapeHTML(String s) { - StringBuilder out = new StringBuilder(Math.max(16, s.length())); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c > 127 || c == '\'' || c == '"' || c == '<' || c == '>' || c == '&') { - out.append("&#"); - out.append((int) c); - out.append(';'); - } else { - out.append(c); - } + @Override + public Repository addingService(ServiceReference reference) { + Repository repository = bc.getService(reference); + Object jcrRepoAlias = reference.getProperty(NodeConstants.CN); + if (jcrRepoAlias != null) { + String alias = jcrRepoAlias.toString(); + registerRepositoryServlets(httpService, alias, repository); } - return out.toString(); + return repository; } - private void writeMeta(StringBuilder buf, String tag, String value) { - buf.append(""); + @Override + public void modifiedService(ServiceReference reference, Repository service) { } - private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException { - buf.append("
"); - if (node.hasProperty(JCR_TITLE)) - buf.append("

").append(node.getProperty(JCR_TITLE).getString()).append("

"); - if (node.hasProperty(JCR_DESCRIPTION)) - buf.append("

").append(node.getProperty(JCR_DESCRIPTION).getString()).append("

"); - NodeIterator children = node.getNodes(); - while (children.hasNext()) { - writeCanonical(buf, children.nextNode()); + @Override + public void removedService(ServiceReference reference, Repository service) { + Object jcrRepoAlias = reference.getProperty(NodeConstants.CN); + if (jcrRepoAlias != null) { + String alias = jcrRepoAlias.toString(); + unregisterRepositoryServlets(httpService, alias); } - buf.append("
"); } } - class RobotServlet extends HttpServlet { - private static final long serialVersionUID = 7935661175336419089L; + private class PrepareHttpStc extends ServiceTracker { + public PrepareHttpStc() { + super(bc, HttpService.class, null); + } @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(); + public HttpService addingService(ServiceReference reference) { + long begin = System.currentTimeMillis(); + if (log.isTraceEnabled()) + log.trace("HTTP prepare starts..."); + HttpService httpService = addHttpService(reference); + if (log.isTraceEnabled()) + log.trace("HTTP prepare duration: " + (System.currentTimeMillis() - begin) + "ms"); + return httpService; } - } - - /** Intercepts all requests. Authenticates. */ - class RootFilter extends HttpFilter { - @Override - public void doFilter(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws IOException, ServletException { - if (log.isTraceEnabled()) { - log.trace(request.getRequestURL() - .append(request.getQueryString() != null ? "?" + request.getQueryString() : "")); - logRequest(request); - } + public void removedService(ServiceReference reference, HttpService service) { + repositories.close(); + repositories = null; + } - String servletPath = request.getServletPath(); + private HttpService addHttpService(ServiceReference sr) { + HttpService httpService = bc.getService(sr); + // TODO find constants + Object httpPort = sr.getProperty("http.port"); + Object httpsPort = sr.getProperty("https.port"); - // client certificate - X509Certificate clientCert = extractCertificate(request); - if (clientCert != null) { - // TODO authenticate - // if (log.isDebugEnabled()) - // log.debug(clientCert.getSubjectX500Principal().getName()); + try { + httpService.registerServlet("/!", new LinkServlet(), null, null); + httpService.registerServlet("/robots.txt", new RobotServlet(), null, null); + // httpService.registerServlet("/html", new HtmlServlet(), null, null); + } catch (Exception e) { + throw new CmsException("Cannot register filters", e); } + // track repositories + if (repositories != null) + throw new CmsException("An http service is already configured"); + repositories = new RepositoriesStc(bc, httpService); + // repositories.open(); + if (cleanState) + KernelUtils.asyncOpen(repositories); + log.info(httpPortsMsg(httpPort, httpsPort)); + // httpAvailable = true; + // checkReadiness(); + + bc.registerService(NodeHttp.class, NodeHttp.this, null); + return httpService; + } - // skip data - if (servletPath.startsWith(PATH_DATA)) { - filterChain.doFilter(request, response); - return; - } + private String httpPortsMsg(Object httpPort, Object httpsPort) { + return (httpPort != null ? "HTTP " + httpPort + " " : " ") + + (httpsPort != null ? "HTTPS " + httpsPort : ""); + } + } - // skip /ui (workbench) for the time being - if (servletPath.startsWith(PATH_WORKBENCH)) { - filterChain.doFilter(request, response); - return; - } + private static class WebdavServlet extends SimpleWebdavServlet { + private static final long serialVersionUID = -4687354117811443881L; + private final Repository repository; - // redirect long RWT paths to anchor - String path = request.getRequestURI().substring(servletPath.length()); - int pathLength = path.length(); - if (pathLength != 0 && (path.charAt(0) == '/') && !servletPath.endsWith("rwt-resources") - && !path.startsWith(KernelConstants.PATH_WORKBENCH) && path.lastIndexOf('/') != 0) { - String newLocation = request.getServletPath() + "#" + path; - response.setHeader("Location", newLocation); - response.setStatus(HttpServletResponse.SC_FOUND); - return; - } + public WebdavServlet(Repository repository, SessionProvider sessionProvider) { + this.repository = repository; + setSessionProvider(sessionProvider); + } + + public Repository getRepository() { + return repository; + } - // process normally - filterChain.doFilter(request, response); + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + WebdavServlet.super.service(request, response); + // try { + // Subject subject = subjectFromRequest(request); + // // TODO make it stronger, with eTags. + // // if (CurrentUser.isAnonymous(subject) && + // // request.getMethod().equals("GET")) { + // // response.setHeader("Cache-Control", "no-transform, public, + // // max-age=300, s-maxage=900"); + // // } + // + // Subject.doAs(subject, new PrivilegedExceptionAction() { + // @Override + // public Void run() throws Exception { + // WebdavServlet.super.service(request, response); + // return null; + // } + // }); + // } catch (PrivilegedActionException e) { + // throw new CmsException("Cannot process webdav request", + // e.getException()); + // } } + } - private void logRequest(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'); + private static class RemotingServlet extends JcrRemotingServlet { + private final Log log = LogFactory.getLog(RemotingServlet.class); + private static final long serialVersionUID = 4605238259548058883L; + private final Repository repository; + private final SessionProvider sessionProvider; + + public RemotingServlet(Repository repository, SessionProvider sessionProvider) { + this.repository = repository; + this.sessionProvider = sessionProvider; } - // attributed - Enumeration an = request.getAttributeNames(); - while (an.hasMoreElements()) { - String attr = an.nextElement(); - Object value = request.getAttribute(attr); - buf.append(" " + attr + ": " + value); - buf.append('\n'); + @Override + protected Repository getRepository() { + return repository; } - log.debug("\n" + buf); - } - private X509Certificate extractCertificate(HttpServletRequest req) { - X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate"); - if (null != certs && certs.length > 0) { - return certs[0]; + @Override + protected SessionProvider getSessionProvider() { + return sessionProvider; + } + + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + if (log.isTraceEnabled()) + HttpUtils.logRequest(log, request); + RemotingServlet.super.service(request, response); } - return null; } - // class CustomDosFilter extends DoSFilter { - // @Override - // protected String extractUserId(ServletRequest request) { - // HttpSession httpSession = ((HttpServletRequest) request) - // .getSession(); - // if (isSessionAuthenticated(httpSession)) { - // String userId = ((SecurityContext) httpSession - // .getAttribute(SPRING_SECURITY_CONTEXT_KEY)) - // .getAuthentication().getName(); - // return userId; - // } - // return super.extractUserId(request); - // - // } - // } }