Skip to content
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
3 changes: 3 additions & 0 deletions docs/source/admin/traffic_router.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ For the most part, the configuration files and :term:`Parameters` used by Traffi
| web.xml | various parameters | Default settings for all Web Applications running in the Traffic Router instance | N/A |
| | | of Tomcat | |
+----------------------------+-------------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------+
| users.properties | API users credentials | Users allowed to access /crs/consistenthash/patternbased/regex and | N/A |
| | | /crs/consistenthash/patternbased/deliveryservice | |
+----------------------------+-------------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------+

.. _tr-profile:

Expand Down
3 changes: 2 additions & 1 deletion docs/source/development/traffic_router.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ To install the Traffic Router Developer environment:
* copy :file:`core/src/main/conf/log4j2.xml` to :file:`core/src/test/conf/`
* copy :file:`core/src/main/conf/traffic_monitor.properties` to :file:`core/src/test/conf/` and then edit the ``traffic_monitor.bootstrap.hosts`` property
* copy :file:`core/src/main/conf/traffic_ops.properties` to :file:`core/src/test/conf/` and then edit the credentials as appropriate for the Traffic Ops instance you will be using.
* copy :file:`core/src/main/conf/users.properties` to :file:`core/src/test/conf/` and then edit the credentials as appropriate for the users you will be using for relevant TR apis.
* Default configuration values now reside in :file:`core/src/main/webapp/WEB-INF/applicationContext.xml`

.. note:: These values may be overridden by creating and/or modifying the property files listed in :file:`core/src/main/resources/applicationProperties.xml`
Expand Down Expand Up @@ -316,4 +317,4 @@ API
:hidden:
:maxdepth: 1

traffic_router/traffic_router_api
traffic_router/traffic_router_api
2 changes: 2 additions & 0 deletions traffic_router/core/src/main/conf/users.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
admin:admin123
apj:jpa@3
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.apache.traffic_control.traffic_router.core.security;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Base64;

public class ApiAuthFilter implements Filter {

private File userFile;
private final Map<String, String> users = new HashMap<>();
private long lastModified = 0;

public void setUserFile(final File userFile) {
this.userFile = userFile;
}

@SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes")
private synchronized void loadUsersIfModified() {
if (userFile.lastModified() > lastModified) {
final Map<String, String> tempUsers = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader(userFile))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty() && line.contains(":")) {
final String[] parts = line.split(":", 2);
tempUsers.put(parts[0], parts[1]);
}
}
users.clear();
users.putAll(tempUsers);
lastModified = userFile.lastModified();
} catch (IOException e) {
throw new RuntimeException("Failed to load users from file: " + userFile.getAbsolutePath(), e);
}
}
}

