]> git.argeo.org Git - lgpl/argeo-commons.git/blob - NodeHttp.java
de7561aa04ae0155dd9a9522cf0fa2be68efe8b3
[lgpl/argeo-commons.git] / NodeHttp.java
1 package org.argeo.cms.internal.kernel;
2
3 import static org.argeo.cms.KernelHeader.ACCESS_CONTROL_CONTEXT;
4
5 import java.io.IOException;
6 import java.security.AccessControlContext;
7 import java.security.AccessController;
8 import java.security.PrivilegedActionException;
9 import java.security.PrivilegedExceptionAction;
10 import java.security.cert.X509Certificate;
11 import java.util.Enumeration;
12 import java.util.Properties;
13 import java.util.StringTokenizer;
14
15 import javax.jcr.Repository;
16 import javax.security.auth.Subject;
17 import javax.security.auth.callback.Callback;
18 import javax.security.auth.callback.CallbackHandler;
19 import javax.security.auth.callback.NameCallback;
20 import javax.security.auth.callback.PasswordCallback;
21 import javax.security.auth.login.LoginContext;
22 import javax.security.auth.login.LoginException;
23 import javax.servlet.FilterChain;
24 import javax.servlet.Servlet;
25 import javax.servlet.ServletException;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28 import javax.servlet.http.HttpSession;
29
30 import org.apache.commons.codec.binary.Base64;
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 import org.argeo.cms.CmsException;
34 import org.argeo.cms.KernelHeader;
35 import org.argeo.jackrabbit.servlet.OpenInViewSessionProvider;
36 import org.argeo.jackrabbit.servlet.RemotingServlet;
37 import org.argeo.jackrabbit.servlet.WebdavServlet;
38 import org.argeo.jcr.ArgeoJcrConstants;
39 import org.eclipse.equinox.http.servlet.ExtendedHttpService;
40 import org.osgi.service.http.NamespaceException;
41
42 /**
43 * Intercepts and enriches http access, mainly focusing on security and
44 * transactionality.
45 */
46 class NodeHttp implements KernelConstants, ArgeoJcrConstants {
47 private final static Log log = LogFactory.getLog(NodeHttp.class);
48
49 private final static String ATTR_AUTH = "auth";
50 private final static String HEADER_AUTHORIZATION = "Authorization";
51 private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
52
53 // private final AuthenticationManager authenticationManager;
54 private final ExtendedHttpService httpService;
55
56 // FIXME Make it more unique
57 private String httpAuthRealm = "Argeo";
58
59 // Filters
60 private final RootFilter rootFilter;
61 // private final DoSFilter dosFilter;
62 // private final QoSFilter qosFilter;
63
64 // WebDav / JCR remoting
65 private OpenInViewSessionProvider sessionProvider;
66
67 NodeHttp(ExtendedHttpService httpService, JackrabbitNode node) {
68 // this.bundleContext = bundleContext;
69 // this.authenticationManager = authenticationManager;
70
71 this.httpService = httpService;
72
73 // Filters
74 rootFilter = new RootFilter();
75 // dosFilter = new CustomDosFilter();
76 // qosFilter = new QoSFilter();
77
78 // DAV
79 sessionProvider = new OpenInViewSessionProvider();
80
81 registerRepositoryServlets(ALIAS_NODE, node);
82 try {
83 httpService.registerFilter("/", rootFilter, null, null);
84 } catch (Exception e) {
85 throw new CmsException("Could not register root filter", e);
86 }
87 }
88
89 public void destroy() {
90 sessionProvider.destroy();
91 unregisterRepositoryServlets(ALIAS_NODE);
92 }
93
94 void registerRepositoryServlets(String alias, Repository repository) {
95 try {
96 registerWebdavServlet(alias, repository, true);
97 registerWebdavServlet(alias, repository, false);
98 registerRemotingServlet(alias, repository, true);
99 registerRemotingServlet(alias, repository, false);
100 } catch (Exception e) {
101 throw new CmsException(
102 "Could not register servlets for repository " + alias, e);
103 }
104 }
105
106 void unregisterRepositoryServlets(String alias) {
107 // FIXME unregister servlets
108 }
109
110 void registerWebdavServlet(String alias, Repository repository,
111 boolean anonymous) throws NamespaceException, ServletException {
112 WebdavServlet webdavServlet = new WebdavServlet(repository,
113 sessionProvider);
114 String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE;
115 String path = pathPrefix + "/" + alias;
116 Properties ip = new Properties();
117 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG);
118 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
119 httpService.registerFilter(path, anonymous ? new AnonymousFilter()
120 : new DavFilter(), null, null);
121 // Cast to servlet because of a weird behaviour in Eclipse
122 httpService.registerServlet(path, (Servlet) webdavServlet, ip, null);
123 }
124
125 void registerRemotingServlet(String alias, Repository repository,
126 boolean anonymous) throws NamespaceException, ServletException {
127 String pathPrefix = anonymous ? REMOTING_PUBLIC : REMOTING_PRIVATE;
128 RemotingServlet remotingServlet = new RemotingServlet(repository,
129 sessionProvider);
130 String path = pathPrefix + "/" + alias;
131 Properties ip = new Properties();
132 ip.setProperty(RemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
133
134 // Looks like a bug in Jackrabbit remoting init
135 ip.setProperty(RemotingServlet.INIT_PARAM_HOME,
136 KernelUtils.getOsgiInstanceDir() + "/tmp/jackrabbit");
137 ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting");
138 // in order to avoid annoying warning.
139 ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG,
140 "");
141 // Cast to servlet because of a weird behaviour in Eclipse
142 httpService.registerFilter(path, anonymous ? new AnonymousFilter()
143 : new DavFilter(), null, null);
144 httpService.registerServlet(path, (Servlet) remotingServlet, ip, null);
145 }
146
147 // private Boolean isSessionAuthenticated(HttpSession httpSession) {
148 // SecurityContext contextFromSession = (SecurityContext) httpSession
149 // .getAttribute(SPRING_SECURITY_CONTEXT_KEY);
150 // return contextFromSession != null;
151 // }
152
153 private void requestBasicAuth(HttpSession httpSession,
154 HttpServletResponse response) {
155 response.setStatus(401);
156 response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\""
157 + httpAuthRealm + "\"");
158 httpSession.setAttribute(ATTR_AUTH, Boolean.TRUE);
159 }
160
161 private CallbackHandler basicAuth(String authHeader) {
162 if (authHeader != null) {
163 StringTokenizer st = new StringTokenizer(authHeader);
164 if (st.hasMoreTokens()) {
165 String basic = st.nextToken();
166 if (basic.equalsIgnoreCase("Basic")) {
167 try {
168 // TODO manipulate char[]
169 String credentials = new String(Base64.decodeBase64(st
170 .nextToken()), "UTF-8");
171 // log.debug("Credentials: " + credentials);
172 int p = credentials.indexOf(":");
173 if (p != -1) {
174 final String login = credentials.substring(0, p)
175 .trim();
176 final char[] password = credentials
177 .substring(p + 1).trim().toCharArray();
178
179 return new CallbackHandler() {
180 public void handle(Callback[] callbacks) {
181 for (Callback cb : callbacks) {
182 if (cb instanceof NameCallback)
183 ((NameCallback) cb).setName(login);
184 else if (cb instanceof PasswordCallback)
185 ((PasswordCallback) cb)
186 .setPassword(password);
187 }
188 }
189 };
190 } else {
191 throw new CmsException(
192 "Invalid authentication token");
193 }
194 } catch (Exception e) {
195 throw new CmsException(
196 "Couldn't retrieve authentication", e);
197 }
198 }
199 }
200 }
201 throw new CmsException("Couldn't retrieve authentication");
202 }
203
204 /** Intercepts all requests. Authenticates. */
205 class RootFilter extends HttpFilter {
206
207 @Override
208 public void doFilter(HttpSession httpSession,
209 HttpServletRequest request, HttpServletResponse response,
210 FilterChain filterChain) throws IOException, ServletException {
211 if (log.isTraceEnabled()) {
212 log.trace(request.getRequestURL().append(
213 request.getQueryString() != null ? "?"
214 + request.getQueryString() : ""));
215 logRequest(request);
216 }
217
218 String servletPath = request.getServletPath();
219
220 // client certificate
221 X509Certificate clientCert = extractCertificate(request);
222 if (clientCert != null) {
223 // TODO authenticate
224 // if (log.isDebugEnabled())
225 // log.debug(clientCert.getSubjectX500Principal().getName());
226 }
227
228 // skip data
229 if (servletPath.startsWith(PATH_DATA)) {
230 filterChain.doFilter(request, response);
231 return;
232 }
233
234 // skip /ui (workbench) for the time being
235 if (servletPath.startsWith(PATH_WORKBENCH)) {
236 filterChain.doFilter(request, response);
237 return;
238 }
239
240 // redirect long RWT paths to anchor
241 String path = request.getRequestURI().substring(
242 servletPath.length());
243 int pathLength = path.length();
244 if (pathLength != 0 && (path.charAt(0) == '/')
245 && !servletPath.endsWith("rwt-resources")
246 && !path.startsWith(KernelConstants.PATH_WORKBENCH)
247 && path.lastIndexOf('/') != 0) {
248 String newLocation = request.getServletPath() + "#" + path;
249 response.setHeader("Location", newLocation);
250 response.setStatus(HttpServletResponse.SC_FOUND);
251 return;
252 }
253
254 // process normally
255 filterChain.doFilter(request, response);
256 }
257 }
258
259 private void logRequest(HttpServletRequest request) {
260 log.debug("contextPath=" + request.getContextPath());
261 log.debug("servletPath=" + request.getServletPath());
262 log.debug("requestURI=" + request.getRequestURI());
263 log.debug("queryString=" + request.getQueryString());
264 StringBuilder buf = new StringBuilder();
265 // headers
266 Enumeration<String> en = request.getHeaderNames();
267 while (en.hasMoreElements()) {
268 String header = en.nextElement();
269 Enumeration<String> values = request.getHeaders(header);
270 while (values.hasMoreElements())
271 buf.append(" " + header + ": " + values.nextElement());
272 buf.append('\n');
273 }
274
275 // attributed
276 Enumeration<String> an = request.getAttributeNames();
277 while (an.hasMoreElements()) {
278 String attr = an.nextElement();
279 Object value = request.getAttribute(attr);
280 buf.append(" " + attr + ": " + value);
281 buf.append('\n');
282 }
283 log.debug("\n" + buf);
284 }
285
286 private X509Certificate extractCertificate(HttpServletRequest req) {
287 X509Certificate[] certs = (X509Certificate[]) req
288 .getAttribute("javax.servlet.request.X509Certificate");
289 if (null != certs && certs.length > 0) {
290 return certs[0];
291 }
292 return null;
293 }
294
295 /** Intercepts all requests. Authenticates. */
296 private class AnonymousFilter extends HttpFilter {
297 @Override
298 public void doFilter(HttpSession httpSession,
299 final HttpServletRequest request,
300 final HttpServletResponse response,
301 final FilterChain filterChain) throws IOException,
302 ServletException {
303
304 // Authenticate from session
305 // if (isSessionAuthenticated(httpSession)) {
306 // filterChain.doFilter(request, response);
307 // return;
308 // }
309
310 Subject subject = KernelUtils.anonymousLogin();
311 try {
312 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
313 public Void run() throws IOException, ServletException {
314 filterChain.doFilter(request, response);
315 return null;
316 }
317 });
318 } catch (PrivilegedActionException e) {
319 if (e.getCause() instanceof ServletException)
320 throw (ServletException) e.getCause();
321 else if (e.getCause() instanceof IOException)
322 throw (IOException) e.getCause();
323 else
324 throw new CmsException("Unexpected exception", e.getCause());
325 }
326 }
327 }
328
329 /** Intercepts all requests. Authenticates. */
330 private class DavFilter extends HttpFilter {
331
332 @Override
333 public void doFilter(final HttpSession httpSession,
334 final HttpServletRequest request,
335 final HttpServletResponse response,
336 final FilterChain filterChain) throws IOException,
337 ServletException {
338
339 AccessControlContext acc = (AccessControlContext) httpSession
340 .getAttribute(KernelHeader.ACCESS_CONTROL_CONTEXT);
341 final Subject subject;
342 if (acc != null) {
343 subject = Subject.getSubject(acc);
344 } else {
345 // Process basic auth
346 String basicAuth = request.getHeader(HEADER_AUTHORIZATION);
347 if (basicAuth != null) {
348 CallbackHandler token = basicAuth(basicAuth);
349 try {
350 LoginContext lc = new LoginContext(
351 KernelHeader.LOGIN_CONTEXT_USER, token);
352 lc.login();
353 subject = lc.getSubject();
354 } catch (LoginException e) {
355 throw new CmsException("Could not login", e);
356 }
357 } else {
358 requestBasicAuth(httpSession, response);
359 return;
360 }
361 }
362 // do filter as subject
363 try {
364 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
365 public Void run() throws IOException, ServletException {
366 // add security context to session
367 httpSession.setAttribute(ACCESS_CONTROL_CONTEXT,
368 AccessController.getContext());
369 filterChain.doFilter(request, response);
370 return null;
371 }
372 });
373 } catch (PrivilegedActionException e) {
374 if (e.getCause() instanceof ServletException)
375 throw (ServletException) e.getCause();
376 else if (e.getCause() instanceof IOException)
377 throw (IOException) e.getCause();
378 else
379 throw new CmsException("Unexpected exception", e.getCause());
380 }
381
382 }
383 }
384
385 // class CustomDosFilter extends DoSFilter {
386 // @Override
387 // protected String extractUserId(ServletRequest request) {
388 // HttpSession httpSession = ((HttpServletRequest) request)
389 // .getSession();
390 // if (isSessionAuthenticated(httpSession)) {
391 // String userId = ((SecurityContext) httpSession
392 // .getAttribute(SPRING_SECURITY_CONTEXT_KEY))
393 // .getAuthentication().getName();
394 // return userId;
395 // }
396 // return super.extractUserId(request);
397 //
398 // }
399 // }
400 }