Author: lindner
Date: Wed Mar 12 23:02:40 2008
New Revision: 636638

URL: http://svn.apache.org/viewvc?rev=636638&view=rev
Log:
Signed fetch code patch submitted by Brian Eaton for SHINDIG-109
Reviewed and applied.  Nice!


Added:
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
    
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
Modified:
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
    
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
 Wed Mar 12 23:02:40 2008
@@ -18,55 +18,31 @@
  */
 package org.apache.shindig.gadgets;
 
+import org.apache.shindig.util.BlobCrypterException;
+
 /**
  * A GadgetSigner implementation that just provides dummy data to satisfy
  * tests and API calls. Do not use this for any security applications.
  */
 public class BasicGadgetSigner implements GadgetSigner {
-  private final long timeToLive;
-
-  /**
-   * [EMAIL PROTECTED]
-   */
-  public GadgetToken createToken(Gadget gadget) {
-    String uri = gadget.getSpec().getUrl().toString();
-    long expiry = System.currentTimeMillis() + this.timeToLive;
-    return new BasicGadgetToken(uri + '$' + expiry);
-  }
 
   /**
    * [EMAIL PROTECTED]
-   * This implementation only validates non-empty tokens. Empty tokens
-   * are considered to always be valid.
+   * 
+   * Returns a token with some faked out values.
    */
   public GadgetToken createToken(String stringToken) throws GadgetException {
-    if (stringToken != null && stringToken.length() != 0) {
-      String[] parts = stringToken.split("\\$");
-      if (parts.length != 2) {
-        throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN,
-            "Invalid token format.");
-      }
-      long expiry = Long.parseLong(parts[1]);
-      if (expiry < System.currentTimeMillis()) {
-        throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN,
-            "Expired token.");
-      }
+    try {
+      return new BasicGadgetToken("fakeowner", "fakeviewer", "fakeapp",
+          "fakedomain");
+    } catch (BlobCrypterException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN, e);
     }
-    return new BasicGadgetToken(stringToken);
-  }
-
-  /**
-   * Create signer
-   * @param timeToLive basic TTL in milliseconds
-   */
-  public BasicGadgetSigner(long timeToLive) {
-    this.timeToLive = timeToLive;
   }
 
   /**
    * Creates a signer with 24 hour token expiry
    */
   public BasicGadgetSigner() {
-    this(24L * 60 * 60 * 1000);
   }
 }

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
 Wed Mar 12 23:02:40 2008
@@ -18,16 +18,33 @@
  */
 package org.apache.shindig.gadgets;
 
