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 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.ServletRequest;
+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.eclipse.equinox.http.servlet.ExtendedHttpService;
-import org.eclipse.jetty.servlets.DoSFilter;
-import org.eclipse.jetty.servlets.QoSFilter;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
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;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
/**
* Intercepts and enriches http access, mainly focusing on security and
* transactionality.
*/
-class NodeHttp implements KernelConstants, ArgeoJcrConstants {
+class NodeHttp implements KernelConstants {
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";
+ // Filters
+ // private final RootFilter rootFilter;
- private final AuthenticationManager authenticationManager;
- private final BundleContext bundleContext;
- private ExtendedHttpService httpService;
+ // private final DoSFilter dosFilter;
+ // private final QoSFilter qosFilter;
- // FIXME Make it more unique
- private String httpAuthRealm = "Argeo";
+ private BundleContext bc;
- // Filters
- private final RootFilter rootFilter;
- private final DoSFilter dosFilter;
- private final QoSFilter qosFilter;
-
- // 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<ExtendedHttpService, ExtendedHttpService> st = new ServiceTracker<ExtendedHttpService, ExtendedHttpService>(
- bundleContext, ExtendedHttpService.class, null);
- st.open();
- try {
- httpService = st.waitForService(1000);
- } catch (InterruptedException e) {
- httpService = null;
- }
+ NodeHttp(HttpService httpService, BundleContext bc) {
+ this.bc = bc;
+ // rootFilter = new RootFilter();
+ // dosFilter = new CustomDosFilter();
+ // qosFilter = new QoSFilter();
- if (httpService == null)
- throw new CmsException("Could not find "
- + ExtendedHttpService.class + " service.");
-
- // Filters
- rootFilter = new RootFilter();
- dosFilter = new CustomDosFilter();
- qosFilter = new QoSFilter();
-
- // 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("/", dosFilter, null, null);
- httpService.registerFilter("/", rootFilter, null, null);
- httpService.registerFilter("/", qosFilter, null, null);
+ httpService.registerServlet("/!", new LinkServlet(), null, null);
+ httpService.registerServlet("/robots.txt", new RobotServlet(), null, null);
} catch (Exception e) {
- throw new CmsException("Cannot publish HTTP services to OSGi", e);
+ throw new CmsException("Cannot register filters", 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);
+ public void destroy() {
}
- 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);
- }
+ class LinkServlet extends HttpServlet {
+ private static final long serialVersionUID = 3749990143146845708L;
- private Boolean isSessionAuthenticated(HttpSession httpSession) {
- SecurityContext contextFromSession = (SecurityContext) httpSession
- .getAttribute(SPRING_SECURITY_CONTEXT_KEY);
- return contextFromSession != 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 void requestBasicAuth(HttpSession httpSession,
- HttpServletResponse response) {
- response.setStatus(401);
- response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\""
- + httpAuthRealm + "\"");
- httpSession.setAttribute(ATTR_AUTH, Boolean.TRUE);
- }
+ if (isBot) {
+ log.warn("# BOT " + request.getHeader("User-Agent"));
+ canonicalAnswer(request, response, path);
+ return;
+ }
+
+ 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<Session>() {
+
+ @Override
+ public Session run() throws Exception {
+ Collection<ServiceReference<Repository>> srs = bc.getServiceReferences(Repository.class, "("
+ + NodeConstants.JCR_REPOSITORY_ALIAS + "=" + NodeConstants.ALIAS_NODE + ")");
+ Repository repository = bc.getService(srs.iterator().next());
+ return repository.login();
+ }
- 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.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("<html>");
+ buf.append("<head>");
+ writeMeta(buf, "og:title", escapeHTML(title));
+ writeMeta(buf, "og:type", "website");
+ buf.append("<meta name='twitter:card' content='summary' />");
+ buf.append("<meta name='twitter:site' content='@argeo_org' />");
+ 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("</head>");
+ buf.append("<body>");
+ buf.append(
+ "<p><b>!! This page is meant for indexing robots, not for real people," + " visit <a href='/#")
+ .append(path).append("'>").append(escapeHTML(title)).append("</a> instead.</b></p>");
+ writeCanonical(buf, node);
+ buf.append("</body>");
+ buf.append("</html>");
+ 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("<meta property='").append(tag).append("' content='").append(value).append("'/>");
+ }
+
+ private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException {
+ buf.append("<div>");
+ if (node.hasProperty(JCR_TITLE))
+ buf.append("<p>").append(node.getProperty(JCR_TITLE).getString()).append("</p>");
+ if (node.hasProperty(JCR_DESCRIPTION))
+ buf.append("<p>").append(node.getProperty(JCR_DESCRIPTION).getString()).append("</p>");
+ NodeIterator children = node.getNodes();
+ while (children.hasNext()) {
+ writeCanonical(buf, children.nextNode());
+ }
+ buf.append("</div>");
+ }
+ }
+
+ 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()) {
- log.debug(request.getContextPath());
- log.debug(request.getServletPath());
- log.debug(request.getRequestURI());
- log.debug(request.getQueryString());
- StringBuilder buf = new StringBuilder();
- Enumeration<String> en = request.getHeaderNames();
- while (en.hasMoreElements()) {
- String header = en.nextElement();
- Enumeration<String> values = request.getHeaders(header);
- while (values.hasMoreElements())
- buf.append(" " + header + ": " + values.nextElement());
- buf.append('\n');
- }
- log.debug("\n" + buf);
+ log.trace(request.getRequestURL()
+ .append(request.getQueryString() != null ? "?" + request.getQueryString() : ""));
+ logRequest(request);
}
String servletPath = request.getServletPath();
+ // client certificate
+ X509Certificate clientCert = extractCertificate(request);
+ if (clientCert != null) {
+ // TODO authenticate
+ // if (log.isDebugEnabled())
+ // log.debug(clientCert.getSubjectX500Principal().getName());
+ }
+
// skip data
if (servletPath.startsWith(PATH_DATA)) {
filterChain.doFilter(request, response);
return;
}
+ // skip /ui (workbench) for the time being
+ if (servletPath.startsWith(PATH_WORKBENCH)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
// redirect long RWT paths to anchor
- String path = request.getRequestURI()
- .substring(servletPath.length()).trim();
- if (!servletPath.endsWith("rwt-resources") && !path.equals("")
- && !path.equals("/")) {
+ 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);
}
}
- /** 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);
+ 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<String> en = request.getHeaderNames();
+ while (en.hasMoreElements()) {
+ String header = en.nextElement();
+ Enumeration<String> values = request.getHeaders(header);
+ while (values.hasMoreElements())
+ buf.append(" " + header + ": " + values.nextElement());
+ buf.append('\n');
}
- }
-
- /** 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);
+ // attributed
+ Enumeration<String> 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);
}
- 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);
-
+ private X509Certificate extractCertificate(HttpServletRequest req) {
+ X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
+ if (null != certs && certs.length > 0) {
+ return certs[0];
}
+ 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);
+ //
+ // }
+ // }
}