1 package org
.argeo
.cms
.internal
.kernel
;
3 import java
.io
.IOException
;
4 import java
.io
.Serializable
;
6 import java
.security
.PrivilegedActionException
;
7 import java
.security
.PrivilegedExceptionAction
;
8 import java
.util
.Properties
;
9 import java
.util
.StringTokenizer
;
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
;
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
;
48 * Intercepts and enriches http access, mainly focusing on security and
51 class DataHttp
implements KernelConstants
{
52 private final static Log log
= LogFactory
.getLog(DataHttp
.class);
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";
58 private final static String DEFAULT_PROTECTED_HANDLERS
= "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
60 private final BundleContext bc
;
61 private final HttpService httpService
;
62 private final ServiceTracker
<Repository
, Repository
> repositories
;
64 // FIXME Make it more unique
65 private String httpAuthRealm
= "Argeo";
67 DataHttp(HttpService httpService
) {
68 this.bc
= FrameworkUtil
.getBundle(getClass()).getBundleContext();
69 this.httpService
= httpService
;
70 repositories
= new ServiceTracker
<>(bc
, Repository
.class, new RepositoriesStc());
74 public void destroy() {
78 void registerRepositoryServlets(String alias
, Repository repository
) {
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
);
91 void unregisterRepositoryServlets(String alias
) {
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
);
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());
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
);
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());
127 private String
webdavPath(String alias
) {
128 return NodeConstants
.PATH_DATA
+ "/" + alias
;
129 // String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE;
130 // return pathPrefix + "/" + alias;
133 private String
remotingPath(String alias
) {
134 return NodeConstants
.PATH_JCR
+ "/" + alias
;
135 // String pathPrefix = anonymous ? NodeConstants.PATH_JCR_PUB :
136 // NodeConstants.PATH_JCR;
139 private Subject
subjectFromRequest(HttpServletRequest request
) {
140 Authorization authorization
= (Authorization
) request
.getAttribute(HttpContext
.AUTHORIZATION
);
141 if (authorization
== null)
142 throw new CmsException("Not authenticated");
144 LoginContext lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
,
145 new HttpRequestCallbackHandler(request
));
147 return lc
.getSubject();
148 } catch (LoginException e
) {
149 throw new CmsException("Cannot login", e
);
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);
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")) {
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(":");
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
);
187 throw new CmsException("Invalid authentication token");
189 } catch (Exception e
) {
190 throw new CmsException("Couldn't retrieve authentication", e
);
198 private class RepositoriesStc
implements ServiceTrackerCustomizer
<Repository
, Repository
> {
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
);
212 public void modifiedService(ServiceReference
<Repository
> reference
, Repository service
) {
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
);
225 private class DataHttpContext
implements HttpContext
{
226 // private final boolean anonymous;
229 // this.anonymous = anonymous;
233 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
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);
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);
255 // if (log.isTraceEnabled())
256 KernelUtils
.logRequestHeaders(log
, request
);
259 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
));
262 } catch (CredentialNotFoundException e
) {
263 CallbackHandler token
= basicAuth(request
);
266 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, token
);
268 // Note: this is impossible to reliably clear the
269 // authorization header when access from a browser.
271 } catch (LoginException e1
) {
272 throw new CmsException("Could not login", e1
);
277 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
);
279 } catch (LoginException e1
) {
280 if (log
.isDebugEnabled())
281 log
.error("Cannot log in anonynous", e1
);
285 // Subject subject = KernelUtils.anonymousLogin();
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);
295 // CallbackHandler token = basicAuth(request);
296 // if (token != null) {
298 // LoginContext lc = new
299 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
301 // // Note: this is impossible to reliably clear the
302 // // authorization header when access from a browser.
304 // } catch (LoginException e1) {
305 // throw new CmsException("Could not login", e1);
308 // String path = request.getServletPath();
309 // if (path.startsWith(REMOTING_PRIVATE))
310 // requestBasicAuth(request, response);
313 } catch (LoginException e
) {
314 throw new CmsException("Could not login", e
);
316 request
.setAttribute(NodeConstants
.LOGIN_CONTEXT_USER
, lc
);
321 public URL
getResource(String name
) {
322 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
326 public String
getMimeType(String name
) {
332 private class RemotingHttpContext
implements HttpContext
{
333 // private final boolean anonymous;
335 RemotingHttpContext() {
336 // this.anonymous = anonymous;
340 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
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);
352 if (log
.isTraceEnabled())
353 KernelUtils
.logRequestHeaders(log
, request
);
356 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
));
358 } catch (CredentialNotFoundException e
) {
359 CallbackHandler token
= basicAuth(request
);
362 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, token
);
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
);
370 requestBasicAuth(request
, response
);
373 } catch (LoginException e
) {
374 throw new CmsException("Could not login", e
);
378 request
.setAttribute(NodeConstants
.LOGIN_CONTEXT_USER
, lc
);
386 public URL
getResource(String name
) {
387 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
391 public String
getMimeType(String name
) {
398 * Implements an open session in view patter: a new JCR session is created
401 private class OpenInViewSessionProvider
implements SessionProvider
, Serializable
{
402 private static final long serialVersionUID
= 2270957712453841368L;
403 private final String alias
;
405 public OpenInViewSessionProvider(String alias
) {
409 public Session
getSession(HttpServletRequest request
, Repository rep
, String workspace
)
410 throws javax
.jcr
.LoginException
, ServletException
, RepositoryException
{
411 return login(request
, rep
, workspace
);
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
);
421 throw new CmsException("No login context available");
423 // LoginContext lc = new
424 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
425 // new HttpRequestCallbackHandler(request));
427 return Subject
.doAs(lc
.getSubject(), new PrivilegedExceptionAction
<Session
>() {
429 public Session
run() throws Exception
{
430 return repository
.login(workspace
);
433 } catch (Exception e
) {
434 throw new CmsException("Cannot log in to JCR", e
);
436 // return repository.login(workspace);
439 public void releaseSession(Session session
) {
440 JcrUtils
.logoutQuietly(session
);
441 if (log
.isTraceEnabled())
442 log
.trace("Logged out remote JCR session " + session
);
446 private class WebdavServlet
extends SimpleWebdavServlet
{
447 private static final long serialVersionUID
= -4687354117811443881L;
448 private final Repository repository
;
450 public WebdavServlet(Repository repository
, SessionProvider sessionProvider
) {
451 this.repository
= repository
;
452 setSessionProvider(sessionProvider
);
455 public Repository
getRepository() {
460 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
461 throws ServletException
, IOException
{
462 WebdavServlet
.super.service(request
, response
);
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");
472 // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
474 // public Void run() throws Exception {
475 // WebdavServlet.super.service(request, response);
479 // } catch (PrivilegedActionException e) {
480 // throw new CmsException("Cannot process webdav request",
481 // e.getException());
486 private class RemotingServlet
extends JcrRemotingServlet
{
487 private static final long serialVersionUID
= 4605238259548058883L;
488 private final Repository repository
;
489 private final SessionProvider sessionProvider
;
491 public RemotingServlet(Repository repository
, SessionProvider sessionProvider
) {
492 this.repository
= repository
;
493 this.sessionProvider
= sessionProvider
;
497 protected Repository
getRepository() {
502 protected SessionProvider
getSessionProvider() {
503 return sessionProvider
;
507 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
508 throws ServletException
, IOException
{
510 Subject subject
= subjectFromRequest(request
);
511 Subject
.doAs(subject
, new PrivilegedExceptionAction
<Void
>() {
513 public Void
run() throws Exception
{
514 RemotingServlet
.super.service(request
, response
);
518 } catch (PrivilegedActionException e
) {
519 throw new CmsException("Cannot process JCR remoting request", e
.getException());