1cdd390f41aafe5f7ce726ecefb684efabc48434
[pithos] / gss / 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 amount of milliseconds a request's timestamp may differ
165          * from the current system time.
166          */
167         private static final int TIME_SKEW = 600000;
168
169         /**
170          * The serial version UID of the class.
171          */
172         private static final long serialVersionUID = 1L;
173
174         /**
175          * The logger.
176          */
177         private static Log logger = LogFactory.getLog(RequestHandler.class);
178
179         /**
180          * Create a mapping between paths and allowed HTTP methods for fast lookup.
181          */
182         private final Map<String, String> methodsAllowed = new HashMap<String, String>(7);
183
184         @Override
185         public void init() throws ServletException {
186                 super.init();
187                 methodsAllowed.put(PATH_FILES, METHOD_GET + ", " + METHOD_POST +
188                                         ", " + METHOD_DELETE + ", " + METHOD_PUT + ", " + METHOD_HEAD);
189                 methodsAllowed.put(PATH_GROUPS, METHOD_GET + ", " + METHOD_POST +
190                                         ", " + METHOD_DELETE);
191                 methodsAllowed.put(PATH_OTHERS, METHOD_GET);
192                 methodsAllowed.put(PATH_SEARCH, METHOD_GET);
193                 methodsAllowed.put(PATH_USERS, METHOD_GET);
194                 methodsAllowed.put(PATH_SHARED, METHOD_GET);
195                 methodsAllowed.put(PATH_TAGS, METHOD_GET);
196                 methodsAllowed.put(PATH_TRASH, METHOD_GET + ", " + METHOD_DELETE);
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_USERS)) {
274             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
275                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
276                 } else if (path.startsWith(PATH_SHARED)) {
277             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
278                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
279                 } else if (path.startsWith(PATH_TAGS)) {
280             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
281                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
282                 } else if (path.startsWith(PATH_TRASH)) {
283             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
284                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
285                 } else if (path.startsWith(PATH_FILES))
286                         // Serve the requested resource, without the data content
287                         new FilesHandler(getServletContext()).serveResource(req, resp, false);
288                 else
289                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
290         }
291
292         /**
293          * Handle storing and updating file resources.
294          *
295      * @param req The servlet request we are processing
296      * @param resp The servlet response we are creating
297          * @throws IOException if the response cannot be sent
298          */
299         @Override
300         protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
301                 // TODO: fix code duplication between doPut() and Webdav.doPut()
302         // Strip the username part
303         String path;
304                 try {
305                         path = getUserPath(req);
306                 } catch (ObjectNotFoundException e) {
307                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
308                         return;
309                 }
310
311         if (path.startsWith(PATH_GROUPS)) {
312             resp.addHeader("Allow", methodsAllowed.get(PATH_GROUPS));
313                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
314                 } else if (path.startsWith(PATH_OTHERS)) {
315             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
316                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
317                 } else if (path.startsWith(PATH_SEARCH)) {
318             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
319                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
320                 } else if (path.startsWith(PATH_USERS)) {
321             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
322                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
323                 } else if (path.startsWith(PATH_SHARED)) {
324             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
325                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
326                 } else if (path.startsWith(PATH_TAGS)) {
327             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
328                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
329                 } else if (path.startsWith(PATH_TRASH)) {
330             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
331                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
332                 } else if (path.startsWith(PATH_FILES))
333                         new FilesHandler(getServletContext()).putResource(req, resp);
334                 else
335                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
336         }
337
338         @Override
339         protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
340                 boolean authDeferred = getAuthDeferred(req);
341         // Strip the username part
342         String path;
343                 try {
344                         path = getUserPath(req);
345                 } catch (ObjectNotFoundException e) {
346                         if (authDeferred) {
347                                 // We do not want to leak information if the request
348                                 // was not authenticated.
349                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
350                                 return;
351                         }
352                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
353                         return;
354                 }
355                 if (authDeferred && !path.startsWith(PATH_FILES)) {
356                         // Only files may be open to the public.
357                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
358                         return;
359                 }
360
361         // Dispatch according to the specified namespace
362         if (path.equals("") || path.equals("/"))
363                         new UserHandler().serveUser(req, resp);
364                 else if (path.startsWith(PATH_FILES))
365                         // Serve the requested resource, including the data content
366                         new FilesHandler(getServletContext()).serveResource(req, resp, true);
367                 else if (path.startsWith(PATH_TRASH))
368                         new TrashHandler().serveTrash(req, resp);
369                 else if (path.startsWith(PATH_SEARCH))
370                         new SearchHandler().serveSearchResults(req, resp);
371                 else if (path.startsWith(PATH_USERS))
372                         new UserSearchHandler().serveResults(req, resp);
373                 else if (path.startsWith(PATH_GROUPS))
374                         new GroupsHandler().serveGroups(req, resp);
375                 else if (path.startsWith(PATH_SHARED))
376                         new SharedHandler().serveShared(req, resp);
377                 else if (path.startsWith(PATH_OTHERS))
378                         new OthersHandler().serveOthers(req, resp);
379                 else if (path.startsWith(PATH_TAGS))
380                         new TagsHandler().serveTags(req, resp);
381                 else
382                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
383         }
384
385     /**
386      * Handle a Delete request.
387      *
388      * @param req The servlet request we are processing
389      * @param resp The servlet response we are processing
390          * @throws IOException if the response cannot be sent
391      */
392     @Override
393         protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
394                 throws IOException {
395         // Strip the username part
396         String path;
397                 try {
398                         path = getUserPath(req);
399                 } catch (ObjectNotFoundException e) {
400                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
401                         return;
402                 }
403
404                 if (path.startsWith(PATH_OTHERS)) {
405             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
406                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
407                 } else if (path.startsWith(PATH_SEARCH)) {
408             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
409                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
410                 } else if (path.startsWith(PATH_USERS)) {
411             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
412                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
413                 } else if (path.startsWith(PATH_SHARED)) {
414             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
415                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
416                 } else if (path.startsWith(PATH_TAGS)) {
417             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
418                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
419                 } else if (path.startsWith(PATH_GROUPS))
420                         new GroupsHandler().deleteGroup(req, resp);
421                 else if (path.startsWith(PATH_TRASH))
422                         new TrashHandler().emptyTrash(req, resp);
423                 else if (path.startsWith(PATH_FILES))
424                         new FilesHandler(getServletContext()).deleteResource(req, resp);
425                 else
426                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
427     }
428
429         @Override
430         protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
431                 boolean authDeferred = getAuthDeferred(req);
432         // Strip the username part
433         String path;
434                 try {
435                         path = getUserPath(req);
436                 } catch (ObjectNotFoundException e) {
437                         if (authDeferred) {
438                                 // We do not want to leak information if the request
439                                 // was not authenticated.
440                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
441                                 return;
442                         }
443                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
444                         return;
445                 }
446                 if (authDeferred && !path.startsWith(PATH_FILES)) {
447                         // Only POST to files may be authenticated without an Authorization header.
448                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
449                         return;
450                 }
451
452                 if (path.startsWith(PATH_OTHERS)) {
453             resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS));
454                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
455                 } else if (path.startsWith(PATH_SEARCH)) {
456             resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH));
457                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
458                 } else if (path.startsWith(PATH_USERS)) {
459             resp.addHeader("Allow", methodsAllowed.get(PATH_USERS));
460                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
461                 } else if (path.startsWith(PATH_SHARED)) {
462             resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED));
463                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
464                 } else if (path.startsWith(PATH_TAGS)) {
465             resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS));
466                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
467                 } else if (path.startsWith(PATH_GROUPS))
468                         new GroupsHandler().postGroup(req, resp);
469                 else if (path.startsWith(PATH_TRASH)) {
470             resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH));
471                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
472                 } else if (path.startsWith(PATH_FILES))
473                         new FilesHandler(getServletContext()).postResource(req, resp);
474                 else
475                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
476         }
477
478         /**
479          * Return the path inside the user namespace.
480          *
481          * @param req the HTTP request
482          * @return the path after the username part has been removed
483          * @throws ObjectNotFoundException if the namespace owner was not found
484          */
485         private String getUserPath(HttpServletRequest req) throws ObjectNotFoundException {
486         String path = getRelativePath(req);
487                 if (path.length() < 2)
488                         return path;
489                 int slash = path.substring(1).indexOf('/');
490                 if (slash == -1)
491                         return path;
492                 String owner = path.substring(1, slash + 1);
493                 User o;
494                 try {
495                         o = getService().findUser(owner);
496                 } catch (RpcException e) {
497                         logger.error("", e);
498                         throw new ObjectNotFoundException("User " + owner + " not found, due to internal server error");
499                 }
500                 if (o != null) {
501                         req.setAttribute(OWNER_ATTRIBUTE, o);
502                         return path.substring(slash + 1);
503                 }
504                 if (!path.startsWith(PATH_SEARCH) && !path.startsWith(PATH_USERS))
505                         throw new ObjectNotFoundException("User " + owner + " not found");
506                 return path;
507         }
508
509         /**
510          * Retrieve the request context path with or without a trailing slash
511          * according to the provided argument.
512          *
513          * @param req the HTTP request
514          * @param withTrailingSlash a flag that denotes whether the path should
515          *                      end with a slash
516          * @return the context path
517          */
518         protected String getContextPath(HttpServletRequest req, boolean withTrailingSlash) {
519                 String contextPath = req.getRequestURL().toString();
520                 if (withTrailingSlash)
521                         return contextPath.endsWith("/")? contextPath: contextPath + '/';
522                 return contextPath.endsWith("/")? contextPath.substring(0, contextPath.length()-1): contextPath;
523
524         }
525
526         /**
527          * @param req
528          * @param resp
529          * @param json
530          * @throws UnsupportedEncodingException
531          * @throws IOException
532          */
533         protected void sendJson(HttpServletRequest req, HttpServletResponse resp, String json) throws UnsupportedEncodingException, IOException {
534                 ByteArrayOutputStream stream = new ByteArrayOutputStream();
535         OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
536         PrintWriter writer = new PrintWriter(osWriter);
537
538         writer.write(json);
539         writer.flush();
540
541         resp.setContentType("text/html;charset=UTF-8");
542         resp.setBufferSize(output);
543                 try {
544                         copy(null, new ByteArrayInputStream(stream.toByteArray()), resp.getOutputStream(), req, null);
545                 } catch (ObjectNotFoundException e) {
546                         // This should never happen with a null first parameter.
547                         logger.error("", e);
548                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
549                         return;
550                 } catch (InsufficientPermissionsException e) {
551                         // This should never happen with a null first parameter.
552                         logger.error("", e);
553                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
554                         return;
555                 } catch (RpcException e) {
556                         logger.error("", e);
557                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
558                         return;
559                 }
560         }
561
562         /**
563          * Retrieve the path to the requested resource after removing the user namespace
564          * part and the subsequent namespace part that differentiates resources like files,
565          * groups, trash, etc.
566          *
567          * @param req the HTTP request
568          * @param namespace the subnamespace
569          * @return the inner path
570          */
571         protected String getInnerPath(HttpServletRequest req, String namespace) {
572         // Strip the username part
573         String path;
574                 try {
575                         path = getUserPath(req);
576                 } catch (ObjectNotFoundException e) {
577                         throw new RuntimeException(e.getMessage());
578                 }
579                 // Chop the resource namespace part
580         path = path.substring(namespace.length());
581                 return path;
582         }
583
584         /**
585          * Confirms the validity of the request.
586          *
587          * @param request the incoming HTTP request
588          * @return true if the request is valid, false otherwise
589          */
590         private boolean isRequestValid(HttpServletRequest request) {
591                 if (logger.isDebugEnabled()) {
592                         Enumeration headers = request.getHeaderNames();
593                         while (headers.hasMoreElements()) {
594                                 String h = (String) headers.nextElement();
595                                         logger.debug(h + ": " + request.getHeader(h));
596                         }
597                 }
598                 // Fetch the timestamp used to guard against replay attacks.
599                 long timestamp = 0;
600                 boolean useGssDateHeader = true;
601                 try {
602                         timestamp = request.getDateHeader(GSS_DATE_HEADER);
603                         if (timestamp == -1) {
604                                 useGssDateHeader = false;
605                                 timestamp = request.getDateHeader(DATE_HEADER);
606                         }
607                 } catch (IllegalArgumentException e) {
608                         return false;
609                 }
610                 if (!isTimeValid(timestamp))
611                         return false;
612
613                 // Fetch the Authorization header and find the user specified in it.
614                 String auth = request.getHeader(AUTHORIZATION_HEADER);
615                 String[] authParts = auth.split(" ");
616                 if (authParts.length != 2)
617                         return false;
618                 String username = authParts[0];
619                 String signature = authParts[1];
620                 User user = null;
621                 try {
622                         user = getService().findUser(username);
623                 } catch (RpcException e) {
624                         return false;
625                 }
626                 if (user == null)
627                         return false;
628
629                 request.setAttribute(USER_ATTRIBUTE, user);
630
631                 // Validate the signature in the Authorization header.
632                 String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER):
633                         request.getHeader(DATE_HEADER);
634                 String data;
635                 // Remove the servlet path from the request URI.
636                 String p = request.getRequestURI();
637                 String servletPath = request.getContextPath() + request.getServletPath();
638                 p = p.substring(servletPath.length());
639                 data = request.getMethod() + dateHeader + p;
640                 return isSignatureValid(signature, user, data);
641         }
642
643         /**
644          * Calculates the signature for the specified data String and then
645          * compares it against the provided signature. If the signatures match,
646          * the method returns true. Otherwise it returns false.
647          *
648          * @param signature the signature to compare against
649          * @param user the current user
650          * @param data the data to sign
651          * @return true if the calculated signature matches the supplied one
652          */
653         protected boolean isSignatureValid(String signature, User user, String data) {
654                 if (logger.isDebugEnabled())
655                         logger.debug("server pre-signing data: "+data);
656                 String serverSignature = null;
657                 // If the authentication token is not valid, the user must get another one.
658                 if (user.getAuthToken() == null)
659                         return false;
660                 // Get an HMAC-SHA1 key from the authentication token.
661                 SecretKeySpec signingKey = new SecretKeySpec(user.getAuthToken(), HMAC_SHA1);
662                 try {
663                         // Get an HMAC-SHA1 Mac instance and initialize with the signing key.
664                         Mac mac = Mac.getInstance(HMAC_SHA1);
665                         mac.init(signingKey);
666                         // Compute the HMAC on input data bytes.
667                         byte[] rawHmac = mac.doFinal(data.getBytes());
668                         serverSignature = new String(Base64.encodeBase64(rawHmac), "US-ASCII");
669                 } catch (Exception e) {
670                         logger.error("Error while creating signature", e);
671                         return false;
672                 }
673
674                 if (logger.isDebugEnabled())
675                         logger.debug("Signature: client="+signature+", server="+serverSignature);
676                 if (!serverSignature.equals(signature))
677                         return false;
678
679                 return true;
680         }
681
682         /**
683          * A helper method that checks if the timestamp of the request
684          * is within TIME_SKEW milliseconds of the current time. If
685          * the timestamp is older (or even newer) than that, it is
686          * considered invalid.
687          *
688          * @param timestamp the time of the request
689          * @return true if the timestamp is valid
690          */
691         protected boolean isTimeValid(long timestamp) {
692                 if (timestamp == -1)
693                         return false;
694                 Calendar cal = Calendar.getInstance();
695                 if (logger.isDebugEnabled())
696                         logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp);
697                 // Ignore the request if the timestamp is too far off.
698                 if (Math.abs(timestamp - cal.getTimeInMillis()) > TIME_SKEW)
699                         return false;
700                 return true;
701         }
702
703         protected boolean getAuthDeferred(HttpServletRequest req) {
704                 Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR);
705                 return attr == null? false: attr;
706         }
707 }