Skip to content

Commit

Permalink
Add Cacheable URLs for FeatureModFiles (#638)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheikah45 committed Aug 13, 2022
1 parent 8bfb582 commit e1b6edc
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.test.context.jdbc.Sql;

import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesRegex;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
Expand All @@ -24,7 +26,10 @@ public void featuredModFileUrlCorrectWithLobbyScope() throws Exception {
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(1)))
.andExpect(jsonPath("$.data[0].type", is("featuredModFile")))
.andExpect(jsonPath("$.data[0].attributes.url", matchesRegex(".*\\?verify=[0-9]+-.*")));
.andExpect(jsonPath("$.data[0].attributes", hasKey("hmacParameter")))
.andExpect(jsonPath("$.data[0].attributes.url", matchesRegex(".*\\?verify=[0-9]+-.*")))
.andExpect(jsonPath("$.data[0].attributes.cacheableUrl", not(matchesRegex(".*\\?.*"))))
.andExpect(jsonPath("$.data[0].attributes.hmacToken", matchesRegex("[0-9]+-.*")));
}

@Test
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/faforever/api/cloudflare/CloudflareService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.faforever.api.cloudflare;

import com.faforever.api.config.FafApiProperties;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Base64;

@Service
public class CloudflareService {

private static final String HMAC_SHA256 = "HmacSHA256";

private final Mac mac = Mac.getInstance(HMAC_SHA256);

public CloudflareService(FafApiProperties fafApiProperties) throws NoSuchAlgorithmException, InvalidKeyException {
String secret = fafApiProperties.getCloudflare().getHmacSecret();
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
}

/**
* Builds hmac token for cloudflare firewall verification as specified
* <a href="https://support.cloudflare.com/hc/en-us/articles/115001376488-Configuring-Token-Authentication">here</a>
* @param uri uri to generate the hmac token for
* @return string representing the hmac token formatted as {timestamp}-{hashedContent}
*/
public String generateCloudFlareHmacToken(String uri) {
return generateCloudFlareHmacToken(URI.create(uri));
}

/**
* Builds hmac token for cloudflare firewall verification as specified
* <a href="https://support.cloudflare.com/hc/en-us/articles/115001376488-Configuring-Token-Authentication">here</a>
* @param uri uri to generate the hmac token for
* @return string representing the hmac token formatted as {timestamp}-{hashedContent}
*/
public String generateCloudFlareHmacToken(URI uri) {
long timeStamp = Instant.now().getEpochSecond();

byte[] macMessage = (uri.getPath() + timeStamp).getBytes(StandardCharsets.UTF_8);

String hmacEncoded = URLEncoder.encode(new String(Base64.getEncoder().encode(mac.doFinal(macMessage)), StandardCharsets.UTF_8), StandardCharsets.UTF_8);
return "%d-%s".formatted(timeStamp, hmacEncoded);
}
}
43 changes: 37 additions & 6 deletions src/main/java/com/faforever/api/featuredmods/FeaturedModFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class FeaturedModFile {
private int version;
// Enriched in FeaturedModFileEnricher
private String url;
private String cacheableUrl;
private String hmacToken;
private String hmacParameter;
private String folderName;
private int fileId;

Expand Down Expand Up @@ -73,12 +76,6 @@ public int getFileId() {
return fileId;
}

@Transient
// Enriched by FeaturedModFileEnricher
public String getUrl() {
return url;
}

/**
* Returns the name of the folder on the server in which the file resides (e.g. {@code updates_faf_files}). Used by
* the {@link FeaturedModFileEnricher}.
Expand All @@ -87,4 +84,38 @@ public String getUrl() {
public String getFolderName() {
return folderName;
}

// Enriched by FeaturedModFileEnricher

/**
* URL with hmac token as query parameter for backwards compatibility with clients, cannot be cached by cloudflare
*/
@Transient
public String getUrl() {
return url;
}

/**
* URL without any query parameters so it can be cached by cloudflare
*/
@Transient
public String getCacheableUrl() {
return cacheableUrl;
}

/**
* Token to be set as header parameter for cloudflare validation
*/
@Transient
public String getHmacToken() {
return hmacToken;
}

/**
* Parameter to set the token as
*/
@Transient
public String getHmacParameter() {
return hmacParameter;
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,39 @@
package com.faforever.api.featuredmods;

import com.faforever.api.cloudflare.CloudflareService;
import com.faforever.api.config.FafApiProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.persistence.PostLoad;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Base64;

@Component
public class FeaturedModFileEnricher {

private static final String HMAC_SHA256 = "HmacSHA256";

private static FafApiProperties fafApiProperties;
private static CloudflareService cloudflareService;

@Inject
public void init(FafApiProperties fafApiProperties) {
public void init(FafApiProperties fafApiProperties, CloudflareService cloudflareService) {
FeaturedModFileEnricher.fafApiProperties = fafApiProperties;
FeaturedModFileEnricher.cloudflareService = cloudflareService;
}

@PostLoad
public void enhance(FeaturedModFile featuredModFile) throws NoSuchAlgorithmException, InvalidKeyException {
public void enhance(FeaturedModFile featuredModFile) {
String folder = featuredModFile.getFolderName();
String urlFormat = fafApiProperties.getFeaturedMod().getFileUrlFormat();
String secret = fafApiProperties.getCloudflare().getHmacSecret();
long timeStamp = Instant.now().getEpochSecond();
URI featuredModUri = URI.create(urlFormat.formatted(folder, featuredModFile.getOriginalFileName()));
String urlString = urlFormat.formatted(folder, featuredModFile.getOriginalFileName());

// Builds hmac token for cloudflare firewall verification as specified at
// https://support.cloudflare.com/hc/en-us/articles/115001376488-Configuring-Token-Authentication
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
byte[] macMessage = (featuredModUri.getPath() + timeStamp).getBytes(StandardCharsets.UTF_8);
String hmacToken = cloudflareService.generateCloudFlareHmacToken(urlString);
String hmacParam = fafApiProperties.getCloudflare().getHmacParam();

String hmacEncoded = URLEncoder.encode(new String(Base64.getEncoder().encode(mac.doFinal(macMessage)), StandardCharsets.UTF_8), StandardCharsets.UTF_8);
String parameterValue = "%d-%s".formatted(timeStamp, hmacEncoded);

String queryParam = fafApiProperties.getCloudflare().getHmacParam();
featuredModFile.setUrl(UriComponentsBuilder.fromUri(featuredModUri).queryParam(queryParam, parameterValue).build().toString());
featuredModFile.setUrl(UriComponentsBuilder.fromUriString(urlString).queryParam(hmacParam, hmacToken).build().toString());
featuredModFile.setCacheableUrl(urlString);
featuredModFile.setHmacToken(hmacToken);
featuredModFile.setHmacParameter(hmacParam);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.yahoo.elide.jsonapi.models.JsonApiDocument;
import com.yahoo.elide.jsonapi.models.Resource;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -23,14 +24,11 @@

@RestController
@RequestMapping(path = "/featuredMods")
@RequiredArgsConstructor
public class FeaturedModsController {

private final FeaturedModService featuredModService;

public FeaturedModsController(FeaturedModService featuredModService) {
this.featuredModService = featuredModService;
}

@RequestMapping(path = "/{modId}/files/{version}")
@ApiOperation("Lists the required files for a specific featured mod version")
@PreAuthorize("hasScope('" + OAuthScope._LOBBY + "')")
Expand Down Expand Up @@ -61,6 +59,9 @@ private Function<FeaturedModFile, Resource> modFileMapper() {
"md5", file.getMd5(),
"name", file.getName(),
"url", file.getUrl(),
"cacheableUrl", file.getCacheableUrl(),
"hmacToken", file.getHmacToken(),
"hmacParameter", file.getHmacParameter(),
"version", file.getVersion()
), null, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.faforever.api.cloudflare;

import com.faforever.api.config.FafApiProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(MockitoExtension.class)
public class CloudflareServiceTest {

private static final String HMAC_SHA256 = "HmacSHA256";

private FafApiProperties fafApiProperties = new FafApiProperties();
private CloudflareService instance;

@BeforeEach
public void setup() throws Exception {
String secret = "foo";
fafApiProperties.getCloudflare().setHmacSecret(secret);

instance = new CloudflareService(fafApiProperties);
}

@Test
public void hmacTokenGeneration() throws Exception {
String token = instance.generateCloudFlareHmacToken("http://example.com/bar");

String[] tokenParts = token.split("-");
String timeStamp = tokenParts[0];

Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(fafApiProperties.getCloudflare().getHmacSecret().getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
byte[] macMessage = ("/bar" + timeStamp).getBytes(StandardCharsets.UTF_8);

String hmacEncoded = URLEncoder.encode(new String(Base64.getEncoder().encode(mac.doFinal(macMessage)), StandardCharsets.UTF_8), StandardCharsets.UTF_8);
assertEquals(hmacEncoded, tokenParts[1]);
}
}

0 comments on commit e1b6edc

Please sign in to comment.