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