]> git.argeo.org Git - lgpl/argeo-commons.git/blob - DataHttp.java
97ca4bb31fd1aab67466efd3742619020b31cc8d
[lgpl/argeo-commons.git] / DataHttp.java
1 package org.argeo.cms.internal.kernel;
2
3 import java.io.IOException;
4 import java.io.Serializable;
5 import java.net.URL;
6 import java.security.PrivilegedActionException;
7 import java.security.PrivilegedExceptionAction;
8 import java.util.Properties;
9 import java.util.StringTokenizer;
10
11 import javax.jcr.Repository;
12 import javax.jcr.RepositoryException;
13 import javax.jcr.Session;
14 import javax.security.auth.Subject;
15 import javax.security.auth.callback.Callback;
16 import javax.security.auth.callback.CallbackHandler;
17 import javax.security.auth.callback.NameCallback;
18 import javax.security.auth.callback.PasswordCallback;
19 import javax.security.auth.login.CredentialNotFoundException;
20 import javax.security.auth.login.LoginContext;
21 import javax.security.auth.login.LoginException;
22 import javax.servlet.ServletException;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25
26 import org.apache.commons.codec.binary.Base64;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 import org.apache.jackrabbit.server.SessionProvider;
30 import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet;
31 import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
32 import org.argeo.cms.CmsException;
33 import org.argeo.cms.auth.HttpRequestCallback;
34 import org.argeo.cms.auth.HttpRequestCallbackHandler;
35 import org.argeo.jcr.JcrUtils;
36 import org.argeo.node.NodeConstants;
37 import org.osgi.framework.BundleContext;
38 import org.osgi.framework.FrameworkUtil;
39 import org.osgi.framework.ServiceReference;
40 import org.osgi.service.http.HttpContext;
41 import org.osgi.service.http.HttpService;
42 import org.osgi.service.http.NamespaceException;
43 import org.osgi.service.useradmin.Authorization;
44 import org.osgi.util.tracker.ServiceTracker;
45 import org.osgi.util.tracker.ServiceTrackerCustomizer;
46
47 /**
48 * Intercepts and enriches http access, mainly focusing on security and
49 * transactionality.
50 */
51 class DataHttp implements KernelConstants {
52 private final static Log log = LogFactory.getLog(DataHttp.class);
53
54 // private final static String ATTR_AUTH = "auth";
55 private final static String HEADER_AUTHORIZATION = "Authorization";
56 private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
57
58 private final static String DEFAULT_PROTECTED_HANDLERS = "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
59
60 private final BundleContext bc;
61 private final HttpService httpService;
62 private final ServiceTracker<Repository, Repository> repositories;
63
64 // FIXME Make it more unique
65 private String httpAuthRealm = "Argeo";
66
67 DataHttp(HttpService httpService) {
68 this.bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
69 this.httpService = httpService;
70 repositories = new ServiceTracker<>(bc, Repository.class, new RepositoriesStc());
71 repositories.open();
72 }
73
74 public void destroy() {
75 repositories.close();
76 }
77
78 void registerRepositoryServlets(String alias, Repository repository) {
79 try {
80 registerWebdavServlet(alias, repository);
81 // registerWebdavServlet(alias, repository, false);
82 // registerRemotingServlet(alias, repository, true);
83 registerRemotingServlet(alias, repository);
84 if (log.isDebugEnabled())
85 log.debug("Registered servlets for repository '" + alias + "'");
86 } catch (Exception e) {
87 throw new CmsException("Could not register servlets for repository '" + alias + "'", e);
88 }
89 }
90
91 void unregisterRepositoryServlets(String alias) {
92 try {
93 httpService.unregister(webdavPath(alias));
94 // httpService.unregister(webdavPath(alias, false));
95 // httpService.unregister(remotingPath(alias, true));
96 httpService.unregister(remotingPath(alias));
97 if (log.isDebugEnabled())
98 log.debug("Unregistered servlets for repository '" + alias + "'");
99 } catch (Exception e) {
100 log.error("Could not unregister servlets for repository '" + alias + "'", e);
101 }
102 }
103
104 void registerWebdavServlet(String alias, Repository repository) throws NamespaceException, ServletException {
105 WebdavServlet webdavServlet = new WebdavServlet(repository, new OpenInViewSessionProvider(alias));
106 String path = webdavPath(alias);
107 Properties ip = new Properties();
108 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG);
109 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
110 httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext());
111 }
112
113 void registerRemotingServlet(String alias, Repository repository) throws NamespaceException, ServletException {
114 RemotingServlet remotingServlet = new RemotingServlet(repository, new OpenInViewSessionProvider(alias));
115 String path = remotingPath(alias);
116 Properties ip = new Properties();
117 ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
118
119 // Looks like a bug in Jackrabbit remoting init
120 ip.setProperty(RemotingServlet.INIT_PARAM_HOME, KernelUtils.getOsgiInstanceDir() + "/tmp/remoting_" + alias);
121 ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting_" + alias);
122 ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, DEFAULT_PROTECTED_HANDLERS);
123 ip.setProperty(RemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false");
124 httpService.registerServlet(path, remotingServlet, ip, new RemotingHttpContext());
125 }
126
127 private String webdavPath(String alias) {
128 return NodeConstants.PATH_DATA + "/" + alias;
129 // String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE;
130 // return pathPrefix + "/" + alias;
131 }
132
133 private String remotingPath(String alias) {
134 return NodeConstants.PATH_JCR + "/" + alias;
135 // String pathPrefix = anonymous ? NodeConstants.PATH_JCR_PUB :
136 // NodeConstants.PATH_JCR;
137 }
138
139 private Subject subjectFromRequest(HttpServletRequest request) {
140 Authorization authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
141 if (authorization == null)
142 throw new CmsException("Not authenticated");
143 try {
144 LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
145 new HttpRequestCallbackHandler(request));
146 lc.login();
147 return lc.getSubject();
148 } catch (LoginException e) {
149 throw new CmsException("Cannot login", e);
150 }
151 }
152
153 private void requestBasicAuth(HttpServletRequest request, HttpServletResponse response) {
154 response.setStatus(401);
155 response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" + httpAuthRealm + "\"");
156 // request.getSession().setAttribute(ATTR_AUTH, Boolean.TRUE);
157 }
158
159 private CallbackHandler basicAuth(final HttpServletRequest httpRequest) {
160 String authHeader = httpRequest.getHeader(HEADER_AUTHORIZATION);
161 if (authHeader != null) {
162 StringTokenizer st = new StringTokenizer(authHeader);
163 if (st.hasMoreTokens()) {
164 String basic = st.nextToken();
165 if (basic.equalsIgnoreCase("Basic")) {
166 try {
167 // TODO manipulate char[]
168 String credentials = new String(Base64.decodeBase64(st.nextToken()), "UTF-8");
169 // log.debug("Credentials: " + credentials);
170 int p = credentials.indexOf(":");
171 if (p != -1) {
172 final String login = credentials.substring(0, p).trim();
173 final char[] password = credentials.substring(p + 1).trim().toCharArray();
174 return new CallbackHandler() {
175 public void handle(Callback[] callbacks) {
176 for (Callback cb : callbacks) {
177 if (cb instanceof NameCallback)
178 ((NameCallback) cb).setName(login);
179 else if (cb instanceof PasswordCallback)
180 ((PasswordCallback) cb).setPassword(password);
181 else if (cb instanceof HttpRequestCallback)
182 ((HttpRequestCallback) cb).setRequest(httpRequest);
183 }
184 }
185 };
186 } else {
187 throw new CmsException("Invalid authentication token");
188 }
189 } catch (Exception e) {
190 throw new CmsException("Couldn't retrieve authentication", e);
191 }
192 }
193 }
194 }
195 return null;
196 }
197
198 private class RepositoriesStc implements ServiceTrackerCustomizer<Repository, Repository> {
199
200 @Override
201 public Repository addingService(ServiceReference<Repository> reference) {
202 Repository repository = bc.getService(reference);
203 Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
204 if (jcrRepoAlias != null) {
205 String alias = jcrRepoAlias.toString();
206 registerRepositoryServlets(alias, repository);
207 }
208 return repository;
209 }
210
211 @Override
212 public void modifiedService(ServiceReference<Repository> reference, Repository service) {
213 }
214
215 @Override
216 public void removedService(ServiceReference<Repository> reference, Repository service) {
217 Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
218 if (jcrRepoAlias != null) {
219 String alias = jcrRepoAlias.toString();
220 unregisterRepositoryServlets(alias);
221 }
222 }
223 }
224
225 private class DataHttpContext implements HttpContext {
226 // private final boolean anonymous;
227
228 DataHttpContext() {
229 // this.anonymous = anonymous;
230 }
231
232 @Override
233 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
234 throws IOException {
235
236 // optimization
237 // HttpSession httpSession = request.getSession();
238 // Object remoteUser = httpSession.getAttribute(REMOTE_USER);
239 // Object authorization = httpSession.getAttribute(AUTHORIZATION);
240 // if (remoteUser != null && authorization != null) {
241 // request.setAttribute(REMOTE_USER, remoteUser);
242 // request.setAttribute(AUTHORIZATION, authorization);
243 // return true;
244 // }
245
246 // if (anonymous) {
247 // Subject subject = KernelUtils.anonymousLogin();
248 // Authorization authorization =
249 // subject.getPrivateCredentials(Authorization.class).iterator().next();
250 // request.setAttribute(REMOTE_USER, NodeConstants.ROLE_ANONYMOUS);
251 // request.setAttribute(AUTHORIZATION, authorization);
252 // return true;
253 // }
254
255 // if (log.isTraceEnabled())
256 KernelUtils.logRequestHeaders(log, request);
257 LoginContext lc;
258 try {
259 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request));
260 lc.login();
261 // return true;
262 } catch (CredentialNotFoundException e) {
263 CallbackHandler token = basicAuth(request);
264 if (token != null) {
265 try {
266 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
267 lc.login();
268 // Note: this is impossible to reliably clear the
269 // authorization header when access from a browser.
270 return true;
271 } catch (LoginException e1) {
272 throw new CmsException("Could not login", e1);
273 }
274 } else {
275 // anonymous
276 try {
277 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER);
278 lc.login();
279 } catch (LoginException e1) {
280 if (log.isDebugEnabled())
281 log.error("Cannot log in anonynous", e1);
282 return false;
283 }
284 }
285 // Subject subject = KernelUtils.anonymousLogin();
286 // authorization =
287 // subject.getPrivateCredentials(Authorization.class).iterator().next();
288 // request.setAttribute(REMOTE_USER,
289 // NodeConstants.ROLE_ANONYMOUS);
290 // request.setAttribute(AUTHORIZATION, authorization);
291 // httpSession.setAttribute(REMOTE_USER,
292 // NodeConstants.ROLE_ANONYMOUS);
293 // httpSession.setAttribute(AUTHORIZATION, authorization);
294 // return true;
295 // CallbackHandler token = basicAuth(request);
296 // if (token != null) {
297 // try {
298 // LoginContext lc = new
299 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
300 // lc.login();
301 // // Note: this is impossible to reliably clear the
302 // // authorization header when access from a browser.
303 // return true;
304 // } catch (LoginException e1) {
305 // throw new CmsException("Could not login", e1);
306 // }
307 // } else {
308 // String path = request.getServletPath();
309 // if (path.startsWith(REMOTING_PRIVATE))
310 // requestBasicAuth(request, response);
311 // return false;
312 // }
313 } catch (LoginException e) {
314 throw new CmsException("Could not login", e);
315 }
316 request.setAttribute(NodeConstants.LOGIN_CONTEXT_USER, lc);
317 return true;
318 }
319
320 @Override
321 public URL getResource(String name) {
322 return KernelUtils.getBundleContext(DataHttp.class).getBundle().getResource(name);
323 }
324
325 @Override
326 public String getMimeType(String name) {
327 return null;
328 }
329
330 }
331
332 private class RemotingHttpContext implements HttpContext {
333 // private final boolean anonymous;
334
335 RemotingHttpContext() {
336 // this.anonymous = anonymous;
337 }
338
339 @Override
340 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
341 throws IOException {
342
343 // if (anonymous) {
344 // Subject subject = KernelUtils.anonymousLogin();
345 // Authorization authorization =
346 // subject.getPrivateCredentials(Authorization.class).iterator().next();
347 // request.setAttribute(REMOTE_USER, NodeConstants.ROLE_ANONYMOUS);
348 // request.setAttribute(AUTHORIZATION, authorization);
349 // return true;
350 // }
351
352 if (log.isTraceEnabled())
353 KernelUtils.logRequestHeaders(log, request);
354 LoginContext lc;
355 try {
356 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request));
357 lc.login();
358 } catch (CredentialNotFoundException e) {
359 CallbackHandler token = basicAuth(request);
360 if (token != null) {
361 try {
362 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
363 lc.login();
364 // Note: this is impossible to reliably clear the
365 // authorization header when access from a browser.
366 } catch (LoginException e1) {
367 throw new CmsException("Could not login", e1);
368 }
369 } else {
370 requestBasicAuth(request, response);
371 lc = null;
372 }
373 } catch (LoginException e) {
374 throw new CmsException("Could not login", e);
375 }
376
377 if (lc != null) {
378 request.setAttribute(NodeConstants.LOGIN_CONTEXT_USER, lc);
379 return true;
380 } else {
381 return false;
382 }
383 }
384
385 @Override
386 public URL getResource(String name) {
387 return KernelUtils.getBundleContext(DataHttp.class).getBundle().getResource(name);
388 }
389
390 @Override
391 public String getMimeType(String name) {
392 return null;
393 }
394
395 }
396
397 /**
398 * Implements an open session in view patter: a new JCR session is created
399 * for each request
400 */
401 private class OpenInViewSessionProvider implements SessionProvider, Serializable {
402 private static final long serialVersionUID = 2270957712453841368L;
403 private final String alias;
404
405 public OpenInViewSessionProvider(String alias) {
406 this.alias = alias;
407 }
408
409 public Session getSession(HttpServletRequest request, Repository rep, String workspace)
410 throws javax.jcr.LoginException, ServletException, RepositoryException {
411 return login(request, rep, workspace);
412 }
413
414 protected Session login(HttpServletRequest request, Repository repository, String workspace)
415 throws RepositoryException {
416 if (log.isTraceEnabled())
417 log.trace("Repo " + alias + ", login to workspace " + (workspace == null ? "<default>" : workspace)
418 + " in web session " + request.getSession().getId());
419 LoginContext lc = (LoginContext) request.getAttribute(NodeConstants.LOGIN_CONTEXT_USER);
420 if (lc == null)
421 throw new CmsException("No login context available");
422 try {
423 // LoginContext lc = new
424 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
425 // new HttpRequestCallbackHandler(request));
426 // lc.login();
427 return Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<Session>() {
428 @Override
429 public Session run() throws Exception {
430 return repository.login(workspace);
431 }
432 });
433 } catch (Exception e) {
434 throw new CmsException("Cannot log in to JCR", e);
435 }
436 // return repository.login(workspace);
437 }
438
439 public void releaseSession(Session session) {
440 JcrUtils.logoutQuietly(session);
441 if (log.isTraceEnabled())
442 log.trace("Logged out remote JCR session " + session);
443 }
444 }
445
446 private class WebdavServlet extends SimpleWebdavServlet {
447 private static final long serialVersionUID = -4687354117811443881L;
448 private final Repository repository;
449
450 public WebdavServlet(Repository repository, SessionProvider sessionProvider) {
451 this.repository = repository;
452 setSessionProvider(sessionProvider);
453 }
454
455 public Repository getRepository() {
456 return repository;
457 }
458
459 @Override
460 protected void service(final HttpServletRequest request, final HttpServletResponse response)
461 throws ServletException, IOException {
462 WebdavServlet.super.service(request, response);
463 // try {
464 // Subject subject = subjectFromRequest(request);
465 // // TODO make it stronger, with eTags.
466 // // if (CurrentUser.isAnonymous(subject) &&
467 // // request.getMethod().equals("GET")) {
468 // // response.setHeader("Cache-Control", "no-transform, public,
469 // // max-age=300, s-maxage=900");
470 // // }
471 //
472 // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
473 // @Override
474 // public Void run() throws Exception {
475 // WebdavServlet.super.service(request, response);
476 // return null;
477 // }
478 // });
479 // } catch (PrivilegedActionException e) {
480 // throw new CmsException("Cannot process webdav request",
481 // e.getException());
482 // }
483 }
484 }
485
486 private class RemotingServlet extends JcrRemotingServlet {
487 private static final long serialVersionUID = 4605238259548058883L;
488 private final Repository repository;
489 private final SessionProvider sessionProvider;
490
491 public RemotingServlet(Repository repository, SessionProvider sessionProvider) {
492 this.repository = repository;
493 this.sessionProvider = sessionProvider;
494 }
495
496 @Override
497 protected Repository getRepository() {
498 return repository;
499 }
500
501 @Override
502 protected SessionProvider getSessionProvider() {
503 return sessionProvider;
504 }
505
506 @Override
507 protected void service(final HttpServletRequest request, final HttpServletResponse response)
508 throws ServletException, IOException {
509 try {
510 Subject subject = subjectFromRequest(request);
511 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
512 @Override
513 public Void run() throws Exception {
514 RemotingServlet.super.service(request, response);
515 return null;
516 }
517 });
518 } catch (PrivilegedActionException e) {
519 throw new CmsException("Cannot process JCR remoting request", e.getException());
520 }
521 }
522 }
523 }