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