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
.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
;
54 * Intercepts and enriches http access, mainly focusing on security and
57 class DataHttp
implements KernelConstants
{
58 private final static Log log
= LogFactory
.getLog(DataHttp
.class);
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";
64 private final static String DEFAULT_PROTECTED_HANDLERS
= "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
66 private final BundleContext bc
;
67 private final HttpService httpService
;
68 private final ServiceTracker
<Repository
, Repository
> repositories
;
70 // FIXME Make it more unique
71 private String httpAuthRealm
= "Argeo";
73 DataHttp(HttpService httpService
) {
74 this.bc
= FrameworkUtil
.getBundle(getClass()).getBundleContext();
75 this.httpService
= httpService
;
76 repositories
= new ServiceTracker
<>(bc
, Repository
.class, new RepositoriesStc());
80 public void destroy() {
84 void registerRepositoryServlets(String alias
, Repository repository
) {
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
);
96 void unregisterRepositoryServlets(String alias
) {
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
);
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());
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());
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
);
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());
140 private String
webdavPath(String alias
) {
141 return NodeConstants
.PATH_DATA
+ "/" + alias
;
144 private String
remotingPath(String alias
) {
145 return NodeConstants
.PATH_JCR
+ "/" + alias
;
148 private String
filesPath(String alias
) {
149 return NodeConstants
.PATH_FILES
+ "/" + alias
;
152 private Subject
subjectFromRequest(HttpServletRequest request
) {
153 Authorization authorization
= (Authorization
) request
.getAttribute(HttpContext
.AUTHORIZATION
);
154 if (authorization
== null)
155 throw new CmsException("Not authenticated");
157 LoginContext lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
,
158 new HttpRequestCallbackHandler(request
));
160 return lc
.getSubject();
161 } catch (LoginException e
) {
162 throw new CmsException("Cannot login", e
);
166 private void askForWwwAuth(HttpServletRequest request
, HttpServletResponse response
) {
167 response
.setStatus(401);
168 response
.setHeader(HEADER_WWW_AUTHENTICATE
, "basic realm=\"" +
169 httpAuthRealm
+ "\"");
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");
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")) {
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(":");
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
);
210 throw new CmsException("Invalid authentication token");
212 } catch (Exception e
) {
213 throw new CmsException("Couldn't retrieve authentication", e
);
215 } else if (basic
.equalsIgnoreCase("Negotiate")) {
217 String _targetName
= "HTTP/mostar.desktop.argeo.pro";
218 String spnegoToken
= st
.nextToken();
219 byte[] authToken
= Base64
.decodeBase64(spnegoToken
);
220 GSSManager manager
= GSSManager
.getInstance();
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
);
228 if (gContext
== null) {
229 log
.debug("SpnegoUserRealm: failed to establish GSSContext");
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
);
236 if (gContext
.isEstablished()) {
237 String clientName
= gContext
.getSrcName().toString();
238 String role
= clientName
.substring(clientName
.indexOf('@') + 1);
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
);
249 } catch (GSSException gsse
) {
259 private class RepositoriesStc
implements ServiceTrackerCustomizer
<Repository
, Repository
> {
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
);
273 public void modifiedService(ServiceReference
<Repository
> reference
, Repository service
) {
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
);
286 private class DataHttpContext
implements HttpContext
{
288 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
291 if (log
.isTraceEnabled())
292 KernelUtils
.logRequestHeaders(log
, request
);
295 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
));
298 } catch (LoginException e
) {
299 CallbackHandler token
= extractHttpAuth(request
,response
);
302 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, token
);
304 // Note: this is impossible to reliably clear the
305 // authorization header when access from a browser.
307 } catch (LoginException e1
) {
308 throw new CmsException("Could not login", e1
);
313 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
);
315 } catch (LoginException e1
) {
316 if (log
.isDebugEnabled())
317 log
.error("Cannot log in anonynous", e1
);
322 request
.setAttribute(NodeConstants
.LOGIN_CONTEXT_USER
, lc
);
327 public URL
getResource(String name
) {
328 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
332 public String
getMimeType(String name
) {
338 private class FilesHttpContext
implements HttpContext
{
340 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
343 if (log
.isTraceEnabled())
344 KernelUtils
.logRequestHeaders(log
, request
);
347 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
));
350 } catch (LoginException e
) {
351 CallbackHandler token
= extractHttpAuth(request
,response
);
354 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, token
);
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
);
362 askForWwwAuth(request
, response
);
367 request
.setAttribute(NodeConstants
.LOGIN_CONTEXT_USER
, lc
);
372 public URL
getResource(String name
) {
373 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
377 public String
getMimeType(String name
) {
383 private class RemotingHttpContext
implements HttpContext
{
384 // private final boolean anonymous;
386 RemotingHttpContext() {
387 // this.anonymous = anonymous;
391 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
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);
403 if (log
.isTraceEnabled())
404 KernelUtils
.logRequestHeaders(log
, request
);
407 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
));
409 } catch (CredentialNotFoundException e
) {
410 CallbackHandler token
= extractHttpAuth(request
,response
);
413 lc
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_USER
, token
);
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
);
421 askForWwwAuth(request
, response
);
424 } catch (LoginException e
) {
425 throw new CmsException("Could not login", e
);
429 request
.setAttribute(NodeConstants
.LOGIN_CONTEXT_USER
, lc
);
437 public URL
getResource(String name
) {
438 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
442 public String
getMimeType(String name
) {
449 * Implements an open session in view patter: a new JCR session is created
452 private class OpenInViewSessionProvider
implements SessionProvider
, Serializable
{
453 private static final long serialVersionUID
= 2270957712453841368L;
454 private final String alias
;
456 public OpenInViewSessionProvider(String alias
) {
460 public Session
getSession(HttpServletRequest request
, Repository rep
, String workspace
)
461 throws javax
.jcr
.LoginException
, ServletException
, RepositoryException
{
462 return login(request
, rep
, workspace
);
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
);
472 throw new CmsException("No login context available");
474 // LoginContext lc = new
475 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
476 // new HttpRequestCallbackHandler(request));
478 return Subject
.doAs(lc
.getSubject(), new PrivilegedExceptionAction
<Session
>() {
480 public Session
run() throws Exception
{
481 return repository
.login(workspace
);
484 } catch (Exception e
) {
485 throw new CmsException("Cannot log in to JCR", e
);
487 // return repository.login(workspace);
490 public void releaseSession(Session session
) {
491 JcrUtils
.logoutQuietly(session
);
492 if (log
.isTraceEnabled())
493 log
.trace("Logged out remote JCR session " + session
);
497 private class WebdavServlet
extends SimpleWebdavServlet
{
498 private static final long serialVersionUID
= -4687354117811443881L;
499 private final Repository repository
;
501 public WebdavServlet(Repository repository
, SessionProvider sessionProvider
) {
502 this.repository
= repository
;
503 setSessionProvider(sessionProvider
);
506 public Repository
getRepository() {
511 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
512 throws ServletException
, IOException
{
513 WebdavServlet
.super.service(request
, response
);
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");
523 // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
525 // public Void run() throws Exception {
526 // WebdavServlet.super.service(request, response);
530 // } catch (PrivilegedActionException e) {
531 // throw new CmsException("Cannot process webdav request",
532 // e.getException());
537 private class RemotingServlet
extends JcrRemotingServlet
{
538 private static final long serialVersionUID
= 4605238259548058883L;
539 private final Repository repository
;
540 private final SessionProvider sessionProvider
;
542 public RemotingServlet(Repository repository
, SessionProvider sessionProvider
) {
543 this.repository
= repository
;
544 this.sessionProvider
= sessionProvider
;
548 protected Repository
getRepository() {
553 protected SessionProvider
getSessionProvider() {
554 return sessionProvider
;
558 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
559 throws ServletException
, IOException
{
561 Subject subject
= subjectFromRequest(request
);
562 Subject
.doAs(subject
, new PrivilegedExceptionAction
<Void
>() {
564 public Void
run() throws Exception
{
565 RemotingServlet
.super.service(request
, response
);
569 } catch (PrivilegedActionException e
) {
570 throw new CmsException("Cannot process JCR remoting request", e
.getException());