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