/* Copyright (c) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sample.authsub.src; import com.google.gdata.client.http.AuthSubUtil; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.security.GeneralSecurityException; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Acts as a proxy and retrieves the requested feed. * <p> * The identity of the user is determined using the provided authentication * cookie. The authentication cookie will be mapped to the associated Google * AuthSub token. The token will be used to retrieve the feed. * <p> * Special care must be taken to ensure the following: * - the requested feed belongs to a pre-specified approved list * - since cookies are used and GData URLs aren't protected, the URL being * requested by the client should contain a secure hash. This is primarily * required for POST/PUT/DELETE but shown in the GET example below as an * example. * * */ public class RetrieveFeedServlet extends HttpServlet { private static String[] acceptedFeedPrefixList = { "http://www.google.com/calendar/feeds", "https://www.google.com/calendar/feeds" }; protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Retrieve the authentication cookie to identify user String principal = Utility.getCookieValueWithName(req.getCookies(), Utility.LOGIN_COOKIE_NAME); if (principal == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unidentified principal."); return; } // Check that the user has an AuthSub token String authSubToken = TokenManager.retrieveToken(principal); if (authSubToken == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "User isn't authorized through AuthSub."); return; } // If no query string, complain if (req.getQueryString() == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Query string is required."); return; } // Parse the query parameters String queryString = URLDecoder.decode(req.getQueryString(), "UTF-8"); Map<String,String> queryParams = Utility.parseQueryString(queryString); String queryUri = queryParams.get("href"); String token = queryParams.get("token"); String timestamp = queryParams.get("timestamp"); if ((queryUri == null) || (token == null) || (timestamp == null)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing a query parameter."); return; } // Verify the feed by checking that it's from a known list of feeds and // that the secure token hasn't expired and is valid. if (!verifyFeedRequest(principal, queryUri, token, timestamp, "GET")) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request failed validation."); return; } // Handle a GET request handleGetRequest(req, resp, queryUri, authSubToken); } /** * Handles a GET request by issuing a GET to the requested feed with the * AuthSub token attached in a header. The output from the server will * be proxied back to the requestor. * POST/PUT/DELETE can be handled in a similar manner except that the XML * sent as part of the request should be sent to the server. */ private void handleGetRequest(HttpServletRequest req, HttpServletResponse resp, String queryUri, String authSubToken) throws ServletException, IOException { HttpURLConnection connection = null; try { connection = openConnectionFollowRedirects(queryUri, authSubToken); } catch (GeneralSecurityException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error creating authSub header."); return; } catch (MalformedURLException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Malformed URL - " + e.getMessage()); return; } catch (IOException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "IOException - " + e.getMessage()); return; } int respCode = connection.getResponseCode(); // Handle error from remote server if (respCode != HttpServletResponse.SC_OK) { Map<String, List<String>> headers = connection.getHeaderFields(); StringBuffer errorMessage = new StringBuffer( "Failed to retrive calendar feed from: "); errorMessage.append(queryUri); errorMessage.append(".\nServer Error Response:\n"); errorMessage.append(connection.getResponseMessage()); for (Iterator<String> iter = headers.keySet().iterator() ; iter.hasNext();) { String header = iter.next(); List<String> headerValues = headers.get(header); for (Iterator<String> headerIter = headerValues.iterator() ; headerIter.hasNext(); ) { String headerVal = headerIter.next(); errorMessage.append(header + ": " + headerVal + ", "); } } resp.sendError(respCode, errorMessage.toString()); return; } // Handle success reply from remote server try { BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; while((line = reader.readLine()) != null) { resp.getWriter().write(line); } } catch(IOException e) { // Ignore } } /** * Open a HTTP connection to the provided URL with the AuthSub token specified * in the header. Follow redirects returned by the server - a new AuthSub * signature will be computed for each of the redirected-to URLs. */ private HttpURLConnection openConnectionFollowRedirects(String urlStr, String authSubToken) throws MalformedURLException, GeneralSecurityException, IOException { boolean redirectsDone = false; HttpURLConnection connection = null; while (!redirectsDone) { URL url = new URL(urlStr); // Open connection to requested feed connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Form AuthSub authentication header String authHeader = null; authHeader = AuthSubUtil.formAuthorizationHeader(authSubToken, Utility.getPrivateKey(), url, "GET"); connection.setRequestProperty("Authorization", authHeader); connection.setInstanceFollowRedirects(false); int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { urlStr = connection.getHeaderField("Location"); // If "Location" is not specified, stop following redirects, and // propagate error to the client of the proxy if (urlStr == null) { redirectsDone = true; } } else { redirectsDone = true; } } return connection; } /** * Verifies the request for a feed by: * a. validating that the request belongs to a known list of feeds * b. validating the token (to protect against url command attacks) *<p> * This verification is in order to prevent the proxy from URL command attacks * which is a cross site scripting problem. */ private boolean verifyFeedRequest(String cookie, String feed, String token, String timestamp, String method) { // Check the list of accepted feed URLs that we are proxying int url_i; for (url_i = 0; url_i < acceptedFeedPrefixList.length; url_i++) { if(feed.toLowerCase().startsWith( acceptedFeedPrefixList[url_i].toLowerCase())) { break; } } if (url_i == acceptedFeedPrefixList.length) { return false; } return SecureUrl.isTokenValid(token, cookie, feed, method, timestamp); } }