Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added an HTTP-Header equivalent for 'format' TR Query Param #3554

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added support for DS plugin parameters for cachekey, slice, cache_range_requests, background_fetch, url_sig as remap.config parameters.
- Updated T3C changes in Ansible playbooks
- Updated all endpoints in infrastructure code to use API version 2.0
- Added HTTP Header equivalent of ``format`` query parameter in Traffic Router - ``X-TC-Format``.

### Fixed
- [#5690](https://github.com/apache/trafficcontrol/issues/5690) - Fixed github action for added/modified db migration file.
Expand Down
26 changes: 26 additions & 0 deletions docs/source/admin/traffic_router.rst
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,32 @@ The following needs to be completed for Steering to work correctly:

.. seealso:: For more information see :ref:`steering-qht`.

Client-Controlled Steering
--------------------------
While normally this would refer to "CLIENT_STEERING" :term:`Delivery Service`\ s, it can also refer to methods provided to clients that can influence "Steering" behavior. There are a few ways this can be accomplished

.. _trred:

``trred``
"""""""""
When a client requests resolution from a Steering :term:`Delivery Service`, they may optionally provide the ``trred`` Query Parameter - e.g. ``http://video.demo1.mycdn.ciab.test/?trred=false``. If provided, its value must be exactly ``false`` (case-insensitive) to have any effect. When this happens, Traffic Router will respond with a ``200 OK`` HTTP response (as opposed to the standard ``302 Moved Temporarily`` with associated ``Location``), and the response body can be expected to contain a list of "targets" from which the client may choose.

``X-TC-Steering-Option``
""""""""""""""""""""""""
Clients may provide an ``X-TC-Steering-Option`` header with a value set to the ``xml_id`` of the desired "target" - thus bypassing Steering behavior.

``format``/``X-TC-Format``
""""""""""""""""""""""""""
In cases where multiple "targets" are presented to the client, the formatting of the target list can be adjusted by either a Query Parameter or an HTTP header. In the case of the ``format`` query parameter, the only permissible value is ``json`` (case-sensitive). Failure to provide exactly that value will result in the parameter being ignored entirely - the response will be the same as if the parameter were not provided at all. e.g. ``http://video.demo1.mycdn.ciab.test/?format=json`` will result in a JSON-encoded response, while ``http://video.demo1.mycdn.ciab.test?format=yaml`` is ignored entirely.

In the case of the ``X-TC-Format`` header, exactly two values are permissible: ``json`` and its MIME-Type equivalent :mimetype:`application/json`.

.. note:: ``format`` and ``X-TC-Format`` only make sense on STEERING :term:`Delivery Service`\ s (since a CLIENT_STEERING :term:`Delivery Service` already provides the same information in the same encoding). Furthermore, they will cause Traffic Router to return a ``200 OK`` response (similar to :ref:`trred`'s behavior) containing *a single target* that would otherwise appear in a ``Location`` header. The only difference between ``format``/``X-TC-Format`` and :ref:`trred` is that the former will return only a single target while the latter returns all available targets as an array.

.. warning:: While not strictly deprecated, it is recommended that, where possible, developers disregard the Query Parameter in favor of the HTTP header. This is because ``format`` is a fairly typical query parameter that may be used by origins (and JSON is an immensely popular encoding format), so using it may cause unintended side-effects.

.. tip:: When using the :ref:`trred` Query Parameter, it is not necessary to use either ``format`` or ``X-TC-Format``; the response will be JSON-encoded by default.

HTTPS for HTTP Delivery Services
================================
.. versionadded:: 1.7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@
import java.util.Map;
import java.util.Set;

@SuppressWarnings("PMD.AvoidDuplicateLiterals")
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";
public static final String FORMAT_HEADER = "X-TC-Format";
private static final String HEAD = "HEAD";
public static final String GET = "GET";

@Autowired
private TrafficRouterManager trafficRouterManager;
Expand Down Expand Up @@ -161,9 +164,11 @@ private void setMultiResponse(final HTTPRouteResult routeResult, final HttpServl
}
}

@SuppressWarnings("PMD.CyclomaticComplexity")
private void setSingleResponse(final HTTPRouteResult routeResult, final HttpServletRequest httpServletRequest, final HttpServletResponse response, final HTTPAccessRecord.Builder httpAccessRecordBuilder) throws IOException {
final String redirect = httpServletRequest.getParameter(REDIRECT_QUERY_PARAM);
final String format = httpServletRequest.getParameter("format");
final String formatHdr = httpServletRequest.getHeader(FORMAT_HEADER);
final URL location = routeResult.getUrl();

if (routeResult.getDeliveryService() != null) {
Expand All @@ -183,6 +188,29 @@ private void setSingleResponse(final HTTPRouteResult routeResult, final HttpServ
}

httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_OK);

} else if (formatHdr != null) {
if ("json".equals(formatHdr) || "application/json".equals(formatHdr)) {
if (HEAD.equals(httpServletRequest.getMethod()) || GET.equals(httpServletRequest.getMethod())) {
response.setContentType("application/json");
final String resp = routeResult.toMultiLocationJSONString();
if (!HEAD.equals(httpServletRequest.getMethod())) {
response.getWriter().println(resp);
httpAccessRecordBuilder.responseURLs(routeResult.getUrls());
} else {
response.setHeader("Content-Length", Integer.toString(resp.length()));
}
httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_OK);
} else {
response.setHeader("Allow", GET + ',' + HEAD);
httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
} else {
response.setContentType("text/plain");
response.getWriter().println("Unsupported target format: " + formatHdr);
httpAccessRecordBuilder.responseCode(HttpServletResponse.SC_NOT_ACCEPTABLE);
}

} else if ("json".equals(format)) {
if (!HEAD.equals(httpServletRequest.getMethod())) {
response.setContentType("application/json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,60 @@ public void itDoesUseLocationFormatResponse() throws IOException, InterruptedExc
}
}

@Test
public void itDoesUseLocationFormatHeader() throws IOException, InterruptedException {

// queryparam-compatible format
HttpGet httpGet = new HttpGet("http://localhost:" + routerHttpPort + "/stuff?fakeClientIpAddress=12.34.56.78");
httpGet.addHeader("Host", "tr." + deliveryServiceId + ".bar");
httpGet.addHeader("X-TC-Format", "json");
CloseableHttpResponse response = null;

try {
response = httpClient.execute(httpGet);
assert response.getStatusLine().getStatusCode() == 200;

HttpEntity entity = response.getEntity();

assert entity.getContent() != null;
System.out.println(entity.getContent());

JsonNode json = (new ObjectMapper(new JsonFactory())).readTree(entity.getContent());
System.out.println(json);

assert json.has("location");
assert validLocations.contains(json.get("location").asText());
assert json.get("location").asText().startsWith("http://");
} finally {
if (response != null) response.close();
}

// MIME-type format
httpGet = new HttpGet("http://localhost:" + routerHttpPort + "/stuff?fakeClientIpAddress=12.34.56.78");
httpGet.addHeader("Host", "tr." + deliveryServiceId + ".bar");
httpGet.addHeader("X-TC-Format", "application/json");
response = null;

try {
response = httpClient.execute(httpGet);
assert response.getStatusLine().getStatusCode() == 200;

HttpEntity entity = response.getEntity();

assert entity.getContent() != null;
System.out.println(entity.getContent());

JsonNode json = (new ObjectMapper(new JsonFactory())).readTree(entity.getContent());
System.out.println(json);

assert json.has("location");
assert validLocations.contains(json.get("location").asText());
assert json.get("location").asText().startsWith("http://");
} finally {
if (response != null) response.close();
}
}

@Test
public void itDoesNotUseLocationFormatResponseForHead() throws IOException, InterruptedException {
HttpHead httpHead = new HttpHead("http://localhost:" + routerHttpPort + "/stuff?fakeClientIpAddress=12.34.56.78&format=json");
Expand Down