1 package org
.argeo
.cms
.internal
.kernel
;
3 import static org
.argeo
.cms
.auth
.AuthConstants
.LOGIN_CONTEXT_USER
;
5 import java
.io
.IOException
;
6 import java
.io
.Serializable
;
8 import java
.security
.PrivilegedActionException
;
9 import java
.security
.PrivilegedExceptionAction
;
10 import java
.util
.Properties
;
11 import java
.util
.StringTokenizer
;
13 import javax
.jcr
.Repository
;
14 import javax
.jcr
.RepositoryException
;
15 import javax
.jcr
.Session
;
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
.CredentialNotFoundException
;
22 import javax
.security
.auth
.login
.LoginContext
;
23 import javax
.security
.auth
.login
.LoginException
;
24 import javax
.servlet
.ServletException
;
25 import javax
.servlet
.http
.HttpServletRequest
;
26 import javax
.servlet
.http
.HttpServletResponse
;
28 import org
.apache
.commons
.codec
.binary
.Base64
;
29 import org
.apache
.commons
.logging
.Log
;
30 import org
.apache
.commons
.logging
.LogFactory
;
31 import org
.apache
.jackrabbit
.server
.SessionProvider
;
32 import org
.apache
.jackrabbit
.server
.remoting
.davex
.JcrRemotingServlet
;
33 import org
.apache
.jackrabbit
.webdav
.simple
.SimpleWebdavServlet
;
34 import org
.argeo
.cms
.CmsException
;
35 import org
.argeo
.cms
.auth
.AuthConstants
;
36 import org
.argeo
.cms
.auth
.HttpRequestCallback
;
37 import org
.argeo
.cms
.auth
.HttpRequestCallbackHandler
;
38 import org
.argeo
.jcr
.ArgeoJcrConstants
;
39 import org
.argeo
.jcr
.JcrUtils
;
40 import org
.osgi
.framework
.BundleContext
;
41 import org
.osgi
.framework
.FrameworkUtil
;
42 import org
.osgi
.framework
.ServiceReference
;
43 import org
.osgi
.service
.http
.HttpContext
;
44 import org
.osgi
.service
.http
.HttpService
;
45 import org
.osgi
.service
.http
.NamespaceException
;
46 import org
.osgi
.service
.useradmin
.Authorization
;
47 import org
.osgi
.util
.tracker
.ServiceTracker
;
48 import org
.osgi
.util
.tracker
.ServiceTrackerCustomizer
;
51 * Intercepts and enriches http access, mainly focusing on security and
54 class DataHttp
implements KernelConstants
, ArgeoJcrConstants
{
55 private final static Log log
= LogFactory
.getLog(DataHttp
.class);
57 // private final static String ATTR_AUTH = "auth";
58 private final static String HEADER_AUTHORIZATION
= "Authorization";
59 private final static String HEADER_WWW_AUTHENTICATE
= "WWW-Authenticate";
61 private final static String DEFAULT_PROTECTED_HANDLERS
= "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
63 private final BundleContext bc
;
64 private final HttpService httpService
;
65 private final ServiceTracker
<Repository
, Repository
> repositories
;
67 // FIXME Make it more unique
68 private String httpAuthRealm
= "Argeo";
70 // WebDav / JCR remoting
71 private OpenInViewSessionProvider sessionProvider
;
73 DataHttp(HttpService httpService
) {
74 this.bc
= FrameworkUtil
.getBundle(getClass()).getBundleContext();
75 sessionProvider
= new OpenInViewSessionProvider();
76 this.httpService
= httpService
;
77 repositories
= new ServiceTracker
<>(bc
, Repository
.class, new RepositoriesStc());
81 public void destroy() {
85 void registerRepositoryServlets(String alias
, Repository repository
) {
87 registerWebdavServlet(alias
, repository
, true);
88 registerWebdavServlet(alias
, repository
, false);
89 registerRemotingServlet(alias
, repository
, true);
90 registerRemotingServlet(alias
, repository
, false);
91 if (log
.isDebugEnabled())
92 log
.debug("Registered servlets for repository '" + alias
+ "'");
93 } catch (Exception e
) {
94 throw new CmsException("Could not register servlets for repository '" + alias
+ "'", e
);
98 void unregisterRepositoryServlets(String alias
) {
100 httpService
.unregister(webdavPath(alias
, true));
101 httpService
.unregister(webdavPath(alias
, false));
102 httpService
.unregister(remotingPath(alias
, true));
103 httpService
.unregister(remotingPath(alias
, false));
104 if (log
.isDebugEnabled())
105 log
.debug("Unregistered servlets for repository '" + alias
+ "'");
106 } catch (Exception e
) {
107 log
.error("Could not unregister servlets for repository '" + alias
+ "'", e
);
111 void registerWebdavServlet(String alias
, Repository repository
, boolean anonymous
)
112 throws NamespaceException
, ServletException
{
113 WebdavServlet webdavServlet
= new WebdavServlet(repository
, sessionProvider
);
114 String path
= webdavPath(alias
, anonymous
);
115 Properties ip
= new Properties();
116 ip
.setProperty(WebdavServlet
.INIT_PARAM_RESOURCE_CONFIG
, WEBDAV_CONFIG
);
117 ip
.setProperty(WebdavServlet
.INIT_PARAM_RESOURCE_PATH_PREFIX
, path
);
118 httpService
.registerServlet(path
, webdavServlet
, ip
, new DataHttpContext(anonymous
));
121 void registerRemotingServlet(String alias
, Repository repository
, boolean anonymous
)
122 throws NamespaceException
, ServletException
{
123 RemotingServlet remotingServlet
= new RemotingServlet(repository
, sessionProvider
);
124 String path
= remotingPath(alias
, anonymous
);
125 Properties ip
= new Properties();
126 ip
.setProperty(JcrRemotingServlet
.INIT_PARAM_RESOURCE_PATH_PREFIX
, path
);
128 // Looks like a bug in Jackrabbit remoting init
129 ip
.setProperty(RemotingServlet
.INIT_PARAM_HOME
, KernelUtils
.getOsgiInstanceDir() + "/tmp/jackrabbit");
130 ip
.setProperty(RemotingServlet
.INIT_PARAM_TMP_DIRECTORY
, "remoting");
131 ip
.setProperty(RemotingServlet
.INIT_PARAM_PROTECTED_HANDLERS_CONFIG
, DEFAULT_PROTECTED_HANDLERS
);
132 httpService
.registerServlet(path
, remotingServlet
, ip
, new DataHttpContext(anonymous
));
135 private String
webdavPath(String alias
, boolean anonymous
) {
136 String pathPrefix
= anonymous ? WEBDAV_PUBLIC
: WEBDAV_PRIVATE
;
137 return pathPrefix
+ "/" + alias
;
140 private String
remotingPath(String alias
, boolean anonymous
) {
141 String pathPrefix
= anonymous ? REMOTING_PUBLIC
: REMOTING_PRIVATE
;
142 return pathPrefix
+ "/" + alias
;
145 private Subject
subjectFromRequest(HttpServletRequest request
) {
146 Authorization authorization
= (Authorization
) request
.getAttribute(HttpContext
.AUTHORIZATION
);
147 if (authorization
== null)
148 throw new CmsException("Not authenticated");
150 LoginContext lc
= new LoginContext(AuthConstants
.LOGIN_CONTEXT_USER
,
151 new HttpRequestCallbackHandler(request
));
153 return lc
.getSubject();
154 } catch (LoginException e
) {
155 throw new CmsException("Cannot login", e
);
159 private class RepositoriesStc
implements ServiceTrackerCustomizer
<Repository
, Repository
> {
162 public Repository
addingService(ServiceReference
<Repository
> reference
) {
163 Repository repository
= bc
.getService(reference
);
164 Object jcrRepoAlias
= reference
.getProperty(ArgeoJcrConstants
.JCR_REPOSITORY_ALIAS
);
165 if (jcrRepoAlias
!= null) {
166 String alias
= jcrRepoAlias
.toString();
167 registerRepositoryServlets(alias
, repository
);
173 public void modifiedService(ServiceReference
<Repository
> reference
, Repository service
) {
177 public void removedService(ServiceReference
<Repository
> reference
, Repository service
) {
178 Object jcrRepoAlias
= reference
.getProperty(ArgeoJcrConstants
.JCR_REPOSITORY_ALIAS
);
179 if (jcrRepoAlias
!= null) {
180 String alias
= jcrRepoAlias
.toString();
181 unregisterRepositoryServlets(alias
);
186 private class DataHttpContext
implements HttpContext
{
187 private final boolean anonymous
;
189 DataHttpContext(boolean anonymous
) {
190 this.anonymous
= anonymous
;
194 public boolean handleSecurity(final HttpServletRequest request
, HttpServletResponse response
)
198 Subject subject
= KernelUtils
.anonymousLogin();
199 Authorization authorization
= subject
.getPrivateCredentials(Authorization
.class).iterator().next();
200 request
.setAttribute(REMOTE_USER
, AuthConstants
.ROLE_ANONYMOUS
);
201 request
.setAttribute(AUTHORIZATION
, authorization
);
205 if (log
.isTraceEnabled())
206 KernelUtils
.logRequestHeaders(log
, request
);
208 new LoginContext(LOGIN_CONTEXT_USER
, new HttpRequestCallbackHandler(request
)).login();
210 } catch (CredentialNotFoundException e
) {
211 CallbackHandler token
= basicAuth(request
);
214 LoginContext lc
= new LoginContext(LOGIN_CONTEXT_USER
, token
);
216 // Note: this is impossible to reliably clear the
217 // authorization header when access from a browser.
219 } catch (LoginException e1
) {
220 throw new CmsException("Could not login", e1
);
223 String path
= request
.getServletPath();
224 if (path
.startsWith(REMOTING_PRIVATE
))
225 requestBasicAuth(request
, response
);
228 } catch (LoginException e
) {
229 throw new CmsException("Could not login", e
);
234 public URL
getResource(String name
) {
235 return KernelUtils
.getBundleContext(DataHttp
.class).getBundle().getResource(name
);
239 public String
getMimeType(String name
) {
243 private void requestBasicAuth(HttpServletRequest request
, HttpServletResponse response
) {
244 response
.setStatus(401);
245 response
.setHeader(HEADER_WWW_AUTHENTICATE
, "basic realm=\"" + httpAuthRealm
+ "\"");
246 // request.getSession().setAttribute(ATTR_AUTH, Boolean.TRUE);
249 private CallbackHandler
basicAuth(final HttpServletRequest httpRequest
) {
250 String authHeader
= httpRequest
.getHeader(HEADER_AUTHORIZATION
);
251 if (authHeader
!= null) {
252 StringTokenizer st
= new StringTokenizer(authHeader
);
253 if (st
.hasMoreTokens()) {
254 String basic
= st
.nextToken();
255 if (basic
.equalsIgnoreCase("Basic")) {
257 // TODO manipulate char[]
258 String credentials
= new String(Base64
.decodeBase64(st
.nextToken()), "UTF-8");
259 // log.debug("Credentials: " + credentials);
260 int p
= credentials
.indexOf(":");
262 final String login
= credentials
.substring(0, p
).trim();
263 final char[] password
= credentials
.substring(p
+ 1).trim().toCharArray();
264 return new CallbackHandler() {
265 public void handle(Callback
[] callbacks
) {
266 for (Callback cb
: callbacks
) {
267 if (cb
instanceof NameCallback
)
268 ((NameCallback
) cb
).setName(login
);
269 else if (cb
instanceof PasswordCallback
)
270 ((PasswordCallback
) cb
).setPassword(password
);
271 else if (cb
instanceof HttpRequestCallback
)
272 ((HttpRequestCallback
) cb
).setRequest(httpRequest
);
277 throw new CmsException("Invalid authentication token");
279 } catch (Exception e
) {
280 throw new CmsException("Couldn't retrieve authentication", e
);
291 * Implements an open session in view patter: a new JCR session is created
294 private class OpenInViewSessionProvider
implements SessionProvider
, Serializable
{
295 private static final long serialVersionUID
= 2270957712453841368L;
297 public Session
getSession(HttpServletRequest request
, Repository rep
, String workspace
)
298 throws javax
.jcr
.LoginException
, ServletException
, RepositoryException
{
299 return login(request
, rep
, workspace
);
302 protected Session
login(HttpServletRequest request
, Repository repository
, String workspace
)
303 throws RepositoryException
{
304 if (log
.isTraceEnabled())
305 log
.trace("Login to workspace " + (workspace
== null ?
"<default>" : workspace
) + " in web session "
306 + request
.getSession().getId());
307 return repository
.login(workspace
);
310 public void releaseSession(Session session
) {
311 JcrUtils
.logoutQuietly(session
);
312 if (log
.isTraceEnabled())
313 log
.trace("Logged out remote JCR session " + session
);
317 private class WebdavServlet
extends SimpleWebdavServlet
{
318 private static final long serialVersionUID
= -4687354117811443881L;
319 private final Repository repository
;
321 public WebdavServlet(Repository repository
, SessionProvider sessionProvider
) {
322 this.repository
= repository
;
323 setSessionProvider(sessionProvider
);
326 public Repository
getRepository() {
331 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
332 throws ServletException
, IOException
{
334 Subject subject
= subjectFromRequest(request
);
335 // TODO make it stronger, with eTags.
336 // if (CurrentUser.isAnonymous(subject) &&
337 // request.getMethod().equals("GET")) {
338 // response.setHeader("Cache-Control", "no-transform, public,
339 // max-age=300, s-maxage=900");
342 Subject
.doAs(subject
, new PrivilegedExceptionAction
<Void
>() {
344 public Void
run() throws Exception
{
345 WebdavServlet
.super.service(request
, response
);
349 } catch (PrivilegedActionException e
) {
350 throw new CmsException("Cannot process webdav request", e
.getException());
355 private class RemotingServlet
extends JcrRemotingServlet
{
356 private static final long serialVersionUID
= 4605238259548058883L;
357 private final Repository repository
;
358 private final SessionProvider sessionProvider
;
360 public RemotingServlet(Repository repository
, SessionProvider sessionProvider
) {
361 this.repository
= repository
;
362 this.sessionProvider
= sessionProvider
;
366 protected Repository
getRepository() {
371 protected SessionProvider
getSessionProvider() {
372 return sessionProvider
;
376 protected void service(final HttpServletRequest request
, final HttpServletResponse response
)
377 throws ServletException
, IOException
{
379 Subject subject
= subjectFromRequest(request
);
380 Subject
.doAs(subject
, new PrivilegedExceptionAction
<Void
>() {
382 public Void
run() throws Exception
{
383 RemotingServlet
.super.service(request
, response
);
387 } catch (PrivilegedActionException e
) {
388 throw new CmsException("Cannot process JCR remoting request", e
.getException());