-import java.net.URL;
-import java.util.Collection;
+import org.apache.shindig.util.BlobCrypter;
+import org.apache.shindig.util.BlobCrypterException;
+
+import java.util.HashMap;
 import java.util.Map;
 
 /**
  * Primitive token implementation that uses stings as tokens.
  */
 class BasicGadgetToken implements GadgetToken {
+  /** serialized form of the token */
   private final String token;
-
+  
+  /** data from the token */
+  private final Map<String, String> tokenData;
+  
+  /** tool to use for signing and encrypting the token */
+  private BlobCrypter crypter = new BlobCrypter(INSECURE_KEY);
+  
+  private static final byte[] INSECURE_KEY =
+    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+  
+  private static final String OWNER_KEY = "o";
+  private static final String APP_KEY = "a";
+  private static final String VIEWER_KEY = "v";
+  private static final String DOMAIN_KEY = "d";
+  
   /**
    * [EMAIL PROTECTED]
    */
@@ -38,17 +55,50 @@
   /**
    * Generates a token from an input string
    * @param token String form of token
+   * @param maxAge max age of the token (in seconds)
+   * @throws BlobCrypterException 
    */
-  public BasicGadgetToken(String token) {
+  public BasicGadgetToken(String token, int maxAge)
+  throws BlobCrypterException {
     this.token = token;
+    this.tokenData = crypter.unwrap(token, maxAge);
+  }
+  
+  public BasicGadgetToken(String owner, String viewer, String app,
+      String domain) throws BlobCrypterException {
+    tokenData = new HashMap<String, String>();
+    tokenData.put(OWNER_KEY, owner);
+    tokenData.put(VIEWER_KEY, viewer);
+    tokenData.put(APP_KEY, app);
+    tokenData.put(DOMAIN_KEY, domain);
+    token = crypter.wrap(tokenData);
+  }
+
+  /**
+   * [EMAIL PROTECTED]
+   */
+  public String getAppId() {
+    return tokenData.get(APP_KEY);
+  }
+
+  /**
+   * [EMAIL PROTECTED]
+   */
+  public String getDomain() {
+    return tokenData.get(DOMAIN_KEY);
+  }
+
+  /**
+   * [EMAIL PROTECTED]
+   */
+  public String getOwnerId() {
+    return tokenData.get(OWNER_KEY);
   }
 
   /**
    * [EMAIL PROTECTED]
-   * Signer that does not sign.
    */
-  public URL signUrl(URL uri, String httpMethod,
-      Map<String, Collection<String>> parameters) {
-    return uri;
+  public String getViewerId() {
+    return tokenData.get(VIEWER_KEY);
   }
 }

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
 Wed Mar 12 23:02:40 2008
@@ -19,34 +19,13 @@
 package org.apache.shindig.gadgets;
 
 /**
- *  Handles generation of signing tokens for various request types.
- *  Implementations are free to define their own signing parameters in any
- *  way that is suitable for their site.
+ *  Handles verification of gadget security tokens.
  */
 public interface GadgetSigner {
 
   /**
-   * Generates a token for the given gadget.
-   * Implementations should also add their own user-related context data
-   * to the token.
-   *
-   * @param gadget
-   * @return The token representation of the input data.
-   */
-  public GadgetToken createToken(Gadget gadget);
-
-  /**
-   * Generates a token from an input string. This call must produce a token 
that
-   * will validate against a token produced directly from a gadget so that the
-   * following function will always returns a valid GadgetToken:
-   *
-   * <code>
-   * GadgetToken testToken(Gadget gadget, GadgetSigner signer) {
-   *   GadgetToken token = signer.createToken(gadget);
-   *   return signer.createToken(token.toSerialForm());
-   * }
-   * </code>
-   *
+   * Decrypts and verifies a gadget security token to return a gadget token.
+   * 
    * @param tokenString String representation of the token to be created.
    * @return The token representation of the input data.
    * @throws GadgetException If tokenString is not a valid token

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
 Wed Mar 12 23:02:40 2008
@@ -18,13 +18,9 @@
  */
 package org.apache.shindig.gadgets;
 
-import java.net.URL;
-import java.util.Collection;
-import java.util.Map;
-
 /**
  * An abstract representation of a signing token.
- * Use in conjuction with @code GadgetSigner.
+ * Use in conjunction with @code GadgetSigner.
  */
 public interface GadgetToken {
 
@@ -37,15 +33,23 @@
    */
   public String toSerialForm();
 
+  /**
+   * @return the owner from the token, or null if there is none.
+   */
+  public String getOwnerId();
+
+  /**
+   * @return the viewer from the token, or null if there is none.
+   */
+  public String getViewerId();
 
   /**
-   * Sign a URL using this token
-   * @param uri The URL to sign
-   * @param httpMethod The HTTP method used
-   * @param parameters associated with the signing request
-   * @return The signed URL
-   * @throws GadgetException
+   * @return the application id from the token, or null if there is none.
+   */
+  public String getAppId();
+  
+  /**
+   * @return the domain from the token, or null if there is none.
    */
-  public URL signUrl(URL uri, String httpMethod,
-      Map<String, Collection<String>> parameters) throws GadgetException;
+  public String getDomain();
 }

Added: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java?rev=636638&view=auto
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
 (added)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
 Wed Mar 12 23:02:40 2008
@@ -0,0 +1,38 @@
+/*
+ * 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 org.apache.shindig.gadgets;
+
+import java.net.URL;
+
+/**
+ * Request signer interface that does nothing, for convenience of writing
+ * code that may or may not need to sign requests.
+ */
+public class NullRequestSigner implements RequestSigner {
+
+  public String getApprovalUrl() {
+    return null;
+  }
+
+  public Status getSigningStatus() {
+    return Status.OK;
+  }
+
+  @SuppressWarnings("unused")
+  public URL signRequest(String method, URL resource, String postBody) throws 
GadgetException {
+    return resource;
+  }
+
+}

Added: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java?rev=636638&view=auto
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
 (added)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
 Wed Mar 12 23:02:40 2008
@@ -0,0 +1,60 @@
+/*
+ * 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 org.apache.shindig.gadgets;
+
+import java.net.URL;
+
+/**
+ * Interface for generating signed requests and handling responses.
+ * 
+ * Most of the functionality here isn't used at the moment, just enough to
+ * implement signed fetch.  The status and approval bits will be used for 
+ * full OAuth.
+ */
+public interface RequestSigner {
+
+  /**
+   * Whether the signing request succeeded.
+   */
+  public enum Status {
+    OK,
+    APPROVAL_NEEDED,
+  }
+  
+  /**
+   * Signs a URL and post body, returning a modified URL including the
+   * signature.  The post body is not modified.
+   * 
+   * @param method how the request will be sent
+   * @param resource the URL to sign
+   * @param postBody the body of the request (may be null)
+   * 
+   * @return a signed URL
+   * @throws GadgetException if an error occurs.
+   */
+  public URL signRequest(String method, URL resource, String postBody)
+  throws GadgetException;
+  
+  /**
+   * @return the status associated with the last signing request.  May return
+   * APPROVAL_NEEDED if user interaction is required.
+   */
+  public Status getSigningStatus();
+
+  /**
+   * @return the URL the user needs to visit to approve the access request.
+   */
+  public String getApprovalUrl();
+}

Added: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java?rev=636638&view=auto
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
 (added)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
 Wed Mar 12 23:02:40 2008
@@ -0,0 +1,277 @@
+/*
+ * 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 org.apache.shindig.gadgets;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuth.Parameter;
+import net.oauth.signature.RSA_SHA1;
+
+import org.apache.shindig.util.Crypto;
+import org.apache.shindig.util.TimeSource;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Implements signed fetch based on the OAuth request signing algorithm.
+ * 
+ * Subclasses can override signMessage to use their own crypto if they don't
+ * like the oauth.net code for some reason.
+ * 
+ * Instances of this class are only accessed by a single thread at a time,
+ * but instances may be created by multiple threads.
+ */
+public class SignedFetchRequestSigner implements RequestSigner {
+
+  protected static final String OPENSOCIAL_OWNERID = "opensocial_ownerid";
+
+  protected static final String OPENSOCIAL_VIEWERID = "opensocial_viewerid";
+
+  protected static final String OPENSOCIAL_APPID = "opensocial_appid";
+  
+  protected static final String XOAUTH_PUBLIC_KEY =
+    "xoauth_signature_publickey";
+
+  protected static final Pattern ALLOWED_PARAM_NAME = Pattern
+      .compile("[\\w_\\-]+");
+
+  protected final TimeSource clock = new TimeSource();
+
+  /**
+   * Authentication token for the user and gadget making the request.
+   */
+  protected GadgetToken authToken;
+
+  /**
+   * Private key we pass to the OAuth RSA_SHA1 algorithm.  This can be a 
+   * PrivateKey object, or a PEM formatted private key, or a DER encoded byte
+   * array for the private key.  (No, really, they accept any of them.)
+   */
+  protected Object privateKeyObject;
+
+  /**
+   * The name of the key, included in the fetch to help with key rotation.
+   */
+  protected String keyName;
+
+  /**
+   * Constructor for subclasses that don't want this code to use their
+   * keys.
+   */
+  protected SignedFetchRequestSigner(GadgetToken authToken) {
+    init(authToken, null, null);
+  }
+
+  /**
+   * Constructor based on signing with the given PrivateKey object.
+   * 
+   * @param authToken verified gadget security token
+   * @param keyName name of the key to include in the request
+   * @param privateKey the key to use for the signing
+   */
+  public SignedFetchRequestSigner(GadgetToken authToken, String keyName,
+      PrivateKey privateKey) {
+    init(authToken, keyName, privateKey);
+  }
+  
+  /**
+   * Constructor based on signing with the given PrivateKey object.
+   * 
+   * @param authToken verified gadget security token
+   * @param keyName name of the key to include in the request
+   * @param privateKey base64 encoded private key
+   */
+  public SignedFetchRequestSigner(GadgetToken authToken, String keyName,
+      String privateKey) {
+    init(authToken, keyName, privateKey);
+  }
+ 
+  /**
+   * Constructor based on signing with the given PrivateKey object.
+   * 
+   * @param authToken verified gadget security token
+   * @param keyName name of the key to include in the request
+   * @param privateKey DER encoded private key
+   */
+  public SignedFetchRequestSigner(GadgetToken authToken, String keyName, 
+      byte[] privateKey) {
+    init(authToken, keyName, privateKey);
+  }
+  
+  protected void init(GadgetToken authToken, String keyName,
+      Object privateKeyObject) {
+    this.authToken = authToken;
+    this.keyName = keyName;
+    this.privateKeyObject = privateKeyObject;
+  }
+
+  /**
+   * Signed fetch doesn't require approvals, always returns null.
+   */
+  public String getApprovalUrl() {
+    return null;
+  }
+
+  /**
+   * Signed fetch doesn't require approvals, always returns Status.OK
+   */
+  public Status getSigningStatus() {
+    return Status.OK;
+  }
+
+  public URL signRequest(String method, URL resource, String postBody)
+  throws GadgetException {
+    try {
+      // Parse the request into parameters for OAuth signing, stripping out
+      // any OAuth or OpenSocial parameters injected by the client
+      String query = resource.getQuery();
+      resource = removeQuery(resource);
+      List<OAuth.Parameter> queryParams = sanitize(OAuth.decodeForm(query));
+      List<OAuth.Parameter> postParams = sanitize(OAuth.decodeForm(postBody));
+      List<OAuth.Parameter> msgParams = new ArrayList<OAuth.Parameter>();
+      msgParams.addAll(queryParams);
+      msgParams.addAll(postParams);
+
+      // Add the OpenSocial parameters
+      addOpenSocialParams(msgParams);
+      
+      // Add the OAuth parameters
+      addOAuthParams(msgParams);
+
+      // Build and sign the OAuthMessage; note that the resource here has
+      // no query string, the parameters are all in msgParams
+      OAuthMessage message = new OAuthMessage(method, resource.toString(),
+          msgParams);
+      
+      // Sign the message, this may jump into a subclass
+      signMessage(message);
+
+      // Rebuild the query string, including all of the parameters we added.
+      // We have to be careful not to copy POST parameters into the query.
+      // If post and query parameters share a name, they end up being removed
+      // from the query.
+      HashSet<String> forPost = new HashSet<String>();
+      for (OAuth.Parameter param : postParams) {
+        forPost.add(param.getKey());
+      }
+      List<Map.Entry<String, String>> newQuery =
+        new ArrayList<Map.Entry<String, String>>();
+      for (Map.Entry<String, String> param : message.getParameters()) {
+        if (! forPost.contains(param.getKey())) {
+          newQuery.add(param);
+        }
+      }
+      String finalQuery = OAuth.formEncode(newQuery);
+      return new URL(resource.getProtocol(), resource.getHost(),
+          resource.getPort(), resource.getPath() + "?" + finalQuery);
+    } catch (Exception e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+          e);
+    }
+  }
+
+  private URL removeQuery(URL resource) throws MalformedURLException {
+    return new URL(resource.getProtocol(), resource.getHost(),
+        resource.getPort(), resource.getPath());
+  }
+  
+
+  private void addOpenSocialParams(List<Parameter> msgParams) {
+    String owner = authToken.getOwnerId();
+    if (owner != null) {
+      msgParams.add(new OAuth.Parameter(OPENSOCIAL_OWNERID, owner));
+    }
+    
+    String viewer = authToken.getViewerId();
+    if (viewer != null) {
+      msgParams.add(new OAuth.Parameter(OPENSOCIAL_VIEWERID, viewer));
+    }
+
+    String app = authToken.getAppId();
+    if (app != null) {
+      msgParams.add(new OAuth.Parameter(OPENSOCIAL_APPID, app));
+    }
+
+  }
+  
+  private void addOAuthParams(List<Parameter> msgParams) {
+    msgParams.add(new OAuth.Parameter(OAuth.OAUTH_TOKEN, ""));
+    
+    String domain = authToken.getDomain();
+    if (domain != null) {
+      msgParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, domain));
+    }
+    
+    if (keyName != null) {
+      msgParams.add(new OAuth.Parameter(XOAUTH_PUBLIC_KEY, keyName));
+    }
+
+    String nonce = Long.toHexString(Crypto.rand.nextLong());
+    msgParams.add(new OAuth.Parameter(OAuth.OAUTH_NONCE, nonce));
+    
+    String timestamp = Long.toString(clock.currentTimeMillis()/1000L);
+    msgParams.add(new OAuth.Parameter(OAuth.OAUTH_TIMESTAMP, timestamp));
+
+    msgParams.add(new OAuth.Parameter(OAuth.OAUTH_SIGNATURE_METHOD,
+        OAuth.RSA_SHA1));
+  }
+
+  /**
+   * Sign a message and append the oauth signature parameter to the message
+   * object.
+   * 
+   * @param message the message to sign
+   * 
+   * @throws Exception because the OAuth libraries require it.
+   */
+  protected void signMessage(OAuthMessage message) throws Exception {
+    OAuthConsumer consumer = new OAuthConsumer(null, null, null, null);
+    consumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKeyObject);
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    message.sign(accessor);
+  }
+  
+  /**
+   * Strip out any owner or viewer id passed by the client.
+   */
+  private List<Parameter> sanitize(List<Parameter> params) {
+    ArrayList<Parameter> list = new ArrayList<Parameter>();
+    for (Parameter p : params) {
+      if (allowParam(p.getKey())) {
+        list.add(p);
+      }
+    }
+    return list;
+  }
+
+  private boolean allowParam(String paramName) {
+    String canonParamName = paramName.toLowerCase();
+    return (!(canonParamName.startsWith("oauth") ||
+        canonParamName.startsWith("xoauth") ||
+        canonParamName.startsWith("opensocial")) &&
+        ALLOWED_PARAM_NAME.matcher(canonParamName).matches());
+  }
+
+
+}

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
 Wed Mar 12 23:02:40 2008
