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