diff --git a/CHANGELOG.md b/CHANGELOG.md index 1940576bf7..62e829d38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - [#7158](https://github.com/apache/trafficcontrol/issues/7158) *Traffic Vault* Fix the `reencrypt` utility to uniquely reencrypt each version of the SSL Certificates. - [#7137](https://github.com/apache/trafficcontrol/pull/7137) *Cache Config* parent.config simulate topology for non topo delivery services. - Adds an extra T3C check for validity of an ssl cert (crash fix). +- Traffic Router now always includes a `Content-Length` header in the response. ## [7.0.0] - 2022-07-19 ### Added diff --git a/docs/source/development/traffic_router/traffic_router_api.rst b/docs/source/development/traffic_router/traffic_router_api.rst index fc6e4cdc8a..9fc1d6adf1 100644 --- a/docs/source/development/traffic_router/traffic_router_api.rst +++ b/docs/source/development/traffic_router/traffic_router_api.rst @@ -70,7 +70,7 @@ Request Structure GET /crs/stats HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -80,14 +80,14 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:02:09 GMT + Content-Length: 1214 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "app": { - "buildTimestamp": "2019-01-10", + "buildTimestamp": "2019-11-04", "name": "traffic_router", "deploy-dir": "/opt/traffic_router", - "git-revision": "437e9df81", + "git-revision": "eabc2b82e", "version": "3.0.0" }, "stats": { @@ -109,26 +109,26 @@ Response Structure "totalDnsCount": 0, "totalHttpCount": 1, "totalDsMissCount": 0, - "appStartTime": 1547584831677, + "appStartTime": 1572895915703, "averageDnsTime": 0, - "averageHttpTime": 1547584863270, + "averageHttpTime": 1572895947202, "updateTracker": { - "lastHttpsCertificatesCheck": 1547586068932, - "lastGeolocationDatabaseUpdaterUpdate": 1547584858917, - "lastCacheStateCheck": 1547586128932, - "lastCacheStateChange": 1547584867102, - "lastNetworkUpdaterUpdate": 1547584857484, - "lastHttpsCertificatesUpdate": 1547586071079, - "lastSteeringWatcherUpdate": 1547584923514, - "lastConfigCheck": 1547586127344, - "lastConfigChange": 1547584863406, - "lastNetworkUpdaterCheck": 1547584857465, - "lastGeolocationDatabaseUpdaterCheck": 1547584858906, - "lastFederationsWatcherUpdate": 1547584863433, - "lastHttpsCertificatesFetchSuccess": 1547586069070, - "lastSteeringWatcherCheck": 1547586124630, - "lastFederationsWatcherCheck": 1547586124584, - "lastHttpsCertificatesFetchAttempt": 1547586068932 + "lastHttpsCertificatesCheck": 1572896852436, + "lastGeolocationDatabaseUpdaterUpdate": 1572895942543, + "lastCacheStateCheck": 1572896884465, + "lastCacheStateChange": 1572895951089, + "lastNetworkUpdaterUpdate": 1572895941407, + "lastHttpsCertificatesUpdate": 1572896854512, + "lastSteeringWatcherUpdate": 1572896007369, + "lastConfigCheck": 1572896881213, + "lastConfigChange": 1572895947297, + "lastNetworkUpdaterCheck": 1572895941392, + "lastGeolocationDatabaseUpdaterCheck": 1572895942533, + "lastFederationsWatcherUpdate": 1572895947336, + "lastHttpsCertificatesFetchSuccess": 1572896852506, + "lastSteeringWatcherCheck": 1572896848090, + "lastFederationsWatcherCheck": 1572896848067, + "lastHttpsCertificatesFetchAttempt": 1572896852436 } }} @@ -153,7 +153,7 @@ Request Structure GET /crs/stats/ip/255.255.255.255 HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -164,8 +164,8 @@ Response Structure HTTP/1.1 200 OK Content-Disposition: inline;filename=f.txt Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:06:09 GMT + Content-Length: 131 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "locationByGeo": { "city": "Woodridge", @@ -194,7 +194,7 @@ Request Structure GET /crs/locations HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -206,8 +206,8 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:12:17 GMT + Content-Length: 35 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "locations": [ "CDN_in_a_Box_Edge" @@ -226,7 +226,7 @@ Request Structure GET /crs/locations/caches HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -236,8 +236,8 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:15:53 GMT + Content-Length: 278 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "locations": { "CDN_in_a_Box_Edge": [ @@ -245,8 +245,8 @@ Response Structure "cacheId": "edge", "fqdn": "edge.infra.ciab.test", "ipAddresses": [ - "172.16.239.100", - "fc01:9400:1000:8:0:0:0:100" + "172.16.239.4", + "fc01:9400:1000:8:0:0:0:4" ], "port": 0, "adminStatus": null, @@ -282,7 +282,7 @@ Request Structure GET /crs/locations/CDN_in_a_Box_Edge/caches HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -292,16 +292,16 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:18:25 GMT + Content-Length: 253 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "caches": [ { "cacheId": "edge", "fqdn": "edge.infra.ciab.test", "ipAddresses": [ - "172.16.239.100", - "fc01:9400:1000:8:0:0:0:100" + "172.16.239.4", + "fc01:9400:1000:8:0:0:0:4" ], "port": 0, "adminStatus": null, @@ -409,7 +409,7 @@ Request Structure GET /crs/consistenthash/deliveryservice?deliveryServiceId=demo1&requestPath=/ HTTP/1.1 Host: trafficrouter.infra.ciab.test - User-Agent: curl/7.47.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -419,8 +419,8 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Tue, 15 Jan 2019 21:40:51 GMT + Content-Length: 828 + Date: Mon, 04 Nov 2019 19:48:04 GMT { "id": "demo1", "coverageZoneOnly": false, @@ -429,8 +429,8 @@ Response Structure "geoRedirectUrlType": "INVALID_URL", "routingName": "video", "missLocation": { - "latitude": 42, - "longitude": -88, + "latitude": 42.0, + "longitude": -88.0, "postalCode": null, "city": null, "countryCode": null, @@ -458,6 +458,13 @@ Response Structure "sslEnabled": true, "acceptHttp": true, "deepCache": "NEVER", + "consistentHashRegex": "", + "consistentHashQueryParams": [ + "abc", + "zyx", + "xxx", + "pdq" + ], "dns": false, "locationLimit": 0, "maxDnsIps": 0, @@ -552,7 +559,7 @@ Request Structure GET /crs/consistenthash/patternbased/regex?regex=%2F.*%3F%28%2F.*%3F%2F%29.*%3F%28%5C.m3u8%29&requestPath=%2Ftext1234%2Fname%2Fasset.m3u8 HTTP/1.1 Host: localhost:3333 - User-Agent: curl/7.54.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -562,13 +569,12 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Fri, 15 Feb 2019 22:06:53 GMT + Content-Length: 137 + Date: Mon, 04 Nov 2019 19:48:04 GMT - { - "resultingPathToConsistentHash":"/name/.m3u8", - "consistentHashRegex":"/.*?(/.*?/).*?(\\.m3u8)", - "requestPath":"/text1234/name/asset.m3u8" + { "resultingPathToConsistentHash": "/name/.m3u8", + "consistentHashRegex": "/.*?(/.*?/).*?(\\.m3u8)", + "requestPath": "/text1234/name/asset.m3u8" } .. _tr-api-crs-consistenthash-patternbased-deliveryservice: @@ -592,9 +598,9 @@ Request Structure .. code-block:: http :caption: Request Example - GET /crs/consistenthash/patternbased/deliveryservice?deliveryServiceId=asdf&requestPath=%2Fsometext1234%2Fstream_name%2Fasset_name.m3u8 HTTP/1.1 + GET /crs/consistenthash/patternbased/deliveryservice?deliveryServiceId=demo1&requestPath=%2Fsometext1234%2Fstream_name%2Fasset_name.m3u8 HTTP/1.1 Host: localhost:3333 - User-Agent: curl/7.54.0 + User-Agent: curl/7.52.1 Accept: */* Response Structure @@ -604,13 +610,12 @@ Response Structure HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 - Transfer-Encoding: chunked - Date: Fri, 15 Feb 2019 22:12:38 GMT + Content-Length: 163 + Date: Mon, 04 Nov 2019 19:48:04 GMT - { - "resultingPathToConsistentHash":"/sometext1234/stream_name/asset_name.m3u8", - "deliveryServiceId":"asdf", - "requestPath":"/sometext1234/stream_name/asset_name.m3u8" + { "resultingPathToConsistentHash": "/sometext1234/stream_name/asset_name.m3u8", + "deliveryServiceId": "demo1", + "requestPath": "/sometext1234/stream_name/asset_name.m3u8" } .. _tr-api-crs-consistenthash-cache-coveragezone-steering: diff --git a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/api/controllers/LocationController.java b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/api/controllers/LocationController.java index b96eb44b95..fedd98d534 100644 --- a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/api/controllers/LocationController.java +++ b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/api/controllers/LocationController.java @@ -35,7 +35,7 @@ public class LocationController { @Autowired private DataExporter dataExporter; - @RequestMapping(value = "/{locID}/caches", method = RequestMethod.GET) + @RequestMapping(value = "/{locID}/caches", method = {RequestMethod.GET, RequestMethod.HEAD}) public @ResponseBody Map> getCaches(@PathVariable("locID") final String locId) { final Map> map = new HashMap>(); @@ -43,17 +43,17 @@ Map> getCaches(@PathVariable("locID") final String locI return map; } - @RequestMapping(value = "", method = RequestMethod.GET) + @RequestMapping(value = "", method = {RequestMethod.GET, RequestMethod.HEAD}) public @ResponseBody - Map> getLocations() { - final Map> locations = new HashMap>(); + Map> getLocations() { + final Map> locations = new HashMap>(); locations.put("locations", dataExporter.getLocations()); return locations; } - @RequestMapping(value = "/caches", method = RequestMethod.GET) + @RequestMapping(value = "/caches", method = {RequestMethod.GET, RequestMethod.HEAD}) public @ResponseBody - Map>> getCaches() { + Map>> getCaches() { final Map>> map = new HashMap>>(); final Map> innerMap = new HashMap>(); diff --git a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponse.java b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponse.java new file mode 100644 index 0000000000..3d2cd2e622 --- /dev/null +++ b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponse.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.traffic_control.traffic_router.core.http; + +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class BufferedResponse extends ContentCachingResponseWrapper { + protected int contentLength; + protected ServletResponse response; + + public BufferedResponse(final HttpServletResponse response) { + super(response); + this.response = response; + } + + @Override + public void setContentLength(final int len) { + contentLength = len; + super.setContentLength(len); + } + + @Override + public void setContentLengthLong(final long len) { + contentLength = (int) len; + super.setContentLengthLong(len); + } + + @Override + public void copyBodyToResponse() throws IOException { + if (this.getContentSize() == 0) { + response.setContentLength(contentLength); + } else { + // When the content size is greater than 0, copyBodyToResponse() + // will set Content-Length. + super.copyBodyToResponse(); + } + } +} diff --git a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponseFilter.java b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponseFilter.java new file mode 100644 index 0000000000..f4224d8255 --- /dev/null +++ b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/BufferedResponseFilter.java @@ -0,0 +1,37 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.traffic_control.traffic_router.core.http; + +import com.google.common.net.HttpHeaders; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class BufferedResponseFilter extends OncePerRequestFilter { + public static final Logger LOGGER = LogManager.getLogger(BufferedResponseFilter.class); + + public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { + final BufferedResponse responseWrapper = new BufferedResponse(response); + chain.doFilter(request, responseWrapper); + responseWrapper.copyBodyToResponse(); + } +} diff --git a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/RouterFilter.java b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/RouterFilter.java index bed96b269d..58b6615183 100644 --- a/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/RouterFilter.java +++ b/traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/http/RouterFilter.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -41,7 +42,6 @@ public class RouterFilter extends OncePerRequestFilter { private static final Logger ACCESS = LogManager.getLogger("org.apache.traffic_control.traffic_router.core.access"); public static final String REDIRECT_QUERY_PARAM = "trred"; - private static final String HEAD = "HEAD"; @Autowired private TrafficRouterManager trafficRouterManager; @@ -143,18 +143,16 @@ private void setMultiResponse(final HTTPRouteResult routeResult, final HttpServl final String redirect = httpServletRequest.getParameter(REDIRECT_QUERY_PARAM); - if (!HEAD.equals(httpServletRequest.getMethod())) { - response.setContentType("application/json"); - response.getWriter().println(routeResult.toMultiLocationJSONString()); - httpAccessRecordBuilder.responseURLs(routeResult.getUrls()); - } + response.setContentType("application/json"); + response.getWriter().println(routeResult.toMultiLocationJSONString()); + httpAccessRecordBuilder.responseURLs(routeResult.getUrls()); // don't actually parse the boolean value; trred would always be false unless the query param is "true" if ("false".equalsIgnoreCase(redirect)) { response.setStatus(HttpServletResponse.SC_OK); httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_OK); } else { - response.setHeader("Location", routeResult.getUrl().toString()); + response.setHeader(HttpHeaders.LOCATION, routeResult.getUrl().toString()); response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_MOVED_TEMPORARILY); httpAccessRecordBuilder.responseURL(routeResult.getUrl()); @@ -176,23 +174,18 @@ private void setSingleResponse(final HTTPRouteResult routeResult, final HttpServ } if ("false".equalsIgnoreCase(redirect)) { - if (!HEAD.equals(httpServletRequest.getMethod())) { - response.setContentType("application/json"); - response.getWriter().println(routeResult.toMultiLocationJSONString()); - httpAccessRecordBuilder.responseURLs(routeResult.getUrls()); - } - + response.setContentType("application/json"); + response.getWriter().println(routeResult.toMultiLocationJSONString()); + httpAccessRecordBuilder.responseURLs(routeResult.getUrls()); httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_OK); } else if ("json".equals(format)) { - if (!HEAD.equals(httpServletRequest.getMethod())) { - response.setContentType("application/json"); - response.getWriter().println(routeResult.toLocationJSONString()); - httpAccessRecordBuilder.responseURL(location); - } - + response.setContentType("application/json"); + response.getWriter().println(routeResult.toLocationJSONString()); + httpAccessRecordBuilder.responseURL(location); httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_OK); } else { - response.sendRedirect(location.toString()); + response.setHeader(HttpHeaders.LOCATION, location.toString()); + response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_MOVED_TEMPORARILY); httpAccessRecordBuilder.responseURL(location); } diff --git a/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml b/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml index 8b0c21fb1e..f478724088 100644 --- a/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml +++ b/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml @@ -351,6 +351,9 @@ + + + /crossdomain.xml /clientaccesspolicy.xml diff --git a/traffic_router/core/src/main/webapp/WEB-INF/web.xml b/traffic_router/core/src/main/webapp/WEB-INF/web.xml index 99e3b5dffc..5586d7b72c 100644 --- a/traffic_router/core/src/main/webapp/WEB-INF/web.xml +++ b/traffic_router/core/src/main/webapp/WEB-INF/web.xml @@ -35,6 +35,20 @@ 1 + + bufferedResponseFilter + org.springframework.web.filter.DelegatingFilterProxy + + targetBeanName + bufferedResponseFilter + + + + + bufferedResponseFilter + /* + + routerFilter org.springframework.web.filter.DelegatingFilterProxy diff --git a/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/BufferedResponseTest.java b/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/BufferedResponseTest.java new file mode 100644 index 0000000000..9f0bbbf352 --- /dev/null +++ b/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/BufferedResponseTest.java @@ -0,0 +1,174 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.traffic_control.traffic_router.core.external; + +import org.apache.traffic_control.traffic_router.core.http.RouterFilter; +import org.apache.traffic_control.traffic_router.core.util.ExternalTest; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.catalina.LifecycleException; +import org.apache.http.HttpHeaders; +import org.apache.http.Header; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.IsEqual.equalTo; + +@Category(ExternalTest.class) +public class BufferedResponseTest { + final private String routerHttpPort = System.getProperty("routerHttpPort", "8888"); + private CloseableHttpClient httpClient; + + @Before + public void before() throws LifecycleException { + httpClient = HttpClientBuilder.create().build(); + } + + @After + public void after() throws Exception { + if (httpClient != null) httpClient.close(); + } + + @Test + public void itSetsContentLengthHeaderFor404() throws IOException { + final String encodedUrl = URLEncoder.encode("http://trafficrouter01.somedeliveryservice.somecdn.domain.foo/stuff", StandardCharsets.UTF_8); + final HttpGet httpGet = new HttpGet("http://localhost:3333/crs/deliveryservices?url=" + encodedUrl); + + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + assertThat(response.getStatusLine().getStatusCode(), equalTo(404)); + assertThat(response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING), nullValue()); + assertThat(response.getFirstHeader(HttpHeaders.CONTENT_LENGTH), notNullValue()); + } + } + + @Test + public void itSetsTheSameContentLengthForHeadAndGet() throws IOException { + final List paths = new ArrayList<>(); + paths.add("http://localhost:3333/crs/stats"); + paths.add("http://localhost:3333/crs/locations/caches"); + paths.add("http://localhost:3333/crs/consistenthash/deliveryservice?deliveryServiceId=csd-target-1&requestPath=/"); + + for (final String path : paths) { + final List requests = new ArrayList<>(); + requests.add(new HttpHead(path)); + requests.add(new HttpGet(path)); + + final List contentLengths = new ArrayList<>(); + + for (final HttpRequestBase request : requests) { + + try (CloseableHttpResponse response = httpClient.execute(request)) { + final Header contentLengthHeader = response.getFirstHeader(HttpHeaders.CONTENT_LENGTH); + + assertThat(response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING), nullValue()); + assertThat(contentLengthHeader, notNullValue()); + contentLengths.add(Integer.parseInt(contentLengthHeader.getValue())); + } + } + + assertThat(contentLengths.size(), equalTo(2)); + assertThat("Expected HEAD and GET requests for " + path + " to have the same Content-Length", contentLengths.get(0), equalTo(contentLengths.get(1))); + } + } + + @Test + public void itSetsAnAccurateContentLengthForGet() throws IOException { + final List paths = new ArrayList<>(); + paths.add("http://localhost:3333/crs/stats"); + paths.add("http://localhost:3333/crs/locations/caches"); + paths.add("http://localhost:3333/crs/consistenthash/deliveryservice?deliveryServiceId=csd-target-1&requestPath=/"); + + for (final String path : paths) { + CloseableHttpResponse response = null; + + try { + final HttpGet httpGet = new HttpGet(path); + response = httpClient.execute(httpGet); + + final ObjectMapper objectMapper = new ObjectMapper(new JsonFactory()); + final String json = EntityUtils.toString(response.getEntity()); + final Header contentLengthHeader = response.getFirstHeader(HttpHeaders.CONTENT_LENGTH); + + /* If the content length is too low and cuts off the response + * body, objectMapper.readTree(json) will likely throw a + * JsonProcessingException. + */ + objectMapper.readTree(json); + assertThat(response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING), nullValue()); + assertThat(contentLengthHeader, notNullValue()); + assertThat(Integer.parseInt(contentLengthHeader.getValue()), equalTo(json.length())); + } finally { + if (response != null) response.close(); + } + } + } + + @Test + public void itSetsContentLengthHeaderForDeliveryServiceSteering() throws IOException { + final String testHostName = "tr.client-steering-test-1.thecdn.example.com"; + final RequestConfig config = RequestConfig.custom().setRedirectsEnabled(false).build(); + final List paths = new ArrayList<>(); + paths.add("/qwerytuiop/asdfghjkl?fakeClientIpAddress=12.34.56.78"); + paths.add("/qwerytuiop/asdfghjkl?fakeClientIpAddress=12.34.56.78&format=json"); + paths.add("/qwerytuiop/asdfghjkl?fakeClientIpAddress=12.34.56.78&" + RouterFilter.REDIRECT_QUERY_PARAM + "=false"); + paths.add("/qwerytuiop/asdfghjkl?fakeClientIpAddress=12.34.56.78&" + RouterFilter.REDIRECT_QUERY_PARAM + "=true"); + + + for (final String path : paths) { + final List requests = new ArrayList<>(); + requests.add(new HttpHead("http://localhost:" + routerHttpPort + path)); + requests.add(new HttpGet("http://localhost:" + routerHttpPort + path)); + + final List contentLengths = new ArrayList<>(); + + for (final HttpRequestBase request : requests) { + + request.setConfig(config); + request.addHeader("Host", testHostName); + try (CloseableHttpResponse response = httpClient.execute(request)) { + final Header contentLengthHeader = response.getFirstHeader(HttpHeaders.CONTENT_LENGTH); + + assertThat(response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING), nullValue()); + assertThat(contentLengthHeader, notNullValue()); + contentLengths.add(Integer.parseInt(contentLengthHeader.getValue())); + } + } + + assertThat(contentLengths.size(), equalTo(2)); + assertThat("Expected HEAD and GET requests for " + testHostName + path + " to have the same Content-Length", contentLengths.get(0), equalTo(contentLengths.get(1))); + } + } +} diff --git a/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/LocationsTest.java b/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/LocationsTest.java index a9f48d9fe7..ff24050893 100644 --- a/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/LocationsTest.java +++ b/traffic_router/core/src/test/java/org/apache/traffic_control/traffic_router/core/external/LocationsTest.java @@ -15,6 +15,7 @@ package org.apache.traffic_control.traffic_router.core.external; +import org.apache.http.HttpHeaders; import org.apache.traffic_control.traffic_router.core.util.ExternalTest; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; @@ -22,6 +23,7 @@ import org.apache.catalina.LifecycleException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; @@ -30,10 +32,15 @@ import org.junit.Test; import org.junit.experimental.categories.Category; +import java.util.ArrayList; +import java.util.List; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.AnyOf.anyOf; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.fail; @Category(ExternalTest.class) public class LocationsTest { @@ -139,4 +146,39 @@ public void itGetsCachesForALocation() throws Exception { if (response != null) response.close(); } } + + @Test + public void itHandlesHeadRequests() throws Exception { + final List paths = new ArrayList(); + paths.add("http://localhost:3333/crs/locations"); + paths.add("http://localhost:3333/crs/locations/caches"); + + CloseableHttpResponse response = null; + + try { + final HttpGet httpGet = new HttpGet("http://localhost:3333/crs/locations"); + response = closeableHttpClient.execute(httpGet); + + ObjectMapper objectMapper = new ObjectMapper(new JsonFactory()); + JsonNode jsonNode = objectMapper.readTree(EntityUtils.toString(response.getEntity())); + + String location = jsonNode.get("locations").get(0).asText(); + paths.add("http://localhost:3333/crs/locations/" + location + "/caches"); + } catch (Exception e) { + fail(e.getMessage()); + } finally { + if (response != null) response.close(); + } + + for (final String path : paths) { + final HttpHead httpHead = new HttpHead(path); + try { + response = closeableHttpClient.execute(httpHead); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + assertThat(response.getFirstHeader(HttpHeaders.CONTENT_LENGTH), notNullValue()); + } finally { + if (response != null) response.close(); + } + } + } }