You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by jo...@apache.org on 2010/03/09 00:50:15 UTC

svn commit: r920573 - in /shindig/trunk/java/gadgets/src: main/java/org/apache/shindig/gadgets/render/ test/java/org/apache/shindig/gadgets/render/

Author: johnh
Date: Mon Mar  8 23:50:15 2010
New Revision: 920573

URL: http://svn.apache.org/viewvc?rev=920573&view=rev
Log:
The existing Sanitizing rewriters, especially SanitizingGadgetRewriter, use both
LinkRewriter, which is being removed in favor of ProxyUriManager, as well as
implementing a custom Visitor pattern.

This CL:
A) removes the use of LinkRewriter in favor of the ProxyUriManager interface
B) removes the custom NodeVisitor implementation in favor of DomWalker


Added:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriter.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriterTest.java

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,394 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.UserDataHandler;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.Inject;
+
+/**
+ * A content rewriter that will sanitize output for simple 'badge' like display.
+ *
+ * This is intentionally not as robust as Caja. It is a simple element whitelist. It can not be used
+ * for sanitizing either javascript or CSS. CSS is desired in the long run, but it can't be proven
+ * safe in the short term.
+ *
+ * Generally used in conjunction with a gadget that gets its dynamic behavior externally (proxied
+ * rendering, OSML, etc.)
+ */
+public class SanitizingGadgetRewriter extends DomWalker.Rewriter {
+
+  /** Key stored as element user-data to bypass sanitization */
+  private static final String BYPASS_SANITIZATION_KEY = "shindig.bypassSanitization";
+
+  /**
+   * Is the Gadget to be rendered sanitized?
+   * @return true if sanitization will be enabled
+   */
+  public static boolean isSanitizedRenderingRequest(Gadget gadget) {
+    return ("1".equals(gadget.getContext().getParameter("sanitize")));
+  }
+  
+  /**
+   * Marks that an element and all its attributes are trusted content.
+   * This status is preserved across {@link Node#cloneNode} calls.  Be
+   * extremely careful when using this, especially with {@code includingChildren}
+   * set to {@code true}, as untrusted content that gets inserted (e.g, via
+   * os:RenderAll in templating) would become trusted.
+   * 
+   * @param element the trusted element
+   * @param includingChildren if true, children of this element will are also
+   *     trusted.  Never set this to true on an element that will ever have
+   *     untrusted children inserted (e.g., if it contains or may contain os:Render).
+   */
+  public static void bypassSanitization(Element element, boolean includingChildren) {
+    element.setUserData(BYPASS_SANITIZATION_KEY,
+        includingChildren ? Bypass.ALL : Bypass.ONLY_SELF, copyOnClone);
+  }
+  
+  private static enum Bypass { ALL, ONLY_SELF, NONE }
+
+    private static UserDataHandler copyOnClone = new UserDataHandler() {
+    public void handle(short operation, String key, Object data, Node src, Node dst) {
+      if (operation == NODE_IMPORTED || operation == NODE_CLONED) {
+        dst.setUserData(key, data, copyOnClone);
+      }
+    }
+  };
+
+  @Inject
+  public SanitizingGadgetRewriter(@AllowedTags Set<String> allowedTags,
+      @AllowedAttributes Set<String> allowedAttributes,
+      ContentRewriterFeature.Factory rewriterFeatureFactory,
+      CajaCssSanitizer cssSanitizer,
+      ProxyUriManager proxyUriManager) {
+    super(new BasicElementFilter(allowedTags, allowedAttributes),
+          new LinkSchemeCheckFilter(),
+          new StyleFilter(proxyUriManager, cssSanitizer),
+          new LinkFilter(proxyUriManager),
+          new ImageFilter(proxyUriManager),
+          new TargetFilter());
+  }
+
+
+  public void rewrite(Gadget gadget, MutableContent content) throws RewritingException {
+    if (gadget.sanitizeOutput()) {
+      boolean sanitized = false;
+      try {
+        super.rewrite(gadget, content);
+        sanitized = true;
+      } finally {
+        // Defensively clean the content in case of failure
+        if (!sanitized) {
+          content.setContent("");
+        }
+      }
+    }
+  }
+
+  /** Convert a NamedNodeMap to a list for easy and safe operations */
+  private static List<Attr> toList(NamedNodeMap nodes) {
+    List<Attr> list = new ArrayList<Attr>(nodes.getLength());
+
+    for (int i = 0, j = nodes.getLength(); i < j; ++i) {
+      list.add((Attr) nodes.item(i));
+    }
+
+    return list;
+  }
+
+  private static Bypass canBypassSanitization(Element element) {
+    Bypass bypass = (Bypass) element.getUserData(BYPASS_SANITIZATION_KEY);
+    if (bypass == null) {
+      bypass = Bypass.NONE;
+    }
+    return bypass;
+  }
+  
+  private static abstract class SanitizingWalker implements DomWalker.Visitor {
+    protected abstract boolean removeTag(Gadget gadget, Element elem, Uri ctx);
+    protected abstract boolean removeAttr(Gadget gadget, Attr attr, Uri ctx);
+    
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      Element elem = null;
+      
+      switch (node.getNodeType()) {
+      case Node.CDATA_SECTION_NODE:
+      case Node.TEXT_NODE:
+      case Node.ENTITY_REFERENCE_NODE:
+        // Never modified.
+        return VisitStatus.BYPASS;
+      case Node.ELEMENT_NODE:
+      case Node.DOCUMENT_NODE:
+        // Continues through to follow-up logic.
+        elem = (Element)node;
+        break;
+      case Node.COMMENT_NODE:
+      default:
+        // Must remove all comments to avoid conditional comment evaluation.
+        // There might be other, unknown types as well. Don't trust them.
+        return VisitStatus.RESERVE_TREE;
+      }
+
+      Bypass bypass = canBypassSanitization(elem);
+      if (bypass == Bypass.ALL) {
+        // This is double-checked in revisit below to ensure no modification/removal occurs.
+        return VisitStatus.RESERVE_TREE;
+      } else if (bypass == Bypass.ONLY_SELF) {
+        return VisitStatus.BYPASS;
+      }
+      
+      if (removeTag(gadget, elem, gadget.getSpec().getUrl())) {
+        // All reserved trees are removed in revisit.
+        return VisitStatus.RESERVE_TREE;
+      }
+      
+      // Otherwise move on to attributes.
+      VisitStatus status = VisitStatus.MODIFY;
+      for (Attr attr : toList(elem.getAttributes())) {
+        if (removeAttr(gadget, attr, gadget.getSpec().getUrl())) {
+          elem.removeAttributeNode(attr);
+        }
+      }
+      
+      return status;
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      // Remove all reserved nodes, since these are all for which removeTag returned true.
+      for (Node node : nodes) {
+        if (node.getNodeType() == Node.COMMENT_NODE ||
+            canBypassSanitization((Element)node) != Bypass.ALL) {
+          node.getParentNode().removeChild(node);
+        }
+      }
+      return true;
+    }
+  }
+
+  /**
+   * Restrict the set of allowed tags and attributes
+   */
+  static class BasicElementFilter extends SanitizingWalker {
+    private final Set<String> allowedTags;
+    private final Set<String> allowedAttributes;
+    
+    private BasicElementFilter(Set<String> allowedTags,
+                               Set<String> allowedAttributes) {
+      this.allowedTags = allowedTags;
+      this.allowedAttributes = allowedAttributes;
+    }
+    
+    public boolean removeTag(Gadget gadget, Element elem, Uri context) {
+      return !allowedTags.contains(elem.getNodeName().toLowerCase());
+    }
+
+    public boolean removeAttr(Gadget gadget, Attr attr, Uri context) {
+      return !allowedAttributes.contains(attr.getName().toLowerCase());
+    }
+  }
+
+  /**
+   * Enfore that all uri's in the document have either http or https as
+   * their scheme
+   */
+  static class LinkSchemeCheckFilter extends SanitizingWalker {
+    private static final Set<String> URI_ATTRIBUTES = ImmutableSet.of("href", "src");
+
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+    
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if (URI_ATTRIBUTES.contains(attr.getName().toLowerCase())) {
+        try {
+          Uri uri = Uri.parse(attr.getValue());
+          String scheme = uri.getScheme();
+          if (scheme != null && !scheme.equals("http") && !scheme.equals("https")) {
+            return true;
+          }
+        } catch (IllegalArgumentException iae) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Enfore that all images in the document are rewritten through the proxy.
+   * Prevents issues in IE where the image content contains script
+   */
+  static class ImageFilter extends SanitizingWalker {
+    private final SanitizingProxyUriManager imageRewriter;
+
+    private ImageFilter(ProxyUriManager proxyUriManager) {
+      this.imageRewriter = new SanitizingProxyUriManager(proxyUriManager, "image/*");
+    }
+    
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+    
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if ("img".equalsIgnoreCase(attr.getOwnerElement().getNodeName()) &&
+          "src".equalsIgnoreCase(attr.getName())) {
+        try {
+          Uri uri = Uri.parse(attr.getValue());
+          attr.setValue(imageRewriter.make(
+              ProxyUriManager.ProxyUri.fromList(gadget, ImmutableList.of(uri)), null)
+                .get(0).toString());
+        } catch (IllegalArgumentException e) {
+          // Invalid Uri, remove.
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Pass the contents of style tags through the CSS sanitizer
+   */
+  static class StyleFilter implements DomWalker.Visitor {
+    private final SanitizingProxyUriManager imageRewriter;
+    private final SanitizingProxyUriManager cssImportRewriter;
+    private final CajaCssSanitizer cssSanitizer;
+
+    private StyleFilter(ProxyUriManager proxyUriManager, CajaCssSanitizer cssSanitizer) {
+      this.imageRewriter = new SanitizingProxyUriManager(proxyUriManager, "image/*");
+      this.cssImportRewriter = new SanitizingProxyUriManager(proxyUriManager, "text/css");
+      this.cssSanitizer = cssSanitizer;
+    }
+  
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      if (node.getNodeType() == Node.ELEMENT_NODE &&
+          "style".equalsIgnoreCase(node.getNodeName())) {
+        cssSanitizer.sanitize(
+            (Element)node, gadget.getSpec().getUrl(), cssImportRewriter, imageRewriter);
+        return VisitStatus.MODIFY;
+      }
+      return VisitStatus.BYPASS;
+    }
+    
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      return false;
+    }
+  }
+
+  /**
+   * Restrict link tags to stylesheet content only and force the link to
+   * be rewritten through the proxy and sanitized
+   */
+  static class LinkFilter extends SanitizingWalker {
+    private final SanitizingProxyUriManager cssImportRewriter;
+
+    private LinkFilter(ProxyUriManager proxyUriManager) {
+      this.cssImportRewriter = new SanitizingProxyUriManager(proxyUriManager, "text/css");
+    }
+    
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      if (!elem.getNodeName().equalsIgnoreCase("link")) {
+        return false;
+      }
+      boolean hasType = false;
+      for (Attr attr : toList(elem.getAttributes())) {
+        if ("rel".equalsIgnoreCase(attr.getName())) {
+          hasType |= "stylesheet".equalsIgnoreCase(attr.getValue());
+        } else if ("type".equalsIgnoreCase(attr.getName())) {
+          hasType |= "text/css".equalsIgnoreCase(attr.getValue());
+        } else if ("href".equalsIgnoreCase(attr.getName())) {
+          try {
+            attr.setValue(cssImportRewriter.make(
+                ProxyUriManager.ProxyUri.fromList(gadget,
+                  ImmutableList.of(Uri.parse(attr.getValue()))), null).get(0).toString());
+          } catch (IllegalArgumentException e) {
+            return true;
+          }
+        }
+      }
+      return !hasType;
+    }
+    
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      return false;
+    }
+  }
+
+  /**
+   * Restrict the value of the target attribute on anchors etc. to
+   * _blank or _self or remove the node
+   */
+  static class TargetFilter extends SanitizingWalker {
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if ("target".equalsIgnoreCase(attr.getName())) {
+        String value = attr.getValue().toLowerCase();
+        if (!("_blank".equals(value) || "_self".equals(value))) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @BindingAnnotation
+  public @interface AllowedTags { }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @BindingAnnotation
+  public @interface AllowedAttributes { }
+}

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.servlet.ProxyBase;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Forcible rewrite the link through the proxy and force sanitization with
+ * an expected mime type.
+ */
+public class SanitizingProxyUriManager implements ProxyUriManager {
+  private final ProxyUriManager wrapped;
+  private final String expectedMime;
+    
+  public SanitizingProxyUriManager(ProxyUriManager wrapped, String expectedMime) {
+    this.wrapped = wrapped;
+    this.expectedMime = expectedMime;
+  }
+
+  public ProxyUri process(Uri uri) throws GadgetException {
+    return wrapped.process(uri);
+  }
+
+  public List<Uri> make(List<ProxyUri> ctx, Integer forcedRefresh) {
+    // Just wraps the original ProxyUriManager and adds a few query params.
+    List<Uri> origUris = wrapped.make(ctx, forcedRefresh);
+    List<Uri> sanitizedUris = Lists.newArrayListWithCapacity(origUris.size());
+      
+    for (Uri origUri : origUris) {
+      UriBuilder newUri = new UriBuilder(origUri);
+      newUri.addQueryParameter(ProxyBase.SANITIZE_CONTENT_PARAM, "1");
+      if (expectedMime != null) {
+        newUri.addQueryParameter(ProxyBase.REWRITE_MIME_TYPE_PARAM, expectedMime);
+      }
+      sanitizedUris.add(newUri.toUri());
+    }
+      
+    return sanitizedUris;
+  }
+}
\ No newline at end of file

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriter.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriter.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriter.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriter.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.sanselan.ImageFormat;
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RequestRewriter;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.inject.Inject;
+
+/**
+ * Rewriter that sanitizes CSS and image content.
+ */
+public class SanitizingRequestRewriter implements RequestRewriter {
+  private static final Logger logger =
+    Logger.getLogger(SanitizingRequestRewriter.class.getName());
+
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final CajaCssSanitizer cssSanitizer;
+  private final ProxyUriManager proxyUriManager;
+  
+  @Inject
+  public SanitizingRequestRewriter(ContentRewriterFeature.Factory featureConfigFactory,
+      CajaCssSanitizer cssSanitizer,
+      ProxyUriManager proxyUriManager) {
+    this.featureConfigFactory = featureConfigFactory;
+    this.cssSanitizer = cssSanitizer;
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  public boolean rewrite(HttpRequest request, HttpResponse resp, MutableContent content) {
+    // Content fetched through the proxy can stipulate that it must be sanitized.
+    if (request.isSanitizationRequested() &&
+        featureConfigFactory.get(request).shouldRewriteURL(request.getUri().toString())) {
+      if (StringUtils.isEmpty(request.getRewriteMimeType())) {
+        logger.log(Level.WARNING, "Request to sanitize without content type for "
+            + request.getUri());
+        content.setContent("");
+        return true;
+      } else if (request.getRewriteMimeType().equalsIgnoreCase("text/css")) {
+        return rewriteProxiedCss(request, resp, content);
+      } else if (request.getRewriteMimeType().toLowerCase().startsWith("image/")) {
+        return rewriteProxiedImage(request, resp, content);
+      } else {
+        logger.log(Level.WARNING, "Request to sanitize unknown content type "
+            + request.getRewriteMimeType()
+            + " for " + request.getUri());
+        content.setContent("");
+        return true;
+      }
+    } else {
+      // No Op
+      return false;
+    }
+  }
+
+  /**
+   * We don't actually rewrite the image we just ensure that it is in fact a valid
+   * and known image type.
+   */
+  private boolean rewriteProxiedImage(HttpRequest request, HttpResponse resp,
+      MutableContent content) {
+    boolean imageIsSafe = false;
+    try {
+      String contentType = resp.getHeader("Content-Type");
+      if (contentType == null || contentType.toLowerCase().startsWith("image/")) {
+        // Unspecified or unknown image mime type.
+        try {
+          ImageFormat imageFormat = Sanselan
+              .guessFormat(new ByteSourceInputStream(resp.getResponse(),
+              request.getUri().getPath()));
+          if (imageFormat == ImageFormat.IMAGE_FORMAT_UNKNOWN) {
+            logger.log(Level.INFO, "Unable to sanitize unknown image type "
+                + request.getUri().toString());
+            return true;
+          }
+          imageIsSafe = true;
+          // Return false to indicate that no rewriting occurred
+          return false;
+        } catch (IOException ioe) {
+          throw new RuntimeException(ioe);
+        } catch (ImageReadException ire) {
+          // Unable to read the image so its not safe
+          logger.log(Level.INFO, "Unable to detect image type for " +request.getUri().toString() +
+              " for sanitized content", ire);
+          return true;
+        }
+      } else {
+        return true;
+      }
+    } finally {
+      if (!imageIsSafe) {
+        content.setContent("");
+      }
+    }
+  }
+
+  /**
+   * Sanitize a CSS file.
+   */
+  private boolean rewriteProxiedCss(HttpRequest request, HttpResponse response,
+      MutableContent content) {
+    String sanitized = "";
+    try {
+      String contentType = response.getHeader("Content-Type");
+      if (contentType == null || contentType.toLowerCase().startsWith("text/")) {
+        SanitizingProxyUriManager cssImageRewriter =
+            new SanitizingProxyUriManager(proxyUriManager, "image/*");
+        SanitizingProxyUriManager cssImportRewriter =
+            new SanitizingProxyUriManager(proxyUriManager, "text/css");
+        sanitized = cssSanitizer.sanitize(content.getContent(), request.getUri(),
+            cssImportRewriter, cssImageRewriter);
+      }
+      
+      return true;
+    } finally {
+      // Set sanitized content in finally to ensure it is always cleared in
+      // the case of errors
+      content.setContent(sanitized);
+    }
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,420 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.servlet.ProxyBase;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SanitizingGadgetRewriterTest extends RewriterTestBase {
+  private static final Set<String> DEFAULT_TAGS = ImmutableSet.of("html", "head", "body");
+  private static final Pattern BODY_REGEX = Pattern.compile(".*<body>(.+)</body>.*");
+
+  private final GadgetContext sanitaryGadgetContext = new GadgetContext() {
+    @Override
+    public String getParameter(String name) {
+      return ProxyBase.SANITIZE_CONTENT_PARAM.equals(name) ? "1" : null;
+    }
+    
+    @Override
+    public String getContainer() {
+      return MOCK_CONTAINER;
+    }
+  };
+
+  private final GadgetContext unsanitaryGadgetContext = new GadgetContext();
+  private final GadgetContext unsanitaryGadgetContextNoCacheAndDebug = new GadgetContext(){
+    @Override
+    public boolean getIgnoreCache() {
+      return true;
+    }
+    @Override
+    public boolean getDebug() {
+      return true;
+    }
+  };
+  private Gadget gadget;
+  private Gadget gadgetNoCacheAndDebug;
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    
+    gadget = new Gadget().setContext(unsanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='x-html-sanitized'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+
+    gadgetNoCacheAndDebug = new Gadget().setContext(unsanitaryGadgetContextNoCacheAndDebug);
+    gadgetNoCacheAndDebug.setSpec(new GadgetSpec(Uri.parse("www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='x-html-sanitized'/></Module>"));
+    gadgetNoCacheAndDebug.setCurrentView(gadgetNoCacheAndDebug.getSpec().getViews().values().iterator().next());
+}
+
+  private String rewrite(Gadget gadget, String content, Set<String> tags, Set<String> attributes)
+      throws Exception {
+    GadgetRewriter rewriter = createRewriter(tags, attributes);
+
+    MutableContent mc = new MutableContent(parser, content);
+    rewriter.rewrite(gadget, mc);
+
+    Matcher matcher = BODY_REGEX.matcher(mc.getContent());
+    if (matcher.matches()) {
+      return matcher.group(1);
+    }
+    return mc.getContent();
+  }
+
+  private static Set<String> set(String... items) {
+    return Sets.newHashSet(items);
+  }
+
+  private GadgetRewriter createRewriter(Set<String> tags, Set<String> attributes) {
+    Set<String> newTags = new HashSet<String>(tags);
+    newTags.addAll(DEFAULT_TAGS);
+    ContentRewriterFeature.Factory rewriterFeatureFactory =
+        new ContentRewriterFeature.Factory(null,
+          new ContentRewriterFeature.DefaultConfig(
+            ".*", "", "HTTP", "embed,img,script,link,style", "false", "false"));
+    return new SanitizingGadgetRewriter(newTags, attributes, rewriterFeatureFactory,
+        new CajaCssSanitizer(new CajaCssParser()), new PassthruManager("host.com", "/proxy"));
+  }
+
+  @Test
+  public void enforceTagWhiteList() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<p>text <b>bold text</b></p><b>Bold text</b>";
+
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b"), set()));
+  }
+
+  @Test
+  public void enforceStyleSanitized() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold; behavior : bad }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<html><head></head><body><p><style>A {\n  font: bold\n}</style>text " +
+        "<b>bold text</b></p><b>Bold text</b></body></html>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b", "style"), set()));
+  }
+
+  @Test
+  public void enforceStyleLinkRewritten() throws Exception {
+    String markup =
+        "<link rel=\"stylesheet\" "
+            + "href=\"http://www.test.com/dir/proxy?"
+            + "url=http%3A%2F%2Fwww.evil.com%2Fx.css&gadget=www.example.org%2Fgadget.xml&"
+            + "fp=45508&rewriteMime=text/css\"/>";
+    String sanitized = 
+        "<html><head><link href=\"http://host.com/proxy?url=http%3A%2F%2Fwww.test.com%2Fdir%2F" +
+        "proxy%3Furl%3Dhttp%253A%252F%252Fwww.evil.com%252Fx.css%26gadget%3Dwww.example.org%252F" +
+        "gadget.xml%26fp%3D45508%26rewriteMime%3Dtext%2Fcss&sanitize=1&rewriteMime=text%2Fcss\" " +
+        "rel=\"stylesheet\"></head><body></body></html>";
+    String rewritten = rewrite(gadget, markup, set("link"), set("rel", "href"));
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceStyleLinkRewrittenNoCacheAndDebug() throws Exception {
+    String markup =
+        "<link rel=\"stylesheet\" "
+            + "href=\"http://www.test.com/dir/proxy?"
+            + "url=http%3A%2F%2Fwww.evil.com%2Fx.css&gadget=www.example.org%2Fgadget.xml&"
+            + "fp=45508&rewriteMime=text/css\"/>";
+    String sanitized = 
+        "<html><head><link href=\"http://host.com/proxy?url=http%3A%2F%2Fwww.test.com%2F"
+            + "dir%2Fproxy%3Furl%3Dhttp%253A%252F%252Fwww.evil.com%252Fx.css%26gadget%3D"
+            + "www.example.org%252Fgadget.xml%26fp%3D45508%26rewriteMime%3Dtext%2Fcss&"
+            + "sanitize=1&rewriteMime=text%2Fcss\" rel=\"stylesheet\">"
+            + "</head><body></body></html>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("link"), set("rel", "href"));
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceNonStyleLinkStripped() throws Exception {
+    String markup =
+        "<link rel=\"script\" "
+            + "href=\"www.exmaple.org/evil.js\"/>";
+    String rewritten = rewrite(gadget, markup, set("link"), set("rel", "href", "type"));
+    assertEquals("<html><head></head><body></body></html>", rewritten);
+  }
+
+  @Test
+  public void enforceNonStyleLinkStrippedNoCacheAndDebug() throws Exception {
+    String markup =
+        "<link rel=\"script\" "
+            + "href=\"www.exmaple.org/evil.js\"/>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("link"), set("rel", "href", "type"));
+    assertEquals("<html><head></head><body></body></html>", rewritten);
+  }
+
+  @Test
+  public void enforceCssImportLinkRewritten() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('www.evil.com/x.js');</style>";
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized = 
+        "<html><head><style>"
+      + "@import url('http://host.com/proxy?url=www.example.org%2Fwww.evil.com%2Fx.js&"
+      + "sanitize=1&rewriteMime=text%2Fcss');"
+      + "</style></head><body></body></html>";
+    String rewritten = rewrite(gadget, markup, set("style"), set());
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceCssImportLinkRewrittenNoCacheAndDebug() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('www.evil.com/x.js');</style>";
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized = 
+        "<html><head><style>"
+      + "@import url('http://host.com/proxy?url=www.example.org%2Fwww.evil.com%2Fx.js&sanitize=1"
+      + "&rewriteMime=text%2Fcss');</style></head><body></body></html>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("style"), set());
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceCssImportBadLinkStripped() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('javascript:doevil()'); A { font : bold }</style>";
+    String sanitized = "<html><head><style>A {\n"
+        + "  font: bold\n"
+        + "}</style></head><body></body></html>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("style"), set()));
+  }
+
+  @Test
+  public void enforceAttributeWhiteList() throws Exception {
+    String markup = "<p foo=\"bar\" bar=\"baz\">Paragraph</p>";
+    String sanitized = "<p bar=\"baz\">Paragraph</p>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("p"), set("bar")));
+  }
+
+  @Test
+  public void enforceImageSrcProxied() throws Exception {
+    String markup = "<img src='http://www.evil.com/x.js'>Evil happens</img>";
+    String sanitized = "<img src=\"http://host.com/proxy?url=http%3A%2F%2F" +
+        "www.evil.com%2Fx.js&sanitize=1&rewriteMime=image%2F*\">Evil happens";
+    assertEquals(sanitized, rewrite(gadget, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceImageSrcProxiedNoCacheAndDebug() throws Exception {
+    String markup = "<img src='http://www.evil.com/x.js'>Evil happens</img>";
+    String sanitized = "<img src=\"http://host.com/proxy?url=http%3A%2F%2Fwww.evil.com" +
+        "%2Fx.js&sanitize=1&rewriteMime=image%2F*\">Evil happens";
+    assertEquals(sanitized, rewrite(gadgetNoCacheAndDebug, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceBadImageUrlStripped() throws Exception {
+    String markup = "<img src='java\\ script:evil()'>Evil happens</img>";
+    String sanitized = "<img>Evil happens";
+    assertEquals(sanitized, rewrite(gadget, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceTargetTopRestricted() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_top\">x</a>";
+    String sanitized = "<a href=\"http://www.example.com\">x</a>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void enforceTargetSelfAllowed() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_self\">x</a>";
+    assertEquals(markup, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void enforceTargetBlankAllowed() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_BlAnK\">x</a>";
+    assertEquals(markup, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void sanitizationBypassAllowed() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+    // Force the content to get re-serialized
+    MutableContent.notifyEdit(document);
+    String fullMarkup = mc.getContent();
+    
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, true);
+    rewriter.rewrite(gadget, mc);
+     
+    // The document should be unchanged
+    assertEquals(fullMarkup, mc.getContent());
+  }
+     
+  @Test
+  public void sanitizationBypassOnlySelf() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+    
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, false);
+    rewriter.rewrite(gadget, mc);
+     
+    // The document should be unchanged
+    String content = mc.getContent();
+    Matcher matcher = BODY_REGEX.matcher(content);
+    matcher.matches();
+    assertEquals("<p foo=\"bar\"></p>", matcher.group(1));
+  }
+     
+  @Test
+  public void sanitizationBypassPreservedAcrossClone() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+    
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, false);
+
+    // Now, clone the paragraph tag and replace the paragraph tag
+    Element cloned = (Element) paragraphTag.cloneNode(true);
+    paragraphTag.getParentNode().replaceChild(cloned, paragraphTag);
+
+    rewriter.rewrite(gadget, mc);
+     
+    // The document should be unchanged
+    String content = mc.getContent();
+    Matcher matcher = BODY_REGEX.matcher(content);
+    matcher.matches();
+    assertEquals("<p foo=\"bar\"></p>", matcher.group(1));
+  }
+     
+ @Test
+  public void restrictHrefAndSrcAttributes() throws Exception {
+    String markup =
+        "<element " +
+        "href=\"http://example.org/valid-href\" " +
+        "src=\"http://example.org/valid-src\"/> " +
+        "<element " +
+        "href=\"https://example.org/valid-href\" " +
+        "src=\"https://example.org/valid-src\"/> " +
+        "<element " +
+        "href=\"http-evil://example.org/valid-href\" " +
+        "src=\"http-evil://example.org/valid-src\"/> " +
+        "<element " +
+        "href=\"javascript:evil()\" " +
+        "src=\"javascript:evil()\" /> " +
+        "<element " +
+        "href=\"//example.org/valid-href\" " +
+        "src=\"//example.org/valid-src\"/>";
+
+    // TODO: This test is only valid when using a parser that converts empty tags to
+    // balanced tags. The default (Neko) parser does this, with special case logic for handling
+    // empty tags like br or link.
+    String sanitized =
+      "<element " +
+      "href=\"http://example.org/valid-href\" " +
+      "src=\"http://example.org/valid-src\"></element> " +
+      "<element " +
+      "href=\"https://example.org/valid-href\" " +
+      "src=\"https://example.org/valid-src\"></element> " +
+      "<element></element> " +
+      "<element></element> " +
+      "<element " +
+      "href=\"//example.org/valid-href\" " +
+      "src=\"//example.org/valid-src\"></element>";
+
+    assertEquals(sanitized, rewrite(gadget, markup, set("element"), set("href", "src")));
+  }
+
+  @Test
+  public void allCommentsStripped() throws Exception {
+    String markup = "<b>Hello, world</b><!--<b>evil</b>-->";
+    assertEquals("<b>Hello, world</b>", rewrite(gadget, markup, set("b"), set()));
+  }
+
+  @Test
+  public void doesNothingWhenNotSanitized() throws Exception {
+    String markup = "<script src=\"http://evil.org/evil\"></script> <b>hello</b>";
+    Gadget gadget = new Gadget().setContext(unsanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='html'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+    assertEquals(markup, rewrite(gadget, markup, set("b"), set()));
+  }
+
+  @Test
+  public void forceSanitizeUnsanitaryGadget() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold; behavior : bad }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<html><head></head><body><p><style>A {\n  font: bold\n}</style>text " +
+        "<b>bold text</b></p><b>Bold text</b></body></html>";
+
+    Gadget gadget = new Gadget().setContext(sanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='html'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b", "style"), set()));
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import static org.easymock.classextension.EasyMock.createMock;
+import static org.easymock.classextension.EasyMock.capture;
+import static org.easymock.classextension.EasyMock.expect;
+import static org.easymock.classextension.EasyMock.replay;
+import static org.easymock.classextension.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.servlet.ProxyBase;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.easymock.Capture;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class SanitizingProxyUriManagerTest {
+  private ProxyUriManager uriManager;
+  private Uri uri;
+  private ProxyUri proxyUri;
+  
+  @Before
+  public void setUp() throws Exception {
+    uriManager = createMock(ProxyUriManager.class);
+    uri = new UriBuilder().setScheme("http").setAuthority("host.com").setPath("/path").toUri();
+    proxyUri = createMock(ProxyUri.class);
+  }
+  
+  @Test
+  public void processPassesThrough() throws Exception {
+    Capture<Uri> uriCapture = new Capture<Uri>();
+    expect(uriManager.process(capture(uriCapture))).andReturn(proxyUri).once();
+    replay(uriManager);
+    
+    SanitizingProxyUriManager rewriter = makeRewriter(null);
+    ProxyUri returned = rewriter.process(uri);
+    
+    verify(uriManager);
+    assertSame(uri, uriCapture.getValue());
+    assertSame(returned, proxyUri);
+  }
+  
+  @Test
+  public void makeSingleNoMime() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    List<ProxyUri> input = Lists.newArrayList(proxyUri);
+    List<Uri> output = Lists.newArrayList(uri);
+    Integer refresh = new Integer(0);
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    
+    SanitizingProxyUriManager rewriter = makeRewriter(null);
+    List<Uri> returned = rewriter.make(input, refresh);
+    
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(1, returned.size());
+    assertEquals("1", returned.get(0).getQueryParameter(ProxyBase.SANITIZE_CONTENT_PARAM));
+    assertNull(returned.get(0).getQueryParameter(ProxyBase.REWRITE_MIME_TYPE_PARAM));
+  }
+  
+  @Test
+  public void makeSingleExpectedMime() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    List<ProxyUri> input = Lists.newArrayList(proxyUri);
+    List<Uri> output = Lists.newArrayList(uri);
+    Integer refresh = new Integer(0);
+    String mime = "my/mime";
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    
+    SanitizingProxyUriManager rewriter = makeRewriter(mime);
+    List<Uri> returned = rewriter.make(input, refresh);
+    
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(1, returned.size());
+    assertEquals("1", returned.get(0).getQueryParameter(ProxyBase.SANITIZE_CONTENT_PARAM));
+    assertEquals(mime, returned.get(0).getQueryParameter(ProxyBase.REWRITE_MIME_TYPE_PARAM));
+  }
+  
+  @Test
+  public void makeList() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    List<ProxyUri> input = Lists.newArrayList(proxyUri);
+    Uri uri2 = new UriBuilder().toUri();
+    List<Uri> output = Lists.newArrayList(uri, uri2);
+    Integer refresh = new Integer(0);
+    String mime = "my/mime";
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    
+    SanitizingProxyUriManager rewriter = makeRewriter(mime);
+    List<Uri> returned = rewriter.make(input, refresh);
+    
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(2, returned.size());
+    assertEquals("1", returned.get(0).getQueryParameter(ProxyBase.SANITIZE_CONTENT_PARAM));
+    assertEquals(mime, returned.get(0).getQueryParameter(ProxyBase.REWRITE_MIME_TYPE_PARAM));
+    assertEquals("1", returned.get(1).getQueryParameter(ProxyBase.SANITIZE_CONTENT_PARAM));
+    assertEquals(mime, returned.get(1).getQueryParameter(ProxyBase.REWRITE_MIME_TYPE_PARAM));
+  }
+  
+  private SanitizingProxyUriManager makeRewriter(String mime) {
+    return new SanitizingProxyUriManager(uriManager, mime);
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriterTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriterTest.java?rev=920573&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriterTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingRequestRewriterTest.java Mon Mar  8 23:50:15 2010
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RequestRewriter;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class SanitizingRequestRewriterTest extends RewriterTestBase {
+  private static final Uri CONTENT_URI = Uri.parse("www.example.org/content");
+
+  private String rewrite(HttpRequest request, HttpResponse response) throws Exception {
+    request.setSanitizationRequested(true);
+    RequestRewriter rewriter = createRewriter(Collections.<String>emptySet(),
+        Collections.<String>emptySet());
+
+    MutableContent mc = new MutableContent(parser, response);
+    if (!rewriter.rewrite(request, response, mc)) {
+      return null;
+    }
+    return mc.getContent();
+  }
+
+  private RequestRewriter createRewriter(Set<String> tags, Set<String> attributes) {
+    ContentRewriterFeature.Factory rewriterFeatureFactory =
+        new ContentRewriterFeature.Factory(null,
+          new ContentRewriterFeature.DefaultConfig(
+            ".*", "", "HTTP", "embed,img,script,link,style", "false", "false"));
+    return new SanitizingRequestRewriter(rewriterFeatureFactory,
+        new CajaCssSanitizer(new CajaCssParser()), new PassthruManager());
+  }
+
+  @Test
+  public void enforceInvalidProxedCssRejected() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+  }
+
+  @Test
+  public void enforceValidProxedCssAccepted() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    HttpResponse response = new HttpResponseBuilder().setResponseString(
+        "@import url('http://www.evil.com/more.css'); A { font : BOLD }").create();
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized = 
+      // Resultant URL is just the "sanitized" version of same, since we're using
+      // PassthruUriManager for testing purposes.
+      "@import url('http://www.evil.com/more.css?sanitize=1&rewriteMime=text%2Fcss');\n"
+        + "A {\n"
+        + "  font: BOLD\n"
+        + '}';
+    String rewritten = rewrite(req, response);
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceValidProxedCssAcceptedNoCache() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    req.setIgnoreCache(true);
+    HttpResponse response = new HttpResponseBuilder().setResponseString(
+        "@import url('http://www.evil.com/more.css'); A { font : BOLD }").create();
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized = 
+      "@import url('http://www.evil.com/more.css?sanitize=1&rewriteMime=text%2Fcss');\n"
+        + "A {\n"
+        + "  font: BOLD\n"
+        + '}';
+    String rewritten = rewrite(req, response);
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceInvalidProxedImageRejected() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("image/*");
+    HttpResponse response = new HttpResponseBuilder().setResponse("NOTIMAGE".getBytes()).create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+  }
+
+  @Test
+  public void validProxiedImageAccepted() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("image/*");
+    HttpResponse response = new HttpResponseBuilder().setResponse(
+        IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(
+            "org/apache/shindig/gadgets/rewrite/image/inefficient.png"))).create();
+    assertNull(rewrite(req, response));
+  }
+
+  @Test
+  public void enforceUnknownMimeTypeRejected() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/foo");
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+  }
+
+  @Test
+  public void enforceMissingMimeTypeRejected() throws Exception {
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    // A request without a mime type, but requesting sanitization, should be rejected
+    req.setRewriteMimeType(null);
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+  }
+}