Author: doll Date: Wed Apr 30 05:39:15 2008 New Revision: 652372 URL: http://svn.apache.org/viewvc?rev=652372&view=rev Log: SHINDIG-218 Patch from Brian Eaton:
If two gadgets can render on the same domain, they can see each other's data. This implements locked-domain for Shindig, using the same syntax that iGoogle uses: http://code.google.com/apis/gadgets/docs/reference.html#lockeddomain The configuration in this patch disables locked-domain, since most people don't have wildcard DNS on their development machines. Edit java/gadgets/conf/gadgets.properties to enable the locked domain feature. Once you've changed that configuration, you can either enable locked domain for all gadgets on a particular container, or allow gadget authors to opt-in to the feature with <require feature="locked-domain"/> in their gadgets. To enable locked-domain for a particular container, edit the container.js file to set gadgets.lockedDomainRequired = true. Added: incubator/shindig/trunk/features/locked-domain/ incubator/shindig/trunk/features/locked-domain/feature.xml incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/Base32.java incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/StringEncoding.java incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/util/StringEncodingTest.java Modified: incubator/shindig/trunk/config/container.js incubator/shindig/trunk/features/features.txt incubator/shindig/trunk/java/gadgets/conf/gadgets.properties incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingTask.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/http/GadgetRenderingTaskTest.java incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpTestFixture.java incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/ProxyHandlerTest.java Modified: incubator/shindig/trunk/config/container.js URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/config/container.js?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/config/container.js (original) +++ incubator/shindig/trunk/config/container.js Wed Apr 30 05:39:15 2008 @@ -43,6 +43,12 @@ // value matching this set will return a 404 error. "gadgets.parent" : null, +// Should all gadgets be forced on to a locked domain? +"gadgets.lockedDomainRequired" : false, + +// DNS domain on which gadgets should render. +"gadgets.lockedDomainSuffix" : "-a.example.com:8080", + // This config data will be passed down to javascript. Please // configure your object using the feature name rather than // the javascript name. Modified: incubator/shindig/trunk/features/features.txt URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/features.txt?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/features/features.txt (original) +++ incubator/shindig/trunk/features/features.txt Wed Apr 30 05:39:15 2008 @@ -4,6 +4,7 @@ features/core/feature.xml features/dynamic-height/feature.xml features/flash/feature.xml +features/locked-domain/feature.xml features/minimessage/feature.xml features/opensocial-0.6/feature.xml features/opensocial-0.7/feature.xml Added: incubator/shindig/trunk/features/locked-domain/feature.xml URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/locked-domain/feature.xml?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/features/locked-domain/feature.xml (added) +++ incubator/shindig/trunk/features/locked-domain/feature.xml Wed Apr 30 05:39:15 2008 @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<!-- +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. +--> +<feature> +<!-- +Required configuration: +--> + + <name>locked-domain</name> +</feature> Modified: incubator/shindig/trunk/java/gadgets/conf/gadgets.properties URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/conf/gadgets.properties?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/conf/gadgets.properties (original) +++ incubator/shindig/trunk/java/gadgets/conf/gadgets.properties Wed Apr 30 05:39:15 2008 @@ -5,5 +5,5 @@ urls.js.prefix=/gadgets/js/ signing.key-name= signing.key-file= - - +locked-domain.enabled=false +locked-domain.embed-host=127.0.0.1:8080 Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java (original) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java Wed Apr 30 05:39:15 2008 @@ -64,6 +64,7 @@ bind(GadgetBlacklist.class).to(BasicGadgetBlacklist.class); bind(Executor.class).toInstance(Executors.newCachedThreadPool()); + bind(LockedDomainService.class).to(HashLockedDomainService.class); bind(ContainerConfig.class).in(Scopes.SINGLETON); bind(GadgetFeatureRegistry.class).in(Scopes.SINGLETON); Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java (added) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java Wed Apr 30 05:39:15 2008 @@ -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; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.shindig.gadgets.spec.Feature; +import org.apache.shindig.util.Base32; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +/** + * Locked domain implementation based on sha1. + * + * The generated domain takes the form: + * + * base32(sha1(gadget url)). + * + * Other domain locking schemes are possible as well. + */ +public class HashLockedDomainService implements LockedDomainService { + + private final ContainerConfig config; + + private final String embedHost; + + private final boolean enabled; + + private final Set<String> suffixes; + + private GadgetReader gadgetReader = new GadgetReader(); + + public static final String LOCKED_DOMAIN_REQUIRED_KEY = + "gadgets.lockedDomainRequired"; + + public static final String LOCKED_DOMAIN_SUFFIX_KEY = + "gadgets.lockedDomainSuffix"; + + /** + * Create a LockedDomainService + * @param config per-container configuration + * @param embedHost host name to use for embedded content + * @param enabled whether this service should do anything at all. + */ + @Inject + public HashLockedDomainService( + ContainerConfig config, + @Named("locked-domain.embed-host")String embedHost, + @Named("locked-domain.enabled")boolean enabled) { + this.config = config; + this.embedHost = embedHost; + this.enabled = enabled; + suffixes = new HashSet<String>(); + Set<String> containers = config.getContainers(); + if (enabled) { + for (String container : containers) { + String suffix = config.get(container, LOCKED_DOMAIN_SUFFIX_KEY); + suffixes.add(suffix); + } + } + } + + public String getEmbedHost() { + return embedHost; + } + + public boolean embedCanRender(String host) { + return (!enabled || host.equals(embedHost)); + } + + public boolean gadgetCanRender(String host, Gadget gadget, String container) { + if (!enabled) { + return true; + } + // Gadgets can opt-in to locked domains, or they can be enabled globally + // for a particular container + if (gadgetReader.gadgetWantsLockedDomain(gadget) || + containerWantsLockedDomain(container)) { + String neededHost = getLockedDomainForGadget( + gadgetReader.getGadgetUrl(gadget), container); + return (neededHost.equals(host)); + } + // Make sure gadgets that don't ask for locked domain aren't allowed + // to render on one. + return !gadgetUsingLockedDomain(host, gadget); + } + + // Simple class for dependency injection, so we don't need a full-fledged + // Gadget mock for these test cases + static class GadgetReader { + protected boolean gadgetWantsLockedDomain(Gadget gadget) { + Map<String, Feature> prefs = + gadget.getSpec().getModulePrefs().getFeatures(); + return prefs.containsKey("locked-domain"); + } + + protected String getGadgetUrl(Gadget gadget) { + return gadget.getContext().getUrl().toString(); + } + } + + // For testing only + void setSpecReader(GadgetReader gadgetReader) { + this.gadgetReader = gadgetReader; + } + + private boolean containerWantsLockedDomain(String container) { + String required = config.get( + container, LOCKED_DOMAIN_REQUIRED_KEY); + return ("true".equals(required)); + } + + private boolean gadgetUsingLockedDomain(String host, Gadget gadget) { + for (String suffix : suffixes) { + if (host.endsWith(suffix)) { + return true; + } + } + return false; + } + + public String getLockedDomainForGadget(String gadget, String container) { + String suffix = config.get(container, LOCKED_DOMAIN_SUFFIX_KEY); + if (suffix == null) { + throw new IllegalStateException( + "Cannot redirect to locked domain if it is not configured"); + } + byte[] sha1 = DigestUtils.sha(gadget); + String hash = new String(Base32.encodeBase32(sha1)); + return hash + suffix; + } +} Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java (added) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java Wed Apr 30 05:39:15 2008 @@ -0,0 +1,66 @@ +/* + * 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; + +/** + * Interface for locked domain, a security mechanism that ensures that + * a gadget is always registered on a fixed, unique domain. This prevents + * attacks from other gadgets that are rendered on the same domain, since all + * modern web browsers implement a same origin policy that prevents pages served + * from different hosts from accessing each other's data. + */ +public interface LockedDomainService { + + /** + * Check whether embedded content (img src, for example) can render on + * a particular host. + * + * @param host host name for rendered content + * @return true if the content should be allowed to render + */ + public boolean embedCanRender(String host); + + /** + * Figure out where embedded content should render. + * + * @return host name for safe rendering of embedded content. + */ + public String getEmbedHost(); + + /** + * Calculate the locked domain for a particular gadget on a particular + * container. + * + * @param gadget URL of the gadget + * @param container name of the container page + * @return the host name on which the gadget should render + */ + public String getLockedDomainForGadget(String gadget, String container); + + /** + * Check whether a gadget should be allowed to render on a particular + * host. + * + * @param host host name for the content + * @param gadget URL of the gadget + * @param container container + * @return + */ + public boolean gadgetCanRender(String host, Gadget gadget, String container); +} Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingTask.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingTask.java?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingTask.java (original) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingTask.java Wed Apr 30 05:39:15 2008 @@ -28,6 +28,7 @@ import org.apache.shindig.gadgets.GadgetServer; import org.apache.shindig.gadgets.GadgetTokenDecoder; import org.apache.shindig.gadgets.JsLibrary; +import org.apache.shindig.gadgets.LockedDomainService; import org.apache.shindig.gadgets.RemoteContent; import org.apache.shindig.gadgets.spec.Feature; import org.apache.shindig.gadgets.spec.LocaleSpec; @@ -78,6 +79,8 @@ private final GadgetTokenDecoder tokenDecoder; private GadgetContext context; private final List<GadgetContentFilter> filters; + private final LockedDomainService domainLocker; + private String container = null; /** * Processes a single rendering request and produces output html or errors. @@ -151,6 +154,39 @@ } } + /** + * Redirect a type=html gadget to a locked domain if necessary. + * + * @param gadget + * @return true if the request was handled, false if the request can proceed + * @throws IOException + * @throws GadgetException + */ + private boolean mustRedirectToLockedDomain(Gadget gadget) + throws IOException, GadgetException { + + String host = request.getHeader("Host"); + String container = context.getContainer(); + if (domainLocker.gadgetCanRender(host, gadget, container)) { + return false; + } + + // Gadget tried to render on wrong domain. + String gadgetUrl = context.getUrl().toString(); + String required = domainLocker.getLockedDomainForGadget( + gadgetUrl, container); + String redir = + request.getScheme() + "://" + + required + + request.getServletPath() + "?" + + request.getQueryString(); + logger.info("Redirecting gadget " + context.getUrl() + " from domain " + + host + " to domain " + redir); + response.sendRedirect(redir); + + return true; + } + /** * Handles type=html gadget output. * @@ -161,6 +197,10 @@ */ private void outputHtmlGadget(Gadget gadget, View view) throws IOException, GadgetException { + if (mustRedirectToLockedDomain(gadget)) { + return; + } + response.setContentType("text/html; charset=UTF-8"); StringBuilder markup = new StringBuilder(); @@ -423,14 +463,12 @@ .append(";\n"); } - /** - * Validates that the parent parameter was acceptable. - * - * @return True if the parent parameter is valid for the current - * container. - */ - private boolean validateParent() { - String container = request.getParameter("container"); + /** Gets the container for the current request. */ + private String getContainerForRequest() { + if (container != null) { + return container; + } + container = request.getParameter("container"); if (container == null) { // The parameter used to be called 'synd' FIXME: schedule removal container = request.getParameter("synd"); @@ -438,6 +476,17 @@ container = ContainerConfig.DEFAULT_CONTAINER; } } + return container; + } + + /** + * Validates that the parent parameter was acceptable. + * + * @return True if the parent parameter is valid for the current + * container. + */ + private boolean validateParent() { + String container = getContainerForRequest(); String parent = request.getParameter("parent"); @@ -474,13 +523,15 @@ GadgetFeatureRegistry registry, ContainerConfig containerConfig, UrlGenerator urlGenerator, - GadgetTokenDecoder tokenDecoder) { + GadgetTokenDecoder tokenDecoder, + LockedDomainService lockedDomainService) { this.server = server; this.registry = registry; this.containerConfig = containerConfig; this.urlGenerator = urlGenerator; this.tokenDecoder = tokenDecoder; + this.domainLocker = lockedDomainService; filters = new LinkedList<GadgetContentFilter>(); } } 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=652372&r1=652371&r2=652372&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 Apr 30 05:39:15 2008 @@ -18,22 +18,6 @@ */ package org.apache.shindig.gadgets.http; -import org.apache.shindig.gadgets.ContentFetcher; -import org.apache.shindig.gadgets.ContentFetcherFactory; -import org.apache.shindig.gadgets.GadgetException; -import org.apache.shindig.gadgets.GadgetToken; -import org.apache.shindig.gadgets.GadgetTokenDecoder; -import org.apache.shindig.gadgets.RemoteContent; -import org.apache.shindig.gadgets.RemoteContentRequest; -import org.apache.shindig.gadgets.oauth.OAuthRequestParams; -import org.apache.shindig.gadgets.spec.Auth; -import org.apache.shindig.gadgets.spec.Preload; -import org.apache.shindig.util.InputStreamConsumer; -import org.json.JSONException; -import org.json.JSONObject; - -import com.google.inject.Inject; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; @@ -47,10 +31,28 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.shindig.gadgets.ContentFetcher; +import org.apache.shindig.gadgets.ContentFetcherFactory; +import org.apache.shindig.gadgets.GadgetException; +import org.apache.shindig.gadgets.GadgetToken; +import org.apache.shindig.gadgets.GadgetTokenDecoder; +import org.apache.shindig.gadgets.LockedDomainService; +import org.apache.shindig.gadgets.RemoteContent; +import org.apache.shindig.gadgets.RemoteContentRequest; +import org.apache.shindig.gadgets.oauth.OAuthRequestParams; +import org.apache.shindig.gadgets.spec.Auth; +import org.apache.shindig.gadgets.spec.Preload; +import org.apache.shindig.util.InputStreamConsumer; +import org.json.JSONException; +import org.json.JSONObject; + +import com.google.inject.Inject; + public class ProxyHandler { public static final String UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >"; public static final String POST_DATA_PARAM = "postData"; @@ -60,6 +62,9 @@ public static final String NOCACHE_PARAM = "nocache"; public static final String URL_PARAM = "url"; private static final String REFRESH_PARAM = "refresh"; + + private static final Logger logger = + Logger.getLogger(ProxyHandler.class.getPackage().getName()); private final GadgetTokenDecoder gadgetTokenDecoder; @@ -79,6 +84,7 @@ // This isn't a final field because we want to support optional injection. // This is a limitation of Guice, but this workaround...works. private ContentFetcherFactory contentFetcherFactory; + private final LockedDomainService domainLocker; @Inject public void setContentFetcher(ContentFetcherFactory contentFetcherFactory) { @@ -87,9 +93,11 @@ @Inject public ProxyHandler(ContentFetcherFactory contentFetcherFactory, - GadgetTokenDecoder gadgetTokenDecoder) { + GadgetTokenDecoder gadgetTokenDecoder, + LockedDomainService lockedDomainService) { this.contentFetcherFactory = contentFetcherFactory; this.gadgetTokenDecoder = gadgetTokenDecoder; + this.domainLocker = lockedDomainService; } /** @@ -260,6 +268,17 @@ HttpServletResponse response) throws IOException, GadgetException { + String host = request.getHeader("Host"); + if (!domainLocker.embedCanRender(host)) { + // Force embedded images and the like to their own domain to avoid XSS + // in gadget domains. + String msg = "Embed request for url " + + getParameter(request, URL_PARAM, "") + + " made to wrong domain " + host; + logger.info(msg); + throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, msg); + } + if (request.getHeader("If-Modified-Since") != null) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/Base32.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/Base32.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/Base32.java (added) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/Base32.java Wed Apr 30 05:39:15 2008 @@ -0,0 +1,65 @@ +/* + * 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.util; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; + +/** + * Implements Base32 encoding. + */ +public class Base32 implements BinaryDecoder, BinaryEncoder { + + private static final StringEncoding ENCODER = + new StringEncoding("0123456789abcdefghijklmnopqrstuv".toCharArray()); + + public static byte[] encodeBase32(byte[] arg0) { + return ENCODER.encode(arg0).getBytes(); + } + + public static byte[] decodeBase32(byte[] arg0) { + return ENCODER.decode(new String(arg0)); + } + + @SuppressWarnings("unused") + public byte[] decode(byte[] arg0) throws DecoderException { + return decodeBase32(arg0); + } + + @SuppressWarnings("unused") + public byte[] encode(byte[] arg0) throws EncoderException { + return encodeBase32(arg0); + } + + public Object decode(Object object) throws DecoderException { + if (!(object instanceof byte[])) { + throw new DecoderException( + "Parameter supplied to Base32 decode is not a byte[]"); + } + return decodeBase32((byte[]) object); + } + + public Object encode(Object object) throws EncoderException { + if (!(object instanceof byte[])) { + throw new EncoderException( + "Parameter supplied to Base64 encode is not a byte[]"); + } + return encodeBase32((byte[]) object); + } +} Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/StringEncoding.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/StringEncoding.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/StringEncoding.java (added) +++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/StringEncoding.java Wed Apr 30 05:39:15 2008 @@ -0,0 +1,103 @@ +/* + * 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.util; + +import java.util.Arrays; +import java.util.TreeSet; + +/** + * Utility class for encoding strings to and from byte arrays. + */ +public class StringEncoding { + private final char[] DIGITS; + private final int SHIFT; + private final int MASK; + + /** Creates a new encoding based on the supplied set of digits. */ + public StringEncoding(final char[] userDigits) { + TreeSet<Character> t = new TreeSet<Character>(); + for (char c : userDigits) { + t.add(c); + } + char[] digits = new char[t.size()]; + int i = 0; + for (char c : t) { + digits[i++] = c; + } + this.DIGITS = digits; + this.MASK = digits.length - 1; + this.SHIFT = Integer.numberOfTrailingZeros(MASK+1); + if ((MASK+1) != (1<<SHIFT) || digits.length >= 256) { + throw new AssertionError(Arrays.toString(digits)); + } + } + + /** Returns the given bytes in their encoded form. */ + public String encode(byte[] data) { + if (data.length == 0) { + return ""; + } + StringBuilder result = + new StringBuilder(1 + data.length * 8 / DIGITS.length); + int buffer = data[0]; + int next = 1; + int bitsLeft = 8; + while (bitsLeft > 0 || next < data.length) { + if (bitsLeft < SHIFT) { + if (next < data.length) { + buffer <<= 8; + buffer |= (data[next++] & 0xff); + bitsLeft += 8; + } else { + int pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + int index = MASK & (buffer >> (bitsLeft - SHIFT)); + bitsLeft -= SHIFT; + result.append(DIGITS[index]); + } + return result.toString(); + } + + /** Decodes the given encoded string and returns the original raw bytes. */ + public byte[] decode(String encoded) { + if (encoded.length() == 0) { + return new byte[0]; + } + int encodedLength = encoded.length(); + int outLength = encodedLength * SHIFT / 8; + byte[] result = new byte[outLength]; + int buffer = 0; + int next = 0; + int bitsLeft = 0; + for (char c : encoded.toCharArray()) { + buffer <<= SHIFT; + buffer |= Arrays.binarySearch(DIGITS, c) & MASK; + bitsLeft += SHIFT; + if (bitsLeft >= 8) { + result[next++] = (byte) (buffer >> (bitsLeft - 8)); + bitsLeft -= 8; + } + } + assert next == outLength && bitsLeft < SHIFT; + return result; + } +} Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java (added) +++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java Wed Apr 30 05:39:15 2008 @@ -0,0 +1,190 @@ +/* + * 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; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.apache.shindig.gadgets.HashLockedDomainService.GadgetReader; + +public class HashLockedDomainServiceTest extends EasyMockTestCase { + + HashLockedDomainService domainLocker; + Gadget gadget; + FakeSpecReader wantsLocked = new FakeSpecReader( + true, "http://somehost.com/somegadget.xml"); + FakeSpecReader noLocked = new FakeSpecReader( + false, "http://somehost.com/somegadget.xml"); + ContainerConfig containerEnabledConfig; + ContainerConfig containerRequiredConfig; + + /** + * Mocked out spec reader, rather than mocking the whole + * Gadget object. + */ + public static class FakeSpecReader extends GadgetReader { + private boolean wantsLockedDomain; + private String gadgetUrl; + + public FakeSpecReader(boolean wantsLockedDomain, String gadgetUrl) { + this.wantsLockedDomain = wantsLockedDomain; + this.gadgetUrl = gadgetUrl; + } + + @Override + protected boolean gadgetWantsLockedDomain(Gadget gadget) { + return wantsLockedDomain; + } + + @Override + protected String getGadgetUrl(Gadget gadget) { + return gadgetUrl; + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + JSONObject json = new JSONObject(); + json.put("gadgets.container", + new JSONArray().put(ContainerConfig.DEFAULT_CONTAINER)); + json.put("gadgets.lockedDomainRequired", true); + json.put("gadgets.lockedDomainSuffix", "-a.example.com:8080"); + containerRequiredConfig = new ContainerConfig(null); + containerRequiredConfig.loadFromString(json.toString()); + + json.put("gadgets.lockedDomainRequired", false); + containerEnabledConfig = new ContainerConfig(null); + containerEnabledConfig.loadFromString(json.toString()); + gadget = mock(Gadget.class); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testDisabledGlobally() { + domainLocker = new HashLockedDomainService( + containerRequiredConfig, "embed.com", false); + assertTrue(domainLocker.embedCanRender("anywhere.com")); + assertTrue(domainLocker.embedCanRender("embed.com")); + assertTrue(domainLocker.gadgetCanRender("embed.com", gadget, "default")); + + domainLocker = new HashLockedDomainService( + containerEnabledConfig, "embed.com", false); + assertTrue(domainLocker.embedCanRender("anywhere.com")); + assertTrue(domainLocker.embedCanRender("embed.com")); + assertTrue(domainLocker.gadgetCanRender("embed.com", gadget, "default")); + } + + public void testEnabledForGadget() { + domainLocker = new HashLockedDomainService( + containerEnabledConfig, "embed.com", true); + assertFalse(domainLocker.embedCanRender("anywhere.com")); + assertTrue(domainLocker.embedCanRender("embed.com")); + domainLocker.setSpecReader(wantsLocked); + assertFalse(domainLocker.gadgetCanRender( + "www.example.com", gadget, "default")); + assertTrue(domainLocker.gadgetCanRender( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + gadget, + "default")); + String target = domainLocker.getLockedDomainForGadget( + wantsLocked.getGadgetUrl(gadget), "default"); + assertEquals( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + target); + } + + public void testNotEnabledForGadget() { + domainLocker = new HashLockedDomainService( + containerEnabledConfig, "embed.com", true); + domainLocker.setSpecReader(noLocked); + assertTrue(domainLocker.gadgetCanRender( + "www.example.com", gadget, "default")); + assertFalse(domainLocker.gadgetCanRender( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + gadget, + "default")); + assertFalse(domainLocker.gadgetCanRender( + "foo-a.example.com:8080", + gadget, + "default")); + assertFalse(domainLocker.gadgetCanRender( + "foo-a.example.com:8080", + gadget, + "othercontainer")); + String target = domainLocker.getLockedDomainForGadget( + wantsLocked.getGadgetUrl(gadget), "default"); + assertEquals( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + target); + } + + public void testRequiredForContainer() { + domainLocker = new HashLockedDomainService( + containerRequiredConfig, "embed.com", true); + domainLocker.setSpecReader(noLocked); + assertFalse(domainLocker.gadgetCanRender( + "www.example.com", gadget, "default")); + assertTrue(domainLocker.gadgetCanRender( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + gadget, + "default")); + String target = domainLocker.getLockedDomainForGadget( + wantsLocked.getGadgetUrl(gadget), "default"); + assertEquals( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + target); + } + + public void testMissingConfig() throws Exception { + JSONObject json = new JSONObject(); + json.put("gadgets.container", + new JSONArray().put(ContainerConfig.DEFAULT_CONTAINER)); + ContainerConfig containerMissingConfig = new ContainerConfig(null); + containerMissingConfig.loadFromString(json.toString()); + domainLocker = new HashLockedDomainService( + containerMissingConfig, "embed.com", false); + domainLocker.setSpecReader(wantsLocked); + assertTrue(domainLocker.gadgetCanRender( + "www.example.com", gadget, "default")); + } + + public void testMultiContainer() throws Exception { + JSONObject json = new JSONObject(); + json.put("gadgets.container", + new JSONArray() + .put(ContainerConfig.DEFAULT_CONTAINER) + .put("other")); + json.put("gadgets.lockedDomainRequired", true); + json.put("gadgets.lockedDomainSuffix", "-a.example.com:8080"); + ContainerConfig inheritsConfig = new ContainerConfig(null); + inheritsConfig.loadFromString(json.toString()); + domainLocker = new HashLockedDomainService( + inheritsConfig, "embed.com", true); + domainLocker.setSpecReader(wantsLocked); + assertFalse(domainLocker.gadgetCanRender( + "www.example.com", gadget, "other")); + assertTrue(domainLocker.gadgetCanRender( + "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", + gadget, + "other")); + } +} Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/GadgetRenderingTaskTest.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/GadgetRenderingTaskTest.java?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/GadgetRenderingTaskTest.java (original) +++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/GadgetRenderingTaskTest.java Wed Apr 30 05:39:15 2008 @@ -19,10 +19,13 @@ package org.apache.shindig.gadgets.http; import org.apache.shindig.gadgets.ContainerConfig; +import org.apache.shindig.gadgets.Gadget; import org.apache.shindig.gadgets.GadgetContext; import org.apache.shindig.gadgets.RemoteContent; import org.apache.shindig.gadgets.RemoteContentRequest; import org.apache.shindig.gadgets.spec.GadgetSpec; +import org.easymock.EasyMock; + import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; import org.json.JSONArray; @@ -70,20 +73,41 @@ * @throws Exception */ private String parseBasicGadget(String view) throws Exception { - - expect(request.getParameter("url")).andReturn(SPEC_URL.toString()); - expect(request.getParameter("libs")).andReturn(LIBS); - expect(request.getParameter("view")).andReturn(view); - expect(request.getParameterNames()).andReturn(EMPTY_PARAMS); - expect(fetcher.fetch(SPEC_REQUEST)).andReturn(new RemoteContent(SPEC_XML)); - expect(response.getWriter()).andReturn(writer); + expectParseRequestParams(view); + expectFetchGadget(); + expectLockedDomainCheck(); + expectWriteResponse(); replay(); gadgetRenderer.process(request, response); verify(); writer.close(); return new String(baos.toByteArray(), "UTF-8"); } - + + private void expectParseRequestParams(String view) throws Exception { + expect(request.getParameter("url")).andReturn(SPEC_URL.toString()); + expect(request.getParameter("view")).andReturn(view); + expect(request.getParameterNames()).andReturn(EMPTY_PARAMS); + expect(request.getParameter("container")).andReturn(null); + expect(request.getHeader("Host")).andReturn("www.example.com"); + } + + private void expectLockedDomainCheck() throws Exception { + expect(lockedDomainService.gadgetCanRender( + EasyMock.eq("www.example.com"), + (Gadget)EasyMock.anyObject(), + EasyMock.eq("default"))).andReturn(true); + } + + private void expectFetchGadget() throws Exception { + expect(fetcher.fetch(SPEC_REQUEST)).andReturn(new RemoteContent(SPEC_XML)); + } + + private void expectWriteResponse() throws Exception { + expect(request.getParameter("libs")).andReturn(LIBS); + expect(response.getWriter()).andReturn(writer); + } + public void testStandardsMode() throws Exception { String content = parseBasicGadget(GadgetSpec.DEFAULT_VIEW); assertTrue(-1 != content.indexOf(GadgetRenderingTask.STRICT_MODE_DOCTYPE)); @@ -125,6 +149,35 @@ assertTrue(-1 != content.indexOf(ALT_CONTENT)); } + + public void testLockedDomainFailure() throws Exception { + expectParseRequestParams(GadgetSpec.DEFAULT_VIEW); + expectFetchGadget(); + expectLockedDomainFailure(); + expectSendRedirect(); + replay(); + gadgetRenderer.process(request, response); + verify(); + writer.close(); + } + + private void expectLockedDomainFailure() { + expect(lockedDomainService.gadgetCanRender( + EasyMock.eq("www.example.com"), + (Gadget)EasyMock.anyObject(), + EasyMock.eq("default"))).andReturn(false); + expect(request.getScheme()).andReturn("http"); + expect(request.getServletPath()).andReturn("/gadgets/ifr"); + expect(request.getQueryString()).andReturn("stuff=foo%20bar"); + expect(lockedDomainService.getLockedDomainForGadget( + SPEC_URL.toString(), "default")).andReturn("locked.example.com"); + } + + private void expectSendRedirect() throws Exception { + response.sendRedirect( + "http://locked.example.com/gadgets/ifr?stuff=foo%20bar"); + EasyMock.expectLastCall().once(); + } // TODO: Lots of ugly tests on html content. } Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpTestFixture.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpTestFixture.java?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpTestFixture.java (original) +++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpTestFixture.java Wed Apr 30 05:39:15 2008 @@ -20,7 +20,7 @@ import org.apache.shindig.gadgets.ContentFetcherFactory; import org.apache.shindig.gadgets.GadgetTestFixture; -import org.apache.shindig.gadgets.GadgetTokenDecoder; +import org.apache.shindig.gadgets.LockedDomainService; public abstract class HttpTestFixture extends GadgetTestFixture { @@ -30,16 +30,17 @@ public final ContentFetcherFactory contentFetcherFactory = mock(ContentFetcherFactory.class); public final UrlGenerator urlGenerator = mock(UrlGenerator.class); - public final GadgetTokenDecoder gadgetTokenDecoder - = mock(GadgetTokenDecoder.class); + public final LockedDomainService lockedDomainService = + mock(LockedDomainService.class); public HttpTestFixture() { super(); proxyHandler = new ProxyHandler( contentFetcherFactory, - gadgetTokenDecoder); + gadgetTokenDecoder, + lockedDomainService); gadgetRenderer = new GadgetRenderingTask(gadgetServer, registry, - containerConfig, urlGenerator, gadgetTokenDecoder); + containerConfig, urlGenerator, gadgetTokenDecoder, lockedDomainService); jsonRpcHandler = new JsonRpcHandler(executor, gadgetServer, urlGenerator); } } Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/ProxyHandlerTest.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/ProxyHandlerTest.java?rev=652372&r1=652371&r2=652372&view=diff ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/ProxyHandlerTest.java (original) +++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/ProxyHandlerTest.java Wed Apr 30 05:39:15 2008 @@ -26,14 +26,19 @@ import org.apache.shindig.gadgets.RemoteContentRequest; import org.apache.shindig.gadgets.spec.Auth; import org.apache.shindig.gadgets.spec.Preload; + import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; import org.json.JSONObject; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.PrintWriter; import java.net.URI; +import java.util.Enumeration; + +import javax.servlet.ServletOutputStream; public class ProxyHandlerTest extends HttpTestFixture { @@ -44,7 +49,22 @@ final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final PrintWriter writer = new PrintWriter(baos); - + + final ServletOutputStream responseStream = new ServletOutputStream() { + @Override + public void write(int b) throws IOException { + baos.write(b); + } + }; + + final static Enumeration<String> EMPTY_LIST = new Enumeration<String>() { + public boolean hasMoreElements() { + return false; + } + public String nextElement() { + return null; + } + }; private void expectGetAndReturnData(String url, byte[] data) throws Exception { @@ -80,6 +100,19 @@ expect(request.getParameter("url")).andReturn(url).atLeastOnce(); expect(response.getWriter()).andReturn(writer).atLeastOnce(); } + + private void setupProxyRequestMock(String host, String url) throws Exception { + expect(request.getMethod()).andReturn("GET").atLeastOnce(); + expect(request.getHeader("Host")).andReturn(host); + expect(request.getParameter("url")).andReturn(url).atLeastOnce(); + expect(request.getHeaderNames()).andReturn(EMPTY_LIST); + expect(response.getOutputStream()).andReturn(responseStream).atLeastOnce(); + } + + private void setupFailedProxyRequestMock(String host, String url) + throws Exception { + expect(request.getHeader("Host")).andReturn(host); + } private JSONObject readJSONResponse(String body) throws Exception { String json @@ -99,6 +132,33 @@ assertEquals(200, info.getInt("rc")); assertEquals(DATA_ONE, info.get("body")); } + + public void testLockedDomainEmbed() throws Exception { + setupProxyRequestMock("www.example.com", URL_ONE); + expect(lockedDomainService.embedCanRender("www.example.com")) + .andReturn(true); + expectGetAndReturnData(URL_ONE, DATA_ONE.getBytes()); + replay(); + proxyHandler.fetch(request, response); + verify(); + responseStream.close(); + assertEquals(DATA_ONE, new String(baos.toByteArray())); + } + + public void testLockedDomainFailedEmbed() throws Exception { + setupFailedProxyRequestMock("www.example.com", URL_ONE); + expect(lockedDomainService.embedCanRender("www.example.com")) + .andReturn(false); + replay(); + try { + proxyHandler.fetch(request, response); + fail("should have thrown"); + } catch (GadgetException e) { + assertTrue( + e.getMessage().indexOf("made to wrong domain www.example.com") != -1); + } + verify(); + } public void testFetchDecodedUrl() throws Exception { String origUrl = "http://www.example.com"; Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/util/StringEncodingTest.java URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/util/StringEncodingTest.java?rev=652372&view=auto ============================================================================== --- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/util/StringEncodingTest.java (added) +++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/util/StringEncodingTest.java Wed Apr 30 05:39:15 2008 @@ -0,0 +1,58 @@ +/* + * 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.util; + +import static org.junit.Assert.*; + +import junit.framework.JUnit4TestAdapter; + +import org.junit.Test; + +public class StringEncodingTest { + public static junit.framework.Test suite() { + return new JUnit4TestAdapter(StringEncodingTest.class); + } + + @Test + public void testBase32() throws Exception { + StringEncoding encoder = new StringEncoding( + "0123456789abcdefghijklmnopqrstuv".toCharArray()); + testEncoding(encoder, new byte[] { 0 }, "00"); + testEncoding(encoder, new byte[] { 0, 0 }, "0000"); + testEncoding(encoder, new byte[] { 10, 0 }, "1800"); + testRoundTrip(encoder, Crypto.getRandomBytes(1)); + testRoundTrip(encoder, Crypto.getRandomBytes(2)); + testRoundTrip(encoder, Crypto.getRandomBytes(3)); + testRoundTrip(encoder, Crypto.getRandomBytes(20)); + testRoundTrip(encoder, Crypto.getRandomBytes(30)); + } + + private void testRoundTrip(StringEncoding encoder, byte[] bytes) { + String encoded = encoder.encode(bytes); + byte[] decoded = encoder.decode(encoded); + assertArrayEquals(bytes, decoded); + } + + private void testEncoding(StringEncoding encoder, byte[] b, String s) { + String encoded = encoder.encode(b); + assertEquals(s, encoded); + byte[] decoded = encoder.decode(encoded); + assertArrayEquals(b, decoded); + } +}

