Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public <T> T getMemberValue(Schema member) {
"name" : "Toucan",
"color" : "red"
}""";
assertThat(result, equalTo(expectedFormat));
assertThat(result.replace("\r", ""), equalTo(expectedFormat));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package software.amazon.smithy.java.core.schema;

import java.util.concurrent.atomic.AtomicInteger;
import software.amazon.smithy.model.traits.CorsTrait;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.DocumentationTrait;
import software.amazon.smithy.model.traits.EndpointTrait;
Expand Down Expand Up @@ -102,6 +103,7 @@ protected TraitKey<?> computeValue(Class<?> clazz) {
public static final TraitKey<XmlAttributeTrait> XML_ATTRIBUTE_TRAIT = TraitKey.get(XmlAttributeTrait.class);
public static final TraitKey<XmlFlattenedTrait> XML_FLATTENED_TRAIT = TraitKey.get(XmlFlattenedTrait.class);
public static final TraitKey<XmlNamespaceTrait> XML_NAMESPACE_TRAIT = TraitKey.get(XmlNamespaceTrait.class);
public static final TraitKey<CorsTrait> CORS_TRAIT = TraitKey.get(CorsTrait.class);

private final Class<T> traitClass;
final int id;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.server.core;

import static software.amazon.smithy.java.core.schema.TraitKey.CORS_TRAIT;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public final class CorsHeaders {
Comment thread
dpacheco-groupieticket marked this conversation as resolved.

private CorsHeaders() {}

private static final Map<String, List<String>> BASE_CORS_HEADERS = Map.of(
"Access-Control-Allow-Methods",
List.of("GET, POST, PUT, DELETE, OPTIONS"),
"Access-Control-Allow-Headers",
List.of("*,Access-Control-Allow-Headers,Access-Control-Allow-Methods,Access-Control-Allow-Origin,Amz-Sdk-Invocation-Id,Amz-Sdk-Request,Authorization,Content-Length,Content-Type,X-Amz-User-Agent,X-Amzn-Trace-Id"),
"Access-Control-Max-Age",
List.of("600"));

public static void addCorsHeaders(HttpJob job) {
if (!shouldAddCorsHeaders(job)) {
return;
}

String requestOrigin = job.request().headers().firstValue("origin");
String configuredOrigin = getConfiguredOrigin(job);

if (!isOriginAllowed(configuredOrigin, requestOrigin)) {
return;
}

job.response().headers().putHeaders(BASE_CORS_HEADERS);
job.response().headers().putHeader("Access-Control-Allow-Origin", List.of(requestOrigin));
}

private static boolean shouldAddCorsHeaders(HttpJob job) {
if (job.operation().getApiOperation().service() == null ||
job.operation().getApiOperation().service().schema() == null
||
!job.operation().getApiOperation().service().schema().hasTrait(CORS_TRAIT)) {
return false;
}
return job.request().headers().hasHeader("origin");
}

private static String getConfiguredOrigin(HttpJob job) {
var corsTrait = job.operation()
.getApiOperation()
.service()
.schema()
.getTrait(CORS_TRAIT);
if (corsTrait != null) {
return corsTrait.getOrigin();
}
return null;
}

private static boolean isOriginAllowed(String configuredOrigin, String requestOrigin) {
if (configuredOrigin == null || requestOrigin == null) {
return false;
}

if (configuredOrigin.equals("*")) {
return true;
}

return Arrays.stream(configuredOrigin.split(","))
.map(String::trim)
.anyMatch(origin -> origin.equalsIgnoreCase(requestOrigin));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.server.core;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.smithy.java.core.schema.ApiService;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.http.api.HttpHeaders;
import software.amazon.smithy.java.http.api.ModifiableHttpHeaders;
import software.amazon.smithy.java.server.Operation;
import software.amazon.smithy.model.traits.CorsTrait;

public class CorsHeadersTest {

record TestCase(
String name,
String configuredOrigin,
String requestOrigin,
boolean shouldHaveHeaders,
String expectedAllowOrigin) {}

private static Stream<TestCase> corsTestCases() {
return Stream.of(
new TestCase(
"No request origin header",
"http://allowed-origin.com",
null,
false,
null),
new TestCase(
"Not allowed origin",
"http://allowed-origin.com",
"http://not-allowed-origin.com",
false,
null),
new TestCase(
"Multiple allowed origins",
"http://allowed-origin.com,http://other-allowed-origin.com",
"http://allowed-origin.com",
true,
"http://allowed-origin.com"),
new TestCase(
"Wildcard origin",
"*",
"http://any-origin.com",
true,
"http://any-origin.com"),
new TestCase(
"Exact match origin",
"http://allowed-origin.com",
"http://allowed-origin.com",
true,
"http://allowed-origin.com"));
}

@ParameterizedTest(name = "{0}")
@MethodSource("corsTestCases")
void testCorsHeaders(TestCase testCase) throws URISyntaxException {
// Create operation with configured CORS origin
Operation testOperation = Operation.of(
"TestOperation",
(input, context) -> new TestStructs.TestOutput(),
new TestStructs.TestApiOperation(new ApiService() {
@Override
public Schema schema() {
return Schema.structureBuilder(
CorsTrait.ID,
CorsTrait.builder().origin(testCase.configuredOrigin).build()).build();
}
}),
new TestStructs.TestService());

// Create request headers
Map<String, List<String>> headerMap =
testCase.requestOrigin != null ? Map.of("Origin", List.of(testCase.requestOrigin)) : Map.of();
HttpHeaders requestHeaders = HttpHeaders.of(headerMap);

// Create request
HttpRequest request = new HttpRequest(
requestHeaders,
new URI("http://test.com"),
"GET");

// Create response and job
HttpResponse response = new HttpResponse(new TestStructs.TestModifiableHttpHeaders());
ServerProtocol protocol = new TestStructs.TestServerProtocol(List.of());
HttpJob job = new HttpJob(testOperation, protocol, request, response);

// Apply CORS headers
CorsHeaders.addCorsHeaders(job);

// Verify headers
if (testCase.shouldHaveHeaders) {
assertContainsHeader(job.response().headers(), testCase.expectedAllowOrigin);
} else {
assertDoNotContainsHeader(job.response().headers());
}
}

private void assertContainsHeader(ModifiableHttpHeaders responseHeaders, String allowedOrigin) {
assertTrue(responseHeaders.hasHeader("Access-Control-Allow-Methods"));
assertTrue(responseHeaders.hasHeader("Access-Control-Allow-Headers"));
assertTrue(responseHeaders.hasHeader("Access-Control-Max-Age"));
assertTrue(responseHeaders.hasHeader("Access-Control-Allow-Origin"));
assertTrue(responseHeaders.allValues("Access-Control-Allow-Origin").contains(allowedOrigin));
}

private void assertDoNotContainsHeader(ModifiableHttpHeaders responseHeaders) {
assertFalse(responseHeaders.hasHeader("Access-Control-Allow-Methods"));
assertFalse(responseHeaders.hasHeader("Access-Control-Allow-Headers"));
assertFalse(responseHeaders.hasHeader("Access-Control-Max-Age"));
assertFalse(responseHeaders.hasHeader("Access-Control-Allow-Origin"));
}
}
Loading