@Override
public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
throws IOException, ServletException {

final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

loadUsersIfModified();

final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Basic ")) {
response.setHeader("WWW-Authenticate", "Basic realm=\"TrafficRouter\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

final String base64Credentials = authHeader.substring("Basic ".length());
final String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
final String[] values = credentials.split(":", 2);
final String username = values[0];
final String password = values.length > 1 ? values[1] : "";

if (!users.containsKey(username) || !users.get(username).equals(password)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

chain.doFilter(req, res);
}

@Override public void init(final FilterConfig filterConfig) {}
@Override public void destroy() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@
<property name="password" value="$[traffic_ops.password]" />
</bean>

<bean id="authFilter" class="org.apache.traffic_control.traffic_router.core.security.ApiAuthFilter">
<property name="userFile" value="file:$[deploy.dir:/opt/traffic_router]/conf/users.properties"/>
</bean>

<bean id="maxmindGeolocationService" class="org.apache.traffic_control.traffic_router.core.loc.MaxmindGeolocationService"/>
<bean id="anonymousIpDatabaseService" class="org.apache.traffic_control.traffic_router.core.loc.AnonymousIpDatabaseService"/>

Expand Down
11 changes: 11 additions & 0 deletions traffic_router/core/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,19 @@
<url-pattern>/*</url-pattern>
</servlet-mapping>

<filter>
<filter-name>authFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>authFilter</filter-name>
<url-pattern>/crs/consistenthash/patternbased/regex</url-pattern>
</filter-mapping>

<servlet-mapping>
<servlet-name>status</servlet-name>
<url-pattern>/crs/*</url-pattern>
</servlet-mapping>

</web-app>
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;


import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClients;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.InputStreamReader;

@Category(ExternalTest.class)
public class ConsistentHashTest {
private CloseableHttpClient closeableHttpClient;
Expand All @@ -55,6 +68,21 @@ public class ConsistentHashTest {
String consistentHashRegex;
List<String> steeredDeliveryServices = new ArrayList<String>();

private String[] readCredentials() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("users.properties")) {
if (inputStream == null) {
throw new RuntimeException("test-creds.txt not found in classpath");
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line = reader.readLine();
if (line == null || !line.contains(":")) {
throw new RuntimeException("Invalid format in test-creds.txt, expected format 'username:password'");
}
return line.split(":", 2);
}
}
}

@Before
public void before() throws Exception {
closeableHttpClient = HttpClientBuilder.create().build();
Expand All @@ -78,6 +106,7 @@ public void before() throws Exception {

resourcePath = "publish/CrConfig.json";
inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);

if (inputStream == null) {
fail("Could not find file '" + resourcePath + "' needed for test from the current classpath as a resource!");
}
Expand Down Expand Up @@ -266,7 +295,7 @@ public void itUsesBypassFiltersWithDeliveryServiceSteering() throws Exception {
}

@Test
public void itUsesRegexToStandardizeRequestPath() throws Exception {
public void itUsesRegexToStandardizeRequestPathWithoutCreds() throws Exception {
CloseableHttpResponse response = null;

try {
Expand All @@ -276,22 +305,48 @@ public void itUsesRegexToStandardizeRequestPath() throws Exception {

response = closeableHttpClient.execute(httpGet);

assertThat("Expected to get 200 response from /consistenthash/patternbased/regex endpoint", response.getStatusLine().getStatusCode(), equalTo(200));
assertThat("Expected to get 401 response from /consistenthash/patternbased/regex endpoint", response.getStatusLine().getStatusCode(), equalTo(401));
} finally {
if (response != null) response.close();
}
}

@Test
public void itUsesRegexToStandardizeRequestPathWithCreds() throws Exception {
String[] creds = readCredentials();
String username = creds[0];
String password = creds[1];

CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));

try (CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultCredentialsProvider(credsProvider)
.build()) {

ObjectMapper objectMapper = new ObjectMapper(new JsonFactory());
JsonNode resp = objectMapper.readTree(EntityUtils.toString(response.getEntity()));
String resultingPathToConsistentHash = resp.get("resultingPathToConsistentHash").asText();

requestPath = URLEncoder.encode("/other/path/other_thing.m3u8", "UTF-8");
httpGet = new HttpGet("http://localhost:3333/crs/consistenthash/patternbased/regex?regex=" + encodedConsistentHashRegex + "&requestPath=" + requestPath);
String requestPath = URLEncoder.encode("/some/path/thing.m3u8", "UTF-8");
String encodedConsistentHashRegex = URLEncoder.encode(consistentHashRegex, "UTF-8");

response = closeableHttpClient.execute(httpGet);
String baseUrl = "http://localhost:3333/crs/consistenthash/patternbased/regex";
String url = baseUrl + "?regex=" + encodedConsistentHashRegex + "&requestPath=" + requestPath;

resp = objectMapper.readTree(EntityUtils.toString(response.getEntity()));
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
assertThat("Expected 200 response", response.getStatusLine().getStatusCode(), equalTo(200));
JsonNode resp = objectMapper.readTree(EntityUtils.toString(response.getEntity()));
String resultingPathToConsistentHash = resp.get("resultingPathToConsistentHash").asText();

assertThat(JsonUtils.optString(resp, "resultingPathToConsistentHash"),equalTo(resultingPathToConsistentHash));
} finally {
if (response != null) response.close();
requestPath = URLEncoder.encode("/other/path/other_thing.m3u8", "UTF-8");
url = baseUrl + "?regex=" + encodedConsistentHashRegex + "&requestPath=" + requestPath;

HttpGet secondRequest = new HttpGet(url);
try (CloseableHttpResponse response2 = httpClient.execute(secondRequest)) {
JsonNode secondResp = objectMapper.readTree(EntityUtils.toString(response2.getEntity()));
assertThat(secondResp.get("resultingPathToConsistentHash").asText(), equalTo(resultingPathToConsistentHash));
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions traffic_router/core/src/test/resources/users.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
admin:secret
Loading