@@ -23,6 +23,8 @@
 import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.GadgetServer;
 import org.apache.shindig.gadgets.GadgetSigner;
+import org.apache.shindig.gadgets.GadgetToken;
+import org.apache.shindig.gadgets.RequestSigner;
 
 import java.util.Set;
 import java.util.logging.Logger;
@@ -101,6 +103,26 @@
    *     parameters, so caution should be taken when adding your own data.
    */
   public abstract String getIframeUrl(Gadget gadget);
+
+  /**
+   * Constructs a RequestSigner object that can be used to sign requests from
+   * the given gadget token to implement signed fetch.
+   * 
+   * @param token the decrypted, verified security token
+   * @return a request signer implementing signed fetch.
+   * 
+   * @see org.apache.shindig.gadgets.SignedFetchRequestSigner
+   */
+  public abstract RequestSigner makeSignedFetchRequestSigner(GadgetToken 
token);
+
+  /**
+   * Constructs a RequestSigner object that can be used to sign requests from
+   * the given gadget token to implement full OAuth.
+   * 
+   * @param token the decrypted, verified security token
+   * @return a request signer implementing signed fetch.
+   */
+  public abstract RequestSigner makeOAuthRequestSigner(GadgetToken token);
 
   /**
    * Initializes this handler using the provided implementation.

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
 Wed Mar 12 23:02:40 2008
@@ -32,9 +32,12 @@
 import org.apache.shindig.gadgets.GadgetServerConfig;
 import org.apache.shindig.gadgets.GadgetSigner;
 import org.apache.shindig.gadgets.GadgetSpecFetcher;
+import org.apache.shindig.gadgets.GadgetToken;
 import org.apache.shindig.gadgets.JsLibrary;
 import org.apache.shindig.gadgets.MessageBundleFetcher;
 import org.apache.shindig.gadgets.RemoteContentFetcher;
+import org.apache.shindig.gadgets.RequestSigner;
+import org.apache.shindig.gadgets.SignedFetchRequestSigner;
 import org.apache.shindig.gadgets.SyndicatorConfig;
 import org.apache.shindig.gadgets.UserPrefs;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
@@ -217,5 +220,34 @@
     } catch (GadgetException e) {
       throw new ServletException(e);
     }
+  }
+
+  @Override
+  public RequestSigner makeOAuthRequestSigner(GadgetToken token) {
+    return null;
+  }
+
+  @Override
+  public RequestSigner makeSignedFetchRequestSigner(GadgetToken token) {
+    // Real implementations should use their own key, probably pulled from
+    // disk rather than hardcoded in the source.
+    final String PRIVATE_KEY_TEXT =
+      "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V" +
+      "A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d" +
+      "7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ" +
+      "hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H" +
+      "X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm" +
+      "uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw" +
+      "rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z" +
+      "zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn" +
+      "qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG" +
+      "WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno" +
+      "cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+" +
+      "3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8" +
+      "AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54" +
+      "Lw03eHTNQghS0A==";
+    final String PRIVATE_KEY_NAME = "shindig-insecure-key";
+    return new SignedFetchRequestSigner(token, PRIVATE_KEY_NAME,
+        PRIVATE_KEY_TEXT);
   }
 }

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
 Wed Mar 12 23:02:40 2008
@@ -24,6 +24,7 @@
 import org.apache.shindig.gadgets.RemoteContent;
 import org.apache.shindig.gadgets.RemoteContentFetcher;
 import org.apache.shindig.gadgets.RemoteContentRequest;
+import org.apache.shindig.gadgets.RequestSigner;
 import org.apache.shindig.util.InputStreamConsumer;
 
 import org.json.JSONException;
@@ -150,7 +151,7 @@
           return new RemoteContent(HttpServletResponse.SC_UNAUTHORIZED,
               "Invalid security token.".getBytes(), null);
         }
-        signedUrl = signUrl(originalUrl, token, request);
+        signedUrl = signUrl(state, originalUrl, token, request);
       } else {
         signedUrl = originalUrl;
       }
@@ -278,10 +279,12 @@
    * @return The signed url.
    */
   @SuppressWarnings("unchecked")
