/* * 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.servlet; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import org.apache.shindig.common.EasyMockTestCase; import org.apache.shindig.common.uri.Uri; import org.apache.shindig.config.ContainerConfig; import org.apache.shindig.gadgets.Gadget; import org.apache.shindig.gadgets.GadgetException; 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.http.RequestPipeline; import org.apache.shindig.gadgets.rewrite.CaptureRewriter; import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry; import org.apache.shindig.gadgets.rewrite.DomWalker; import org.apache.shindig.gadgets.rewrite.MutableContent; import org.apache.shindig.gadgets.rewrite.ResponseRewriter; import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry; import org.apache.shindig.gadgets.rewrite.RewritingException; import org.apache.shindig.gadgets.uri.ProxyUriManager; import org.apache.shindig.gadgets.uri.UriCommon.Param; import org.easymock.Capture; import org.easymock.EasyMock; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; import org.junit.Test; import java.util.Arrays; import java.util.List; import java.util.Map; public class ProxyHandlerTest extends EasyMockTestCase { private final static String URL_ONE = "http://www.example.org/test.html"; private final static String DATA_ONE = "hello world"; public final RequestPipeline pipeline = mock(RequestPipeline.class); public CaptureRewriter rewriter = new CaptureRewriter(); public ResponseRewriterRegistry rewriterRegistry = new DefaultResponseRewriterRegistry(Arrays.<ResponseRewriter>asList(rewriter), null); private ProxyUriManager.ProxyUri request; private final ProxyHandler proxyHandler = new ProxyHandler(pipeline, rewriterRegistry, true); private void expectGetAndReturnData(String url, byte[] data) throws Exception { HttpRequest req = new HttpRequest(Uri.parse(url)); HttpResponse resp = new HttpResponseBuilder().setResponse(data).create(); expect(pipeline.execute(req)).andReturn(resp); } private void expectGetAndReturnHeaders(String url, Map<String, List<String>> headers) throws Exception { HttpRequest req = new HttpRequest(Uri.parse(url)); HttpResponse resp = new HttpResponseBuilder().addAllHeaders(headers).create(); expect(pipeline.execute(req)).andReturn(resp); } private void setupProxyRequestMock(String host, String url, boolean noCache, int refresh, String rewriteMime, String fallbackUrl) throws Exception { request = new ProxyUriManager.ProxyUri( refresh, false, noCache, ContainerConfig.DEFAULT_CONTAINER, null, Uri.parse(url)); request.setFallbackUrl(fallbackUrl); request.setRewriteMimeType(rewriteMime); } private void setupNoArgsProxyRequestMock(String host, String url) throws Exception { request = new ProxyUriManager.ProxyUri( -1, false, false, ContainerConfig.DEFAULT_CONTAINER, null, url != null ? Uri.parse(url) : null); } private ResponseRewriter getResponseRewriterThatThrowsExceptions( final StringBuilder stringBuilder) { return new DomWalker.Rewriter() { public void rewrite(Gadget gadget, MutableContent content) throws RewritingException { stringBuilder.append("exceptionThrown"); throw new RewritingException("sad", 404); } public void rewrite(HttpRequest request, HttpResponseBuilder builder) throws RewritingException { stringBuilder.append("exceptionThrown"); throw new RewritingException("sad", 404); } }; } @Test public void testInvalidHeaderDropped() throws Exception { String url = "http://example.org/mypage.html"; String domain = "example.org"; setupProxyRequestMock(domain, url, true, -1, null, null); HttpRequest req = new HttpRequest(Uri.parse(url)) .setIgnoreCache(true); String contentType = "text/html; charset=UTF-8"; HttpResponse resp = new HttpResponseBuilder() .setResponseString("Hello") .addHeader("Content-Type", contentType) .addHeader("Content-Length", "200") // Disallowed header. .addHeader(":", "someDummyValue") // Invalid header name. .create(); expect(pipeline.execute(req)).andReturn(resp); replay(); HttpResponse recorder = proxyHandler.fetch(request); verify(); assertNull(recorder.getHeader(":")); assertNull(recorder.getHeader("Content-Length")); assertEquals(recorder.getHeader("Content-Type"), contentType); } @Test public void testLockedDomainEmbed() throws Exception { setupNoArgsProxyRequestMock("www.example.com", URL_ONE); expectGetAndReturnData(URL_ONE, DATA_ONE.getBytes()); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); assertEquals(DATA_ONE, response.getResponseAsString()); assertTrue(rewriter.responseWasRewritten()); } @Test(expected=GadgetException.class) public void testNoUrl() throws Exception { setupNoArgsProxyRequestMock("www.example.com", null); replay(); proxyHandler.fetch(request); fail("Proxy should raise exception if there is no url"); } @Test public void testHttpRequestFillsParentAndContainer() throws Exception { setupNoArgsProxyRequestMock("www.example.com", URL_ONE); //HttpRequest req = new HttpRequest(Uri.parse(URL_ONE)); HttpResponse resp = new HttpResponseBuilder().setResponse(DATA_ONE.getBytes()).create(); Capture<HttpRequest> httpRequest = new Capture<HttpRequest>(); expect(pipeline.execute(capture(httpRequest))).andReturn(resp); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); // Check that the HttpRequest passed in has all the relevant fields sets assertEquals("default", httpRequest.getValue().getContainer()); assertEquals(Uri.parse(URL_ONE), httpRequest.getValue().getUri()); assertEquals(DATA_ONE, response.getResponseAsString()); assertTrue(rewriter.responseWasRewritten()); } @Test public void testHeadersPreserved() throws Exception { // Some headers may be blacklisted. These are OK. String url = "http://example.org/file.evil"; String domain = "example.org"; String contentType = "text/evil; charset=UTF-8"; String magicGarbage = "fadfdfdfd"; Map<String, List<String>> headers = Maps.newHashMap(); headers.put("Content-Type", Arrays.asList(contentType)); headers.put("X-Magic-Garbage", Arrays.asList(magicGarbage)); setupNoArgsProxyRequestMock(domain, url); expectGetAndReturnHeaders(url, headers); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); assertEquals(contentType, response.getHeader("Content-Type")); assertEquals(magicGarbage, response.getHeader("X-Magic-Garbage")); assertTrue(rewriter.responseWasRewritten()); } @Test public void testOctetSetOnNullContentType() throws Exception { String url = "http://example.org/file.evil"; String domain = "example.org"; setupNoArgsProxyRequestMock(domain, url); expectGetAndReturnHeaders(url, Maps.<String, List<String>>newHashMap()); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); assertEquals("application/octet-stream", response.getHeader("Content-Type")); assertNotNull(response.getHeader("Content-Disposition")); assertTrue(rewriter.responseWasRewritten()); } @Test public void testNoContentDispositionForFlash() throws Exception { assertNoContentDispositionForFlash("application/x-shockwave-flash"); } @Test public void testNoContentDispositionForFlashUtf8() throws Exception { assertNoContentDispositionForFlash("application/x-shockwave-flash;charset=utf-8"); } private void assertNoContentDispositionForFlash(String contentType) throws Exception { // Some headers may be blacklisted. These are OK. String url = "http://example.org/file.evil"; String domain = "example.org"; Map<String, List<String>> headers = ImmutableMap.of("Content-Type", Arrays.asList(contentType)); setupNoArgsProxyRequestMock(domain, url); expectGetAndReturnHeaders(url, headers); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); assertEquals(contentType, response.getHeader("Content-Type")); assertNull(response.getHeader("Content-Disposition")); assertTrue(rewriter.responseWasRewritten()); } @Test public void testGetFallback() throws Exception { String url = "http://example.org/file.evil"; String domain = "example.org"; String fallback_url = "http://fallback.com/fallback.png"; setupProxyRequestMock(domain, url, true, -1, null, fallback_url); HttpRequest req = new HttpRequest(Uri.parse(url)).setIgnoreCache(true); HttpResponse resp = HttpResponse.error(); HttpResponse fallback_resp = new HttpResponse("Fallback"); expect(pipeline.execute(req)).andReturn(resp); expect(pipeline.execute(isA(HttpRequest.class))).andReturn(fallback_resp); replay(); proxyHandler.fetch(request); verify(); } @Test public void testNoCache() throws Exception { String url = "http://example.org/file.evil"; String domain = "example.org"; setupProxyRequestMock(domain, url, true, -1, null, null); HttpRequest req = new HttpRequest(Uri.parse(url)).setIgnoreCache(true); HttpResponse resp = new HttpResponse("Hello"); expect(pipeline.execute(req)).andReturn(resp); replay(); proxyHandler.fetch(request); verify(); } // ProxyHandler throws INTERNAL_SERVER_ERRORS without isRecoverable() check. @Test public void testRecoverableRewritingException() throws Exception { String url = "http://example.org/mypage.html"; String domain = "example.org"; setupProxyRequestMock(domain, url, true, -1, null, null); String contentType = "text/html; charset=UTF-8"; HttpResponse resp = new HttpResponseBuilder() .setResponseString("Hello") .addHeader("Content-Type", contentType) .create(); expect(pipeline.execute((HttpRequest) EasyMock.anyObject())).andReturn(resp); replay(); final StringBuilder stringBuilder = new StringBuilder(""); ResponseRewriter rewriter = getResponseRewriterThatThrowsExceptions(stringBuilder); ResponseRewriterRegistry rewriterRegistry = new DefaultResponseRewriterRegistry( Arrays.<ResponseRewriter>asList(rewriter), null); ProxyHandler proxyHandler = new ProxyHandler(pipeline, rewriterRegistry, true); request.setReturnOriginalContentOnError(true); HttpResponse recorder = proxyHandler.fetch(request); verify(); // Ensure that original content is returned. assertEquals(recorder.getHeader("Content-Type"), contentType); assertEquals("Hello", recorder.getResponseAsString()); assertEquals("exceptionThrown", stringBuilder.toString()); } @Test public void testThrowExceptionIfReturnOriginalContentOnErrorNotSet() throws Exception { String url = "http://example.org/mypage.html"; String domain = "example.org"; setupProxyRequestMock(domain, url, true, -1, null, null); String contentType = "text/html; charset=UTF-8"; HttpResponse resp = new HttpResponseBuilder() .setResponseString("Hello") .addHeader("Content-Type", contentType) .create(); expect(pipeline.execute((HttpRequest) EasyMock.anyObject())).andReturn(resp); replay(); final StringBuilder stringBuilder = new StringBuilder(""); ResponseRewriter rewriter = getResponseRewriterThatThrowsExceptions(stringBuilder); ResponseRewriterRegistry rewriterRegistry = new DefaultResponseRewriterRegistry( Arrays.<ResponseRewriter>asList(rewriter), null); ProxyHandler proxyHandler = new ProxyHandler(pipeline, rewriterRegistry, true); boolean exceptionCaught = false; try { proxyHandler.fetch(request); } catch (GadgetException e) { exceptionCaught = true; assertEquals(404, e.getHttpStatusCode()); } assertTrue(exceptionCaught); assertEquals("exceptionThrown", stringBuilder.toString()); } /** * Override HttpRequest equals to check for cache control fields */ static class HttpRequestCache extends HttpRequest { public HttpRequestCache(Uri uri) { super(uri); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof HttpRequest)) { return false; } HttpRequest req = (HttpRequest)obj; return super.equals(obj) && req.getCacheTtl() == getCacheTtl() && req.getIgnoreCache() == getIgnoreCache(); } @Override public int hashCode() { return Objects.hashCode(super.hashCode(), getCacheTtl(), getIgnoreCache()); } } @Test public void testWithCache() throws Exception { String url = "http://example.org/file.evil"; String domain = "example.org"; setupProxyRequestMock(domain, url, false, 120, null, null); HttpRequest req = new HttpRequestCache(Uri.parse(url)).setCacheTtl(120).setIgnoreCache(false); HttpResponse resp = new HttpResponse("Hello"); expect(pipeline.execute(req)).andReturn(resp); replay(); proxyHandler.fetch(request); verify(); } @Test public void testWithBadTtl() throws Exception { String url = "http://example.org/file.evil"; String domain = "example.org"; setupProxyRequestMock(domain, url, false, -1, null, null); HttpRequest req = new HttpRequestCache(Uri.parse(url)).setCacheTtl(-1).setIgnoreCache(false); HttpResponse resp = new HttpResponse("Hello"); expect(pipeline.execute(req)).andReturn(resp); replay(); proxyHandler.fetch(request); verify(); } private void expectMime(String expectedMime, String contentMime, String outputMime) throws Exception { String url = "http://example.org/file.img?" + Param.REWRITE_MIME_TYPE.getKey() + '=' + expectedMime; String domain = "example.org"; setupProxyRequestMock(domain, url, false, -1, expectedMime, null); HttpRequest req = new HttpRequest(Uri.parse(url)) .setRewriteMimeType(expectedMime); HttpResponse resp = new HttpResponseBuilder() .setResponseString("Hello") .addHeader("Content-Type", contentMime) .create(); expect(pipeline.execute(req)).andReturn(resp); replay(); HttpResponse response = proxyHandler.fetch(request); verify(); assertEquals(outputMime, response.getHeader("Content-Type")); reset(); } @Test public void testMimeMatchPass() throws Exception { expectMime("text/css", "text/css", "text/css; charset=UTF-8"); } @Test public void testMimeMatchPassWithAdditionalAttributes() throws Exception { expectMime("text/css", "text/css", "text/css; charset=UTF-8"); } @Test public void testMimeMatchOverrideNonMatch() throws Exception { expectMime("text/css", "image/png", "text/css; charset=UTF-8"); } @Test public void testMimeMatchVarySupport() throws Exception { // We use CaptureRewrite which always rewrite - always set encoding expectMime("image/*", "image/gif", "image/gif"); } }