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 serial version UID of the class.
166 private static final long serialVersionUID = 1L;
171 private static Log logger = LogFactory.getLog(RequestHandler.class);
174 * Create a mapping between paths and allowed HTTP methods for fast lookup.
176 private final Map<String, String> methodsAllowed = new HashMap<String, String>(7);
179 public void init() throws ServletException {
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);
194 * Return the root of every API request URL.
196 protected String getApiRoot() {
197 return getConfiguration().getString("restUrl", "http://localhost:8080/gss/rest/");
201 public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
202 String method = request.getMethod();
203 String path = getRelativePath(request);
205 if (logger.isDebugEnabled())
206 logger.debug("[" + method + "] " + path);
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);
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);
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);
232 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
236 protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
237 boolean authDeferred = getAuthDeferred(req);
238 // Strip the username part
241 path = getUserPath(req);
242 } catch (ObjectNotFoundException e) {
244 // We do not want to leak information if the request
245 // was not authenticated.
246 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
249 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
252 if (authDeferred && !path.startsWith(PATH_FILES)) {
253 // Only files may be open to the public.
254 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
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);
283 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
287 * Handle storing and updating file resources.
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
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
299 path = getUserPath(req);
300 } catch (ObjectNotFoundException e) {
301 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
329 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
333 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
334 boolean authDeferred = getAuthDeferred(req);
335 // Strip the username part
338 path = getUserPath(req);
339 } catch (ObjectNotFoundException e) {
341 // We do not want to leak information if the request
342 // was not authenticated.
343 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
346 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
349 if (authDeferred && !path.startsWith(PATH_FILES)) {
350 // Only files may be open to the public.
351 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
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);
376 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
380 * Handle a Delete request.
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
387 protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
389 // Strip the username part
392 path = getUserPath(req);
393 } catch (ObjectNotFoundException e) {
394 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
420 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
424 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
425 boolean authDeferred = getAuthDeferred(req);
426 // Strip the username part
429 path = getUserPath(req);
430 } catch (ObjectNotFoundException e) {
432 // We do not want to leak information if the request
433 // was not authenticated.
434 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
437 resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
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);
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);
471 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
475 * Return the path inside the user namespace.
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
481 private String getUserPath(HttpServletRequest req) throws ObjectNotFoundException {
482 String path = getRelativePath(req);
483 if (path.length() < 2)
485 int slash = path.substring(1).indexOf('/');
488 String owner = path.substring(1, slash + 1);
491 o = getService().findUser(owner);
492 } catch (RpcException e) {
494 throw new ObjectNotFoundException("User " + owner + " not found, due to internal server error");
497 req.setAttribute(OWNER_ATTRIBUTE, o);
498 return path.substring(slash + 1);
500 if (!path.startsWith(PATH_SEARCH) && !path.startsWith(PATH_USERS))
501 throw new ObjectNotFoundException("User " + owner + " not found");
506 * Retrieve the request context path with or without a trailing slash
507 * according to the provided argument.
509 * @param req the HTTP request
510 * @param withTrailingSlash a flag that denotes whether the path should
512 * @return the context path
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;
526 * @throws UnsupportedEncodingException
527 * @throws IOException
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);
537 resp.setContentType("application/json;charset=UTF-8");
538 resp.setBufferSize(output);
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.
544 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
546 } catch (InsufficientPermissionsException e) {
547 // This should never happen with a null first parameter.
549 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
551 } catch (RpcException e) {
553 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
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.
563 * @param req the HTTP request
564 * @param namespace the subnamespace
565 * @return the inner path
567 protected String getInnerPath(HttpServletRequest req, String namespace) {
568 // Strip the username part
571 path = getUserPath(req);
572 } catch (ObjectNotFoundException e) {
573 throw new RuntimeException(e.getMessage());
575 // Chop the resource namespace part
576 path = path.substring(namespace.length());
581 * Confirms the validity of the request.
583 * @param request the incoming HTTP request
584 * @return true if the request is valid, false otherwise
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));
594 // Fetch the timestamp used to guard against replay attacks.
596 boolean useGssDateHeader = true;
598 timestamp = request.getDateHeader(GSS_DATE_HEADER);
599 if (timestamp == -1) {
600 useGssDateHeader = false;
601 timestamp = request.getDateHeader(DATE_HEADER);
603 } catch (IllegalArgumentException e) {
606 if (!isTimeValid(timestamp))
609 // Fetch the Authorization header and find the user specified in it.
610 String auth = request.getHeader(AUTHORIZATION_HEADER);
611 String[] authParts = auth.split(" ");
612 if (authParts.length != 2)
614 String username = authParts[0];
615 String signature = authParts[1];
618 user = getService().findUser(username);
619 } catch (RpcException e) {
625 request.setAttribute(USER_ATTRIBUTE, user);
627 // Validate the signature in the Authorization header.
628 String dateHeader = useGssDateHeader? request.getHeader(GSS_DATE_HEADER):
629 request.getHeader(DATE_HEADER);
631 // Remove the servlet path from the request URI.
632 String p = request.getRequestURI();
633 String servletPath = request.getContextPath() + request.getServletPath();
634 p = p.substring(servletPath.length());
635 data = request.getMethod() + dateHeader + p;
636 return isSignatureValid(signature, user, data);
640 * Calculates the signature for the specified data String and then
641 * compares it against the provided signature. If the signatures match,
642 * the method returns true. Otherwise it returns false.
644 * @param signature the signature to compare against
645 * @param user the current user
646 * @param data the data to sign
647 * @return true if the calculated signature matches the supplied one
649 protected boolean isSignatureValid(String signature, User user, String data) {
650 if (logger.isDebugEnabled())
651 logger.debug("server pre-signing data: "+data);
652 String serverSignature = null;
653 // If the authentication token is not valid, the user must get another one.
654 if (user.getAuthToken() == null)
656 // Get an HMAC-SHA1 key from the authentication token.
657 SecretKeySpec signingKey = new SecretKeySpec(user.getAuthToken(), HMAC_SHA1);
659 // Get an HMAC-SHA1 Mac instance and initialize with the signing key.
660 Mac mac = Mac.getInstance(HMAC_SHA1);
661 mac.init(signingKey);
662 // Compute the HMAC on input data bytes.
663 byte[] rawHmac = mac.doFinal(data.getBytes());
664 serverSignature = new String(Base64.encodeBase64(rawHmac), "US-ASCII");
665 } catch (Exception e) {
666 logger.error("Error while creating signature", e);
670 if (logger.isDebugEnabled())
671 logger.debug("Signature: client="+signature+", server="+serverSignature);
672 if (!serverSignature.equals(signature))
679 * A helper method that checks if the timestamp of the request
680 * is within TIME_SKEW milliseconds of the current time. If
681 * the timestamp is older (or even newer) than that, it is
682 * considered invalid.
684 * @param timestamp the time of the request
685 * @return true if the timestamp is valid
687 protected boolean isTimeValid(long timestamp) {
690 Calendar cal = Calendar.getInstance();
691 if (logger.isDebugEnabled())
692 logger.debug("Time: server=" + cal.getTimeInMillis() + ", client=" + timestamp);
693 // Ignore the request if the timestamp is too far off.
694 if (Math.abs(timestamp - cal.getTimeInMillis()) > getConfiguration().getInt("timeSkew", 600000))
699 protected boolean getAuthDeferred(HttpServletRequest req) {
700 Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR);
701 return attr == null? false: attr;
705 * Return the actual requested path in the API namespace.
707 * @param request the servlet request we are processing
708 * @return the relative path
711 protected String getRelativePath(HttpServletRequest request) {
712 // Remove the servlet path from the request URI.
713 String p = request.getRequestURI();
714 String servletPath = request.getContextPath() + request.getServletPath();
715 String result = p.substring(servletPath.length());
716 if (result == null || result.equals(""))