Use exponential backoff when updating the password or last login time in WebDAV.
[pithos] / src / gr / ebs / gss / server / rest / RequestHandler.java
1 /*
2  * Copyright 2008, 2009 Electronic Business Systems Ltd.
3  *
4  * This file is part of GSS.
5  *
6  * GSS is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * GSS is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with GSS.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 package gr.ebs.gss.server.rest;
20
21 import static gr.ebs.gss.server.configuration.GSSConfigurationFactory.getConfiguration;
22 import gr.ebs.gss.client.exceptions.InsufficientPermissionsException;
23 import gr.ebs.gss.client.exceptions.ObjectNotFoundException;
24 import gr.ebs.gss.client.exceptions.RpcException;
25 import gr.ebs.gss.server.domain.User;
26 import gr.ebs.gss.server.domain.dto.FileHeaderDTO;
27 import gr.ebs.gss.server.webdav.Webdav;
28
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.IOException;
32 import java.io.OutputStreamWriter;
33 import java.io.PrintWriter;
34 import java.io.UnsupportedEncodingException;
35 import java.util.Calendar;
36 import java.util.Enumeration;
37 import java.util.HashMap;
38 import java.util.Map;
39
40 import javax.crypto.Mac;
41 import javax.crypto.spec.SecretKeySpec;
42 import javax.servlet.ServletException;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
45
46 import org.apache.commons.codec.binary.Base64;
47 import org.apache.commons.logging.Log;
48 import org.apache.commons.logging.LogFactory;
49
50 /**
51  * The servlet that handles requests for the REST API.
52  *
53  * @author past
54  */
55 public class RequestHandler extends Webdav {
56         /**
57          * The request attribute containing the flag that will be used to indicate an
58          * authentication bypass has occurred. We will have to check for authentication
59          * later. This is a shortcut for dealing with publicly-readable files.
60          */
61         protected static final String AUTH_DEFERRED_ATTR = "authDeferred";
62
63         /**
64          * The path for the search subsystem.
65          */
66         protected static final String PATH_SEARCH = "/search";
67
68         /**
69          * The path for the user search subsystem.
70          */
71         protected static final String PATH_USERS = "/users";
72
73         /**
74          * The path for the resource manipulation subsystem.
75          */
76         protected static final String PATH_FILES = FileHeaderDTO.PATH_FILES;
77
78         /**
79          * The path for the trash virtual folder.
80          */
81         protected static final String PATH_TRASH = "/trash";
82
83         /**
84          * The path for the subsystem that deals with the user attributes.
85          */
86         protected static final String PATH_GROUPS = "/groups";
87
88         /**
89          * The path for the shared resources virtual folder.
90          */
91         protected static final String PATH_SHARED = "/shared";
92
93         /**
94          * The path for the other users' shared resources virtual folder.
95          */
96         protected static final String PATH_OTHERS = "/others";
97
98         /**
99          * The path for tags created by the user.
100          */
101         protected static final String PATH_TAGS = "/tags";
102
103         /**
104          * The path for token renewal.
105          */
106         protected static final String PATH_TOKEN = "/newtoken";
107
108         /**
109          * The GSS-specific header for the request timestamp.
110          */
111         private static final String GSS_DATE_HEADER = "X-GSS-Date";
112
113         /**
114          * The RFC 2616 date header.
115          */
116         private static final String DATE_HEADER = "Date";
117
118         /**
119          * The Authorization HTTP header.
120          */
121         private static final String AUTHORIZATION_HEADER = "Authorization";
122
123         /**
124          * The group parameter name.
125          */
126         protected static final String GROUP_PARAMETER = "name";
127
128         /**
129          * The username parameter name.
130          */
131         protected static final String USERNAME_PARAMETER = "name";
132
133         /**
134          * The "new folder name" parameter name.
135          */
136         protected static final String NEW_FOLDER_PARAMETER = "new";
137
138         /**
139          * The resource update parameter name.
140          */
141         protected static final String RESOURCE_UPDATE_PARAMETER = "update";
142
143         /**
144          * The resource trash parameter name.
145          */
146         protected static final String RESOURCE_TRASH_PARAMETER = "trash";
147
148         /**
149          * The resource restore parameter name.
150          */
151         protected static final String RESOURCE_RESTORE_PARAMETER = "restore";
152
153         /**
154          * The resource copy parameter name.
155          */
156         protected static final String RESOURCE_COPY_PARAMETER = "copy";
157
158         /**
159          * The resource move parameter name.
160          */
161         protected static final String RESOURCE_MOVE_PARAMETER = "move";
162
163         /**
164          * The HMAC-SHA1 hash name.
165          */
166         private static final String HMAC_SHA1 = "HmacSHA1";
167
168         /**
169          * The serial version UID of the class.
170          */
171         private static final long serialVersionUID = 1L;
172
173         /**
174          * The logger.
175          */
176         private static Log logger = LogFactory.getLog(RequestHandler.class);
177
178         /**
179          * Create a mapping between paths and allowed HTTP methods for fast lookup.
180          */
181         private final Map<String, String> methodsAllowed = new HashMap<String, String>(7);
182
183         @Override
184         public void init() throws ServletException {
185                 super.init();
186                 methodsAllowed.put(PATH_FILES, METHOD_GET + ", " + METHOD_POST +
187                                         ", " + METHOD_DELETE + ", " + METHOD_PUT + ", " + METHOD_HEAD);
188                 methodsAllowed.put(PATH_GROUPS, METHOD_GET + ", " + METHOD_POST +
189                                         ", " + METHOD_DELETE);
190                 methodsAllowed.put(PATH_OTHERS, METHOD_GET);
191                 methodsAllowed.put(PATH_SEARCH, METHOD_GET);
192                 methodsAllowed.put(PATH_USERS, METHOD_GET);
193                 methodsAllowed.put(PATH_SHARED, METHOD_GET);
194                 methodsAllowed.put(PATH_TAGS, METHOD_GET);
195                 methodsAllowed.put(PATH_TRASH, METHOD_GET + ", " + METHOD_DELETE);
196                 methodsAllowed.put(PATH_TOKEN, METHOD_GET);
197         }
198
199         /**
200          * Return the root of every API request URL.
201          */
202         protected String getApiRoot() {
203                 return getConfiguration().getString("restUrl", "http://localhost:8080/gss/rest/");
204         }
205
206         @Override
207         public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
208         String method = request.getMethod();
209                 String path = getRelativePath(request);
210
211                 if (logger.isDebugEnabled())
212             logger.debug("[" + method + "] " + path);
213
214                 if (!isRequestValid(request)) {
215                         if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD) &&
216                                                 !method.equals(METHOD_POST)) {
217                                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
218                                 return;
219                         }
220                         // Raise a flag to indicate we will have to check for
221                         // authentication later. This is a shortcut for dealing
222                         // with publicly-readable files.
223                         request.setAttribute(AUTH_DEFERRED_ATTR, true);
224                 }
225
226                 // Dispatch to the appropriate method handler.
227                 if (method.equals(METHOD_GET))
228                         doGet(request, response);
229                 else if (method.equals(METHOD_POST))
230                         doPost(request, response);
231                 else if (method.equals(METHOD_PUT))
232                         doPut(request, response);
233                 else if (method.equals(METHOD_DELETE))
234                         doDelete(request, response);
235                 else if (method.equals(METHOD_HEAD))
236                         doHead(request, response);
237                 else
238                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
239         }
240
241         @Override
242         protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
243                 boolean authDeferred = getAuthDeferred(req);
244         // Strip the username part
245         String path;
246                 try {
247                         path = getUserPath(req);
248                 } catch (ObjectNotFoundException e) {
249                         if (authDeferred) {
250                                 // We do not want to leak information if the request
251                                 // was not authenticated.
252                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
253                                 return;
254                         }
255                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
256                         return;
257                 }
258                 if (authDeferred && !path.startsWith(PATH_FILES)) {
259                         // Only files may be open to the public.
260                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
261                         return;
262                 }
263
264                 if (path.startsWith(PATH_GROUPS)) {
265             resp.addHeader("Allow", methodsAllowed.get(PATH_GROUPS));
266                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
267                 } else if (path.startsWith(PATH_OTHERS)) {
268             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
269                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
270                 } else if (path.startsWith(PATH_SEARCH)) {
271             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
272                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
273                 } else if (path.startsWith(PATH_TOKEN)) {
274             resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN));
275                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
276                 } else if (path.startsWith(PATH_USERS)) {
277             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
278                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
279                 } else if (path.startsWith(PATH_SHARED)) {
280             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
281                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
282                 } else if (path.startsWith(PATH_TAGS)) {
283             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
284                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
285                 } else if (path.startsWith(PATH_TRASH)) {
286             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
287                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
288                 } else if (path.startsWith(PATH_FILES))
289                         // Serve the requested resource, without the data content
290                         new FilesHandler(getServletContext()).serveResource(req, resp, false);
291                 else
292                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
293         }
294
295         /**
296          * Handle storing and updating file resources.
297          *
298      * @param req The servlet request we are processing
299      * @param resp The servlet response we are creating
300          * @throws IOException if the response cannot be sent
301          */
302         @Override
303         protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
304                 // TODO: fix code duplication between doPut() and Webdav.doPut()
305         // Strip the username part
306         String path;
307                 try {
308                         path = getUserPath(req);
309                 } catch (ObjectNotFoundException e) {
310                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
311                         return;
312                 }
313
314         if (path.startsWith(PATH_GROUPS)) {
315             resp.addHeader("Allow", methodsAllowed.get(PATH_GROUPS));
316                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
317                 } else if (path.startsWith(PATH_OTHERS)) {
318             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
319                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
320                 } else if (path.startsWith(PATH_SEARCH)) {
321             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
322                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
323                 } else if (path.startsWith(PATH_TOKEN)) {
324             resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN));
325                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
326                 } else if (path.startsWith(PATH_USERS)) {
327             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
328                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
329                 } else if (path.startsWith(PATH_SHARED)) {
330             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
331                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
332                 } else if (path.startsWith(PATH_TAGS)) {
333             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
334                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
335                 } else if (path.startsWith(PATH_TRASH)) {
336             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
337                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
338                 } else if (path.startsWith(PATH_FILES))
339                         new FilesHandler(getServletContext()).putResource(req, resp);
340                 else
341                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
342         }
343
344         @Override
345         protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
346                 boolean authDeferred = getAuthDeferred(req);
347         // Strip the username part
348         String path;
349                 try {
350                         path = getUserPath(req);
351                 } catch (ObjectNotFoundException e) {
352                         if (authDeferred) {
353                                 // We do not want to leak information if the request
354                                 // was not authenticated.
355                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
356                                 return;
357                         }
358                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
359                         return;
360                 }
361                 if (authDeferred && !path.startsWith(PATH_FILES)) {
362                         // Only files may be open to the public.
363                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
364                         return;
365                 }
366
367         // Dispatch according to the specified namespace
368         if (path.equals("") || path.equals("/"))
369                         new UserHandler().serveUser(req, resp);
370                 else if (path.startsWith(PATH_FILES))
371                         // Serve the requested resource, including the data content
372                         new FilesHandler(getServletContext()).serveResource(req, resp, true);
373                 else if (path.startsWith(PATH_TRASH))
374                         new TrashHandler().serveTrash(req, resp);
375                 else if (path.startsWith(PATH_SEARCH))
376                         new SearchHandler().serveSearchResults(req, resp);
377                 else if (path.startsWith(PATH_USERS))
378                         new UserSearchHandler().serveResults(req, resp);
379                 else if (path.startsWith(PATH_GROUPS))
380                         new GroupsHandler().serveGroups(req, resp);
381                 else if (path.startsWith(PATH_SHARED))
382                         new SharedHandler().serveShared(req, resp);
383                 else if (path.startsWith(PATH_OTHERS))
384                         new OthersHandler().serveOthers(req, resp);
385                 else if (path.startsWith(PATH_TAGS))
386                         new TagsHandler().serveTags(req, resp);
387                 else if (path.startsWith(PATH_TOKEN))
388                         new TokenHandler().newToken(req, resp);
389                 else
390                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
391         }
392
393     /**
394      * Handle a Delete request.
395      *
396      * @param req The servlet request we are processing
397      * @param resp The servlet response we are processing
398          * @throws IOException if the response cannot be sent
399      */
400     @Override
401         protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
402                 throws IOException {
403         // Strip the username part
404         String path;
405                 try {
406                         path = getUserPath(req);
407                 } catch (ObjectNotFoundException e) {
408                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
409                         return;
410                 }
411
412                 if (path.startsWith(PATH_OTHERS)) {
413             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
414                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
415                 } else if (path.startsWith(PATH_SEARCH)) {
416             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
417                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
418                 } else if (path.startsWith(PATH_TOKEN)) {
419             resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN));
420                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
421                 } else if (path.startsWith(PATH_USERS)) {
422             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
423                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
424                 } else if (path.startsWith(PATH_SHARED)) {
425             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
426                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
427                 } else if (path.startsWith(PATH_TAGS)) {
428             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
429                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
430                 } else if (path.startsWith(PATH_GROUPS))
431                         new GroupsHandler().deleteGroup(req, resp);
432                 else if (path.startsWith(PATH_TRASH))
433                         new TrashHandler().emptyTrash(req, resp);
434                 else if (path.startsWith(PATH_FILES))
435                         new FilesHandler(getServletContext()).deleteResource(req, resp);
436                 else
437                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
438     }
439
440         @Override
441         protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
442                 boolean authDeferred = getAuthDeferred(req);
443         // Strip the username part
444         String path;
445                 try {
446                         path = getUserPath(req);
447                 } catch (ObjectNotFoundException e) {
448                         if (authDeferred) {
449                                 // We do not want to leak information if the request
450                                 // was not authenticated.
451                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
452                                 return;
453                         }
454                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
455                         return;
456                 }
457                 if (authDeferred && !path.startsWith(PATH_FILES)) {
458                         // Only POST to files may be authenticated without an Authorization header.
459                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
460                         return;
461                 }
462
463                 if (path.startsWith(PATH_OTHERS)) {
464             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
465                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
466                 } else if (path.startsWith(PATH_SEARCH)) {
467             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
468                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
469                 } else if (path.startsWith(PATH_TOKEN)) {
470             resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN));
471                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
472                 } else if (path.startsWith(PATH_USERS)) {
473                         resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
474                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
475                 } else if (path.startsWith(PATH_SHARED)) {
476             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
477                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
478                 } else if (path.startsWith(PATH_TAGS)) {
479             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
480                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
481                 } else if (path.startsWith(PATH_GROUPS))
482                         new GroupsHandler().postGroup(req, resp);
483                 else if (path.startsWith(PATH_TRASH)) {
484             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
485                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
486                 } else if (path.startsWith(PATH_FILES))
487                         new FilesHandler(getServletContext()).postResource(req, resp);
488                 else if (path.equals("/"))
489                         new UserHandler().postUser(req, resp);
490                 else
491                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
492         }
493
494         /**
495          * Return the path inside the user namespace.
496          *
497          * @param req the HTTP request
498          * @return the path after the username part has been removed
499          * @throws ObjectNotFoundException if the namespace owner was not found
500          */
501         private String getUserPath(HttpServletRequest req) throws ObjectNotFoundException {
502         String path = getRelativePath(req);
503                 if (path.length() < 2)
504                         return path;
505                 int slash = path.substring(1).indexOf('/');
506                 if (slash == -1)
507                         return path;
508                 String owner = path.substring(1, slash + 1);
509                 User o;
510                 try {
511                         o = getService().findUser(owner);
512                 } catch (RpcException e) {
513                         logger.error("", e);
514                         throw new ObjectNotFoundException("User " + owner +
515                                         " not found, due to internal server error");
516                 }
517                 if (o != null) {
518                         req.setAttribute(OWNER_ATTRIBUTE, o);
519                         return path.substring(slash + 1);
520                 }
521                 if (!path.startsWith(PATH_SEARCH) && !path.startsWith(PATH_USERS) &&
522                                 !path.startsWith(PATH_TOKEN))
523                         throw new ObjectNotFoundException("User " + owner + " not found");
524                 return path;
525         }
526
527         /**
528          * Retrieve the request context path with or without a trailing slash
529          * according to the provided argument.
530          *
531          * @param req the HTTP request
532          * @param withTrailingSlash a flag that denotes whether the path should
533          *                      end with a slash
534          * @return the context path
535          */
536         protected String getContextPath(HttpServletRequest req, boolean withTrailingSlash) {
537                 String contextPath = req.getRequestURL().toString();
538                 if (withTrailingSlash)
539                         return contextPath.endsWith("/")? contextPath: contextPath + '/';
540                 return contextPath.endsWith("/")? contextPath.substring(0, contextPath.length()-1): contextPath;
541
542         }
543
544         /**
545          * @param req
546          * @param resp
547          * @param json
548          * @throws UnsupportedEncodingException
549          * @throws IOException
550          */
551         protected void sendJson(HttpServletRequest req, HttpServletResponse resp, String json) throws UnsupportedEncodingException, IOException {
552                 ByteArrayOutputStream stream = new ByteArrayOutputStream();
553         OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
554         PrintWriter writer = new PrintWriter(osWriter);
555
556         writer.write(json);
557         writer.flush();
558
559         resp.setContentType("application/json;charset=UTF-8");
560         resp.setBufferSize(output);
561                 try {
562                         copy(null, new ByteArrayInputStream(stream.toByteArray()), resp.getOutputStream(), req, null);
563                 } catch (ObjectNotFoundException e) {
564                         // This should never happen with a null first parameter.
565                         logger.error("", e);
566                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
567                         return;
568                 } catch (InsufficientPermissionsException e) {
569                         // This should never happen with a null first parameter.
570                         logger.error("", e);
571                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
572                         return;
573                 } catch (RpcException e) {
574                         logger.error("", e);
575                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
576                         return;
577                 }
578         }
579
580         /**
581          * Retrieve the path to the requested resource after removing the user namespace
582          * part and the subsequent namespace part that differentiates resources like files,
583          * groups, trash, etc.
584          *
585          * @param req the HTTP request
586          * @param namespace the subnamespace
587          * @return the inner path
588          */
589         protected String getInnerPath(HttpServletRequest req, String namespace) {
590         // Strip the username part
591         String path;
592                 try {
593                         path = getUserPath(req);
594                 } catch (ObjectNotFoundException e) {
595                         throw new RuntimeException(e.getMessage());
596                 }
597                 // Chop the resource namespace part
598         path = path.substring(namespace.length());
599                 return path;
600         }
601
602         /**
603          * Confirms the validity of the request.
604          *
605          * @param request the incoming HTTP request
606          * @return true if the request is valid, false otherwise
607          */
608         private boolean isRequestValid(HttpServletRequest request) {
609                 if (logger.isDebugEnabled()) {
610                         Enumeration headers = request.getHeaderNames();
611                         while (headers.hasMoreElements()) {
612                                 String h = (String) headers.nextElement();
613                                         logger.debug(h + ": " + request.getHeader(h));
614                         }
615                 }
616                 // Fetch the timestamp used to guard against replay attacks.
617                 long timestamp = 0;
618                 boolean useGssDateHeader = true;
619                 try {
620                         timestamp = request.getDateHeader(GSS_DATE_HEADER);
621                         if (timestamp == -1) {
622                                 useGssDateHeader = false;
623                                 timestamp = request.getDateHeader(DATE_HEADER);
624                         }
625                 } catch (IllegalArgumentException e) {
626                         return false;
627                 }
628                 if (!isTimeValid(timestamp))
629                         return false;
630
631                 // Fetch the Authorization header and find the user specified in it.
632                 String auth = request.getHeader(AUTHORIZATION_HEADER);
633                 if (auth == null)
634                         return false;
635                 String[] authParts = auth.split(" ");
636                 if (authParts.length != 2)
637                         return false;
638                 String username = authParts[0];
639                 String signature = authParts[1];
640                 User user = null;
641                 try {
642                         user = getService().findUser(username);
643                 } catch (RpcException e) {
644                         return false;
645                 }
646                 if (user == null)
647                         return false;
648
649                 request.setAttribute(USER_ATTRIBUTE, user);
650
651                 // Validate the signature in the Authorization header.
652                 String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER):
653                         request.getHeader(DATE_HEADER);
654                 String data;
655                 // Remove the servlet path from the request URI.
656                 String p = request.getRequestURI();
657                 String servletPath = request.getContextPath() + request.getServletPath();
658                 p = p.substring(servletPath.length());
659                 data = request.getMethod() + dateHeader + p;
660                 return isSignatureValid(signature, user, data);
661         }
662
663         /**
664          * Calculates the signature for the specified data String and then
665          * compares it against the provided signature. If the signatures match,
666          * the method returns true. Otherwise it returns false.
667          *
668          * @param signature the signature to compare against
669          * @param user the current user
670          * @param data the data to sign
671          * @return true if the calculated signature matches the supplied one
672          */
673         protected boolean isSignatureValid(String signature, User user, String data) {
674                 if (logger.isDebugEnabled())
675                         logger.debug("server pre-signing data: "+data);
676                 String serverSignature = null;
677                 // If the authentication token is not valid, the user must get another one.
678                 if (user.getAuthToken() == null)
679                         return false;
680                 // Get an HMAC-SHA1 key from the authentication token.
681                 SecretKeySpec signingKey = new SecretKeySpec(user.getAuthToken(), HMAC_SHA1);
682                 try {
683                         // Get an HMAC-SHA1 Mac instance and initialize with the signing key.
684                         Mac mac = Mac.getInstance(HMAC_SHA1);
685                         mac.init(signingKey);
686                         // Compute the HMAC on input data bytes.
687                         byte[] rawHmac = mac.doFinal(data.getBytes());
688                         serverSignature = new String(Base64.encodeBase64(rawHmac), "US-ASCII");
689                 } catch (Exception e) {
690                         logger.error("Error while creating signature", e);
691                         return false;
692                 }
693
694                 if (logger.isDebugEnabled())
695                         logger.debug("Signature: client="+signature+", server="+serverSignature);
696                 if (!serverSignature.equals(signature))
697                         return false;
698
699                 return true;
700         }
701
702         /**
703          * A helper method that checks if the timestamp of the request
704          * is within TIME_SKEW milliseconds of the current time. If
705          * the timestamp is older (or even newer) than that, it is
706          * considered invalid.
707          *
708          * @param timestamp the time of the request
709          * @return true if the timestamp is valid
710          */
711         protected boolean isTimeValid(long timestamp) {
712                 if (timestamp == -1)
713                         return false;
714                 Calendar cal = Calendar.getInstance();
715                 if (logger.isDebugEnabled())
716                         logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp);
717                 // Ignore the request if the timestamp is too far off.
718                 if (Math.abs(timestamp - cal.getTimeInMillis()) > getConfiguration().getInt("timeSkew", 600000))
719                         return false;
720                 return true;
721         }
722
723         protected boolean getAuthDeferred(HttpServletRequest req) {
724                 Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR);
725                 return attr == null? false: attr;
726         }
727
728         /**
729          * Return the actual requested path in the API namespace.
730          *
731          * @param request the servlet request we are processing
732          * @return the relative path
733          */
734         @Override
735         protected String getRelativePath(HttpServletRequest request) {
736                 // Remove the servlet path from the request URI.
737                 String p = request.getRequestURI();
738                 String servletPath = request.getContextPath() + request.getServletPath();
739                 String result = p.substring(servletPath.length());
740                 if (result == null || result.equals(""))
741                         result = "/";
742                 return result;
743
744         }
745 }