X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Finternal%2Fkernel%2FNodeHttp.java;h=4a6ad233799246036f73afd3748c538f81dde16c;hb=972528f4de2d00690362c01d3ce843ca9cd10250;hp=418771d981669295308a51958aa6b979e0a38385;hpb=9417e33dda6b8987312a89ca44cdb2f511f04050;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 418771d98..4a6ad2337 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,37 +1,39 @@ package org.argeo.cms.internal.kernel; -import static org.argeo.jackrabbit.servlet.WebdavServlet.INIT_PARAM_RESOURCE_CONFIG; +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 java.util.Properties; -import java.util.StringTokenizer; +import javax.jcr.Node; +import javax.jcr.NodeIterator; import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; import javax.servlet.FilterChain; -import javax.servlet.Servlet; 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.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; -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.argeo.security.NodeAuthenticationToken; -import org.eclipse.equinox.http.servlet.ExtendedHttpService; -import org.osgi.service.http.NamespaceException; -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; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.HttpService; /** * Intercepts and enriches http access, mainly focusing on security and @@ -40,144 +42,199 @@ import org.springframework.security.core.context.SecurityContextHolder; 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"; - - private final AuthenticationManager authenticationManager; - private final ExtendedHttpService httpService; - - // FIXME Make it more unique - private String httpAuthRealm = "Argeo"; - // Filters - private final RootFilter rootFilter; + // private final RootFilter rootFilter; + // private final DoSFilter dosFilter; // private final QoSFilter qosFilter; - // WebDav / JCR remoting - private OpenInViewSessionProvider sessionProvider; + private BundleContext bc; - NodeHttp(ExtendedHttpService httpService, JackrabbitNode node, - NodeSecurity authenticationManager) { - // this.bundleContext = bundleContext; - this.authenticationManager = authenticationManager; - - this.httpService = httpService; - - // Filters - rootFilter = new RootFilter(); + NodeHttp(HttpService httpService, BundleContext bc) { + this.bc = bc; + // rootFilter = new RootFilter(); // dosFilter = new CustomDosFilter(); // qosFilter = new QoSFilter(); - // DAV - sessionProvider = new OpenInViewSessionProvider(); - try { - registerWebdavServlet(ALIAS_NODE, node, true); - registerWebdavServlet(ALIAS_NODE, node, false); - registerRemotingServlet(ALIAS_NODE, node, true); - registerRemotingServlet(ALIAS_NODE, node, false); - - httpService.registerFilter("/", rootFilter, null, null); + httpService.registerServlet("/!", new LinkServlet(), null, null); + httpService.registerServlet("/robots.txt", new RobotServlet(), null, null); } catch (Exception e) { - throw new CmsException("Could not initialise http", e); + throw new CmsException("Cannot register filters", e); } } public void destroy() { - sessionProvider.destroy(); } - void registerWebdavServlet(String alias, Repository repository, - boolean anonymous) throws NamespaceException, ServletException { - WebdavServlet webdavServlet = new WebdavServlet(repository, - sessionProvider); - String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE; - String path = pathPrefix + "/" + alias; - Properties ip = new Properties(); - ip.setProperty(INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG); - ip.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, ip, null); - } + class LinkServlet extends HttpServlet { + private static final long serialVersionUID = 3749990143146845708L; - void registerRemotingServlet(String alias, Repository repository, - boolean anonymous) throws NamespaceException, ServletException { - String pathPrefix = anonymous ? REMOTING_PUBLIC : REMOTING_PRIVATE; - RemotingServlet remotingServlet = new RemotingServlet(repository, - sessionProvider); - String path = pathPrefix + "/" + alias; - Properties ip = new Properties(); - ip.setProperty(RemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path); - - // Looks like a bug in Jackrabbit remoting init - ip.setProperty(RemotingServlet.INIT_PARAM_HOME, - KernelUtils.getOsgiInstanceDir() + "/tmp/jackrabbit"); - ip.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, ip, null); - } + @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; + } - private Boolean isSessionAuthenticated(HttpSession httpSession) { - SecurityContext contextFromSession = (SecurityContext) httpSession - .getAttribute(SPRING_SECURITY_CONTEXT_KEY); - return contextFromSession != null; - } + if (isBot) { + log.warn("# BOT " + request.getHeader("User-Agent")); + canonicalAnswer(request, response, path); + return; + } - private void requestBasicAuth(HttpSession httpSession, - HttpServletResponse response) { - response.setStatus(401); - response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" - + httpAuthRealm + "\""); - httpSession.setAttribute(ATTR_AUTH, Boolean.TRUE); - } + if (isCompatibleBrowser && log.isTraceEnabled()) + log.trace("# BWS " + request.getHeader("User-Agent")); + redirectTo(response, "/#" + path); + } + + private void redirectTo(HttpServletResponse response, String location) { + response.setHeader("Location", location); + response.setStatus(HttpServletResponse.SC_FOUND); + } + + // private boolean canonicalAnswerNeededBy(HttpServletRequest request) { + // String userAgent = request.getHeader("User-Agent").toLowerCase(); + // return userAgent.startsWith("facebookexternalhit/"); + // } + + /** 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, "(" + + ArgeoJcrConstants.JCR_REPOSITORY_ALIAS + "=" + ArgeoJcrConstants.ALIAS_NODE + ")"); + Repository repository = bc.getService(srs.iterator().next()); + return repository.login(); + } - private NodeAuthenticationToken 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 NodeAuthenticationToken(login, - password.toCharArray()); - } else { - throw new CmsException( - "Invalid authentication token"); - } - } catch (Exception e) { - throw new CmsException( - "Couldn't retrieve authentication", e); + }); + 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); } } - throw new CmsException("Couldn't retrieve authentication"); + + /** + * 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); + } + } + return out.toString(); + } + + private void writeMeta(StringBuilder buf, String tag, String value) { + buf.append(""); + } + + 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()); + } + buf.append("
"); + } + } + + 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(); + } + } /** Intercepts all requests. Authenticates. */ class RootFilter extends HttpFilter { @Override - public void doFilter(HttpSession httpSession, - HttpServletRequest request, HttpServletResponse response, + public void doFilter(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { - if (log.isTraceEnabled()) + if (log.isTraceEnabled()) { + log.trace(request.getRequestURL() + .append(request.getQueryString() != null ? "?" + request.getQueryString() : "")); logRequest(request); + } String servletPath = request.getServletPath(); @@ -202,12 +259,10 @@ class NodeHttp implements KernelConstants, ArgeoJcrConstants { } // redirect long RWT paths to anchor - String path = request.getRequestURI().substring( - servletPath.length()); + String path = request.getRequestURI().substring(servletPath.length()); int pathLength = path.length(); - if (pathLength != 0 && (path.charAt(0) == '/') - && !servletPath.endsWith("rwt-resources") - && !path.equals("/")) { + 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); @@ -220,10 +275,10 @@ class NodeHttp implements KernelConstants, ArgeoJcrConstants { } private void logRequest(HttpServletRequest request) { - log.debug(request.getContextPath()); - log.debug(request.getServletPath()); - log.debug(request.getRequestURI()); - log.debug(request.getQueryString()); + 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(); @@ -247,63 +302,13 @@ class NodeHttp implements KernelConstants, ArgeoJcrConstants { } private X509Certificate extractCertificate(HttpServletRequest req) { - X509Certificate[] certs = (X509Certificate[]) req - .getAttribute("javax.servlet.request.X509Certificate"); + X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate"); if (null != certs && certs.length > 0) { return certs[0]; } return null; } - /** Intercepts all requests. Authenticates. */ - private 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. */ - private 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); - } - } - // class CustomDosFilter extends DoSFilter { // @Override // protected String extractUserId(ServletRequest request) {