]> git.argeo.org Git - lgpl/argeo-commons.git/blob - kernel/NodeHttp.java
Prepare next development cycle
[lgpl/argeo-commons.git] / kernel / 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.security.PrivilegedExceptionAction;
11 import java.security.cert.X509Certificate;
12 import java.util.Calendar;
13 import java.util.Enumeration;
14
15 import javax.jcr.Node;
16 import javax.jcr.NodeIterator;
17 import javax.jcr.Repository;
18 import javax.jcr.RepositoryException;
19 import javax.jcr.Session;
20 import javax.security.auth.Subject;
21 import javax.servlet.FilterChain;
22 import javax.servlet.ServletException;
23 import javax.servlet.http.HttpServlet;
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
26 import javax.servlet.http.HttpSession;
27
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30 import org.argeo.cms.CmsException;
31 import org.argeo.cms.util.CmsUtils;
32 import org.argeo.jcr.ArgeoJcrConstants;
33 import org.argeo.jcr.JcrUtils;
34 import org.eclipse.equinox.http.servlet.ExtendedHttpService;
35
36 /**
37 * Intercepts and enriches http access, mainly focusing on security and
38 * transactionality.
39 */
40 class NodeHttp implements KernelConstants, ArgeoJcrConstants {
41 private final static Log log = LogFactory.getLog(NodeHttp.class);
42
43 // Filters
44 // private final RootFilter rootFilter;
45
46 // private final DoSFilter dosFilter;
47 // private final QoSFilter qosFilter;
48
49 private Repository repository;
50
51 NodeHttp(ExtendedHttpService httpService, Repository node) {
52 this.repository = node;
53 // rootFilter = new RootFilter();
54 // dosFilter = new CustomDosFilter();
55 // qosFilter = new QoSFilter();
56
57 try {
58 httpService.registerServlet("/!", new LinkServlet(repository), null, null);
59 httpService.registerServlet("/robots.txt", new RobotServlet(), null, null);
60 } catch (Exception e) {
61 throw new CmsException("Cannot register filters", e);
62 }
63 }
64
65 public void destroy() {
66 }
67
68 static class LinkServlet extends HttpServlet {
69 private static final long serialVersionUID = 3749990143146845708L;
70 private final Repository repository;
71
72 public LinkServlet(Repository repository) {
73 this.repository = repository;
74 }
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 return repository.login();
122 }
123
124 });
125 Node node = session.getNode(path);
126 String title = node.hasProperty(JCR_TITLE) ? node.getProperty(JCR_TITLE).getString() : node.getName();
127 String desc = node.hasProperty(JCR_DESCRIPTION) ? node.getProperty(JCR_DESCRIPTION).getString() : null;
128 Calendar lastUpdate = node.hasProperty(JCR_LAST_MODIFIED)
129 ? node.getProperty(JCR_LAST_MODIFIED).getDate() : null;
130 String url = CmsUtils.getCanonicalUrl(node, request);
131 String imgUrl = null;
132 loop: for (NodeIterator it = node.getNodes(); it.hasNext();) {
133 // Takes the first found cms:image
134 Node child = it.nextNode();
135 if (child.isNodeType(CMS_IMAGE)) {
136 imgUrl = CmsUtils.getDataUrl(child, request);
137 break loop;
138 }
139 }
140 StringBuilder buf = new StringBuilder();
141 buf.append("<html>");
142 buf.append("<head>");
143 writeMeta(buf, "og:title", escapeHTML(title));
144 writeMeta(buf, "og:type", "website");
145 buf.append("<meta name='twitter:card' content='summary' />");
146 buf.append("<meta name='twitter:site' content='@argeo_org' />");
147 writeMeta(buf, "og:url", url);
148 if (desc != null)
149 writeMeta(buf, "og:description", escapeHTML(desc));
150 if (imgUrl != null)
151 writeMeta(buf, "og:image", imgUrl);
152 if (lastUpdate != null)
153 writeMeta(buf, "og:updated_time", Long.toString(lastUpdate.getTime().getTime()));
154 buf.append("</head>");
155 buf.append("<body>");
156 buf.append(
157 "<p><b>!! This page is meant for indexing robots, not for real people," + " visit <a href='/#")
158 .append(path).append("'>").append(escapeHTML(title)).append("</a> instead.</b></p>");
159 writeCanonical(buf, node);
160 buf.append("</body>");
161 buf.append("</html>");
162 writer.print(buf.toString());
163
164 response.setHeader("Content-Type", "text/html");
165 writer.flush();
166 } catch (Exception e) {
167 throw new CmsException("Cannot write canonical answer", e);
168 } finally {
169 JcrUtils.logoutQuietly(session);
170 }
171 }
172
173 /**
174 * From
175 * http://stackoverflow.com/questions/1265282/recommended-method-for-
176 * escaping-html-in-java (+ escaping '). TODO Use
177 * org.apache.commons.lang.StringEscapeUtils
178 */
179 private static String escapeHTML(String s) {
180 StringBuilder out = new StringBuilder(Math.max(16, s.length()));
181 for (int i = 0; i < s.length(); i++) {
182 char c = s.charAt(i);
183 if (c > 127 || c == '\'' || c == '"' || c == '<' || c == '>' || c == '&') {
184 out.append("&#");
185 out.append((int) c);
186 out.append(';');
187 } else {
188 out.append(c);
189 }
190 }
191 return out.toString();
192 }
193
194 private void writeMeta(StringBuilder buf, String tag, String value) {
195 buf.append("<meta property='").append(tag).append("' content='").append(value).append("'/>");
196 }
197
198 private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException {
199 buf.append("<div>");
200 if (node.hasProperty(JCR_TITLE))
201 buf.append("<p>").append(node.getProperty(JCR_TITLE).getString()).append("</p>");
202 if (node.hasProperty(JCR_DESCRIPTION))
203 buf.append("<p>").append(node.getProperty(JCR_DESCRIPTION).getString()).append("</p>");
204 NodeIterator children = node.getNodes();
205 while (children.hasNext()) {
206 writeCanonical(buf, children.nextNode());
207 }
208 buf.append("</div>");
209 }
210 }
211
212 class RobotServlet extends HttpServlet {
213 private static final long serialVersionUID = 7935661175336419089L;
214
215 @Override
216 protected void service(HttpServletRequest request, HttpServletResponse response)
217 throws ServletException, IOException {
218 PrintWriter writer = response.getWriter();
219 writer.append("User-agent: *\n");
220 writer.append("Disallow:\n");
221 response.setHeader("Content-Type", "text/plain");
222 writer.flush();
223 }
224
225 }
226
227 /** Intercepts all requests. Authenticates. */
228 class RootFilter extends HttpFilter {
229
230 @Override
231 public void doFilter(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response,
232 FilterChain filterChain) throws IOException, ServletException {
233 if (log.isTraceEnabled()) {
234 log.trace(request.getRequestURL()
235 .append(request.getQueryString() != null ? "?" + request.getQueryString() : ""));
236 logRequest(request);
237 }
238
239 String servletPath = request.getServletPath();
240
241 // client certificate
242 X509Certificate clientCert = extractCertificate(request);
243 if (clientCert != null) {
244 // TODO authenticate
245 // if (log.isDebugEnabled())
246 // log.debug(clientCert.getSubjectX500Principal().getName());
247 }
248
249 // skip data
250 if (servletPath.startsWith(PATH_DATA)) {
251 filterChain.doFilter(request, response);
252 return;
253 }
254
255 // skip /ui (workbench) for the time being
256 if (servletPath.startsWith(PATH_WORKBENCH)) {
257 filterChain.doFilter(request, response);
258 return;
259 }
260
261 // redirect long RWT paths to anchor
262 String path = request.getRequestURI().substring(servletPath.length());
263 int pathLength = path.length();
264 if (pathLength != 0 && (path.charAt(0) == '/') && !servletPath.endsWith("rwt-resources")
265 && !path.startsWith(KernelConstants.PATH_WORKBENCH) && path.lastIndexOf('/') != 0) {
266 String newLocation = request.getServletPath() + "#" + path;
267 response.setHeader("Location", newLocation);
268 response.setStatus(HttpServletResponse.SC_FOUND);
269 return;
270 }
271
272 // process normally
273 filterChain.doFilter(request, response);
274 }
275 }
276
277 private void logRequest(HttpServletRequest request) {
278 log.debug("contextPath=" + request.getContextPath());
279 log.debug("servletPath=" + request.getServletPath());
280 log.debug("requestURI=" + request.getRequestURI());
281 log.debug("queryString=" + request.getQueryString());
282 StringBuilder buf = new StringBuilder();
283 // headers
284 Enumeration<String> en = request.getHeaderNames();
285 while (en.hasMoreElements()) {
286 String header = en.nextElement();
287 Enumeration<String> values = request.getHeaders(header);
288 while (values.hasMoreElements())
289 buf.append(" " + header + ": " + values.nextElement());
290 buf.append('\n');
291 }
292
293 // attributed
294 Enumeration<String> an = request.getAttributeNames();
295 while (an.hasMoreElements()) {
296 String attr = an.nextElement();
297 Object value = request.getAttribute(attr);
298 buf.append(" " + attr + ": " + value);
299 buf.append('\n');
300 }
301 log.debug("\n" + buf);
302 }
303
304 private X509Certificate extractCertificate(HttpServletRequest req) {
305 X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
306 if (null != certs && certs.length > 0) {
307 return certs[0];
308 }
309 return null;
310 }
311
312 // class CustomDosFilter extends DoSFilter {
313 // @Override
314 // protected String extractUserId(ServletRequest request) {
315 // HttpSession httpSession = ((HttpServletRequest) request)
316 // .getSession();
317 // if (isSessionAuthenticated(httpSession)) {
318 // String userId = ((SecurityContext) httpSession
319 // .getAttribute(SPRING_SECURITY_CONTEXT_KEY))
320 // .getAuthentication().getName();
321 // return userId;
322 // }
323 // return super.extractUserId(request);
324 //
325 // }
326 // }
327 }