]> git.argeo.org Git - lgpl/argeo-commons.git/blob - NodeHttp.java
f17e1579764a62810b65214c0438bf3b41cc1b33
[lgpl/argeo-commons.git] / NodeHttp.java
1 package org.argeo.cms.internal.kernel;
2
3 import static javax.jcr.Property.JCR_DESCRIPTION;
4 import static javax.jcr.Property.JCR_LAST_MODIFIED;
5 import static javax.jcr.Property.JCR_TITLE;
6 import static org.argeo.cms.CmsTypes.CMS_IMAGE;
7
8 import java.io.IOException;
9 import java.io.PrintWriter;
10 import java.net.MalformedURLException;
11 import java.net.URL;
12 import java.security.PrivilegedExceptionAction;
13 import java.security.cert.X509Certificate;
14 import java.util.Calendar;
15 import java.util.Collection;
16 import java.util.Enumeration;
17
18 import javax.jcr.Node;
19 import javax.jcr.NodeIterator;
20 import javax.jcr.Repository;
21 import javax.jcr.RepositoryException;
22 import javax.jcr.Session;
23 import javax.security.auth.Subject;
24 import javax.servlet.FilterChain;
25 import javax.servlet.ServletException;
26 import javax.servlet.http.HttpServlet;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
29 import javax.servlet.http.HttpSession;
30
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 import org.argeo.cms.CmsException;
34 import org.argeo.jcr.JcrUtils;
35 import org.argeo.node.NodeConstants;
36 import org.argeo.node.NodeUtils;
37 import org.osgi.framework.BundleContext;
38 import org.osgi.framework.ServiceReference;
39 import org.osgi.service.http.HttpService;
40
41 /**
42 * Intercepts and enriches http access, mainly focusing on security and
43 * transactionality.
44 */
45 class NodeHttp implements KernelConstants {
46 private final static Log log = LogFactory.getLog(NodeHttp.class);
47
48 // Filters
49 // private final RootFilter rootFilter;
50
51 // private final DoSFilter dosFilter;
52 // private final QoSFilter qosFilter;
53
54 private BundleContext bc;
55
56 NodeHttp(HttpService httpService, BundleContext bc) {
57 this.bc = bc;
58 // rootFilter = new RootFilter();
59 // dosFilter = new CustomDosFilter();
60 // qosFilter = new QoSFilter();
61
62 try {
63 httpService.registerServlet("/!", new LinkServlet(), null, null);
64 httpService.registerServlet("/robots.txt", new RobotServlet(), null, null);
65 } catch (Exception e) {
66 throw new CmsException("Cannot register filters", e);
67 }
68 }
69
70 public void destroy() {
71 }
72
73 class LinkServlet extends HttpServlet {
74 private static final long serialVersionUID = 3749990143146845708L;
75
76 @Override
77 protected void service(HttpServletRequest request, HttpServletResponse response)
78 throws ServletException, IOException {
79 String path = request.getPathInfo();
80 String userAgent = request.getHeader("User-Agent").toLowerCase();
81 boolean isBot = false;
82 boolean isCompatibleBrowser = false;
83 if (userAgent.contains("bot") || userAgent.contains("facebook") || userAgent.contains("twitter")) {
84 isBot = true;
85 } else if (userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox")
86 || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium")
87 || userAgent.contains("opera") || userAgent.contains("browser")) {
88 isCompatibleBrowser = true;
89 }
90
91 if (isBot) {
92 log.warn("# BOT " + request.getHeader("User-Agent"));
93 canonicalAnswer(request, response, path);
94 return;
95 }
96
97 if (isCompatibleBrowser && log.isTraceEnabled())
98 log.trace("# BWS " + request.getHeader("User-Agent"));
99 redirectTo(response, "/#" + path);
100 }
101
102 private void redirectTo(HttpServletResponse response, String location) {
103 response.setHeader("Location", location);
104 response.setStatus(HttpServletResponse.SC_FOUND);
105 }
106
107 // private boolean canonicalAnswerNeededBy(HttpServletRequest request) {
108 // String userAgent = request.getHeader("User-Agent").toLowerCase();
109 // return userAgent.startsWith("facebookexternalhit/");
110 // }
111
112 /** For bots which don't understand RWT. */
113 private void canonicalAnswer(HttpServletRequest request, HttpServletResponse response, String path) {
114 Session session = null;
115 try {
116 PrintWriter writer = response.getWriter();
117 session = Subject.doAs(KernelUtils.anonymousLogin(), new PrivilegedExceptionAction<Session>() {
118
119 @Override
120 public Session run() throws Exception {
121 Collection<ServiceReference<Repository>> srs = bc.getServiceReferences(Repository.class,
122 "(" + NodeConstants.CN + "=" + NodeConstants.NODE + ")");
123 Repository repository = bc.getService(srs.iterator().next());
124 return repository.login();
125 }
126
127 });
128 Node node = session.getNode(path);
129 String title = node.hasProperty(JCR_TITLE) ? node.getProperty(JCR_TITLE).getString() : node.getName();
130 String desc = node.hasProperty(JCR_DESCRIPTION) ? node.getProperty(JCR_DESCRIPTION).getString() : null;
131 Calendar lastUpdate = node.hasProperty(JCR_LAST_MODIFIED)
132 ? node.getProperty(JCR_LAST_MODIFIED).getDate() : null;
133 String url = getCanonicalUrl(node, request);
134 String imgUrl = null;
135 loop: for (NodeIterator it = node.getNodes(); it.hasNext();) {
136 // Takes the first found cms:image
137 Node child = it.nextNode();
138 if (child.isNodeType(CMS_IMAGE)) {
139 imgUrl = getDataUrl(child, request);
140 break loop;
141 }
142 }
143 StringBuilder buf = new StringBuilder();
144 buf.append("<html>");
145 buf.append("<head>");
146 writeMeta(buf, "og:title", escapeHTML(title));
147 writeMeta(buf, "og:type", "website");
148 buf.append("<meta name='twitter:card' content='summary' />");
149 buf.append("<meta name='twitter:site' content='@argeo_org' />");
150 writeMeta(buf, "og:url", url);
151 if (desc != null)
152 writeMeta(buf, "og:description", escapeHTML(desc));
153 if (imgUrl != null)
154 writeMeta(buf, "og:image", imgUrl);
155 if (lastUpdate != null)
156 writeMeta(buf, "og:updated_time", Long.toString(lastUpdate.getTime().getTime()));
157 buf.append("</head>");
158 buf.append("<body>");
159 buf.append(
160 "<p><b>!! This page is meant for indexing robots, not for real people," + " visit <a href='/#")
161 .append(path).append("'>").append(escapeHTML(title)).append("</a> instead.</b></p>");
162 writeCanonical(buf, node);
163 buf.append("</body>");
164 buf.append("</html>");
165 writer.print(buf.toString());
166
167 response.setHeader("Content-Type", "text/html");
168 writer.flush();
169 } catch (Exception e) {
170 throw new CmsException("Cannot write canonical answer", e);
171 } finally {
172 JcrUtils.logoutQuietly(session);
173 }
174 }
175
176 /**
177 * From
178 * http://stackoverflow.com/questions/1265282/recommended-method-for-
179 * escaping-html-in-java (+ escaping '). TODO Use
180 * org.apache.commons.lang.StringEscapeUtils
181 */
182 private String escapeHTML(String s) {
183 StringBuilder out = new StringBuilder(Math.max(16, s.length()));
184 for (int i = 0; i < s.length(); i++) {
185 char c = s.charAt(i);
186 if (c > 127 || c == '\'' || c == '"' || c == '<' || c == '>' || c == '&') {
187 out.append("&#");
188 out.append((int) c);
189 out.append(';');
190 } else {
191 out.append(c);
192 }
193 }
194 return out.toString();
195 }
196
197 private void writeMeta(StringBuilder buf, String tag, String value) {
198 buf.append("<meta property='").append(tag).append("' content='").append(value).append("'/>");
199 }
200
201 private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException {
202 buf.append("<div>");
203 if (node.hasProperty(JCR_TITLE))
204 buf.append("<p>").append(node.getProperty(JCR_TITLE).getString()).append("</p>");
205 if (node.hasProperty(JCR_DESCRIPTION))
206 buf.append("<p>").append(node.getProperty(JCR_DESCRIPTION).getString()).append("</p>");
207 NodeIterator children = node.getNodes();
208 while (children.hasNext()) {
209 writeCanonical(buf, children.nextNode());
210 }
211 buf.append("</div>");
212 }
213
214 // DATA
215 private StringBuilder getServerBaseUrl(HttpServletRequest request) {
216 try {
217 URL url = new URL(request.getRequestURL().toString());
218 StringBuilder buf = new StringBuilder();
219 buf.append(url.getProtocol()).append("://").append(url.getHost());
220 if (url.getPort() != -1)
221 buf.append(':').append(url.getPort());
222 return buf;
223 } catch (MalformedURLException e) {
224 throw new CmsException("Cannot extract server base URL from " + request.getRequestURL(), e);
225 }
226 }
227
228 private String getDataUrl(Node node, HttpServletRequest request) throws RepositoryException {
229 try {
230 StringBuilder buf = getServerBaseUrl(request);
231 buf.append(NodeUtils.getDataPath(NodeConstants.NODE, node));
232 return new URL(buf.toString()).toString();
233 } catch (MalformedURLException e) {
234 throw new CmsException("Cannot build data URL for " + node, e);
235 }
236 }
237
238 // public static String getDataPath(Node node) throws
239 // RepositoryException {
240 // assert node != null;
241 // String userId = node.getSession().getUserID();
242 //// if (log.isTraceEnabled())
243 //// log.trace(userId + " : " + node.getPath());
244 // StringBuilder buf = new StringBuilder();
245 // boolean isAnonymous =
246 // userId.equalsIgnoreCase(NodeConstants.ROLE_ANONYMOUS);
247 // if (isAnonymous)
248 // buf.append(WEBDAV_PUBLIC);
249 // else
250 // buf.append(WEBDAV_PRIVATE);
251 // Session session = node.getSession();
252 // Repository repository = session.getRepository();
253 // String cn;
254 // if (repository.isSingleValueDescriptor(NodeConstants.CN)) {
255 // cn = repository.getDescriptor(NodeConstants.CN);
256 // } else {
257 //// log.warn("No cn defined in repository, using " +
258 // NodeConstants.NODE);
259 // cn = NodeConstants.NODE;
260 // }
261 // return
262 // buf.append('/').append(cn).append('/').append(session.getWorkspace().getName()).append(node.getPath())
263 // .toString();
264 // }
265
266 private String getCanonicalUrl(Node node, HttpServletRequest request) throws RepositoryException {
267 try {
268 StringBuilder buf = getServerBaseUrl(request);
269 buf.append('/').append('!').append(node.getPath());
270 return new URL(buf.toString()).toString();
271 } catch (MalformedURLException e) {
272 throw new CmsException("Cannot build data URL for " + node, e);
273 }
274 // return request.getRequestURL().append('!').append(node.getPath())
275 // .toString();
276 }
277
278 }
279
280 class RobotServlet extends HttpServlet {
281 private static final long serialVersionUID = 7935661175336419089L;
282
283 @Override
284 protected void service(HttpServletRequest request, HttpServletResponse response)
285 throws ServletException, IOException {
286 PrintWriter writer = response.getWriter();
287 writer.append("User-agent: *\n");
288 writer.append("Disallow:\n");
289 response.setHeader("Content-Type", "text/plain");
290 writer.flush();
291 }
292
293 }
294
295 /** Intercepts all requests. Authenticates. */
296 class RootFilter extends HttpFilter {
297
298 @Override
299 public void doFilter(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response,
300 FilterChain filterChain) throws IOException, ServletException {
301 if (log.isTraceEnabled()) {
302 log.trace(request.getRequestURL()
303 .append(request.getQueryString() != null ? "?" + request.getQueryString() : ""));
304 logRequest(request);
305 }
306
307 String servletPath = request.getServletPath();
308
309 // client certificate
310 X509Certificate clientCert = extractCertificate(request);
311 if (clientCert != null) {
312 // TODO authenticate
313 // if (log.isDebugEnabled())
314 // log.debug(clientCert.getSubjectX500Principal().getName());
315 }
316
317 // skip data
318 if (servletPath.startsWith(NodeConstants.PATH_DATA)) {
319 filterChain.doFilter(request, response);
320 return;
321 }
322
323 // skip /ui (workbench) for the time being
324 if (servletPath.startsWith(PATH_WORKBENCH)) {
325 filterChain.doFilter(request, response);
326 return;
327 }
328
329 // redirect long RWT paths to anchor
330 String path = request.getRequestURI().substring(servletPath.length());
331 int pathLength = path.length();
332 if (pathLength != 0 && (path.charAt(0) == '/') && !servletPath.endsWith("rwt-resources")
333 && !path.startsWith(KernelConstants.PATH_WORKBENCH) && path.lastIndexOf('/') != 0) {
334 String newLocation = request.getServletPath() + "#" + path;
335 response.setHeader("Location", newLocation);
336 response.setStatus(HttpServletResponse.SC_FOUND);
337 return;
338 }
339
340 // process normally
341 filterChain.doFilter(request, response);
342 }
343 }
344
345 private void logRequest(HttpServletRequest request) {
346 log.debug("contextPath=" + request.getContextPath());
347 log.debug("servletPath=" + request.getServletPath());
348 log.debug("requestURI=" + request.getRequestURI());
349 log.debug("queryString=" + request.getQueryString());
350 StringBuilder buf = new StringBuilder();
351 // headers
352 Enumeration<String> en = request.getHeaderNames();
353 while (en.hasMoreElements()) {
354 String header = en.nextElement();
355 Enumeration<String> values = request.getHeaders(header);
356 while (values.hasMoreElements())
357 buf.append(" " + header + ": " + values.nextElement());
358 buf.append('\n');
359 }
360
361 // attributed
362 Enumeration<String> an = request.getAttributeNames();
363 while (an.hasMoreElements()) {
364 String attr = an.nextElement();
365 Object value = request.getAttribute(attr);
366 buf.append(" " + attr + ": " + value);
367 buf.append('\n');
368 }
369 log.debug("\n" + buf);
370 }
371
372 private X509Certificate extractCertificate(HttpServletRequest req) {
373 X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
374 if (null != certs && certs.length > 0) {
375 return certs[0];
376 }
377 return null;
378 }
379
380 // class CustomDosFilter extends DoSFilter {
381 // @Override
382 // protected String extractUserId(ServletRequest request) {
383 // HttpSession httpSession = ((HttpServletRequest) request)
384 // .getSession();
385 // if (isSessionAuthenticated(httpSession)) {
386 // String userId = ((SecurityContext) httpSession
387 // .getAttribute(SPRING_SECURITY_CONTEXT_KEY))
388 // .getAuthentication().getName();
389 // return userId;
390 // }
391 // return super.extractUserId(request);
392 //
393 // }
394 // }
395 }