2 * Copyright 2008, 2009 Electronic Business Systems Ltd.
4 * This file is part of GSS.
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.
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.
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/>.
19 package gr.ebs.gss.server.rest;
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;
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;
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;
46 import org.apache.commons.codec.binary.Base64;
47 import org.apache.commons.logging.Log;
48 import org.apache.commons.logging.LogFactory;
51 * The servlet that handles requests for the REST API.
55 public class RequestHandler extends Webdav {
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.
61 protected static final String AUTH_DEFERRED_ATTR = "authDeferred";
64 * The path for the search subsystem.
66 protected static final String PATH_SEARCH = "/search";
69 * The path for the user search subsystem.
71 protected static final String PATH_USERS = "/users";
74 * The path for the resource manipulation subsystem.
76 protected static final String PATH_FILES = FileHeaderDTO.PATH_FILES;
79 * The path for the trash virtual folder.
81 protected static final String PATH_TRASH = "/trash";
84 * The path for the subsystem that deals with the user attributes.
86 protected static final String PATH_GROUPS = "/groups";
89 * The path for the shared resources virtual folder.
91 protected static final String PATH_SHARED = "/shared";
94 * The path for the other users' shared resources virtual folder.
96 protected static final String PATH_OTHERS = "/others";
99 * The path for tags created by the user.
101 protected static final String PATH_TAGS = "/tags";
104 * The GSS-specific header for the request timestamp.
106 private static final String GSS_DATE_HEADER = "X-GSS-Date";
109 * The RFC 2616 date header.
111 private static final String DATE_HEADER = "Date";
114 * The Authorization HTTP header.
116 private static final String AUTHORIZATION_HEADER = "Authorization";
119 * The group parameter name.
121 protected static final String GROUP_PARAMETER = "name";
124 * The username parameter name.
126 protected static final String USERNAME_PARAMETER = "name";
129 * The "new folder name" parameter name.
131 protected static final String NEW_FOLDER_PARAMETER = "new";
134 * The resource update parameter name.
136 protected static final String RESOURCE_UPDATE_PARAMETER = "update";
139 * The resource trash parameter name.
141 protected static final String RESOURCE_TRASH_PARAMETER = "trash";
144 * The resource restore parameter name.
146 protected static final String RESOURCE_RESTORE_PARAMETER = "restore";
149 * The resource copy parameter name.
151 protected static final String RESOURCE_COPY_PARAMETER = "copy";
154 * The resource move parameter name.
156 protected static final String RESOURCE_MOVE_PARAMETER = "move";
159 * The HMAC-SHA1 hash name.
161 private static final String HMAC_SHA1 = "HmacSHA1";
164 * The amount of milliseconds a request's timestamp may differ
165 * from the current system time.
167 private static final int TIME_SKEW = 600000;
170 * The serial version UID of the class.
172 private static final long serialVersionUID = 1L;
177 private static Log logger = LogFactory.getLog(RequestHandler.class);
180 * Create a mapping between paths and allowed HTTP methods for fast lookup.
182 private final Map<String, String> methodsAllowed = new HashMap<String, String>(7);
185 public void init() throws ServletException {
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);
200 * Return the root of every API request URL.
202 protected String getApiRoot() {
203 return getConfiguration().getString("restUrl", "http://localhost:8080/gss/rest/");
207 public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
208 String method = request.getMethod();
209 String path = getRelativePath(request);
211 if (logger.isDebugEnabled())
212 logger.debug("[" + method + "] " + path);
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);
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);
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);
238 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
242 protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
243 boolean authDeferred = getAuthDeferred(req);
244 // Strip the username part
247 path = getUserPath(req);
248 } catch (ObjectNotFoundException e) {
250 // We do not want to leak information if the request
251 // was not authenticated.
252 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
255 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
258 if (authDeferred && !path.startsWith(PATH_FILES)) {
259 // Only files may be open to the public.
260 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
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);
289 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
293 * Handle storing and updating file resources.
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
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
305 path = getUserPath(req);
306 } catch (ObjectNotFoundException e) {
307 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
335 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
339 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
340 boolean authDeferred = getAuthDeferred(req);
341 // Strip the username part
344 path = getUserPath(req);
345 } catch (ObjectNotFoundException e) {
347 // We do not want to leak information if the request
348 // was not authenticated.
349 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
352 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
355 if (authDeferred && !path.startsWith(PATH_FILES)) {
356 // Only files may be open to the public.
357 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
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);
382 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
386 * Handle a Delete request.
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
393 protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
395 // Strip the username part
398 path = getUserPath(req);
399 } catch (ObjectNotFoundException e) {
400 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
426 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
430 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
431 boolean authDeferred = getAuthDeferred(req);
432 // Strip the username part
435 path = getUserPath(req);
436 } catch (ObjectNotFoundException e) {
438 // We do not want to leak information if the request
439 // was not authenticated.
440 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
443 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
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);
475 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
479 * Return the path inside the user namespace.
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
485 private String getUserPath(HttpServletRequest req) throws ObjectNotFoundException {
486 String path = getRelativePath(req);
487 if (path.length() < 2)
489 int slash = path.substring(1).indexOf('/');
492 String owner = path.substring(1, slash + 1);
495 o = getService().findUser(owner);
496 } catch (RpcException e) {
498 throw new ObjectNotFoundException("User " + owner + " not found, due to internal server error");
501 req.setAttribute(OWNER_ATTRIBUTE, o);
502 return path.substring(slash + 1);
504 if (!path.startsWith(PATH_SEARCH) && !path.startsWith(PATH_USERS))
505 throw new ObjectNotFoundException("User " + owner + " not found");
510 * Retrieve the request context path with or without a trailing slash
511 * according to the provided argument.
513 * @param req the HTTP request
514 * @param withTrailingSlash a flag that denotes whether the path should
516 * @return the context path
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;
530 * @throws UnsupportedEncodingException
531 * @throws IOException
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);
541 resp.setContentType("text/html;charset=UTF-8");
542 resp.setBufferSize(output);
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.
548 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
550 } catch (InsufficientPermissionsException e) {
551 // This should never happen with a null first parameter.
553 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
555 } catch (RpcException e) {
557 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
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.
567 * @param req the HTTP request
568 * @param namespace the subnamespace
569 * @return the inner path
571 protected String getInnerPath(HttpServletRequest req, String namespace) {
572 // Strip the username part
575 path = getUserPath(req);
576 } catch (ObjectNotFoundException e) {
577 throw new RuntimeException(e.getMessage());
579 // Chop the resource namespace part
580 path = path.substring(namespace.length());
585 * Confirms the validity of the request.
587 * @param request the incoming HTTP request
588 * @return true if the request is valid, false otherwise
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));
598 // Fetch the timestamp used to guard against replay attacks.
600 boolean useGssDateHeader = true;
602 timestamp = request.getDateHeader(GSS_DATE_HEADER);
603 if (timestamp == -1) {
604 useGssDateHeader = false;
605 timestamp = request.getDateHeader(DATE_HEADER);
607 } catch (IllegalArgumentException e) {
610 if (!isTimeValid(timestamp))
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)
618 String username = authParts[0];
619 String signature = authParts[1];
622 user = getService().findUser(username);
623 } catch (RpcException e) {
629 request.setAttribute(USER_ATTRIBUTE, user);
631 // Validate the signature in the Authorization header.
632 String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER):
633 request.getHeader(DATE_HEADER);
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);
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.
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
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)
660 // Get an HMAC-SHA1 key from the authentication token.
661 SecretKeySpec signingKey = new SecretKeySpec(user.getAuthToken(), HMAC_SHA1);
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);
674 if (logger.isDebugEnabled())
675 logger.debug("Signature: client="+signature+", server="+serverSignature);
676 if (!serverSignature.equals(signature))
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.
688 * @param timestamp the time of the request
689 * @return true if the timestamp is valid
691 protected boolean isTimeValid(long timestamp) {
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)
703 protected boolean getAuthDeferred(HttpServletRequest req) {
704 Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR);
705 return attr == null? false: attr;