-  private URL signUrl(URL originalUrl, GadgetToken token,
+  private URL signUrl(CrossServletState state, URL originalUrl, GadgetToken 
token,
       HttpServletRequest request) throws GadgetException {
     String method = getParameter(request, "httpMethod", "GET");
-    return token.signUrl(originalUrl, method, request.getParameterMap());
+    String body = getParameter(request, "postBody", null);
+    RequestSigner signer = state.makeSignedFetchRequestSigner(token);
+    return signer.signRequest(method, originalUrl, body);
   }
 
   /**

Modified: 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
 Wed Mar 12 23:02:40 2008
@@ -78,6 +78,16 @@
     public void init(ServletContext config) {
 
     }
+
+    @Override
+    public RequestSigner makeOAuthRequestSigner(GadgetToken token) {
+      return null;
+    }
+
+    @Override
+    public RequestSigner makeSignedFetchRequestSigner(GadgetToken token) {
+      return null;
+    }
   };
 
   public GadgetTestFixture() {

Added: 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java?rev=636638&view=auto
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
 (added)
+++ 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
 Wed Mar 12 23:02:40 2008
@@ -0,0 +1,225 @@
+/*
+ * 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 org.apache.shindig.gadgets;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuth.Parameter;
+import net.oauth.signature.RSA_SHA1;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Tests the signed fetch code.
+ */
+public class SignedFetchRequestSignerTest extends TestCase {
+  private static final String PRIVATE_KEY_TEXT =
+    "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V" +
+    "A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d" +
+    "7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ" +
+    "hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H" +
+    "X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm" +
+    "uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw" +
+    "rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z" +
+    "zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn" +
+    "qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG" +
+    "WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno" +
+    "cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+" +
+    "3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8" +
+    "AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54" +
+    "Lw03eHTNQghS0A==";
+
+  private static final String CERTIFICATE_TEXT =
+    "-----BEGIN CERTIFICATE-----\n" +
+    "MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0\n" +
+    "IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV\n" +
+    "BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\n" +
+    "gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY\n" +
+    "zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb\n" +
+    "mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3\n" +
+    "DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d\n" +
+    "4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb\n" +
+    "WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J\n" +
+    "-----END CERTIFICATE-----";
+
+  private SignedFetchRequestSigner signer;
+  private BasicGadgetToken authToken;
+  private OAuthAccessor accessor;
+  
+  @Override
+  public void setUp() throws Exception {
+    authToken = new BasicGadgetToken("o", "v", "a", "d");
+    signer = new SignedFetchRequestSigner(authToken, "foo", PRIVATE_KEY_TEXT);
+    OAuthConsumer consumer = new OAuthConsumer(null, null, null, null);
+    consumer.setProperty(RSA_SHA1.X509_CERTIFICATE, CERTIFICATE_TEXT);
+    accessor = new OAuthAccessor(consumer);
+  }
+  
+  public void testParametersSet() throws Exception {
+    URL unsigned = new URL("http://test";);
+    URL out = signer.signRequest("GET", unsigned, null);
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertTrue(contains(queryParams, "opensocial_ownerid", "o"));
+    Assert.assertTrue(contains(queryParams, "opensocial_viewerid", "v"));
+    Assert.assertTrue(contains(queryParams, "opensocial_appid", "a"));
+    Assert.assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "d"));
+    Assert.assertTrue(
+        contains(queryParams, "xoauth_signature_publickey", "foo"));
+  }
+  
+  public void testTrickyParametersInQuery() throws Exception {
+    String tricky = "%6fpensocial_ownerid=gotcha";
+    URL unsigned = new URL("http://test?"; + tricky);
+    URL out = signer.signRequest("GET", unsigned, null);
+    Assert.assertFalse(out.getQuery().contains("gotcha"));
+    assertSignatureOK("GET", out.toString(), null);
+  }
+  
+  public void testTrickyParametersInBody() throws Exception {
+    URL unsigned = new URL("http://test";);
+    String tricky = "%6fpensocial_ownerid=gotcha";
+    URL out = signer.signRequest("POST", unsigned, tricky);
+    assertSignatureInvalid("POST", out.toString(), tricky);       
+  }
+  
+  public void testGetNoQuery() throws Exception {
+    URL unsigned = new URL("http://test";);
+    URL out = signer.signRequest("GET", unsigned, null);
+    assertSignatureOK("GET", out.toString(), null);
+  }
+  
+  public void testGetWithQuery() throws Exception {
+    URL unsigned = new URL("http://test?a=b";);
+    URL out = signer.signRequest("GET", unsigned, null);
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertTrue(contains(queryParams, "a", "b"));
+    assertSignatureOK("GET", out.toString(), null);
+  }
+
+
+  public void testGetWithQueryMultiParam() throws Exception {
+    URL unsigned = new URL("http://test?a=b&a=c";);
+    URL out = signer.signRequest("GET", unsigned, null);
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertTrue(contains(queryParams, "a", "b"));
+    Assert.assertTrue(contains(queryParams, "a", "c"));
+    assertSignatureOK("GET", out.toString(), null);
+  }
+  
+  public void testPostNoQueryNoData() throws Exception {
+    URL unsigned = new URL("http://test";);
+    URL out = signer.signRequest("POST", unsigned, null);
+    assertSignatureOK("POST", out.toString(), null);    
+  }
+  
+  public void testPostWithQueryNoData() throws Exception {
+    URL unsigned =  new URL("http://test?name=value";);
+    URL out = signer.signRequest("POST", unsigned, null);
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertTrue(contains(queryParams, "name", "value"));
+    assertSignatureOK("POST", out.toString(), null);    
+  }
+  
+  public void testPostNoQueryWithData() throws Exception {
+    URL unsigned =  new URL("http://test";);
+    URL out = signer.signRequest("POST", unsigned, "name=value");
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertFalse(contains(queryParams, "name", "value"));
+    assertSignatureOK("POST", out.toString(), "name=value");    
+  }
+  
+  public void testPostWithQueryWithData() throws Exception {
+    URL unsigned =  new URL("http://test?queryName=queryValue";);
+    URL out = signer.signRequest("POST", unsigned, "name=value");
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertTrue(contains(queryParams, "queryName", "queryValue"));
+    assertSignatureOK("POST", out.toString(), "name=value");    
+  }
+  
+  public void testStripOpenSocialParamsFromQuery() throws Exception {
+    URL unsigned =  new URL("http://test?opensocial_foo=bar";);
+    URL out = signer.signRequest("POST", unsigned, "name=value");
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertFalse(contains(queryParams, "opensocial_foo", "bar"));
+    assertSignatureOK("POST", out.toString(), "name=value");       
+  }
+  
+  public void testStripOAuthParamsFromQuery() throws Exception {
+    URL unsigned =  new URL("http://test?oauth_foo=bar";);
+    URL out = signer.signRequest("POST", unsigned, "name=value");
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+    Assert.assertFalse(contains(queryParams, "oauth_foo", "bar"));
+    assertSignatureOK("POST", out.toString(), "name=value");       
+  }
+
+  public void testStripOpenSocialParamsFromBody() throws Exception {
+    URL unsigned =  new URL("http://test";);
+    URL out = signer.signRequest("POST", unsigned, "opensocial_foo=bar");
+    assertSignatureInvalid("POST", out.toString(), "opensocial_foo=bar");      
 
+  }
+  
+  public void testStripOAuthParamsFromBody() throws Exception {
+    URL unsigned =  new URL("http://test";);
+    URL out = signer.signRequest("POST", unsigned, "oauth_foo=bar");
+    assertSignatureInvalid("POST", out.toString(), "oauth_foo=bar");       
+  }
+  
+  private void assertSignatureOK(String method, String urlStr, String body)
+  throws Exception {
+    URL url = new URL(urlStr);
+    URL noQuery = new URL(url.getProtocol(), url.getHost(), url.getPort(),
+        url.getPath());
+    List<OAuth.Parameter> queryParams = OAuth.decodeForm(url.getQuery());
+    List<OAuth.Parameter> postParams = OAuth.decodeForm(body);
+    
+    ArrayList<OAuth.Parameter> msgParams = new ArrayList<OAuth.Parameter>();
+    msgParams.addAll(queryParams);
+    msgParams.addAll(postParams);
+    
+    OAuthMessage message = new OAuthMessage(method, noQuery.toString(),
+        msgParams);
+
+    // Throws on failure
+    message.validateSignature(accessor);
+  }
+  
+  private void assertSignatureInvalid(String method, String urlStr, String 
body) {
+    try {
+      assertSignatureOK(method, urlStr, body);
+      fail("Signature verification should have failed");
+    } catch (Exception e) {
+      // good
+    }
+  }
+  
+  // Checks whether the given parameter list contains the specified
+  // key/value pair
+  private boolean contains(List<Parameter> params, String key, String value) {
+    for (Parameter p : params) {
+      if (p.getKey().equals(key) && p.getValue().equals(value)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}


Reply via email to