]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/DataHttp.java
[maven-release-plugin] prepare for next development iteration
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / DataHttp.java
1 package org.argeo.cms.internal.kernel;
2
3 import static org.argeo.cms.auth.AuthConstants.LOGIN_CONTEXT_USER;
4
5 import java.io.IOException;
6 import java.io.Serializable;
7 import java.net.URL;
8 import java.security.PrivilegedActionException;
9 import java.security.PrivilegedExceptionAction;
10 import java.util.Properties;
11 import java.util.StringTokenizer;
12
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;
27
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.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
45 /**
46 * Intercepts and enriches http access, mainly focusing on security and
47 * transactionality.
48 */
49 class DataHttp implements KernelConstants, ArgeoJcrConstants {
50 private final static Log log = LogFactory.getLog(DataHttp.class);
51
52 // private final static String ATTR_AUTH = "auth";
53 private final static String HEADER_AUTHORIZATION = "Authorization";
54 private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
55
56 private final static String DEFAULT_PROTECTED_HANDLERS = "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
57
58 private final HttpService httpService;
59
60 // FIXME Make it more unique
61 private String httpAuthRealm = "Argeo";
62
63 // WebDav / JCR remoting
64 private OpenInViewSessionProvider sessionProvider;
65
66 DataHttp(HttpService httpService) {
67 this.httpService = httpService;
68 sessionProvider = new OpenInViewSessionProvider();
69 // registerRepositoryServlets(ALIAS_NODE, node);
70 }
71
72 public void destroy() {
73 // unregisterRepositoryServlets(ALIAS_NODE);
74 }
75
76 void registerRepositoryServlets(String alias, Repository repository) {
77 try {
78 registerWebdavServlet(alias, repository, true);
79 registerWebdavServlet(alias, repository, false);
80 registerRemotingServlet(alias, repository, true);
81 registerRemotingServlet(alias, repository, false);
82 } catch (Exception e) {
83 throw new CmsException("Could not register servlets for repository " + alias, e);
84 }
85 }
86
87 void unregisterRepositoryServlets(String alias) {
88 // FIXME unregister servlets
89 }
90
91 void registerWebdavServlet(String alias, Repository repository, boolean anonymous)
92 throws NamespaceException, ServletException {
93 WebdavServlet webdavServlet = new WebdavServlet(repository, sessionProvider);
94 String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE;
95 String path = pathPrefix + "/" + alias;
96 Properties ip = new Properties();
97 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG);
98 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
99 httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext(anonymous));
100 }
101
102 void registerRemotingServlet(String alias, Repository repository, boolean anonymous)
103 throws NamespaceException, ServletException {
104 String pathPrefix = anonymous ? REMOTING_PUBLIC : REMOTING_PRIVATE;
105 RemotingServlet remotingServlet = new RemotingServlet(repository, sessionProvider);
106 String path = pathPrefix + "/" + alias;
107 Properties ip = new Properties();
108 ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
109
110 // Looks like a bug in Jackrabbit remoting init
111 ip.setProperty(RemotingServlet.INIT_PARAM_HOME, KernelUtils.getOsgiInstanceDir() + "/tmp/jackrabbit");
112 ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting");
113 ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, DEFAULT_PROTECTED_HANDLERS);
114 httpService.registerServlet(path, remotingServlet, ip, new DataHttpContext(anonymous));
115 }
116
117 private Subject subjectFromRequest(HttpServletRequest request) {
118 Authorization authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
119 if (authorization == null)
120 throw new CmsException("Not authenticated");
121 try {
122 LoginContext lc = new LoginContext(AuthConstants.LOGIN_CONTEXT_USER,
123 new HttpRequestCallbackHandler(request));
124 lc.login();
125 return lc.getSubject();
126 } catch (LoginException e) {
127 throw new CmsException("Cannot login", e);
128 }
129 }
130
131 private class DataHttpContext implements HttpContext {
132 private final boolean anonymous;
133
134 DataHttpContext(boolean anonymous) {
135 this.anonymous = anonymous;
136 }
137
138 @Override
139 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
140 throws IOException {
141
142 if (anonymous) {
143 Subject subject = KernelUtils.anonymousLogin();
144 Authorization authorization = subject.getPrivateCredentials(Authorization.class).iterator().next();
145 request.setAttribute(REMOTE_USER, AuthConstants.ROLE_ANONYMOUS);
146 request.setAttribute(AUTHORIZATION, authorization);
147 return true;
148 }
149
150 if (log.isTraceEnabled())
151 KernelUtils.logRequestHeaders(log, request);
152 try {
153 new LoginContext(LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request)).login();
154 return true;
155 } catch (CredentialNotFoundException e) {
156 CallbackHandler token = basicAuth(request);
157 if (token != null) {
158 try {
159 LoginContext lc = new LoginContext(LOGIN_CONTEXT_USER, token);
160 lc.login();
161 // Note: this is impossible to reliably clear the
162 // authorization header when access from a browser.
163 return true;
164 } catch (LoginException e1) {
165 throw new CmsException("Could not login", e1);
166 }
167 } else {
168 String path = request.getServletPath();
169 if (path.startsWith(REMOTING_PRIVATE))
170 requestBasicAuth(request, response);
171 return false;
172 }
173 } catch (LoginException e) {
174 throw new CmsException("Could not login", e);
175 }
176 }
177
178 @Override
179 public URL getResource(String name) {
180 return Activator.getBundleContext().getBundle().getResource(name);
181 }
182
183 @Override
184 public String getMimeType(String name) {
185 return null;
186 }
187
188 private void requestBasicAuth(HttpServletRequest request, HttpServletResponse response) {
189 response.setStatus(401);
190 response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" + httpAuthRealm + "\"");
191 // request.getSession().setAttribute(ATTR_AUTH, Boolean.TRUE);
192 }
193
194 private CallbackHandler basicAuth(final HttpServletRequest httpRequest) {
195 String authHeader = httpRequest.getHeader(HEADER_AUTHORIZATION);
196 if (authHeader != null) {
197 StringTokenizer st = new StringTokenizer(authHeader);
198 if (st.hasMoreTokens()) {
199 String basic = st.nextToken();
200 if (basic.equalsIgnoreCase("Basic")) {
201 try {
202 // TODO manipulate char[]
203 String credentials = new String(Base64.decodeBase64(st.nextToken()), "UTF-8");
204 // log.debug("Credentials: " + credentials);
205 int p = credentials.indexOf(":");
206 if (p != -1) {
207 final String login = credentials.substring(0, p).trim();
208 final char[] password = credentials.substring(p + 1).trim().toCharArray();
209 return new CallbackHandler() {
210 public void handle(Callback[] callbacks) {
211 for (Callback cb : callbacks) {
212 if (cb instanceof NameCallback)
213 ((NameCallback) cb).setName(login);
214 else if (cb instanceof PasswordCallback)
215 ((PasswordCallback) cb).setPassword(password);
216 else if (cb instanceof HttpRequestCallback)
217 ((HttpRequestCallback) cb).setRequest(httpRequest);
218 }
219 }
220 };
221 } else {
222 throw new CmsException("Invalid authentication token");
223 }
224 } catch (Exception e) {
225 throw new CmsException("Couldn't retrieve authentication", e);
226 }
227 }
228 }
229 }
230 return null;
231 }
232
233 }
234
235 /**
236 * Implements an open session in view patter: a new JCR session is created
237 * for each request
238 */
239 private class OpenInViewSessionProvider implements SessionProvider, Serializable {
240 private static final long serialVersionUID = 2270957712453841368L;
241
242 public Session getSession(HttpServletRequest request, Repository rep, String workspace)
243 throws javax.jcr.LoginException, ServletException, RepositoryException {
244 return login(request, rep, workspace);
245 }
246
247 protected Session login(HttpServletRequest request, Repository repository, String workspace)
248 throws RepositoryException {
249 if (log.isTraceEnabled())
250 log.trace("Login to workspace " + (workspace == null ? "<default>" : workspace) + " in web session "
251 + request.getSession().getId());
252 return repository.login(workspace);
253 }
254
255 public void releaseSession(Session session) {
256 JcrUtils.logoutQuietly(session);
257 if (log.isTraceEnabled())
258 log.trace("Logged out remote JCR session " + session);
259 }
260 }
261
262 private class WebdavServlet extends SimpleWebdavServlet {
263 private static final long serialVersionUID = -4687354117811443881L;
264 private final Repository repository;
265
266 public WebdavServlet(Repository repository, SessionProvider sessionProvider) {
267 this.repository = repository;
268 setSessionProvider(sessionProvider);
269 }
270
271 public Repository getRepository() {
272 return repository;
273 }
274
275 @Override
276 protected void service(final HttpServletRequest request, final HttpServletResponse response)
277 throws ServletException, IOException {
278 try {
279 Subject subject = subjectFromRequest(request);
280 // TODO make it stronger, with eTags.
281 // if (CurrentUser.isAnonymous(subject) &&
282 // request.getMethod().equals("GET")) {
283 // response.setHeader("Cache-Control", "no-transform, public,
284 // max-age=300, s-maxage=900");
285 // }
286
287 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
288 @Override
289 public Void run() throws Exception {
290 WebdavServlet.super.service(request, response);
291 return null;
292 }
293 });
294 } catch (PrivilegedActionException e) {
295 throw new CmsException("Cannot process webdav request", e.getException());
296 }
297 }
298 }
299
300 private class RemotingServlet extends JcrRemotingServlet {
301 private static final long serialVersionUID = 4605238259548058883L;
302 private final Repository repository;
303 private final SessionProvider sessionProvider;
304
305 public RemotingServlet(Repository repository, SessionProvider sessionProvider) {
306 this.repository = repository;
307 this.sessionProvider = sessionProvider;
308 }
309
310 @Override
311 protected Repository getRepository() {
312 return repository;
313 }
314
315 @Override
316 protected SessionProvider getSessionProvider() {
317 return sessionProvider;
318 }
319
320 @Override
321 protected void service(final HttpServletRequest request, final HttpServletResponse response)
322 throws ServletException, IOException {
323 try {
324 Subject subject = subjectFromRequest(request);
325 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
326 @Override
327 public Void run() throws Exception {
328 RemotingServlet.super.service(request, response);
329 return null;
330 }
331 });
332 } catch (PrivilegedActionException e) {
333 throw new CmsException("Cannot process JCR remoting request", e.getException());
334 }
335 }
336 }
337 }