From 5b54ef4f855be4b8e88e83a0b451832fb94db495 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 30 Oct 2019 10:38:26 -0400 Subject: [PATCH 001/100] New module for CORS support in MP. Basic wiring of filters and new annotation. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/cors/pom.xml | 62 ++++++++++++ .../microprofile/cors/CrossOrigin.java | 81 ++++++++++++++++ .../cors/CrossOriginAutoDiscoverable.java | 33 +++++++ .../microprofile/cors/CrossOriginFilter.java | 97 +++++++++++++++++++ .../microprofile/cors/package-info.java | 20 ++++ .../cors/src/main/java9/module-info.java | 29 ++++++ ...sfish.jersey.internal.spi.AutoDiscoverable | 17 ++++ .../microprofile/cors/CrossOriginTest.java | 90 +++++++++++++++++ microprofile/pom.xml | 1 + 9 files changed, 430 insertions(+) create mode 100644 microprofile/cors/pom.xml create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java create mode 100644 microprofile/cors/src/main/java9/module-info.java create mode 100644 microprofile/cors/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable create mode 100644 microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml new file mode 100644 index 00000000000..c754e690f87 --- /dev/null +++ b/microprofile/cors/pom.xml @@ -0,0 +1,62 @@ + + + + + 4.0.0 + + io.helidon.microprofile + helidon-microprofile-project + 1.3.2-SNAPSHOT + + + helidon-microprofile-cors + Helidon Microprofile CORS + + + + javax.ws.rs + javax.ws.rs-api + provided + + + io.helidon.jersey + helidon-jersey-common + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-jersey + + + io.helidon.microprofile.bundles + internal-test-libs + test + + + io.helidon.microprofile.server + helidon-microprofile-server + test + + + \ No newline at end of file diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java new file mode 100644 index 00000000000..5ec3a6d5aed --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import javax.ws.rs.HttpMethod; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Documented +public @interface CrossOrigin { + + String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + + String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + /** + * A list of origins that are allowed such as {@code "http://foo.com"} or + * {@code "*"} to allow all origins. Corresponds to header {@code + * Access-Control-Allow-Origin}. + */ + String[] value() default {"*"}; + + /** + * A list of request headers that are allowed or {@code "*"} to allow all headers. + * Corresponds to {@code Access-Control-Allow-Headers}. + */ + String[] allowHeaders() default {"*"}; + + /** + * A list of response headers allowed for clients other than the "standard" + * ones. Corresponds to {@code Access-Control-Expose-Headers}. + */ + String[] exposeHeaders() default {}; + + /** + * A list of supported HTTP request methods. In response to pre-flight + * requests. Corresponds to {@code Access-Control-Allow-Methods}. + */ + HttpMethod[] allowMethods() default {}; + + /** + * Whether the client can send cookies or credentials. Corresponds to {@code + * Access-Control-Allow-Credentials}. + */ + String allowCredentials() default ""; + + /** + * Pre-flight response duration in seconds. After time expires, a new pre-flight + * request is required. Corresponds to {@code Access-Control-Max-Age}. + */ + long maxAge() default 3600; +} \ No newline at end of file diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java new file mode 100644 index 00000000000..6ac5b308fdb --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import javax.ws.rs.core.FeatureContext; + +import org.glassfish.jersey.internal.spi.AutoDiscoverable; + +/** + * Class CrossOriginAutoDiscoverable. + */ +public class CrossOriginAutoDiscoverable implements AutoDiscoverable { + + @Override + public void configure(FeatureContext context) { + // TODO: config property to enable this + context.register(CrossOriginFilter.class); + } +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java new file mode 100644 index 00000000000..27bf46055cc --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Optional; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; + +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; + +/** + * Class CrossOriginFilter. + */ +public class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { + + @Context + private ResourceInfo resourceInfo; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + lookupAnnotation(resourceInfo.getResourceClass(), resourceInfo.getResourceMethod()) + .ifPresent(crossOrigin -> { + MultivaluedMap headers = responseContext.getHeaders(); + if (!headers.containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)) { + headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, formatArray(crossOrigin.value())); + } + }); + } + + /** + * Looks up a {@code CrossOrigin} annotation in method first and then class. + * + * @param beanClass The class. + * @param method The method. + * @return Outcome of lookup. + */ + static Optional lookupAnnotation(Class beanClass, Method method) { + CrossOrigin annotation = method.getAnnotation(CrossOrigin.class); + if (annotation == null) { + annotation = beanClass.getAnnotation(CrossOrigin.class); + if (annotation == null) { + annotation = method.getDeclaringClass().getAnnotation(CrossOrigin.class); + } + } + return Optional.ofNullable(annotation); + } + + /** + * Formats an array as a comma-separate list without brackets. + * + * @param array The array. + * @param Type of elements in array. + * @return Formatted array. + */ + static String formatArray(T[] array) { + if (array.length == 0) { + return ""; + } + int i = 0; + StringBuilder builder = new StringBuilder(); + do { + builder.append(array[i++].toString()); + if (i == array.length) { + break; + } + builder.append(", "); + } while (true); + return builder.toString(); + } +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java new file mode 100644 index 00000000000..f55ed8c659d --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * CORS implementation. + */ +package io.helidon.microprofile.cors; diff --git a/microprofile/cors/src/main/java9/module-info.java b/microprofile/cors/src/main/java9/module-info.java new file mode 100644 index 00000000000..a859b7b0b82 --- /dev/null +++ b/microprofile/cors/src/main/java9/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Support for CORS. + */ +module io.helidon.microprofile.cors { + + requires transitive java.ws.rs; + requires io.helidon.jersey.common; + + exports io.helidon.microprofile.cors; + + provides org.glassfish.jersey.internal.spi.AutoDiscoverable + with io.helidon.microprofile.cors.CrossOriginAutoDiscoverable; +} diff --git a/microprofile/cors/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/microprofile/cors/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable new file mode 100644 index 00000000000..cfe20c8d84b --- /dev/null +++ b/microprofile/cors/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.microprofile.cors.CrossOriginAutoDiscoverable \ No newline at end of file diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java new file mode 100644 index 00000000000..7fdee8f7631 --- /dev/null +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import java.util.Set; + +import io.helidon.common.CollectionsHelper; +import io.helidon.microprofile.server.Server; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; + +/** + * Class CrossOriginTest. + */ +public class CrossOriginTest { + + private static Client client; + + @BeforeAll + static void initClass() { + client = ClientBuilder.newClient(); + } + + @AfterAll + static void destroyClass() { + client.close(); + } + + static public class CorsApplication extends Application { + + @Override + public Set> getClasses() { + return CollectionsHelper.setOf(CorsResource.class); + } + } + + @Path("/cors") + static public class CorsResource { + + @GET + @CrossOrigin + public String cors1() { + return "cors1"; + } + } + + @Test + void testCors() { + Server server = Server.builder() + .addApplication("/app", new CorsApplication()) + .build(); + server.start(); + + try { + WebTarget target = client.target("http://localhost:" + server.port()); + Response response = target.path("/app/cors").request().get(); + assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); + } finally { + server.stop(); + } + } +} diff --git a/microprofile/pom.xml b/microprofile/pom.xml index a9f2762165c..d1a406bbe20 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -48,5 +48,6 @@ rest-client access-log grpc + cors From 9eff239073130eada9c71ea41d96d4f85001c0d0 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 30 Oct 2019 11:28:01 -0400 Subject: [PATCH 002/100] Additional header support and tests. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOrigin.java | 4 +- .../microprofile/cors/CrossOriginFilter.java | 35 +++++++--- .../microprofile/cors/CrossOriginTest.java | 66 ++++++++++++++----- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index 5ec3a6d5aed..d8233f5fb8a 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -65,13 +65,13 @@ * A list of supported HTTP request methods. In response to pre-flight * requests. Corresponds to {@code Access-Control-Allow-Methods}. */ - HttpMethod[] allowMethods() default {}; + String[] allowMethods() default {"*"}; /** * Whether the client can send cookies or credentials. Corresponds to {@code * Access-Control-Allow-Credentials}. */ - String allowCredentials() default ""; + boolean allowCredentials() default false; /** * Pre-flight response duration in seconds. After time expires, a new pre-flight diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 27bf46055cc..0be83901a06 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -16,10 +16,6 @@ package io.helidon.microprofile.cors; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Optional; - import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -27,8 +23,16 @@ import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; import javax.ws.rs.core.MultivaluedMap; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Optional; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; /** * Class CrossOriginFilter. @@ -48,8 +52,19 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont lookupAnnotation(resourceInfo.getResourceClass(), resourceInfo.getResourceMethod()) .ifPresent(crossOrigin -> { MultivaluedMap headers = responseContext.getHeaders(); - if (!headers.containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)) { - headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, formatArray(crossOrigin.value())); + formatArray(crossOrigin.value()).ifPresent( + s -> headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, s)); + formatArray(crossOrigin.allowMethods()).ifPresent( + s -> headers.add(ACCESS_CONTROL_ALLOW_METHODS, s)); + formatArray(crossOrigin.allowHeaders()).ifPresent( + s -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, s)); + formatArray(crossOrigin.exposeHeaders()).ifPresent( + s -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, s)); + if (crossOrigin.allowCredentials()) { + headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + if (crossOrigin.maxAge() >= 0) { + headers.add(ACCESS_CONTROL_MAX_AGE, crossOrigin.maxAge()); } }); } @@ -77,11 +92,11 @@ static Optional lookupAnnotation(Class beanClass, Method method) * * @param array The array. * @param Type of elements in array. - * @return Formatted array. + * @return Formatted array as an {@code Optional}. */ - static String formatArray(T[] array) { + static Optional formatArray(T[] array) { if (array.length == 0) { - return ""; + return Optional.empty(); } int i = 0; StringBuilder builder = new StringBuilder(); @@ -92,6 +107,6 @@ static String formatArray(T[] array) { } builder.append(", "); } while (true); - return builder.toString(); + return Optional.of(builder.toString()); } } diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 7fdee8f7631..eaf1a80f971 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -17,13 +17,13 @@ package io.helidon.microprofile.cors; import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; -import javax.ws.rs.core.Response; - +import javax.ws.rs.core.MultivaluedMap; import java.util.Set; import io.helidon.common.CollectionsHelper; @@ -33,9 +33,16 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.CoreMatchers.is; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNull.nullValue; /** * Class CrossOriginTest. @@ -43,14 +50,20 @@ public class CrossOriginTest { private static Client client; + private static Server server; @BeforeAll static void initClass() { client = ClientBuilder.newClient(); + server = Server.builder() + .addApplication("/app", new CorsApplication()) + .build(); + server.start(); } @AfterAll static void destroyClass() { + server.stop(); client.close(); } @@ -67,24 +80,45 @@ static public class CorsResource { @GET @CrossOrigin + @Path("defaults") + public String defaults() { + return "defaults"; + } + + @GET + @CrossOrigin(value = {"http://foo.bar", "http://bar.foo"}, + allowHeaders = {"X-foo", "X-bar"}, + allowMethods = {HttpMethod.GET, HttpMethod.PUT}, + allowCredentials = true, + maxAge = -1) + @Path("cors1") public String cors1() { return "cors1"; } } @Test - void testCors() { - Server server = Server.builder() - .addApplication("/app", new CorsApplication()) - .build(); - server.start(); + void testCorsDefaults() { + WebTarget target = client.target("http://localhost:" + server.port()); + MultivaluedMap headers = target.path("/app/cors/defaults").request().get().getHeaders(); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("*")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("*")); + assertThat(headers.getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(nullValue())); + assertThat(headers.getFirst(ACCESS_CONTROL_EXPOSE_HEADERS), is(nullValue())); + } - try { - WebTarget target = client.target("http://localhost:" + server.port()); - Response response = target.path("/app/cors").request().get(); - assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); - } finally { - server.stop(); - } + @Test + void testCors1() { + WebTarget target = client.target("http://localhost:" + server.port()); + MultivaluedMap headers = target.path("/app/cors/cors1").request().get().getHeaders(); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), + is("http://foo.bar, http://bar.foo")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("GET, PUT")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("X-foo, X-bar")); + assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + assertThat(headers.getFirst(ACCESS_CONTROL_EXPOSE_HEADERS), is(nullValue())); + assertThat(headers.getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } } From da2de217e461c58ce8682a0f607fa16a21ffc43a Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 30 Oct 2019 13:20:32 -0400 Subject: [PATCH 003/100] Class annotations and checkstyle. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOrigin.java | 36 +++++++++++++++++-- .../cors/CrossOriginAutoDiscoverable.java | 1 - .../microprofile/cors/CrossOriginFilter.java | 7 ++-- .../microprofile/cors/CrossOriginTest.java | 10 ++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index d8233f5fb8a..35dcfe055a8 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -16,7 +16,6 @@ package io.helidon.microprofile.cors; -import javax.ws.rs.HttpMethod; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -25,57 +24,90 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; +/** + * CrossOrigin annotation. + */ @Target({TYPE, METHOD}) @Retention(RUNTIME) @Documented public @interface CrossOrigin { + /** + * Header Access-Control-Allow-Origin. + */ String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + /** + * Header Access-Control-Expose-Headers. + */ String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + /** + * Header Access-Control-Max-Age. + */ String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + /** + * Header Access-Control-Allow-Credentials. + */ String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + /** + * Header Access-Control-Allow-Methods. + */ String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + /** + * Header Access-Control-Allow-Headers. + */ String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; /** * A list of origins that are allowed such as {@code "http://foo.com"} or * {@code "*"} to allow all origins. Corresponds to header {@code * Access-Control-Allow-Origin}. + * + * @return Allowed origins. */ String[] value() default {"*"}; /** * A list of request headers that are allowed or {@code "*"} to allow all headers. * Corresponds to {@code Access-Control-Allow-Headers}. + * + * @return Allowed headers. */ String[] allowHeaders() default {"*"}; /** * A list of response headers allowed for clients other than the "standard" * ones. Corresponds to {@code Access-Control-Expose-Headers}. + * + * @return Exposed headers. */ String[] exposeHeaders() default {}; /** * A list of supported HTTP request methods. In response to pre-flight * requests. Corresponds to {@code Access-Control-Allow-Methods}. + * + * @return Allowed methods. */ String[] allowMethods() default {"*"}; /** * Whether the client can send cookies or credentials. Corresponds to {@code * Access-Control-Allow-Credentials}. + * + * @return Allowed credentials. */ boolean allowCredentials() default false; /** * Pre-flight response duration in seconds. After time expires, a new pre-flight * request is required. Corresponds to {@code Access-Control-Max-Age}. + * + * @return Max age. */ long maxAge() default 3600; -} \ No newline at end of file +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java index 6ac5b308fdb..b8fab2a7e8d 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java @@ -27,7 +27,6 @@ public class CrossOriginAutoDiscoverable implements AutoDiscoverable { @Override public void configure(FeatureContext context) { - // TODO: config property to enable this context.register(CrossOriginFilter.class); } } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 0be83901a06..3ea80fd2119 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -16,6 +16,10 @@ package io.helidon.microprofile.cors; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Optional; + import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -23,9 +27,6 @@ import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; import javax.ws.rs.core.MultivaluedMap; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Optional; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index eaf1a80f971..15f985b52f8 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -16,6 +16,8 @@ package io.helidon.microprofile.cors; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; import javax.ws.rs.GET; import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; @@ -67,19 +69,21 @@ static void destroyClass() { client.close(); } + @ApplicationScoped static public class CorsApplication extends Application { @Override public Set> getClasses() { - return CollectionsHelper.setOf(CorsResource.class); + return CollectionsHelper.setOf(CorsResource1.class); } } + @CrossOrigin + @RequestScoped @Path("/cors") - static public class CorsResource { + static public class CorsResource1 { @GET - @CrossOrigin @Path("defaults") public String defaults() { return "defaults"; From 406ecf96c98d0ce90699c9d96f6084a40ad370cb Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 31 Oct 2019 11:27:55 -0400 Subject: [PATCH 004/100] Basic support for pre-flight requests. Using @CrossOrigin with @OPTIONS for compatibility. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOrigin.java | 15 ++ .../microprofile/cors/CrossOriginFilter.java | 93 ++-------- .../microprofile/cors/CrossOriginHelper.java | 165 ++++++++++++++++++ .../microprofile/cors/CrossOriginTest.java | 72 ++++---- 4 files changed, 239 insertions(+), 106 deletions(-) create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index 35dcfe055a8..060ed964515 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -32,6 +32,21 @@ @Documented public @interface CrossOrigin { + /** + * Header Origin. + */ + String ORIGIN = "Origin"; + + /** + * Header Access-Control-Request-Method. + */ + String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + + /** + * Header Access-Control-Request-Headers. + */ + String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + /** * Header Access-Control-Allow-Origin. */ diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 3ea80fd2119..0a9145b0aed 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -16,98 +16,43 @@ package io.helidon.microprofile.cors; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Optional; - import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; -import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.microprofile.cors.CrossOriginHelper.findRequestType; +import static io.helidon.microprofile.cors.CrossOriginHelper.processPreFlight; /** * Class CrossOriginFilter. */ -public class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { +class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { @Context private ResourceInfo resourceInfo; @Override - public void filter(ContainerRequestContext requestContext) throws IOException { - } - - @Override - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) - throws IOException { - lookupAnnotation(resourceInfo.getResourceClass(), resourceInfo.getResourceMethod()) - .ifPresent(crossOrigin -> { - MultivaluedMap headers = responseContext.getHeaders(); - formatArray(crossOrigin.value()).ifPresent( - s -> headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, s)); - formatArray(crossOrigin.allowMethods()).ifPresent( - s -> headers.add(ACCESS_CONTROL_ALLOW_METHODS, s)); - formatArray(crossOrigin.allowHeaders()).ifPresent( - s -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, s)); - formatArray(crossOrigin.exposeHeaders()).ifPresent( - s -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, s)); - if (crossOrigin.allowCredentials()) { - headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - } - if (crossOrigin.maxAge() >= 0) { - headers.add(ACCESS_CONTROL_MAX_AGE, crossOrigin.maxAge()); - } - }); - } - - /** - * Looks up a {@code CrossOrigin} annotation in method first and then class. - * - * @param beanClass The class. - * @param method The method. - * @return Outcome of lookup. - */ - static Optional lookupAnnotation(Class beanClass, Method method) { - CrossOrigin annotation = method.getAnnotation(CrossOrigin.class); - if (annotation == null) { - annotation = beanClass.getAnnotation(CrossOrigin.class); - if (annotation == null) { - annotation = method.getDeclaringClass().getAnnotation(CrossOrigin.class); - } + public void filter(ContainerRequestContext requestContext) { + switch (findRequestType(requestContext)) { + case NORMAL: + // no-op + return; + case CORS: + break; + case PREFLIGHT: + Response response = processPreFlight(requestContext, resourceInfo); + requestContext.abortWith(response); + break; + default: + throw new IllegalStateException("Invalid value for enum RequestType"); } - return Optional.ofNullable(annotation); } - /** - * Formats an array as a comma-separate list without brackets. - * - * @param array The array. - * @param Type of elements in array. - * @return Formatted array as an {@code Optional}. - */ - static Optional formatArray(T[] array) { - if (array.length == 0) { - return Optional.empty(); - } - int i = 0; - StringBuilder builder = new StringBuilder(); - do { - builder.append(array[i++].toString()); - if (i == array.length) { - break; - } - builder.append(", "); - } while (true); - return Optional.of(builder.toString()); + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { } } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java new file mode 100644 index 00000000000..014bbc989e3 --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; + +/** + * Class CrossOriginHelper. + */ +class CrossOriginHelper { + + enum RequestType { + NORMAL, + CORS, + PREFLIGHT + } + + /** + * Determines the type of a request for CORS processing. + * + * @param requestContext The request context. + * @return The type of request. + */ + static RequestType findRequestType(ContainerRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + + // If no origin header or same as host, then just normal + String origin = headers.getFirst(ORIGIN); + String host = headers.getFirst(HttpHeaders.HOST); + if (origin == null || origin.contains("://" + host)) { + return RequestType.NORMAL; + } + + // Is this a pre-flight request? + if (requestContext.getMethod().equalsIgnoreCase("OPTIONS") + && headers.containsKey(ACCESS_CONTROL_REQUEST_METHOD)) { + return RequestType.PREFLIGHT; + } + + // A CORS request that is not a pre-flight one + return RequestType.CORS; + } + + /** + * Process a pre-flight request. + * + * @param requestContext The request context. + * @param resourceInfo Info about the matched resource. + * @return A response to send back to the client. + */ + static Response processPreFlight(ContainerRequestContext requestContext, ResourceInfo resourceInfo) { + MultivaluedMap headers = requestContext.getHeaders(); + + String origin = headers.getFirst(ORIGIN); + Optional crossOrigin = lookupAnnotation(resourceInfo); + + // If CORS not enabled, deny request + if (!crossOrigin.isPresent()) { + return forbidden("CORS origin is denied"); + } + + // If enabled but not whitelisted, deny request + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + if (!allowedOrigins.contains(origin) && !allowedOrigins.contains("*")) { + return forbidden("CORS origin not in allowed list"); + } + + // TODO + + return Response.ok().build(); + } + + static Response forbidden(String message) { + return Response.status(Response.Status.FORBIDDEN).entity(message).build(); + } + + /** + * Looks up a {@code CrossOrigin} annotation in method first and then class. + * + * @param resourceInfo Info about the matched resource. + * @return Outcome of lookup. + */ + static Optional lookupAnnotation(ResourceInfo resourceInfo) { + Method method = resourceInfo.getResourceMethod(); + if (method == null) { + return Optional.empty(); + } + CrossOrigin annotation = method.getAnnotation(CrossOrigin.class); + if (annotation == null) { + Class beanClass = resourceInfo.getResourceClass(); + annotation = beanClass.getAnnotation(CrossOrigin.class); + if (annotation == null) { + annotation = method.getDeclaringClass().getAnnotation(CrossOrigin.class); + } + } + return Optional.ofNullable(annotation); + } + + /** + * Formats an array as a comma-separate list without brackets. + * + * @param array The array. + * @param Type of elements in array. + * @return Formatted array as an {@code Optional}. + */ + static Optional formatHeader(T[] array) { + if (array.length == 0) { + return Optional.empty(); + } + int i = 0; + StringBuilder builder = new StringBuilder(); + do { + builder.append(array[i++].toString()); + if (i == array.length) { + break; + } + builder.append(", "); + } while (true); + return Optional.of(builder.toString()); + } + + /** + * Parse list header value as a set. + * + * @param header Header value as a list. + * @return Set of header values. + */ + static Set parseHeader(String header) { + Set result = new HashSet<>(); + StringTokenizer tokenizer = new StringTokenizer(header, " ,"); + while (tokenizer.hasMoreTokens()) { + result.add(tokenizer.nextToken().trim()); + } + return result; + } +} diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 15f985b52f8..ec37aaa8da7 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -20,12 +20,15 @@ import javax.enterprise.context.RequestScoped; import javax.ws.rs.GET; import javax.ws.rs.HttpMethod; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; import java.util.Set; import io.helidon.common.CollectionsHelper; @@ -42,6 +45,8 @@ import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsNull.nullValue; @@ -53,18 +58,24 @@ public class CrossOriginTest { private static Client client; private static Server server; + private static WebTarget target; + + static { + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + } @BeforeAll static void initClass() { - client = ClientBuilder.newClient(); server = Server.builder() .addApplication("/app", new CorsApplication()) .build(); server.start(); + client = ClientBuilder.newClient(); + target = client.target("http://localhost:" + server.port()); } @AfterAll - static void destroyClass() { + static void destroyClass() throws Exception { server.stop(); client.close(); } @@ -78,51 +89,48 @@ public Set> getClasses() { } } - @CrossOrigin @RequestScoped @Path("/cors") static public class CorsResource1 { - @GET - @Path("defaults") - public String defaults() { - return "defaults"; - } - - @GET + @OPTIONS @CrossOrigin(value = {"http://foo.bar", "http://bar.foo"}, allowHeaders = {"X-foo", "X-bar"}, allowMethods = {HttpMethod.GET, HttpMethod.PUT}, allowCredentials = true, maxAge = -1) - @Path("cors1") - public String cors1() { - return "cors1"; + public String options() { + return "options"; + } + + @GET + public String getCors() { + return "getCors"; + } + + @PUT + public String putCors() { + return "putCors"; } } @Test - void testCorsDefaults() { - WebTarget target = client.target("http://localhost:" + server.port()); - MultivaluedMap headers = target.path("/app/cors/defaults").request().get().getHeaders(); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("*")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("*")); - assertThat(headers.getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(nullValue())); - assertThat(headers.getFirst(ACCESS_CONTROL_EXPOSE_HEADERS), is(nullValue())); + void testPreflightForbidden() { + Response response = target.path("/app/cors") + .request() + .header(ORIGIN, "http://not.allowed") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); } @Test - void testCors1() { - WebTarget target = client.target("http://localhost:" + server.port()); - MultivaluedMap headers = target.path("/app/cors/cors1").request().get().getHeaders(); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), - is("http://foo.bar, http://bar.foo")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("GET, PUT")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("X-foo, X-bar")); - assertThat(headers.getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); - assertThat(headers.getFirst(ACCESS_CONTROL_EXPOSE_HEADERS), is(nullValue())); - assertThat(headers.getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); + void testPreflightAllowed() { + Response response = target.path("/app/cors") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); } } From c545adf37126789b0d1c013edd95d63756d19375 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 31 Oct 2019 11:46:18 -0400 Subject: [PATCH 005/100] Tests for request methods. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOriginHelper.java | 6 ++++ .../microprofile/cors/CrossOriginTest.java | 33 ++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index 014bbc989e3..33ddc21dc4d 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -94,6 +94,12 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc return forbidden("CORS origin not in allowed list"); } + String method = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD); + List allowedMethods = Arrays.asList(crossOrigin.get().allowMethods()); + if (!allowedMethods.contains(method) && !allowedMethods.contains("*")) { + return forbidden("CORS method not in allowed list"); + } + // TODO return Response.ok().build(); diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index ec37aaa8da7..3a70b16b955 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -27,7 +27,6 @@ import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.util.Set; @@ -38,18 +37,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; -import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; - import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsNull.nullValue; /** * Class CrossOriginTest. @@ -115,7 +106,7 @@ public String putCors() { } @Test - void testPreflightForbidden() { + void testForbiddenOrigin() { Response response = target.path("/app/cors") .request() .header(ORIGIN, "http://not.allowed") @@ -125,7 +116,27 @@ void testPreflightForbidden() { } @Test - void testPreflightAllowed() { + void testAllowedOrigin() { + Response response = target.path("/app/cors") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void testForbiddenMethod() { + Response response = target.path("/app/cors") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); + } + + @Test + void testAllowedMethod() { Response response = target.path("/app/cors") .request() .header(ORIGIN, "http://foo.bar") From ca98276f2453451c6561e2f43a49c34fdac4f386 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 31 Oct 2019 16:34:26 -0400 Subject: [PATCH 006/100] Support for other headers and more tests. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOriginHelper.java | 90 ++++++++++++- .../microprofile/cors/CrossOriginTest.java | 127 ++++++++++++++++-- 2 files changed, 202 insertions(+), 15 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index 33ddc21dc4d..5f324af96d2 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -30,6 +32,12 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; @@ -90,21 +98,75 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.get().value()); - if (!allowedOrigins.contains(origin) && !allowedOrigins.contains("*")) { + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins)) { return forbidden("CORS origin not in allowed list"); } + // Check if method is allowed String method = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD); List allowedMethods = Arrays.asList(crossOrigin.get().allowMethods()); - if (!allowedMethods.contains(method) && !allowedMethods.contains("*")) { + if (!allowedMethods.contains("*") && !contains(method, allowedMethods)) { return forbidden("CORS method not in allowed list"); } - // TODO + // Check if headers are allowed + Set requestHeaders = parseHeader(headers.get(ACCESS_CONTROL_REQUEST_HEADERS)); + List allowedHeaders = Arrays.asList(crossOrigin.get().allowHeaders()); + if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { + return forbidden("CORS headers not in allowed list"); + } + + // Build successful response + Response.ResponseBuilder builder = Response.ok(); + builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + builder.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, crossOrigin.get().allowCredentials()); + builder.header(ACCESS_CONTROL_ALLOW_METHODS, method); + builder.header(ACCESS_CONTROL_ALLOW_HEADERS, formatHeader(requestHeaders.toArray())); + long maxAge = crossOrigin.get().maxAge(); + if (maxAge > 0) { + builder.header(ACCESS_CONTROL_MAX_AGE, maxAge); + } + return builder.build(); + } - return Response.ok().build(); + /** + * Checks containment in a {@code Collection} case insensitively. + * + * @param item The string. + * @param collection The collection. + * @return Outcome of test. + */ + static boolean contains(String item, Collection collection) { + for (String s : collection) { + if (s.equalsIgnoreCase(item)) { + return true; + } + } + return false; } + /** + * Checks containment in two collections, case insensitively. + * + * @param left First collection. + * @param right Second collection. + * @return Outcome of test. + */ + static boolean contains(Collection left, Collection right) { + for (String s : left) { + if (!contains(s, right)) { + return false; + } + } + return true; + } + + /** + * Returns response with forbidden status and entity created from message. + * + * @param message Message in entity. + * @return A {@code Response} instance. + */ static Response forbidden(String message) { return Response.status(Response.Status.FORBIDDEN).entity(message).build(); } @@ -162,10 +224,26 @@ static Optional formatHeader(T[] array) { */ static Set parseHeader(String header) { Set result = new HashSet<>(); - StringTokenizer tokenizer = new StringTokenizer(header, " ,"); + StringTokenizer tokenizer = new StringTokenizer(header, ","); while (tokenizer.hasMoreTokens()) { - result.add(tokenizer.nextToken().trim()); + String value = tokenizer.nextToken().trim(); + if (value.length() > 0) { + result.add(value); + } } return result; } + + /** + * Parse a list of list of headers as a set. + * + * @param headers Header value as a list, each a potential list. + * @return Set of header values. + */ + static Set parseHeader(List headers) { + if (headers == null) { + return Collections.emptySet(); + } + return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); + } } diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 3a70b16b955..0a745b594bd 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; import static org.hamcrest.CoreMatchers.is; @@ -76,14 +77,35 @@ static public class CorsApplication extends Application { @Override public Set> getClasses() { - return CollectionsHelper.setOf(CorsResource1.class); + return CollectionsHelper.setOf(CorsResource1.class, CorsResource2.class); } } @RequestScoped - @Path("/cors") + @Path("/cors1") static public class CorsResource1 { + @OPTIONS + @CrossOrigin + public String options() { + return "options"; + } + + @GET + public String getCors() { + return "getCors"; + } + + @PUT + public String putCors() { + return "putCors"; + } + } + + @RequestScoped + @Path("/cors2") + static public class CorsResource2 { + @OPTIONS @CrossOrigin(value = {"http://foo.bar", "http://bar.foo"}, allowHeaders = {"X-foo", "X-bar"}, @@ -106,8 +128,50 @@ public String putCors() { } @Test - void testForbiddenOrigin() { - Response response = target.path("/app/cors") + void test1AllowedOrigin() { + Response response = target.path("/app/cors1") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test1AllowedMethod() { + Response response = target.path("/app/cors1") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test1AllowedHeaders1() { + Response response = target.path("/app/cors1") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test1AllowedHeaders2() { + Response response = target.path("/app/cors1") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test2ForbiddenOrigin() { + Response response = target.path("/app/cors2") .request() .header(ORIGIN, "http://not.allowed") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") @@ -117,7 +181,7 @@ void testForbiddenOrigin() { @Test void testAllowedOrigin() { - Response response = target.path("/app/cors") + Response response = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") @@ -126,8 +190,8 @@ void testAllowedOrigin() { } @Test - void testForbiddenMethod() { - Response response = target.path("/app/cors") + void test2ForbiddenMethod() { + Response response = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") @@ -136,11 +200,56 @@ void testForbiddenMethod() { } @Test - void testAllowedMethod() { - Response response = target.path("/app/cors") + void test2AllowedMethod() { + Response response = target.path("/app/cors2") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test2ForbiddenHeader() { + Response response = target.path("/app/cors2") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar, X-oops") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); + } + + @Test + void test2AllowedHeaders1() { + Response response = target.path("/app/cors2") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test2AllowedHeaders2() { + Response response = target.path("/app/cors2") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") + .options(); + assertThat(response.getStatusInfo(), is(Response.Status.OK)); + } + + @Test + void test2AllowedHeaders3() { + Response response = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") .options(); assertThat(response.getStatusInfo(), is(Response.Status.OK)); } From 7497b6972eafa7055bc14951a8daaffc647b3b8f Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 4 Nov 2019 10:23:40 -0500 Subject: [PATCH 007/100] Test headers in pre-flight responses. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOriginHelper.java | 2 +- .../microprofile/cors/CrossOriginTest.java | 112 +++++++++++------- 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index 5f324af96d2..b7a3d6a7b74 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -121,7 +121,7 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); builder.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, crossOrigin.get().allowCredentials()); builder.header(ACCESS_CONTROL_ALLOW_METHODS, method); - builder.header(ACCESS_CONTROL_ALLOW_HEADERS, formatHeader(requestHeaders.toArray())); + formatHeader(requestHeaders.toArray()).ifPresent(h -> builder.header(ACCESS_CONTROL_ALLOW_HEADERS, h)); long maxAge = crossOrigin.get().maxAge(); if (maxAge > 0) { builder.header(ACCESS_CONTROL_MAX_AGE, maxAge); diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 0a745b594bd..9ba50cfb5ba 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -37,10 +37,17 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; /** @@ -129,128 +136,153 @@ public String putCors() { @Test void test1AllowedOrigin() { - Response response = target.path("/app/cors1") + Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); - } - - @Test - void test1AllowedMethod() { - Response response = target.path("/app/cors1") - .request() - .header(ORIGIN, "http://foo.bar") - .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") - .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); } @Test void test1AllowedHeaders1() { - Response response = target.path("/app/cors1") + Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("X-foo")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); } @Test void test1AllowedHeaders2() { - Response response = target.path("/app/cors1") + Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-foo")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); } @Test void test2ForbiddenOrigin() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://not.allowed") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); + assertThat(res.getStatusInfo(), is(Response.Status.FORBIDDEN)); } @Test void testAllowedOrigin() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } @Test void test2ForbiddenMethod() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); - } - - @Test - void test2AllowedMethod() { - Response response = target.path("/app/cors2") - .request() - .header(ORIGIN, "http://foo.bar") - .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") - .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.FORBIDDEN)); } @Test void test2ForbiddenHeader() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar, X-oops") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.FORBIDDEN)); + assertThat(res.getStatusInfo(), is(Response.Status.FORBIDDEN)); } @Test void test2AllowedHeaders1() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-foo")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } @Test void test2AllowedHeaders2() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-foo")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } @Test void test2AllowedHeaders3() { - Response response = target.path("/app/cors2") + Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") .header(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar") .options(); - assertThat(response.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-foo")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), + containsString("X-bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } } From 2d4de2a43b669d20d3cd902fa915963d7d6e7472 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 4 Nov 2019 14:18:31 -0500 Subject: [PATCH 008/100] Restricted @CrossOrigin to methods only (OPTIONS really). Process other types of requests. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOrigin.java | 3 +- .../microprofile/cors/CrossOriginFilter.java | 28 +++-- .../microprofile/cors/CrossOriginHelper.java | 104 +++++++++++++++--- .../microprofile/cors/CrossOriginTest.java | 48 +++++--- 4 files changed, 144 insertions(+), 39 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index 060ed964515..341d6adba10 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -21,13 +21,12 @@ import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * CrossOrigin annotation. */ -@Target({TYPE, METHOD}) +@Target(METHOD) @Retention(RUNTIME) @Documented public @interface CrossOrigin { diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 0a9145b0aed..9a8ab770247 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -16,6 +16,8 @@ package io.helidon.microprofile.cors; +import java.util.Optional; + import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -24,7 +26,12 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; +import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.CORS; +import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.NORMAL; +import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.PREFLIGHT; import static io.helidon.microprofile.cors.CrossOriginHelper.findRequestType; +import static io.helidon.microprofile.cors.CrossOriginHelper.processCorsRequest; +import static io.helidon.microprofile.cors.CrossOriginHelper.processCorsResponse; import static io.helidon.microprofile.cors.CrossOriginHelper.processPreFlight; /** @@ -37,22 +44,25 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { - switch (findRequestType(requestContext)) { - case NORMAL: - // no-op - return; - case CORS: - break; - case PREFLIGHT: + CrossOriginHelper.RequestType type = findRequestType(requestContext); + if (type != NORMAL) { + if (type == PREFLIGHT) { Response response = processPreFlight(requestContext, resourceInfo); requestContext.abortWith(response); - break; - default: + } else if (type == CORS) { + Optional response = processCorsRequest(requestContext, resourceInfo); + response.ifPresent(requestContext::abortWith); + } else { throw new IllegalStateException("Invalid value for enum RequestType"); + } } } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + CrossOriginHelper.RequestType type = findRequestType(requestContext); + if (type == CORS) { + processCorsResponse(requestContext, responseContext, resourceInfo); + } } } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index b7a3d6a7b74..f6cd08c557b 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -26,7 +26,10 @@ import java.util.Set; import java.util.StringTokenizer; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.Path; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; @@ -52,6 +55,9 @@ enum RequestType { PREFLIGHT } + private CrossOriginHelper() { + } + /** * Determines the type of a request for CORS processing. * @@ -78,6 +84,59 @@ static RequestType findRequestType(ContainerRequestContext requestContext) { return RequestType.CORS; } + /** + * Process an actual CORS request. + * + * @param requestContext The request context. + * @param resourceInfo Info about the matched resource. + * @return A response to send back to the client. + */ + static Optional processCorsRequest(ContainerRequestContext requestContext, ResourceInfo resourceInfo) { + MultivaluedMap headers = requestContext.getHeaders(); + String origin = headers.getFirst(ORIGIN); + Optional crossOrigin = lookupAnnotation(resourceInfo); + + // Annotation must be present for actual request + if (!crossOrigin.isPresent()) { + return Optional.of(forbidden("CORS origin is denied")); + } + + // If enabled but not whitelisted, deny request + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins)) { + return Optional.of(forbidden("CORS origin not in allowed list")); + } + + // Succesful processing of request + return Optional.empty(); + } + + /** + * Process a CORS response. + * + * @param requestContext The request context. + * @param responseContext The response context. + * @return A response to send back to the client. + */ + static void processCorsResponse(ContainerRequestContext requestContext, ContainerResponseContext responseContext, + ResourceInfo resourceInfo) { + Optional crossOrigin = lookupAnnotation(resourceInfo); + MultivaluedMap headers = responseContext.getHeaders(); + + // Add Origin header + String origin = requestContext.getHeaders().getFirst(ORIGIN); + headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + + // Add Access-Control-Allow-Credentials + if (crossOrigin.get().allowCredentials()) { + headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + + // Add Access-Control-Allow-Headers if non-empty + formatHeader(crossOrigin.get().exposeHeaders()).ifPresent( + h -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, h)); + } + /** * Process a pre-flight request. * @@ -119,9 +178,12 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc // Build successful response Response.ResponseBuilder builder = Response.ok(); builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); - builder.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, crossOrigin.get().allowCredentials()); + if (crossOrigin.get().allowCredentials()) { + builder.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } builder.header(ACCESS_CONTROL_ALLOW_METHODS, method); - formatHeader(requestHeaders.toArray()).ifPresent(h -> builder.header(ACCESS_CONTROL_ALLOW_HEADERS, h)); + formatHeader(requestHeaders.toArray()).ifPresent( + h -> builder.header(ACCESS_CONTROL_ALLOW_HEADERS, h)); long maxAge = crossOrigin.get().maxAge(); if (maxAge > 0) { builder.header(ACCESS_CONTROL_MAX_AGE, maxAge); @@ -172,25 +234,37 @@ static Response forbidden(String message) { } /** - * Looks up a {@code CrossOrigin} annotation in method first and then class. + * Looks up a {@code CrossOrigin} annotation on the resource method matched first + * and if not present on a method annotated by {@code OPTIONS} on the same resource. * * @param resourceInfo Info about the matched resource. * @return Outcome of lookup. */ static Optional lookupAnnotation(ResourceInfo resourceInfo) { - Method method = resourceInfo.getResourceMethod(); - if (method == null) { - return Optional.empty(); - } - CrossOrigin annotation = method.getAnnotation(CrossOrigin.class); - if (annotation == null) { - Class beanClass = resourceInfo.getResourceClass(); - annotation = beanClass.getAnnotation(CrossOrigin.class); - if (annotation == null) { - annotation = method.getDeclaringClass().getAnnotation(CrossOrigin.class); - } + Method resourceMethod = resourceInfo.getResourceMethod(); + Class resourceClass = resourceInfo.getResourceClass(); + + CrossOrigin corsAnnot; + OPTIONS optsAnnot = resourceMethod.getAnnotation(OPTIONS.class); + if (optsAnnot != null) { + corsAnnot = resourceMethod.getAnnotation(CrossOrigin.class); + } else { + Path pathAnnot = resourceMethod.getAnnotation(Path.class); + Optional optionsMethod = Arrays.stream(resourceClass.getDeclaredMethods()) + .filter(m -> { + OPTIONS optsAnnot2 = m.getAnnotation(OPTIONS.class); + if (optsAnnot2 != null) { + if (pathAnnot != null) { + Path pathAnnot2 = m.getAnnotation(Path.class); + return pathAnnot2 != null && pathAnnot.value().equals(pathAnnot2.value()); + } + return true; + } + return false; + }).findFirst(); + corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)).orElse(null); } - return Optional.ofNullable(annotation); + return Optional.ofNullable(corsAnnot); } /** diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 9ba50cfb5ba..44c9923c7d5 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -25,8 +25,10 @@ import javax.ws.rs.Path; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Set; @@ -45,6 +47,7 @@ import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.microprofile.cors.CrossOrigin.ORIGIN; + import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; @@ -135,7 +138,7 @@ public String putCors() { } @Test - void test1AllowedOrigin() { + void test1PreFlightAllowedOrigin() { Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") @@ -143,14 +146,13 @@ void test1AllowedOrigin() { .options(); assertThat(res.getStatusInfo(), is(Response.Status.OK)); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); - assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); } @Test - void test1AllowedHeaders1() { + void test1PreFlightAllowedHeaders1() { Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") @@ -159,14 +161,13 @@ void test1AllowedHeaders1() { .options(); assertThat(res.getStatusInfo(), is(Response.Status.OK)); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); - assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is("X-foo")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); } @Test - void test1AllowedHeaders2() { + void test1PreFlightAllowedHeaders2() { Response res = target.path("/app/cors1") .request() .header(ORIGIN, "http://foo.bar") @@ -175,7 +176,6 @@ void test1AllowedHeaders2() { .options(); assertThat(res.getStatusInfo(), is(Response.Status.OK)); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); - assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("false")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS).toString(), containsString("X-foo")); @@ -185,7 +185,7 @@ void test1AllowedHeaders2() { } @Test - void test2ForbiddenOrigin() { + void test2PreFlightForbiddenOrigin() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://not.allowed") @@ -195,7 +195,7 @@ void test2ForbiddenOrigin() { } @Test - void testAllowedOrigin() { + void test2PreFlightAllowedOrigin() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -210,7 +210,7 @@ void testAllowedOrigin() { } @Test - void test2ForbiddenMethod() { + void test2PreFlightForbiddenMethod() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -220,7 +220,7 @@ void test2ForbiddenMethod() { } @Test - void test2ForbiddenHeader() { + void test2PreFlightForbiddenHeader() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -231,7 +231,7 @@ void test2ForbiddenHeader() { } @Test - void test2AllowedHeaders1() { + void test2PreFlightAllowedHeaders1() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -248,7 +248,7 @@ void test2AllowedHeaders1() { } @Test - void test2AllowedHeaders2() { + void test2PreFlightAllowedHeaders2() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -267,7 +267,7 @@ void test2AllowedHeaders2() { } @Test - void test2AllowedHeaders3() { + void test2PreFlightAllowedHeaders3() { Response res = target.path("/app/cors2") .request() .header(ORIGIN, "http://foo.bar") @@ -285,4 +285,26 @@ void test2AllowedHeaders3() { containsString("X-bar")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is(nullValue())); } + + @Test + void test1ActualAllowedOrigin() { + Response res = target.path("/app/cors1") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .put(Entity.entity("", MediaType.TEXT_PLAIN_TYPE)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + } + + @Test + void test2ActualAllowedOrigin() { + Response res = target.path("/app/cors2") + .request() + .header(ORIGIN, "http://foo.bar") + .put(Entity.entity("", MediaType.TEXT_PLAIN_TYPE)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); + } } From 76f6e1cfaa39e1109c3b3b03b21334455cf01f4b Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 5 Nov 2019 11:30:40 -0500 Subject: [PATCH 009/100] Ensure comparisons use the correct case rules. Some changes to tests. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/cors/CrossOriginHelper.java | 33 +++++++++++-------- .../microprofile/cors/CrossOriginTest.java | 18 +++++----- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index f6cd08c557b..d20e96f17b5 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; +import java.util.function.BiFunction; import javax.ws.rs.OPTIONS; import javax.ws.rs.Path; @@ -39,6 +40,7 @@ import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.microprofile.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; @@ -85,7 +87,8 @@ static RequestType findRequestType(ContainerRequestContext requestContext) { } /** - * Process an actual CORS request. + * Process an actual CORS request. Additional headers are added by {@code processCorsResponse} + * to the response. * * @param requestContext The request context. * @param resourceInfo Info about the matched resource. @@ -103,11 +106,11 @@ static Optional processCorsRequest(ContainerRequestContext requestCont // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.get().value()); - if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins)) { + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { return Optional.of(forbidden("CORS origin not in allowed list")); } - // Succesful processing of request + // Successful processing of request return Optional.empty(); } @@ -123,7 +126,8 @@ static void processCorsResponse(ContainerRequestContext requestContext, Containe Optional crossOrigin = lookupAnnotation(resourceInfo); MultivaluedMap headers = responseContext.getHeaders(); - // Add Origin header + // Add Access-Control-Allow-Origin header + // TODO: Allow any origin as "*" here String origin = requestContext.getHeaders().getFirst(ORIGIN); headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); @@ -131,10 +135,9 @@ static void processCorsResponse(ContainerRequestContext requestContext, Containe if (crossOrigin.get().allowCredentials()) { headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } - - // Add Access-Control-Allow-Headers if non-empty + // Add Access-Control-Expose-Headers if non-empty formatHeader(crossOrigin.get().exposeHeaders()).ifPresent( - h -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, h)); + h -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, h)); } /** @@ -157,14 +160,14 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.get().value()); - if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins)) { + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { return forbidden("CORS origin not in allowed list"); } // Check if method is allowed String method = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD); List allowedMethods = Arrays.asList(crossOrigin.get().allowMethods()); - if (!allowedMethods.contains("*") && !contains(method, allowedMethods)) { + if (!allowedMethods.contains("*") && !contains(method, allowedMethods, String::equals)) { return forbidden("CORS method not in allowed list"); } @@ -192,15 +195,16 @@ static Response processPreFlight(ContainerRequestContext requestContext, Resourc } /** - * Checks containment in a {@code Collection} case insensitively. + * Checks containment in a {@code Collection}. * * @param item The string. * @param collection The collection. + * @param eq Equality function. * @return Outcome of test. */ - static boolean contains(String item, Collection collection) { + static boolean contains(String item, Collection collection, BiFunction eq) { for (String s : collection) { - if (s.equalsIgnoreCase(item)) { + if (eq.apply(item, s)) { return true; } } @@ -216,7 +220,7 @@ static boolean contains(String item, Collection collection) { */ static boolean contains(Collection left, Collection right) { for (String s : left) { - if (!contains(s, right)) { + if (!contains(s, right, String::equalsIgnoreCase)) { return false; } } @@ -297,6 +301,9 @@ static Optional formatHeader(T[] array) { * @return Set of header values. */ static Set parseHeader(String header) { + if (header == null) { + return Collections.emptySet(); + } Set result = new HashSet<>(); StringTokenizer tokenizer = new StringTokenizer(header, ","); while (tokenizer.hasMoreTokens()) { diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 44c9923c7d5..be204500b8c 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -18,7 +18,7 @@ import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.RequestScoped; -import javax.ws.rs.GET; +import javax.ws.rs.DELETE; import javax.ws.rs.HttpMethod; import javax.ws.rs.OPTIONS; import javax.ws.rs.PUT; @@ -77,7 +77,7 @@ static void initClass() { } @AfterAll - static void destroyClass() throws Exception { + static void destroyClass() { server.stop(); client.close(); } @@ -101,9 +101,9 @@ public String options() { return "options"; } - @GET - public String getCors() { - return "getCors"; + @DELETE + public String deleteCors() { + return "deleteCors"; } @PUT @@ -119,16 +119,16 @@ static public class CorsResource2 { @OPTIONS @CrossOrigin(value = {"http://foo.bar", "http://bar.foo"}, allowHeaders = {"X-foo", "X-bar"}, - allowMethods = {HttpMethod.GET, HttpMethod.PUT}, + allowMethods = {HttpMethod.DELETE, HttpMethod.PUT}, allowCredentials = true, maxAge = -1) public String options() { return "options"; } - @GET - public String getCors() { - return "getCors"; + @DELETE + public String deleteCors() { + return "deleteCors"; } @PUT From 0c4d0c6c5dec34fbfb71f0064dde11920c7626f7 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 6 Nov 2019 15:20:46 -0500 Subject: [PATCH 010/100] Initial support for CORS using Config properties. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/cors/pom.xml | 4 + .../microprofile/cors/CrossOrigin.java | 7 +- .../microprofile/cors/CrossOriginConfig.java | 179 ++++++++++++++++++ .../microprofile/cors/CrossOriginFilter.java | 19 +- .../microprofile/cors/CrossOriginHelper.java | 65 +++++-- .../cors/src/main/java9/module-info.java | 2 + .../microprofile/cors/CrossOriginTest.java | 66 +++++-- .../META-INF/microprofile-config.properties | 3 + 8 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginConfig.java create mode 100644 microprofile/cors/src/test/resources/META-INF/microprofile-config.properties diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index c754e690f87..f44f9ef957a 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -48,6 +48,10 @@ io.helidon.webserver helidon-webserver-jersey + + io.helidon.microprofile.config + helidon-microprofile-config + io.helidon.microprofile.bundles internal-test-libs diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index 341d6adba10..fa591642a14 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -76,6 +76,11 @@ */ String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + /** + * Default cache expiration in seconds. + */ + long DEFAULT_AGE = 3600; + /** * A list of origins that are allowed such as {@code "http://foo.com"} or * {@code "*"} to allow all origins. Corresponds to header {@code @@ -123,5 +128,5 @@ * * @return Max age. */ - long maxAge() default 3600; + long maxAge() default DEFAULT_AGE; } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginConfig.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginConfig.java new file mode 100644 index 00000000000..d56608b28a4 --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginConfig.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.microprofile.cors; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import io.helidon.config.Config; + +import static io.helidon.microprofile.cors.CrossOriginHelper.parseHeader; + +/** + * Class CrossOriginConfig. + */ +public class CrossOriginConfig implements CrossOrigin { + + private final String pathPrefix; + private final String[] value; + private final String[] allowHeaders; + private final String[] exposeHeaders; + private final String[] allowMethods; + private final boolean allowCredentials; + private final long maxAge; + + private CrossOriginConfig(Builder builder) { + this.pathPrefix = builder.pathPrefix; + this.value = builder.value; + this.allowHeaders = builder.allowHeaders; + this.exposeHeaders = builder.exposeHeaders; + this.allowMethods = builder.allowMethods; + this.allowCredentials = builder.allowCredentials; + this.maxAge = builder.maxAge; + } + + /** + * Returns path prefix. + * + * @return Path prefix. + */ + public String pathPrefix() { + return pathPrefix; + } + + @Override + public String[] value() { + return value; + } + + @Override + public String[] allowHeaders() { + return allowHeaders; + } + + @Override + public String[] exposeHeaders() { + return exposeHeaders; + } + + @Override + public String[] allowMethods() { + return allowMethods; + } + + @Override + public boolean allowCredentials() { + return allowCredentials; + } + + @Override + public long maxAge() { + return maxAge; + } + + @Override + public Class annotationType() { + return CrossOrigin.class; + } + + /** + * Builder for {@link CrossOriginConfig}. + */ + static class Builder implements io.helidon.common.Builder { + + private static final String[] ALLOW_ALL = {"*"}; + + private String pathPrefix; + private String[] value = ALLOW_ALL; + private String[] allowHeaders = ALLOW_ALL; + private String[] exposeHeaders; + private String[] allowMethods = ALLOW_ALL; + private boolean allowCredentials; + private long maxAge = DEFAULT_AGE; + + public Builder pathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + return this; + } + + public Builder value(String[] value) { + this.value = value; + return this; + } + + public Builder allowHeaders(String[] allowHeaders) { + this.allowHeaders = allowHeaders; + return this; + } + + public Builder exposeHeaders(String[] allowHeaders) { + this.exposeHeaders = exposeHeaders; + return this; + } + + public Builder allowMethods(String[] allowMethods) { + this.allowMethods = allowMethods; + return this; + } + + public Builder allowCredentials(boolean allowCredentials) { + this.allowCredentials = allowCredentials; + return this; + } + + public Builder maxAge(long maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public CrossOriginConfig build() { + return new CrossOriginConfig(this); + } + } + + static class CrossOriginConfigMapper implements Function> { + + @Override + public List apply(Config config) { + List result = new ArrayList<>(); + int i = 0; + do { + Config item = config.get(Integer.toString(i++)); + if (!item.exists()) { + break; + } + Builder builder = new Builder(); + item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); + item.get("allow-origins").as(String.class).ifPresent( + s -> builder.value(parseHeader(s).toArray(new String[]{}))); + item.get("allow-methods").as(String.class).ifPresent( + s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); + item.get("allow-headers").as(String.class).ifPresent( + s -> builder.allowHeaders(parseHeader(s).toArray(new String[]{}))); + item.get("expose-headers").as(String.class).ifPresent( + s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); + item.get("allow-credentials").as(Boolean.class).ifPresent(builder::allowCredentials); + item.get("max-age").as(Long.class).ifPresent(builder::maxAge); + result.add(builder.build()); + } while (true); + return result; + } + } +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 9a8ab770247..2639b117f50 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.cors; +import java.util.List; import java.util.Optional; import javax.ws.rs.container.ContainerRequestContext; @@ -26,6 +27,11 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; +import io.helidon.microprofile.config.MpConfig; + +import org.eclipse.microprofile.config.ConfigProvider; + +import static io.helidon.microprofile.cors.CrossOriginConfig.CrossOriginConfigMapper; import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.CORS; import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.NORMAL; import static io.helidon.microprofile.cors.CrossOriginHelper.RequestType.PREFLIGHT; @@ -42,15 +48,22 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; + private List crossOriginConfigs; + + CrossOriginFilter() { + MpConfig config = (MpConfig) ConfigProvider.getConfig(); + crossOriginConfigs = config.helidonConfig().get("cors").as(new CrossOriginConfigMapper()).get(); + } + @Override public void filter(ContainerRequestContext requestContext) { CrossOriginHelper.RequestType type = findRequestType(requestContext); if (type != NORMAL) { if (type == PREFLIGHT) { - Response response = processPreFlight(requestContext, resourceInfo); + Response response = processPreFlight(requestContext, resourceInfo, crossOriginConfigs); requestContext.abortWith(response); } else if (type == CORS) { - Optional response = processCorsRequest(requestContext, resourceInfo); + Optional response = processCorsRequest(requestContext, resourceInfo, crossOriginConfigs); response.ifPresent(requestContext::abortWith); } else { throw new IllegalStateException("Invalid value for enum RequestType"); @@ -62,7 +75,7 @@ public void filter(ContainerRequestContext requestContext) { public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { CrossOriginHelper.RequestType type = findRequestType(requestContext); if (type == CORS) { - processCorsResponse(requestContext, responseContext, resourceInfo); + processCorsResponse(requestContext, responseContext, resourceInfo, crossOriginConfigs); } } } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java index d20e96f17b5..bf30da728f4 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java @@ -92,12 +92,15 @@ static RequestType findRequestType(ContainerRequestContext requestContext) { * * @param requestContext The request context. * @param resourceInfo Info about the matched resource. + * @param crossOriginConfigs List of {@code CrossOriginConfig}. * @return A response to send back to the client. */ - static Optional processCorsRequest(ContainerRequestContext requestContext, ResourceInfo resourceInfo) { + static Optional processCorsRequest(ContainerRequestContext requestContext, + ResourceInfo resourceInfo, + List crossOriginConfigs) { MultivaluedMap headers = requestContext.getHeaders(); String origin = headers.getFirst(ORIGIN); - Optional crossOrigin = lookupAnnotation(resourceInfo); + Optional crossOrigin = lookupCrossOrigin(requestContext, resourceInfo, crossOriginConfigs); // Annotation must be present for actual request if (!crossOrigin.isPresent()) { @@ -119,22 +122,26 @@ static Optional processCorsRequest(ContainerRequestContext requestCont * * @param requestContext The request context. * @param responseContext The response context. + * @param crossOriginConfigs List of {@code CrossOriginConfig}. * @return A response to send back to the client. */ - static void processCorsResponse(ContainerRequestContext requestContext, ContainerResponseContext responseContext, - ResourceInfo resourceInfo) { - Optional crossOrigin = lookupAnnotation(resourceInfo); + static void processCorsResponse(ContainerRequestContext requestContext, + ContainerResponseContext responseContext, + ResourceInfo resourceInfo, + List crossOriginConfigs) { + Optional crossOrigin = lookupCrossOrigin(requestContext, resourceInfo, crossOriginConfigs); MultivaluedMap headers = responseContext.getHeaders(); - // Add Access-Control-Allow-Origin header - // TODO: Allow any origin as "*" here + // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials String origin = requestContext.getHeaders().getFirst(ORIGIN); - headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); - - // Add Access-Control-Allow-Credentials if (crossOrigin.get().allowCredentials()) { headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } else { + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + headers.add(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); } + // Add Access-Control-Expose-Headers if non-empty formatHeader(crossOrigin.get().exposeHeaders()).ifPresent( h -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, h)); @@ -145,13 +152,16 @@ static void processCorsResponse(ContainerRequestContext requestContext, Containe * * @param requestContext The request context. * @param resourceInfo Info about the matched resource. + * @param crossOriginConfigs List of {@code CrossOriginConfig}. * @return A response to send back to the client. */ - static Response processPreFlight(ContainerRequestContext requestContext, ResourceInfo resourceInfo) { + static Response processPreFlight(ContainerRequestContext requestContext, + ResourceInfo resourceInfo, + List crossOriginConfigs) { MultivaluedMap headers = requestContext.getHeaders(); String origin = headers.getFirst(ORIGIN); - Optional crossOrigin = lookupAnnotation(resourceInfo); + Optional crossOrigin = lookupCrossOrigin(requestContext, resourceInfo, crossOriginConfigs); // If CORS not enabled, deny request if (!crossOrigin.isPresent()) { @@ -241,10 +251,24 @@ static Response forbidden(String message) { * Looks up a {@code CrossOrigin} annotation on the resource method matched first * and if not present on a method annotated by {@code OPTIONS} on the same resource. * + * @param requestContext The request context. * @param resourceInfo Info about the matched resource. + * @param crossOriginConfigs List of {@code CrossOriginConfig}. * @return Outcome of lookup. */ - static Optional lookupAnnotation(ResourceInfo resourceInfo) { + static Optional lookupCrossOrigin(ContainerRequestContext requestContext, + ResourceInfo resourceInfo, + List crossOriginConfigs) { + // First search in configs + for (CrossOriginConfig config : crossOriginConfigs) { + String pathPrefix = normalize(config.pathPrefix()); + String uriPath = normalize(requestContext.getUriInfo().getPath()); + if (uriPath.startsWith(pathPrefix)) { + return Optional.of(config); + } + } + + // If not found, inspect resource matched Method resourceMethod = resourceInfo.getResourceMethod(); Class resourceClass = resourceInfo.getResourceClass(); @@ -279,7 +303,7 @@ static Optional lookupAnnotation(ResourceInfo resourceInfo) { * @return Formatted array as an {@code Optional}. */ static Optional formatHeader(T[] array) { - if (array.length == 0) { + if (array == null || array.length == 0) { return Optional.empty(); } int i = 0; @@ -327,4 +351,17 @@ static Set parseHeader(List headers) { } return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); } + + /** + * Trim leading or trailing slashes of a path. + * + * @param path The path. + * @return Normalized path. + */ + private static String normalize(String path) { + int length = path.length(); + int beginIndex = path.charAt(0) == '/' ? 1 : 0; + int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; + return path.substring(beginIndex, endIndex); + } } diff --git a/microprofile/cors/src/main/java9/module-info.java b/microprofile/cors/src/main/java9/module-info.java index a859b7b0b82..62520d82fea 100644 --- a/microprofile/cors/src/main/java9/module-info.java +++ b/microprofile/cors/src/main/java9/module-info.java @@ -20,7 +20,9 @@ module io.helidon.microprofile.cors { requires transitive java.ws.rs; + requires io.helidon.microprofile.config; requires io.helidon.jersey.common; + requires microprofile.config.api; exports io.helidon.microprofile.cors; diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index be204500b8c..73b20591856 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -87,7 +87,7 @@ static public class CorsApplication extends Application { @Override public Set> getClasses() { - return CollectionsHelper.setOf(CorsResource1.class, CorsResource2.class); + return CollectionsHelper.setOf(CorsResource1.class, CorsResource2.class, CorsResource3.class); } } @@ -97,18 +97,17 @@ static public class CorsResource1 { @OPTIONS @CrossOrigin - public String options() { - return "options"; + public void options() { } @DELETE - public String deleteCors() { - return "deleteCors"; + public Response deleteCors() { + return Response.ok().build(); } @PUT - public String putCors() { - return "putCors"; + public Response putCors() { + return Response.ok().build(); } } @@ -122,18 +121,32 @@ static public class CorsResource2 { allowMethods = {HttpMethod.DELETE, HttpMethod.PUT}, allowCredentials = true, maxAge = -1) - public String options() { - return "options"; + public void options() { } @DELETE - public String deleteCors() { - return "deleteCors"; + public Response deleteCors() { + return Response.ok().build(); } @PUT - public String putCors() { - return "putCors"; + public Response putCors() { + return Response.ok().build(); + } + } + + @RequestScoped + @Path("/cors3") // Configured in META-INF/microprofile-config.properties + static public class CorsResource3 { + + @DELETE + public Response deleteCors() { + return Response.ok().build(); + } + + @PUT + public Response putCors() { + return Response.ok().build(); } } @@ -294,7 +307,7 @@ void test1ActualAllowedOrigin() { .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") .put(Entity.entity("", MediaType.TEXT_PLAIN_TYPE)); assertThat(res.getStatusInfo(), is(Response.Status.OK)); - assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("*")); } @Test @@ -307,4 +320,29 @@ void test2ActualAllowedOrigin() { assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true")); } + + @Test + void test3PreFlightAllowedOrigin() { + Response res = target.path("/app/cors3") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .options(); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_METHODS), is("PUT")); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue())); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_MAX_AGE), is("3600")); + } + + @Test + void test3ActualAllowedOrigin() { + Response res = target.path("/app/cors3") + .request() + .header(ORIGIN, "http://foo.bar") + .header(ACCESS_CONTROL_REQUEST_METHOD, "PUT") + .put(Entity.entity("", MediaType.TEXT_PLAIN_TYPE)); + assertThat(res.getStatusInfo(), is(Response.Status.OK)); + assertThat(res.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://foo.bar")); + } } diff --git a/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties b/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..0f9da053cf5 --- /dev/null +++ b/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,3 @@ +cors.0.path-prefix=/cors3 +cors.0.allow-origins=http://foo.bar, http://bar.foo +cors.0.allow-methods=DELETE, PUT From 539c0923cd3a9ded1ccb9462abf78ba4e714e9cc Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 7 Nov 2019 10:10:57 -0500 Subject: [PATCH 011/100] Set priority on CrossOriginFilter. Signed-off-by: Santiago Pericas-Geertsen --- .../java/io/helidon/microprofile/cors/CrossOriginFilter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 2639b117f50..34a6b65786d 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Optional; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -43,6 +45,7 @@ /** * Class CrossOriginFilter. */ +@Priority(Priorities.HEADER_DECORATOR) class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { @Context From eb9d7ce4a836c7f9ea85040cccd5e163326e1315 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 2 Apr 2020 15:52:31 -0500 Subject: [PATCH 012/100] Initial commit --- .../java/io/helidon/cors/CORSSupport.java | 192 ++++++++++ .../java/io/helidon/cors/CrossOrigin.java | 132 +++++++ .../io/helidon/cors/CrossOriginConfig.java | 179 +++++++++ .../io/helidon/cors/CrossOriginHelper.java | 362 ++++++++++++++++++ .../java/io/helidon/cors/package-info.java | 26 ++ pom.xml | 1 + 6 files changed, 892 insertions(+) create mode 100644 cors/src/main/java/io/helidon/cors/CORSSupport.java create mode 100644 cors/src/main/java/io/helidon/cors/CrossOrigin.java create mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginConfig.java create mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginHelper.java create mode 100644 cors/src/main/java/io/helidon/cors/package-info.java diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java new file mode 100644 index 00000000000..7ac38195900 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; +import io.helidon.cors.CrossOriginHelper.RequestType; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import static io.helidon.cors.CrossOriginHelper.requestType; + +/** + * Provides support for CORS in an application or a built-in Helidon service. + *

+ * The application uses the {@link Builder} to set CORS-related values, including the @{code cors} config node from the + * application config, if any. + */ +public class CORSSupport implements Service { + + public static CORSSupport create(Config config) { + return builder().config(config).build(); + } + + static CORSSupport.Builder builder() { + return new Builder(); + } + + private static final Logger LOGGER = Logger.getLogger(CORSSupport.class.getName()); + + static { + HelidonFeatures.register(HelidonFlavor.SE, "CORS"); + } + + private final List crossOriginConfigs; + + private CORSSupport(Builder builder) { + crossOriginConfigs = builder.configs(); + } + + @Override + public void update(Routing.Rules rules) { + configureCORS(rules); + } + + private void configureCORS(Routing.Rules rules) { + if (!crossOriginConfigs.isEmpty()) { + rules.any(this::handleCORS); + } + } + + private void handleCORS(ServerRequest request, ServerResponse response) { + RequestType requestType = requestType(firstHeaderGetter(request), + containsKeyFn(request), + request.method().name()); + + switch (requestType) { + case PREFLIGHT: + ServerResponse preflightResponse = CrossOriginHelper.processPreFlight(request.path().toString(), + crossOriginConfigs, + firstHeaderGetter(request), + allGetter(request), + responseSetter(response), + responseStatusSetter(response), + headerAdder(response)); + preflightResponse.send(); + break; + + case CORS: + Optional corsResponse = CrossOriginHelper.processCorsRequest(request.path().toString(), + crossOriginConfigs, + firstHeaderGetter(request), + responseSetter(response)); + /* + * An actual response means there is an error. Otherwise, since we know this is a CORS request, do the CORS + * post-processing after other + */ + corsResponse.ifPresentOrElse(ServerResponse::send, () -> finishCORSResponse(request, response)); + break; + + case NORMAL: + request.next(); + break; + + default: + throw new IllegalStateException(String.format("Unrecognized request type during CORS checking: %s", requestType)); + + } + } + + private Function firstHeaderGetter(ServerRequest request) { + return firstHeaderGetter(request::headers); + } + + private Function firstHeaderGetter(ServerResponse response) { + return firstHeaderGetter(response::headers); + } + + private Function firstHeaderGetter(Supplier headers) { + return (headerName) -> headers.get().first(headerName).orElse(null); + } + + private Function containsKeyFn(ServerRequest request) { + return (key) -> request.headers().first(key).isPresent(); + } + + private Function> allGetter(ServerRequest request) { + return (key) -> request.headers().all(key); + } + + private BiFunction responseSetter(ServerResponse response) { + return (errorMsg, statusCode) -> response.status(Http.ResponseStatus.create(statusCode, errorMsg)); + } + + private Function responseStatusSetter(ServerResponse response) { + return (statusCode) -> response.status(statusCode); + } + + private BiConsumer headerAdder(ServerResponse response) { + return (key, value) -> response.headers().add(key, value.toString()); + } + + private void finishCORSResponse(ServerRequest request, ServerResponse response) { + CrossOriginHelper.prepareCorsResponse(request.path().toString(), + crossOriginConfigs, + firstHeaderGetter(response), + headerAdder(response)); + + request.next(); + } + + public static class Builder implements io.helidon.common.Builder { + + private Optional corsConfig = Optional.empty(); + + @Override + public CORSSupport build() { + return new CORSSupport(this); + } + + /** + * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the + * provided {@code Config} instance from its own config using the key {@value CrossOriginHelper#CORS_CONFIG_KEY}. + * + * @param config the CORS config + * @return the updated builder + */ + public Builder config(Config config) { + this.corsConfig = Optional.of(config); + return this; + } + + /** + * Returns CORS-related information that was derived from the app's or component's config node. + * + * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions + */ + List configs() { + return corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) + .orElse(Collections.emptyList()); + } + } +} diff --git a/cors/src/main/java/io/helidon/cors/CrossOrigin.java b/cors/src/main/java/io/helidon/cors/CrossOrigin.java new file mode 100644 index 00000000000..e6b3df117d1 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CrossOrigin.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * CrossOrigin annotation. + */ +@Target(METHOD) +@Retention(RUNTIME) +@Documented +public @interface CrossOrigin { + + /** + * Header Origin. + */ + String ORIGIN = "Origin"; + + /** + * Header Access-Control-Request-Method. + */ + String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + + /** + * Header Access-Control-Request-Headers. + */ + String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + + /** + * Header Access-Control-Allow-Origin. + */ + String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + /** + * Header Access-Control-Expose-Headers. + */ + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * Header Access-Control-Max-Age. + */ + String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + + /** + * Header Access-Control-Allow-Credentials. + */ + String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + /** + * Header Access-Control-Allow-Methods. + */ + String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + /** + * Header Access-Control-Allow-Headers. + */ + String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + /** + * Default cache expiration in seconds. + */ + long DEFAULT_AGE = 3600; + + /** + * A list of origins that are allowed such as {@code "http://foo.com"} or + * {@code "*"} to allow all origins. Corresponds to header {@code + * Access-Control-Allow-Origin}. + * + * @return Allowed origins. + */ + String[] value() default {"*"}; + + /** + * A list of request headers that are allowed or {@code "*"} to allow all headers. + * Corresponds to {@code Access-Control-Allow-Headers}. + * + * @return Allowed headers. + */ + String[] allowHeaders() default {"*"}; + + /** + * A list of response headers allowed for clients other than the "standard" + * ones. Corresponds to {@code Access-Control-Expose-Headers}. + * + * @return Exposed headers. + */ + String[] exposeHeaders() default {}; + + /** + * A list of supported HTTP request methods. In response to pre-flight + * requests. Corresponds to {@code Access-Control-Allow-Methods}. + * + * @return Allowed methods. + */ + String[] allowMethods() default {"*"}; + + /** + * Whether the client can send cookies or credentials. Corresponds to {@code + * Access-Control-Allow-Credentials}. + * + * @return Allowed credentials. + */ + boolean allowCredentials() default false; + + /** + * Pre-flight response duration in seconds. After time expires, a new pre-flight + * request is required. Corresponds to {@code Access-Control-Max-Age}. + * + * @return Max age. + */ + long maxAge() default DEFAULT_AGE; +} diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java new file mode 100644 index 00000000000..2d438f5ea8b --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.config.Config; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static io.helidon.cors.CrossOriginHelper.parseHeader; + +/** + * Class CrossOriginConfig. + */ +public class CrossOriginConfig implements CrossOrigin { + + private final String pathPrefix; + private final String[] value; + private final String[] allowHeaders; + private final String[] exposeHeaders; + private final String[] allowMethods; + private final boolean allowCredentials; + private final long maxAge; + + private CrossOriginConfig(Builder builder) { + this.pathPrefix = builder.pathPrefix; + this.value = builder.value; + this.allowHeaders = builder.allowHeaders; + this.exposeHeaders = builder.exposeHeaders; + this.allowMethods = builder.allowMethods; + this.allowCredentials = builder.allowCredentials; + this.maxAge = builder.maxAge; + } + + /** + * Returns path prefix. + * + * @return Path prefix. + */ + public String pathPrefix() { + return pathPrefix; + } + + @Override + public String[] value() { + return value; + } + + @Override + public String[] allowHeaders() { + return allowHeaders; + } + + @Override + public String[] exposeHeaders() { + return exposeHeaders; + } + + @Override + public String[] allowMethods() { + return allowMethods; + } + + @Override + public boolean allowCredentials() { + return allowCredentials; + } + + @Override + public long maxAge() { + return maxAge; + } + + @Override + public Class annotationType() { + return CrossOrigin.class; + } + + /** + * Builder for {@link CrossOriginConfig}. + */ + static class Builder implements io.helidon.common.Builder { + + private static final String[] ALLOW_ALL = {"*"}; + + private String pathPrefix; + private String[] value = ALLOW_ALL; + private String[] allowHeaders = ALLOW_ALL; + private String[] exposeHeaders; + private String[] allowMethods = ALLOW_ALL; + private boolean allowCredentials; + private long maxAge = DEFAULT_AGE; + + public Builder pathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + return this; + } + + public Builder value(String[] value) { + this.value = value; + return this; + } + + public Builder allowHeaders(String[] allowHeaders) { + this.allowHeaders = allowHeaders; + return this; + } + + public Builder exposeHeaders(String[] allowHeaders) { + this.exposeHeaders = exposeHeaders; + return this; + } + + public Builder allowMethods(String[] allowMethods) { + this.allowMethods = allowMethods; + return this; + } + + public Builder allowCredentials(boolean allowCredentials) { + this.allowCredentials = allowCredentials; + return this; + } + + public Builder maxAge(long maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public CrossOriginConfig build() { + return new CrossOriginConfig(this); + } + } + + static class CrossOriginConfigMapper implements Function> { + + @Override + public List apply(Config config) { + List result = new ArrayList<>(); + int i = 0; + do { + Config item = config.get(Integer.toString(i++)); + if (!item.exists()) { + break; + } + Builder builder = new Builder(); + item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); + item.get("allow-origins").as(String.class).ifPresent( + s -> builder.value(parseHeader(s).toArray(new String[]{}))); + item.get("allow-methods").as(String.class).ifPresent( + s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); + item.get("allow-headers").as(String.class).ifPresent( + s -> builder.allowHeaders(parseHeader(s).toArray(new String[]{}))); + item.get("expose-headers").as(String.class).ifPresent( + s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); + item.get("allow-credentials").as(Boolean.class).ifPresent(builder::allowCredentials); + item.get("max-age").as(Long.class).ifPresent(builder::maxAge); + result.add(builder.build()); + } while (true); + return result; + } + } +} diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java new file mode 100644 index 00000000000..91225f2da9d --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static io.helidon.common.http.Http.Header.HOST; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOrigin.ORIGIN; + +/** + * Centralizes common logic to both SE and MP CORS support. + *

+ * To serve both masters, several methods here accept functions that are intended to operate on items such as headers, + * responses, etc. The SE and MP implementations of these items have no common superclasses or interfaces, so the methods + * here have to call back to the SE and MP implementations of CORS support to look for or set headers, set response status or + * error messages, etc. The JavaDoc explains these functions according to their intended uses, although being functions they + * could operate on anything. + *

+ */ +public class CrossOriginHelper { + + public static final String CORS_CONFIG_KEY = "cors"; + + static final String ORIGIN_DENIED = "CORS origin is denied"; + static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; + static final String METHOD_NOT_IN_ALLOWED_LIST = "CORS method is not in allowed list"; + static final String HEADERS_NOT_IN_ALLOWED_LIST = "CORS headers not in allowed list"; + + /** + * CORS-related classification of HTTP requests. + */ + public enum RequestType { + /** + * A non-CORS request + */ + NORMAL, + + /** + * A CORS request, either a simple one or a non-simple one already preceded by a preflight request + */ + CORS, + + /** + * A CORS preflight request + */ + PREFLIGHT + } + + /** + * Analyzes the method and headers to determine the type of request, from the CORS perspective. + * + * @param firstHeaderGetter accepts a header name and returns the first value or null if no values exist + * @param headerContainsKeyChecker sees if a header name exists + * @param method String containing the HTTP method name + * @return RequestType + */ + public static RequestType requestType(Function firstHeaderGetter, Function headerContainsKeyChecker, + String method) { + String origin = firstHeaderGetter.apply(ORIGIN); + String host = firstHeaderGetter.apply(HOST); + if (origin == null ||origin.contains("://" + host)) { + return RequestType.NORMAL; + } + + // Is this a pre-flight request? + if (method.equalsIgnoreCase("OPTIONS") + && headerContainsKeyChecker.apply(ACCESS_CONTROL_REQUEST_METHOD)) { + return RequestType.PREFLIGHT; + } + + // A CORS request that is not a pre-flight one + return RequestType.CORS; + } + + /** + * Looks for a matching CORS config entry for the specified path among the provided CORS configuration information, returning + * an {@code Optional} of the matching {@code CrossOrigin} instance for the path, if any. + * + * @param path the possibly unnormalized request path to check + * @param crossOriginConfigs CORS configuration + * @return Optional for the matching config, or an empty Optional if none matched + */ + static Optional lookupCrossOrigin(String path, List crossOriginConfigs) { + for (CrossOriginConfig config : crossOriginConfigs) { + String pathPrefix = normalize(config.pathPrefix()); + String uriPath = normalize(path); + if (uriPath.startsWith(pathPrefix)) { + return Optional.of(config); + } + } + + return Optional.empty(); + } + + /** + * Validates information about an incoming request as a CORS request. + * + * @param path possibly-unnormalized path from the request + * @param crossOriginConfigs config information for CORS + * @param firstHeaderGetter accepts a header name and returns the first value; null otherwise + * @param responseSetter accepts an error message and a status code and sets those values in an HTTP response and returns + * that response + * @param the type for the HTTP response as returned from the responseSetter + * @return Optional of an error response (returned by the responseSetter) if the request was an invalid CORS request; + * Optional.empty() if it was a valid CORS request + */ + public static Optional processCorsRequest(String path, List crossOriginConfigs, + Function firstHeaderGetter, BiFunction responseSetter) { + String origin = firstHeaderGetter.apply(ORIGIN); + Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); + if (!crossOrigin.isPresent()) { + return Optional.of(forbidden(ORIGIN_DENIED, responseSetter)); + } + + // If enabled but not whitelisted, deny request + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { + return Optional.of(forbidden(ORIGIN_NOT_IN_ALLOWED_LIST, responseSetter)); + } + + // Successful processing of request + return Optional.empty(); + } + + /** + * Prepares a CORS response. + * + * @param path the possibly non-normalized path from the request + * @param crossOriginConfigs config information for CORS + * @param firstHeaderGetter function which accepts a header name and returns the first value; null otherwise + * @param headerAdder bi-consumer that accepts a header name and value and (presumably) adds it as a header to an HTTP response + */ + public static void prepareCorsResponse(String path, List crossOriginConfigs, + Function firstHeaderGetter, BiConsumer headerAdder) { + Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); + + // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials + String origin = firstHeaderGetter.apply(ORIGIN); + if (crossOrigin.get().allowCredentials()) { + headerAdder.accept(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } else { + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); + } + + // Add Access-Control-Expose-Headers if non-empty + formatHeader(crossOrigin.get().exposeHeaders()).ifPresent( + h -> headerAdder.accept(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + } + + /** + * Processes a pre-flight request. + * + * @param path possibly non-normalized path from the request + * @param crossOriginConfigs config information for CORS + * @param firstHeaderGetter accepts a header name and returns the first value, if any; null otherwise + * @param allHeaderGetter accepts a key and returns a list of values, if any, associated with that key; empty list otherwise + * @param responseSetter accepts an error message and a status code and sets those values in an HTTP response and returns + * that response + * @param responseStatusSetter accepts an Integer status code and returns a response with that status set + * @param headerAdder accepts a header name and value and adds it as a header to an HTTP response + * @param the type for the returned HTTP response (as returned from the response setter functions) + * @return the T returned by the responseStatusSetter with CORS-related headers set via headerAdder (for a successful + */ + public static T processPreFlight(String path, + List crossOriginConfigs, Function firstHeaderGetter, + Function> allHeaderGetter, + BiFunction responseSetter, Function responseStatusSetter, + BiConsumer headerAdder) { + + String origin = firstHeaderGetter.apply(ORIGIN); + Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); + + // If CORS not enabled, deny request + if (!crossOrigin.isPresent()) { + return forbidden(ORIGIN_DENIED, responseSetter); + } + + // If enabled but not whitelisted, deny request + List allowedOrigins = Arrays.asList(crossOrigin.get().value()); + if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { + return forbidden(ORIGIN_NOT_IN_ALLOWED_LIST, responseSetter); + } + + // Check if method is allowed + String method = firstHeaderGetter.apply(ACCESS_CONTROL_REQUEST_METHOD); + List allowedMethods = Arrays.asList(crossOrigin.get().allowMethods()); + if (!allowedMethods.contains("*") && !contains(method, allowedMethods, String::equals)) { + return forbidden(METHOD_NOT_IN_ALLOWED_LIST, responseSetter); + } + + // Check if headers are allowed + Set requestHeaders = parseHeader(allHeaderGetter.apply(ACCESS_CONTROL_REQUEST_HEADERS)); + List allowedHeaders = Arrays.asList(crossOrigin.get().allowHeaders()); + if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { + return forbidden(HEADERS_NOT_IN_ALLOWED_LIST, responseSetter); + } + + // Build successful response + T response = responseStatusSetter.apply(Http.Status.OK_200.code()); + headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + if (crossOrigin.get().allowCredentials()) { + headerAdder.accept(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + headerAdder.accept(ACCESS_CONTROL_ALLOW_METHODS, method); + formatHeader(requestHeaders.toArray()).ifPresent( + h -> headerAdder.accept(ACCESS_CONTROL_ALLOW_HEADERS, h)); + long maxAge = crossOrigin.get().maxAge(); + if (maxAge > 0) { + headerAdder.accept(ACCESS_CONTROL_MAX_AGE, maxAge); + } + return response; + } + + /** + * Formats an array as a comma-separate list without brackets. + * + * @param array The array. + * @param Type of elements in array. + * @return Formatted array as an {@code Optional}. + */ + static Optional formatHeader(T[] array) { + if (array == null || array.length == 0) { + return Optional.empty(); + } + int i = 0; + StringBuilder builder = new StringBuilder(); + do { + builder.append(array[i++].toString()); + if (i == array.length) { + break; + } + builder.append(", "); + } while (true); + return Optional.of(builder.toString()); + } + + /** + * Parse list header value as a set. + * + * @param header Header value as a list. + * @return Set of header values. + */ + static Set parseHeader(String header) { + if (header == null) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + StringTokenizer tokenizer = new StringTokenizer(header, ","); + while (tokenizer.hasMoreTokens()) { + String value = tokenizer.nextToken().trim(); + if (value.length() > 0) { + result.add(value); + } + } + return result; + } + + /** + * Parse a list of list of headers as a set. + * + * @param headers Header value as a list, each a potential list. + * @return Set of header values. + */ + static Set parseHeader(List headers) { + if (headers == null) { + return Collections.emptySet(); + } + return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); + } + + /** + * Trim leading or trailing slashes of a path. + * + * @param path The path. + * @return Normalized path. + */ + static String normalize(String path) { + int length = path.length(); + int beginIndex = path.charAt(0) == '/' ? 1 : 0; + int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; + return path.substring(beginIndex, endIndex); + } + + /** + * Returns response with forbidden status and entity created from message. + * + * @param message Message in entity. + * @return A {@code Response} instance. + */ + static T forbidden(String message, BiFunction responseSetter) { + return responseSetter.apply(message, Http.Status.FORBIDDEN_403.code()); + } + + /** + * Checks containment in a {@code Collection}. + * + * @param item The string. + * @param collection The collection. + * @param eq Equality function. + * @return Outcome of test. + */ + static boolean contains(String item, Collection collection, BiFunction eq) { + for (String s : collection) { + if (eq.apply(item, s)) { + return true; + } + } + return false; + } + + /** + * Checks containment in two collections, case insensitively. + * + * @param left First collection. + * @param right Second collection. + * @return Outcome of test. + */ + static boolean contains(Collection left, Collection right) { + for (String s : left) { + if (!contains(s, right, String::equalsIgnoreCase)) { + return false; + } + } + return true; + } +} diff --git a/cors/src/main/java/io/helidon/cors/package-info.java b/cors/src/main/java/io/helidon/cors/package-info.java new file mode 100644 index 00000000000..5f0e4ba32de --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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. + * + */ + +/** + * Helidon SE CORS Support. + *

+ * Use {@link CORSSupport} and its {@code Builder} to include support for CORS in your application. + *

+ * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CCORS information for + * your application yourself in your application's config file. + */ +package io.helidon.cors; \ No newline at end of file diff --git a/pom.xml b/pom.xml index f2ed3fbd8b2..41723d4a5d9 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,7 @@ webclient integrations dbclient + cors From d3db49e3c43da0974ef9a788369d32d67dd1918e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 07:49:20 -0500 Subject: [PATCH 013/100] A few more changes --- .../java/io/helidon/cors/CORSSupport.java | 28 +++- .../java/io/helidon/cors/GreetService.java | 41 +++++ .../test/java/io/helidon/cors/SimpleTest.java | 79 +++++++++ .../test/java/io/helidon/cors/TestUtil.java | 152 ++++++++++++++++++ cors/src/test/resources/application.yaml | 29 ++++ 5 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 cors/src/test/java/io/helidon/cors/GreetService.java create mode 100644 cors/src/test/java/io/helidon/cors/SimpleTest.java create mode 100644 cors/src/test/java/io/helidon/cors/TestUtil.java create mode 100644 cors/src/test/resources/application.yaml diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 7ac38195900..0062eac1793 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -37,6 +37,7 @@ import java.util.function.Supplier; import java.util.logging.Logger; +import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; import static io.helidon.cors.CrossOriginHelper.requestType; /** @@ -47,11 +48,32 @@ */ public class CORSSupport implements Service { + /** + * Creates a {@code CORSSupport} instance based on the default configuration and any + * {@value CrossOriginHelper#CORS_CONFIG_KEY} config node in it. + */ + public static CORSSupport create() { + Config corsConfig = Config.create().get(CORS_CONFIG_KEY); + return create(corsConfig); + } + + /** + * Creates a {@code CORSSupport} instance based on only configuration. + * + * @param config the config node containing CORS-related info; typically obtained by retrieving config using the + * "{@value CrossOriginHelper#CORS_CONFIG_KEY}" key from the application's or component's config + * @return configured {@code CORSSupport} instance + */ public static CORSSupport create(Config config) { return builder().config(config).build(); } - static CORSSupport.Builder builder() { + /** + * Returns a builder for constructing a {@code CORSSupport} instance. + * + * @return the new builder + */ + public static CORSSupport.Builder builder() { return new Builder(); } @@ -101,8 +123,8 @@ private void handleCORS(ServerRequest request, ServerResponse response) { firstHeaderGetter(request), responseSetter(response)); /* - * An actual response means there is an error. Otherwise, since we know this is a CORS request, do the CORS - * post-processing after other + * Any response carries a CORS error which we send immediately. Otherwise, since we know this is a CORS + * request, do the CORS post-processing and then pass the baton to the next handler. */ corsResponse.ifPresentOrElse(ServerResponse::send, () -> finishCORSResponse(request, response)); break; diff --git a/cors/src/test/java/io/helidon/cors/GreetService.java b/cors/src/test/java/io/helidon/cors/GreetService.java new file mode 100644 index 00000000000..d4d92d58932 --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/GreetService.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +import java.util.Date; + +public class GreetService implements Service { + + @Override + public void update(Routing.Rules rules) { + rules.get("/", this::getDefaultMessageHandler); + } + + private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { + String msg = String.format("%s %s!", "Hello", new Date().toString()); + response.status(Http.Status.OK_200.code()); + response.send(msg); + } + + +} diff --git a/cors/src/test/java/io/helidon/cors/SimpleTest.java b/cors/src/test/java/io/helidon/cors/SimpleTest.java new file mode 100644 index 00000000000..7f7ce3f2850 --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/SimpleTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; +import io.helidon.common.http.MediaType; +import io.helidon.media.common.MediaSupport; +import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientResponse; +import io.helidon.webserver.Routing; +import io.helidon.webserver.Service; +import io.helidon.webserver.WebServer; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +public class SimpleTest { + + private static WebServer server; + private static WebClient client; + + private static final CORSSupport.Builder SIMPLE_BUILDER = CORSSupport.builder(); + private static final Supplier GREETING_BUILDER = () -> new GreetService(); + + @BeforeAll + public static void startup() throws InterruptedException, ExecutionException, TimeoutException { + Routing.Builder routingBuilder = TestUtil.prepRouting(); + routingBuilder.register(SIMPLE_BUILDER); + routingBuilder.register("/greet", GREETING_BUILDER); + + server = TestUtil.startServer(0, routingBuilder); + client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/greet") + .build(); + } + + @AfterAll + public static void shutdown() { + TestUtil.shutdownServer(server); + } + + @Test + public void testSimple() throws Exception { + + WebClientResponse response = client.get() + .path("/") + .accept(MediaType.TEXT_PLAIN) + .request() + .toCompletableFuture() + .get(); + + String msg = response.content().as(String.class).toCompletableFuture().get(); + Http.ResponseStatus result = response.status(); + + assertThat(result.code(), is(Http.Status.OK_200.code())); + } +} diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java new file mode 100644 index 00000000000..f0430dd5fdc --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.MediaType; +import io.helidon.config.Config; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.Service; +import io.helidon.webserver.WebServer; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class TestUtil { + + private static final Logger LOGGER = Logger.getLogger(TestUtil.class.getName()); + + /** + * Starts the web server at an available port and sets up CORS using the supplied builder. + * + * @param builders the {@code Builder}s to set up for the server. + * @return the {@code WebServer} set up with OpenAPI support + */ +// public static WebServer startServer(Supplier... builders) { +// try { +// return startServer(0, builders); +// } catch (InterruptedException | ExecutionException | TimeoutException ex) { +// throw new RuntimeException("Error starting server for test", ex); +// } +// } + + public static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, ExecutionException, TimeoutException { + Config config = Config.create(); + ServerConfiguration serverConfig = ServerConfiguration.builder(config) + .port(port) + .build(); + WebServer server = WebServer.create(serverConfig, routingBuilder).start().toCompletableFuture().get(10, TimeUnit.SECONDS); + return server; + } + + static Routing.Builder prepRouting() { + Config config = Config.create(); + CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); + return Routing.builder() + .register(corsSupportBuilder); + } + + /** + * Start the Web Server + * + * @param port the port on which to start the server; if less than 1, the port is dynamically selected + * @param builders Builder instances to use in starting the server + * @return {@code WebServer} that has been started + * @throws java.lang.InterruptedException if the start was interrupted + * @throws java.util.concurrent.ExecutionException if the start failed + * @throws java.util.concurrent.TimeoutException if the start timed out + */ + public static WebServer startServer( + int port, + Supplier... builders) throws + InterruptedException, ExecutionException, TimeoutException { + + WebServer result = WebServer.create(ServerConfiguration.builder() + .port(port) + .build(), + Routing.builder() + .register(builders) + .build()) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + LOGGER.log(Level.INFO, "Started server at: https://localhost:{0}", result.port()); + return result; + } + + /** + * Shuts down the specified web server. + * + * @param ws the {@code WebServer} instance to stop + */ + public static void shutdownServer(WebServer ws) { + if (ws != null) { + try { + stopServer(ws); + } catch (InterruptedException | ExecutionException | TimeoutException ex) { + throw new RuntimeException("Error shutting down server for test", ex); + } + } + } + + /** + * Stop the web server. + * + * @param server the {@code WebServer} to stop + * @throws InterruptedException if the stop operation was interrupted + * @throws ExecutionException if the stop operation failed as it ran + * @throws TimeoutException if the stop operation timed out + */ + public static void stopServer(WebServer server) throws + InterruptedException, ExecutionException, TimeoutException { + if (server != null) { + server.shutdown().toCompletableFuture().get(10, TimeUnit.SECONDS); + } + } + + /** + * Returns a {@code HttpURLConnection} for the requested method and path and + * {code @MediaType} from the specified {@link WebServer}. + * + * @param port port to connect to + * @param method HTTP method to use in building the connection + * @param path path to the resource in the web server + * @param mediaType {@code MediaType} to be Accepted + * @return the connection to the server and path + * @throws Exception in case of errors creating the connection + */ + public static HttpURLConnection getURLConnection( + int port, + String method, + String path, + MediaType mediaType) throws Exception { + URL url = new URL("http://localhost:" + port + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + if (mediaType != null) { + conn.setRequestProperty("Accept", mediaType.toString()); + } + System.out.println("Connecting: " + method + " " + url); + return conn; + } +} diff --git a/cors/src/test/resources/application.yaml b/cors/src/test/resources/application.yaml new file mode 100644 index 00000000000..6b47a92f8a2 --- /dev/null +++ b/cors/src/test/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# 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. +# +app: + greeting: "Hello" + +client: + follow-redirects: true + max-redirects: 8 + +cors: + - path-prefix: /cors1 + allow-origins: ["*"] + allow-methods: ["*"] + - path-prefix: /cors2 + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["DELETE", "PUT"] From 221617bbc2c22e031958e69179f6927cfd1a8d79 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 11:53:03 -0500 Subject: [PATCH 014/100] More changes, some bug fixes --- .../java/io/helidon/cors/CORSSupport.java | 2 +- .../io/helidon/cors/CrossOriginConfig.java | 8 +- .../helidon/cors/AbstractCORSTestService.java | 40 +++ .../io/helidon/cors/CORSTestServices.java | 77 +++++ .../java/io/helidon/cors/CustomMatchers.java | 87 +++++ .../test/java/io/helidon/cors/SimpleTest.java | 318 +++++++++++++++++- cors/src/test/resources/application.yaml | 6 + 7 files changed, 522 insertions(+), 16 deletions(-) create mode 100644 cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java create mode 100644 cors/src/test/java/io/helidon/cors/CORSTestServices.java create mode 100644 cors/src/test/java/io/helidon/cors/CustomMatchers.java diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 0062eac1793..77e7f8b6a08 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -174,7 +174,7 @@ private BiConsumer headerAdder(ServerResponse response) { private void finishCORSResponse(ServerRequest request, ServerResponse response) { CrossOriginHelper.prepareCorsResponse(request.path().toString(), crossOriginConfigs, - firstHeaderGetter(response), + firstHeaderGetter(request), headerAdder(response)); request.next(); diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 2d438f5ea8b..4bb11e9e304 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -161,13 +161,13 @@ public List apply(Config config) { } Builder builder = new Builder(); item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); - item.get("allow-origins").as(String.class).ifPresent( + item.get("allow-origins").asList(String.class).ifPresent( s -> builder.value(parseHeader(s).toArray(new String[]{}))); - item.get("allow-methods").as(String.class).ifPresent( + item.get("allow-methods").asList(String.class).ifPresent( s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); - item.get("allow-headers").as(String.class).ifPresent( + item.get("allow-headers").asList(String.class).ifPresent( s -> builder.allowHeaders(parseHeader(s).toArray(new String[]{}))); - item.get("expose-headers").as(String.class).ifPresent( + item.get("expose-headers").asList(String.class).ifPresent( s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); item.get("allow-credentials").as(Boolean.class).ifPresent(builder::allowCredentials); item.get("max-age").as(Long.class).ifPresent(builder::maxAge); diff --git a/cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java b/cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java new file mode 100644 index 00000000000..fe3b1c02abc --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +abstract class AbstractCORSTestService implements Service { + + void ok(ServerRequest request, ServerResponse response) { + response.status(Http.Status.OK_200.code()); + response.send(); + } + + @Override + public void update(Routing.Rules rules) { + rules + .delete(this::ok) + .put(this::ok) + .options(this::ok) + ; + } +} diff --git a/cors/src/test/java/io/helidon/cors/CORSTestServices.java b/cors/src/test/java/io/helidon/cors/CORSTestServices.java new file mode 100644 index 00000000000..16f7185d499 --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/CORSTestServices.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import java.util.List; + +import io.helidon.common.http.Http; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + + +class CORSTestServices { + + static final List SERVICES = List.of(new Service1(), new Service2(), new Service3()); + + static abstract class AbstractCORSTestService implements Service { + + void ok(ServerRequest request, ServerResponse response) { + response.status(Http.Status.OK_200.code()); + response.send(); + } + + @Override + public void update(Routing.Rules rules) { + rules + .delete(this::ok) + .put(this::ok) + ; + } + + abstract String path(); + } + + static class Service1 extends AbstractCORSTestService { + + static final String PATH = "/cors1"; + + @Override + String path() { + return PATH; + } + } + + static class Service2 extends AbstractCORSTestService { + static final String PATH = "/cors2"; + + @Override + String path() { + return PATH; + } + } + + static class Service3 extends AbstractCORSTestService { + static final String PATH = "/cors3"; + + @Override + String path() { + return PATH; + } + } +} diff --git a/cors/src/test/java/io/helidon/cors/CustomMatchers.java b/cors/src/test/java/io/helidon/cors/CustomMatchers.java new file mode 100644 index 00000000000..b05102f2ddb --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/CustomMatchers.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.hamcrest.TypeSafeMatcher; + +import java.util.Optional; + +/** + * Some useful custom matchers. + */ +class CustomMatchers { + + static IsPresent isPresent(Matcher matcher) { + return new IsPresent(matcher); + } + + static IsPresent isPresent() { + return isPresent(null); + } + + static IsNotPresent isNotPresent() { + return new IsNotPresent(); + } + + /** + * Makes sure the {@code Optional} is present, and if an additional matcher was provider, makes sure that the optional's + * value passes the matcher. + * + * @param type of the value in the Optional + */ + static class IsPresent extends TypeSafeMatcher> { + + private final Matcher matcher; + + IsPresent(Matcher m) { + matcher = m; + } + + IsPresent() { + matcher = null; + } + + @Override + protected boolean matchesSafely(Optional t) { + return t.isPresent() && (matcher == null || matcher.matches(t.get())); + } + + @Override + public void describeTo(Description description) { + description.appendText("is present"); + if (matcher != null) { + description.appendText(" and matches " + matcher.toString()); + } + } + } + + static class IsNotPresent extends TypeSafeMatcher> { + + @Override + protected boolean matchesSafely(Optional o) { + return !o.isPresent(); + } + + @Override + public void describeTo(Description description) { + description.appendText("is not present"); + } + } +} diff --git a/cors/src/test/java/io/helidon/cors/SimpleTest.java b/cors/src/test/java/io/helidon/cors/SimpleTest.java index 7f7ce3f2850..112924bb260 100644 --- a/cors/src/test/java/io/helidon/cors/SimpleTest.java +++ b/cors/src/test/java/io/helidon/cors/SimpleTest.java @@ -16,43 +16,59 @@ */ package io.helidon.cors; +import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.common.http.MediaType; -import io.helidon.media.common.MediaSupport; +import io.helidon.cors.CORSTestServices.Service1; +import io.helidon.cors.CORSTestServices.Service2; +import io.helidon.cors.CORSTestServices.Service3; import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientRequestBuilder; import io.helidon.webclient.WebClientResponse; import io.helidon.webserver.Routing; -import io.helidon.webserver.Service; import io.helidon.webserver.WebServer; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CustomMatchers.isNotPresent; +import static io.helidon.cors.CustomMatchers.isPresent; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; public class SimpleTest { private static WebServer server; private static WebClient client; - private static final CORSSupport.Builder SIMPLE_BUILDER = CORSSupport.builder(); - private static final Supplier GREETING_BUILDER = () -> new GreetService(); - @BeforeAll public static void startup() throws InterruptedException, ExecutionException, TimeoutException { - Routing.Builder routingBuilder = TestUtil.prepRouting(); - routingBuilder.register(SIMPLE_BUILDER); - routingBuilder.register("/greet", GREETING_BUILDER); + Routing.Builder routingBuilder = TestUtil.prepRouting() + .register(CORSSupport.builder()) + .register("/greet", () -> new GreetService()); + CORSTestServices.SERVICES.forEach(s -> routingBuilder.register(s.path(), s)); server = TestUtil.startServer(0, routingBuilder); client = WebClient.builder() - .baseUri("http://localhost:" + server.port() + "/greet") + .baseUri("http://localhost:" + server.port()) .build(); } @@ -65,7 +81,7 @@ public static void shutdown() { public void testSimple() throws Exception { WebClientResponse response = client.get() - .path("/") + .path("/greet") .accept(MediaType.TEXT_PLAIN) .request() .toCompletableFuture() @@ -76,4 +92,284 @@ public void testSimple() throws Exception { assertThat(result.code(), is(Http.Status.OK_200.code())); } + + @Test + void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service1.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + } + + @Test + void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service1.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(is("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + } + + @Test + void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service1.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + } + + @Test + void test2PreFlightForbiddenOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://not.allowed"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + } + + @Test + void test2PreFlightForbiddenMethod() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "POST"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightForbiddenHeader() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar, X-oops"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + } + + @Test + void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + } + + @Test + void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .put() + .path(Service1.PATH) + .contentType(MediaType.TEXT_PLAIN); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("*"))); + } + + @Test + void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .put() + .path(Service2.PATH) + .contentType(MediaType.TEXT_PLAIN); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); + } + + @Test + void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service3.PATH); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + } + + @Test + void test3ActualAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .put() + .path(Service3.PATH) + .contentType(MediaType.TEXT_PLAIN); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + } } diff --git a/cors/src/test/resources/application.yaml b/cors/src/test/resources/application.yaml index 6b47a92f8a2..51c96023924 100644 --- a/cors/src/test/resources/application.yaml +++ b/cors/src/test/resources/application.yaml @@ -27,3 +27,9 @@ cors: - path-prefix: /cors2 allow-origins: ["http://foo.bar", "http://bar.foo"] allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-bar", "X-foo"] + allow-credentials: true + max-age: -1 + - path-prefix: /cors3 + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["DELETE", "PUT"] From e96188ba3a3334d42a78ca6985b041a969161faa Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 13:22:20 -0500 Subject: [PATCH 015/100] Add the missing pom along with some other update --- cors/pom.xml | 85 +++++++++++++++++++ .../cors/{SimpleTest.java => CORSTests.java} | 2 +- .../java/io/helidon/cors/CustomMatchers.java | 2 +- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 cors/pom.xml rename cors/src/test/java/io/helidon/cors/{SimpleTest.java => CORSTests.java} (99%) diff --git a/cors/pom.xml b/cors/pom.xml new file mode 100644 index 00000000000..8c90edda049 --- /dev/null +++ b/cors/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + io.helidon + helidon-project + 2.0.0-SNAPSHOT + + + io.helidon.openapi + helidon-cors + + Helidon CORS + + + Helidon CORS implementation + + + jar + + + + + + + + + + + io.helidon.webserver + helidon-webserver + + + org.glassfish + javax.json + runtime + + + io.helidon.config + helidon-config + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.glassfish.jersey.core + jersey-client + test + + + io.helidon.config + helidon-config-yaml + test + + + io.helidon.webclient + helidon-webclient + test + + + diff --git a/cors/src/test/java/io/helidon/cors/SimpleTest.java b/cors/src/test/java/io/helidon/cors/CORSTests.java similarity index 99% rename from cors/src/test/java/io/helidon/cors/SimpleTest.java rename to cors/src/test/java/io/helidon/cors/CORSTests.java index 112924bb260..5d22802e540 100644 --- a/cors/src/test/java/io/helidon/cors/SimpleTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTests.java @@ -54,7 +54,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -public class SimpleTest { +public class CORSTests { private static WebServer server; private static WebClient client; diff --git a/cors/src/test/java/io/helidon/cors/CustomMatchers.java b/cors/src/test/java/io/helidon/cors/CustomMatchers.java index b05102f2ddb..7cf89a7ef44 100644 --- a/cors/src/test/java/io/helidon/cors/CustomMatchers.java +++ b/cors/src/test/java/io/helidon/cors/CustomMatchers.java @@ -29,7 +29,7 @@ class CustomMatchers { static IsPresent isPresent(Matcher matcher) { - return new IsPresent(matcher); + return new IsPresent(matcher); } static IsPresent isPresent() { From d4e8be78ee665e894619b58adb0d8864636105bf Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 13:44:11 -0500 Subject: [PATCH 016/100] Clean up the pom; name the test class so it will actually run --- cors/pom.xml | 7 +------ .../java/io/helidon/cors/{CORSTests.java => CORSTest.java} | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) rename cors/src/test/java/io/helidon/cors/{CORSTests.java => CORSTest.java} (98%) diff --git a/cors/pom.xml b/cors/pom.xml index 8c90edda049..a5288a76d7c 100644 --- a/cors/pom.xml +++ b/cors/pom.xml @@ -24,7 +24,7 @@ 2.0.0-SNAPSHOT - io.helidon.openapi + io.helidon.cors helidon-cors Helidon CORS @@ -66,11 +66,6 @@ hamcrest-all test - - org.glassfish.jersey.core - jersey-client - test - io.helidon.config helidon-config-yaml diff --git a/cors/src/test/java/io/helidon/cors/CORSTests.java b/cors/src/test/java/io/helidon/cors/CORSTest.java similarity index 98% rename from cors/src/test/java/io/helidon/cors/CORSTests.java rename to cors/src/test/java/io/helidon/cors/CORSTest.java index 5d22802e540..38ac1668765 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTests.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -40,21 +40,17 @@ import static io.helidon.cors.CustomMatchers.isPresent; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Response; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -public class CORSTests { +public class CORSTest { private static WebServer server; private static WebClient client; From 2dd1aaa60c6a5a0658c039e2d51fb698aef59320 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 13:57:01 -0500 Subject: [PATCH 017/100] Rename custom matchers --- .../test/java/io/helidon/cors/CORSTest.java | 109 +++++++++++------- .../java/io/helidon/cors/CustomMatchers.java | 21 ++-- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index 38ac1668765..12c47486314 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -16,6 +16,9 @@ */ package io.helidon.cors; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.common.http.MediaType; @@ -36,8 +39,8 @@ import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.cors.CrossOrigin.ORIGIN; -import static io.helidon.cors.CustomMatchers.isNotPresent; -import static io.helidon.cors.CustomMatchers.isPresent; +import static io.helidon.cors.CustomMatchers.notPresent; +import static io.helidon.cors.CustomMatchers.present; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -47,9 +50,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - public class CORSTest { private static WebServer server; @@ -105,10 +105,10 @@ void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); } @Test @@ -128,10 +128,10 @@ void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedExcep .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(is("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(is("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); } @Test @@ -151,11 +151,11 @@ void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedExcep .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); } @Test @@ -193,11 +193,11 @@ void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); } @Test @@ -254,11 +254,11 @@ void test2PreFlightAllowedHeaders1() throws ExecutionException, InterruptedExcep .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); } @Test @@ -267,6 +267,31 @@ void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedExcep .method(Http.Method.OPTIONS.name()) .path(Service2.PATH); + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); + } + + @Test + void test2PreFlightAllowedHeaders3() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(Service2.PATH); + Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); @@ -279,12 +304,12 @@ void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedExcep .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isPresent(containsString("X-bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isNotPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); } @Test @@ -304,7 +329,7 @@ void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("*"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("*"))); } @Test @@ -323,8 +348,8 @@ void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), isPresent(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); } @Test @@ -343,10 +368,10 @@ void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), isPresent(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), isNotPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), isPresent(is("3600"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); } @Test @@ -366,6 +391,6 @@ void test3ActualAllowedOrigin() throws ExecutionException, InterruptedException .get(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), isPresent(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); } } diff --git a/cors/src/test/java/io/helidon/cors/CustomMatchers.java b/cors/src/test/java/io/helidon/cors/CustomMatchers.java index 7cf89a7ef44..0f9ca0c47da 100644 --- a/cors/src/test/java/io/helidon/cors/CustomMatchers.java +++ b/cors/src/test/java/io/helidon/cors/CustomMatchers.java @@ -18,7 +18,6 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; -import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import java.util.Optional; @@ -28,16 +27,16 @@ */ class CustomMatchers { - static IsPresent isPresent(Matcher matcher) { - return new IsPresent(matcher); + static Present present(Matcher matcher) { + return new Present(matcher); } - static IsPresent isPresent() { - return isPresent(null); + static Present present() { + return present(null); } - static IsNotPresent isNotPresent() { - return new IsNotPresent(); + static NotPresent notPresent() { + return new NotPresent(); } /** @@ -46,15 +45,15 @@ static IsNotPresent isNotPresent() { * * @param type of the value in the Optional */ - static class IsPresent extends TypeSafeMatcher> { + static class Present extends TypeSafeMatcher> { private final Matcher matcher; - IsPresent(Matcher m) { + Present(Matcher m) { matcher = m; } - IsPresent() { + Present() { matcher = null; } @@ -72,7 +71,7 @@ public void describeTo(Description description) { } } - static class IsNotPresent extends TypeSafeMatcher> { + static class NotPresent extends TypeSafeMatcher> { @Override protected boolean matchesSafely(Optional o) { From 1edc14ba83c72282b773dd87abab802691ebcaf9 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 13:59:37 -0500 Subject: [PATCH 018/100] Commit module-info.java finally --- cors/src/main/java/module-info.java | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 cors/src/main/java/module-info.java diff --git a/cors/src/main/java/module-info.java b/cors/src/main/java/module-info.java new file mode 100644 index 00000000000..abc446bccf1 --- /dev/null +++ b/cors/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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. + * + */ + +/** + * The Helidon SE CORS module + */ +module io.helidon.cors { + requires java.logging; + + requires io.helidon.common; + requires io.helidon.config; + requires io.helidon.webserver; + + exports io.helidon.cors; +} From 6279cdd0171a14fbff7dc179e706f8447dd04d70 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 3 Apr 2020 18:18:46 -0500 Subject: [PATCH 019/100] Instead of so many functions, etc., use two adapter interfaces for requests and responses --- .../java/io/helidon/cors/CORSSupport.java | 119 +++++--- .../io/helidon/cors/CrossOriginConfig.java | 2 +- .../io/helidon/cors/CrossOriginHelper.java | 262 ++++++++++++------ 3 files changed, 254 insertions(+), 129 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 77e7f8b6a08..2083fb347e5 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -101,27 +101,25 @@ private void configureCORS(Routing.Rules rules) { } private void handleCORS(ServerRequest request, ServerResponse response) { - RequestType requestType = requestType(firstHeaderGetter(request), - containsKeyFn(request), - request.method().name()); + RequestAdapter requestAdapter = new RequestAdapter(request); + RequestType requestType = requestType(requestAdapter); switch (requestType) { case PREFLIGHT: ServerResponse preflightResponse = CrossOriginHelper.processPreFlight(request.path().toString(), crossOriginConfigs, - firstHeaderGetter(request), - allGetter(request), - responseSetter(response), - responseStatusSetter(response), - headerAdder(response)); + () -> Optional.empty(), + requestAdapter, + new ResponseFactory(response)); preflightResponse.send(); break; case CORS: - Optional corsResponse = CrossOriginHelper.processCorsRequest(request.path().toString(), + Optional corsResponse = CrossOriginHelper.processRequest(request.path().toString(), crossOriginConfigs, - firstHeaderGetter(request), - responseSetter(response)); + () -> Optional.empty(), + requestAdapter, + new ResponseFactory(response)); /* * Any response carries a CORS error which we send immediately. Otherwise, since we know this is a CORS * request, do the CORS post-processing and then pass the baton to the next handler. @@ -139,43 +137,12 @@ private void handleCORS(ServerRequest request, ServerResponse response) { } } - private Function firstHeaderGetter(ServerRequest request) { - return firstHeaderGetter(request::headers); - } - - private Function firstHeaderGetter(ServerResponse response) { - return firstHeaderGetter(response::headers); - } - - private Function firstHeaderGetter(Supplier headers) { - return (headerName) -> headers.get().first(headerName).orElse(null); - } - - private Function containsKeyFn(ServerRequest request) { - return (key) -> request.headers().first(key).isPresent(); - } - - private Function> allGetter(ServerRequest request) { - return (key) -> request.headers().all(key); - } - - private BiFunction responseSetter(ServerResponse response) { - return (errorMsg, statusCode) -> response.status(Http.ResponseStatus.create(statusCode, errorMsg)); - } - - private Function responseStatusSetter(ServerResponse response) { - return (statusCode) -> response.status(statusCode); - } - - private BiConsumer headerAdder(ServerResponse response) { - return (key, value) -> response.headers().add(key, value.toString()); - } - private void finishCORSResponse(ServerRequest request, ServerResponse response) { - CrossOriginHelper.prepareCorsResponse(request.path().toString(), + CrossOriginHelper.prepareResponse(request.path().toString(), crossOriginConfigs, - firstHeaderGetter(request), - headerAdder(response)); + () -> Optional.empty(), + new RequestAdapter(request), + new ResponseFactory(response)); request.next(); } @@ -211,4 +178,64 @@ List configs() { .orElse(Collections.emptyList()); } } + + private static class RequestAdapter implements CrossOriginHelper.RequestAdapter { + + private final ServerRequest request; + + RequestAdapter(ServerRequest request) { + this.request = request; + } + @Override + public Optional firstHeader(String key) { + return request.headers().first(key); + } + + @Override + public boolean headerContainsKey(String key) { + return firstHeader(key).isPresent(); + } + + @Override + public List allHeaders(String key) { + return request.headers().all(key); + } + + @Override + public String method() { + return request.method().name(); + } + } + + private static class ResponseFactory implements CrossOriginHelper.ResponseFactory { + + private final ServerResponse serverResponse; + + ResponseFactory(ServerResponse serverResponse) { + this.serverResponse = serverResponse; + } + + @Override + public CrossOriginHelper.ResponseFactory addHeader(String key, String value) { + serverResponse.headers().add(key, value); + return this; + } + + @Override + public CrossOriginHelper.ResponseFactory addHeader(String key, Object value) { + serverResponse.headers().add(key, value.toString()); + return this; + } + + @Override + public ServerResponse forbidden(String message) { + serverResponse.status(Http.ResponseStatus.create(Http.Status.FORBIDDEN_403.code(), message)); + return serverResponse; + } + + @Override + public ServerResponse build() { + return serverResponse; + } + } } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 4bb11e9e304..70e0f35fbf3 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -148,7 +148,7 @@ public CrossOriginConfig build() { } } - static class CrossOriginConfigMapper implements Function> { + public static class CrossOriginConfigMapper implements Function> { @Override public List apply(Config config) { diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index 91225f2da9d..bc502a91a23 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -16,8 +16,6 @@ */ package io.helidon.cors; -import io.helidon.common.http.Http; - import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -26,9 +24,8 @@ import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; -import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Function; +import java.util.function.Supplier; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; @@ -44,11 +41,8 @@ /** * Centralizes common logic to both SE and MP CORS support. *

- * To serve both masters, several methods here accept functions that are intended to operate on items such as headers, - * responses, etc. The SE and MP implementations of these items have no common superclasses or interfaces, so the methods - * here have to call back to the SE and MP implementations of CORS support to look for or set headers, set response status or - * error messages, etc. The JavaDoc explains these functions according to their intended uses, although being functions they - * could operate on anything. + * To serve both masters, several methods here accept an adapter for requests and a factory for responses. Both of these + * are minimal and very specific to the needs of CORS support. *

*/ public class CrossOriginHelper { @@ -80,25 +74,101 @@ public enum RequestType { PREFLIGHT } + /** + * Minimal abstraction of an HTTP request. + */ + public interface RequestAdapter { + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + } + + /** + * Minimal abstraction of an HTTP response factory. + * + * @param the type of the response created by the factory + */ + public interface ResponseFactory { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the factory + */ + ResponseFactory addHeader(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the factory + */ + ResponseFactory addHeader(String key, Object value); + + + /** + * Returns an instance of the response type with the forbidden status and the specified error mesage. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns an instance of the response type with headers set and status set to OK. + * + * @return new response instance + */ + T build(); + } + /** * Analyzes the method and headers to determine the type of request, from the CORS perspective. * - * @param firstHeaderGetter accepts a header name and returns the first value or null if no values exist - * @param headerContainsKeyChecker sees if a header name exists - * @param method String containing the HTTP method name - * @return RequestType + * @param request request adatper + * @return RequestType the CORS request type of the request */ - public static RequestType requestType(Function firstHeaderGetter, Function headerContainsKeyChecker, - String method) { - String origin = firstHeaderGetter.apply(ORIGIN); - String host = firstHeaderGetter.apply(HOST); - if (origin == null ||origin.contains("://" + host)) { + public static RequestType requestType(RequestAdapter request) { + Optional origin = request.firstHeader(ORIGIN); + Optional host = request.firstHeader(HOST); + if (origin.isEmpty() || origin.get().contains("://" + host)) { return RequestType.NORMAL; } // Is this a pre-flight request? - if (method.equalsIgnoreCase("OPTIONS") - && headerContainsKeyChecker.apply(ACCESS_CONTROL_REQUEST_METHOD)) { + if (request.method().equalsIgnoreCase("OPTIONS") + && request.headerContainsKey(ACCESS_CONTROL_REQUEST_METHOD)) { return RequestType.PREFLIGHT; } @@ -114,7 +184,8 @@ public static RequestType requestType(Function firstHeaderGetter * @param crossOriginConfigs CORS configuration * @return Optional for the matching config, or an empty Optional if none matched */ - static Optional lookupCrossOrigin(String path, List crossOriginConfigs) { + static Optional lookupCrossOrigin(String path, List crossOriginConfigs, + Supplier> secondaryLookup) { for (CrossOriginConfig config : crossOriginConfigs) { String pathPrefix = normalize(config.pathPrefix()); String uriPath = normalize(path); @@ -123,7 +194,7 @@ static Optional lookupCrossOrigin(String path, List lookupCrossOrigin(String path, List the type for the HTTP response as returned from the responseSetter * @return Optional of an error response (returned by the responseSetter) if the request was an invalid CORS request; * Optional.empty() if it was a valid CORS request */ - public static Optional processCorsRequest(String path, List crossOriginConfigs, - Function firstHeaderGetter, BiFunction responseSetter) { - String origin = firstHeaderGetter.apply(ORIGIN); - Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); - if (!crossOrigin.isPresent()) { - return Optional.of(forbidden(ORIGIN_DENIED, responseSetter)); + public static Optional processRequest(String path, + List crossOriginConfigs, + Supplier> secondaryCrossOriginLookup, + RequestAdapter request, + ResponseFactory responseFactory) { + Optional origin = request.firstHeader(ORIGIN); + Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup); + if (crossOrigin.isEmpty()) { + return Optional.of(responseFactory.forbidden(ORIGIN_DENIED)); } // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.get().value()); if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { - return Optional.of(forbidden(ORIGIN_NOT_IN_ALLOWED_LIST, responseSetter)); + return Optional.of(responseFactory.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); } // Successful processing of request @@ -161,26 +235,37 @@ public static Optional processCorsRequest(String path, Lis * * @param path the possibly non-normalized path from the request * @param crossOriginConfigs config information for CORS - * @param firstHeaderGetter function which accepts a header name and returns the first value; null otherwise - * @param headerAdder bi-consumer that accepts a header name and value and (presumably) adds it as a header to an HTTP response + * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) + * @param request request + * @param responseFactory response factory */ - public static void prepareCorsResponse(String path, List crossOriginConfigs, - Function firstHeaderGetter, BiConsumer headerAdder) { - Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); + public static T prepareResponse(String path, List crossOriginConfigs, + Supplier> secondaryCrossOriginLookup, + RequestAdapter request, + ResponseFactory responseFactory) { + CrossOrigin crossOrigin = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup) + .orElseThrow(() -> new IllegalArgumentException( + "Could not locate expected CORS information while preparing response to request " + request)); // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials - String origin = firstHeaderGetter.apply(ORIGIN); - if (crossOrigin.get().allowCredentials()) { - headerAdder.accept(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + Optional originOpt = request.firstHeader(ORIGIN); + if (originOpt.isEmpty()) { + return responseFactory.forbidden(noRequiredHeader(ORIGIN)); + } + String origin = originOpt.get(); + if (crossOrigin.allowCredentials()) { + responseFactory.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") + .addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); } else { - List allowedOrigins = Arrays.asList(crossOrigin.get().value()); - headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); + List allowedOrigins = Arrays.asList(crossOrigin.value()); + responseFactory.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); } // Add Access-Control-Expose-Headers if non-empty - formatHeader(crossOrigin.get().exposeHeaders()).ifPresent( - h -> headerAdder.accept(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + formatHeader(crossOrigin.exposeHeaders()).ifPresent( + h -> responseFactory.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + + return responseFactory.build(); } /** @@ -188,63 +273,66 @@ public static void prepareCorsResponse(String path, List cros * * @param path possibly non-normalized path from the request * @param crossOriginConfigs config information for CORS - * @param firstHeaderGetter accepts a header name and returns the first value, if any; null otherwise - * @param allHeaderGetter accepts a key and returns a list of values, if any, associated with that key; empty list otherwise - * @param responseSetter accepts an error message and a status code and sets those values in an HTTP response and returns - * that response - * @param responseStatusSetter accepts an Integer status code and returns a response with that status set - * @param headerAdder accepts a header name and value and adds it as a header to an HTTP response + * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) + * @param request the request + * @param responseFactory factory for preparing and creating the response * @param the type for the returned HTTP response (as returned from the response setter functions) * @return the T returned by the responseStatusSetter with CORS-related headers set via headerAdder (for a successful */ public static T processPreFlight(String path, - List crossOriginConfigs, Function firstHeaderGetter, - Function> allHeaderGetter, - BiFunction responseSetter, Function responseStatusSetter, - BiConsumer headerAdder) { + List crossOriginConfigs, + Supplier> secondaryCrossOriginLookup, + RequestAdapter request, + ResponseFactory responseFactory) { - String origin = firstHeaderGetter.apply(ORIGIN); - Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs); + Optional originOpt = request.firstHeader(ORIGIN); + Optional crossOriginOpt = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup); // If CORS not enabled, deny request - if (!crossOrigin.isPresent()) { - return forbidden(ORIGIN_DENIED, responseSetter); + if (crossOriginOpt.isEmpty()) { + return responseFactory.forbidden(ORIGIN_DENIED); + } + if (originOpt.isEmpty()) { + return responseFactory.forbidden(noRequiredHeader(ORIGIN)); } + CrossOrigin crossOrigin = crossOriginOpt.get(); + // If enabled but not whitelisted, deny request - List allowedOrigins = Arrays.asList(crossOrigin.get().value()); - if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { - return forbidden(ORIGIN_NOT_IN_ALLOWED_LIST, responseSetter); + List allowedOrigins = Arrays.asList(crossOrigin.value()); + if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { + return responseFactory.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); } // Check if method is allowed - String method = firstHeaderGetter.apply(ACCESS_CONTROL_REQUEST_METHOD); - List allowedMethods = Arrays.asList(crossOrigin.get().allowMethods()); + Optional method = request.firstHeader(ACCESS_CONTROL_REQUEST_METHOD); + List allowedMethods = Arrays.asList(crossOrigin.allowMethods()); if (!allowedMethods.contains("*") && !contains(method, allowedMethods, String::equals)) { - return forbidden(METHOD_NOT_IN_ALLOWED_LIST, responseSetter); + return responseFactory.forbidden(METHOD_NOT_IN_ALLOWED_LIST); } // Check if headers are allowed - Set requestHeaders = parseHeader(allHeaderGetter.apply(ACCESS_CONTROL_REQUEST_HEADERS)); - List allowedHeaders = Arrays.asList(crossOrigin.get().allowHeaders()); + Set requestHeaders = parseHeader(request.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); + List allowedHeaders = Arrays.asList(crossOrigin.allowHeaders()); if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { - return forbidden(HEADERS_NOT_IN_ALLOWED_LIST, responseSetter); + return responseFactory.forbidden(HEADERS_NOT_IN_ALLOWED_LIST); } // Build successful response - T response = responseStatusSetter.apply(Http.Status.OK_200.code()); - headerAdder.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); - if (crossOrigin.get().allowCredentials()) { - headerAdder.accept(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + + responseFactory.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, originOpt.get()); + if (crossOrigin.allowCredentials()) { + responseFactory.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } - headerAdder.accept(ACCESS_CONTROL_ALLOW_METHODS, method); + responseFactory.addHeader(ACCESS_CONTROL_ALLOW_METHODS, + method.orElseThrow(noRequiredHeaderExcFactory(ACCESS_CONTROL_REQUEST_METHOD))); formatHeader(requestHeaders.toArray()).ifPresent( - h -> headerAdder.accept(ACCESS_CONTROL_ALLOW_HEADERS, h)); - long maxAge = crossOrigin.get().maxAge(); + h -> responseFactory.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, h)); + long maxAge = crossOrigin.maxAge(); if (maxAge > 0) { - headerAdder.accept(ACCESS_CONTROL_MAX_AGE, maxAge); + responseFactory.addHeader(ACCESS_CONTROL_MAX_AGE, maxAge); } - return response; + return responseFactory.build(); } /** @@ -318,13 +406,15 @@ static String normalize(String path) { } /** - * Returns response with forbidden status and entity created from message. + * Checks containment in a {@code Collection}. * - * @param message Message in entity. - * @return A {@code Response} instance. + * @param item The string. + * @param collection The collection. + * @param eq Equality function. + * @return Outcome of test. */ - static T forbidden(String message, BiFunction responseSetter) { - return responseSetter.apply(message, Http.Status.FORBIDDEN_403.code()); + static boolean contains(Optional item, Collection collection, BiFunction eq) { + return item.isPresent() ? contains(item.get(), collection, eq) : false; } /** @@ -359,4 +449,12 @@ static boolean contains(Collection left, Collection right) { } return true; } + + private static Supplier noRequiredHeaderExcFactory(String header) { + return () -> new IllegalArgumentException(noRequiredHeader(header)); + } + + private static String noRequiredHeader(String header) { + return "CORS request does not have required header " + header; + } } From 7347eb1dfcdd652bad4b94fa52d0d83ad47aa17a Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sat, 4 Apr 2020 07:35:34 -0500 Subject: [PATCH 020/100] Rename some things, clean up unchecked warnings in test classes --- .../java/io/helidon/cors/CORSSupport.java | 35 ++++---- .../io/helidon/cors/CrossOriginHelper.java | 80 +++++++++--------- .../test/java/io/helidon/cors/CORSTest.java | 4 +- .../java/io/helidon/cors/CustomMatchers.java | 2 +- .../test/java/io/helidon/cors/TestUtil.java | 82 +------------------ 5 files changed, 64 insertions(+), 139 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 2083fb347e5..13442920892 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -18,11 +18,11 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; -import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; import io.helidon.cors.CrossOriginHelper.RequestType; +import io.helidon.cors.CrossOriginHelper.ResponseAdapter; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; @@ -31,10 +31,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.logging.Logger; import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; @@ -106,20 +102,20 @@ private void handleCORS(ServerRequest request, ServerResponse response) { switch (requestType) { case PREFLIGHT: - ServerResponse preflightResponse = CrossOriginHelper.processPreFlight(request.path().toString(), + ServerResponse preflightResponse = CrossOriginHelper.processPreFlight( crossOriginConfigs, () -> Optional.empty(), requestAdapter, - new ResponseFactory(response)); + new SEResponseAdapter(response)); preflightResponse.send(); break; case CORS: - Optional corsResponse = CrossOriginHelper.processRequest(request.path().toString(), + Optional corsResponse = CrossOriginHelper.processRequest( crossOriginConfigs, () -> Optional.empty(), requestAdapter, - new ResponseFactory(response)); + new SEResponseAdapter(response)); /* * Any response carries a CORS error which we send immediately. Otherwise, since we know this is a CORS * request, do the CORS post-processing and then pass the baton to the next handler. @@ -138,15 +134,18 @@ private void handleCORS(ServerRequest request, ServerResponse response) { } private void finishCORSResponse(ServerRequest request, ServerResponse response) { - CrossOriginHelper.prepareResponse(request.path().toString(), + CrossOriginHelper.prepareResponse( crossOriginConfigs, () -> Optional.empty(), new RequestAdapter(request), - new ResponseFactory(response)); + new SEResponseAdapter(response)); request.next(); } + /** + * Builder for {@code CORSSupport} instances. + */ public static class Builder implements io.helidon.common.Builder { private Optional corsConfig = Optional.empty(); @@ -186,6 +185,12 @@ private static class RequestAdapter implements CrossOriginHelper.RequestAdapter RequestAdapter(ServerRequest request) { this.request = request; } + + @Override + public String path() { + return request.path().toString(); + } + @Override public Optional firstHeader(String key) { return request.headers().first(key); @@ -207,22 +212,22 @@ public String method() { } } - private static class ResponseFactory implements CrossOriginHelper.ResponseFactory { + private static class SEResponseAdapter implements ResponseAdapter { private final ServerResponse serverResponse; - ResponseFactory(ServerResponse serverResponse) { + SEResponseAdapter(ServerResponse serverResponse) { this.serverResponse = serverResponse; } @Override - public CrossOriginHelper.ResponseFactory addHeader(String key, String value) { + public ResponseAdapter addHeader(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public CrossOriginHelper.ResponseFactory addHeader(String key, Object value) { + public ResponseAdapter addHeader(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index bc502a91a23..f7e9ce63194 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -79,6 +79,12 @@ public enum RequestType { */ public interface RequestAdapter { + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + /** * Retrieves the first value for the specified header as a String. * @@ -116,7 +122,7 @@ public interface RequestAdapter { * * @param the type of the response created by the factory */ - public interface ResponseFactory { + public interface ResponseAdapter { /** * Arranges to add the specified header and value to the eventual response. @@ -125,7 +131,7 @@ public interface ResponseFactory { * @param value header value to add * @return the factory */ - ResponseFactory addHeader(String key, String value); + ResponseAdapter addHeader(String key, String value); /** * Arranges to add the specified header and value to the eventual response. @@ -134,7 +140,7 @@ public interface ResponseFactory { * @param value header value to add * @return the factory */ - ResponseFactory addHeader(String key, Object value); + ResponseAdapter addHeader(String key, Object value); /** @@ -200,30 +206,29 @@ static Optional lookupCrossOrigin(String path, List the type for the HTTP response as returned from the responseSetter * @return Optional of an error response (returned by the responseSetter) if the request was an invalid CORS request; * Optional.empty() if it was a valid CORS request */ - public static Optional processRequest(String path, + public static Optional processRequest( List crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter request, - ResponseFactory responseFactory) { + ResponseAdapter responseAdapter) { Optional origin = request.firstHeader(ORIGIN); - Optional crossOrigin = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup); + Optional crossOrigin = lookupCrossOrigin(request.path(), crossOriginConfigs, secondaryCrossOriginLookup); if (crossOrigin.isEmpty()) { - return Optional.of(responseFactory.forbidden(ORIGIN_DENIED)); + return Optional.of(responseAdapter.forbidden(ORIGIN_DENIED)); } // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.get().value()); if (!allowedOrigins.contains("*") && !contains(origin, allowedOrigins, String::equals)) { - return Optional.of(responseFactory.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); + return Optional.of(responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); } // Successful processing of request @@ -233,67 +238,62 @@ public static Optional processRequest(String path, /** * Prepares a CORS response. * - * @param path the possibly non-normalized path from the request * @param crossOriginConfigs config information for CORS * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param request request - * @param responseFactory response factory + * @param responseAdapter response factory */ - public static T prepareResponse(String path, List crossOriginConfigs, + public static T prepareResponse(List crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter request, - ResponseFactory responseFactory) { - CrossOrigin crossOrigin = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup) + ResponseAdapter responseAdapter) { + CrossOrigin crossOrigin = lookupCrossOrigin(request.path(), crossOriginConfigs, secondaryCrossOriginLookup) .orElseThrow(() -> new IllegalArgumentException( "Could not locate expected CORS information while preparing response to request " + request)); // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials - Optional originOpt = request.firstHeader(ORIGIN); - if (originOpt.isEmpty()) { - return responseFactory.forbidden(noRequiredHeader(ORIGIN)); - } - String origin = originOpt.get(); + String origin = request.firstHeader(ORIGIN).orElseThrow(noRequiredHeaderExcFactory(ORIGIN)); + if (crossOrigin.allowCredentials()) { - responseFactory.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") + responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") .addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); } else { List allowedOrigins = Arrays.asList(crossOrigin.value()); - responseFactory.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); + responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); } // Add Access-Control-Expose-Headers if non-empty formatHeader(crossOrigin.exposeHeaders()).ifPresent( - h -> responseFactory.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + h -> responseAdapter.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, h)); - return responseFactory.build(); + return responseAdapter.build(); } /** * Processes a pre-flight request. * - * @param path possibly non-normalized path from the request * @param crossOriginConfigs config information for CORS * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param request the request - * @param responseFactory factory for preparing and creating the response + * @param responseAdapter factory for preparing and creating the response * @param the type for the returned HTTP response (as returned from the response setter functions) * @return the T returned by the responseStatusSetter with CORS-related headers set via headerAdder (for a successful */ - public static T processPreFlight(String path, + public static T processPreFlight( List crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter request, - ResponseFactory responseFactory) { + ResponseAdapter responseAdapter) { Optional originOpt = request.firstHeader(ORIGIN); - Optional crossOriginOpt = lookupCrossOrigin(path, crossOriginConfigs, secondaryCrossOriginLookup); + Optional crossOriginOpt = lookupCrossOrigin(request.path(), crossOriginConfigs, secondaryCrossOriginLookup); // If CORS not enabled, deny request if (crossOriginOpt.isEmpty()) { - return responseFactory.forbidden(ORIGIN_DENIED); + return responseAdapter.forbidden(ORIGIN_DENIED); } if (originOpt.isEmpty()) { - return responseFactory.forbidden(noRequiredHeader(ORIGIN)); + return responseAdapter.forbidden(noRequiredHeader(ORIGIN)); } CrossOrigin crossOrigin = crossOriginOpt.get(); @@ -301,38 +301,38 @@ public static T processPreFlight(String path, // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.value()); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { - return responseFactory.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); + return responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); } // Check if method is allowed Optional method = request.firstHeader(ACCESS_CONTROL_REQUEST_METHOD); List allowedMethods = Arrays.asList(crossOrigin.allowMethods()); if (!allowedMethods.contains("*") && !contains(method, allowedMethods, String::equals)) { - return responseFactory.forbidden(METHOD_NOT_IN_ALLOWED_LIST); + return responseAdapter.forbidden(METHOD_NOT_IN_ALLOWED_LIST); } // Check if headers are allowed Set requestHeaders = parseHeader(request.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); List allowedHeaders = Arrays.asList(crossOrigin.allowHeaders()); if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { - return responseFactory.forbidden(HEADERS_NOT_IN_ALLOWED_LIST); + return responseAdapter.forbidden(HEADERS_NOT_IN_ALLOWED_LIST); } // Build successful response - responseFactory.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, originOpt.get()); + responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, originOpt.get()); if (crossOrigin.allowCredentials()) { - responseFactory.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } - responseFactory.addHeader(ACCESS_CONTROL_ALLOW_METHODS, + responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_METHODS, method.orElseThrow(noRequiredHeaderExcFactory(ACCESS_CONTROL_REQUEST_METHOD))); formatHeader(requestHeaders.toArray()).ifPresent( - h -> responseFactory.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, h)); + h -> responseAdapter.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, h)); long maxAge = crossOrigin.maxAge(); if (maxAge > 0) { - responseFactory.addHeader(ACCESS_CONTROL_MAX_AGE, maxAge); + responseAdapter.addHeader(ACCESS_CONTROL_MAX_AGE, maxAge); } - return responseFactory.build(); + return responseAdapter.build(); } /** diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index 12c47486314..e64f06e44c9 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -58,8 +58,7 @@ public class CORSTest { @BeforeAll public static void startup() throws InterruptedException, ExecutionException, TimeoutException { Routing.Builder routingBuilder = TestUtil.prepRouting() - .register(CORSSupport.builder()) - .register("/greet", () -> new GreetService()); + .register("/greet", new GreetService()); CORSTestServices.SERVICES.forEach(s -> routingBuilder.register(s.path(), s)); server = TestUtil.startServer(0, routingBuilder); @@ -83,7 +82,6 @@ public void testSimple() throws Exception { .toCompletableFuture() .get(); - String msg = response.content().as(String.class).toCompletableFuture().get(); Http.ResponseStatus result = response.status(); assertThat(result.code(), is(Http.Status.OK_200.code())); diff --git a/cors/src/test/java/io/helidon/cors/CustomMatchers.java b/cors/src/test/java/io/helidon/cors/CustomMatchers.java index 0f9ca0c47da..0fc02d49952 100644 --- a/cors/src/test/java/io/helidon/cors/CustomMatchers.java +++ b/cors/src/test/java/io/helidon/cors/CustomMatchers.java @@ -27,7 +27,7 @@ */ class CustomMatchers { - static Present present(Matcher matcher) { + static Present present(Matcher matcher) { return new Present(matcher); } diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index f0430dd5fdc..f1e808e9de6 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -16,82 +16,30 @@ */ package io.helidon.cors; -import io.helidon.common.http.MediaType; import io.helidon.config.Config; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; -import io.helidon.webserver.Service; import io.helidon.webserver.WebServer; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; public class TestUtil { - private static final Logger LOGGER = Logger.getLogger(TestUtil.class.getName()); - - /** - * Starts the web server at an available port and sets up CORS using the supplied builder. - * - * @param builders the {@code Builder}s to set up for the server. - * @return the {@code WebServer} set up with OpenAPI support - */ -// public static WebServer startServer(Supplier... builders) { -// try { -// return startServer(0, builders); -// } catch (InterruptedException | ExecutionException | TimeoutException ex) { -// throw new RuntimeException("Error starting server for test", ex); -// } -// } - public static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, ExecutionException, TimeoutException { Config config = Config.create(); ServerConfiguration serverConfig = ServerConfiguration.builder(config) .port(port) .build(); - WebServer server = WebServer.create(serverConfig, routingBuilder).start().toCompletableFuture().get(10, TimeUnit.SECONDS); - return server; + return WebServer.create(serverConfig, routingBuilder).start().toCompletableFuture().get(10, TimeUnit.SECONDS); } static Routing.Builder prepRouting() { Config config = Config.create(); CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); return Routing.builder() - .register(corsSupportBuilder); - } - - /** - * Start the Web Server - * - * @param port the port on which to start the server; if less than 1, the port is dynamically selected - * @param builders Builder instances to use in starting the server - * @return {@code WebServer} that has been started - * @throws java.lang.InterruptedException if the start was interrupted - * @throws java.util.concurrent.ExecutionException if the start failed - * @throws java.util.concurrent.TimeoutException if the start timed out - */ - public static WebServer startServer( - int port, - Supplier... builders) throws - InterruptedException, ExecutionException, TimeoutException { - - WebServer result = WebServer.create(ServerConfiguration.builder() - .port(port) - .build(), - Routing.builder() - .register(builders) - .build()) - .start() - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); - LOGGER.log(Level.INFO, "Started server at: https://localhost:{0}", result.port()); - return result; + .register(corsSupportBuilder.build()); } /** @@ -123,30 +71,4 @@ public static void stopServer(WebServer server) throws server.shutdown().toCompletableFuture().get(10, TimeUnit.SECONDS); } } - - /** - * Returns a {@code HttpURLConnection} for the requested method and path and - * {code @MediaType} from the specified {@link WebServer}. - * - * @param port port to connect to - * @param method HTTP method to use in building the connection - * @param path path to the resource in the web server - * @param mediaType {@code MediaType} to be Accepted - * @return the connection to the server and path - * @throws Exception in case of errors creating the connection - */ - public static HttpURLConnection getURLConnection( - int port, - String method, - String path, - MediaType mediaType) throws Exception { - URL url = new URL("http://localhost:" + port + path); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod(method); - if (mediaType != null) { - conn.setRequestProperty("Accept", mediaType.toString()); - } - System.out.println("Connecting: " + method + " " + url); - return conn; - } } From c042cb1ebbcb379e65fd8f429ed92a58ffc0291b Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sat, 4 Apr 2020 08:09:19 -0500 Subject: [PATCH 021/100] Minor method rename --- cors/src/main/java/io/helidon/cors/CORSSupport.java | 2 +- cors/src/main/java/io/helidon/cors/CrossOriginHelper.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 13442920892..f55fd96fde6 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -239,7 +239,7 @@ public ServerResponse forbidden(String message) { } @Override - public ServerResponse build() { + public ServerResponse get() { return serverResponse; } } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index f7e9ce63194..86bca4dd463 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -156,7 +156,7 @@ public interface ResponseAdapter { * * @return new response instance */ - T build(); + T get(); } /** @@ -266,7 +266,7 @@ public static T prepareResponse(List crossOriginConfigs, formatHeader(crossOrigin.exposeHeaders()).ifPresent( h -> responseAdapter.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, h)); - return responseAdapter.build(); + return responseAdapter.get(); } /** @@ -332,7 +332,7 @@ public static T processPreFlight( if (maxAge > 0) { responseAdapter.addHeader(ACCESS_CONTROL_MAX_AGE, maxAge); } - return responseAdapter.build(); + return responseAdapter.get(); } /** From 255c26164961f86b1707eababb462de457adc0ea Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sat, 4 Apr 2020 12:46:55 -0500 Subject: [PATCH 022/100] Adapt MP CORS code to layer on the new SE CORS artifact --- microprofile/cors/pom.xml | 7 +- .../microprofile/cors/CrossOrigin.java | 132 ------- .../cors/CrossOriginAutoDiscoverable.java | 2 +- .../microprofile/cors/CrossOriginConfig.java | 179 --------- .../microprofile/cors/CrossOriginFilter.java | 166 +++++++- .../microprofile/cors/CrossOriginHelper.java | 367 ------------------ .../cors/CrossOriginHelperMP.java | 81 ++++ .../cors/src/main/java/module-info.java | 1 + .../microprofile/cors/CrossOriginTest.java | 17 +- .../META-INF/microprofile-config.properties | 15 + 10 files changed, 263 insertions(+), 704 deletions(-) delete mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java delete mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginConfig.java delete mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelper.java create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelperMP.java diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 3404dcee107..70c8956cd18 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -1,7 +1,7 @@ + + io.helidon.cors + helidon-cors + ${helidon.version} + diff --git a/cors/pom.xml b/cors/pom.xml index a5288a76d7c..3092c9d8624 100644 --- a/cors/pom.xml +++ b/cors/pom.xml @@ -38,10 +38,6 @@ - - - - io.helidon.webserver From 29aaed2f8a31228850e52ea9d44fcabd0ef2e1f4 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 05:45:55 -0500 Subject: [PATCH 034/100] Remove explicit version for cors from pom; revise module-info so downstream projects can see CrossOrigin from cors --- microprofile/cors/pom.xml | 1 - microprofile/cors/src/main/java/module-info.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 70c8956cd18..9abf519ae72 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -55,7 +55,6 @@ io.helidon.cors helidon-cors - ${project.version} io.helidon.microprofile.bundles diff --git a/microprofile/cors/src/main/java/module-info.java b/microprofile/cors/src/main/java/module-info.java index adcabbd2179..35ee23ba9e0 100644 --- a/microprofile/cors/src/main/java/module-info.java +++ b/microprofile/cors/src/main/java/module-info.java @@ -21,7 +21,7 @@ requires transitive java.ws.rs; requires io.helidon.config; - requires io.helidon.cors; + requires transitive io.helidon.cors; // for CrossOrigin requires jersey.common; requires microprofile.config.api; From 23a61f1afe32e19d9b086fdaaf76a2cc575d3e30 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 10:06:31 -0500 Subject: [PATCH 035/100] Fix tests so each app adds its own CORS service instance, since each will have its own config --- .../test/java/io/helidon/cors/CORSTest.java | 81 ++++-------- .../io/helidon/cors/CORSTestServices.java | 49 +++----- .../java/io/helidon/cors/GreetService.java | 17 ++- .../io/helidon/cors/TestTwoCORSConfigs.java | 62 ++++++++++ .../test/java/io/helidon/cors/TestUtil.java | 116 +++++++++++++++++- cors/src/test/resources/twoCORS.yaml | 22 ++++ 6 files changed, 253 insertions(+), 94 deletions(-) create mode 100644 cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java create mode 100644 cors/src/test/resources/twoCORS.yaml diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index e64f06e44c9..cb20622b61d 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -22,15 +22,14 @@ import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.common.http.MediaType; -import io.helidon.cors.CORSTestServices.Service1; -import io.helidon.cors.CORSTestServices.Service2; -import io.helidon.cors.CORSTestServices.Service3; import io.helidon.webclient.WebClient; import io.helidon.webclient.WebClientRequestBuilder; import io.helidon.webclient.WebClientResponse; -import io.helidon.webserver.Routing; import io.helidon.webserver.WebServer; +import static io.helidon.cors.CORSTestServices.SERVICE_1; +import static io.helidon.cors.CORSTestServices.SERVICE_2; +import static io.helidon.cors.CORSTestServices.SERVICE_3; import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; @@ -52,19 +51,14 @@ public class CORSTest { + private static final String CONTEXT_ROOT = "/greet"; private static WebServer server; private static WebClient client; @BeforeAll public static void startup() throws InterruptedException, ExecutionException, TimeoutException { - Routing.Builder routingBuilder = TestUtil.prepRouting() - .register("/greet", new GreetService()); - CORSTestServices.SERVICES.forEach(s -> routingBuilder.register(s.path(), s)); - - server = TestUtil.startServer(0, routingBuilder); - client = WebClient.builder() - .baseUri("http://localhost:" + server.port()) - .build(); + server = TestUtil.startupServerWithApps(); + client = TestUtil.startupClient(server); } @AfterAll @@ -76,7 +70,7 @@ public static void shutdown() { public void testSimple() throws Exception { WebClientResponse response = client.get() - .path("/greet") + .path(CONTEXT_ROOT) .accept(MediaType.TEXT_PLAIN) .request() .toCompletableFuture() @@ -89,21 +83,11 @@ public void testSimple() throws Exception { @Test void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(Service1.PATH); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); + String origin = "http://foo.bar"; + WebClientResponse res = TestUtil.runTest1PreFlightAllowedOrigin(client, CONTEXT_ROOT, origin); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(origin))); assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); @@ -113,7 +97,7 @@ void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service1.PATH); + .path(TestUtil.path(SERVICE_1)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -136,7 +120,7 @@ void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedExcep void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service1.PATH); + .path(TestUtil.path(SERVICE_1)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -160,7 +144,7 @@ void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedExcep void test2PreFlightForbiddenOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://not.allowed"); @@ -178,7 +162,7 @@ void test2PreFlightForbiddenOrigin() throws ExecutionException, InterruptedExcep void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -202,7 +186,7 @@ void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti void test2PreFlightForbiddenMethod() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -220,7 +204,7 @@ void test2PreFlightForbiddenMethod() throws ExecutionException, InterruptedExcep void test2PreFlightForbiddenHeader() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -237,33 +221,14 @@ void test2PreFlightForbiddenHeader() throws ExecutionException, InterruptedExcep @Test void test2PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); + TestUtil.test2PreFlightAllowedHeaders1(client, CONTEXT_ROOT,"http://foo.bar", "X-foo"); } @Test void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -288,7 +253,7 @@ void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedExcep void test2PreFlightAllowedHeaders3() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service2.PATH); + .path(TestUtil.path(SERVICE_2)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -314,7 +279,7 @@ void test2PreFlightAllowedHeaders3() throws ExecutionException, InterruptedExcep void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .put() - .path(Service1.PATH) + .path(TestUtil.path(SERVICE_1)) .contentType(MediaType.TEXT_PLAIN); Headers headers = reqBuilder.headers(); @@ -334,7 +299,7 @@ void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .put() - .path(Service2.PATH) + .path(TestUtil.path(SERVICE_2)) .contentType(MediaType.TEXT_PLAIN); Headers headers = reqBuilder.headers(); @@ -354,7 +319,7 @@ void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .method(Http.Method.OPTIONS.name()) - .path(Service3.PATH); + .path(TestUtil.path(SERVICE_3)); Headers headers = reqBuilder.headers(); headers.add(ORIGIN, "http://foo.bar"); @@ -376,7 +341,7 @@ void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedExcepti void test3ActualAllowedOrigin() throws ExecutionException, InterruptedException { WebClientRequestBuilder reqBuilder = client .put() - .path(Service3.PATH) + .path(TestUtil.path(SERVICE_3)) .contentType(MediaType.TEXT_PLAIN); Headers headers = reqBuilder.headers(); diff --git a/cors/src/test/java/io/helidon/cors/CORSTestServices.java b/cors/src/test/java/io/helidon/cors/CORSTestServices.java index 16f7185d499..34f0481072f 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTestServices.java +++ b/cors/src/test/java/io/helidon/cors/CORSTestServices.java @@ -27,9 +27,24 @@ class CORSTestServices { - static final List SERVICES = List.of(new Service1(), new Service2(), new Service3()); + static final CORSTestService SERVICE_1 = new CORSTestService("/cors1"); + static final CORSTestService SERVICE_2 = new CORSTestService("/cors2"); + static final CORSTestService SERVICE_3 = new CORSTestService("/cors3"); + static final CORSTestService SERVICE_4 = new CORSTestService("/cors4"); - static abstract class AbstractCORSTestService implements Service { + static final List SERVICES = List.of(SERVICE_1, SERVICE_2, SERVICE_3, SERVICE_4); + + static class CORSTestService implements Service { + + private final String path; + + CORSTestService(String path) { + this.path = path; + } + + String path() { + return path; + } void ok(ServerRequest request, ServerResponse response) { response.status(Http.Status.OK_200.code()); @@ -43,35 +58,5 @@ public void update(Routing.Rules rules) { .put(this::ok) ; } - - abstract String path(); - } - - static class Service1 extends AbstractCORSTestService { - - static final String PATH = "/cors1"; - - @Override - String path() { - return PATH; - } - } - - static class Service2 extends AbstractCORSTestService { - static final String PATH = "/cors2"; - - @Override - String path() { - return PATH; - } - } - - static class Service3 extends AbstractCORSTestService { - static final String PATH = "/cors3"; - - @Override - String path() { - return PATH; - } } } diff --git a/cors/src/test/java/io/helidon/cors/GreetService.java b/cors/src/test/java/io/helidon/cors/GreetService.java index d4d92d58932..f9970d62533 100644 --- a/cors/src/test/java/io/helidon/cors/GreetService.java +++ b/cors/src/test/java/io/helidon/cors/GreetService.java @@ -23,16 +23,31 @@ import io.helidon.webserver.Service; import java.util.Date; +import java.util.stream.Stream; public class GreetService implements Service { + private String greeting; + + GreetService() { + this("Hello"); + } + + GreetService(String initialGreeting) { + greeting = initialGreeting; + } + @Override public void update(Routing.Rules rules) { rules.get("/", this::getDefaultMessageHandler); + // Add the cors paths so requests to them have a place to land. + for (int i = 1; i <=3; i++) { + rules.any("/cors" + i, this::getDefaultMessageHandler); + } } private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { - String msg = String.format("%s %s!", "Hello", new Date().toString()); + String msg = String.format("%s %s!", greeting, new Date().toString()); response.status(Http.Status.OK_200.code()); response.send(msg); } diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java new file mode 100644 index 00000000000..f3ef80540e8 --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.cors; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import io.helidon.common.http.Http; +import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientResponse; +import io.helidon.webserver.WebServer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestTwoCORSConfigs { + + private static WebServer server; + private static WebClient client; + + @BeforeAll + public static void startup() throws InterruptedException, ExecutionException, TimeoutException { + server = TestUtil.startupServerWithApps(); + client = TestUtil.startupClient(server); + } + + @Test + void test1PreFlightAllowedOriginOtherGreeting() throws ExecutionException, InterruptedException { + WebClientResponse res = TestUtil.runTest1PreFlightAllowedOrigin(client, TestUtil.OTHER_GREETING_PATH, + "http://otherfoo.bar"); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + + + @AfterAll + public static void shutdown() { + TestUtil.shutdownServer(server); + } + + +} diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index f1e808e9de6..dcdcc66de84 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -16,7 +16,14 @@ */ package io.helidon.cors; +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.cors.CORSTestServices.CORSTestService; +import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientRequestBuilder; +import io.helidon.webclient.WebClientResponse; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.WebServer; @@ -25,9 +32,28 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static io.helidon.cors.CORSTestServices.SERVICE_1; +import static io.helidon.cors.CORSTestServices.SERVICE_2; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CustomMatchers.notPresent; +import static io.helidon.cors.CustomMatchers.present; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class TestUtil { - public static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, ExecutionException, TimeoutException { + static final String GREETING_PATH = "/greet"; + static final String OTHER_GREETING_PATH = "/othergreet"; + + static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, ExecutionException, TimeoutException { Config config = Config.create(); ServerConfiguration serverConfig = ServerConfiguration.builder(config) .port(port) @@ -36,10 +62,86 @@ public static WebServer startServer(int port, Routing.Builder routingBuilder) th } static Routing.Builder prepRouting() { + /* + * Use the default config for the service at "/greet" and load a specific config for "/othergreet". + */ Config config = Config.create(); CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); - return Routing.builder() - .register(corsSupportBuilder.build()); + + Config twoCORSConfig = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .sources(ConfigSources.classpath("twoCORS.yaml")) + .build(); + CORSSupport.Builder twoCORSSupportBuilder = + CORSSupport.builder().config(twoCORSConfig.get(CrossOriginHelper.CORS_CONFIG_KEY)); + + Routing.Builder builder = Routing.builder() + .register(GREETING_PATH, corsSupportBuilder.build(), new GreetService()) + .register(OTHER_GREETING_PATH, twoCORSSupportBuilder.build(), new GreetService("Other Hello")); +// CORSTestServices.SERVICES.forEach(s -> { +// builder.register(path(s), s); +// builder.register(path(OTHER_GREETING_PATH, s), s); +// }); + + return builder; + } + + static WebServer startupServerWithApps() throws InterruptedException, ExecutionException, TimeoutException { + Routing.Builder routingBuilder = TestUtil.prepRouting(); + return startServer(0, routingBuilder); + } + + static WebClient startupClient(WebServer server) { + return WebClient.builder() + .baseUri("http://localhost:" + server.port()) + .build(); + } + + static WebClientResponse runTest1PreFlightAllowedOrigin(WebClient client, String prefix, String origin) throws ExecutionException, + InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(path(prefix, SERVICE_1)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, origin); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + return res; + } + + static void test2PreFlightAllowedHeaders1(WebClient client, String prefix, String origin, String headerToCheck) throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(path(prefix, SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, origin); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, headerToCheck); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(origin))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString(headerToCheck))); + assertThat(res.headers() + .first(ACCESS_CONTROL_MAX_AGE), notPresent()); } /** @@ -71,4 +173,12 @@ public static void stopServer(WebServer server) throws server.shutdown().toCompletableFuture().get(10, TimeUnit.SECONDS); } } + + static String path(CORSTestService testService) { + return GREETING_PATH + testService.path(); + } + + static String path(String prefix, CORSTestService testService) { + return prefix + testService.path(); + } } diff --git a/cors/src/test/resources/twoCORS.yaml b/cors/src/test/resources/twoCORS.yaml new file mode 100644 index 00000000000..38027bcc324 --- /dev/null +++ b/cors/src/test/resources/twoCORS.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# 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. +# +cors: + - path-prefix: /cors2 + allow-origins: ["http://otherfoo.bar", "http://otherbar.foo"] + allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-otherBar", "X-otherFoo"] + allow-credentials: true + max-age: -1 From eee6c7519c3b061edd745fc5bae2072d12a34c76 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 11:20:51 -0500 Subject: [PATCH 036/100] Clarify some comments, clean up one spot in the code. --- .../io/helidon/cors/CrossOriginHelper.java | 32 +++++++------- .../test/java/io/helidon/cors/TestUtil.java | 42 ++++++++++++------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index f1359b20325..a68a0cdf3e0 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -247,16 +247,12 @@ public static void prepareResponse(List crossOriginCon RequestType requestType = requestType(requestAdapter); - switch (requestType) { - case CORS: - prepareCORSResponse( - crossOriginConfigs, - secondaryCrossOriginLookup, - requestAdapter, - responseAdapter); - break; - - default: + if (requestType == RequestType.CORS) { + prepareCORSResponse( + crossOriginConfigs, + secondaryCrossOriginLookup, + requestAdapter, + responseAdapter); } } @@ -321,7 +317,7 @@ static Optional processCORSRequest( } /** - * Prepares a CORS response. + * Prepares a CORS response by updating the response's headers. * * @param crossOriginConfigs config information for CORS * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) @@ -363,6 +359,9 @@ static U prepareCORSResponse(List crossOriginConfigs, /** * Processes a pre-flight request, returning either a preflight response or an error response if the CORS information was * invalid. + *

+ * Having determined that we have a pre-flight request, we will always return either a forbidden or a successful response. + *

* * @param crossOriginConfigs config information for CORS * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) @@ -398,15 +397,18 @@ static U processCORSPreFlightRequest( return responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); } - // Check if method is allowed Optional methodOpt = requestAdapter.firstHeader(ACCESS_CONTROL_REQUEST_METHOD); + if (methodOpt.isEmpty()) { + return responseAdapter.forbidden(METHOD_NOT_IN_ALLOWED_LIST); + } + + // Check if method is allowed + String method = methodOpt.get(); List allowedMethods = Arrays.asList(crossOrigin.allowMethods()); if (!allowedMethods.contains("*") - && methodOpt.isPresent() - && !contains(methodOpt.get(), allowedMethods, String::equals)) { + && !contains(method, allowedMethods, String::equals)) { return responseAdapter.forbidden(METHOD_NOT_IN_ALLOWED_LIST); } - String method = methodOpt.get(); // Check if headers are allowed Set requestHeaders = parseHeader(requestAdapter.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index dcdcc66de84..a33b21afe7c 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -20,6 +20,7 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.config.ConfigSources; +import io.helidon.config.spi.ConfigSource; import io.helidon.cors.CORSTestServices.CORSTestService; import io.helidon.webclient.WebClient; import io.helidon.webclient.WebClientRequestBuilder; @@ -31,6 +32,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; @@ -53,7 +55,13 @@ public class TestUtil { static final String GREETING_PATH = "/greet"; static final String OTHER_GREETING_PATH = "/othergreet"; - static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, ExecutionException, TimeoutException { + static WebServer startupServerWithApps() throws InterruptedException, ExecutionException, TimeoutException { + Routing.Builder routingBuilder = TestUtil.prepRouting(); + return startServer(0, routingBuilder); + } + + private static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException, + ExecutionException, TimeoutException { Config config = Config.create(); ServerConfiguration serverConfig = ServerConfiguration.builder(config) .port(port) @@ -63,33 +71,37 @@ static WebServer startServer(int port, Routing.Builder routingBuilder) throws In static Routing.Builder prepRouting() { /* - * Use the default config for the service at "/greet" and load a specific config for "/othergreet". + * Use the default config for the service at "/greet." */ - Config config = Config.create(); + Config config = minimalConfig(); CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); - Config twoCORSConfig = Config.builder() - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .sources(ConfigSources.classpath("twoCORS.yaml")) - .build(); + /* + * Load a specific config for "/othergreet." + */ + Config twoCORSConfig = minimalConfig(ConfigSources.classpath("twoCORS.yaml")); CORSSupport.Builder twoCORSSupportBuilder = CORSSupport.builder().config(twoCORSConfig.get(CrossOriginHelper.CORS_CONFIG_KEY)); Routing.Builder builder = Routing.builder() .register(GREETING_PATH, corsSupportBuilder.build(), new GreetService()) .register(OTHER_GREETING_PATH, twoCORSSupportBuilder.build(), new GreetService("Other Hello")); -// CORSTestServices.SERVICES.forEach(s -> { -// builder.register(path(s), s); -// builder.register(path(OTHER_GREETING_PATH, s), s); -// }); return builder; } - static WebServer startupServerWithApps() throws InterruptedException, ExecutionException, TimeoutException { - Routing.Builder routingBuilder = TestUtil.prepRouting(); - return startServer(0, routingBuilder); + private static Config minimalConfig(Supplier configSource) { + Config.Builder builder = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource(); + if (configSource != null) { + builder.sources(configSource); + } + return builder.build(); + } + + private static Config minimalConfig() { + return minimalConfig(null); } static WebClient startupClient(WebServer server) { From 3c5456deb22ad04ab5fa0737ec8bd0ca45bd19cf Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 11:40:55 -0500 Subject: [PATCH 037/100] Preparing response does not need to return anything --- cors/src/main/java/io/helidon/cors/CrossOriginHelper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index a68a0cdf3e0..3929d2a2a64 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -169,7 +169,7 @@ public interface ResponseAdapter { T forbidden(String message); /** - * Prepares the response type with headers set and status set to OK. + * Prepares the response with only the headers set on this adapter and the status set to OK. * * @return response instance */ @@ -325,9 +325,8 @@ static Optional processCORSRequest( * @param responseAdapter response adapter * @param type for the request wrapped by the requestAdapter * @param type for the response wrapper by the responseAdapter - * @return U the response provided by the responseAdapter */ - static U prepareCORSResponse(List crossOriginConfigs, + static void prepareCORSResponse(List crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -352,8 +351,6 @@ static U prepareCORSResponse(List crossOriginConfigs, // Add Access-Control-Expose-Headers if non-empty formatHeader(crossOrigin.exposeHeaders()).ifPresent( h -> responseAdapter.header(ACCESS_CONTROL_EXPOSE_HEADERS, h)); - - return responseAdapter.ok(); } /** From 4add7c9d1a3653e5dc783409f1c64ca7ffa25068 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 11:58:12 -0500 Subject: [PATCH 038/100] Clean up some doc comments --- .../main/java/io/helidon/cors/CrossOriginHelper.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index 3929d2a2a64..af75f2490a9 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -137,6 +137,12 @@ public interface RequestAdapter { /** * Minimal abstraction of an HTTP response. * + *

+ * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

+ * * @param the type of the response wrapped by the adapter */ public interface ResponseAdapter { @@ -160,7 +166,7 @@ public interface ResponseAdapter { ResponseAdapter header(String key, Object value); /** - * Prepares the response type with the forbidden status and the specified error message, without any headers assigned + * Returns a response with the forbidden status and the specified error message, without any headers assigned * using the {@code header} methods. * * @param message error message to use in setting the response status @@ -169,7 +175,7 @@ public interface ResponseAdapter { T forbidden(String message); /** - * Prepares the response with only the headers set on this adapter and the status set to OK. + * Returns a response with only the headers that were set on this adapter and the status set to OK. * * @return response instance */ @@ -182,10 +188,12 @@ public interface ResponseAdapter { * non-preflight CORS request). *

* If the optional is empty, this processor has either: + *

*
    *
  • recognized the request as a valid non-preflight CORS request and has set headers in the response adapter, or
  • *
  • recognized the request as a non-CORS request entirely.
  • *
+ *

* In either case of an empty optional return value, the caller should proceed with its own request processing and sends its * response at will as long as that processing includes the header settings assigned using the response adapter. *

From f030f2bce5e940cdf1c55ae0e51bc561063d7e53 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 13:46:15 -0500 Subject: [PATCH 039/100] Replace hard-coded constant --- .../java/io/helidon/microprofile/cors/CrossOriginFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index ceca78dfd1b..a22a894f13b 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -46,6 +46,7 @@ import org.eclipse.microprofile.config.ConfigProvider; import static io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; +import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; import static io.helidon.cors.CrossOriginHelper.prepareResponse; /** @@ -61,7 +62,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - crossOriginConfigs = config.get("cors").as(new CrossOriginConfigMapper()).get(); + crossOriginConfigs = config.get(CORS_CONFIG_KEY).as(new CrossOriginConfigMapper()).get(); } @Override From 2ed0560806e00d0babe5444eb795aad755ae676d Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 16:27:53 -0500 Subject: [PATCH 040/100] Refactor CrossOrigin to the MP module and move associated constants from there to CrossOriginConfig --- .../java/io/helidon/cors/CORSSupport.java | 19 +++ .../java/io/helidon/cors/CrossOrigin.java | 132 --------------- .../io/helidon/cors/CrossOriginConfig.java | 150 +++++++++++++++--- .../io/helidon/cors/CrossOriginHelper.java | 40 ++--- .../test/java/io/helidon/cors/CORSTest.java | 16 +- .../test/java/io/helidon/cors/TestUtil.java | 16 +- 6 files changed, 184 insertions(+), 189 deletions(-) delete mode 100644 cors/src/main/java/io/helidon/cors/CrossOrigin.java diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 7d95b08780f..890267ce5ba 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -17,7 +17,9 @@ package io.helidon.cors; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.logging.Logger; @@ -128,6 +130,7 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques public static class Builder implements io.helidon.common.Builder { private Optional corsConfig = Optional.empty(); + private final Map crossOrigins = new HashMap<>(); @Override public CORSSupport build() { @@ -155,6 +158,22 @@ List configs() { return corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) .orElse(Collections.emptyList()); } + + /** + * Adds cross origin information associated with a given path. + * + * @param path the path to which the cross origin information applies + * @param crossOrigin the cross origin information + * @return updated builder + */ + public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + crossOrigins.put(path, crossOrigin); + return this; + } + + Map crossOrigins() { + return Collections.unmodifiableMap(crossOrigins); + } } private static class SERequestAdapter implements RequestAdapter { diff --git a/cors/src/main/java/io/helidon/cors/CrossOrigin.java b/cors/src/main/java/io/helidon/cors/CrossOrigin.java deleted file mode 100644 index e6b3df117d1..00000000000 --- a/cors/src/main/java/io/helidon/cors/CrossOrigin.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.cors; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * CrossOrigin annotation. - */ -@Target(METHOD) -@Retention(RUNTIME) -@Documented -public @interface CrossOrigin { - - /** - * Header Origin. - */ - String ORIGIN = "Origin"; - - /** - * Header Access-Control-Request-Method. - */ - String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - - /** - * Header Access-Control-Request-Headers. - */ - String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; - - /** - * Header Access-Control-Allow-Origin. - */ - String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - - /** - * Header Access-Control-Expose-Headers. - */ - String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; - - /** - * Header Access-Control-Max-Age. - */ - String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - - /** - * Header Access-Control-Allow-Credentials. - */ - String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - - /** - * Header Access-Control-Allow-Methods. - */ - String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - - /** - * Header Access-Control-Allow-Headers. - */ - String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - - /** - * Default cache expiration in seconds. - */ - long DEFAULT_AGE = 3600; - - /** - * A list of origins that are allowed such as {@code "http://foo.com"} or - * {@code "*"} to allow all origins. Corresponds to header {@code - * Access-Control-Allow-Origin}. - * - * @return Allowed origins. - */ - String[] value() default {"*"}; - - /** - * A list of request headers that are allowed or {@code "*"} to allow all headers. - * Corresponds to {@code Access-Control-Allow-Headers}. - * - * @return Allowed headers. - */ - String[] allowHeaders() default {"*"}; - - /** - * A list of response headers allowed for clients other than the "standard" - * ones. Corresponds to {@code Access-Control-Expose-Headers}. - * - * @return Exposed headers. - */ - String[] exposeHeaders() default {}; - - /** - * A list of supported HTTP request methods. In response to pre-flight - * requests. Corresponds to {@code Access-Control-Allow-Methods}. - * - * @return Allowed methods. - */ - String[] allowMethods() default {"*"}; - - /** - * Whether the client can send cookies or credentials. Corresponds to {@code - * Access-Control-Allow-Credentials}. - * - * @return Allowed credentials. - */ - boolean allowCredentials() default false; - - /** - * Pre-flight response duration in seconds. After time expires, a new pre-flight - * request is required. Corresponds to {@code Access-Control-Max-Age}. - * - * @return Max age. - */ - long maxAge() default DEFAULT_AGE; -} diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index a7bccc8f0fd..f864ad2f6e7 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -16,7 +16,6 @@ package io.helidon.cors; -import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,7 +28,48 @@ /** * Class CrossOriginConfig. */ -public class CrossOriginConfig implements CrossOrigin { +public class CrossOriginConfig /* implements CrossOrigin */ { + + /** + * Default cache expiration in seconds. + */ + public static final long DEFAULT_AGE = 3600; + /** + * Header Access-Control-Allow-Headers. + */ + public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + /** + * Header Access-Control-Allow-Methods. + */ + public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + /** + * Header Access-Control-Allow-Credentials. + */ + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + /** + * Header Access-Control-Max-Age. + */ + public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + /** + * Header Access-Control-Expose-Headers. + */ + public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + /** + * Header Access-Control-Allow-Origin. + */ + public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + /** + * Header Access-Control-Request-Headers. + */ + public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + /** + * Header Access-Control-Request-Method. + */ + public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** + * Header Origin. + */ + public static final String ORIGIN = "Origin"; private final String pathPrefix; private final String[] value; @@ -50,57 +90,69 @@ private CrossOriginConfig(Builder builder) { } /** - * Returns path prefix. * - * @return Path prefix. + * @return Path prefix */ public String pathPrefix() { return pathPrefix; } - @Override + /** + * + * @return origins + */ public String[] value() { return copyOf(value); } - @Override + /** + * + * @return allowHeaders + */ public String[] allowHeaders() { return copyOf(allowHeaders); } - @Override + /** + * + * @return exposeHeaders + */ public String[] exposeHeaders() { return copyOf(exposeHeaders); } - @Override + /** + * + * @return allowMethods + */ public String[] allowMethods() { return copyOf(allowMethods); } - @Override + /** + * + * @return allowCredentials + */ public boolean allowCredentials() { return allowCredentials; } - @Override + /** + * + * @return maxAge + */ public long maxAge() { return maxAge; } - @Override - public Class annotationType() { - return CrossOrigin.class; - } - - private String[] copyOf(String[] strings) { + private static String[] copyOf(String[] strings) { return strings != null ? Arrays.copyOf(strings, strings.length) : new String[0]; } /** * Builder for {@link CrossOriginConfig}. */ - static class Builder implements io.helidon.common.Builder { + public static class Builder implements io.helidon.common.Builder { private static final String[] ALLOW_ALL = {"*"}; @@ -112,36 +164,89 @@ static class Builder implements io.helidon.common.Builder { private boolean allowCredentials; private long maxAge = DEFAULT_AGE; + private Builder() { + } + + /** + * + * @return a new {@code CrossOriginConfig.Builder} + */ + public static Builder create() { + return new Builder(); + } + + /** + * Sets the path prefix. + * + * @param pathPrefix the path prefix + * @return updated builder + */ public Builder pathPrefix(String pathPrefix) { this.pathPrefix = pathPrefix; return this; } + /** + * Sets the values (origins). + * + * @param value the origin value + * @return updated builder + */ public Builder value(String[] value) { - this.value = value; + this.value = copyOf(value); return this; } + /** + * Sets the allow headers. + * + * @param allowHeaders the allow headers value(s) + * @return updated builder + */ public Builder allowHeaders(String[] allowHeaders) { - this.allowHeaders = allowHeaders; + this.allowHeaders = copyOf(allowHeaders); return this; } + /** + * Sets the expose headers. + * + * @param exposeHeaders the expose headers value(s) + * @return updated builder + */ public Builder exposeHeaders(String[] exposeHeaders) { - this.exposeHeaders = exposeHeaders; + this.exposeHeaders = copyOf(exposeHeaders); return this; } + /** + * Sets the allow methods. + * + * @param allowMethods the allow method value(s) + * @return updated builder + */ public Builder allowMethods(String[] allowMethods) { - this.allowMethods = allowMethods; + this.allowMethods = copyOf(allowMethods); return this; } + /** + * Sets the allow credentials flag. + * + * @param allowCredentials the allow credentials flag + * @return updated builder + */ public Builder allowCredentials(boolean allowCredentials) { this.allowCredentials = allowCredentials; return this; } + /** + * Sets the maximum age. + * + * @param maxAge the maximum age + * @return updated builder + */ public Builder maxAge(long maxAge) { this.maxAge = maxAge; return this; @@ -153,6 +258,9 @@ public CrossOriginConfig build() { } } + /** + * Functional interface for converting a Helidon config instance to a {@code CrossOriginConfig} instance. + */ public static class CrossOriginConfigMapper implements Function> { @Override diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index af75f2490a9..a5431962343 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -30,15 +30,15 @@ import io.helidon.common.http.Http; import static io.helidon.common.http.Http.Header.HOST; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_EXPOSE_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOriginConfig.ORIGIN; /** * Centralizes common logic to both SE and MP CORS support for processing requests and preparing responses. @@ -208,7 +208,7 @@ public interface ResponseAdapter { * valid CORS request */ public static Optional processRequest(List crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { RequestType requestType = requestType(requestAdapter); @@ -249,7 +249,7 @@ public static Optional processRequest(List crossOri * @param the type for the HTTP response as returned from the responseSetter */ public static void prepareResponse(List crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -304,11 +304,11 @@ static RequestType requestType(RequestAdapter requestAdapter) { */ static Optional processCORSRequest( List crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { Optional originOpt = requestAdapter.firstHeader(ORIGIN); - Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, + Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup); if (crossOriginOpt.isEmpty()) { return Optional.of(responseAdapter.forbidden(ORIGIN_DENIED)); @@ -335,10 +335,10 @@ static Optional processCORSRequest( * @param type for the response wrapper by the responseAdapter */ static void prepareCORSResponse(List crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - CrossOrigin crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup) + CrossOriginConfig crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup) .orElseThrow(() -> new IllegalArgumentException( "Could not locate expected CORS information while preparing response to request " + requestAdapter)); @@ -378,12 +378,12 @@ static void prepareCORSResponse(List crossOriginConfig */ static U processCORSPreFlightRequest( List crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { Optional originOpt = requestAdapter.firstHeader(ORIGIN); - Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, + Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup); // If CORS not enabled, deny request @@ -394,7 +394,7 @@ static U processCORSPreFlightRequest( return responseAdapter.forbidden(noRequiredHeader(ORIGIN)); } - CrossOrigin crossOrigin = crossOriginOpt.get(); + CrossOriginConfig crossOrigin = crossOriginOpt.get(); // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.value()); @@ -447,8 +447,8 @@ static U processCORSPreFlightRequest( * @param secondaryLookup Supplier for CrossOrigin used if none found in config * @return Optional for the matching config, or an empty Optional if none matched */ - static Optional lookupCrossOrigin(String path, List crossOriginConfigs, - Supplier> secondaryLookup) { + static Optional lookupCrossOrigin(String path, List crossOriginConfigs, + Supplier> secondaryLookup) { for (CrossOriginConfig config : crossOriginConfigs) { String pathPrefix = normalize(config.pathPrefix()); String uriPath = normalize(path); diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index cb20622b61d..187b05bd46a 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -30,14 +30,14 @@ import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; import static io.helidon.cors.CORSTestServices.SERVICE_3; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static io.helidon.cors.CustomMatchers.notPresent; import static io.helidon.cors.CustomMatchers.present; diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index a33b21afe7c..2ca67d77f67 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -36,14 +36,14 @@ import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static io.helidon.cors.CustomMatchers.notPresent; import static io.helidon.cors.CustomMatchers.present; import static org.hamcrest.CoreMatchers.containsString; From 70f7c5891ff5d30e1ad9f681652c9cc575c02e01 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 16:28:39 -0500 Subject: [PATCH 041/100] Move CrossOrigin back to MP --- microprofile/cors/pom.xml | 1 + .../microprofile/cors/CrossOrigin.java | 132 ++++++++++++++++++ .../microprofile/cors/CrossOriginFilter.java | 23 ++- .../cors/CrossOriginHelperMP.java | 81 ----------- .../cors/src/main/java/module-info.java | 2 +- .../microprofile/cors/CrossOriginTest.java | 17 ++- 6 files changed, 159 insertions(+), 97 deletions(-) create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java delete mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelperMP.java diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 9abf519ae72..4fb47ed16c0 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -55,6 +55,7 @@ io.helidon.cors helidon-cors + ${helidon.version} io.helidon.microprofile.bundles diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java new file mode 100644 index 00000000000..e0a30bf7e97 --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.cors; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * CrossOrigin annotation. + */ +@Target(METHOD) +@Retention(RUNTIME) +@Documented +public @interface CrossOrigin { + + /** + * Header Origin. + */ + String ORIGIN = "Origin"; + + /** + * Header Access-Control-Request-Method. + */ + String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + + /** + * Header Access-Control-Request-Headers. + */ + String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + + /** + * Header Access-Control-Allow-Origin. + */ + String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + /** + * Header Access-Control-Expose-Headers. + */ + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * Header Access-Control-Max-Age. + */ + String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + + /** + * Header Access-Control-Allow-Credentials. + */ + String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + /** + * Header Access-Control-Allow-Methods. + */ + String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + /** + * Header Access-Control-Allow-Headers. + */ + String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + /** + * Default cache expiration in seconds. + */ + long DEFAULT_AGE = 3600; + + /** + * A list of origins that are allowed such as {@code "http://foo.com"} or + * {@code "*"} to allow all origins. Corresponds to header {@code + * Access-Control-Allow-Origin}. + * + * @return Allowed origins. + */ + String[] value() default {"*"}; + + /** + * A list of request headers that are allowed or {@code "*"} to allow all headers. + * Corresponds to {@code Access-Control-Allow-Headers}. + * + * @return Allowed headers. + */ + String[] allowHeaders() default {"*"}; + + /** + * A list of response headers allowed for clients other than the "standard" + * ones. Corresponds to {@code Access-Control-Expose-Headers}. + * + * @return Exposed headers. + */ + String[] exposeHeaders() default {}; + + /** + * A list of supported HTTP request methods. In response to pre-flight + * requests. Corresponds to {@code Access-Control-Allow-Methods}. + * + * @return Allowed methods. + */ + String[] allowMethods() default {"*"}; + + /** + * Whether the client can send cookies or credentials. Corresponds to {@code + * Access-Control-Allow-Credentials}. + * + * @return Allowed credentials. + */ + boolean allowCredentials() default false; + + /** + * Pre-flight response duration in seconds. After time expires, a new pre-flight + * request is required. Corresponds to {@code Access-Control-Max-Age}. + * + * @return Max age. + */ + long maxAge() default DEFAULT_AGE; +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index a22a894f13b..cbcf5536074 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -37,7 +37,6 @@ import javax.ws.rs.core.Response; import io.helidon.config.Config; -import io.helidon.cors.CrossOrigin; import io.helidon.cors.CrossOriginConfig; import io.helidon.cors.CrossOriginHelper; import io.helidon.cors.CrossOriginHelper.RequestAdapter; @@ -68,7 +67,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), + crossOriginFromAnnotationFinder(requestContext.getUriInfo().getPath(), resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter()); response.ifPresent(requestContext::abortWith); @@ -77,7 +76,7 @@ public void filter(ContainerRequestContext requestContext) { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { prepareResponse(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), + crossOriginFromAnnotationFinder(requestContext.getUriInfo().getPath(), resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); } @@ -159,7 +158,7 @@ public Response ok() { } - static Supplier> crossOriginFromAnnotationFinder(ResourceInfo resourceInfo) { + static Supplier> crossOriginFromAnnotationFinder(String path, ResourceInfo resourceInfo) { return () -> { // If not found, inspect resource matched @@ -168,10 +167,10 @@ static Supplier> crossOriginFromAnnotationFinder(ResourceI CrossOrigin corsAnnot; OPTIONS optsAnnot = resourceMethod.getAnnotation(OPTIONS.class); + Path pathAnnot = resourceMethod.getAnnotation(Path.class); if (optsAnnot != null) { corsAnnot = resourceMethod.getAnnotation(CrossOrigin.class); } else { - Path pathAnnot = resourceMethod.getAnnotation(Path.class); Optional optionsMethod = Arrays.stream(resourceClass.getDeclaredMethods()) .filter(m -> { OPTIONS optsAnnot2 = m.getAnnotation(OPTIONS.class); @@ -189,7 +188,19 @@ static Supplier> crossOriginFromAnnotationFinder(ResourceI corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) .orElse(null); } - return Optional.ofNullable(corsAnnot); + return Optional.ofNullable(annotationToConfig(path, corsAnnot)); }; } + + private static CrossOriginConfig annotationToConfig(String path, CrossOrigin crossOrigin) { + return CrossOriginConfig.Builder.create() + .pathPrefix(path) + .value(crossOrigin.value()) + .allowHeaders(crossOrigin.allowHeaders()) + .exposeHeaders(crossOrigin.exposeHeaders()) + .allowMethods(crossOrigin.allowMethods()) + .allowCredentials(crossOrigin.allowCredentials()) + .maxAge(crossOrigin.maxAge()) + .build(); + } } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelperMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelperMP.java deleted file mode 100644 index c041d4d5fc6..00000000000 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginHelperMP.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.microprofile.cors; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Optional; -import java.util.function.Supplier; - -import javax.ws.rs.OPTIONS; -import javax.ws.rs.Path; -import javax.ws.rs.container.ResourceInfo; - -import io.helidon.cors.CrossOrigin; - -/** - * Helper methods for MP CORS support. - */ -class CrossOriginHelperMP { - - private CrossOriginHelperMP() { - } - - /** - * Returns a {@code Supplier} for an {@code Optional>} for the provided resource information (annotation). - *

- * We use a supplier because this code is needed only if there is no configuration for the specified resource. We can avoid - * executing this unless we really need it. - *

- * - * @param resourceInfo the information about the resource - * @return Supplier for a CrossOrigin for the specified resource - */ - static Supplier> crossOriginFromAnnotationFinder(ResourceInfo resourceInfo) { - - return () -> { - // If not found, inspect resource matched - Method resourceMethod = resourceInfo.getResourceMethod(); - Class resourceClass = resourceInfo.getResourceClass(); - - CrossOrigin corsAnnot; - OPTIONS optsAnnot = resourceMethod.getAnnotation(OPTIONS.class); - if (optsAnnot != null) { - corsAnnot = resourceMethod.getAnnotation(CrossOrigin.class); - } else { - Path pathAnnot = resourceMethod.getAnnotation(Path.class); - Optional optionsMethod = Arrays.stream(resourceClass.getDeclaredMethods()) - .filter(m -> { - OPTIONS optsAnnot2 = m.getAnnotation(OPTIONS.class); - if (optsAnnot2 != null) { - if (pathAnnot != null) { - Path pathAnnot2 = m.getAnnotation(Path.class); - return pathAnnot2 != null && pathAnnot.value() - .equals(pathAnnot2.value()); - } - return true; - } - return false; - }) - .findFirst(); - corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) - .orElse(null); - } - return Optional.ofNullable(corsAnnot); - }; - } -} diff --git a/microprofile/cors/src/main/java/module-info.java b/microprofile/cors/src/main/java/module-info.java index 35ee23ba9e0..adcabbd2179 100644 --- a/microprofile/cors/src/main/java/module-info.java +++ b/microprofile/cors/src/main/java/module-info.java @@ -21,7 +21,7 @@ requires transitive java.ws.rs; requires io.helidon.config; - requires transitive io.helidon.cors; // for CrossOrigin + requires io.helidon.cors; requires jersey.common; requires microprofile.config.api; diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 4c976d433d3..54dd0daf25e 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -32,21 +32,20 @@ import javax.ws.rs.core.Response; import java.util.Set; -import io.helidon.cors.CrossOrigin; import io.helidon.microprofile.server.Server; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOrigin.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOrigin.ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; From a80e5a25f508bdc429daf6757bbdcee3a0445679 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 16:54:38 -0500 Subject: [PATCH 042/100] Remove redundant def of ORIGIN; use Http.Header.ORIGIN instead --- cors/src/main/java/io/helidon/cors/CrossOriginConfig.java | 4 ---- cors/src/main/java/io/helidon/cors/CrossOriginHelper.java | 8 +++++--- cors/src/test/java/io/helidon/cors/CORSTest.java | 2 +- cors/src/test/java/io/helidon/cors/TestUtil.java | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index f864ad2f6e7..1e7027dc314 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -66,10 +66,6 @@ public class CrossOriginConfig /* implements CrossOrigin */ { * Header Access-Control-Request-Method. */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - /** - * Header Origin. - */ - public static final String ORIGIN = "Origin"; private final String pathPrefix; private final String[] value; diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index a5431962343..bd261fc90a0 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -30,6 +30,7 @@ import io.helidon.common.http.Http; import static io.helidon.common.http.Http.Header.HOST; +import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -38,7 +39,6 @@ import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOriginConfig.ORIGIN; /** * Centralizes common logic to both SE and MP CORS support for processing requests and preparing responses. @@ -350,10 +350,12 @@ static void prepareCORSResponse(List crossOriginConfig if (crossOrigin.allowCredentials()) { responseAdapter.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") - .header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + .header(ACCESS_CONTROL_ALLOW_ORIGIN, origin) + .header(Http.Header.VARY, ORIGIN); } else { List allowedOrigins = Arrays.asList(crossOrigin.value()); - responseAdapter.header(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin); + responseAdapter.header(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin) + .header(Http.Header.VARY, ORIGIN); } // Add Access-Control-Expose-Headers if non-empty diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index 187b05bd46a..7002e0515ff 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -27,6 +27,7 @@ import io.helidon.webclient.WebClientResponse; import io.helidon.webserver.WebServer; +import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; import static io.helidon.cors.CORSTestServices.SERVICE_3; @@ -37,7 +38,6 @@ import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static io.helidon.cors.CustomMatchers.notPresent; import static io.helidon.cors.CustomMatchers.present; diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index 2ca67d77f67..10b4706faf3 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -34,6 +34,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; @@ -43,7 +44,6 @@ import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static io.helidon.cors.CustomMatchers.notPresent; import static io.helidon.cors.CustomMatchers.present; import static org.hamcrest.CoreMatchers.containsString; From 4e362c8e7b084d1cd32983241f51da40c2c58766 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 16:56:24 -0500 Subject: [PATCH 043/100] Use Http.Header.ORIGIN; remove temp workaround in pom from pushed version --- microprofile/cors/pom.xml | 1 - .../test/java/io/helidon/microprofile/cors/CrossOriginTest.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 4fb47ed16c0..9abf519ae72 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -55,7 +55,6 @@ io.helidon.cors helidon-cors - ${helidon.version} io.helidon.microprofile.bundles diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 54dd0daf25e..4d097b54162 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -38,6 +38,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -45,7 +46,6 @@ import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CrossOriginConfig.ORIGIN; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; From e8f0b79266f4f001cd80d41f2e96fefac8403b74 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 17:21:22 -0500 Subject: [PATCH 044/100] Convert collection of CrossOriginConfig to a map --- .../java/io/helidon/cors/CORSSupport.java | 6 ++--- .../io/helidon/cors/CrossOriginConfig.java | 14 +++++++----- .../io/helidon/cors/CrossOriginHelper.java | 22 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 890267ce5ba..a1001f14dc6 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -85,7 +85,7 @@ public static CORSSupport.Builder builder() { HelidonFeatures.register(HelidonFlavor.SE, "CORS"); } - private final List crossOriginConfigs; + private final Map crossOriginConfigs; private CORSSupport(Builder builder) { crossOriginConfigs = builder.configs(); @@ -154,9 +154,9 @@ public Builder config(Config config) { * * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions */ - List configs() { + Map configs() { return corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) - .orElse(Collections.emptyList()); + .orElse(Collections.emptyMap()); } /** diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 1e7027dc314..96f02a5e6c6 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -16,13 +16,14 @@ package io.helidon.cors; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; import io.helidon.config.Config; +import static io.helidon.cors.CrossOriginHelper.normalize; import static io.helidon.cors.CrossOriginHelper.parseHeader; /** @@ -257,11 +258,11 @@ public CrossOriginConfig build() { /** * Functional interface for converting a Helidon config instance to a {@code CrossOriginConfig} instance. */ - public static class CrossOriginConfigMapper implements Function> { + public static class CrossOriginConfigMapper implements Function> { @Override - public List apply(Config config) { - List result = new ArrayList<>(); + public Map apply(Config config) { + Map result = new HashMap<>(); int i = 0; do { Config item = config.get(Integer.toString(i++)); @@ -269,6 +270,7 @@ public List apply(Config config) { break; } Builder builder = new Builder(); + String path = item.get("path-prefix").as(String.class).orElse(null); item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); item.get("allow-origins").asList(String.class).ifPresent( s -> builder.value(parseHeader(s).toArray(new String[]{}))); @@ -280,7 +282,7 @@ public List apply(Config config) { s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); item.get("allow-credentials").as(Boolean.class).ifPresent(builder::allowCredentials); item.get("max-age").as(Long.class).ifPresent(builder::maxAge); - result.add(builder.build()); + result.put(normalize(path), builder.build()); } while (true); return result; } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java index bd261fc90a0..654557877c7 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; @@ -207,7 +208,7 @@ public interface ResponseAdapter { * @return Optional of an error response if the request was an invalid CORS request; Optional.empty() if it was a * valid CORS request */ - public static Optional processRequest(List crossOriginConfigs, + public static Optional processRequest(Map crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -248,7 +249,7 @@ public static Optional processRequest(List crossOri * @param type for the {@code Request} managed by the requestAdapter * @param the type for the HTTP response as returned from the responseSetter */ - public static void prepareResponse(List crossOriginConfigs, + public static void prepareResponse(Map crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -303,7 +304,7 @@ static RequestType requestType(RequestAdapter requestAdapter) { * valid CORS request */ static Optional processCORSRequest( - List crossOriginConfigs, + Map crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -334,7 +335,7 @@ static Optional processCORSRequest( * @param type for the request wrapped by the requestAdapter * @param type for the response wrapper by the responseAdapter */ - static void prepareCORSResponse(List crossOriginConfigs, + static void prepareCORSResponse(Map crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -379,7 +380,7 @@ static void prepareCORSResponse(List crossOriginConfig * @return the response returned by the response adapter with CORS-related headers set (for a successful CORS preflight) */ static U processCORSPreFlightRequest( - List crossOriginConfigs, + Map crossOriginConfigs, Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { @@ -449,14 +450,11 @@ static U processCORSPreFlightRequest( * @param secondaryLookup Supplier for CrossOrigin used if none found in config * @return Optional for the matching config, or an empty Optional if none matched */ - static Optional lookupCrossOrigin(String path, List crossOriginConfigs, + static Optional lookupCrossOrigin(String path, Map crossOriginConfigs, Supplier> secondaryLookup) { - for (CrossOriginConfig config : crossOriginConfigs) { - String pathPrefix = normalize(config.pathPrefix()); - String uriPath = normalize(path); - if (uriPath.startsWith(pathPrefix)) { - return Optional.of(config); - } + String normalizedPath = normalize(path); + if (crossOriginConfigs.containsKey(normalizedPath)) { + return Optional.of(crossOriginConfigs.get(normalizedPath)); } return secondaryLookup.get(); From cf3e4cd81a5b953bbc293b1d19722ad0d9d469f0 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 17:21:53 -0500 Subject: [PATCH 045/100] Convert collection of CrossOriginConfig to map --- .../microprofile/cors/CrossOrigin.java | 51 +------------------ .../microprofile/cors/CrossOriginFilter.java | 5 +- 2 files changed, 4 insertions(+), 52 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index e0a30bf7e97..c3908bee218 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -20,6 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static io.helidon.cors.CrossOriginConfig.DEFAULT_AGE; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -31,56 +32,6 @@ @Documented public @interface CrossOrigin { - /** - * Header Origin. - */ - String ORIGIN = "Origin"; - - /** - * Header Access-Control-Request-Method. - */ - String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - - /** - * Header Access-Control-Request-Headers. - */ - String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; - - /** - * Header Access-Control-Allow-Origin. - */ - String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - - /** - * Header Access-Control-Expose-Headers. - */ - String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; - - /** - * Header Access-Control-Max-Age. - */ - String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - - /** - * Header Access-Control-Allow-Credentials. - */ - String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - - /** - * Header Access-Control-Allow-Methods. - */ - String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - - /** - * Header Access-Control-Allow-Headers. - */ - String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - - /** - * Default cache expiration in seconds. - */ - long DEFAULT_AGE = 3600; - /** * A list of origins that are allowed such as {@code "http://foo.com"} or * {@code "*"} to allow all origins. Corresponds to header {@code diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index cbcf5536074..ff9c2e6c4b8 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; @@ -57,7 +58,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private List crossOriginConfigs; + private Map crossOriginConfigs; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); @@ -188,7 +189,7 @@ static Supplier> crossOriginFromAnnotationFinder(Str corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) .orElse(null); } - return Optional.ofNullable(annotationToConfig(path, corsAnnot)); + return Optional.ofNullable(corsAnnot == null ? null : annotationToConfig(path, corsAnnot)); }; } From 03b1e83508290387d6c19c80543451897db64059 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 17:48:41 -0500 Subject: [PATCH 046/100] Remove path from CrossOriginConfig --- .../io/helidon/cors/CrossOriginConfig.java | 24 +--------------- .../test/java/io/helidon/cors/CORSTest.java | 19 +------------ .../io/helidon/cors/TestTwoCORSConfigs.java | 4 ++- .../test/java/io/helidon/cors/TestUtil.java | 28 +++++++++++++++++++ cors/src/test/resources/twoCORS.yaml | 2 ++ 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 96f02a5e6c6..52e2ce075e8 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -68,7 +68,6 @@ public class CrossOriginConfig /* implements CrossOrigin */ { */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - private final String pathPrefix; private final String[] value; private final String[] allowHeaders; private final String[] exposeHeaders; @@ -77,7 +76,6 @@ public class CrossOriginConfig /* implements CrossOrigin */ { private final long maxAge; private CrossOriginConfig(Builder builder) { - this.pathPrefix = builder.pathPrefix; this.value = builder.value; this.allowHeaders = builder.allowHeaders; this.exposeHeaders = builder.exposeHeaders; @@ -86,14 +84,6 @@ private CrossOriginConfig(Builder builder) { this.maxAge = builder.maxAge; } - /** - * - * @return Path prefix - */ - public String pathPrefix() { - return pathPrefix; - } - /** * * @return origins @@ -153,7 +143,6 @@ public static class Builder implements io.helidon.common.Builder apply(Config config) { } Builder builder = new Builder(); String path = item.get("path-prefix").as(String.class).orElse(null); - item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); +// item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); item.get("allow-origins").asList(String.class).ifPresent( s -> builder.value(parseHeader(s).toArray(new String[]{}))); item.get("allow-methods").asList(String.class).ifPresent( diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index 7002e0515ff..5685cca2455 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -317,24 +317,7 @@ void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException @Test void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_3)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .submit("") - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + TestUtil.test3PreFlightAllowedOrigin(client); } @Test diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java index f3ef80540e8..c742347ae1a 100644 --- a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java +++ b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java @@ -51,7 +51,9 @@ void test1PreFlightAllowedOriginOtherGreeting() throws ExecutionException, Inter } @Test - + void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + TestUtil.test3PreFlightAllowedOrigin(client); + } @AfterAll public static void shutdown() { diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index 10b4706faf3..62cad1792d0 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -37,6 +37,7 @@ import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.cors.CORSTestServices.SERVICE_1; import static io.helidon.cors.CORSTestServices.SERVICE_2; +import static io.helidon.cors.CORSTestServices.SERVICE_3; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -83,6 +84,12 @@ static Routing.Builder prepRouting() { CORSSupport.Builder twoCORSSupportBuilder = CORSSupport.builder().config(twoCORSConfig.get(CrossOriginHelper.CORS_CONFIG_KEY)); + CrossOriginConfig cors3COC= CrossOriginConfig.Builder.create() + .value(new String[] {"http://foo.bar", "http://bar.foo"}) + .allowMethods(new String[] {"DELETE", "PUT"}) + .build(); + twoCORSSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); + Routing.Builder builder = Routing.builder() .register(GREETING_PATH, corsSupportBuilder.build(), new GreetService()) .register(OTHER_GREETING_PATH, twoCORSSupportBuilder.build(), new GreetService("Other Hello")); @@ -193,4 +200,25 @@ static String path(CORSTestService testService) { static String path(String prefix, CORSTestService testService) { return prefix + testService.path(); } + + static void test3PreFlightAllowedOrigin(WebClient client) throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_3)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + } } diff --git a/cors/src/test/resources/twoCORS.yaml b/cors/src/test/resources/twoCORS.yaml index 38027bcc324..d7994c9bcb8 100644 --- a/cors/src/test/resources/twoCORS.yaml +++ b/cors/src/test/resources/twoCORS.yaml @@ -20,3 +20,5 @@ cors: allow-headers: ["X-otherBar", "X-otherFoo"] allow-credentials: true max-age: -1 + +# /cors3 info is added programmatically From af8ea3ab22507df021091cdf2506a14532a120d2 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 17:49:03 -0500 Subject: [PATCH 047/100] Remove path from CrossOriginConfig --- .../helidon/microprofile/cors/CrossOriginFilter.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index ff9c2e6c4b8..72a2790fa59 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -68,7 +68,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, - crossOriginFromAnnotationFinder(requestContext.getUriInfo().getPath(), resourceInfo), + crossOriginFromAnnotationFinder(resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter()); response.ifPresent(requestContext::abortWith); @@ -77,7 +77,7 @@ public void filter(ContainerRequestContext requestContext) { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { prepareResponse(crossOriginConfigs, - crossOriginFromAnnotationFinder(requestContext.getUriInfo().getPath(), resourceInfo), + crossOriginFromAnnotationFinder(resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); } @@ -159,7 +159,7 @@ public Response ok() { } - static Supplier> crossOriginFromAnnotationFinder(String path, ResourceInfo resourceInfo) { + static Supplier> crossOriginFromAnnotationFinder(ResourceInfo resourceInfo) { return () -> { // If not found, inspect resource matched @@ -189,13 +189,12 @@ static Supplier> crossOriginFromAnnotationFinder(Str corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) .orElse(null); } - return Optional.ofNullable(corsAnnot == null ? null : annotationToConfig(path, corsAnnot)); + return Optional.ofNullable(corsAnnot == null ? null : annotationToConfig(corsAnnot)); }; } - private static CrossOriginConfig annotationToConfig(String path, CrossOrigin crossOrigin) { + private static CrossOriginConfig annotationToConfig(CrossOrigin crossOrigin) { return CrossOriginConfig.Builder.create() - .pathPrefix(path) .value(crossOrigin.value()) .allowHeaders(crossOrigin.allowHeaders()) .exposeHeaders(crossOrigin.exposeHeaders()) From da278186f363dcb6092b750063d70864f43d0459 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 6 Apr 2020 18:53:23 -0500 Subject: [PATCH 048/100] Add feature registration --- .../io/helidon/microprofile/cors/CrossOriginFilter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 72a2790fa59..3e7efb4a0ae 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -37,6 +37,8 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig; import io.helidon.cors.CrossOriginHelper; @@ -55,6 +57,10 @@ @Priority(Priorities.HEADER_DECORATOR) class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { + static { + HelidonFeatures.register(HelidonFlavor.MP, "CORS"); + } + @Context private ResourceInfo resourceInfo; From 44569e2f17f23d5f190881a1e388b0c4f955f7fa Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 7 Apr 2020 09:58:53 -0500 Subject: [PATCH 049/100] Remove unneeded dep in pom; fix CrossOriginConfig addition to CORSSupport builder; refactor tests --- cors/pom.xml | 5 - .../java/io/helidon/cors/CORSSupport.java | 11 +- .../io/helidon/cors/CrossOriginConfig.java | 1 - .../io/helidon/cors/AbstractCORSTest.java | 333 ++++++++++++++++++ .../test/java/io/helidon/cors/CORSTest.java | 277 +-------------- .../io/helidon/cors/TestTwoCORSConfigs.java | 29 +- .../test/java/io/helidon/cors/TestUtil.java | 114 +----- cors/src/test/resources/application.yaml | 4 +- cors/src/test/resources/twoCORS.yaml | 2 +- 9 files changed, 396 insertions(+), 380 deletions(-) create mode 100644 cors/src/test/java/io/helidon/cors/AbstractCORSTest.java diff --git a/cors/pom.xml b/cors/pom.xml index 3092c9d8624..d5e8c1e717b 100644 --- a/cors/pom.xml +++ b/cors/pom.xml @@ -43,11 +43,6 @@ io.helidon.webserver helidon-webserver - - org.glassfish - javax.json - runtime - io.helidon.config helidon-config diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index a1001f14dc6..93f944a30e4 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -36,6 +36,7 @@ import io.helidon.webserver.Service; import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; +import static io.helidon.cors.CrossOriginHelper.normalize; import static io.helidon.cors.CrossOriginHelper.prepareResponse; import static io.helidon.cors.CrossOriginHelper.processRequest; @@ -88,7 +89,7 @@ public static CORSSupport.Builder builder() { private final Map crossOriginConfigs; private CORSSupport(Builder builder) { - crossOriginConfigs = builder.configs(); + crossOriginConfigs = builder.crossOriginConfigs(); } @Override @@ -154,9 +155,11 @@ public Builder config(Config config) { * * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions */ - Map configs() { - return corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) + Map crossOriginConfigs() { + Map result = corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) .orElse(Collections.emptyMap()); + result.putAll(crossOrigins); + return result; } /** @@ -167,7 +170,7 @@ Map configs() { * @return updated builder */ public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - crossOrigins.put(path, crossOrigin); + crossOrigins.put(normalize(path), crossOrigin); return this; } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 52e2ce075e8..01b1e409f0e 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -249,7 +249,6 @@ public Map apply(Config config) { } Builder builder = new Builder(); String path = item.get("path-prefix").as(String.class).orElse(null); -// item.get("path-prefix").as(String.class).ifPresent(builder::pathPrefix); item.get("allow-origins").asList(String.class).ifPresent( s -> builder.value(parseHeader(s).toArray(new String[]{}))); item.get("allow-methods").asList(String.class).ifPresent( diff --git a/cors/src/test/java/io/helidon/cors/AbstractCORSTest.java b/cors/src/test/java/io/helidon/cors/AbstractCORSTest.java new file mode 100644 index 00000000000..898f1dd86dd --- /dev/null +++ b/cors/src/test/java/io/helidon/cors/AbstractCORSTest.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.common.http.MediaType; +import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientRequestBuilder; +import io.helidon.webclient.WebClientResponse; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutionException; + +import static io.helidon.common.http.Http.Header.ORIGIN; +import static io.helidon.cors.CORSTestServices.SERVICE_1; +import static io.helidon.cors.CORSTestServices.SERVICE_2; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.cors.CustomMatchers.notPresent; +import static io.helidon.cors.CustomMatchers.present; +import static io.helidon.cors.TestUtil.path; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public abstract class AbstractCORSTest { + + abstract String contextRoot(); + + abstract WebClient client(); + + abstract String fooOrigin(); + + abstract String fooHeader(); + + @Test + public void testSimple() throws Exception { + + WebClientResponse response = client().get() + .path(contextRoot()) + .accept(MediaType.TEXT_PLAIN) + .request() + .toCompletableFuture() + .get(); + + Http.ResponseStatus result = response.status(); + + assertThat(result.code(), is(Http.Status.OK_200.code())); + } + @Test + void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_1)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(is("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + } + + @Test + void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_1)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + } + + @Test + void test2PreFlightForbiddenOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://not.allowed"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); + } + + @Test + void test2PreFlightForbiddenMethod() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "POST"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightForbiddenHeader() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar, X-oops"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + } + + @Test + void test2PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(contextRoot(), SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, fooOrigin()); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, fooHeader()); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(fooOrigin()))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers() + .first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString(fooHeader()))); + assertThat(res.headers() + .first(ACCESS_CONTROL_MAX_AGE), notPresent()); + } + + @Test + void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); + } + + @Test + void test2PreFlightAllowedHeaders3() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(SERVICE_2)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); + } + + @Test + void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .put() + .path(path(SERVICE_1)) + .contentType(MediaType.TEXT_PLAIN); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("*"))); + } + + @Test + void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .put() + .path(path(SERVICE_2)) + .contentType(MediaType.TEXT_PLAIN); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, "http://foo.bar"); + + WebClientResponse res = reqBuilder + .submit("") + .toCompletableFuture() + .get(); + + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + } + + WebClientResponse runTest1PreFlightAllowedOrigin() throws ExecutionException, + InterruptedException { + WebClientRequestBuilder reqBuilder = client() + .method(Http.Method.OPTIONS.name()) + .path(path(contextRoot(), SERVICE_1)); + + Headers headers = reqBuilder.headers(); + headers.add(ORIGIN, fooOrigin()); + headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + WebClientResponse res = reqBuilder + .request() + .toCompletableFuture() + .get(); + + return res; + } +} diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index 5685cca2455..a69f74835e1 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -49,7 +49,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -public class CORSTest { +public class CORSTest extends AbstractCORSTest { private static final String CONTEXT_ROOT = "/greet"; private static WebServer server; @@ -66,277 +66,36 @@ public static void shutdown() { TestUtil.shutdownServer(server); } - @Test - public void testSimple() throws Exception { - - WebClientResponse response = client.get() - .path(CONTEXT_ROOT) - .accept(MediaType.TEXT_PLAIN) - .request() - .toCompletableFuture() - .get(); - - Http.ResponseStatus result = response.status(); - assertThat(result.code(), is(Http.Status.OK_200.code())); + @Override + String fooOrigin() { + return "http://foo.bar"; } - @Test - void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - String origin = "http://foo.bar"; - WebClientResponse res = TestUtil.runTest1PreFlightAllowedOrigin(client, CONTEXT_ROOT, origin); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(origin))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + @Override + WebClient client() { + return CORSTest.client; } - @Test - void test1PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_1)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(is("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); - } - - @Test - void test1PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_1)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); + @Override + String contextRoot() { + return CONTEXT_ROOT; } - @Test - void test2PreFlightForbiddenOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://not.allowed"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); + @Override + String fooHeader() { + return "X-foo"; } @Test - void test2PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - + void test1PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { + String origin = fooOrigin(); + WebClientResponse res = runTest1PreFlightAllowedOrigin(); assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); + assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(origin))); assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); - } - - @Test - void test2PreFlightForbiddenMethod() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "POST"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); - } - - @Test - void test2PreFlightForbiddenHeader() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar, X-oops"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); - } - - @Test - void test2PreFlightAllowedHeaders1() throws ExecutionException, InterruptedException { - TestUtil.test2PreFlightAllowedHeaders1(client, CONTEXT_ROOT,"http://foo.bar", "X-foo"); - } - - @Test - void test2PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); - } - - @Test - void test2PreFlightAllowedHeaders3() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(TestUtil.path(SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), notPresent()); - } - - @Test - void test1ActualAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .put() - .path(TestUtil.path(SERVICE_1)) - .contentType(MediaType.TEXT_PLAIN); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .submit("") - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("*"))); - } - - @Test - void test2ActualAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .put() - .path(TestUtil.path(SERVICE_2)) - .contentType(MediaType.TEXT_PLAIN); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - - WebClientResponse res = reqBuilder - .submit("") - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); - } - - @Test - void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - TestUtil.test3PreFlightAllowedOrigin(client); - } - - @Test - void test3ActualAllowedOrigin() throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .put() - .path(TestUtil.path(SERVICE_3)) - .contentType(MediaType.TEXT_PLAIN); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .submit("") - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); + assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); } } diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java index c742347ae1a..8fe90c4d006 100644 --- a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java +++ b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java @@ -31,7 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public class TestTwoCORSConfigs { +public class TestTwoCORSConfigs extends AbstractCORSTest { private static WebServer server; private static WebClient client; @@ -44,21 +44,34 @@ public static void startup() throws InterruptedException, ExecutionException, Ti @Test void test1PreFlightAllowedOriginOtherGreeting() throws ExecutionException, InterruptedException { - WebClientResponse res = TestUtil.runTest1PreFlightAllowedOrigin(client, TestUtil.OTHER_GREETING_PATH, - "http://otherfoo.bar"); + WebClientResponse res = runTest1PreFlightAllowedOrigin(); assertThat(res.status(), is(Http.Status.FORBIDDEN_403)); } - @Test - void test3PreFlightAllowedOrigin() throws ExecutionException, InterruptedException { - TestUtil.test3PreFlightAllowedOrigin(client); - } - @AfterAll public static void shutdown() { TestUtil.shutdownServer(server); } + @Override + String contextRoot() { + return TestUtil.OTHER_GREETING_PATH; + } + + @Override + WebClient client() { + return client; + } + + @Override + String fooOrigin() { + return "http://otherfoo.bar"; + } + + @Override + String fooHeader() { + return "X-otherfoo"; + } } diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index 62cad1792d0..f7b9bb14e8c 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -16,40 +16,21 @@ */ package io.helidon.cors; -import io.helidon.common.http.Headers; -import io.helidon.common.http.Http; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + import io.helidon.config.Config; import io.helidon.config.ConfigSources; import io.helidon.config.spi.ConfigSource; import io.helidon.cors.CORSTestServices.CORSTestService; import io.helidon.webclient.WebClient; -import io.helidon.webclient.WebClientRequestBuilder; -import io.helidon.webclient.WebClientResponse; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.WebServer; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; - -import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.cors.CORSTestServices.SERVICE_1; -import static io.helidon.cors.CORSTestServices.SERVICE_2; import static io.helidon.cors.CORSTestServices.SERVICE_3; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CustomMatchers.notPresent; -import static io.helidon.cors.CustomMatchers.present; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; public class TestUtil { @@ -71,11 +52,17 @@ private static WebServer startServer(int port, Routing.Builder routingBuilder) t } static Routing.Builder prepRouting() { + CrossOriginConfig cors3COC= CrossOriginConfig.Builder.create() + .value(new String[] {"http://foo.bar", "http://bar.foo"}) + .allowMethods(new String[] {"DELETE", "PUT"}) + .build(); + /* * Use the default config for the service at "/greet." */ Config config = minimalConfig(); CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); + corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* * Load a specific config for "/othergreet." @@ -84,15 +71,11 @@ static Routing.Builder prepRouting() { CORSSupport.Builder twoCORSSupportBuilder = CORSSupport.builder().config(twoCORSConfig.get(CrossOriginHelper.CORS_CONFIG_KEY)); - CrossOriginConfig cors3COC= CrossOriginConfig.Builder.create() - .value(new String[] {"http://foo.bar", "http://bar.foo"}) - .allowMethods(new String[] {"DELETE", "PUT"}) - .build(); - twoCORSSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); - + CORSSupport greetingCORSSupport = corsSupportBuilder.build(); + CORSSupport otherGreetingCORSSupport = twoCORSSupportBuilder.build(); Routing.Builder builder = Routing.builder() - .register(GREETING_PATH, corsSupportBuilder.build(), new GreetService()) - .register(OTHER_GREETING_PATH, twoCORSSupportBuilder.build(), new GreetService("Other Hello")); + .register(GREETING_PATH, greetingCORSSupport, new GreetService()) + .register(OTHER_GREETING_PATH, otherGreetingCORSSupport, new GreetService("Other Hello")); return builder; } @@ -117,52 +100,6 @@ static WebClient startupClient(WebServer server) { .build(); } - static WebClientResponse runTest1PreFlightAllowedOrigin(WebClient client, String prefix, String origin) throws ExecutionException, - InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(path(prefix, SERVICE_1)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, origin); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - return res; - } - - static void test2PreFlightAllowedHeaders1(WebClient client, String prefix, String origin, String headerToCheck) throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(path(prefix, SERVICE_2)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, origin); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - headers.add(ACCESS_CONTROL_REQUEST_HEADERS, headerToCheck); - - WebClientResponse res = reqBuilder - .request() - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers() - .first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is(origin))); - assertThat(res.headers() - .first(ACCESS_CONTROL_ALLOW_CREDENTIALS), present(is("true"))); - assertThat(res.headers() - .first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers() - .first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString(headerToCheck))); - assertThat(res.headers() - .first(ACCESS_CONTROL_MAX_AGE), notPresent()); - } - /** * Shuts down the specified web server. * @@ -200,25 +137,4 @@ static String path(CORSTestService testService) { static String path(String prefix, CORSTestService testService) { return prefix + testService.path(); } - - static void test3PreFlightAllowedOrigin(WebClient client) throws ExecutionException, InterruptedException { - WebClientRequestBuilder reqBuilder = client - .method(Http.Method.OPTIONS.name()) - .path(path(SERVICE_3)); - - Headers headers = reqBuilder.headers(); - headers.add(ORIGIN, "http://foo.bar"); - headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT"); - - WebClientResponse res = reqBuilder - .submit("") - .toCompletableFuture() - .get(); - - assertThat(res.status(), is(Http.Status.OK_200)); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT"))); - assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), notPresent()); - assertThat(res.headers().first(ACCESS_CONTROL_MAX_AGE), present(is("3600"))); - } } diff --git a/cors/src/test/resources/application.yaml b/cors/src/test/resources/application.yaml index 51c96023924..49e41dd8456 100644 --- a/cors/src/test/resources/application.yaml +++ b/cors/src/test/resources/application.yaml @@ -30,6 +30,4 @@ cors: allow-headers: ["X-bar", "X-foo"] allow-credentials: true max-age: -1 - - path-prefix: /cors3 - allow-origins: ["http://foo.bar", "http://bar.foo"] - allow-methods: ["DELETE", "PUT"] +# info for /cors3 is added programmatically \ No newline at end of file diff --git a/cors/src/test/resources/twoCORS.yaml b/cors/src/test/resources/twoCORS.yaml index d7994c9bcb8..7e81a73b824 100644 --- a/cors/src/test/resources/twoCORS.yaml +++ b/cors/src/test/resources/twoCORS.yaml @@ -21,4 +21,4 @@ cors: allow-credentials: true max-age: -1 -# /cors3 info is added programmatically +# /cors3 info is excluded from some tests, looking for failures From 09e8130ec313d8f1373fa88ebe217dffaad72e1e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 7 Apr 2020 11:55:54 -0500 Subject: [PATCH 050/100] Rename CrossOriginHelper to ...Internal; improve package-info --- .../java/io/helidon/cors/CORSSupport.java | 24 ++++--- .../io/helidon/cors/CrossOriginConfig.java | 36 +++++----- ...er.java => CrossOriginHelperInternal.java} | 18 +++-- .../java/io/helidon/cors/package-info.java | 67 ++++++++++++++++++- .../test/java/io/helidon/cors/TestUtil.java | 8 +-- cors/src/test/resources/twoCORS.yaml | 2 +- 6 files changed, 110 insertions(+), 45 deletions(-) rename cors/src/main/java/io/helidon/cors/{CrossOriginHelper.java => CrossOriginHelperInternal.java} (98%) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 93f944a30e4..120c3768c6d 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -28,31 +28,33 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; -import io.helidon.cors.CrossOriginHelper.RequestAdapter; -import io.helidon.cors.CrossOriginHelper.ResponseAdapter; +import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelper.normalize; -import static io.helidon.cors.CrossOriginHelper.prepareResponse; -import static io.helidon.cors.CrossOriginHelper.processRequest; +import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; +import static io.helidon.cors.CrossOriginHelperInternal.normalize; +import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.cors.CrossOriginHelperInternal.processRequest; /** * Provides support for CORS in an application or a built-in Helidon service. *

- * The application uses the {@link Builder} to set CORS-related values, including the @{code cors} config node from the + * The application uses the {@link Builder} to set CORS-related values, including the {@value CrossOriginConfig#CORS_CONFIG_KEY} + * config node from + * the * application config, if any. */ public class CORSSupport implements Service { /** * Creates a {@code CORSSupport} instance based on the default configuration and any - * {@value CrossOriginHelper#CORS_CONFIG_KEY} config node in it. + * {@value CrossOriginConfig#CORS_CONFIG_KEY} config node in it. * - * @return new {@code CORSSupport} instance set up with the "{@value CrossOriginHelper#CORS_CONFIG_KEY}" config from the + * @return new {@code CORSSupport} instance set up with the "{@value CrossOriginConfig#CORS_CONFIG_KEY}" config from the * default configuration */ public static CORSSupport create() { @@ -64,7 +66,7 @@ public static CORSSupport create() { * Creates a {@code CORSSupport} instance based on only configuration. * * @param config the config node containing CORS-related info; typically obtained by retrieving config using the - * "{@value CrossOriginHelper#CORS_CONFIG_KEY}" key from the application's or component's config + * "{@value CrossOriginConfig#CORS_CONFIG_KEY}" key from the application's or component's config * @return configured {@code CORSSupport} instance */ public static CORSSupport create(Config config) { @@ -140,7 +142,7 @@ public CORSSupport build() { /** * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the - * provided {@code Config} instance from its own config using the key {@value CrossOriginHelper#CORS_CONFIG_KEY}. + * provided {@code Config} instance from its own config using the key {@value CrossOriginConfig#CORS_CONFIG_KEY}. * * @param config the CORS config * @return the updated builder diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 01b1e409f0e..7f61aecf608 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -23,8 +23,8 @@ import io.helidon.config.Config; -import static io.helidon.cors.CrossOriginHelper.normalize; -import static io.helidon.cors.CrossOriginHelper.parseHeader; +import static io.helidon.cors.CrossOriginHelperInternal.normalize; +import static io.helidon.cors.CrossOriginHelperInternal.parseHeader; /** * Class CrossOriginConfig. @@ -67,8 +67,12 @@ public class CrossOriginConfig /* implements CrossOrigin */ { * Header Access-Control-Request-Method. */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** + * Key used for retrieving CORS-related configuration. + */ + public static final String CORS_CONFIG_KEY = "cors"; - private final String[] value; + private final String[] allowOrigins; private final String[] allowHeaders; private final String[] exposeHeaders; private final String[] allowMethods; @@ -76,7 +80,7 @@ public class CrossOriginConfig /* implements CrossOrigin */ { private final long maxAge; private CrossOriginConfig(Builder builder) { - this.value = builder.value; + this.allowOrigins = builder.origins; this.allowHeaders = builder.allowHeaders; this.exposeHeaders = builder.exposeHeaders; this.allowMethods = builder.allowMethods; @@ -88,8 +92,8 @@ private CrossOriginConfig(Builder builder) { * * @return origins */ - public String[] value() { - return copyOf(value); + public String[] allowOrigins() { + return copyOf(allowOrigins); } /** @@ -143,7 +147,7 @@ public static class Builder implements io.helidon.common.Builder apply(Config config) { Builder builder = new Builder(); String path = item.get("path-prefix").as(String.class).orElse(null); item.get("allow-origins").asList(String.class).ifPresent( - s -> builder.value(parseHeader(s).toArray(new String[]{}))); + s -> builder.allowOrigins(parseHeader(s).toArray(new String[]{}))); item.get("allow-methods").asList(String.class).ifPresent( s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); item.get("allow-headers").asList(String.class).ifPresent( @@ -264,4 +268,4 @@ public Map apply(Config config) { return result; } } -} +} \ No newline at end of file diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java similarity index 98% rename from cors/src/main/java/io/helidon/cors/CrossOriginHelper.java rename to cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java index 654557877c7..1008be1e8ca 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelper.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java @@ -42,22 +42,20 @@ import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; /** + * Not for use by developers. + *

This class is reserved for internal Helidon use. Do not use it from your applications. It might change or vanish at any time + * .

* Centralizes common logic to both SE and MP CORS support for processing requests and preparing responses. *

* To serve both masters, several methods here accept adapters for requests and responses. Both of these are minimal and very * specific to the needs of CORS support. *

*/ -public class CrossOriginHelper { +public class CrossOriginHelperInternal { - private CrossOriginHelper() { + private CrossOriginHelperInternal() { } - /** - * Key used for retrieving CORS-related configuration. - */ - public static final String CORS_CONFIG_KEY = "cors"; - static final String ORIGIN_DENIED = "CORS origin is denied"; static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; static final String METHOD_NOT_IN_ALLOWED_LIST = "CORS method is not in allowed list"; @@ -316,7 +314,7 @@ static Optional processCORSRequest( } // If enabled but not whitelisted, deny request - List allowedOrigins = Arrays.asList(crossOriginOpt.get().value()); + List allowedOrigins = Arrays.asList(crossOriginOpt.get().allowOrigins()); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { return Optional.of(responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); } @@ -354,7 +352,7 @@ static void prepareCORSResponse(Map crossOrigi .header(ACCESS_CONTROL_ALLOW_ORIGIN, origin) .header(Http.Header.VARY, ORIGIN); } else { - List allowedOrigins = Arrays.asList(crossOrigin.value()); + List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); responseAdapter.header(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin) .header(Http.Header.VARY, ORIGIN); } @@ -400,7 +398,7 @@ static U processCORSPreFlightRequest( CrossOriginConfig crossOrigin = crossOriginOpt.get(); // If enabled but not whitelisted, deny request - List allowedOrigins = Arrays.asList(crossOrigin.value()); + List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { return responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); } diff --git a/cors/src/main/java/io/helidon/cors/package-info.java b/cors/src/main/java/io/helidon/cors/package-info.java index 578b48dafb3..7055dfcb199 100644 --- a/cors/src/main/java/io/helidon/cors/package-info.java +++ b/cors/src/main/java/io/helidon/cors/package-info.java @@ -18,9 +18,70 @@ /** * Helidon SE CORS Support. *

- * Use {@link io.helidon.cors.CORSSupport} and its {@code Builder} to include support for CORS in your application. + * Use {@link io.helidon.cors.CORSSupport} and its {@code Builder} to add CORS handling to resources in your application. *

- * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CCORS information for - * your application yourself in your application's config file. + * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for + * your application another way, in three steps: + *

    + *
  1. Create an instance of {@code CORSSupport.Builder} for your Helidon service (your application): + *
    + *     CORSSupport.Builder corsBuilder = CORSSupport.builder();
    + * 
    + *
  2. + *
  3. Next, give the builder information about how to set up CORS for some or all of the resources in your app. You can use one + * or more of these approaches: + *
      + *
    • using configuration + *

      Often you would add a {@value io.helidon.cors.CrossOriginConfig#CORS_CONFIG_KEY} section to your application's + * default configuration file, like this: + *

      + *     cors:
      + *       - path-prefix: /cors1
      + *         allow-origins: ["*"]
      + *         allow-methods: ["*"]
      + *       - path-prefix: /cors2
      + *         allow-origins: ["http://foo.bar", "http://bar.foo"]
      + *         allow-methods: ["DELETE", "PUT"]
      + *         allow-headers: ["X-bar", "X-foo"]
      + *         allow-credentials: true
      + *         max-age: -1
      + *     
      + * and add code similar to this to retrieve it and use it: + *
      + *         Config corsConfig = Config.create().get(CrossOriginConfig.CORS_CONFIG_KEY);
      + *         corsBuilder.config(corsConfig);
      + *     
      + *
    • using the {@link io.helidon.cors.CrossOriginConfig} class + *

      Your code can create {@code CrossOriginConfig} instances and make them known to a {@code CORSSupport.Builder} + * using the {@link io.helidon.cors.CORSSupport.Builder#addCrossOrigin(java.lang.String, io.helidon.cors.CrossOriginConfig)} + * method. The {@code String} argument is the path within your application's context root to which this CORS + * set-up should apply. The following example has the same effect as the {@code /cors2} section from the config example above: + *

      + *
      + *         CrossOriginConfig cors2Setup = CrossOriginConfig.Builder.create()
      + *                 .allowOrigins("http://foo.bar", "http://bar.foo")
      + *                 .allowMethods("DELETE", "PUT")
      + *                 .allowHeaders("X-bar", "X-foo")
      + *                 .allowCredentials(true),
      + *                 .minAge(-1)
      + *                 .build();
      + *         corsBuilder().addCrossOrigin("/cors2", cors2Setup);
      + *     
      + *
    • + *
    + *
  4. Finally, create and register the {@code CORSSupport} instance on the same path as and + * before your own resources. The following code uses the {@code CORSSupport.Builder} from the earlier examples and + * registers it and your actual service on the same path with Helidon: + *
    + *     Routing.Builder builder = Routing.builder()
    + *                 .register("/myapp", corsBuilder.build(), new MyApp());
    + * 
    + *
  5. + *
+ * + *

+ * Note that {@code CrossOriginHelperInternal}, while {@code public}, is not intended for use by developers. It is + * reserved for internal Helidon use and might change at any time. + *

*/ package io.helidon.cors; diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index f7b9bb14e8c..153548ef396 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -53,15 +53,15 @@ private static WebServer startServer(int port, Routing.Builder routingBuilder) t static Routing.Builder prepRouting() { CrossOriginConfig cors3COC= CrossOriginConfig.Builder.create() - .value(new String[] {"http://foo.bar", "http://bar.foo"}) - .allowMethods(new String[] {"DELETE", "PUT"}) + .allowOrigins("http://foo.bar", "http://bar.foo") + .allowMethods("DELETE", "PUT") .build(); /* * Use the default config for the service at "/greet." */ Config config = minimalConfig(); - CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)); + CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginConfig.CORS_CONFIG_KEY)); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -69,7 +69,7 @@ static Routing.Builder prepRouting() { */ Config twoCORSConfig = minimalConfig(ConfigSources.classpath("twoCORS.yaml")); CORSSupport.Builder twoCORSSupportBuilder = - CORSSupport.builder().config(twoCORSConfig.get(CrossOriginHelper.CORS_CONFIG_KEY)); + CORSSupport.builder().config(twoCORSConfig.get(CrossOriginConfig.CORS_CONFIG_KEY)); CORSSupport greetingCORSSupport = corsSupportBuilder.build(); CORSSupport otherGreetingCORSSupport = twoCORSSupportBuilder.build(); diff --git a/cors/src/test/resources/twoCORS.yaml b/cors/src/test/resources/twoCORS.yaml index 7e81a73b824..125137e9580 100644 --- a/cors/src/test/resources/twoCORS.yaml +++ b/cors/src/test/resources/twoCORS.yaml @@ -21,4 +21,4 @@ cors: allow-credentials: true max-age: -1 -# /cors3 info is excluded from some tests, looking for failures +# Purposefully exclude /cors1. /cors3 information is added programmatically. From 51a0f3e77f1f7f43a569bbf60d3f657db9e2811f Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 7 Apr 2020 11:56:16 -0500 Subject: [PATCH 051/100] Adapt to change in API for CrossOriginConfig --- .../microprofile/cors/CrossOriginFilter.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 3e7efb4a0ae..9ffd81006ae 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -41,15 +41,15 @@ import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig; -import io.helidon.cors.CrossOriginHelper; -import io.helidon.cors.CrossOriginHelper.RequestAdapter; -import io.helidon.cors.CrossOriginHelper.ResponseAdapter; +import io.helidon.cors.CrossOriginHelperInternal; +import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; import org.eclipse.microprofile.config.ConfigProvider; import static io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; -import static io.helidon.cors.CrossOriginHelper.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelper.prepareResponse; +import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; +import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; /** * Class CrossOriginFilter. @@ -73,7 +73,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { - Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, + Optional response = CrossOriginHelperInternal.processRequest(crossOriginConfigs, crossOriginFromAnnotationFinder(resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter()); @@ -201,7 +201,7 @@ static Supplier> crossOriginFromAnnotationFinder(Res private static CrossOriginConfig annotationToConfig(CrossOrigin crossOrigin) { return CrossOriginConfig.Builder.create() - .value(crossOrigin.value()) + .allowOrigins(crossOrigin.value()) .allowHeaders(crossOrigin.allowHeaders()) .exposeHeaders(crossOrigin.exposeHeaders()) .allowMethods(crossOrigin.allowMethods()) From fe98eb2d771e7ff970cb404b3233df03bd93c57a Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 8 Apr 2020 06:02:35 -0500 Subject: [PATCH 052/100] Comment about replaceAll for headers in response adapter ok() method --- .../io/helidon/microprofile/cors/CrossOriginFilter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 9ffd81006ae..afac146efd0 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -159,10 +159,14 @@ public Response forbidden(String message) { @Override public Response ok() { Response.ResponseBuilder builder = Response.ok(); + /* + * The Helidon CORS support code invokes ok() only for creating a CORS preflight response. In these cases no user + * code will have a chance to set headers in the response. That means we can use replaceAll here because the only + * headers needed in the response are the ones set using this adapter. + */ builder.replaceAll(headers); return builder.build(); } - } static Supplier> crossOriginFromAnnotationFinder(ResourceInfo resourceInfo) { From 08cadce98eba6eea71f0b59b0884f62a59635f13 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 8 Apr 2020 06:05:25 -0500 Subject: [PATCH 053/100] A little clean-up --- .../java/io/helidon/cors/CORSSupport.java | 45 ++++++++----------- .../io/helidon/cors/CrossOriginConfig.java | 2 +- .../test/java/io/helidon/cors/TestUtil.java | 13 +++--- 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 120c3768c6d..358aad33aee 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -16,7 +16,6 @@ */ package io.helidon.cors; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +29,7 @@ import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.webserver.Handler; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; @@ -43,30 +43,26 @@ /** * Provides support for CORS in an application or a built-in Helidon service. *

- * The application uses the {@link Builder} to set CORS-related values, including the {@value CrossOriginConfig#CORS_CONFIG_KEY} - * config node from - * the - * application config, if any. + * The application uses the {@link Builder} to set CORS-related values. + *

*/ -public class CORSSupport implements Service { +public class CORSSupport implements Service, Handler { /** - * Creates a {@code CORSSupport} instance based on the default configuration and any - * {@value CrossOriginConfig#CORS_CONFIG_KEY} config node in it. + * Creates a {@code CORSSupport} instance based on only the {@value CrossOriginConfig#CORS_CONFIG_KEY} config node in the + * default configuration. * - * @return new {@code CORSSupport} instance set up with the "{@value CrossOriginConfig#CORS_CONFIG_KEY}" config from the - * default configuration + * @return new {@code CORSSupport} instance set up default configuration */ public static CORSSupport create() { - Config corsConfig = Config.create().get(CORS_CONFIG_KEY); - return create(corsConfig); + return builder().build(); } /** - * Creates a {@code CORSSupport} instance based on only configuration. + * Creates a {@code CORSSupport} instance based on only the specified configuration. * * @param config the config node containing CORS-related info; typically obtained by retrieving config using the - * "{@value CrossOriginConfig#CORS_CONFIG_KEY}" key from the application's or component's config + * "{@value CrossOriginConfig#CORS_CONFIG_KEY}" key from some containing configuration source * @return configured {@code CORSSupport} instance */ public static CORSSupport create(Config config) { @@ -96,16 +92,13 @@ private CORSSupport(Builder builder) { @Override public void update(Routing.Rules rules) { - configureCORS(rules); - } - - private void configureCORS(Routing.Rules rules) { if (!crossOriginConfigs.isEmpty()) { - rules.any(this::handleCORS); + rules.any(this::accept); } } - private void handleCORS(ServerRequest request, ServerResponse response) { + @Override + public void accept(ServerRequest request, ServerResponse response) { RequestAdapter requestAdapter = new SERequestAdapter(request); ResponseAdapter responseAdapter = new SEResponseAdapter(response); @@ -153,13 +146,15 @@ public Builder config(Config config) { } /** - * Returns CORS-related information that was derived from the app's or component's config node. + * Returns CORS-related information supplied to the builder. If no config was supplied to the builder, the builder uses + * the {@value CrossOriginConfig#CORS_CONFIG_KEY} node, if any, from the application's config. * * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions */ Map crossOriginConfigs() { - Map result = corsConfig.map(c -> c.as(new CrossOriginConfigMapper()).get()) - .orElse(Collections.emptyMap()); + Map result = corsConfig + .orElse(Config.create().get(CORS_CONFIG_KEY)) + .as(new CrossOriginConfigMapper()).get(); result.putAll(crossOrigins); return result; } @@ -175,10 +170,6 @@ public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { crossOrigins.put(normalize(path), crossOrigin); return this; } - - Map crossOrigins() { - return Collections.unmodifiableMap(crossOrigins); - } } private static class SERequestAdapter implements RequestAdapter { diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index 7f61aecf608..cfe80a000ca 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -268,4 +268,4 @@ public Map apply(Config config) { return result; } } -} \ No newline at end of file +} diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index 153548ef396..f95171dd2c1 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -58,10 +58,9 @@ static Routing.Builder prepRouting() { .build(); /* - * Use the default config for the service at "/greet." + * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - Config config = minimalConfig(); - CORSSupport.Builder corsSupportBuilder = CORSSupport.builder().config(config.get(CrossOriginConfig.CORS_CONFIG_KEY)); + CORSSupport.Builder corsSupportBuilder = CORSSupport.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -81,13 +80,11 @@ static Routing.Builder prepRouting() { } private static Config minimalConfig(Supplier configSource) { - Config.Builder builder = Config.builder() + Config.Builder configBuilder = Config.builder() .disableEnvironmentVariablesSource() .disableSystemPropertiesSource(); - if (configSource != null) { - builder.sources(configSource); - } - return builder.build(); + configBuilder.sources(configSource); + return configBuilder.build(); } private static Config minimalConfig() { From dd66c59340178ed6627de123bc3f9da88fd1e106 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 8 Apr 2020 18:20:12 -0500 Subject: [PATCH 054/100] At least for the moment, split handler and service implementations. Might unite them next. --- .../java/io/helidon/cors/CORSSupport.java | 205 +------------ .../io/helidon/cors/CrossOriginConfig.java | 84 +++++- .../io/helidon/cors/CrossOriginHandler.java | 277 ++++++++++++++++++ .../cors/CrossOriginHelperInternal.java | 117 +++++--- .../io/helidon/cors/CrossOriginService.java | 156 ++++++++++ .../java/io/helidon/cors/GreetService.java | 2 +- .../test/java/io/helidon/cors/TestUtil.java | 17 +- 7 files changed, 596 insertions(+), 262 deletions(-) create mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginHandler.java create mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginService.java diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 358aad33aee..346442d05f7 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -16,7 +16,6 @@ */ package io.helidon.cors; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -25,222 +24,20 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; -import io.helidon.config.Config; -import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; -import io.helidon.webserver.Handler; -import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.Service; -import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelperInternal.normalize; import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; import static io.helidon.cors.CrossOriginHelperInternal.processRequest; /** * Provides support for CORS in an application or a built-in Helidon service. - *

- * The application uses the {@link Builder} to set CORS-related values. - *

*/ -public class CORSSupport implements Service, Handler { - - /** - * Creates a {@code CORSSupport} instance based on only the {@value CrossOriginConfig#CORS_CONFIG_KEY} config node in the - * default configuration. - * - * @return new {@code CORSSupport} instance set up default configuration - */ - public static CORSSupport create() { - return builder().build(); - } - - /** - * Creates a {@code CORSSupport} instance based on only the specified configuration. - * - * @param config the config node containing CORS-related info; typically obtained by retrieving config using the - * "{@value CrossOriginConfig#CORS_CONFIG_KEY}" key from some containing configuration source - * @return configured {@code CORSSupport} instance - */ - public static CORSSupport create(Config config) { - return builder().config(config).build(); - } - - /** - * Returns a builder for constructing a {@code CORSSupport} instance. - * - * @return the new builder - */ - public static CORSSupport.Builder builder() { - return new Builder(); - } +public class CORSSupport { private static final Logger LOGGER = Logger.getLogger(CORSSupport.class.getName()); - static { - HelidonFeatures.register(HelidonFlavor.SE, "CORS"); - } - - private final Map crossOriginConfigs; - - private CORSSupport(Builder builder) { - crossOriginConfigs = builder.crossOriginConfigs(); - } - - @Override - public void update(Routing.Rules rules) { - if (!crossOriginConfigs.isEmpty()) { - rules.any(this::accept); - } - } - - @Override - public void accept(ServerRequest request, ServerResponse response) { - RequestAdapter requestAdapter = new SERequestAdapter(request); - ResponseAdapter responseAdapter = new SEResponseAdapter(response); - - Optional responseOpt = processRequest(crossOriginConfigs, - Optional::empty, - requestAdapter, - responseAdapter); - - responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, response)); - } - - private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ServerResponse response) { - prepareResponse( - crossOriginConfigs, - Optional::empty, - requestAdapter, - new SEResponseAdapter(response)); - - requestAdapter.request().next(); - } - - /** - * Builder for {@code CORSSupport} instances. - */ - public static class Builder implements io.helidon.common.Builder { - - private Optional corsConfig = Optional.empty(); - private final Map crossOrigins = new HashMap<>(); - - @Override - public CORSSupport build() { - return new CORSSupport(this); - } - - /** - * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the - * provided {@code Config} instance from its own config using the key {@value CrossOriginConfig#CORS_CONFIG_KEY}. - * - * @param config the CORS config - * @return the updated builder - */ - public Builder config(Config config) { - this.corsConfig = Optional.of(config); - return this; - } - - /** - * Returns CORS-related information supplied to the builder. If no config was supplied to the builder, the builder uses - * the {@value CrossOriginConfig#CORS_CONFIG_KEY} node, if any, from the application's config. - * - * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions - */ - Map crossOriginConfigs() { - Map result = corsConfig - .orElse(Config.create().get(CORS_CONFIG_KEY)) - .as(new CrossOriginConfigMapper()).get(); - result.putAll(crossOrigins); - return result; - } - - /** - * Adds cross origin information associated with a given path. - * - * @param path the path to which the cross origin information applies - * @param crossOrigin the cross origin information - * @return updated builder - */ - public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - crossOrigins.put(normalize(path), crossOrigin); - return this; - } - } - - private static class SERequestAdapter implements RequestAdapter { - - private final ServerRequest request; - - SERequestAdapter(ServerRequest request) { - this.request = request; - } - - @Override - public String path() { - return request.path().toString(); - } - - @Override - public Optional firstHeader(String key) { - return request.headers().first(key); - } - - @Override - public boolean headerContainsKey(String key) { - return firstHeader(key).isPresent(); - } - - @Override - public List allHeaders(String key) { - return request.headers().all(key); - } - - @Override - public String method() { - return request.method().name(); - } - - @Override - public ServerRequest request() { - return request; - } - } - - private static class SEResponseAdapter implements ResponseAdapter { - - private final ServerResponse serverResponse; - - SEResponseAdapter(ServerResponse serverResponse) { - this.serverResponse = serverResponse; - } - - @Override - public ResponseAdapter header(String key, String value) { - serverResponse.headers().add(key, value); - return this; - } - - @Override - public ResponseAdapter header(String key, Object value) { - serverResponse.headers().add(key, value.toString()); - return this; - } - - @Override - public ServerResponse forbidden(String message) { - serverResponse.status(Http.ResponseStatus.create(Http.Status.FORBIDDEN_403.code(), message)); - return serverResponse; - } - @Override - public ServerResponse ok() { - serverResponse.status(Http.Status.OK_200.code()); - return serverResponse; - } - } } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index cfe80a000ca..b1334fefdc2 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -27,7 +27,7 @@ import static io.helidon.cors.CrossOriginHelperInternal.parseHeader; /** - * Class CrossOriginConfig. + * Represents information about cross origin request sharing. */ public class CrossOriginConfig /* implements CrossOrigin */ { @@ -90,7 +90,15 @@ private CrossOriginConfig(Builder builder) { /** * - * @return origins + * @return a new builder for cross origin config + */ + public static Builder builder() { + return Builder.create(); + } + + /** + * + * @return the allowed origins */ public String[] allowOrigins() { return copyOf(allowOrigins); @@ -98,7 +106,7 @@ public String[] allowOrigins() { /** * - * @return allowHeaders + * @return the allowed headers */ public String[] allowHeaders() { return copyOf(allowHeaders); @@ -106,7 +114,7 @@ public String[] allowHeaders() { /** * - * @return exposeHeaders + * @return headers OK to expose in responses */ public String[] exposeHeaders() { return copyOf(exposeHeaders); @@ -114,7 +122,7 @@ public String[] exposeHeaders() { /** * - * @return allowMethods + * @return allowed methods */ public String[] allowMethods() { return copyOf(allowMethods); @@ -122,7 +130,7 @@ public String[] allowMethods() { /** * - * @return allowCredentials + * @return allowed credentials */ public boolean allowCredentials() { return allowCredentials; @@ -130,7 +138,7 @@ public boolean allowCredentials() { /** * - * @return maxAge + * @return maximum age */ public long maxAge() { return maxAge; @@ -140,10 +148,65 @@ private static String[] copyOf(String[] strings) { return strings != null ? Arrays.copyOf(strings, strings.length) : new String[0]; } + /** + * Defines common behavior between {@code CrossOriginConfig} and {@link CrossOriginHandler.Builder}. + * + * @param the type of the implementing class so the fluid methods can return the correct type + */ + interface Setter { + /** + * Sets the allowOrigins. + * + * @param origins the origin value(s) + * @return updated builder + */ + public T allowOrigins(String... origins); + + /** + * Sets the allow headers. + * + * @param allowHeaders the allow headers value(s) + * @return updated builder + */ + public T allowHeaders(String... allowHeaders); + + /** + * Sets the expose headers. + * + * @param exposeHeaders the expose headers value(s) + * @return updated builder + */ + public T exposeHeaders(String... exposeHeaders); + + /** + * Sets the allow methods. + * + * @param allowMethods the allow method value(s) + * @return updated builder + */ + public T allowMethods(String... allowMethods); + + /** + * Sets the allow credentials flag. + * + * @param allowCredentials the allow credentials flag + * @return updated builder + */ + public T allowCredentials(boolean allowCredentials); + + /** + * Sets the maximum age. + * + * @param maxAge the maximum age + * @return updated builder + */ + public T maxAge(long maxAge); + } + /** * Builder for {@link CrossOriginConfig}. */ - public static class Builder implements io.helidon.common.Builder { + public static class Builder implements Setter, io.helidon.common.Builder { private static final String[] ALLOW_ALL = {"*"}; @@ -171,6 +234,7 @@ public static Builder create() { * @param origins the origin value(s) * @return updated builder */ + @Override public Builder allowOrigins(String... origins) { this.origins = copyOf(origins); return this; @@ -182,6 +246,7 @@ public Builder allowOrigins(String... origins) { * @param allowHeaders the allow headers value(s) * @return updated builder */ + @Override public Builder allowHeaders(String... allowHeaders) { this.allowHeaders = copyOf(allowHeaders); return this; @@ -193,6 +258,7 @@ public Builder allowHeaders(String... allowHeaders) { * @param exposeHeaders the expose headers value(s) * @return updated builder */ + @Override public Builder exposeHeaders(String... exposeHeaders) { this.exposeHeaders = copyOf(exposeHeaders); return this; @@ -204,6 +270,7 @@ public Builder exposeHeaders(String... exposeHeaders) { * @param allowMethods the allow method value(s) * @return updated builder */ + @Override public Builder allowMethods(String... allowMethods) { this.allowMethods = copyOf(allowMethods); return this; @@ -226,6 +293,7 @@ public Builder allowCredentials(boolean allowCredentials) { * @param maxAge the maximum age * @return updated builder */ + @Override public Builder maxAge(long maxAge) { this.maxAge = maxAge; return this; diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java b/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java new file mode 100644 index 00000000000..b6d4bd96448 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; +import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.cors.CrossOriginHelperInternal.processRequest; + +/** + * Performs CORS request and response handling according to a single {@link CrossOriginConfig} instance. + */ +public class CrossOriginHandler implements io.helidon.webserver.Handler { + + /** + * Creates a handler according to the previously prepared {@code CrossOriginConfig}. + * + * @param crossOriginConfig the cross origin config to use for the handler + * @return the configured handler + */ + public static CrossOriginHandler create(CrossOriginConfig crossOriginConfig) { + return Builder.create(crossOriginConfig).build(); + } + + /** + * Creates a handler using default settings, as defined by the defaults in {@code CrossOriginConfig}. + * + * @return the configured handler + */ + public static CrossOriginHandler create() { + return Builder.create(CrossOriginConfig.builder().build()).build(); + } + + /** + * Creates a handler by looking in the provided Helidon {@code Config} node using the path as the key and interpreting the + * subtree there as {@code CrossOriginConfig} values. + * + * @param corsConfig config node to be interpreted as CORS information + * @param path the path to use in looking for the CORS information + * @return a handler initialized with the specified config information + */ + public static CrossOriginHandler create(Config corsConfig, String path) { + + Map fromConfig = corsConfig + .as(new CrossOriginConfig.CrossOriginConfigMapper()).get(); + return fromConfig.containsKey(path) ? create(fromConfig.get(path)) : create(); + } + + /** + * Creates a handler by looking in the application's default configuration for a node with key + * {@value CrossOriginConfig#CORS_CONFIG_KEY}, then looking within that subtree for a key matching the provided path. + * + * @param path the path to use in looking for the CORS information in the {@value CrossOriginConfig#CORS_CONFIG_KEY} + * @return + */ + public static CrossOriginHandler create(String path) { + return create(Config.create().get(CORS_CONFIG_KEY), path); + } + + /** + * Returns a builder which allows the caller to set individual items of the CORS behavior without having to construct a full + * {@code CrossOriginConfig} instance first. + * + * @return a builder initialized with default CORS configuration + */ + public static CrossOriginHandler.Builder builder() { + return Builder.create(); + } + + private final CrossOriginConfig crossOriginConfig; + + private CrossOriginHandler(Builder builder) { + crossOriginConfig = builder.crossOriginConfigBuilder.build(); + } + + /** + * Builder for {@code CORSSupport.Handler} instances. + *

+ * This builder is basically a shortcut which allows callers to set cross origin data directly on the handler without + * constructing a {@code CrossOriginConfig} first and then passing it to the handler builder. + *

+ */ + public static class Builder implements CrossOriginConfig.Setter, io.helidon.common.Builder { + + private final CrossOriginConfig.Builder crossOriginConfigBuilder = CrossOriginConfig.builder(); + + private Builder() { + } + + private Builder(CrossOriginConfig crossOriginConfig) { + crossOriginConfigBuilder.allowCredentials(crossOriginConfig.allowCredentials()); + crossOriginConfigBuilder.allowHeaders(crossOriginConfig.allowHeaders()); + crossOriginConfigBuilder.allowMethods(crossOriginConfig.allowMethods()); + crossOriginConfigBuilder.allowOrigins(crossOriginConfig.allowOrigins()); + crossOriginConfigBuilder.exposeHeaders(crossOriginConfig.exposeHeaders()); + crossOriginConfigBuilder.maxAge(crossOriginConfig.maxAge()); + } + + /** + * Creates a builder initialized with default cross origin information. + * + * @return initialized builder + */ + public static Builder create() { + return new Builder(); + } + + /** + * Creates a builder initialized with the specified {@code CrossOriginConfig} data. + * + * @param crossOriginConfig the cross origin config to use for initializing the builder + * @return the initialized builder + */ + public static Builder create(CrossOriginConfig crossOriginConfig) { + return new Builder(crossOriginConfig); + } + + @Override + public CrossOriginHandler build() { + return new CrossOriginHandler(this); + } + + @Override + public Builder allowOrigins(String... origins) { + crossOriginConfigBuilder.allowOrigins(origins); + return this; + } + + @Override + public Builder allowHeaders(String... allowHeaders) { + crossOriginConfigBuilder.allowHeaders(allowHeaders); + return this; + } + + @Override + public Builder exposeHeaders(String... exposeHeaders) { + crossOriginConfigBuilder.exposeHeaders(exposeHeaders);; + return this; + } + + @Override + public Builder allowMethods(String... allowMethods) { + crossOriginConfigBuilder.allowMethods(allowMethods);; + return this; + } + + @Override + public Builder allowCredentials(boolean allowCredentials) { + crossOriginConfigBuilder.allowCredentials(allowCredentials);; + return this; + } + + @Override + public Builder maxAge(long maxAge) { + crossOriginConfigBuilder.maxAge(maxAge);; + return this; + } + } + + @Override + public void accept(ServerRequest request, ServerResponse response) { + RequestAdapter requestAdapter = new SERequestAdapter(request); + ResponseAdapter responseAdapter = new SEResponseAdapter(response); + + Optional responseOpt = processRequest( + crossOriginConfig, + requestAdapter, + responseAdapter); + + responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, response)); + } + + private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ServerResponse response) { + prepareResponse( + crossOriginConfig, + requestAdapter, + new SEResponseAdapter(response)); + + requestAdapter.request().next(); + } + + static class SERequestAdapter implements RequestAdapter { + + private final ServerRequest request; + + SERequestAdapter(ServerRequest request) { + this.request = request; + } + + @Override + public String path() { + return request.path().toString(); + } + + @Override + public Optional firstHeader(String key) { + return request.headers().first(key); + } + + @Override + public boolean headerContainsKey(String key) { + return firstHeader(key).isPresent(); + } + + @Override + public List allHeaders(String key) { + return request.headers().all(key); + } + + @Override + public String method() { + return request.method().name(); + } + + @Override + public ServerRequest request() { + return request; + } + } + + static class SEResponseAdapter implements ResponseAdapter { + + private final ServerResponse serverResponse; + + SEResponseAdapter(ServerResponse serverResponse) { + this.serverResponse = serverResponse; + } + + @Override + public ResponseAdapter header(String key, String value) { + serverResponse.headers().add(key, value); + return this; + } + + @Override + public ResponseAdapter header(String key, Object value) { + serverResponse.headers().add(key, value.toString()); + return this; + } + + @Override + public ServerResponse forbidden(String message) { + serverResponse.status(Http.ResponseStatus.create(Http.Status.FORBIDDEN_403.code(), message)); + return serverResponse; + } + + @Override + public ServerResponse ok() { + serverResponse.status(Http.Status.OK_200.code()); + return serverResponse; + } + } +} diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java index 1008be1e8ca..50e30b2b2fb 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java @@ -28,6 +28,8 @@ import java.util.function.BiFunction; import java.util.function.Supplier; +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; import static io.helidon.common.http.Http.Header.HOST; @@ -80,6 +82,9 @@ public enum RequestType { */ PREFLIGHT } + static { + HelidonFeatures.register(HelidonFlavor.SE, "CORS"); + } /** * Minimal abstraction of an HTTP request. @@ -210,28 +215,76 @@ public static Optional processRequest(Map c Supplier> secondaryCrossOriginLookup, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + + Optional crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, + secondaryCrossOriginLookup); + + RequestType requestType = requestType(requestAdapter); + + if (requestType == RequestType.NORMAL) { + return Optional.empty(); + } + + // If this is a CORS request of some sort and CORS is not enabled, deny the request. + if (crossOrigin.isEmpty()) { + return Optional.of(responseAdapter.forbidden(ORIGIN_DENIED)); + } + return processRequest(requestType, crossOrigin.get(), requestAdapter, responseAdapter); + } + /** + * Processes a request according to the CORS rules, returning an {@code Optional} of the response type if + * the caller should send the response immediately (such as for a preflight response or an error response to a + * non-preflight CORS request). + *

+ * If the optional is empty, this processor has either: + *

+ *
    + *
  • recognized the request as a valid non-preflight CORS request and has set headers in the response adapter, or
  • + *
  • recognized the request as a non-CORS request entirely.
  • + *
+ *

+ * In either case of an empty optional return value, the caller should proceed with its own request processing and sends its + * response at will as long as that processing includes the header settings assigned using the response adapter. + *

+ * @param crossOrigin cross origin config to use in handling this request + * @param requestAdapter abstraction of a request + * @param responseAdapter abstraction of a response + * @param type for the {@code Request} managed by the requestAdapter + * @param the type for the HTTP response as returned from the responseSetter + * @return Optional of an error response if the request was an invalid CORS request; Optional.empty() if it was a + * valid CORS request + */ + public static Optional processRequest(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { RequestType requestType = requestType(requestAdapter); + if (requestType == RequestType.NORMAL) { + return Optional.empty(); + } + return processRequest(requestType, crossOrigin, requestAdapter, responseAdapter); + } + + static Optional processRequest(RequestType requestType, CrossOriginConfig crossOrigin, + RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { + switch (requestType) { case PREFLIGHT: - U result = processCORSPreFlightRequest(crossOriginConfigs, secondaryCrossOriginLookup, requestAdapter, + U result = processCORSPreFlightRequest(crossOrigin, requestAdapter, responseAdapter); return Optional.of(result); case CORS: - Optional corsResponse = processCORSRequest(crossOriginConfigs, secondaryCrossOriginLookup, requestAdapter, + Optional corsResponse = processCORSRequest(crossOrigin, requestAdapter, responseAdapter); if (corsResponse.isEmpty()) { /* * There has been no rejection of the CORS settings, so prep the response headers. */ - prepareCORSResponse(crossOriginConfigs, secondaryCrossOriginLookup, requestAdapter, responseAdapter); + prepareCORSResponse(crossOrigin, requestAdapter, responseAdapter); } return corsResponse; - case NORMAL: - return Optional.empty(); - default: throw new IllegalArgumentException("Unexpected value for enum RequestType"); } @@ -255,14 +308,22 @@ public static void prepareResponse(Map crossOr RequestType requestType = requestType(requestAdapter); if (requestType == RequestType.CORS) { + CrossOriginConfig crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup) + .orElseThrow(() -> new IllegalArgumentException( + "Could not locate expected CORS information while preparing response to request " + requestAdapter)); prepareCORSResponse( - crossOriginConfigs, - secondaryCrossOriginLookup, + crossOrigin, requestAdapter, responseAdapter); } } + public static void prepareResponse(CrossOriginConfig crossOrigin, + RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { + prepareCORSResponse(crossOrigin, requestAdapter, responseAdapter); + } + /** * Analyzes the request to determine the type of request, from the CORS perspective. * @@ -292,8 +353,6 @@ static RequestType requestType(RequestAdapter requestAdapter) { * Validates information about an incoming request as a CORS request and, if anything is wrong with CORS information, * returns an {@code Optional} error response reporting the problem. * - * @param crossOriginConfigs config information for CORS - * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param requestAdapter abstraction of a request * @param responseAdapter abstraction of a response * @param type for the request wrapped by the requestAdapter @@ -302,19 +361,13 @@ static RequestType requestType(RequestAdapter requestAdapter) { * valid CORS request */ static Optional processCORSRequest( - Map crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + CrossOriginConfig crossOriginConfig, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - Optional originOpt = requestAdapter.firstHeader(ORIGIN); - Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, - secondaryCrossOriginLookup); - if (crossOriginOpt.isEmpty()) { - return Optional.of(responseAdapter.forbidden(ORIGIN_DENIED)); - } // If enabled but not whitelisted, deny request - List allowedOrigins = Arrays.asList(crossOriginOpt.get().allowOrigins()); + List allowedOrigins = Arrays.asList(crossOriginConfig.allowOrigins()); + Optional originOpt = requestAdapter.firstHeader(ORIGIN); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { return Optional.of(responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); } @@ -326,21 +379,14 @@ static Optional processCORSRequest( /** * Prepares a CORS response by updating the response's headers. * - * @param crossOriginConfigs config information for CORS - * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param requestAdapter request adapter * @param responseAdapter response adapter * @param type for the request wrapped by the requestAdapter * @param type for the response wrapper by the responseAdapter */ - static void prepareCORSResponse(Map crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + static void prepareCORSResponse(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - CrossOriginConfig crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup) - .orElseThrow(() -> new IllegalArgumentException( - "Could not locate expected CORS information while preparing response to request " + requestAdapter)); - // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials. // // Throw an exception if there is no ORIGIN because we should not even be here unless this is a CORS request, which would @@ -369,34 +415,21 @@ static void prepareCORSResponse(Map crossOrigi * Having determined that we have a pre-flight request, we will always return either a forbidden or a successful response. *

* - * @param crossOriginConfigs config information for CORS - * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param requestAdapter the request adapter * @param responseAdapter the response adapter * @param type for the request wrapped by the requestAdapter * @param type for the response wrapper by the responseAdapter * @return the response returned by the response adapter with CORS-related headers set (for a successful CORS preflight) */ - static U processCORSPreFlightRequest( - Map crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, + static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { Optional originOpt = requestAdapter.firstHeader(ORIGIN); - Optional crossOriginOpt = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, - secondaryCrossOriginLookup); - - // If CORS not enabled, deny request - if (crossOriginOpt.isEmpty()) { - return responseAdapter.forbidden(ORIGIN_DENIED); - } if (originOpt.isEmpty()) { return responseAdapter.forbidden(noRequiredHeader(ORIGIN)); } - CrossOriginConfig crossOrigin = crossOriginOpt.get(); - // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { @@ -525,7 +558,7 @@ static String normalize(String path) { int length = path.length(); int beginIndex = path.charAt(0) == '/' ? 1 : 0; int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; - return path.substring(beginIndex, endIndex); + return (endIndex<= beginIndex) ? "" : path.substring(beginIndex, endIndex); } /** diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginService.java b/cors/src/main/java/io/helidon/cors/CrossOriginService.java new file mode 100644 index 00000000000..a37b4b715fe --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/CrossOriginService.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.cors; + +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; +import io.helidon.cors.CrossOriginHandler.SERequestAdapter; +import io.helidon.cors.CrossOriginHandler.SEResponseAdapter; +import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; +import static io.helidon.cors.CrossOriginHelperInternal.normalize; +import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.cors.CrossOriginHelperInternal.processRequest; + +public class CrossOriginService implements Service { + + private final Map crossOriginConfigs; + + private CrossOriginService(Builder builder) { + crossOriginConfigs = builder.crossOriginConfigs(); + } + + public static CrossOriginService create() { + return builder().build(); + } + + public static CrossOriginService create(Config config) { + return builder(config).build(); + } + + public static Builder builder() { + return Builder.create(); + } + + public static Builder builder(Config config) { + return Builder.create(config); + } + + @Override + public void update(Routing.Rules rules) { + if (!crossOriginConfigs.isEmpty()) { + rules.any(this::accept); + } + } + +// @Override + public void accept(ServerRequest request, ServerResponse response) { + RequestAdapter requestAdapter = new SERequestAdapter(request); + ResponseAdapter responseAdapter = new SEResponseAdapter(response); + + Optional responseOpt = processRequest(crossOriginConfigs, + Optional::empty, + requestAdapter, + responseAdapter); + + responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, response)); + } + + private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ServerResponse response) { + prepareResponse( + crossOriginConfigs, + Optional::empty, + requestAdapter, + new SEResponseAdapter(response)); + + requestAdapter.request().next(); + } + + private static String ensureLeadingSlash(String path) { + return "/" + normalize(path); + } + + /** + * Builder for {@code CORSSupport} instances. + */ + public static class Builder implements io.helidon.common.Builder { + + private Optional corsConfig = Optional.empty(); + private final Map crossOrigins = new HashMap<>(); + + public static Builder create() { + return create(Config.create().get(CORS_CONFIG_KEY)); + } + + public static Builder create(Config config) { + return new Builder().config(config); + } + + @Override + public CrossOriginService build() { + return new CrossOriginService(this); + } + + /** + * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the + * provided {@code Config} instance from its own config using the key {@value CrossOriginConfig#CORS_CONFIG_KEY}. + * + * @param config the CORS config + * @return the updated builder + */ + public Builder config(Config config) { + this.corsConfig = Optional.of(config); + return this; + } + + /** + * Returns CORS-related information supplied to the builder. If no config was supplied to the builder, the builder uses + * the {@value CrossOriginConfig#CORS_CONFIG_KEY} node, if any, from the application's config. + * + * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions + */ + Map crossOriginConfigs() { + Map result = corsConfig + .orElse(Config.create().get(CORS_CONFIG_KEY)) + .as(new CrossOriginConfigMapper()).get(); + result.putAll(crossOrigins); + return result; + } + + /** + * Adds cross origin information associated with a given path. + * + * @param path the path to which the cross origin information applies + * @param crossOrigin the cross origin information + * @return updated builder + */ + public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + crossOrigins.put(normalize(path), crossOrigin); + return this; + } + } +} diff --git a/cors/src/test/java/io/helidon/cors/GreetService.java b/cors/src/test/java/io/helidon/cors/GreetService.java index f9970d62533..9393ddff605 100644 --- a/cors/src/test/java/io/helidon/cors/GreetService.java +++ b/cors/src/test/java/io/helidon/cors/GreetService.java @@ -46,7 +46,7 @@ public void update(Routing.Rules rules) { } } - private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { + void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { String msg = String.format("%s %s!", greeting, new Date().toString()); response.status(Http.Status.OK_200.code()); response.send(msg); diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index f95171dd2c1..2401ef2c411 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -31,6 +31,7 @@ import io.helidon.webserver.WebServer; import static io.helidon.cors.CORSTestServices.SERVICE_3; +import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; public class TestUtil { @@ -60,21 +61,23 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CORSSupport.Builder corsSupportBuilder = CORSSupport.builder(); + CrossOriginService.Builder corsSupportBuilder = CrossOriginService.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* * Load a specific config for "/othergreet." */ Config twoCORSConfig = minimalConfig(ConfigSources.classpath("twoCORS.yaml")); - CORSSupport.Builder twoCORSSupportBuilder = - CORSSupport.builder().config(twoCORSConfig.get(CrossOriginConfig.CORS_CONFIG_KEY)); + CrossOriginService.Builder twoCORSSupportBuilder = + CrossOriginService.builder().config(twoCORSConfig.get(CORS_CONFIG_KEY)); - CORSSupport greetingCORSSupport = corsSupportBuilder.build(); - CORSSupport otherGreetingCORSSupport = twoCORSSupportBuilder.build(); Routing.Builder builder = Routing.builder() - .register(GREETING_PATH, greetingCORSSupport, new GreetService()) - .register(OTHER_GREETING_PATH, otherGreetingCORSSupport, new GreetService("Other Hello")); + .register(GREETING_PATH, + CrossOriginService.create(), // use "cors" from default app config + new GreetService()) + .register(OTHER_GREETING_PATH, + CrossOriginService.create(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself + new GreetService("Other Hello")); return builder; } From affd2d635333e13d50bfc3347be2ac0aabfb69e3 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 06:10:16 -0500 Subject: [PATCH 055/100] Clean up the API a bit. More to come --- .../io/helidon/cors/CrossOriginHandler.java | 40 +++----- .../io/helidon/cors/CrossOriginService.java | 95 +++++++++++++++---- .../test/java/io/helidon/cors/TestUtil.java | 4 +- 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java b/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java index b6d4bd96448..ec2b8a8369b 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java @@ -37,13 +37,13 @@ public class CrossOriginHandler implements io.helidon.webserver.Handler { /** - * Creates a handler according to the previously prepared {@code CrossOriginConfig}. + * Creates a handler according to the supplied {@code CrossOriginConfig}. * * @param crossOriginConfig the cross origin config to use for the handler * @return the configured handler */ public static CrossOriginHandler create(CrossOriginConfig crossOriginConfig) { - return Builder.create(crossOriginConfig).build(); + return builder().crossOriginConfig(crossOriginConfig).build(); } /** @@ -52,7 +52,7 @@ public static CrossOriginHandler create(CrossOriginConfig crossOriginConfig) { * @return the configured handler */ public static CrossOriginHandler create() { - return Builder.create(CrossOriginConfig.builder().build()).build(); + return builder().build(); // Builder.create(CrossOriginConfig.builder().build()).build(); } /** @@ -87,8 +87,8 @@ public static CrossOriginHandler create(String path) { * * @return a builder initialized with default CORS configuration */ - public static CrossOriginHandler.Builder builder() { - return Builder.create(); + public static Builder builder() { + return new Builder(); } private final CrossOriginConfig crossOriginConfig; @@ -106,37 +106,27 @@ private CrossOriginHandler(Builder builder) { */ public static class Builder implements CrossOriginConfig.Setter, io.helidon.common.Builder { + private Optional config = Optional.empty(); + private final CrossOriginConfig.Builder crossOriginConfigBuilder = CrossOriginConfig.builder(); private Builder() { } - private Builder(CrossOriginConfig crossOriginConfig) { + /** + * Sets the builder initialized with the specified {@code CrossOriginConfig} data. + * + * @param crossOriginConfig the cross origin config to use for initializing the builder + * @return the initialized builder + */ + public Builder crossOriginConfig(CrossOriginConfig crossOriginConfig) { crossOriginConfigBuilder.allowCredentials(crossOriginConfig.allowCredentials()); crossOriginConfigBuilder.allowHeaders(crossOriginConfig.allowHeaders()); crossOriginConfigBuilder.allowMethods(crossOriginConfig.allowMethods()); crossOriginConfigBuilder.allowOrigins(crossOriginConfig.allowOrigins()); crossOriginConfigBuilder.exposeHeaders(crossOriginConfig.exposeHeaders()); crossOriginConfigBuilder.maxAge(crossOriginConfig.maxAge()); - } - - /** - * Creates a builder initialized with default cross origin information. - * - * @return initialized builder - */ - public static Builder create() { - return new Builder(); - } - - /** - * Creates a builder initialized with the specified {@code CrossOriginConfig} data. - * - * @param crossOriginConfig the cross origin config to use for initializing the builder - * @return the initialized builder - */ - public static Builder create(CrossOriginConfig crossOriginConfig) { - return new Builder(crossOriginConfig); + return this; } @Override diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginService.java b/cors/src/main/java/io/helidon/cors/CrossOriginService.java index a37b4b715fe..ca0e430ea38 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginService.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginService.java @@ -36,6 +36,24 @@ import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; import static io.helidon.cors.CrossOriginHelperInternal.processRequest; +/** + * A Helidon service implementation that implements CORS for endpoints in the application or in built-in Helidon services such + * as OpenAPI and metrics. + *

+ * The caller can set up the {@code CrossOriginService} in a combination of these ways: + *

    + *
  • from the {@value CrossOriginConfig#CORS_CONFIG_KEY} node in the application's default config,
  • + *
  • from a {@link Config} node supplied programmatically, and
  • + *
  • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which + * it applies.
  • + *
+ * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. + *

+ *

+ * If none of these sources is used, the {@code CrossOriginService} applies defaults as described for + * {@link CrossOriginConfig}. + *

+ */ public class CrossOriginService implements Service { private final Map crossOriginConfigs; @@ -44,20 +62,46 @@ private CrossOriginService(Builder builder) { crossOriginConfigs = builder.crossOriginConfigs(); } + /** + * Creates a {@code CrossOriginService} which supports the default CORS set-up. + * + * @return the service + */ public static CrossOriginService create() { return builder().build(); } - public static CrossOriginService create(Config config) { - return builder(config).build(); + /** + * Returns a {@code CrossOriginService} set up using the supplied {@link Config} node. + * + * @param config the config node containing CORS information + * @return the initialized service + */ + public static CrossOriginService fromConfig(Config config) { + return builder().config(config).build(); } + /** + * Creates a {@code CrossOriginService} set up using the {@value CrossOriginConfig#CORS_CONFIG_KEY} node in the + * application's default config. + * + * @return the initialized service + */ + public static CrossOriginService fromConfig() { + return fromConfig(Config.create().get(CORS_CONFIG_KEY)); + } + + /** + * Creates a {@code Builder} for assembling a {@code CrossOriginService}. + * + * @return the builder + */ public static Builder builder() { - return Builder.create(); + return new Builder(); } public static Builder builder(Config config) { - return Builder.create(config); + return builder().config(config); } @Override @@ -95,20 +139,14 @@ private static String ensureLeadingSlash(String path) { } /** - * Builder for {@code CORSSupport} instances. + * Builder for {@code CrossOriginService} instances. */ public static class Builder implements io.helidon.common.Builder { - private Optional corsConfig = Optional.empty(); - private final Map crossOrigins = new HashMap<>(); + private final Map crossOriginConfigs = new HashMap<>(); - public static Builder create() { - return create(Config.create().get(CORS_CONFIG_KEY)); - } - - public static Builder create(Config config) { - return new Builder().config(config); - } + private Config corsConfig = Config.empty(); + private Optional crossOriginConfigBuilder = Optional.empty(); // CrossOriginConfig.builder(); @Override public CrossOriginService build() { @@ -123,21 +161,36 @@ public CrossOriginService build() { * @return the updated builder */ public Builder config(Config config) { - this.corsConfig = Optional.of(config); + this.corsConfig = config; + return this; + } + + /** + * Initializes the builder's CORS config from the {@value CrossOriginConfig#CORS_CONFIG_KEY} node from the default + * application config. + * + * @return the updated builder + */ + public Builder config() { + corsConfig = Config.create().get(CORS_CONFIG_KEY); return this; } /** - * Returns CORS-related information supplied to the builder. If no config was supplied to the builder, the builder uses - * the {@value CrossOriginConfig#CORS_CONFIG_KEY} node, if any, from the application's config. + * Returns the aggregation of CORS-related information supplied to the builder, constructed in this order (in case of + * conflicts, last wins): + *
    + *
  1. from {@code Config} supplied using {@link #config(Config)}or inferred using {@link #config()}, then
  2. + *
  3. from {@code CrossOriginConfig} instances added using {@link #addCrossOrigin(String, CrossOriginConfig)}.
  4. + *
* * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions */ Map crossOriginConfigs() { Map result = corsConfig - .orElse(Config.create().get(CORS_CONFIG_KEY)) - .as(new CrossOriginConfigMapper()).get(); - result.putAll(crossOrigins); + .as(new CrossOriginConfigMapper()) + .get(); + result.putAll(crossOriginConfigs); return result; } @@ -149,7 +202,7 @@ Map crossOriginConfigs() { * @return updated builder */ public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - crossOrigins.put(normalize(path), crossOrigin); + crossOriginConfigs.put(normalize(path), crossOrigin); return this; } } diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java index 2401ef2c411..15d1311e10e 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java @@ -73,10 +73,10 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CrossOriginService.create(), // use "cors" from default app config + CrossOriginService.fromConfig(), // use "cors" from default app config new GreetService()) .register(OTHER_GREETING_PATH, - CrossOriginService.create(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself + CrossOriginService.fromConfig(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself new GreetService("Other Hello")); return builder; From f6be1df52d0310ca0bab9220e2e96d1220d8fc5b Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 07:46:08 -0500 Subject: [PATCH 056/100] Refactoring, combining the separate service and handler into CORSSupport; moving the req and resp handler interfaces and SE impls into their own files --- .../java/io/helidon/cors/CORSSupport.java | 239 +++++++++++++++- .../io/helidon/cors/CrossOriginConfig.java | 18 +- .../io/helidon/cors/CrossOriginHandler.java | 267 ------------------ .../cors/CrossOriginHelperInternal.java | 130 ++------- .../io/helidon/cors/CrossOriginService.java | 209 -------------- .../java/io/helidon/cors/RequestAdapter.java | 72 +++++ .../java/io/helidon/cors/ResponseAdapter.java | 65 +++++ .../io/helidon/cors/SERequestAdapter.java | 64 +++++ .../io/helidon/cors/SEResponseAdapter.java | 56 ++++ .../java/io/helidon/cors/package-info.java | 2 +- .../io/helidon/cors/TestTwoCORSConfigs.java | 2 +- .../test/java/io/helidon/cors/TestUtil.java | 12 +- 12 files changed, 522 insertions(+), 614 deletions(-) delete mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginHandler.java delete mode 100644 cors/src/main/java/io/helidon/cors/CrossOriginService.java create mode 100644 cors/src/main/java/io/helidon/cors/RequestAdapter.java create mode 100644 cors/src/main/java/io/helidon/cors/ResponseAdapter.java create mode 100644 cors/src/main/java/io/helidon/cors/SERequestAdapter.java create mode 100644 cors/src/main/java/io/helidon/cors/SEResponseAdapter.java diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java index 346442d05f7..7aeab65182f 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java @@ -16,28 +16,247 @@ */ package io.helidon.cors; -import java.util.List; +import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.logging.Logger; -import io.helidon.common.HelidonFeatures; -import io.helidon.common.HelidonFlavor; -import io.helidon.common.http.Http; -import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; -import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; +import io.helidon.webserver.Handler; +import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; +import static io.helidon.cors.CrossOriginHelperInternal.normalize; import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; import static io.helidon.cors.CrossOriginHelperInternal.processRequest; /** - * Provides support for CORS in an application or a built-in Helidon service. + * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon + * services (such as OpenAPI and metrics). + *

+ * The caller can set up the {@code CORSSupport} in a combination of these ways: + *

    + *
  • from the {@value CORSSupport#CORS_CONFIG_KEY} node in the application's default config,
  • + *
  • from a {@link Config} node supplied programmatically,
  • + *
  • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which + * it applies, and
  • + *
  • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the + * "/" path).
  • + *
+ * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. + *

+ *

+ * If none of these sources is used, the {@code CORSSupport} applies defaults as described for + * {@link CrossOriginConfig}. + *

*/ -public class CORSSupport { +public class CORSSupport implements Service, Handler { - private static final Logger LOGGER = Logger.getLogger(CORSSupport.class.getName()); + /** + * Key used for retrieving CORS-related configuration from application- or service-level configuration. + */ + public static final String CORS_CONFIG_KEY = "cors"; + private final Map crossOriginConfigs; + + private CORSSupport(Builder builder) { + crossOriginConfigs = builder.crossOriginConfigs(); + } + + /** + * Creates a {@code CORSSupport} which supports the default CORS set-up. + * + * @return the service + */ + public static CORSSupport create() { + return builder().build(); + } + + /** + * Returns a {@code CORSSupport} set up using the supplied {@link Config} node. + * + * @param config the config node containing CORS information + * @return the initialized service + */ + public static CORSSupport fromConfig(Config config) { + return builder().config(config).build(); + } + + /** + * Creates a {@code CORSSupport} set up using the {@value CORSSupport#CORS_CONFIG_KEY} node in the + * application's default config. + * + * @return the initialized service + */ + public static CORSSupport fromConfig() { + return fromConfig(Config.create().get(CORS_CONFIG_KEY)); + } + + /** + * Creates a {@code Builder} for assembling a {@code CORSSupport}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. The config node + * should contain the actual CORS settings, not a {@value CORS_CONFIG_KEY} node which contains them. + * + * @param config node containing CORS information + * @return builder initialized with the CORS set-up from the config + */ + public static Builder builder(Config config) { + return builder().config(config); + } + + @Override + public void update(Routing.Rules rules) { + if (!crossOriginConfigs.isEmpty()) { + rules.any(this::accept); + } + } + + @Override + public void accept(ServerRequest request, ServerResponse response) { + RequestAdapter requestAdapter = new SERequestAdapter(request); + ResponseAdapter responseAdapter = new SEResponseAdapter(response); + + Optional responseOpt = processRequest(crossOriginConfigs, + Optional::empty, + requestAdapter, + responseAdapter); + + responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, responseAdapter)); + } + + private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { + prepareResponse( + crossOriginConfigs, + Optional::empty, + requestAdapter, + responseAdapter); + + requestAdapter.request().next(); + } + + /** + * Builder for {@code CORSSupport} instances. + */ + public static class Builder implements io.helidon.common.Builder, CrossOriginConfig.Setter { + + private final Map crossOriginConfigs = new HashMap<>(); + + private Config corsConfig = Config.empty(); + private Optional crossOriginConfigBuilderOpt = Optional.empty(); + + @Override + public CORSSupport build() { + return new CORSSupport(this); + } + + /** + * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the + * provided {@code Config} instance from its own config using the key {@value CORSSupport#CORS_CONFIG_KEY}. + * + * @param config the CORS config + * @return the updated builder + */ + public Builder config(Config config) { + this.corsConfig = config; + return this; + } + + /** + * Initializes the builder's CORS config from the {@value CORSSupport#CORS_CONFIG_KEY} node from the default + * application config. + * + * @return the updated builder + */ + public Builder config() { + corsConfig = Config.create().get(CORS_CONFIG_KEY); + return this; + } + + /** + * Adds cross origin information associated with a given path. + * + * @param path the path to which the cross origin information applies + * @param crossOrigin the cross origin information + * @return updated builder + */ + public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + crossOriginConfigs.put(normalize(path), crossOrigin); + return this; + } + + @Override + public Builder allowOrigins(String... origins) { + crossOriginConfigBuilder().allowOrigins(origins); + return this; + } + + @Override + public Builder allowHeaders(String... allowHeaders) { + crossOriginConfigBuilder().allowHeaders(allowHeaders); + return this; + } + + @Override + public Builder exposeHeaders(String... exposeHeaders) { + crossOriginConfigBuilder().exposeHeaders(exposeHeaders); + return this; + } + + @Override + public Builder allowMethods(String... allowMethods) { + crossOriginConfigBuilder().allowMethods(allowMethods); + return this; + } + + @Override + public Builder allowCredentials(boolean allowCredentials) { + crossOriginConfigBuilder().allowCredentials(allowCredentials); + return this; + } + + @Override + public Builder maxAge(long maxAge) { + crossOriginConfigBuilder().maxAge(maxAge); + return this; + } + + /** + * Returns the aggregation of CORS-related information supplied to the builder, constructed in this order (in case of + * conflicts, later steps override earlier ones): + *
    + *
  1. from {@code CrossOriginConfig} instances added using {@link #addCrossOrigin(String, CrossOriginConfig)},
  2. + *
  3. from invocations of the setter methods from {@link CrossOriginConfig} to set behavior for the "/" path,
  4. + *
  5. from {@code Config} supplied using {@link #config(Config)}or inferred using {@link #config()}.
  6. + *
+ * + * @return map of CrossOriginConfig instances, each entry describing a path and its associated CORS set-up + */ + Map crossOriginConfigs() { + final Map result = new HashMap<>(crossOriginConfigs); + crossOriginConfigBuilderOpt.ifPresent(opt -> result.put("/", opt.get())); + result.putAll(corsConfig + .as(new CrossOriginConfigMapper()) + .get()); + return result; + } + + private CrossOriginConfig.Builder crossOriginConfigBuilder() { + if (crossOriginConfigBuilderOpt.isEmpty()) { + crossOriginConfigBuilderOpt = Optional.of(CrossOriginConfig.builder()); + } + return crossOriginConfigBuilderOpt.get(); + } + } } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java index b1334fefdc2..b24d7d6abb5 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java @@ -67,10 +67,6 @@ public class CrossOriginConfig /* implements CrossOrigin */ { * Header Access-Control-Request-Method. */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - /** - * Key used for retrieving CORS-related configuration. - */ - public static final String CORS_CONFIG_KEY = "cors"; private final String[] allowOrigins; private final String[] allowHeaders; @@ -149,7 +145,7 @@ private static String[] copyOf(String[] strings) { } /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CrossOriginHandler.Builder}. + * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder}. * * @param the type of the implementing class so the fluid methods can return the correct type */ @@ -160,7 +156,7 @@ interface Setter { * @param origins the origin value(s) * @return updated builder */ - public T allowOrigins(String... origins); + T allowOrigins(String... origins); /** * Sets the allow headers. @@ -168,7 +164,7 @@ interface Setter { * @param allowHeaders the allow headers value(s) * @return updated builder */ - public T allowHeaders(String... allowHeaders); + T allowHeaders(String... allowHeaders); /** * Sets the expose headers. @@ -176,7 +172,7 @@ interface Setter { * @param exposeHeaders the expose headers value(s) * @return updated builder */ - public T exposeHeaders(String... exposeHeaders); + T exposeHeaders(String... exposeHeaders); /** * Sets the allow methods. @@ -184,7 +180,7 @@ interface Setter { * @param allowMethods the allow method value(s) * @return updated builder */ - public T allowMethods(String... allowMethods); + T allowMethods(String... allowMethods); /** * Sets the allow credentials flag. @@ -192,7 +188,7 @@ interface Setter { * @param allowCredentials the allow credentials flag * @return updated builder */ - public T allowCredentials(boolean allowCredentials); + T allowCredentials(boolean allowCredentials); /** * Sets the maximum age. @@ -200,7 +196,7 @@ interface Setter { * @param maxAge the maximum age * @return updated builder */ - public T maxAge(long maxAge); + T maxAge(long maxAge); } /** diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java b/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java deleted file mode 100644 index ec2b8a8369b..00000000000 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHandler.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * 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 io.helidon.cors; - -import io.helidon.common.http.Http; -import io.helidon.config.Config; -import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; -import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; -import io.helidon.webserver.ServerRequest; -import io.helidon.webserver.ServerResponse; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; -import static io.helidon.cors.CrossOriginHelperInternal.processRequest; - -/** - * Performs CORS request and response handling according to a single {@link CrossOriginConfig} instance. - */ -public class CrossOriginHandler implements io.helidon.webserver.Handler { - - /** - * Creates a handler according to the supplied {@code CrossOriginConfig}. - * - * @param crossOriginConfig the cross origin config to use for the handler - * @return the configured handler - */ - public static CrossOriginHandler create(CrossOriginConfig crossOriginConfig) { - return builder().crossOriginConfig(crossOriginConfig).build(); - } - - /** - * Creates a handler using default settings, as defined by the defaults in {@code CrossOriginConfig}. - * - * @return the configured handler - */ - public static CrossOriginHandler create() { - return builder().build(); // Builder.create(CrossOriginConfig.builder().build()).build(); - } - - /** - * Creates a handler by looking in the provided Helidon {@code Config} node using the path as the key and interpreting the - * subtree there as {@code CrossOriginConfig} values. - * - * @param corsConfig config node to be interpreted as CORS information - * @param path the path to use in looking for the CORS information - * @return a handler initialized with the specified config information - */ - public static CrossOriginHandler create(Config corsConfig, String path) { - - Map fromConfig = corsConfig - .as(new CrossOriginConfig.CrossOriginConfigMapper()).get(); - return fromConfig.containsKey(path) ? create(fromConfig.get(path)) : create(); - } - - /** - * Creates a handler by looking in the application's default configuration for a node with key - * {@value CrossOriginConfig#CORS_CONFIG_KEY}, then looking within that subtree for a key matching the provided path. - * - * @param path the path to use in looking for the CORS information in the {@value CrossOriginConfig#CORS_CONFIG_KEY} - * @return - */ - public static CrossOriginHandler create(String path) { - return create(Config.create().get(CORS_CONFIG_KEY), path); - } - - /** - * Returns a builder which allows the caller to set individual items of the CORS behavior without having to construct a full - * {@code CrossOriginConfig} instance first. - * - * @return a builder initialized with default CORS configuration - */ - public static Builder builder() { - return new Builder(); - } - - private final CrossOriginConfig crossOriginConfig; - - private CrossOriginHandler(Builder builder) { - crossOriginConfig = builder.crossOriginConfigBuilder.build(); - } - - /** - * Builder for {@code CORSSupport.Handler} instances. - *

- * This builder is basically a shortcut which allows callers to set cross origin data directly on the handler without - * constructing a {@code CrossOriginConfig} first and then passing it to the handler builder. - *

- */ - public static class Builder implements CrossOriginConfig.Setter, io.helidon.common.Builder { - - private Optional config = Optional.empty(); - - private final CrossOriginConfig.Builder crossOriginConfigBuilder = CrossOriginConfig.builder(); - - private Builder() { - } - - /** - * Sets the builder initialized with the specified {@code CrossOriginConfig} data. - * - * @param crossOriginConfig the cross origin config to use for initializing the builder - * @return the initialized builder - */ - public Builder crossOriginConfig(CrossOriginConfig crossOriginConfig) { - crossOriginConfigBuilder.allowCredentials(crossOriginConfig.allowCredentials()); - crossOriginConfigBuilder.allowHeaders(crossOriginConfig.allowHeaders()); - crossOriginConfigBuilder.allowMethods(crossOriginConfig.allowMethods()); - crossOriginConfigBuilder.allowOrigins(crossOriginConfig.allowOrigins()); - crossOriginConfigBuilder.exposeHeaders(crossOriginConfig.exposeHeaders()); - crossOriginConfigBuilder.maxAge(crossOriginConfig.maxAge()); - return this; - } - - @Override - public CrossOriginHandler build() { - return new CrossOriginHandler(this); - } - - @Override - public Builder allowOrigins(String... origins) { - crossOriginConfigBuilder.allowOrigins(origins); - return this; - } - - @Override - public Builder allowHeaders(String... allowHeaders) { - crossOriginConfigBuilder.allowHeaders(allowHeaders); - return this; - } - - @Override - public Builder exposeHeaders(String... exposeHeaders) { - crossOriginConfigBuilder.exposeHeaders(exposeHeaders);; - return this; - } - - @Override - public Builder allowMethods(String... allowMethods) { - crossOriginConfigBuilder.allowMethods(allowMethods);; - return this; - } - - @Override - public Builder allowCredentials(boolean allowCredentials) { - crossOriginConfigBuilder.allowCredentials(allowCredentials);; - return this; - } - - @Override - public Builder maxAge(long maxAge) { - crossOriginConfigBuilder.maxAge(maxAge);; - return this; - } - } - - @Override - public void accept(ServerRequest request, ServerResponse response) { - RequestAdapter requestAdapter = new SERequestAdapter(request); - ResponseAdapter responseAdapter = new SEResponseAdapter(response); - - Optional responseOpt = processRequest( - crossOriginConfig, - requestAdapter, - responseAdapter); - - responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, response)); - } - - private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ServerResponse response) { - prepareResponse( - crossOriginConfig, - requestAdapter, - new SEResponseAdapter(response)); - - requestAdapter.request().next(); - } - - static class SERequestAdapter implements RequestAdapter { - - private final ServerRequest request; - - SERequestAdapter(ServerRequest request) { - this.request = request; - } - - @Override - public String path() { - return request.path().toString(); - } - - @Override - public Optional firstHeader(String key) { - return request.headers().first(key); - } - - @Override - public boolean headerContainsKey(String key) { - return firstHeader(key).isPresent(); - } - - @Override - public List allHeaders(String key) { - return request.headers().all(key); - } - - @Override - public String method() { - return request.method().name(); - } - - @Override - public ServerRequest request() { - return request; - } - } - - static class SEResponseAdapter implements ResponseAdapter { - - private final ServerResponse serverResponse; - - SEResponseAdapter(ServerResponse serverResponse) { - this.serverResponse = serverResponse; - } - - @Override - public ResponseAdapter header(String key, String value) { - serverResponse.headers().add(key, value); - return this; - } - - @Override - public ResponseAdapter header(String key, Object value) { - serverResponse.headers().add(key, value.toString()); - return this; - } - - @Override - public ServerResponse forbidden(String message) { - serverResponse.status(Http.ResponseStatus.create(Http.Status.FORBIDDEN_403.code(), message)); - return serverResponse; - } - - @Override - public ServerResponse ok() { - serverResponse.status(Http.Status.OK_200.code()); - return serverResponse; - } - } -} diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java index 50e30b2b2fb..37b160dd627 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java +++ b/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java @@ -86,106 +86,6 @@ public enum RequestType { HelidonFeatures.register(HelidonFlavor.SE, "CORS"); } - /** - * Minimal abstraction of an HTTP request. - * - * @param type of the request wrapped by the adapter - */ - public interface RequestAdapter { - - /** - * - * @return possibly unnormalized path from the request - */ - String path(); - - /** - * Retrieves the first value for the specified header as a String. - * - * @param key header name to retrieve - * @return the first header value for the key - */ - Optional firstHeader(String key); - - /** - * Reports whether the specified header exists. - * - * @param key header name to check for - * @return whether the header exists among the request's headers - */ - boolean headerContainsKey(String key); - - /** - * Retrieves all header values for a given key as Strings. - * - * @param key header name to retrieve - * @return header values for the header; empty list if none - */ - List allHeaders(String key); - - /** - * Reports the method name for the request. - * - * @return the method name - */ - String method(); - - /** - * Returns the request this adapter wraps. - * - * @return the request - */ - T request(); - } - - /** - * Minimal abstraction of an HTTP response. - * - *

- * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} - * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the - * actual response. - *

- * - * @param the type of the response wrapped by the adapter - */ - public interface ResponseAdapter { - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, String value); - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, Object value); - - /** - * Returns a response with the forbidden status and the specified error message, without any headers assigned - * using the {@code header} methods. - * - * @param message error message to use in setting the response status - * @return the factory - */ - T forbidden(String message); - - /** - * Returns a response with only the headers that were set on this adapter and the status set to OK. - * - * @return response instance - */ - T ok(); - } - /** * Processes a request according to the CORS rules, returning an {@code Optional} of the response type if * the caller should send the response immediately (such as for a preflight response or an error response to a @@ -281,7 +181,7 @@ static Optional processRequest(RequestType requestType, CrossOriginCon /* * There has been no rejection of the CORS settings, so prep the response headers. */ - prepareCORSResponse(crossOrigin, requestAdapter, responseAdapter); + addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); } return corsResponse; @@ -308,20 +208,29 @@ public static void prepareResponse(Map crossOr RequestType requestType = requestType(requestAdapter); if (requestType == RequestType.CORS) { - CrossOriginConfig crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup) + CrossOriginConfig crossOrigin = lookupCrossOrigin( + requestAdapter.path(), + crossOriginConfigs, + secondaryCrossOriginLookup) .orElseThrow(() -> new IllegalArgumentException( "Could not locate expected CORS information while preparing response to request " + requestAdapter)); - prepareCORSResponse( - crossOrigin, - requestAdapter, - responseAdapter); + addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); } } + /** + * Prepares a response with CORS headers, if the supplied request is in fact a CORS request. + * + * @param crossOrigin the CORS settings to apply to this request + * @param requestAdapter abstraction of a request + * @param responseAdapter abstraction of a response + * @param type for the {@code Request} managed by the requestAdapter + * @param the type for the HTTP response as returned from the responseSetter + */ public static void prepareResponse(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - prepareCORSResponse(crossOrigin, requestAdapter, responseAdapter); + addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); } /** @@ -353,6 +262,7 @@ static RequestType requestType(RequestAdapter requestAdapter) { * Validates information about an incoming request as a CORS request and, if anything is wrong with CORS information, * returns an {@code Optional} error response reporting the problem. * + * @param crossOriginConfig the CORS settings to apply to this request * @param requestAdapter abstraction of a request * @param responseAdapter abstraction of a response * @param type for the request wrapped by the requestAdapter @@ -379,12 +289,13 @@ static Optional processCORSRequest( /** * Prepares a CORS response by updating the response's headers. * + * @param crossOrigin the CORS settings to apply to the response * @param requestAdapter request adapter * @param responseAdapter response adapter * @param type for the request wrapped by the requestAdapter * @param type for the response wrapper by the responseAdapter */ - static void prepareCORSResponse(CrossOriginConfig crossOrigin, + static void addCORSHeadersToResponse(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { // Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials. @@ -415,6 +326,7 @@ static void prepareCORSResponse(CrossOriginConfig crossOrigin, * Having determined that we have a pre-flight request, we will always return either a forbidden or a successful response. *

* + * @param crossOrigin the CORS settings to apply to this request * @param requestAdapter the request adapter * @param responseAdapter the response adapter * @param type for the request wrapped by the requestAdapter @@ -558,7 +470,7 @@ static String normalize(String path) { int length = path.length(); int beginIndex = path.charAt(0) == '/' ? 1 : 0; int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; - return (endIndex<= beginIndex) ? "" : path.substring(beginIndex, endIndex); + return (endIndex <= beginIndex) ? "" : path.substring(beginIndex, endIndex); } /** diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginService.java b/cors/src/main/java/io/helidon/cors/CrossOriginService.java deleted file mode 100644 index ca0e430ea38..00000000000 --- a/cors/src/main/java/io/helidon/cors/CrossOriginService.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * 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 io.helidon.cors; - -import io.helidon.config.Config; -import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; -import io.helidon.cors.CrossOriginHandler.SERequestAdapter; -import io.helidon.cors.CrossOriginHandler.SEResponseAdapter; -import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter; -import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter; -import io.helidon.webserver.Routing; -import io.helidon.webserver.ServerRequest; -import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.Service; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelperInternal.normalize; -import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; -import static io.helidon.cors.CrossOriginHelperInternal.processRequest; - -/** - * A Helidon service implementation that implements CORS for endpoints in the application or in built-in Helidon services such - * as OpenAPI and metrics. - *

- * The caller can set up the {@code CrossOriginService} in a combination of these ways: - *

    - *
  • from the {@value CrossOriginConfig#CORS_CONFIG_KEY} node in the application's default config,
  • - *
  • from a {@link Config} node supplied programmatically, and
  • - *
  • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which - * it applies.
  • - *
- * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. - *

- *

- * If none of these sources is used, the {@code CrossOriginService} applies defaults as described for - * {@link CrossOriginConfig}. - *

- */ -public class CrossOriginService implements Service { - - private final Map crossOriginConfigs; - - private CrossOriginService(Builder builder) { - crossOriginConfigs = builder.crossOriginConfigs(); - } - - /** - * Creates a {@code CrossOriginService} which supports the default CORS set-up. - * - * @return the service - */ - public static CrossOriginService create() { - return builder().build(); - } - - /** - * Returns a {@code CrossOriginService} set up using the supplied {@link Config} node. - * - * @param config the config node containing CORS information - * @return the initialized service - */ - public static CrossOriginService fromConfig(Config config) { - return builder().config(config).build(); - } - - /** - * Creates a {@code CrossOriginService} set up using the {@value CrossOriginConfig#CORS_CONFIG_KEY} node in the - * application's default config. - * - * @return the initialized service - */ - public static CrossOriginService fromConfig() { - return fromConfig(Config.create().get(CORS_CONFIG_KEY)); - } - - /** - * Creates a {@code Builder} for assembling a {@code CrossOriginService}. - * - * @return the builder - */ - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(Config config) { - return builder().config(config); - } - - @Override - public void update(Routing.Rules rules) { - if (!crossOriginConfigs.isEmpty()) { - rules.any(this::accept); - } - } - -// @Override - public void accept(ServerRequest request, ServerResponse response) { - RequestAdapter requestAdapter = new SERequestAdapter(request); - ResponseAdapter responseAdapter = new SEResponseAdapter(response); - - Optional responseOpt = processRequest(crossOriginConfigs, - Optional::empty, - requestAdapter, - responseAdapter); - - responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, response)); - } - - private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ServerResponse response) { - prepareResponse( - crossOriginConfigs, - Optional::empty, - requestAdapter, - new SEResponseAdapter(response)); - - requestAdapter.request().next(); - } - - private static String ensureLeadingSlash(String path) { - return "/" + normalize(path); - } - - /** - * Builder for {@code CrossOriginService} instances. - */ - public static class Builder implements io.helidon.common.Builder { - - private final Map crossOriginConfigs = new HashMap<>(); - - private Config corsConfig = Config.empty(); - private Optional crossOriginConfigBuilder = Optional.empty(); // CrossOriginConfig.builder(); - - @Override - public CrossOriginService build() { - return new CrossOriginService(this); - } - - /** - * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the - * provided {@code Config} instance from its own config using the key {@value CrossOriginConfig#CORS_CONFIG_KEY}. - * - * @param config the CORS config - * @return the updated builder - */ - public Builder config(Config config) { - this.corsConfig = config; - return this; - } - - /** - * Initializes the builder's CORS config from the {@value CrossOriginConfig#CORS_CONFIG_KEY} node from the default - * application config. - * - * @return the updated builder - */ - public Builder config() { - corsConfig = Config.create().get(CORS_CONFIG_KEY); - return this; - } - - /** - * Returns the aggregation of CORS-related information supplied to the builder, constructed in this order (in case of - * conflicts, last wins): - *
    - *
  1. from {@code Config} supplied using {@link #config(Config)}or inferred using {@link #config()}, then
  2. - *
  3. from {@code CrossOriginConfig} instances added using {@link #addCrossOrigin(String, CrossOriginConfig)}.
  4. - *
- * - * @return list of CrossOriginConfig instances, each describing a path and its associated constraints or permissions - */ - Map crossOriginConfigs() { - Map result = corsConfig - .as(new CrossOriginConfigMapper()) - .get(); - result.putAll(crossOriginConfigs); - return result; - } - - /** - * Adds cross origin information associated with a given path. - * - * @param path the path to which the cross origin information applies - * @param crossOrigin the cross origin information - * @return updated builder - */ - public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - crossOriginConfigs.put(normalize(path), crossOrigin); - return this; - } - } -} diff --git a/cors/src/main/java/io/helidon/cors/RequestAdapter.java b/cors/src/main/java/io/helidon/cors/RequestAdapter.java new file mode 100644 index 00000000000..65a7a43ffa3 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/RequestAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import java.util.List; +import java.util.Optional; + +/** + * Minimal abstraction of an HTTP request. + * + * @param type of the request wrapped by the adapter + */ +public interface RequestAdapter { + + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + + /** + * Returns the request this adapter wraps. + * + * @return the request + */ + T request(); +} diff --git a/cors/src/main/java/io/helidon/cors/ResponseAdapter.java b/cors/src/main/java/io/helidon/cors/ResponseAdapter.java new file mode 100644 index 00000000000..86356707bb0 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/ResponseAdapter.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +/** + * Minimal abstraction of an HTTP response. + * + *

+ * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

+ * + * @param the type of the response wrapped by the adapter + */ +public interface ResponseAdapter { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, Object value); + + /** + * Returns a response with the forbidden status and the specified error message, without any headers assigned + * using the {@code header} methods. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns a response with only the headers that were set on this adapter and the status set to OK. + * + * @return response instance + */ + T ok(); +} diff --git a/cors/src/main/java/io/helidon/cors/SERequestAdapter.java b/cors/src/main/java/io/helidon/cors/SERequestAdapter.java new file mode 100644 index 00000000000..b32c286dca6 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/SERequestAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import java.util.List; +import java.util.Optional; + +import io.helidon.webserver.ServerRequest; + +/** + * Helidon SE implementation of {@link RequestAdapter}. + */ +class SERequestAdapter implements RequestAdapter { + + private final ServerRequest request; + + SERequestAdapter(ServerRequest request) { + this.request = request; + } + + @Override + public String path() { + return request.path().toString(); + } + + @Override + public Optional firstHeader(String key) { + return request.headers().first(key); + } + + @Override + public boolean headerContainsKey(String key) { + return firstHeader(key).isPresent(); + } + + @Override + public List allHeaders(String key) { + return request.headers().all(key); + } + + @Override + public String method() { + return request.method().name(); + } + + @Override + public ServerRequest request() { + return request; + } +} diff --git a/cors/src/main/java/io/helidon/cors/SEResponseAdapter.java b/cors/src/main/java/io/helidon/cors/SEResponseAdapter.java new file mode 100644 index 00000000000..6cab06eaa84 --- /dev/null +++ b/cors/src/main/java/io/helidon/cors/SEResponseAdapter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.cors; + +import io.helidon.common.http.Http; +import io.helidon.webserver.ServerResponse; + +/** + * SE implementation of {@link ResponseAdapter}. + */ +class SEResponseAdapter implements ResponseAdapter { + + private final ServerResponse serverResponse; + + SEResponseAdapter(ServerResponse serverResponse) { + this.serverResponse = serverResponse; + } + + @Override + public ResponseAdapter header(String key, String value) { + serverResponse.headers().add(key, value); + return this; + } + + @Override + public ResponseAdapter header(String key, Object value) { + serverResponse.headers().add(key, value.toString()); + return this; + } + + @Override + public ServerResponse forbidden(String message) { + serverResponse.status(Http.ResponseStatus.create(Http.Status.FORBIDDEN_403.code(), message)); + return serverResponse; + } + + @Override + public ServerResponse ok() { + serverResponse.status(Http.Status.OK_200.code()); + return serverResponse; + } +} diff --git a/cors/src/main/java/io/helidon/cors/package-info.java b/cors/src/main/java/io/helidon/cors/package-info.java index 7055dfcb199..a90abb7e405 100644 --- a/cors/src/main/java/io/helidon/cors/package-info.java +++ b/cors/src/main/java/io/helidon/cors/package-info.java @@ -32,7 +32,7 @@ * or more of these approaches: *
    *
  • using configuration - *

    Often you would add a {@value io.helidon.cors.CrossOriginConfig#CORS_CONFIG_KEY} section to your application's + *

    Often you would add a {@value io.helidon.cors.CORSSupport#CORS_CONFIG_KEY} section to your application's * default configuration file, like this: *

      *     cors:
    diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java
    index 8fe90c4d006..d4318aac50f 100644
    --- a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java
    +++ b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2020 Oracle and/or its affiliates.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/cors/src/test/java/io/helidon/cors/TestUtil.java
    index 15d1311e10e..c541dde625f 100644
    --- a/cors/src/test/java/io/helidon/cors/TestUtil.java
    +++ b/cors/src/test/java/io/helidon/cors/TestUtil.java
    @@ -31,7 +31,7 @@
     import io.helidon.webserver.WebServer;
     
     import static io.helidon.cors.CORSTestServices.SERVICE_3;
    -import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY;
    +import static io.helidon.cors.CORSSupport.CORS_CONFIG_KEY;
     
     public class TestUtil {
     
    @@ -61,22 +61,22 @@ static Routing.Builder prepRouting() {
             /*
              * Use the default config for the service at "/greet" and then programmatically add the config for /cors3.
              */
    -        CrossOriginService.Builder corsSupportBuilder = CrossOriginService.builder();
    +        CORSSupport.Builder corsSupportBuilder = CORSSupport.builder();
             corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC);
     
             /*
              * Load a specific config for "/othergreet."
              */
             Config twoCORSConfig = minimalConfig(ConfigSources.classpath("twoCORS.yaml"));
    -        CrossOriginService.Builder twoCORSSupportBuilder =
    -                CrossOriginService.builder().config(twoCORSConfig.get(CORS_CONFIG_KEY));
    +        CORSSupport.Builder twoCORSSupportBuilder =
    +                CORSSupport.builder().config(twoCORSConfig.get(CORS_CONFIG_KEY));
     
             Routing.Builder builder = Routing.builder()
                     .register(GREETING_PATH,
    -                          CrossOriginService.fromConfig(), // use "cors" from default app config
    +                          CORSSupport.fromConfig(), // use "cors" from default app config
                               new GreetService())
                     .register(OTHER_GREETING_PATH,
    -                          CrossOriginService.fromConfig(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself
    +                          CORSSupport.fromConfig(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself
                               new GreetService("Other Hello"));
     
             return builder;
    
    From 3021fde539e2b4919a26b4bf5c5a80204a98c8a5 Mon Sep 17 00:00:00 2001
    From: "tim.quinn@oracle.com" 
    Date: Thu, 9 Apr 2020 07:46:33 -0500
    Subject: [PATCH 057/100] Adapt to refactoring of req and resp adapters
    
    ---
     .../io/helidon/microprofile/cors/CrossOriginFilter.java     | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
    index afac146efd0..788faa68966 100644
    --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
    +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
    @@ -42,13 +42,13 @@
     import io.helidon.config.Config;
     import io.helidon.cors.CrossOriginConfig;
     import io.helidon.cors.CrossOriginHelperInternal;
    -import io.helidon.cors.CrossOriginHelperInternal.RequestAdapter;
    -import io.helidon.cors.CrossOriginHelperInternal.ResponseAdapter;
    +import io.helidon.cors.RequestAdapter;
    +import io.helidon.cors.ResponseAdapter;
     
     import org.eclipse.microprofile.config.ConfigProvider;
     
     import static io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper;
    -import static io.helidon.cors.CrossOriginConfig.CORS_CONFIG_KEY;
    +import static io.helidon.cors.CORSSupport.CORS_CONFIG_KEY;
     import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse;
     
     /**
    
    From ee61a31369c6406fa1c0561e74f31a4ab022403d Mon Sep 17 00:00:00 2001
    From: "tim.quinn@oracle.com" 
    Date: Thu, 9 Apr 2020 08:05:16 -0500
    Subject: [PATCH 058/100] Javadoc improvements
    
    ---
     cors/src/main/java/io/helidon/cors/CORSSupport.java      | 2 ++
     cors/src/main/java/io/helidon/cors/package-info.java     | 3 ++-
     cors/src/test/java/io/helidon/cors/CORSTest.java         | 9 +++++++--
     .../test/java/io/helidon/cors/TestTwoCORSConfigs.java    | 7 ++++++-
     4 files changed, 17 insertions(+), 4 deletions(-)
    
    diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/cors/src/main/java/io/helidon/cors/CORSSupport.java
    index 7aeab65182f..2f8d1afdfb3 100644
    --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java
    +++ b/cors/src/main/java/io/helidon/cors/CORSSupport.java
    @@ -37,6 +37,7 @@
      * services (such as OpenAPI and metrics).
      * 

    * The caller can set up the {@code CORSSupport} in a combination of these ways: + *

    *
      *
    • from the {@value CORSSupport#CORS_CONFIG_KEY} node in the application's default config,
    • *
    • from a {@link Config} node supplied programmatically,
    • @@ -45,6 +46,7 @@ *
    • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the * "/" path).
    • *
    + *

    * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. *

    *

    diff --git a/cors/src/main/java/io/helidon/cors/package-info.java b/cors/src/main/java/io/helidon/cors/package-info.java index a90abb7e405..215ba909f20 100644 --- a/cors/src/main/java/io/helidon/cors/package-info.java +++ b/cors/src/main/java/io/helidon/cors/package-info.java @@ -18,7 +18,8 @@ /** * Helidon SE CORS Support. *

    - * Use {@link io.helidon.cors.CORSSupport} and its {@code Builder} to add CORS handling to resources in your application. + * Use {@link io.helidon.cors.CORSSupport} and its {@link io.helidon.cors.CORSSupport.Builder} to add CORS handling to resources + * in your application. *

    * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for * your application another way, in three steps: diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/cors/src/test/java/io/helidon/cors/CORSTest.java index a69f74835e1..335370794de 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/cors/src/test/java/io/helidon/cors/CORSTest.java @@ -47,17 +47,22 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class CORSTest extends AbstractCORSTest { private static final String CONTEXT_ROOT = "/greet"; private static WebServer server; - private static WebClient client; + private WebClient client; @BeforeAll public static void startup() throws InterruptedException, ExecutionException, TimeoutException { server = TestUtil.startupServerWithApps(); + } + + @BeforeEach + public void startupClient() { client = TestUtil.startupClient(server); } @@ -74,7 +79,7 @@ String fooOrigin() { @Override WebClient client() { - return CORSTest.client; + return client; } @Override diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java index d4318aac50f..9a11284ad99 100644 --- a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java +++ b/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -34,11 +35,15 @@ public class TestTwoCORSConfigs extends AbstractCORSTest { private static WebServer server; - private static WebClient client; + private WebClient client; @BeforeAll public static void startup() throws InterruptedException, ExecutionException, TimeoutException { server = TestUtil.startupServerWithApps(); + } + + @BeforeEach + public void startupClient() { client = TestUtil.startupClient(server); } From 297602aa2a75cc509817ceec1384724974890eec Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 13:34:55 -0500 Subject: [PATCH 059/100] Refactor SE CORS under webserver. Work on package-info JavaDoc a bit --- bom/pom.xml | 7 +- .../java/io/helidon/cors/RequestAdapter.java | 72 ------- .../java/io/helidon/cors/ResponseAdapter.java | 65 ------- .../java/io/helidon/cors/package-info.java | 88 --------- pom.xml | 2 +- {cors => webserver/cors}/pom.xml | 13 +- .../helidon/webserver}/cors/CORSSupport.java | 22 +-- .../webserver}/cors/CrossOriginConfig.java | 16 +- .../cors/CrossOriginHelperInternal.java | 134 +++++++++++-- .../webserver}/cors/SERequestAdapter.java | 6 +- .../webserver}/cors/SEResponseAdapter.java | 10 +- .../helidon/webserver/cors/package-info.java | 180 ++++++++++++++++++ .../cors}/src/main/java/module-info.java | 4 +- .../webserver}/cors/AbstractCORSTest.java | 26 +-- .../cors/AbstractCORSTestService.java | 2 +- .../io/helidon/webserver}/cors/CORSTest.java | 27 +-- .../webserver}/cors/CORSTestServices.java | 2 +- .../webserver}/cors/CustomMatchers.java | 2 +- .../helidon/webserver}/cors/GreetService.java | 3 +- .../webserver}/cors/TestTwoCORSConfigs.java | 2 +- .../io/helidon/webserver}/cors/TestUtil.java | 10 +- .../cors}/src/test/resources/application.yaml | 0 .../cors}/src/test/resources/twoCORS.yaml | 0 webserver/pom.xml | 1 + 24 files changed, 370 insertions(+), 324 deletions(-) delete mode 100644 cors/src/main/java/io/helidon/cors/RequestAdapter.java delete mode 100644 cors/src/main/java/io/helidon/cors/ResponseAdapter.java delete mode 100644 cors/src/main/java/io/helidon/cors/package-info.java rename {cors => webserver/cors}/pom.xml (89%) rename {cors/src/main/java/io/helidon => webserver/cors/src/main/java/io/helidon/webserver}/cors/CORSSupport.java (92%) rename {cors/src/main/java/io/helidon => webserver/cors/src/main/java/io/helidon/webserver}/cors/CrossOriginConfig.java (96%) rename {cors/src/main/java/io/helidon => webserver/cors/src/main/java/io/helidon/webserver}/cors/CrossOriginHelperInternal.java (84%) rename {cors/src/main/java/io/helidon => webserver/cors/src/main/java/io/helidon/webserver}/cors/SERequestAdapter.java (87%) rename {cors/src/main/java/io/helidon => webserver/cors/src/main/java/io/helidon/webserver}/cors/SEResponseAdapter.java (77%) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java rename {cors => webserver/cors}/src/main/java/module-info.java (91%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/AbstractCORSTest.java (92%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/AbstractCORSTestService.java (97%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/CORSTest.java (70%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/CORSTestServices.java (98%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/CustomMatchers.java (98%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/GreetService.java (96%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/TestTwoCORSConfigs.java (98%) rename {cors/src/test/java/io/helidon => webserver/cors/src/test/java/io/helidon/webserver}/cors/TestUtil.java (94%) rename {cors => webserver/cors}/src/test/resources/application.yaml (100%) rename {cors => webserver/cors}/src/test/resources/twoCORS.yaml (100%) diff --git a/bom/pom.xml b/bom/pom.xml index 350f653f8c9..7c2b835648b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -68,6 +68,11 @@ helidon-webserver-tyrus ${helidon.version} + + io.helidon.webserver + helidon-webserver-cors + ${helidon.version}} + io.helidon.jersey @@ -803,7 +808,7 @@ - io.helidon.cors + io.helidon.webserver.cors helidon-cors ${helidon.version} diff --git a/cors/src/main/java/io/helidon/cors/RequestAdapter.java b/cors/src/main/java/io/helidon/cors/RequestAdapter.java deleted file mode 100644 index 65a7a43ffa3..00000000000 --- a/cors/src/main/java/io/helidon/cors/RequestAdapter.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.cors; - -import java.util.List; -import java.util.Optional; - -/** - * Minimal abstraction of an HTTP request. - * - * @param type of the request wrapped by the adapter - */ -public interface RequestAdapter { - - /** - * - * @return possibly unnormalized path from the request - */ - String path(); - - /** - * Retrieves the first value for the specified header as a String. - * - * @param key header name to retrieve - * @return the first header value for the key - */ - Optional firstHeader(String key); - - /** - * Reports whether the specified header exists. - * - * @param key header name to check for - * @return whether the header exists among the request's headers - */ - boolean headerContainsKey(String key); - - /** - * Retrieves all header values for a given key as Strings. - * - * @param key header name to retrieve - * @return header values for the header; empty list if none - */ - List allHeaders(String key); - - /** - * Reports the method name for the request. - * - * @return the method name - */ - String method(); - - /** - * Returns the request this adapter wraps. - * - * @return the request - */ - T request(); -} diff --git a/cors/src/main/java/io/helidon/cors/ResponseAdapter.java b/cors/src/main/java/io/helidon/cors/ResponseAdapter.java deleted file mode 100644 index 86356707bb0..00000000000 --- a/cors/src/main/java/io/helidon/cors/ResponseAdapter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.cors; - -/** - * Minimal abstraction of an HTTP response. - * - *

    - * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} - * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the - * actual response. - *

    - * - * @param the type of the response wrapped by the adapter - */ -public interface ResponseAdapter { - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, String value); - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, Object value); - - /** - * Returns a response with the forbidden status and the specified error message, without any headers assigned - * using the {@code header} methods. - * - * @param message error message to use in setting the response status - * @return the factory - */ - T forbidden(String message); - - /** - * Returns a response with only the headers that were set on this adapter and the status set to OK. - * - * @return response instance - */ - T ok(); -} diff --git a/cors/src/main/java/io/helidon/cors/package-info.java b/cors/src/main/java/io/helidon/cors/package-info.java deleted file mode 100644 index 215ba909f20..00000000000 --- a/cors/src/main/java/io/helidon/cors/package-info.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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. - * - */ - -/** - * Helidon SE CORS Support. - *

    - * Use {@link io.helidon.cors.CORSSupport} and its {@link io.helidon.cors.CORSSupport.Builder} to add CORS handling to resources - * in your application. - *

    - * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for - * your application another way, in three steps: - *

      - *
    1. Create an instance of {@code CORSSupport.Builder} for your Helidon service (your application): - *
      - *     CORSSupport.Builder corsBuilder = CORSSupport.builder();
      - * 
      - *
    2. - *
    3. Next, give the builder information about how to set up CORS for some or all of the resources in your app. You can use one - * or more of these approaches: - *
        - *
      • using configuration - *

        Often you would add a {@value io.helidon.cors.CORSSupport#CORS_CONFIG_KEY} section to your application's - * default configuration file, like this: - *

        - *     cors:
        - *       - path-prefix: /cors1
        - *         allow-origins: ["*"]
        - *         allow-methods: ["*"]
        - *       - path-prefix: /cors2
        - *         allow-origins: ["http://foo.bar", "http://bar.foo"]
        - *         allow-methods: ["DELETE", "PUT"]
        - *         allow-headers: ["X-bar", "X-foo"]
        - *         allow-credentials: true
        - *         max-age: -1
        - *     
        - * and add code similar to this to retrieve it and use it: - *
        - *         Config corsConfig = Config.create().get(CrossOriginConfig.CORS_CONFIG_KEY);
        - *         corsBuilder.config(corsConfig);
        - *     
        - *
      • using the {@link io.helidon.cors.CrossOriginConfig} class - *

        Your code can create {@code CrossOriginConfig} instances and make them known to a {@code CORSSupport.Builder} - * using the {@link io.helidon.cors.CORSSupport.Builder#addCrossOrigin(java.lang.String, io.helidon.cors.CrossOriginConfig)} - * method. The {@code String} argument is the path within your application's context root to which this CORS - * set-up should apply. The following example has the same effect as the {@code /cors2} section from the config example above: - *

        - *
        - *         CrossOriginConfig cors2Setup = CrossOriginConfig.Builder.create()
        - *                 .allowOrigins("http://foo.bar", "http://bar.foo")
        - *                 .allowMethods("DELETE", "PUT")
        - *                 .allowHeaders("X-bar", "X-foo")
        - *                 .allowCredentials(true),
        - *                 .minAge(-1)
        - *                 .build();
        - *         corsBuilder().addCrossOrigin("/cors2", cors2Setup);
        - *     
        - *
      • - *
      - *
    4. Finally, create and register the {@code CORSSupport} instance on the same path as and - * before your own resources. The following code uses the {@code CORSSupport.Builder} from the earlier examples and - * registers it and your actual service on the same path with Helidon: - *
      - *     Routing.Builder builder = Routing.builder()
      - *                 .register("/myapp", corsBuilder.build(), new MyApp());
      - * 
      - *
    5. - *
    - * - *

    - * Note that {@code CrossOriginHelperInternal}, while {@code public}, is not intended for use by developers. It is - * reserved for internal Helidon use and might change at any time. - *

    - */ -package io.helidon.cors; diff --git a/pom.xml b/pom.xml index 41723d4a5d9..273906282e2 100644 --- a/pom.xml +++ b/pom.xml @@ -175,7 +175,6 @@ webclient integrations dbclient - cors @@ -978,6 +977,7 @@ helidon-parent,helidon-dependencies,helidon-bom,helidon-se,helidon-mp,io.grpc,he **/test/**/*.java **/*_.java **/io/grpc/stub/**/*.java + **/io/helidon/webserver/cors/CrossOriginHelperInternal.java diff --git a/cors/pom.xml b/webserver/cors/pom.xml similarity index 89% rename from cors/pom.xml rename to webserver/cors/pom.xml index d5e8c1e717b..53f7ef1df57 100644 --- a/cors/pom.xml +++ b/webserver/cors/pom.xml @@ -19,15 +19,15 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.helidon - helidon-project + io.helidon.webserver + helidon-webserver-project 2.0.0-SNAPSHOT - io.helidon.cors - helidon-cors + io.helidon.webserver.cors + helidon-webserver-cors - Helidon CORS + Helidon Webserver CORS Helidon CORS implementation @@ -35,9 +35,6 @@ jar - - - io.helidon.webserver diff --git a/cors/src/main/java/io/helidon/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java similarity index 92% rename from cors/src/main/java/io/helidon/cors/CORSSupport.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 2f8d1afdfb3..209f7bb5d4f 100644 --- a/cors/src/main/java/io/helidon/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -14,23 +14,24 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.HashMap; import java.util.Map; import java.util.Optional; import io.helidon.config.Config; -import io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; import io.helidon.webserver.Handler; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import io.helidon.webserver.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.webserver.cors.CrossOriginHelperInternal.ResponseAdapter; -import static io.helidon.cors.CrossOriginHelperInternal.normalize; -import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; -import static io.helidon.cors.CrossOriginHelperInternal.processRequest; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.normalize; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.processRequest; /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon @@ -154,7 +155,8 @@ public static class Builder implements io.helidon.common.Builder, C private final Map crossOriginConfigs = new HashMap<>(); - private Config corsConfig = Config.empty(); + private final Map crossOriginConfigsAssembledFromConfigs = new HashMap<>(); + private Optional crossOriginConfigBuilderOpt = Optional.empty(); @Override @@ -170,7 +172,7 @@ public CORSSupport build() { * @return the updated builder */ public Builder config(Config config) { - this.corsConfig = config; + crossOriginConfigsAssembledFromConfigs.putAll(config.as(new CrossOriginConfig.CrossOriginConfigMapper()).get()); return this; } @@ -181,7 +183,7 @@ public Builder config(Config config) { * @return the updated builder */ public Builder config() { - corsConfig = Config.create().get(CORS_CONFIG_KEY); + config(Config.create().get(CORS_CONFIG_KEY)); return this; } @@ -247,9 +249,7 @@ public Builder maxAge(long maxAge) { Map crossOriginConfigs() { final Map result = new HashMap<>(crossOriginConfigs); crossOriginConfigBuilderOpt.ifPresent(opt -> result.put("/", opt.get())); - result.putAll(corsConfig - .as(new CrossOriginConfigMapper()) - .get()); + result.putAll(crossOriginConfigsAssembledFromConfigs); return result; } diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java similarity index 96% rename from cors/src/main/java/io/helidon/cors/CrossOriginConfig.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index b24d7d6abb5..d733492e69c 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.Arrays; import java.util.HashMap; @@ -23,8 +23,8 @@ import io.helidon.config.Config; -import static io.helidon.cors.CrossOriginHelperInternal.normalize; -import static io.helidon.cors.CrossOriginHelperInternal.parseHeader; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.normalize; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.parseHeader; /** * Represents information about cross origin request sharing. @@ -89,7 +89,7 @@ private CrossOriginConfig(Builder builder) { * @return a new builder for cross origin config */ public static Builder builder() { - return Builder.create(); + return new Builder(); } /** @@ -216,14 +216,6 @@ public static class Builder implements Setter, io.helidon.common.Builde private Builder() { } - /** - * - * @return a new {@code CrossOriginConfig.Builder} - */ - public static Builder create() { - return new Builder(); - } - /** * Sets the allowOrigins. * diff --git a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelperInternal.java similarity index 84% rename from cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelperInternal.java index 37b160dd627..0080978e58b 100644 --- a/cors/src/main/java/io/helidon/cors/CrossOriginHelperInternal.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelperInternal.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.Arrays; import java.util.Collection; @@ -34,21 +34,23 @@ import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_EXPOSE_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_EXPOSE_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; /** * Not for use by developers. - *

    This class is reserved for internal Helidon use. Do not use it from your applications. It might change or vanish at any time - * .

    - * Centralizes common logic to both SE and MP CORS support for processing requests and preparing responses. - *

    + * + * Centralizes internal logic common to both SE and MP CORS support for processing requests and preparing responses. + * + *

    This class is reserved for internal Helidon use. Do not use it from your applications. It might change or vanish at + * any time.

    + *

    * To serve both masters, several methods here accept adapters for requests and responses. Both of these are minimal and very * specific to the needs of CORS support. *

    @@ -64,6 +66,8 @@ private CrossOriginHelperInternal() { static final String HEADERS_NOT_IN_ALLOWED_LIST = "CORS headers not in allowed list"; /** + * Not for use by developers. + * * CORS-related classification of HTTP requests. */ public enum RequestType { @@ -525,4 +529,108 @@ private static Supplier noRequiredHeaderExcFactory(Str private static String noRequiredHeader(String header) { return "CORS request does not have required header " + header; } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP request. + * + * @param type of the request wrapped by the adapter + */ + public interface RequestAdapter { + + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + + /** + * Returns the request this adapter wraps. + * + * @return the request + */ + T request(); + } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP response. + * + *

    + * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

    + * + * @param the type of the response wrapped by the adapter + */ + public interface ResponseAdapter { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, Object value); + + /** + * Returns a response with the forbidden status and the specified error message, without any headers assigned + * using the {@code header} methods. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns a response with only the headers that were set on this adapter and the status set to OK. + * + * @return response instance + */ + T ok(); + } } diff --git a/cors/src/main/java/io/helidon/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java similarity index 87% rename from cors/src/main/java/io/helidon/cors/SERequestAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index b32c286dca6..fe2904c0c13 100644 --- a/cors/src/main/java/io/helidon/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.List; import java.util.Optional; @@ -22,9 +22,9 @@ import io.helidon.webserver.ServerRequest; /** - * Helidon SE implementation of {@link RequestAdapter}. + * Helidon SE implementation of {@link CrossOriginHelperInternal.RequestAdapter}. */ -class SERequestAdapter implements RequestAdapter { +class SERequestAdapter implements CrossOriginHelperInternal.RequestAdapter { private final ServerRequest request; diff --git a/cors/src/main/java/io/helidon/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java similarity index 77% rename from cors/src/main/java/io/helidon/cors/SEResponseAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index 6cab06eaa84..f84290960f1 100644 --- a/cors/src/main/java/io/helidon/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -14,15 +14,15 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import io.helidon.common.http.Http; import io.helidon.webserver.ServerResponse; /** - * SE implementation of {@link ResponseAdapter}. + * SE implementation of {@link CrossOriginHelperInternal.ResponseAdapter}. */ -class SEResponseAdapter implements ResponseAdapter { +class SEResponseAdapter implements CrossOriginHelperInternal.ResponseAdapter { private final ServerResponse serverResponse; @@ -31,13 +31,13 @@ class SEResponseAdapter implements ResponseAdapter { } @Override - public ResponseAdapter header(String key, String value) { + public CrossOriginHelperInternal.ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public ResponseAdapter header(String key, Object value) { + public CrossOriginHelperInternal.ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java new file mode 100644 index 00000000000..0aca20ae410 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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. + * + */ + +/** + * Helidon SE CORS Support. + *

    + * Use {@link io.helidon.webserver.cors.CORSSupport} and its {@link io.helidon.webserver.cors.CORSSupport.Builder} to add CORS + * handling to resources in your application. + *

    + * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for + * your application another way. You can use Helidon configuration, the Helidon CORS API, or a combination. + *

    Configuration

    + *

    Using the application default configuration

    + * You can add a {@value io.helidon.webserver.cors.CORSSupport#CORS_CONFIG_KEY} section to your application's default config + * file to define the CORS behavior for your application endpoints. + *
    + *     cors:
    + *       - path-prefix: /cors1
    + *         allow-origins: ["*"]
    + *         allow-methods: ["*"]
    + *       - path-prefix: /cors2
    + *         allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *         allow-methods: ["DELETE", "PUT"]
    + *         allow-headers: ["X-bar", "X-foo"]
    + *         allow-credentials: true
    + *         max-age: -1
    + * 
    + * This defines CORS behavior for two paths, {@code /cors1} and {@code /cors2}, within your application's context root. + *

    + * Assuming you have written your application class {@code MyApp} to extend {@link io.helidon.webserver.Service}, the + * following code applies the CORS configuration above to it: + *

    + *
    + *         Routing.Builder builder = Routing.builder()
    + *                 .register("/myapp", CORSSupport.fromConfig(), new MyApp());
    + *     
    + * Helidon will perform no CORS processing for any paths in your app other than {@code /cors1} and {@code /cors2}. + *

    Using an explicit configuration object

    + * You can create your own Helidon {@link io.helidon.config.Config} object that contains CORS information and use it instead of + * the application default config. + * The config node you create and give to {@code CORSSupport} should not nest the CORS information inside a + * {@value io.helidon.webserver.cors.CORSSupport#CORS_CONFIG_KEY} section. Instead, it would look like this: + *
    + *     - path-prefix: /cors1
    + *       allow-origins: ["*"]
    + *       allow-methods: ["*"]
    + *     - path-prefix: /cors2
    + *       allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *       allow-methods: ["DELETE", "PUT"]
    + *       allow-headers: ["X-bar", "X-foo"]
    + *       allow-credentials: true
    + *       max-age: -1
    + * 
    + *

    + * If the above config were stored in a resource in your app called {@code myAppCORS.yaml} then the following code would + * apply it to your app: + *

    + *
    + *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myAppCORS.yaml")).build();
    + *         Routing.Builder builder = Routing.builder()
    + *                 .register("/myapp", CORSSupport.fromConfig(myAppConfig), new MyApp());
    + *     
    + *

    The Helidon CORS API

    + * You can define your application's CORS behavior programmatically -- together with configuration if you want -- by: + *
      + *
    • creating a {@link io.helidon.webserver.cors.CrossOriginConfig.Builder} instance,
    • + *
    • operating on it to create the CORS set-up you want,
    • + *
    • using the builder's {@code build()} method to create the {@code CrossOriginConfig} instance, and
    • + *
    • using the {@code CORSSupport.Builder} to associate a path with the {@code CrossOriginConfig} object.
    • + *
    + *

    + * The next example shows creating CORS information and associating it with the path {@code /cors3} within the app. + *

    + *         CrossOriginConfig corsForCORS3= CrossOriginConfig.builder()
    + *             .allowOrigins("http://foo.bar", "http://bar.foo")
    + *             .allowMethods("DELETE", "PUT")
    + *             .build();
    + *
    + *         Routing.Builder builder = Routing.builder()
    + *                 .register("/myapp", CORSSupport.builder()
    + *                                 .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app
    + *                                 .build(), new MyApp());
    + * 
    + * Invoke {@code addCrossOrigin} multiple times to link more paths with CORS configuration. You can reuse one {@code + * CrossOriginConfig} object with more than one path if that meets your needs. + *

    + * The following example shows how you can combine configuration and the API to prepare the {@code CORSSupport.Builder} + * which you could then pass to the {@code register} method. To help with readability as things get more complicated, this + * example saves the {@code CORSSupport.Builder} in a variable rather than constructing it in-line when invoking + * {@code register}: + *

    + *
    + *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
    + *                  .config(myAppConfig)
    + *                  .addCrossOrigin("/cors3", corsFORCORS3);
    + *
    + *         Routing.Builder builder = Routing.builder()
    + *                 .register("/myapp", corsBuilder.build(), new MyApp());
    + * 
    + * + *

    Convenience API for the "/" path

    + * Sometimes you might want to prepare just one set of CORS information, for the "/" path. The Helidon CORS API provides a + * short-cut for this. The {@code CORSSupport.Builder} class supports all the mutator methods from {@code CrossOriginConfig} + * such as {@code allowOrigins}, and on {@code CORSSupport.Builder} these methods implicitly affect the "/" path. + * The following code + *
    + *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
    + *             .allowOrigins("http://foo.bar", "http://bar.foo")
    + *             .allowMethods("DELETE", "PUT");
    + * 
    + * has the same effect as this more verbose version: + *
    + *         CrossOriginConfig corsForCORS3= CrossOriginConfig.builder()
    + *             .allowOrigins("http://foo.bar", "http://bar.foo")
    + *             .allowMethods("DELETE", "PUT")
    + *             .build();
    + *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
    + *                 .addCrossOrigin("/", corsForCORS3);
    + * 
    + *

    {@code CORSSupport} as a handler

    + * The previous examples use a {@code CORSSupport} instance as a Helidon {@link io.helidon.webserver.Service} which you can + * register with the routing rules. You can also use a {@code CORSSupport} object as a {@link io.helidon.webserver.Handler} in + * setting up the routing rules for an HTTP method and path. The next example sets up CORS processing for the {@code PUT} + * HTTP method on the {@code /cors4} path within the app. The application code simply accepts the request graciously and + * replies with success: + *
    {@code
    + *         Routing.Builder builder = Routing.builder()
    + *                 .put("/cors4", CORSSupport.builder()
    + *                               .allowOrigins("http://foo.bar", "http://bar.foo")
    + *                               .allowMethods("DELETE", "PUT"),
    + *                      (req, resp) -> resp.status(Http.Status.OK_200));
    + * }
    + * You can do this multiple times and even combine it with service registrations: + *
    {@code
    + *         Routing.Builder builder = Routing.builder()
    + *                 .put("/cors4", CORSSupport.builder()
    + *                                   .allowOrigins("http://foo.bar", "http://bar.foo")
    + *                                   .allowMethods("DELETE", "PUT"),
    + *                      (req, resp) -> resp.status(Http.Status.OK_200))
    + *                 .get("/cors4", CORSSupport.builder()
    + *                                   .allowOrigins("*")
    + *                                   .minAge(-1),
    + *                      (req, resp) -> resp.send("Hello, World!"))
    + *                 .register(CORSSupport.fromConfig());
    + * }
    + *

    Resolving conflicting settings

    + * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupport.Builder} resolves conflicts CORS + * set-up this way: + *
      + *
    • Multiple invocations of {@code CORSSupport.Builder.config} effectively merge the configured values which designate + * different paths into a single, unified configuration. + * The configured values provided by the latest invocation of {@code CORSSupport.Builder.config} will override any + * previously-set configuration values for a given path.
    • + *
    • Multiple uses of the CORS API other than the {@code config} method) for different paths are merged among + * themselves. The last invocation of the non-config API for a given path wins.
    • + *
    • Use of the convenience {@code CrossOriginConfig}-style methods on {@code CORSSupport.Builder} act as non-config + * programmatic settings for the "/" path.
    • + *
    • Configured values override ones set programmatically for a given path.
    • + *
    + *

    Warning about internal classes

    + *

    + * Note that {@code CrossOriginHelperInternal}, while {@code public}, is not intended for use by developers. It is + * reserved for internal Helidon use and might change at any time. + *

    + */ +package io.helidon.webserver.cors; diff --git a/cors/src/main/java/module-info.java b/webserver/cors/src/main/java/module-info.java similarity index 91% rename from cors/src/main/java/module-info.java rename to webserver/cors/src/main/java/module-info.java index abc446bccf1..482b698d841 100644 --- a/cors/src/main/java/module-info.java +++ b/webserver/cors/src/main/java/module-info.java @@ -18,12 +18,12 @@ /** * The Helidon SE CORS module */ -module io.helidon.cors { +module io.helidon.webserver.cors { requires java.logging; requires io.helidon.common; requires io.helidon.config; requires io.helidon.webserver; - exports io.helidon.cors; + exports io.helidon.webserver.cors; } diff --git a/cors/src/test/java/io/helidon/cors/AbstractCORSTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java similarity index 92% rename from cors/src/test/java/io/helidon/cors/AbstractCORSTest.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java index 898f1dd86dd..c411d22951c 100644 --- a/cors/src/test/java/io/helidon/cors/AbstractCORSTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import io.helidon.common.http.Headers; import io.helidon.common.http.Http; @@ -27,18 +27,18 @@ import java.util.concurrent.ExecutionException; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.cors.CORSTestServices.SERVICE_1; -import static io.helidon.cors.CORSTestServices.SERVICE_2; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CustomMatchers.notPresent; -import static io.helidon.cors.CustomMatchers.present; -import static io.helidon.cors.TestUtil.path; +import static io.helidon.webserver.cors.CORSTestServices.SERVICE_1; +import static io.helidon.webserver.cors.CORSTestServices.SERVICE_2; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.webserver.cors.CustomMatchers.notPresent; +import static io.helidon.webserver.cors.CustomMatchers.present; +import static io.helidon.webserver.cors.TestUtil.path; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java similarity index 97% rename from cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java index fe3b1c02abc..ba8e480e80a 100644 --- a/cors/src/test/java/io/helidon/cors/AbstractCORSTestService.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import io.helidon.common.http.Http; import io.helidon.webserver.Routing; diff --git a/cors/src/test/java/io/helidon/cors/CORSTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java similarity index 70% rename from cors/src/test/java/io/helidon/cors/CORSTest.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java index 335370794de..ffe71f2d58a 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java @@ -14,34 +14,23 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import io.helidon.common.http.Headers; import io.helidon.common.http.Http; -import io.helidon.common.http.MediaType; import io.helidon.webclient.WebClient; -import io.helidon.webclient.WebClientRequestBuilder; import io.helidon.webclient.WebClientResponse; import io.helidon.webserver.WebServer; -import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.cors.CORSTestServices.SERVICE_1; -import static io.helidon.cors.CORSTestServices.SERVICE_2; -import static io.helidon.cors.CORSTestServices.SERVICE_3; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.cors.CustomMatchers.notPresent; -import static io.helidon.cors.CustomMatchers.present; - -import static org.hamcrest.CoreMatchers.containsString; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.webserver.cors.CustomMatchers.notPresent; +import static io.helidon.webserver.cors.CustomMatchers.present; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/cors/src/test/java/io/helidon/cors/CORSTestServices.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java similarity index 98% rename from cors/src/test/java/io/helidon/cors/CORSTestServices.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java index 34f0481072f..e24bba1d522 100644 --- a/cors/src/test/java/io/helidon/cors/CORSTestServices.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.List; diff --git a/cors/src/test/java/io/helidon/cors/CustomMatchers.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CustomMatchers.java similarity index 98% rename from cors/src/test/java/io/helidon/cors/CustomMatchers.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/CustomMatchers.java index 0fc02d49952..9b50d8c195a 100644 --- a/cors/src/test/java/io/helidon/cors/CustomMatchers.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CustomMatchers.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import org.hamcrest.Description; import org.hamcrest.Matcher; diff --git a/cors/src/test/java/io/helidon/cors/GreetService.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/GreetService.java similarity index 96% rename from cors/src/test/java/io/helidon/cors/GreetService.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/GreetService.java index 9393ddff605..28c35218a96 100644 --- a/cors/src/test/java/io/helidon/cors/GreetService.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/GreetService.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import io.helidon.common.http.Http; import io.helidon.webserver.Routing; @@ -23,7 +23,6 @@ import io.helidon.webserver.Service; import java.util.Date; -import java.util.stream.Stream; public class GreetService implements Service { diff --git a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java similarity index 98% rename from cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java index 9a11284ad99..c675d10cd04 100644 --- a/cors/src/test/java/io/helidon/cors/TestTwoCORSConfigs.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; diff --git a/cors/src/test/java/io/helidon/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java similarity index 94% rename from cors/src/test/java/io/helidon/cors/TestUtil.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index c541dde625f..18b7d677229 100644 --- a/cors/src/test/java/io/helidon/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.cors; +package io.helidon.webserver.cors; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -24,14 +24,14 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; import io.helidon.config.spi.ConfigSource; -import io.helidon.cors.CORSTestServices.CORSTestService; +import io.helidon.webserver.cors.CORSTestServices.CORSTestService; import io.helidon.webclient.WebClient; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.WebServer; -import static io.helidon.cors.CORSTestServices.SERVICE_3; -import static io.helidon.cors.CORSSupport.CORS_CONFIG_KEY; +import static io.helidon.webserver.cors.CORSTestServices.SERVICE_3; +import static io.helidon.webserver.cors.CORSSupport.CORS_CONFIG_KEY; public class TestUtil { @@ -53,7 +53,7 @@ private static WebServer startServer(int port, Routing.Builder routingBuilder) t } static Routing.Builder prepRouting() { - CrossOriginConfig cors3COC= CrossOriginConfig.Builder.create() + CrossOriginConfig cors3COC= CrossOriginConfig.builder() .allowOrigins("http://foo.bar", "http://bar.foo") .allowMethods("DELETE", "PUT") .build(); diff --git a/cors/src/test/resources/application.yaml b/webserver/cors/src/test/resources/application.yaml similarity index 100% rename from cors/src/test/resources/application.yaml rename to webserver/cors/src/test/resources/application.yaml diff --git a/cors/src/test/resources/twoCORS.yaml b/webserver/cors/src/test/resources/twoCORS.yaml similarity index 100% rename from cors/src/test/resources/twoCORS.yaml rename to webserver/cors/src/test/resources/twoCORS.yaml diff --git a/webserver/pom.xml b/webserver/pom.xml index 95fad1f6084..6bf4990df31 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -37,5 +37,6 @@ test-support access-log tyrus + cors From 7055b7b66a804a2ba8302e64646cb54a6431d922 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 13:48:48 -0500 Subject: [PATCH 060/100] adjust to SE refactoring --- .../helidon/microprofile/cors/CrossOrigin.java | 2 +- .../microprofile/cors/CrossOriginFilter.java | 16 ++++++++-------- microprofile/cors/src/main/java/module-info.java | 2 +- .../microprofile/cors/CrossOriginTest.java | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java index c3908bee218..c8babdc2ac3 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOrigin.java @@ -20,7 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static io.helidon.cors.CrossOriginConfig.DEFAULT_AGE; +import static io.helidon.webserver.cors.CrossOriginConfig.DEFAULT_AGE; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 788faa68966..e9b91aba18f 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -40,16 +40,16 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; -import io.helidon.cors.CrossOriginConfig; -import io.helidon.cors.CrossOriginHelperInternal; -import io.helidon.cors.RequestAdapter; -import io.helidon.cors.ResponseAdapter; +import io.helidon.webserver.cors.CrossOriginConfig; +import io.helidon.webserver.cors.CrossOriginHelperInternal; +import io.helidon.webserver.cors.CrossOriginHelperInternal.RequestAdapter; +import io.helidon.webserver.cors.CrossOriginHelperInternal.ResponseAdapter; import org.eclipse.microprofile.config.ConfigProvider; -import static io.helidon.cors.CrossOriginConfig.CrossOriginConfigMapper; -import static io.helidon.cors.CORSSupport.CORS_CONFIG_KEY; -import static io.helidon.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.webserver.cors.CrossOriginConfig.CrossOriginConfigMapper; +import static io.helidon.webserver.cors.CORSSupport.CORS_CONFIG_KEY; +import static io.helidon.webserver.cors.CrossOriginHelperInternal.prepareResponse; /** * Class CrossOriginFilter. @@ -204,7 +204,7 @@ static Supplier> crossOriginFromAnnotationFinder(Res } private static CrossOriginConfig annotationToConfig(CrossOrigin crossOrigin) { - return CrossOriginConfig.Builder.create() + return CrossOriginConfig.builder() .allowOrigins(crossOrigin.value()) .allowHeaders(crossOrigin.allowHeaders()) .exposeHeaders(crossOrigin.exposeHeaders()) diff --git a/microprofile/cors/src/main/java/module-info.java b/microprofile/cors/src/main/java/module-info.java index adcabbd2179..289d20fe1e3 100644 --- a/microprofile/cors/src/main/java/module-info.java +++ b/microprofile/cors/src/main/java/module-info.java @@ -21,7 +21,7 @@ requires transitive java.ws.rs; requires io.helidon.config; - requires io.helidon.cors; + requires io.helidon.webserver.cors; requires jersey.common; requires microprofile.config.api; diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java index 4d097b54162..4d3d7263277 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/CrossOriginTest.java @@ -39,13 +39,13 @@ import org.junit.jupiter.api.Test; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; -import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; From d80fdc1e039c64f2978642f704e27b45cae5c081 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 14:19:32 -0500 Subject: [PATCH 061/100] Fix up fallout of refactoring SE cors to under webserver --- bom/pom.xml | 2 +- microprofile/cors/pom.xml | 4 ++-- webserver/cors/pom.xml | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 7c2b835648b..5467dd0b9ee 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -71,7 +71,7 @@ io.helidon.webserver helidon-webserver-cors - ${helidon.version}} + ${helidon.version} diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 9abf519ae72..4606d59299d 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -53,8 +53,8 @@ helidon-microprofile-config - io.helidon.cors - helidon-cors + io.helidon.webserver + helidon-webserver-cors io.helidon.microprofile.bundles diff --git a/webserver/cors/pom.xml b/webserver/cors/pom.xml index 53f7ef1df57..5a08fbf5e20 100644 --- a/webserver/cors/pom.xml +++ b/webserver/cors/pom.xml @@ -24,7 +24,6 @@ 2.0.0-SNAPSHOT - io.helidon.webserver.cors helidon-webserver-cors Helidon Webserver CORS From 194a9157abf372ac32fb7d2817b2545663a2d829 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 16:03:12 -0500 Subject: [PATCH 062/100] Refactor CrossOriginHelperInternal to .internal package and rename --- .../CrossOriginHelper.java} | 0 .../webserver/cors/internal/package-info.java | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CrossOriginHelperInternal.java => internal/CrossOriginHelper.java} (100%) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelperInternal.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java similarity index 100% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelperInternal.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java new file mode 100644 index 00000000000..ab03a4a92d1 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors.internal; \ No newline at end of file From d8d6d44a0f86f016cb0f8b92a9d56743c5caef6e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 16:04:00 -0500 Subject: [PATCH 063/100] Refactor CrossOriginHelperInternal to internal package and rename --- bom/pom.xml | 5 ++ .../bundles/helidon-microprofile/pom.xml | 4 ++ .../microprofile/cors/CrossOriginFilter.java | 12 ++-- pom.xml | 1 - .../helidon/webserver/cors/CORSSupport.java | 61 +++++++++++++++++-- .../webserver/cors/CrossOriginConfig.java | 4 +- .../webserver/cors/SERequestAdapter.java | 5 +- .../webserver/cors/SEResponseAdapter.java | 9 +-- .../cors/internal/CrossOriginHelper.java | 61 +++---------------- .../webserver/cors/internal/package-info.java | 5 +- .../helidon/webserver/cors/package-info.java | 2 +- webserver/cors/src/main/java/module-info.java | 1 + 12 files changed, 94 insertions(+), 76 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 5467dd0b9ee..c5696bc8b43 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -166,6 +166,11 @@ helidon-microprofile-grpc-client ${helidon.version} + + io.helidon.microprofile + helidon-microprofile-cors + ${helidon.version} + io.helidon.media diff --git a/microprofile/bundles/helidon-microprofile/pom.xml b/microprofile/bundles/helidon-microprofile/pom.xml index b2e4cb3ffdf..35a96f3008f 100644 --- a/microprofile/bundles/helidon-microprofile/pom.xml +++ b/microprofile/bundles/helidon-microprofile/pom.xml @@ -74,6 +74,10 @@ io.helidon.microprofile.tracing helidon-microprofile-tracing + + io.helidon.microprofile + helidon-microprofile-cors + org.glassfish.jersey.media jersey-media-json-binding diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index e9b91aba18f..a025790015b 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -41,15 +41,15 @@ import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; import io.helidon.webserver.cors.CrossOriginConfig; -import io.helidon.webserver.cors.CrossOriginHelperInternal; -import io.helidon.webserver.cors.CrossOriginHelperInternal.RequestAdapter; -import io.helidon.webserver.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.webserver.cors.internal.CrossOriginHelper; +import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; +import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; import org.eclipse.microprofile.config.ConfigProvider; -import static io.helidon.webserver.cors.CrossOriginConfig.CrossOriginConfigMapper; import static io.helidon.webserver.cors.CORSSupport.CORS_CONFIG_KEY; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.prepareResponse; +import static io.helidon.webserver.cors.CrossOriginConfig.CrossOriginConfigMapper; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.prepareResponse; /** * Class CrossOriginFilter. @@ -73,7 +73,7 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { - Optional response = CrossOriginHelperInternal.processRequest(crossOriginConfigs, + Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, crossOriginFromAnnotationFinder(resourceInfo), new MPRequestAdapter(requestContext), new MPResponseAdapter()); diff --git a/pom.xml b/pom.xml index 273906282e2..f2ed3fbd8b2 100644 --- a/pom.xml +++ b/pom.xml @@ -977,7 +977,6 @@ helidon-parent,helidon-dependencies,helidon-bom,helidon-se,helidon-mp,io.grpc,he **/test/**/*.java **/*_.java **/io/grpc/stub/**/*.java - **/io/helidon/webserver/cors/CrossOriginHelperInternal.java diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 209f7bb5d4f..2db053e65d1 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -16,9 +16,14 @@ */ package io.helidon.webserver.cors; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.StringTokenizer; import io.helidon.config.Config; import io.helidon.webserver.Handler; @@ -26,12 +31,11 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -import io.helidon.webserver.cors.CrossOriginHelperInternal.RequestAdapter; -import io.helidon.webserver.cors.CrossOriginHelperInternal.ResponseAdapter; +import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; +import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.normalize; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.prepareResponse; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.processRequest; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.prepareResponse; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.processRequest; /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon @@ -117,6 +121,53 @@ public static Builder builder(Config config) { return builder().config(config); } + /** + * Trim leading or trailing slashes of a path. + * + * @param path The path. + * @return Normalized path. + */ + public static String normalize(String path) { + int length = path.length(); + int beginIndex = path.charAt(0) == '/' ? 1 : 0; + int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; + return (endIndex <= beginIndex) ? "" : path.substring(beginIndex, endIndex); + } + + /** + * Parse list header value as a set. + * + * @param header Header value as a list. + * @return Set of header values. + */ + public static Set parseHeader(String header) { + if (header == null) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + StringTokenizer tokenizer = new StringTokenizer(header, ","); + while (tokenizer.hasMoreTokens()) { + String value = tokenizer.nextToken().trim(); + if (value.length() > 0) { + result.add(value); + } + } + return result; + } + + /** + * Parse a list of list of headers as a set. + * + * @param headers Header value as a list, each a potential list. + * @return Set of header values. + */ + public static Set parseHeader(List headers) { + if (headers == null) { + return Collections.emptySet(); + } + return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); + } + @Override public void update(Routing.Rules rules) { if (!crossOriginConfigs.isEmpty()) { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index d733492e69c..29198de626d 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -23,8 +23,8 @@ import io.helidon.config.Config; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.normalize; -import static io.helidon.webserver.cors.CrossOriginHelperInternal.parseHeader; +import static io.helidon.webserver.cors.CORSSupport.normalize; +import static io.helidon.webserver.cors.CORSSupport.parseHeader; /** * Represents information about cross origin request sharing. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index fe2904c0c13..34dc48ad322 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -20,11 +20,12 @@ import java.util.Optional; import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.cors.internal.CrossOriginHelper; /** - * Helidon SE implementation of {@link CrossOriginHelperInternal.RequestAdapter}. + * Helidon SE implementation of {@link CrossOriginHelper.RequestAdapter}. */ -class SERequestAdapter implements CrossOriginHelperInternal.RequestAdapter { +class SERequestAdapter implements CrossOriginHelper.RequestAdapter { private final ServerRequest request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index f84290960f1..3550d7b8e25 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -18,11 +18,12 @@ import io.helidon.common.http.Http; import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.cors.internal.CrossOriginHelper; /** - * SE implementation of {@link CrossOriginHelperInternal.ResponseAdapter}. + * SE implementation of {@link CrossOriginHelper.ResponseAdapter}. */ -class SEResponseAdapter implements CrossOriginHelperInternal.ResponseAdapter { +class SEResponseAdapter implements CrossOriginHelper.ResponseAdapter { private final ServerResponse serverResponse; @@ -31,13 +32,13 @@ class SEResponseAdapter implements CrossOriginHelperInternal.ResponseAdapter header(String key, String value) { + public CrossOriginHelper.ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public CrossOriginHelperInternal.ResponseAdapter header(String key, Object value) { + public CrossOriginHelper.ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index 0080978e58b..a1ab9307c87 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -14,26 +14,26 @@ * limitations under the License. * */ -package io.helidon.webserver.cors; +package io.helidon.webserver.cors.internal; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.StringTokenizer; import java.util.function.BiFunction; import java.util.function.Supplier; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; +import io.helidon.webserver.cors.CORSSupport; +import io.helidon.webserver.cors.CrossOriginConfig; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; +import static io.helidon.webserver.cors.CORSSupport.normalize; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -55,9 +55,9 @@ * specific to the needs of CORS support. *

    */ -public class CrossOriginHelperInternal { +public class CrossOriginHelper { - private CrossOriginHelperInternal() { + private CrossOriginHelper() { } static final String ORIGIN_DENIED = "CORS origin is denied"; @@ -366,7 +366,7 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, } // Check if headers are allowed - Set requestHeaders = parseHeader(requestAdapter.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); + Set requestHeaders = CORSSupport.parseHeader(requestAdapter.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); List allowedHeaders = Arrays.asList(crossOrigin.allowHeaders()); if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { return responseAdapter.forbidden(HEADERS_NOT_IN_ALLOWED_LIST); @@ -430,53 +430,6 @@ static Optional formatHeader(T[] array) { return Optional.of(builder.toString()); } - /** - * Parse list header value as a set. - * - * @param header Header value as a list. - * @return Set of header values. - */ - static Set parseHeader(String header) { - if (header == null) { - return Collections.emptySet(); - } - Set result = new HashSet<>(); - StringTokenizer tokenizer = new StringTokenizer(header, ","); - while (tokenizer.hasMoreTokens()) { - String value = tokenizer.nextToken().trim(); - if (value.length() > 0) { - result.add(value); - } - } - return result; - } - - /** - * Parse a list of list of headers as a set. - * - * @param headers Header value as a list, each a potential list. - * @return Set of header values. - */ - static Set parseHeader(List headers) { - if (headers == null) { - return Collections.emptySet(); - } - return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); - } - - /** - * Trim leading or trailing slashes of a path. - * - * @param path The path. - * @return Normalized path. - */ - static String normalize(String path) { - int length = path.length(); - int beginIndex = path.charAt(0) == '/' ? 1 : 0; - int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; - return (endIndex <= beginIndex) ? "" : path.substring(beginIndex, endIndex); - } - /** * Checks containment in a {@code Collection}. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java index ab03a4a92d1..72083662285 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java @@ -14,4 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors.internal; \ No newline at end of file +/** + * Elements reserved for internal use. + */ +package io.helidon.webserver.cors.internal; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 0aca20ae410..a1a4ae51fe9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -173,7 +173,7 @@ *
*

Warning about internal classes

*

- * Note that {@code CrossOriginHelperInternal}, while {@code public}, is not intended for use by developers. It is + * Note that {@code CrossOriginHelper}, while {@code public}, is not intended for use by developers. It is * reserved for internal Helidon use and might change at any time. *

*/ diff --git a/webserver/cors/src/main/java/module-info.java b/webserver/cors/src/main/java/module-info.java index 482b698d841..e7a86743ef7 100644 --- a/webserver/cors/src/main/java/module-info.java +++ b/webserver/cors/src/main/java/module-info.java @@ -26,4 +26,5 @@ requires io.helidon.webserver; exports io.helidon.webserver.cors; + exports io.helidon.webserver.cors.internal to io.helidon.microprofile.cors; } From e103ecad9cd3bf0fac942c8d31d57749b3216ba3 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 9 Apr 2020 18:30:56 -0500 Subject: [PATCH 064/100] Try to fix javadoc error in pipeline --- microprofile/cors/src/main/java/module-info.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/microprofile/cors/src/main/java/module-info.java b/microprofile/cors/src/main/java/module-info.java index 289d20fe1e3..bbad13e0a35 100644 --- a/microprofile/cors/src/main/java/module-info.java +++ b/microprofile/cors/src/main/java/module-info.java @@ -22,6 +22,14 @@ requires transitive java.ws.rs; requires io.helidon.config; requires io.helidon.webserver.cors; + + // Following to help with JavaDoc... + requires io.helidon.jersey.common; + requires io.helidon.webserver.jersey; + requires io.helidon.webserver; + requires io.helidon.microprofile.config; + + // --- requires jersey.common; requires microprofile.config.api; From 02b7ff719d3ae13f50cad6fb156c9d5dea99f39d Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 10 Apr 2020 06:18:25 -0500 Subject: [PATCH 065/100] Change config structure to add 'enabled' and put paths under a 'paths' subnode; also make SE and MP resilient if there is no 'cors' config node at all --- .../microprofile/cors/CrossOriginFilter.java | 32 ++++++++------ .../META-INF/microprofile-config.properties | 6 +-- .../helidon/webserver/cors/CORSSupport.java | 31 ++++++++++++- .../cors/internal/CrossOriginHelper.java | 43 +++++++++++++++++++ .../cors/src/test/resources/application.yaml | 19 ++++---- .../cors/src/test/resources/twoCORS.yaml | 13 +++--- 6 files changed, 111 insertions(+), 33 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index a025790015b..a161e216368 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -40,6 +40,7 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; +import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; import io.helidon.webserver.cors.internal.CrossOriginHelper; import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; @@ -47,8 +48,6 @@ import org.eclipse.microprofile.config.ConfigProvider; -import static io.helidon.webserver.cors.CORSSupport.CORS_CONFIG_KEY; -import static io.helidon.webserver.cors.CrossOriginConfig.CrossOriginConfigMapper; import static io.helidon.webserver.cors.internal.CrossOriginHelper.prepareResponse; /** @@ -64,28 +63,35 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private Map crossOriginConfigs; + private final boolean corsEnabled; + private final Map crossOriginConfigs; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - crossOriginConfigs = config.get(CORS_CONFIG_KEY).as(new CrossOriginConfigMapper()).get(); + Config corsConfig = config.get(CORSSupport.CORS_CONFIG_KEY); + corsEnabled = CrossOriginHelper.isCORSEnabled(corsConfig); + crossOriginConfigs = CrossOriginHelper.toCrossOriginConfigs(corsConfig); } @Override public void filter(ContainerRequestContext requestContext) { - Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), - new MPRequestAdapter(requestContext), - new MPResponseAdapter()); - response.ifPresent(requestContext::abortWith); + if (corsEnabled) { + Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, + crossOriginFromAnnotationFinder(resourceInfo), + new MPRequestAdapter(requestContext), + new MPResponseAdapter()); + response.ifPresent(requestContext::abortWith); + } } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - prepareResponse(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), - new MPRequestAdapter(requestContext), - new MPResponseAdapter(responseContext)); + if (corsEnabled) { + prepareResponse(crossOriginConfigs, + crossOriginFromAnnotationFinder(resourceInfo), + new MPRequestAdapter(requestContext), + new MPResponseAdapter(responseContext)); + } } static class MPRequestAdapter implements RequestAdapter { diff --git a/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties b/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties index bfe47f814ae..2b69176252a 100644 --- a/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties +++ b/microprofile/cors/src/test/resources/META-INF/microprofile-config.properties @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -cors.0.path-prefix=/cors3 -cors.0.allow-origins=http://foo.bar, http://bar.foo -cors.0.allow-methods=DELETE, PUT +cors.paths.0.path-prefix=/cors3 +cors.paths.0.allow-origins=http://foo.bar, http://bar.foo +cors.paths.0.allow-methods=DELETE, PUT diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 2db053e65d1..407b8a63167 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -66,10 +66,23 @@ public class CORSSupport implements Service, Handler { */ public static final String CORS_CONFIG_KEY = "cors"; + /** + * Key for the node within the CORS config indicating whether CORS support is enabled. + */ + public static final String CORS_ENABLED_CONFIG_KEY = "enabled"; + + /** + * Key for the node within the CORS config that contains the list of path information. + */ + public static final String CORS_PATHS_CONFIG_KEY = "paths"; + + private final Map crossOriginConfigs; + private final boolean isEnabled; private CORSSupport(Builder builder) { crossOriginConfigs = builder.crossOriginConfigs(); + isEnabled = builder.isEnabled; } /** @@ -170,13 +183,17 @@ public static Set parseHeader(List headers) { @Override public void update(Routing.Rules rules) { - if (!crossOriginConfigs.isEmpty()) { + if (isEnabled && !crossOriginConfigs.isEmpty()) { rules.any(this::accept); } } @Override public void accept(ServerRequest request, ServerResponse response) { + if (!isEnabled || crossOriginConfigs.isEmpty()) { + request.next(); + return; + } RequestAdapter requestAdapter = new SERequestAdapter(request); ResponseAdapter responseAdapter = new SEResponseAdapter(response); @@ -210,6 +227,8 @@ public static class Builder implements io.helidon.common.Builder, C private Optional crossOriginConfigBuilderOpt = Optional.empty(); + private boolean isEnabled = true; + @Override public CORSSupport build() { return new CORSSupport(this); @@ -223,7 +242,15 @@ public CORSSupport build() { * @return the updated builder */ public Builder config(Config config) { - crossOriginConfigsAssembledFromConfigs.putAll(config.as(new CrossOriginConfig.CrossOriginConfigMapper()).get()); + Config pathsConfig = config.get(CORS_PATHS_CONFIG_KEY); + if (pathsConfig.exists()) { + crossOriginConfigsAssembledFromConfigs + .putAll(pathsConfig.as(new CrossOriginConfig.CrossOriginConfigMapper()).get()); + } + Config enabledConfig = config.get(CORS_ENABLED_CONFIG_KEY); + if (enabledConfig.exists()) { + isEnabled = enabledConfig.asBoolean().get(); + } return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index a1ab9307c87..f62b754e0d2 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,11 +29,14 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; +import io.helidon.config.Config; import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; +import static io.helidon.webserver.cors.CORSSupport.CORS_ENABLED_CONFIG_KEY; +import static io.helidon.webserver.cors.CORSSupport.CORS_PATHS_CONFIG_KEY; import static io.helidon.webserver.cors.CORSSupport.normalize; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; @@ -237,6 +241,45 @@ public static void prepareResponse(CrossOriginConfig crossOrigin, addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); } + /** + * Indicates whether CORS support is turned on based on config. + * + * CORS is disabled only if all of the following are true: + *
    + *
  • the {@value CORSSupport#CORS_CONFIG_KEY} config node exists,
  • + *
  • that node contains the subnode {@value CORSSupport#CORS_ENABLED_CONFIG_KEY}, and
  • + *
  • that subnode's value is {@code false}.
  • + *
+ * Otherwise, CORS support is enabled. + * + * @param corsConfig the (possibly missing) CORS config node + * @return whether CORS support should be provided or not + */ + public static boolean isCORSEnabled(Config corsConfig) { + if (!corsConfig.exists()) { + return true; + } + Config corsEnabledNode = corsConfig.get(CORS_ENABLED_CONFIG_KEY); + return !corsEnabledNode.exists() || corsEnabledNode.asBoolean().get(); + } + + /** + * Encapsulates how to build the map from each path to its {@link CrossOriginConfig} from the + * {@value CORSSupport#CORS_CONFIG_KEY} config node. + * + * @param corsNode the (possibly missing) CORS config node + * @return a map from paths to {@code CrossOriginConfig} instances; never null + */ + public static Map toCrossOriginConfigs(Config corsNode) { + if (corsNode.exists() && isCORSEnabled(corsNode)) { + Config pathsNode = corsNode.get(CORS_PATHS_CONFIG_KEY); + if (pathsNode.exists()) { + return pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()).get(); + } + } + return Collections.emptyMap(); + } + /** * Analyzes the request to determine the type of request, from the CORS perspective. * diff --git a/webserver/cors/src/test/resources/application.yaml b/webserver/cors/src/test/resources/application.yaml index 49e41dd8456..b2bceaa5e26 100644 --- a/webserver/cors/src/test/resources/application.yaml +++ b/webserver/cors/src/test/resources/application.yaml @@ -21,13 +21,14 @@ client: max-redirects: 8 cors: - - path-prefix: /cors1 - allow-origins: ["*"] - allow-methods: ["*"] - - path-prefix: /cors2 - allow-origins: ["http://foo.bar", "http://bar.foo"] - allow-methods: ["DELETE", "PUT"] - allow-headers: ["X-bar", "X-foo"] - allow-credentials: true - max-age: -1 + paths: + - path-prefix: /cors1 + allow-origins: ["*"] + allow-methods: ["*"] + - path-prefix: /cors2 + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-bar", "X-foo"] + allow-credentials: true + max-age: -1 # info for /cors3 is added programmatically \ No newline at end of file diff --git a/webserver/cors/src/test/resources/twoCORS.yaml b/webserver/cors/src/test/resources/twoCORS.yaml index 125137e9580..693c6e881d2 100644 --- a/webserver/cors/src/test/resources/twoCORS.yaml +++ b/webserver/cors/src/test/resources/twoCORS.yaml @@ -14,11 +14,12 @@ # limitations under the License. # cors: - - path-prefix: /cors2 - allow-origins: ["http://otherfoo.bar", "http://otherbar.foo"] - allow-methods: ["DELETE", "PUT"] - allow-headers: ["X-otherBar", "X-otherFoo"] - allow-credentials: true - max-age: -1 + paths: + - path-prefix: /cors2 + allow-origins: ["http://otherfoo.bar", "http://otherbar.foo"] + allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-otherBar", "X-otherFoo"] + allow-credentials: true + max-age: -1 # Purposefully exclude /cors1. /cors3 information is added programmatically. From c4586aca4ee8a6047677f53ff4381b88196ccee9 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 10 Apr 2020 09:10:33 -0500 Subject: [PATCH 066/100] CrossOriginHelper now instantiated, internalizing logic about whether it's enabled or not and the path mapping --- .../microprofile/cors/CrossOriginFilter.java | 34 +-- .../helidon/webserver/cors/CORSSupport.java | 79 ++---- .../webserver/cors/SERequestAdapter.java | 5 + .../cors/internal/CrossOriginHelper.java | 226 ++++++++++++------ .../helidon/webserver/cors/package-info.java | 71 ++++-- .../io/helidon/webserver/cors/TestUtil.java | 6 +- 6 files changed, 248 insertions(+), 173 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index a161e216368..5e4845bc356 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -19,7 +19,6 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.function.Supplier; @@ -40,7 +39,6 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; -import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; import io.helidon.webserver.cors.internal.CrossOriginHelper; import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; @@ -48,8 +46,6 @@ import org.eclipse.microprofile.config.ConfigProvider; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.prepareResponse; - /** * Class CrossOriginFilter. */ @@ -63,21 +59,20 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private final boolean corsEnabled; - private final Map crossOriginConfigs; + private final CrossOriginHelper corsHelper; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - Config corsConfig = config.get(CORSSupport.CORS_CONFIG_KEY); - corsEnabled = CrossOriginHelper.isCORSEnabled(corsConfig); - crossOriginConfigs = CrossOriginHelper.toCrossOriginConfigs(corsConfig); + corsHelper = CrossOriginHelper.builder() + .config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)) + .secondaryLookupSupplier(crossOriginFromAnnotationFinderSupplier()) + .build(); } @Override public void filter(ContainerRequestContext requestContext) { - if (corsEnabled) { - Optional response = CrossOriginHelper.processRequest(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), + if (corsHelper.isActive()) { + Optional response = corsHelper.processRequest( new MPRequestAdapter(requestContext), new MPResponseAdapter()); response.ifPresent(requestContext::abortWith); @@ -86,14 +81,17 @@ public void filter(ContainerRequestContext requestContext) { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - if (corsEnabled) { - prepareResponse(crossOriginConfigs, - crossOriginFromAnnotationFinder(resourceInfo), + if (corsHelper.isActive()) { + corsHelper.prepareResponse( new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); } } + private ResourceInfo resourceInfo() { + return resourceInfo; + } + static class MPRequestAdapter implements RequestAdapter { private final ContainerRequestContext requestContext; @@ -131,6 +129,10 @@ public String method() { public ContainerRequestContext request() { return requestContext; } + + @Override + public void next() { + } } static class MPResponseAdapter implements ResponseAdapter { @@ -175,7 +177,7 @@ public Response ok() { } } - static Supplier> crossOriginFromAnnotationFinder(ResourceInfo resourceInfo) { + Supplier> crossOriginFromAnnotationFinderSupplier() { return () -> { // If not found, inspect resource matched diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 407b8a63167..0a62c7d3bbd 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -31,12 +31,10 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import io.helidon.webserver.cors.internal.CrossOriginHelper; import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.prepareResponse; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.processRequest; - /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon * services (such as OpenAPI and metrics). @@ -44,7 +42,7 @@ * The caller can set up the {@code CORSSupport} in a combination of these ways: *

*
    - *
  • from the {@value CORSSupport#CORS_CONFIG_KEY} node in the application's default config,
  • + *
  • from the {@value CrossOriginHelper#CORS_CONFIG_KEY} node in the application's default config,
  • *
  • from a {@link Config} node supplied programmatically,
  • *
  • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which * it applies, and
  • @@ -61,28 +59,13 @@ */ public class CORSSupport implements Service, Handler { - /** - * Key used for retrieving CORS-related configuration from application- or service-level configuration. - */ - public static final String CORS_CONFIG_KEY = "cors"; - - /** - * Key for the node within the CORS config indicating whether CORS support is enabled. - */ - public static final String CORS_ENABLED_CONFIG_KEY = "enabled"; - - /** - * Key for the node within the CORS config that contains the list of path information. - */ - public static final String CORS_PATHS_CONFIG_KEY = "paths"; - - private final Map crossOriginConfigs; - private final boolean isEnabled; + private final CrossOriginHelper helper; private CORSSupport(Builder builder) { - crossOriginConfigs = builder.crossOriginConfigs(); - isEnabled = builder.isEnabled; + CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); + builder.configOpt.ifPresent(helperBuilder::config); + helper = helperBuilder.build(); } /** @@ -100,20 +83,10 @@ public static CORSSupport create() { * @param config the config node containing CORS information * @return the initialized service */ - public static CORSSupport fromConfig(Config config) { + public static CORSSupport create(Config config) { return builder().config(config).build(); } - /** - * Creates a {@code CORSSupport} set up using the {@value CORSSupport#CORS_CONFIG_KEY} node in the - * application's default config. - * - * @return the initialized service - */ - public static CORSSupport fromConfig() { - return fromConfig(Config.create().get(CORS_CONFIG_KEY)); - } - /** * Creates a {@code Builder} for assembling a {@code CORSSupport}. * @@ -125,7 +98,8 @@ public static Builder builder() { /** * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. The config node - * should contain the actual CORS settings, not a {@value CORS_CONFIG_KEY} node which contains them. + * should contain the actual CORS settings, not a {@value CrossOriginHelper#CORS_CONFIG_KEY} node which contains + * them. * * @param config node containing CORS information * @return builder initialized with the CORS set-up from the config @@ -183,35 +157,28 @@ public static Set parseHeader(List headers) { @Override public void update(Routing.Rules rules) { - if (isEnabled && !crossOriginConfigs.isEmpty()) { + if (helper.isActive()) { rules.any(this::accept); } } @Override public void accept(ServerRequest request, ServerResponse response) { - if (!isEnabled || crossOriginConfigs.isEmpty()) { + if (!helper.isActive()) { request.next(); return; } RequestAdapter requestAdapter = new SERequestAdapter(request); ResponseAdapter responseAdapter = new SEResponseAdapter(response); - Optional responseOpt = processRequest(crossOriginConfigs, - Optional::empty, - requestAdapter, - responseAdapter); + Optional responseOpt = helper.processRequest(requestAdapter, responseAdapter); responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, responseAdapter)); } private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - prepareResponse( - crossOriginConfigs, - Optional::empty, - requestAdapter, - responseAdapter); + helper.prepareResponse(requestAdapter, responseAdapter); requestAdapter.request().next(); } @@ -227,7 +194,7 @@ public static class Builder implements io.helidon.common.Builder, C private Optional crossOriginConfigBuilderOpt = Optional.empty(); - private boolean isEnabled = true; + private Optional configOpt = Optional.empty(); @Override public CORSSupport build() { @@ -235,33 +202,25 @@ public CORSSupport build() { } /** - * Saves CORS config information derived from the {@code Config}. Typically, the app or component will retrieve the - * provided {@code Config} instance from its own config using the key {@value CORSSupport#CORS_CONFIG_KEY}. + * Saves CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance + * from its own config using the key {@value CrossOriginHelper#CORS_CONFIG_KEY}. * * @param config the CORS config * @return the updated builder */ public Builder config(Config config) { - Config pathsConfig = config.get(CORS_PATHS_CONFIG_KEY); - if (pathsConfig.exists()) { - crossOriginConfigsAssembledFromConfigs - .putAll(pathsConfig.as(new CrossOriginConfig.CrossOriginConfigMapper()).get()); - } - Config enabledConfig = config.get(CORS_ENABLED_CONFIG_KEY); - if (enabledConfig.exists()) { - isEnabled = enabledConfig.asBoolean().get(); - } + configOpt = Optional.ofNullable(config.exists() ? config : null); return this; } /** - * Initializes the builder's CORS config from the {@value CORSSupport#CORS_CONFIG_KEY} node from the default + * Initializes the builder's CORS config from the {@value CrossOriginHelper#CORS_CONFIG_KEY} node from the default * application config. * * @return the updated builder */ public Builder config() { - config(Config.create().get(CORS_CONFIG_KEY)); + config(Config.create().get(CrossOriginHelper.CORS_CONFIG_KEY)); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index 34dc48ad322..3b418b7208f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -58,6 +58,11 @@ public String method() { return request.method().name(); } + @Override + public void next() { + request.next(); + } + @Override public ServerRequest request() { return request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index f62b754e0d2..5bc4dae3b1c 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Supplier; @@ -35,8 +36,6 @@ import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.webserver.cors.CORSSupport.CORS_ENABLED_CONFIG_KEY; -import static io.helidon.webserver.cors.CORSSupport.CORS_PATHS_CONFIG_KEY; import static io.helidon.webserver.cors.CORSSupport.normalize; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; @@ -61,8 +60,18 @@ */ public class CrossOriginHelper { - private CrossOriginHelper() { - } + /** + * Key for the node within the CORS config that contains the list of path information. + */ + static final String CORS_PATHS_CONFIG_KEY = "paths"; + /** + * Key for the node within the CORS config indicating whether CORS support is enabled. + */ + static final String CORS_ENABLED_CONFIG_KEY = "enabled"; + /** + * Key used for retrieving CORS-related configuration from application- or service-level configuration. + */ + public static final String CORS_CONFIG_KEY = "cors"; // public for JavaDoc references static final String ORIGIN_DENIED = "CORS origin is denied"; static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; @@ -94,6 +103,123 @@ public enum RequestType { HelidonFeatures.register(HelidonFlavor.SE, "CORS"); } + /** + * Creates a new instance using CORS config in the provided {@link Config}. + * + * @param config Config node containing CORS set-up + * @return new instance based on the config + */ + public static CrossOriginHelper create(Config config) { + return builder().config(config).build(); + } + + /** + * Creates a new instance that is enabled but with no path mappings. + * + * @return the new instance + */ + public static CrossOriginHelper create() { + return builder().build(); + } + + private final boolean isEnabled; + private final Map crossOriginConfigs; + private final Supplier> secondaryCrossOriginLookup; + + private CrossOriginHelper() { + this(builder()); + } + + private CrossOriginHelper(Builder builder) { + isEnabled = builder.isEnabled(); + crossOriginConfigs = builder.crossOriginConfigs(); + secondaryCrossOriginLookup = builder.secondaryCrossOriginLookupOpt.orElse(Optional::empty); + } + + /** + * Creates a builder for a new {@code CrossOriginHelper}. + * + * @return initialized builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for {@code CrossOriginHelper}s. + */ + public static class Builder implements io.helidon.common.Builder { + + private Optional corsConfigOpt = Optional.empty(); + private Optional>> secondaryCrossOriginLookupOpt = Optional.empty(); + + /** + * Sets the CORS config node (allowed to be missing). + * + * @param corsConfig the CORS config node + * @return updated builder + */ + public Builder config(Config corsConfig) { + corsConfigOpt = Optional.ofNullable(corsConfig.exists() ? corsConfig : null); + return this; + } + + /** + * Sets the supplier for the secondary lookup of CORS information (typically not contained in + * configuration). + * + * @param secondaryLookup the supplier + * @return updated builder + */ + public Builder secondaryLookupSupplier(Supplier> secondaryLookup) { + secondaryCrossOriginLookupOpt = Optional.of(secondaryLookup); + return this; + } + + /** + * Creates the {@code CrossOriginHelper}. + * + * @return initialized {@code CrossOriginHelper} + */ + public CrossOriginHelper build() { + return new CrossOriginHelper(this); + } + + boolean isEnabled() { + if (corsConfigOpt.isEmpty()) { + return true; + } + Config corsConfig = corsConfigOpt.get(); + if (!corsConfig.exists()) { + return true; + } + Config corsEnabledNode = corsConfig.get(CORS_ENABLED_CONFIG_KEY); + return !corsEnabledNode.exists() || corsEnabledNode.asBoolean().get(); + } + + Map crossOriginConfigs() { + AtomicReference> result = new AtomicReference<>(); + corsConfigOpt.ifPresentOrElse(corsConfig -> { + Config pathsNode = corsConfig.get(CORS_PATHS_CONFIG_KEY); + if (pathsNode.exists()) { + result.set(pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()) + .get()); + } + }, () -> result.set(Collections.emptyMap())); + return result.get(); + } + } + + /** + * Reports whether this helper, due to its set-up, will affect any requests or responses. It accounts for such things as + * whether the helper is enabled and whether any paths were set-up. + * + * @return whether the helper will have any effect on requests or responses + */ + public boolean isActive() { + return isEnabled && !crossOriginConfigs.isEmpty(); + } + /** * Processes a request according to the CORS rules, returning an {@code Optional} of the response type if * the caller should send the response immediately (such as for a preflight response or an error response to a @@ -110,8 +236,6 @@ public enum RequestType { * response at will as long as that processing includes the header settings assigned using the response adapter. *

    * - * @param crossOriginConfigs config information for CORS - * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param requestAdapter abstraction of a request * @param responseAdapter abstraction of a response * @param type for the {@code Request} managed by the requestAdapter @@ -119,10 +243,12 @@ public enum RequestType { * @return Optional of an error response if the request was an invalid CORS request; Optional.empty() if it was a * valid CORS request */ - public static Optional processRequest(Map crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, - RequestAdapter requestAdapter, - ResponseAdapter responseAdapter) { + public Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + + if (!isEnabled) { + requestAdapter.next(); + return Optional.empty(); + } Optional crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, secondaryCrossOriginLookup); @@ -162,8 +288,14 @@ public static Optional processRequest(Map c * @return Optional of an error response if the request was an invalid CORS request; Optional.empty() if it was a * valid CORS request */ - public static Optional processRequest(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, + public Optional processRequest(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + + if (!isEnabled) { + requestAdapter.next(); + return Optional.empty(); + } + RequestType requestType = requestType(requestAdapter); if (requestType == RequestType.NORMAL) { @@ -201,17 +333,16 @@ static Optional processRequest(RequestType requestType, CrossOriginCon /** * Prepares a response with CORS headers, if the supplied request is in fact a CORS request. * - * @param crossOriginConfigs config information for CORS - * @param secondaryCrossOriginLookup locates {@code CrossOrigin} from other than config (e.g., annotations for MP) * @param requestAdapter abstraction of a request * @param responseAdapter abstraction of a response * @param type for the {@code Request} managed by the requestAdapter * @param the type for the HTTP response as returned from the responseSetter */ - public static void prepareResponse(Map crossOriginConfigs, - Supplier> secondaryCrossOriginLookup, - RequestAdapter requestAdapter, - ResponseAdapter responseAdapter) { + public void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + + if (!isEnabled) { + return; + } RequestType requestType = requestType(requestAdapter); @@ -219,67 +350,13 @@ public static void prepareResponse(Map crossOr CrossOriginConfig crossOrigin = lookupCrossOrigin( requestAdapter.path(), crossOriginConfigs, - secondaryCrossOriginLookup) + secondaryCrossOriginLookup) .orElseThrow(() -> new IllegalArgumentException( "Could not locate expected CORS information while preparing response to request " + requestAdapter)); addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); } } - /** - * Prepares a response with CORS headers, if the supplied request is in fact a CORS request. - * - * @param crossOrigin the CORS settings to apply to this request - * @param requestAdapter abstraction of a request - * @param responseAdapter abstraction of a response - * @param type for the {@code Request} managed by the requestAdapter - * @param the type for the HTTP response as returned from the responseSetter - */ - public static void prepareResponse(CrossOriginConfig crossOrigin, - RequestAdapter requestAdapter, - ResponseAdapter responseAdapter) { - addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); - } - - /** - * Indicates whether CORS support is turned on based on config. - * - * CORS is disabled only if all of the following are true: - *
      - *
    • the {@value CORSSupport#CORS_CONFIG_KEY} config node exists,
    • - *
    • that node contains the subnode {@value CORSSupport#CORS_ENABLED_CONFIG_KEY}, and
    • - *
    • that subnode's value is {@code false}.
    • - *
    - * Otherwise, CORS support is enabled. - * - * @param corsConfig the (possibly missing) CORS config node - * @return whether CORS support should be provided or not - */ - public static boolean isCORSEnabled(Config corsConfig) { - if (!corsConfig.exists()) { - return true; - } - Config corsEnabledNode = corsConfig.get(CORS_ENABLED_CONFIG_KEY); - return !corsEnabledNode.exists() || corsEnabledNode.asBoolean().get(); - } - - /** - * Encapsulates how to build the map from each path to its {@link CrossOriginConfig} from the - * {@value CORSSupport#CORS_CONFIG_KEY} config node. - * - * @param corsNode the (possibly missing) CORS config node - * @return a map from paths to {@code CrossOriginConfig} instances; never null - */ - public static Map toCrossOriginConfigs(Config corsNode) { - if (corsNode.exists() && isCORSEnabled(corsNode)) { - Config pathsNode = corsNode.get(CORS_PATHS_CONFIG_KEY); - if (pathsNode.exists()) { - return pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()).get(); - } - } - return Collections.emptyMap(); - } - /** * Analyzes the request to determine the type of request, from the CORS perspective. * @@ -572,6 +649,11 @@ public interface RequestAdapter { */ String method(); + /** + * Processes the next handler/filter/request processor in the chain. + */ + void next(); + /** * Returns the request this adapter wraps. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index a1a4ae51fe9..2f34df028e9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -24,20 +24,40 @@ * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for * your application another way. You can use Helidon configuration, the Helidon CORS API, or a combination. *

    Configuration

    + *

    Format

    + * CORS configuration has two top-level items: + *
      + *
    • {@code enabled} - indicates whether CORS processing should be enabled or not; default {@code true}
    • + *
    • {@code paths} - contains a list of sub-items describing the CORS set-up for one path + *
        + *
      • {@code path-prefix} - the path this entry applies to
      • + *
      • {@code allow-origins} - array of origin URL strings
      • + *
      • {@code allow-methods} - array of method name strings (uppercase)
      • + *
      • {@code allow-headers} - array of header strings
      • + *
      • {@code expose-headers} - array of header strings
      • + *
      • {@code allow-credentials} - boolean
      • + *
      • {@code max-age} - long
      • + *
    • + *
    + *

    + * The {@code enabled} setting allows configuration to completely disable CORS processing, regardless of other settings in + * config or programmatic set-up of CORS in the application. + *

    *

    Using the application default configuration

    - * You can add a {@value io.helidon.webserver.cors.CORSSupport#CORS_CONFIG_KEY} section to your application's default config + * You can add a {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY} section to your application's default config * file to define the CORS behavior for your application endpoints. *
      *     cors:
    - *       - path-prefix: /cors1
    - *         allow-origins: ["*"]
    - *         allow-methods: ["*"]
    - *       - path-prefix: /cors2
    - *         allow-origins: ["http://foo.bar", "http://bar.foo"]
    - *         allow-methods: ["DELETE", "PUT"]
    - *         allow-headers: ["X-bar", "X-foo"]
    - *         allow-credentials: true
    - *         max-age: -1
    + *       paths:
    + *         - path-prefix: /cors1
    + *           allow-origins: ["*"]
    + *           allow-methods: ["*"]
    + *         - path-prefix: /cors2
    + *           allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *           allow-methods: ["DELETE", "PUT"]
    + *           allow-headers: ["X-bar", "X-foo"]
    + *           allow-credentials: true
    + *           max-age: -1
      * 
    * This defines CORS behavior for two paths, {@code /cors1} and {@code /cors2}, within your application's context root. *

    @@ -46,24 +66,28 @@ *

    *
      *         Routing.Builder builder = Routing.builder()
    - *                 .register("/myapp", CORSSupport.fromConfig(), new MyApp());
    + *                 .register("/myapp", CORSSupport.builder()
    + *                                      .config() // uses the {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY}} default application config
    + *                                      .build(),
    + *                                new MyApp());
      *     
    * Helidon will perform no CORS processing for any paths in your app other than {@code /cors1} and {@code /cors2}. *

    Using an explicit configuration object

    * You can create your own Helidon {@link io.helidon.config.Config} object that contains CORS information and use it instead of * the application default config. * The config node you create and give to {@code CORSSupport} should not nest the CORS information inside a - * {@value io.helidon.webserver.cors.CORSSupport#CORS_CONFIG_KEY} section. Instead, it would look like this: + * {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY} section. Instead, it would look like this: *
    - *     - path-prefix: /cors1
    - *       allow-origins: ["*"]
    - *       allow-methods: ["*"]
    - *     - path-prefix: /cors2
    - *       allow-origins: ["http://foo.bar", "http://bar.foo"]
    - *       allow-methods: ["DELETE", "PUT"]
    - *       allow-headers: ["X-bar", "X-foo"]
    - *       allow-credentials: true
    - *       max-age: -1
    + *     paths:
    + *       - path-prefix: /cors1
    + *         allow-origins: ["*"]
    + *         allow-methods: ["*"]
    + *       - path-prefix: /cors2
    + *         allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *         allow-methods: ["DELETE", "PUT"]
    + *         allow-headers: ["X-bar", "X-foo"]
    + *         allow-credentials: true
    + *         max-age: -1
      * 
    *

    * If the above config were stored in a resource in your app called {@code myAppCORS.yaml} then the following code would @@ -72,7 +96,10 @@ *

      *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myAppCORS.yaml")).build();
      *         Routing.Builder builder = Routing.builder()
    - *                 .register("/myapp", CORSSupport.fromConfig(myAppConfig), new MyApp());
    + *                 .register("/myapp", CORSSupport.builder()
    + *                                      .config(myAppConfig)
    + *                                      .build(),
    + *                                new MyApp());
      *     
    *

    The Helidon CORS API

    * You can define your application's CORS behavior programmatically -- together with configuration if you want -- by: diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index 18b7d677229..bd09ca6c84e 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -31,7 +31,7 @@ import io.helidon.webserver.WebServer; import static io.helidon.webserver.cors.CORSTestServices.SERVICE_3; -import static io.helidon.webserver.cors.CORSSupport.CORS_CONFIG_KEY; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.CORS_CONFIG_KEY; public class TestUtil { @@ -73,10 +73,10 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CORSSupport.fromConfig(), // use "cors" from default app config + CORSSupport.builder().config().build(), // use "cors" from default app config new GreetService()) .register(OTHER_GREETING_PATH, - CORSSupport.fromConfig(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself + CORSSupport.create(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself new GreetService("Other Hello")); return builder; From 1061eb6d6b0283a0c7659869e4873f97e0d644b2 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 10 Apr 2020 09:24:25 -0500 Subject: [PATCH 067/100] Simplify filter a bit; let the helper check whether processing is enabled or not. --- .../microprofile/cors/CrossOriginFilter.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 5e4845bc356..9cf20f0c3a9 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -71,21 +71,13 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Override public void filter(ContainerRequestContext requestContext) { - if (corsHelper.isActive()) { - Optional response = corsHelper.processRequest( - new MPRequestAdapter(requestContext), - new MPResponseAdapter()); - response.ifPresent(requestContext::abortWith); - } + Optional response = corsHelper.processRequest(new MPRequestAdapter(requestContext), new MPResponseAdapter()); + response.ifPresent(requestContext::abortWith); } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - if (corsHelper.isActive()) { - corsHelper.prepareResponse( - new MPRequestAdapter(requestContext), - new MPResponseAdapter(responseContext)); - } + corsHelper.prepareResponse(new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); } private ResourceInfo resourceInfo() { From 508b2bd78bc87ee53d3a80a04dd675552d8d6425 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 10 Apr 2020 15:34:14 -0500 Subject: [PATCH 068/100] Add logging; do a little refactoring --- .../microprofile/cors/CrossOriginFilter.java | 4 - .../helidon/webserver/cors/CORSSupport.java | 75 +----- .../webserver/cors/CrossOriginConfig.java | 9 +- .../cors/internal/CrossOriginHelper.java | 237 ++++++++++++------ .../webserver/cors/internal/LogHelper.java | 133 ++++++++++ 5 files changed, 304 insertions(+), 154 deletions(-) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 9cf20f0c3a9..9a96c259d28 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -80,10 +80,6 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont corsHelper.prepareResponse(new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); } - private ResourceInfo resourceInfo() { - return resourceInfo; - } - static class MPRequestAdapter implements RequestAdapter { private final ContainerRequestContext requestContext; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 0a62c7d3bbd..7a185336bdf 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -16,14 +16,9 @@ */ package io.helidon.webserver.cors; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.StringTokenizer; import io.helidon.config.Config; import io.helidon.webserver.Handler; @@ -35,6 +30,8 @@ import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; + /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon * services (such as OpenAPI and metrics). @@ -59,7 +56,6 @@ */ public class CORSSupport implements Service, Handler { - private final CrossOriginHelper helper; private CORSSupport(Builder builder) { @@ -108,57 +104,10 @@ public static Builder builder(Config config) { return builder().config(config); } - /** - * Trim leading or trailing slashes of a path. - * - * @param path The path. - * @return Normalized path. - */ - public static String normalize(String path) { - int length = path.length(); - int beginIndex = path.charAt(0) == '/' ? 1 : 0; - int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; - return (endIndex <= beginIndex) ? "" : path.substring(beginIndex, endIndex); - } - - /** - * Parse list header value as a set. - * - * @param header Header value as a list. - * @return Set of header values. - */ - public static Set parseHeader(String header) { - if (header == null) { - return Collections.emptySet(); - } - Set result = new HashSet<>(); - StringTokenizer tokenizer = new StringTokenizer(header, ","); - while (tokenizer.hasMoreTokens()) { - String value = tokenizer.nextToken().trim(); - if (value.length() > 0) { - result.add(value); - } - } - return result; - } - - /** - * Parse a list of list of headers as a set. - * - * @param headers Header value as a list, each a potential list. - * @return Set of header values. - */ - public static Set parseHeader(List headers) { - if (headers == null) { - return Collections.emptySet(); - } - return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); - } - @Override public void update(Routing.Rules rules) { if (helper.isActive()) { - rules.any(this::accept); + rules.any(this); } } @@ -272,24 +221,6 @@ public Builder maxAge(long maxAge) { return this; } - /** - * Returns the aggregation of CORS-related information supplied to the builder, constructed in this order (in case of - * conflicts, later steps override earlier ones): - *
      - *
    1. from {@code CrossOriginConfig} instances added using {@link #addCrossOrigin(String, CrossOriginConfig)},
    2. - *
    3. from invocations of the setter methods from {@link CrossOriginConfig} to set behavior for the "/" path,
    4. - *
    5. from {@code Config} supplied using {@link #config(Config)}or inferred using {@link #config()}.
    6. - *
    - * - * @return map of CrossOriginConfig instances, each entry describing a path and its associated CORS set-up - */ - Map crossOriginConfigs() { - final Map result = new HashMap<>(crossOriginConfigs); - crossOriginConfigBuilderOpt.ifPresent(opt -> result.put("/", opt.get())); - result.putAll(crossOriginConfigsAssembledFromConfigs); - return result; - } - private CrossOriginConfig.Builder crossOriginConfigBuilder() { if (crossOriginConfigBuilderOpt.isEmpty()) { crossOriginConfigBuilderOpt = Optional.of(CrossOriginConfig.builder()); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 29198de626d..eb2e8a536ce 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -23,8 +23,8 @@ import io.helidon.config.Config; -import static io.helidon.webserver.cors.CORSSupport.normalize; -import static io.helidon.webserver.cors.CORSSupport.parseHeader; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.parseHeader; /** * Represents information about cross origin request sharing. @@ -145,11 +145,12 @@ private static String[] copyOf(String[] strings) { } /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder}. + * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder} for assiging CORS-related + * attributes. * * @param the type of the implementing class so the fluid methods can return the correct type */ - interface Setter { + interface Setter> { /** * Sets the allowOrigins. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index 5bc4dae3b1c..bafc8f49654 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -19,24 +19,26 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.logging.Logger; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; import io.helidon.config.Config; -import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; +import io.helidon.webserver.cors.internal.LogHelper.Headers; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.webserver.cors.CORSSupport.normalize; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -45,6 +47,7 @@ import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.webserver.cors.internal.LogHelper.DECISION_LEVEL; /** * Not for use by developers. @@ -78,6 +81,57 @@ public class CrossOriginHelper { static final String METHOD_NOT_IN_ALLOWED_LIST = "CORS method is not in allowed list"; static final String HEADERS_NOT_IN_ALLOWED_LIST = "CORS headers not in allowed list"; + static final Logger LOGGER = Logger.getLogger(CrossOriginHelper.class.getName()); + + private static final Supplier> EMPTY_SECONDARY_SUPPLIER = Optional::empty; + + /** + * Trim leading or trailing slashes of a path. + * + * @param path The path. + * @return Normalized path. + */ + public static String normalize(String path) { + int length = path.length(); + int beginIndex = path.charAt(0) == '/' ? 1 : 0; + int endIndex = path.charAt(length - 1) == '/' ? length - 1 : length; + return (endIndex <= beginIndex) ? "" : path.substring(beginIndex, endIndex); + } + + /** + * Parse list header value as a set. + * + * @param header Header value as a list. + * @return Set of header values. + */ + public static Set parseHeader(String header) { + if (header == null) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + StringTokenizer tokenizer = new StringTokenizer(header, ","); + while (tokenizer.hasMoreTokens()) { + String value = tokenizer.nextToken().trim(); + if (value.length() > 0) { + result.add(value); + } + } + return result; + } + + /** + * Parse a list of list of headers as a set. + * + * @param headers Header value as a list, each a potential list. + * @return Set of header values. + */ + public static Set parseHeader(List headers) { + if (headers == null) { + return Collections.emptySet(); + } + return parseHeader(headers.stream().reduce("", (a, b) -> a + "," + b)); + } + /** * Not for use by developers. * @@ -133,7 +187,7 @@ private CrossOriginHelper() { private CrossOriginHelper(Builder builder) { isEnabled = builder.isEnabled(); crossOriginConfigs = builder.crossOriginConfigs(); - secondaryCrossOriginLookup = builder.secondaryCrossOriginLookupOpt.orElse(Optional::empty); + secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; } /** @@ -151,7 +205,7 @@ public static Builder builder() { public static class Builder implements io.helidon.common.Builder { private Optional corsConfigOpt = Optional.empty(); - private Optional>> secondaryCrossOriginLookupOpt = Optional.empty(); + private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; /** * Sets the CORS config node (allowed to be missing). @@ -172,7 +226,7 @@ public Builder config(Config corsConfig) { * @return updated builder */ public Builder secondaryLookupSupplier(Supplier> secondaryLookup) { - secondaryCrossOriginLookupOpt = Optional.of(secondaryLookup); + secondaryCrossOriginLookup = secondaryLookup; return this; } @@ -182,7 +236,11 @@ public Builder secondaryLookupSupplier(Supplier> sec * @return initialized {@code CrossOriginHelper} */ public CrossOriginHelper build() { - return new CrossOriginHelper(this); + CrossOriginHelper result = new CrossOriginHelper(this); + + LOGGER.config(() -> String.format("CrossOriginHelper configured as: %s", result.toString())); + + return result; } boolean isEnabled() { @@ -211,8 +269,7 @@ Map crossOriginConfigs() { } /** - * Reports whether this helper, due to its set-up, will affect any requests or responses. It accounts for such things as - * whether the helper is enabled and whether any paths were set-up. + * Reports whether this helper, due to its set-up, will affect any requests or responses. * * @return whether the helper will have any effect on requests or responses */ @@ -246,6 +303,7 @@ public boolean isActive() { public Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { if (!isEnabled) { + LOGGER.log(DECISION_LEVEL, () -> String.format("CORS ignoring request %s; processing is disabled", requestAdapter)); requestAdapter.next(); return Optional.empty(); } @@ -256,52 +314,22 @@ public Optional processRequest(RequestAdapter requestAdapter, Respo RequestType requestType = requestType(requestAdapter); if (requestType == RequestType.NORMAL) { + LOGGER.log(DECISION_LEVEL, "passing normal request through unchanged"); return Optional.empty(); } - // If this is a CORS request of some sort and CORS is not enabled, deny the request. + // If this is a CORS request of some sort and there is no matching CORS configuration, deny the request. if (crossOrigin.isEmpty()) { - return Optional.of(responseAdapter.forbidden(ORIGIN_DENIED)); + return Optional.of(forbid(requestAdapter, responseAdapter, ORIGIN_DENIED, + () -> "no matching CORS configuration for path " + requestAdapter.path())); } return processRequest(requestType, crossOrigin.get(), requestAdapter, responseAdapter); } - /** - * Processes a request according to the CORS rules, returning an {@code Optional} of the response type if - * the caller should send the response immediately (such as for a preflight response or an error response to a - * non-preflight CORS request). - *

    - * If the optional is empty, this processor has either: - *

    - *
      - *
    • recognized the request as a valid non-preflight CORS request and has set headers in the response adapter, or
    • - *
    • recognized the request as a non-CORS request entirely.
    • - *
    - *

    - * In either case of an empty optional return value, the caller should proceed with its own request processing and sends its - * response at will as long as that processing includes the header settings assigned using the response adapter. - *

    - * @param crossOrigin cross origin config to use in handling this request - * @param requestAdapter abstraction of a request - * @param responseAdapter abstraction of a response - * @param type for the {@code Request} managed by the requestAdapter - * @param the type for the HTTP response as returned from the responseSetter - * @return Optional of an error response if the request was an invalid CORS request; Optional.empty() if it was a - * valid CORS request - */ - public Optional processRequest(CrossOriginConfig crossOrigin, RequestAdapter requestAdapter, - ResponseAdapter responseAdapter) { - if (!isEnabled) { - requestAdapter.next(); - return Optional.empty(); - } - - RequestType requestType = requestType(requestAdapter); - - if (requestType == RequestType.NORMAL) { - return Optional.empty(); - } - return processRequest(requestType, crossOrigin, requestAdapter, responseAdapter); + @Override + public String toString() { + return String.format("CrossOriginHelper{isEnabled=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", + isEnabled, crossOriginConfigs, secondaryCrossOriginLookup == EMPTY_SECONDARY_SUPPLIER ? "(not set)" : "(set)"); } static Optional processRequest(RequestType requestType, CrossOriginConfig crossOrigin, @@ -341,10 +369,12 @@ static Optional processRequest(RequestType requestType, CrossOriginCon public void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { if (!isEnabled) { + LOGGER.log(DECISION_LEVEL, + () -> String.format("CORS ignoring request %s; CORS processing is dieabled", requestAdapter)); return; } - RequestType requestType = requestType(requestAdapter); + RequestType requestType = requestType(requestAdapter, true); // silent: already logged during req processing if (requestType == RequestType.CORS) { CrossOriginConfig crossOrigin = lookupCrossOrigin( @@ -364,22 +394,44 @@ public void prepareResponse(RequestAdapter requestAdapter, ResponseAda * @param type of request wrapped by the adapter * @return RequestType the CORS request type of the request */ + static RequestType requestType(RequestAdapter requestAdapter, boolean silent) { + if (isRequestTypeNormal(requestAdapter, silent)) { + return RequestType.NORMAL; + } + + return inferCORSRequestType(requestAdapter, silent); + } + static RequestType requestType(RequestAdapter requestAdapter) { + return requestType(requestAdapter, false); + } + + private static boolean isRequestTypeNormal(RequestAdapter requestAdapter, boolean silent) { // If no origin header or same as host, then just normal Optional originOpt = requestAdapter.firstHeader(ORIGIN); Optional hostOpt = requestAdapter.firstHeader(HOST); - if (originOpt.isEmpty() || (hostOpt.isPresent() && originOpt.get().contains("://" + hostOpt.get()))) { - return RequestType.NORMAL; - } - // Is this a pre-flight request? - if (requestAdapter.method().equalsIgnoreCase(Http.Method.OPTIONS.name()) - && requestAdapter.headerContainsKey(ACCESS_CONTROL_REQUEST_METHOD)) { - return RequestType.PREFLIGHT; + boolean result = originOpt.isEmpty() || (hostOpt.isPresent() && originOpt.get().contains("://" + hostOpt.get())); + if (!silent && LOGGER.isLoggable(DECISION_LEVEL)) { + LogHelper.isRequestTypeNormal(result, requestAdapter, originOpt, hostOpt); } + return result; + } + + private static RequestType inferCORSRequestType(RequestAdapter requestAdapter, boolean silent) { + + String methodName = requestAdapter.method(); + boolean isMethodOPTION = methodName.equalsIgnoreCase(Http.Method.OPTIONS.name()); + boolean requestContainsAccessControlRequestMethodHeader = requestAdapter.headerContainsKey(ACCESS_CONTROL_REQUEST_METHOD); - // A CORS request that is not a pre-flight one - return RequestType.CORS; + RequestType result = isMethodOPTION && requestContainsAccessControlRequestMethodHeader + ? RequestType.PREFLIGHT + : RequestType.CORS; + + if (!silent && !LOGGER.isLoggable(DECISION_LEVEL)) { + LogHelper.inferCORSRequestType(result, requestAdapter, methodName, requestContainsAccessControlRequestMethodHeader); + } + return result; } /** @@ -403,7 +455,10 @@ static Optional processCORSRequest( List allowedOrigins = Arrays.asList(crossOriginConfig.allowOrigins()); Optional originOpt = requestAdapter.firstHeader(ORIGIN); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { - return Optional.of(responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST)); + return Optional.of(forbid(requestAdapter, + responseAdapter, + ORIGIN_NOT_IN_ALLOWED_LIST, + () -> String.format("actual: %s, allowed: %s", originOpt.orElse("(MISSING)"), allowedOrigins))); } // Successful processing of request @@ -429,18 +484,24 @@ static void addCORSHeadersToResponse(CrossOriginConfig crossOrigin, String origin = requestAdapter.firstHeader(ORIGIN).orElseThrow(noRequiredHeaderExcFactory(ORIGIN)); if (crossOrigin.allowCredentials()) { - responseAdapter.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") - .header(ACCESS_CONTROL_ALLOW_ORIGIN, origin) - .header(Http.Header.VARY, ORIGIN); + new Headers() + .add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") + .add(ACCESS_CONTROL_ALLOW_ORIGIN, origin) + .add(Http.Header.VARY, ORIGIN) + .set(responseAdapter::header, "allow-credentials was set in CORS config"); } else { List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); - responseAdapter.header(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin) - .header(Http.Header.VARY, ORIGIN); + new Headers() + .add(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin) + .add(Http.Header.VARY, ORIGIN) + .set(responseAdapter::header, "allow-credentials was not set in CORS config"); } // Add Access-Control-Expose-Headers if non-empty + Headers headers = new Headers(); formatHeader(crossOrigin.exposeHeaders()).ifPresent( - h -> responseAdapter.header(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + h -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, h)); + headers.set(responseAdapter::header, "expose-headers was set in CORS config"); } /** @@ -463,18 +524,24 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, Optional originOpt = requestAdapter.firstHeader(ORIGIN); if (originOpt.isEmpty()) { - return responseAdapter.forbidden(noRequiredHeader(ORIGIN)); + return forbid(requestAdapter, responseAdapter, noRequiredHeader(ORIGIN)); } // If enabled but not whitelisted, deny request List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); if (!allowedOrigins.contains("*") && !contains(originOpt, allowedOrigins, String::equals)) { - return responseAdapter.forbidden(ORIGIN_NOT_IN_ALLOWED_LIST); + return forbid(requestAdapter, + responseAdapter, + ORIGIN_NOT_IN_ALLOWED_LIST, + () -> "actual origin: " + originOpt.get() + ", allowedOrigins: " + allowedOrigins); } Optional methodOpt = requestAdapter.firstHeader(ACCESS_CONTROL_REQUEST_METHOD); if (methodOpt.isEmpty()) { - return responseAdapter.forbidden(METHOD_NOT_IN_ALLOWED_LIST); + return forbid(requestAdapter, + responseAdapter, + METHOD_NOT_IN_ALLOWED_LIST, + () -> "header " + ACCESS_CONTROL_REQUEST_METHOD + " absent from request"); } // Check if method is allowed @@ -482,29 +549,39 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, List allowedMethods = Arrays.asList(crossOrigin.allowMethods()); if (!allowedMethods.contains("*") && !contains(method, allowedMethods, String::equals)) { - return responseAdapter.forbidden(METHOD_NOT_IN_ALLOWED_LIST); + return forbid(requestAdapter, + responseAdapter, + METHOD_NOT_IN_ALLOWED_LIST, + () -> String.format("header %s had value %s but allowedMethods is %s", ACCESS_CONTROL_REQUEST_METHOD, + methodOpt.get(), allowedMethods)); } // Check if headers are allowed - Set requestHeaders = CORSSupport.parseHeader(requestAdapter.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); + Set requestHeaders = parseHeader(requestAdapter.allHeaders(ACCESS_CONTROL_REQUEST_HEADERS)); List allowedHeaders = Arrays.asList(crossOrigin.allowHeaders()); if (!allowedHeaders.contains("*") && !contains(requestHeaders, allowedHeaders)) { - return responseAdapter.forbidden(HEADERS_NOT_IN_ALLOWED_LIST); + return forbid(requestAdapter, + responseAdapter, + HEADERS_NOT_IN_ALLOWED_LIST, + () -> String.format("requested headers %s incompatible with allowed headers %s", requestHeaders, + allowedHeaders)); } // Build successful response - responseAdapter.header(ACCESS_CONTROL_ALLOW_ORIGIN, originOpt.get()); + Headers headers = new Headers() + .add(ACCESS_CONTROL_ALLOW_ORIGIN, originOpt.get()); if (crossOrigin.allowCredentials()) { - responseAdapter.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + headers.add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true", "allowCredentials config was set"); } - responseAdapter.header(ACCESS_CONTROL_ALLOW_METHODS, method); + headers.add(ACCESS_CONTROL_ALLOW_METHODS, method); formatHeader(requestHeaders.toArray()).ifPresent( - h -> responseAdapter.header(ACCESS_CONTROL_ALLOW_HEADERS, h)); + h -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, h)); long maxAge = crossOrigin.maxAge(); if (maxAge > 0) { - responseAdapter.header(ACCESS_CONTROL_MAX_AGE, maxAge); + headers.add(ACCESS_CONTROL_MAX_AGE, maxAge, "maxAge > 0"); } + headers.set(responseAdapter::header, "headers set on preflight request"); return responseAdapter.ok(); } @@ -603,6 +680,18 @@ private static String noRequiredHeader(String header) { return "CORS request does not have required header " + header; } + private static U forbid(RequestAdapter requestAdapter, ResponseAdapter responseAdapter, + String reason) { + return forbid(requestAdapter, responseAdapter, reason, null); + } + + private static U forbid(RequestAdapter requestAdapter, ResponseAdapter responseAdapter, String publicReason, + Supplier privateExplanation) { + LOGGER.log(DECISION_LEVEL, String.format("CORS denying request %s: %s", requestAdapter, + publicReason + (privateExplanation == null ? "" : "; " + privateExplanation.get()))); + return responseAdapter.forbidden(publicReason); + } + /** * Not for use by developers. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java new file mode 100644 index 00000000000..c9a7a032b07 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors.internal; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.logging.Level; + +import io.helidon.common.http.Http; +import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; +import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestType; + +import static io.helidon.common.http.Http.Header.HOST; +import static io.helidon.common.http.Http.Header.ORIGIN; +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.webserver.cors.internal.CrossOriginHelper.LOGGER; + +class LogHelper { + + static final Level DECISION_LEVEL = Level.FINE; + + private LogHelper() { + } + + /** + * Collects headers for assignment to a request or response and logging during assignment. + */ + static class Headers { + private final List> headers = new ArrayList<>(); + private final List notes = LOGGER.isLoggable(DECISION_LEVEL) ? new ArrayList<>() : null; + + Headers add(String key, Object value) { + headers.add(new AbstractMap.SimpleEntry<>(key, value)); + return this; + } + + Headers add(String key, Object value, String note) { + add(key, value); + if (notes != null) { + notes.add(note); + } + return this; + } + + void set(BiConsumer consumer, String note) { + headers.forEach(entry -> consumer.accept(entry.getKey(), entry.getValue())); + LOGGER.log(DECISION_LEVEL, () -> note + ": " + headers + (notes == null ? "" : notes)); + } + } + + static boolean isRequestTypeNormal(boolean result, RequestAdapter requestAdapter, Optional originOpt, + Optional hostOpt) { + // If no origin header or same as host, then just normal + + List reasonsWhyNormal = new ArrayList<>(); + List factorsWhyCrossHost = new ArrayList<>(); + + if (originOpt.isEmpty()) { + reasonsWhyNormal.add("header " + ORIGIN + " is absent"); + } else { + factorsWhyCrossHost.add(String.format("header %s is present (%s)", ORIGIN, originOpt.get())); + } + + if (hostOpt.isEmpty()) { + reasonsWhyNormal.add("header " + HOST + " is absent"); + } else { + factorsWhyCrossHost.add(String.format("header %s is present (%s)", HOST, hostOpt.get())); + } + + if (hostOpt.isPresent() && originOpt.isPresent()) { + String partOfOriginMatchingHost = "://" + hostOpt.get(); + if (originOpt.get() + .contains(partOfOriginMatchingHost)) { + reasonsWhyNormal.add(String.format("header %s '%s' matches header %s '%s'; not cross-host", ORIGIN, + originOpt.get(), HOST, hostOpt.get())); + } else { + factorsWhyCrossHost.add(String.format("header %s (%s) does not match header %s %s; cross-host", ORIGIN, + originOpt.get(), HOST, hostOpt.get())); + } + } + + if (result) { + LOGGER.log(LogHelper.DECISION_LEVEL, + () -> String.format("Request %s is not cross-host: %s", requestAdapter, reasonsWhyNormal)); + } else { + LOGGER.log(LogHelper.DECISION_LEVEL, + () -> String.format("Request %s is cross-host: %s", requestAdapter, factorsWhyCrossHost)); + } + return result; + } + + static RequestType inferCORSRequestType(RequestType result, RequestAdapter requestAdapter, String methodName, + boolean requestContainsAccessControlRequestMethodHeader) { + List reasonsWhyCORS = new ArrayList<>(); // any reason is determinative + List factorsWhyPreflight = new ArrayList<>(); // factors contribute but, individually, do not determine + + if (!methodName.equalsIgnoreCase(Http.Method.OPTIONS.name())) { + reasonsWhyCORS.add(String.format("method is %s, not %s", methodName, Http.Method.OPTIONS.name())); + } else { + factorsWhyPreflight.add(String.format("method is %s", methodName)); + } + + if (!requestContainsAccessControlRequestMethodHeader) { + reasonsWhyCORS.add(String.format("header %s is absent", ACCESS_CONTROL_REQUEST_METHOD)); + } else { + factorsWhyPreflight.add(String.format("header %s is present(%s)", ACCESS_CONTROL_REQUEST_METHOD, + requestAdapter.firstHeader(ACCESS_CONTROL_REQUEST_METHOD))); + } + + LOGGER.log(DECISION_LEVEL, String.format("Request %s is of type %s; %s", requestAdapter, result.name(), + result == RequestType.PREFLIGHT ? factorsWhyPreflight : reasonsWhyCORS)); + + return result; + } +} From 71ca237f40713a47af0dd55ec7cba6de9bd01405 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sat, 11 Apr 2020 09:11:57 -0500 Subject: [PATCH 069/100] Some more refactoring; removing use of default config from SE impl because the developer is always setting it up anyway and will pass an explicit config node --- .../microprofile/cors/CrossOriginFilter.java | 13 +- .../microprofile/cors/package-info.java | 9 +- .../helidon/webserver/cors/CORSSupport.java | 63 +++---- .../webserver/cors/CrossOriginConfig.java | 61 +----- .../internal/CrossOriginConfigAggregator.java | 178 ++++++++++++++++++ .../cors/internal/CrossOriginHelper.java | 62 +++--- .../webserver/cors/internal/Setter.java | 75 ++++++++ .../helidon/webserver/cors/package-info.java | 66 +++---- .../io/helidon/webserver/cors/TestUtil.java | 9 +- .../cors/src/test/resources/application.yaml | 2 +- .../cors/src/test/resources/twoCORS.yaml | 2 +- 11 files changed, 349 insertions(+), 191 deletions(-) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 9a96c259d28..a7066016a51 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -40,6 +40,7 @@ import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; import io.helidon.webserver.cors.CrossOriginConfig; +import io.helidon.webserver.cors.internal.CrossOriginConfigAggregator; import io.helidon.webserver.cors.internal.CrossOriginHelper; import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; @@ -52,6 +53,11 @@ @Priority(Priorities.HEADER_DECORATOR) class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilter { + /** + * Key used for retrieving CORS-related configuration from MP configuration. + */ + public static final String CORS_CONFIG_KEY = "cors"; + static { HelidonFeatures.register(HelidonFlavor.MP, "CORS"); } @@ -64,8 +70,9 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); corsHelper = CrossOriginHelper.builder() - .config(config.get(CrossOriginHelper.CORS_CONFIG_KEY)) - .secondaryLookupSupplier(crossOriginFromAnnotationFinderSupplier()) + .aggregator(CrossOriginConfigAggregator.create() + .config(config.get(CORS_CONFIG_KEY))) + .secondaryLookupSupplier(crossOriginFromAnnotationSupplier()) .build(); } @@ -165,7 +172,7 @@ public Response ok() { } } - Supplier> crossOriginFromAnnotationFinderSupplier() { + Supplier> crossOriginFromAnnotationSupplier() { return () -> { // If not found, inspect resource matched diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java index e51fe2ef58c..69104e983bf 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java @@ -15,6 +15,13 @@ */ /** - * CORS implementation. + *

    CORS implementation for Helidon MicroProfile.

    + * Adding the Helidon MP CORS module to your application enables CORS support automatically, implementing the configuration in + * the {@value io.helidon.microprofile.cors.CrossOriginFilter#CORS_CONFIG_KEY} section of your MicroProfile configuration. + * + * See Helidon + * CORS Support for information about the CORS configuration format. + * */ package io.helidon.microprofile.cors; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 7a185336bdf..a90fbf2811e 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -16,8 +16,6 @@ */ package io.helidon.webserver.cors; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import io.helidon.config.Config; @@ -26,11 +24,11 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import io.helidon.webserver.cors.internal.CrossOriginConfigAggregator; import io.helidon.webserver.cors.internal.CrossOriginHelper; import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter; - -import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; +import io.helidon.webserver.cors.internal.Setter; /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon @@ -39,7 +37,6 @@ * The caller can set up the {@code CORSSupport} in a combination of these ways: *

    *
      - *
    • from the {@value CrossOriginHelper#CORS_CONFIG_KEY} node in the application's default config,
    • *
    • from a {@link Config} node supplied programmatically,
    • *
    • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which * it applies, and
    • @@ -59,8 +56,7 @@ public class CORSSupport implements Service, Handler { private final CrossOriginHelper helper; private CORSSupport(Builder builder) { - CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); - builder.configOpt.ifPresent(helperBuilder::config); + CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder().aggregator(builder.aggregator); helper = helperBuilder.build(); } @@ -93,9 +89,7 @@ public static Builder builder() { } /** - * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. The config node - * should contain the actual CORS settings, not a {@value CrossOriginHelper#CORS_CONFIG_KEY} node which contains - * them. + * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. * * @param config node containing CORS information * @return builder initialized with the CORS set-up from the config @@ -135,15 +129,12 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques /** * Builder for {@code CORSSupport} instances. */ - public static class Builder implements io.helidon.common.Builder, CrossOriginConfig.Setter { - - private final Map crossOriginConfigs = new HashMap<>(); - - private final Map crossOriginConfigsAssembledFromConfigs = new HashMap<>(); + public static class Builder implements io.helidon.common.Builder, Setter { - private Optional crossOriginConfigBuilderOpt = Optional.empty(); + private final CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create(); - private Optional configOpt = Optional.empty(); + Builder() { + } @Override public CORSSupport build() { @@ -151,25 +142,25 @@ public CORSSupport build() { } /** - * Saves CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance - * from its own config using the key {@value CrossOriginHelper#CORS_CONFIG_KEY}. + * Merges CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance + * from its own config. * * @param config the CORS config * @return the updated builder */ public Builder config(Config config) { - configOpt = Optional.ofNullable(config.exists() ? config : null); + aggregator.config(config); return this; } /** - * Initializes the builder's CORS config from the {@value CrossOriginHelper#CORS_CONFIG_KEY} node from the default - * application config. + * Sets whether CORS support should be enabled or not. * - * @return the updated builder + * @param value whether to use CORS support + * @return updated builder */ - public Builder config() { - config(Config.create().get(CrossOriginHelper.CORS_CONFIG_KEY)); + public Builder enabled(boolean value) { + aggregator.enabled(value); return this; } @@ -181,52 +172,44 @@ public Builder config() { * @return updated builder */ public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - crossOriginConfigs.put(normalize(path), crossOrigin); + aggregator.addCrossOrigin(path, crossOrigin); return this; } @Override public Builder allowOrigins(String... origins) { - crossOriginConfigBuilder().allowOrigins(origins); + aggregator.allowOrigins(origins); return this; } @Override public Builder allowHeaders(String... allowHeaders) { - crossOriginConfigBuilder().allowHeaders(allowHeaders); + aggregator.allowHeaders(allowHeaders); return this; } @Override public Builder exposeHeaders(String... exposeHeaders) { - crossOriginConfigBuilder().exposeHeaders(exposeHeaders); + aggregator.exposeHeaders(exposeHeaders); return this; } @Override public Builder allowMethods(String... allowMethods) { - crossOriginConfigBuilder().allowMethods(allowMethods); + aggregator.allowMethods(allowMethods); return this; } @Override public Builder allowCredentials(boolean allowCredentials) { - crossOriginConfigBuilder().allowCredentials(allowCredentials); + aggregator.allowCredentials(allowCredentials); return this; } @Override public Builder maxAge(long maxAge) { - crossOriginConfigBuilder().maxAge(maxAge); + aggregator.maxAge(maxAge); return this; } - - private CrossOriginConfig.Builder crossOriginConfigBuilder() { - if (crossOriginConfigBuilderOpt.isEmpty()) { - crossOriginConfigBuilderOpt = Optional.of(CrossOriginConfig.builder()); - } - return crossOriginConfigBuilderOpt.get(); - } } - } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index eb2e8a536ce..d3b2f0832ca 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -22,6 +22,7 @@ import java.util.function.Function; import io.helidon.config.Config; +import io.helidon.webserver.cors.internal.Setter; import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; import static io.helidon.webserver.cors.internal.CrossOriginHelper.parseHeader; @@ -67,6 +68,10 @@ public class CrossOriginConfig /* implements CrossOrigin */ { * Header Access-Control-Request-Method. */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** + * Key for the node within the CORS config that contains the list of path information. + */ + public static final String CORS_PATHS_CONFIG_KEY = "paths"; private final String[] allowOrigins; private final String[] allowHeaders; @@ -144,62 +149,6 @@ private static String[] copyOf(String[] strings) { return strings != null ? Arrays.copyOf(strings, strings.length) : new String[0]; } - /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder} for assiging CORS-related - * attributes. - * - * @param the type of the implementing class so the fluid methods can return the correct type - */ - interface Setter> { - /** - * Sets the allowOrigins. - * - * @param origins the origin value(s) - * @return updated builder - */ - T allowOrigins(String... origins); - - /** - * Sets the allow headers. - * - * @param allowHeaders the allow headers value(s) - * @return updated builder - */ - T allowHeaders(String... allowHeaders); - - /** - * Sets the expose headers. - * - * @param exposeHeaders the expose headers value(s) - * @return updated builder - */ - T exposeHeaders(String... exposeHeaders); - - /** - * Sets the allow methods. - * - * @param allowMethods the allow method value(s) - * @return updated builder - */ - T allowMethods(String... allowMethods); - - /** - * Sets the allow credentials flag. - * - * @param allowCredentials the allow credentials flag - * @return updated builder - */ - T allowCredentials(boolean allowCredentials); - - /** - * Sets the maximum age. - * - * @param maxAge the maximum age - * @return updated builder - */ - T maxAge(long maxAge); - } - /** * Builder for {@link CrossOriginConfig}. */ diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java new file mode 100644 index 00000000000..9b43915b4a0 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors.internal; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.webserver.cors.CrossOriginConfig; + +import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; + +/** + * Not for developer use. Collects CORS set-up information from various sources. + */ +public class CrossOriginConfigAggregator implements Setter { + + // Records paths and configs added via addCrossOriginConfig + private final Map crossOriginConfigs = new HashMap<>(); + + // Records the merged paths and configs added via the config method + private final Map crossOriginConfigsAssembledFromConfigs = new HashMap<>(); + + // Accumulates config via the setter methods from CrossOriginConfig + private Optional crossOriginConfigBuilderOpt = Optional.empty(); + + // To be enabled, there must be a "cors" config node. + private Optional isEnabledFromConfig = Optional.empty(); + + private boolean isEnabledFromAPI = true; + + /** + * Factory method. + * + * @return new CrossOriginConfigAggregatpr + */ + public static CrossOriginConfigAggregator create() { + return new CrossOriginConfigAggregator(); + } + + private CrossOriginConfigAggregator() { + } + + /** + * Reports whether the sources of CORS information have left CORS enabled or not. + * + * @return if CORS processing should be done + */ + public boolean isEnabled() { + return isEnabledFromConfig.orElse(isEnabledFromAPI); + } + + /** + * Aggregates the cross origin config from all sources into one. + * + * @return merged path-to-origin map + */ + public Map crossOriginConfigs() { + Map result = new HashMap<>(); + + result.putAll(crossOriginConfigs); + crossOriginConfigBuilderOpt.ifPresent(builder -> result.put("/", builder.build())); + result.putAll(crossOriginConfigsAssembledFromConfigs); + + return result; + } + + /** + * Add cross-origin information from a {@link Config} node. + * + * @param config {@code Config} node containing + * @return updated builder + */ + public CrossOriginConfigAggregator config(Config config) { + /* + * Merge the newly-provided config with what we've assembled so far. We do not merge the config for a given path; + * we add paths that are not already present and override paths that are there. + */ + if (config.exists()) { + Config isEnabledNode = config.get("enabled"); + if (isEnabledNode.exists()) { + // Last config setting wins. + isEnabledFromConfig = Optional.of(isEnabledNode.asBoolean().get()); + } else { + // If config exists but enabled is missing, default enabled is true. + isEnabledFromConfig = Optional.of(Boolean.TRUE); + } + Config pathsNode = config.get(CrossOriginConfig.CORS_PATHS_CONFIG_KEY); + if (pathsNode.exists()) { + crossOriginConfigsAssembledFromConfigs.putAll(pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()) + .get()); + } + } + return this; + } + + /** + * Adds cross origin information associated with a given path. + * + * @param path the path to which the cross origin information applies + * @param crossOrigin the cross origin information + * @return updated builder + */ + public CrossOriginConfigAggregator addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + crossOriginConfigs.put(normalize(path), crossOrigin); + return this; + } + + /** + * Sets whether the app wants to enable CORS. + * + * @param value whether CORS should be enabled + * @return updated builder + */ + public CrossOriginConfigAggregator enabled(boolean value) { + isEnabledFromAPI = value; + return this; + } + + @Override + public CrossOriginConfigAggregator allowOrigins(String... origins) { + crossOriginConfigBuilder().allowOrigins(origins); + return this; + } + + @Override + public CrossOriginConfigAggregator allowHeaders(String... allowHeaders) { + crossOriginConfigBuilder().allowHeaders(allowHeaders); + return this; + } + + @Override + public CrossOriginConfigAggregator exposeHeaders(String... exposeHeaders) { + crossOriginConfigBuilder().exposeHeaders(exposeHeaders); + return this; + } + + @Override + public CrossOriginConfigAggregator allowMethods(String... allowMethods) { + crossOriginConfigBuilder().allowMethods(allowMethods); + return this; + } + + @Override + public CrossOriginConfigAggregator allowCredentials(boolean allowCredentials) { + crossOriginConfigBuilder().allowCredentials(allowCredentials); + return this; + } + + @Override + public CrossOriginConfigAggregator maxAge(long maxAge) { + crossOriginConfigBuilder().maxAge(maxAge); + return this; + } + + private CrossOriginConfig.Builder crossOriginConfigBuilder() { + if (crossOriginConfigBuilderOpt.isEmpty()) { + crossOriginConfigBuilderOpt = Optional.of(CrossOriginConfig.builder()); + } + return crossOriginConfigBuilderOpt.get(); + } + +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index bafc8f49654..31d6518e8a1 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -25,7 +25,6 @@ import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.logging.Logger; @@ -63,18 +62,10 @@ */ public class CrossOriginHelper { - /** - * Key for the node within the CORS config that contains the list of path information. - */ - static final String CORS_PATHS_CONFIG_KEY = "paths"; /** * Key for the node within the CORS config indicating whether CORS support is enabled. */ static final String CORS_ENABLED_CONFIG_KEY = "enabled"; - /** - * Key used for retrieving CORS-related configuration from application- or service-level configuration. - */ - public static final String CORS_CONFIG_KEY = "cors"; // public for JavaDoc references static final String ORIGIN_DENIED = "CORS origin is denied"; static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; @@ -164,7 +155,10 @@ public enum RequestType { * @return new instance based on the config */ public static CrossOriginHelper create(Config config) { - return builder().config(config).build(); + CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create() + .config(config); + + return builder().aggregator(aggregator).build(); } /** @@ -185,6 +179,7 @@ private CrossOriginHelper() { } private CrossOriginHelper(Builder builder) { + builder.validate(); isEnabled = builder.isEnabled(); crossOriginConfigs = builder.crossOriginConfigs(); secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; @@ -204,18 +199,14 @@ public static Builder builder() { */ public static class Builder implements io.helidon.common.Builder { - private Optional corsConfigOpt = Optional.empty(); private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; - /** - * Sets the CORS config node (allowed to be missing). - * - * @param corsConfig the CORS config node - * @return updated builder - */ - public Builder config(Config corsConfig) { - corsConfigOpt = Optional.ofNullable(corsConfig.exists() ? corsConfig : null); - return this; + private Optional aggregatorOpt = Optional.empty(); + + void validate() { + if (aggregatorOpt.isEmpty()) { + throw new IllegalStateException("CrossOriginHelper.Builder aggregator must be set but has not been"); + } } /** @@ -230,6 +221,17 @@ public Builder secondaryLookupSupplier(Supplier> sec return this; } + /** + * Sets the aggregator to use for this builder. + * + * @param aggregator the aggregator + * @return updated builder + */ + public Builder aggregator(CrossOriginConfigAggregator aggregator) { + aggregatorOpt = Optional.of(aggregator); + return this; + } + /** * Creates the {@code CrossOriginHelper}. * @@ -244,27 +246,11 @@ public CrossOriginHelper build() { } boolean isEnabled() { - if (corsConfigOpt.isEmpty()) { - return true; - } - Config corsConfig = corsConfigOpt.get(); - if (!corsConfig.exists()) { - return true; - } - Config corsEnabledNode = corsConfig.get(CORS_ENABLED_CONFIG_KEY); - return !corsEnabledNode.exists() || corsEnabledNode.asBoolean().get(); + return aggregatorOpt.isPresent() && aggregatorOpt.get().isEnabled(); } Map crossOriginConfigs() { - AtomicReference> result = new AtomicReference<>(); - corsConfigOpt.ifPresentOrElse(corsConfig -> { - Config pathsNode = corsConfig.get(CORS_PATHS_CONFIG_KEY); - if (pathsNode.exists()) { - result.set(pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()) - .get()); - } - }, () -> result.set(Collections.emptyMap())); - return result.get(); + return aggregatorOpt.isPresent() ? aggregatorOpt.get().crossOriginConfigs() : Collections.emptyMap(); } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java new file mode 100644 index 00000000000..05af8ae6c86 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors.internal; + +import io.helidon.webserver.cors.CORSSupport; + +/** + * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder} for assiging CORS-related + * attributes. + * + * @param the type of the implementing class so the fluid methods can return the correct type + */ +public interface Setter { + /** + * Sets the allowOrigins. + * + * @param origins the origin value(s) + * @return updated builder + */ + T allowOrigins(String... origins); + + /** + * Sets the allow headers. + * + * @param allowHeaders the allow headers value(s) + * @return updated builder + */ + T allowHeaders(String... allowHeaders); + + /** + * Sets the expose headers. + * + * @param exposeHeaders the expose headers value(s) + * @return updated builder + */ + T exposeHeaders(String... exposeHeaders); + + /** + * Sets the allow methods. + * + * @param allowMethods the allow method value(s) + * @return updated builder + */ + T allowMethods(String... allowMethods); + + /** + * Sets the allow credentials flag. + * + * @param allowCredentials the allow credentials flag + * @return updated builder + */ + T allowCredentials(boolean allowCredentials); + + /** + * Sets the maximum age. + * + * @param maxAge the maximum age + * @return updated builder + */ + T maxAge(long maxAge); +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 2f34df028e9..189421f8e38 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -16,7 +16,7 @@ */ /** - * Helidon SE CORS Support. + *

      Helidon SE CORS Support

      *

      * Use {@link io.helidon.webserver.cors.CORSSupport} and its {@link io.helidon.webserver.cors.CORSSupport.Builder} to add CORS * handling to resources in your application. @@ -43,41 +43,14 @@ * The {@code enabled} setting allows configuration to completely disable CORS processing, regardless of other settings in * config or programmatic set-up of CORS in the application. *

      - *

      Using the application default configuration

      - * You can add a {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY} section to your application's default config - * file to define the CORS behavior for your application endpoints. - *
      - *     cors:
      - *       paths:
      - *         - path-prefix: /cors1
      - *           allow-origins: ["*"]
      - *           allow-methods: ["*"]
      - *         - path-prefix: /cors2
      - *           allow-origins: ["http://foo.bar", "http://bar.foo"]
      - *           allow-methods: ["DELETE", "PUT"]
      - *           allow-headers: ["X-bar", "X-foo"]
      - *           allow-credentials: true
      - *           max-age: -1
      - * 
      - * This defines CORS behavior for two paths, {@code /cors1} and {@code /cors2}, within your application's context root. - *

      - * Assuming you have written your application class {@code MyApp} to extend {@link io.helidon.webserver.Service}, the - * following code applies the CORS configuration above to it: - *

      - *
      - *         Routing.Builder builder = Routing.builder()
      - *                 .register("/myapp", CORSSupport.builder()
      - *                                      .config() // uses the {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY}} default application config
      - *                                      .build(),
      - *                                new MyApp());
      - *     
      - * Helidon will perform no CORS processing for any paths in your app other than {@code /cors1} and {@code /cors2}. - *

      Using an explicit configuration object

      - * You can create your own Helidon {@link io.helidon.config.Config} object that contains CORS information and use it instead of - * the application default config. - * The config node you create and give to {@code CORSSupport} should not nest the CORS information inside a - * {@value io.helidon.webserver.cors.internal.CrossOriginHelper#CORS_CONFIG_KEY} section. Instead, it would look like this: + *

      Finding and applying CORS configuration

      + * Although Helidon prescribes the CORS config format, you can put it wherever you want in your application's configuration + * file. Your application code will retrieve the CORS config from its location within your configuration and then use that + * config node with the {@link io.helidon.webserver.cors.CORSSupport.Builder} in preparing CORS support for your app. + * + * If you set up this configuration *
      + *   my-cors:
        *     paths:
        *       - path-prefix: /cors1
        *         allow-origins: ["*"]
      @@ -90,14 +63,13 @@
        *         max-age: -1
        * 
      *

      - * If the above config were stored in a resource in your app called {@code myAppCORS.yaml} then the following code would - * apply it to your app: + * in a resource called {@code myApp.yaml} then the following code would apply it to your app: *

      *
      - *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myAppCORS.yaml")).build();
      + *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build();
        *         Routing.Builder builder = Routing.builder()
        *                 .register("/myapp", CORSSupport.builder()
      - *                                      .config(myAppConfig)
      + *                                      .config(myAppConfig.get("my-cors"))
        *                                      .build(),
        *                                new MyApp());
        *     
      @@ -120,19 +92,23 @@ * Routing.Builder builder = Routing.builder() * .register("/myapp", CORSSupport.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app - * .build(), new MyApp()); + * .build(), + * new MyApp()); * + * Notice that you pass two services to the {@code register} method: the {@code CORSSupport} instance and your app + * instance. Helidon will process requests to the path you specify with those services in that order. + *

      * Invoke {@code addCrossOrigin} multiple times to link more paths with CORS configuration. You can reuse one {@code * CrossOriginConfig} object with more than one path if that meets your needs. + *

      *

      - * The following example shows how you can combine configuration and the API to prepare the {@code CORSSupport.Builder} - * which you could then pass to the {@code register} method. To help with readability as things get more complicated, this - * example saves the {@code CORSSupport.Builder} in a variable rather than constructing it in-line when invoking - * {@code register}: + * The following example shows how you can combine configuration and the API. To help with readability as things get more + * complicated, this example saves the {@code CORSSupport.Builder} in a variable rather than constructing it in-line when + * invoking {@code register}: *

      *
        *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
      - *                  .config(myAppConfig)
      + *                  .config(myAppConfig.get("my-cors"))
        *                  .addCrossOrigin("/cors3", corsFORCORS3);
        *
        *         Routing.Builder builder = Routing.builder()
      diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      index bd09ca6c84e..aa35fbe5f9e 100644
      --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      @@ -31,7 +31,6 @@
       import io.helidon.webserver.WebServer;
       
       import static io.helidon.webserver.cors.CORSTestServices.SERVICE_3;
      -import static io.helidon.webserver.cors.internal.CrossOriginHelper.CORS_CONFIG_KEY;
       
       public class TestUtil {
       
      @@ -46,7 +45,7 @@ static WebServer startupServerWithApps() throws InterruptedException, ExecutionE
           private static WebServer startServer(int port, Routing.Builder routingBuilder) throws InterruptedException,
                   ExecutionException, TimeoutException {
               Config config = Config.create();
      -        ServerConfiguration serverConfig = ServerConfiguration.builder(config)
      +        ServerConfiguration serverConfig = ServerConfiguration.builder(config.get("server"))
                       .port(port)
                       .build();
               return WebServer.create(serverConfig, routingBuilder).start().toCompletableFuture().get(10, TimeUnit.SECONDS);
      @@ -68,15 +67,13 @@ static Routing.Builder prepRouting() {
                * Load a specific config for "/othergreet."
                */
               Config twoCORSConfig = minimalConfig(ConfigSources.classpath("twoCORS.yaml"));
      -        CORSSupport.Builder twoCORSSupportBuilder =
      -                CORSSupport.builder().config(twoCORSConfig.get(CORS_CONFIG_KEY));
       
               Routing.Builder builder = Routing.builder()
                       .register(GREETING_PATH,
      -                          CORSSupport.builder().config().build(), // use "cors" from default app config
      +                          CORSSupport.builder().config(Config.create().get("cors-setup")).build(),
                                 new GreetService())
                       .register(OTHER_GREETING_PATH,
      -                          CORSSupport.create(twoCORSConfig.get(CORS_CONFIG_KEY)), // custom config - get "cors" yourself
      +                          CORSSupport.create(twoCORSConfig.get("cors-2-setup")),
                                 new GreetService("Other Hello"));
       
               return builder;
      diff --git a/webserver/cors/src/test/resources/application.yaml b/webserver/cors/src/test/resources/application.yaml
      index b2bceaa5e26..d8726b9022f 100644
      --- a/webserver/cors/src/test/resources/application.yaml
      +++ b/webserver/cors/src/test/resources/application.yaml
      @@ -20,7 +20,7 @@ client:
         follow-redirects: true
         max-redirects: 8
       
      -cors:
      +cors-setup:
         paths:
           - path-prefix: /cors1
             allow-origins: ["*"]
      diff --git a/webserver/cors/src/test/resources/twoCORS.yaml b/webserver/cors/src/test/resources/twoCORS.yaml
      index 693c6e881d2..38d38b5cbd4 100644
      --- a/webserver/cors/src/test/resources/twoCORS.yaml
      +++ b/webserver/cors/src/test/resources/twoCORS.yaml
      @@ -13,7 +13,7 @@
       # See the License for the specific language governing permissions and
       # limitations under the License.
       #
      -cors:
      +cors-2-setup:
         paths:
           - path-prefix: /cors2
             allow-origins: ["http://otherfoo.bar", "http://otherbar.foo"]
      
      From a50bab1dca3829e7984d27b9b0cf6c90464c6882 Mon Sep 17 00:00:00 2001
      From: "tim.quinn@oracle.com" 
      Date: Sat, 11 Apr 2020 12:41:00 -0500
      Subject: [PATCH 070/100] Minor package-info edits
      
      ---
       .../helidon/webserver/cors/package-info.java  | 31 ++++++++++++-------
       1 file changed, 19 insertions(+), 12 deletions(-)
      
      diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java
      index 189421f8e38..d3f5ea660d1 100644
      --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java
      +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java
      @@ -68,10 +68,11 @@
        *     
        *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build();
        *         Routing.Builder builder = Routing.builder()
      - *                 .register("/myapp", CORSSupport.builder()
      + *                 .register("/myapp",
      + *                           CORSSupport.builder()
        *                                      .config(myAppConfig.get("my-cors"))
        *                                      .build(),
      - *                                new MyApp());
      + *                           new MyApp());
        *     
      *

      The Helidon CORS API

      * You can define your application's CORS behavior programmatically -- together with configuration if you want -- by: @@ -90,10 +91,11 @@ * .build(); * * Routing.Builder builder = Routing.builder() - * .register("/myapp", CORSSupport.builder() + * .register("/myapp", + * CORSSupport.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app * .build(), - * new MyApp()); + * new MyApp()); *
      * Notice that you pass two services to the {@code register} method: the {@code CORSSupport} instance and your app * instance. Helidon will process requests to the path you specify with those services in that order. @@ -112,7 +114,9 @@ * .addCrossOrigin("/cors3", corsFORCORS3); * * Routing.Builder builder = Routing.builder() - * .register("/myapp", corsBuilder.build(), new MyApp()); + * .register("/myapp", + * corsBuilder.build(), + * new MyApp()); * * *

      Convenience API for the "/" path

      @@ -142,7 +146,8 @@ * replies with success: *
      {@code
        *         Routing.Builder builder = Routing.builder()
      - *                 .put("/cors4", CORSSupport.builder()
      + *                 .put("/cors4",
      + *                      CORSSupport.builder()
        *                               .allowOrigins("http://foo.bar", "http://bar.foo")
        *                               .allowMethods("DELETE", "PUT"),
        *                      (req, resp) -> resp.status(Http.Status.OK_200));
      @@ -150,13 +155,15 @@
        * You can do this multiple times and even combine it with service registrations:
        * 
      {@code
        *         Routing.Builder builder = Routing.builder()
      - *                 .put("/cors4", CORSSupport.builder()
      - *                                   .allowOrigins("http://foo.bar", "http://bar.foo")
      - *                                   .allowMethods("DELETE", "PUT"),
      + *                 .put("/cors4",
      + *                      CORSSupport.builder()
      + *                               .allowOrigins("http://foo.bar", "http://bar.foo")
      + *                               .allowMethods("DELETE", "PUT"),
        *                      (req, resp) -> resp.status(Http.Status.OK_200))
      - *                 .get("/cors4", CORSSupport.builder()
      - *                                   .allowOrigins("*")
      - *                                   .minAge(-1),
      + *                 .get("/cors4",
      + *                      CORSSupport.builder()
      + *                               .allowOrigins("*")
      + *                               .minAge(-1),
        *                      (req, resp) -> resp.send("Hello, World!"))
        *                 .register(CORSSupport.fromConfig());
        * }
      From 4277f157cc744f4ab5ae2d39ff8f3af9e56c47d0 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 12 Apr 2020 06:49:38 -0500 Subject: [PATCH 071/100] Add test case for .put, .any, etc. methods on Routing.Rules; delegate look-up of CORS config to the aggregator to encapsulate the details --- .../internal/CrossOriginConfigAggregator.java | 63 ++++++++++--- .../cors/internal/CrossOriginHelper.java | 57 ++++-------- .../helidon/webserver/cors/package-info.java | 33 ++++--- .../cors/TestHandlerRegistration.java | 92 +++++++++++++++++++ .../io/helidon/webserver/cors/TestUtil.java | 15 ++- 5 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java index 9b43915b4a0..e4215e71276 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import io.helidon.config.Config; import io.helidon.webserver.cors.CrossOriginConfig; @@ -30,6 +31,7 @@ */ public class CrossOriginConfigAggregator implements Setter { + private static final String CONVENIENCE_PATH = "*"; // Records paths and configs added via addCrossOriginConfig private final Map crossOriginConfigs = new HashMap<>(); @@ -65,21 +67,6 @@ public boolean isEnabled() { return isEnabledFromConfig.orElse(isEnabledFromAPI); } - /** - * Aggregates the cross origin config from all sources into one. - * - * @return merged path-to-origin map - */ - public Map crossOriginConfigs() { - Map result = new HashMap<>(); - - result.putAll(crossOriginConfigs); - crossOriginConfigBuilderOpt.ifPresent(builder -> result.put("/", builder.build())); - result.putAll(crossOriginConfigsAssembledFromConfigs); - - return result; - } - /** * Add cross-origin information from a {@link Config} node. * @@ -168,6 +155,52 @@ public CrossOriginConfigAggregator maxAge(long maxAge) { return this; } + /** + * Looks for a matching CORS config entry for the specified path among the provided CORS configuration information, returning + * an {@code Optional} of the matching {@code CrossOrigin} instance for the path, if any. + * + * @param path the possibly unnormalized request path to check + * @param secondaryLookup Supplier for CrossOrigin used if none found in config + * @return Optional for the matching config, or an empty Optional if none matched + */ + Optional lookupCrossOrigin(String path, Supplier> secondaryLookup) { + String normalizedPath = normalize(path); + + // Check settings from config first, including wildcard. + if (crossOriginConfigsAssembledFromConfigs.containsKey(normalizedPath)) { + return Optional.of(crossOriginConfigsAssembledFromConfigs.get(normalizedPath)); + } + if (crossOriginConfigsAssembledFromConfigs.containsKey(CONVENIENCE_PATH)) { + return Optional.of(crossOriginConfigsAssembledFromConfigs.get(CONVENIENCE_PATH)); + } + + // Check explicit settings using the CrossOriginConfig methods. + if (crossOriginConfigBuilderOpt.isPresent()) { + return Optional.of(crossOriginConfigBuilderOpt.get().build()); + } + + // Check explicit settings using addCrossOriginConfig. + if (crossOriginConfigs.containsKey(normalizedPath)) { + return Optional.of(crossOriginConfigs.get(normalizedPath)); + } + if (crossOriginConfigs.containsKey(CONVENIENCE_PATH)) { + return Optional.of(crossOriginConfigs.get(CONVENIENCE_PATH)); + } + + return secondaryLookup.get(); + } + + @Override + public String toString() { + return "CrossOriginConfigAggregator{" + + "crossOriginConfigsAssembledFromConfigs=" + crossOriginConfigsAssembledFromConfigs + + ", crossOriginConfigBuilder=" + crossOriginConfigBuilderOpt.map(CrossOriginConfig.Builder::toString).orElse( + "-empty-") + + ", isEnabledFromConfig=" + isEnabledFromConfig + + ", isEnabledFromAPI=" + isEnabledFromAPI + + '}'; + } + private CrossOriginConfig.Builder crossOriginConfigBuilder() { if (crossOriginConfigBuilderOpt.isEmpty()) { crossOriginConfigBuilderOpt = Optional.of(CrossOriginConfig.builder()); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java index 31d6518e8a1..18fd730110f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginHelper.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; @@ -171,7 +170,8 @@ public static CrossOriginHelper create() { } private final boolean isEnabled; - private final Map crossOriginConfigs; + private final CrossOriginConfigAggregator aggregator; +// private final Map crossOriginConfigs; private final Supplier> secondaryCrossOriginLookup; private CrossOriginHelper() { @@ -181,7 +181,7 @@ private CrossOriginHelper() { private CrossOriginHelper(Builder builder) { builder.validate(); isEnabled = builder.isEnabled(); - crossOriginConfigs = builder.crossOriginConfigs(); + aggregator = builder.aggregator(); secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; } @@ -199,13 +199,15 @@ public static Builder builder() { */ public static class Builder implements io.helidon.common.Builder { + private static final String NO_AGGREGATOR_MESSAGE = "CrossOriginHelper.Builder aggregator must be set but has not been"; + private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; private Optional aggregatorOpt = Optional.empty(); void validate() { if (aggregatorOpt.isEmpty()) { - throw new IllegalStateException("CrossOriginHelper.Builder aggregator must be set but has not been"); + throw new IllegalStateException(NO_AGGREGATOR_MESSAGE); } } @@ -232,6 +234,10 @@ public Builder aggregator(CrossOriginConfigAggregator aggregator) { return this; } + CrossOriginConfigAggregator aggregator() { + return aggregatorOpt.orElseThrow(() -> new IllegalStateException(NO_AGGREGATOR_MESSAGE)); + } + /** * Creates the {@code CrossOriginHelper}. * @@ -246,11 +252,7 @@ public CrossOriginHelper build() { } boolean isEnabled() { - return aggregatorOpt.isPresent() && aggregatorOpt.get().isEnabled(); - } - - Map crossOriginConfigs() { - return aggregatorOpt.isPresent() ? aggregatorOpt.get().crossOriginConfigs() : Collections.emptyMap(); + return aggregatorOpt.map(CrossOriginConfigAggregator::isEnabled).orElse(false); } } @@ -260,7 +262,7 @@ Map crossOriginConfigs() { * @return whether the helper will have any effect on requests or responses */ public boolean isActive() { - return isEnabled && !crossOriginConfigs.isEmpty(); + return aggregator.isEnabled(); } /** @@ -288,14 +290,13 @@ public boolean isActive() { */ public Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - if (!isEnabled) { + if (!isActive()) { LOGGER.log(DECISION_LEVEL, () -> String.format("CORS ignoring request %s; processing is disabled", requestAdapter)); requestAdapter.next(); return Optional.empty(); } - Optional crossOrigin = lookupCrossOrigin(requestAdapter.path(), crossOriginConfigs, - secondaryCrossOriginLookup); + Optional crossOrigin = aggregator.lookupCrossOrigin(requestAdapter.path(), secondaryCrossOriginLookup); RequestType requestType = requestType(requestAdapter); @@ -314,8 +315,8 @@ public Optional processRequest(RequestAdapter requestAdapter, Respo @Override public String toString() { - return String.format("CrossOriginHelper{isEnabled=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", - isEnabled, crossOriginConfigs, secondaryCrossOriginLookup == EMPTY_SECONDARY_SUPPLIER ? "(not set)" : "(set)"); + return String.format("CrossOriginHelper{isActive=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", + isActive(), aggregator, secondaryCrossOriginLookup == EMPTY_SECONDARY_SUPPLIER ? "(not set)" : "(set)"); } static Optional processRequest(RequestType requestType, CrossOriginConfig crossOrigin, @@ -354,7 +355,7 @@ static Optional processRequest(RequestType requestType, CrossOriginCon */ public void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - if (!isEnabled) { + if (!isActive()) { LOGGER.log(DECISION_LEVEL, () -> String.format("CORS ignoring request %s; CORS processing is dieabled", requestAdapter)); return; @@ -363,10 +364,9 @@ public void prepareResponse(RequestAdapter requestAdapter, ResponseAda RequestType requestType = requestType(requestAdapter, true); // silent: already logged during req processing if (requestType == RequestType.CORS) { - CrossOriginConfig crossOrigin = lookupCrossOrigin( + CrossOriginConfig crossOrigin = aggregator.lookupCrossOrigin( requestAdapter.path(), - crossOriginConfigs, - secondaryCrossOriginLookup) + secondaryCrossOriginLookup) .orElseThrow(() -> new IllegalArgumentException( "Could not locate expected CORS information while preparing response to request " + requestAdapter)); addCORSHeadersToResponse(crossOrigin, requestAdapter, responseAdapter); @@ -571,25 +571,6 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, return responseAdapter.ok(); } - /** - * Looks for a matching CORS config entry for the specified path among the provided CORS configuration information, returning - * an {@code Optional} of the matching {@code CrossOrigin} instance for the path, if any. - * - * @param path the possibly unnormalized request path to check - * @param crossOriginConfigs CORS configuration - * @param secondaryLookup Supplier for CrossOrigin used if none found in config - * @return Optional for the matching config, or an empty Optional if none matched - */ - static Optional lookupCrossOrigin(String path, Map crossOriginConfigs, - Supplier> secondaryLookup) { - String normalizedPath = normalize(path); - if (crossOriginConfigs.containsKey(normalizedPath)) { - return Optional.of(crossOriginConfigs.get(normalizedPath)); - } - - return secondaryLookup.get(); - } - /** * Formats an array as a comma-separate list without brackets. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index d3f5ea660d1..5ff79520989 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -141,25 +141,36 @@ *

      {@code CORSSupport} as a handler

      * The previous examples use a {@code CORSSupport} instance as a Helidon {@link io.helidon.webserver.Service} which you can * register with the routing rules. You can also use a {@code CORSSupport} object as a {@link io.helidon.webserver.Handler} in - * setting up the routing rules for an HTTP method and path. The next example sets up CORS processing for the {@code PUT} - * HTTP method on the {@code /cors4} path within the app. The application code simply accepts the request graciously and - * replies with success: + * setting up the routing rules for an HTTP method and path. The next example sets up CORS processing for the {@code PUT} and + * {@code OPTIONS} HTTP methods on the {@code /cors4} path within the app. The application code for both simply accepts the + * request graciously and replies with success: *
      {@code
      + *         CORSSupport cors4Support = CORSSupport.builder()
      + *                 .allowOrigins("http://foo.bar", "http://bar.foo")
      + *                 .allowMethods("PUT")
      + *                 .build();
        *         Routing.Builder builder = Routing.builder()
        *                 .put("/cors4",
      - *                      CORSSupport.builder()
      - *                               .allowOrigins("http://foo.bar", "http://bar.foo")
      - *                               .allowMethods("DELETE", "PUT"),
      - *                      (req, resp) -> resp.status(Http.Status.OK_200));
      + *                      cors4Support,
      + *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
      + *                 .options("/cors4",
      + *                      cors4Support,
      + *                      (req, resp) -> resp.status(Http.Status.OK_200).send());
        * }
      + * Remember that the CORS protocol uses the {@code OPTIONS} HTTP method for preflight requests. If you use the handler-based + * methods on {@code Routing.Builder} be sure to invoke the {@code options} method as well to set up routing for {@code OPTIONS} + * requests. You could invoke the {@code any} method as a short-cut. + *

      * You can do this multiple times and even combine it with service registrations: + *

      *
      {@code
        *         Routing.Builder builder = Routing.builder()
        *                 .put("/cors4",
      - *                      CORSSupport.builder()
      - *                               .allowOrigins("http://foo.bar", "http://bar.foo")
      - *                               .allowMethods("DELETE", "PUT"),
      - *                      (req, resp) -> resp.status(Http.Status.OK_200))
      + *                      cors4Support,
      + *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
      + *                 .options("/cors4",
      + *                      cors4Support,
      + *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
        *                 .get("/cors4",
        *                      CORSSupport.builder()
        *                               .allowOrigins("*")
      diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java
      new file mode 100644
      index 00000000000..01354222912
      --- /dev/null
      +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java
      @@ -0,0 +1,92 @@
      +/*
      + * Copyright (c) 2020 Oracle and/or its affiliates.
      + *
      + * 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 io.helidon.webserver.cors;
      +
      +import io.helidon.common.http.Headers;
      +import io.helidon.common.http.Http;
      +import io.helidon.webclient.WebClient;
      +import io.helidon.webclient.WebClientRequestBuilder;
      +import io.helidon.webclient.WebClientResponse;
      +import io.helidon.webserver.WebServer;
      +import org.junit.jupiter.api.AfterAll;
      +import org.junit.jupiter.api.BeforeAll;
      +import org.junit.jupiter.api.BeforeEach;
      +import org.junit.jupiter.api.Test;
      +
      +import java.util.concurrent.ExecutionException;
      +import java.util.concurrent.TimeoutException;
      +
      +import static io.helidon.common.http.Http.Header.ORIGIN;
      +import static io.helidon.webserver.cors.CORSTestServices.SERVICE_1;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS;
      +import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD;
      +import static io.helidon.webserver.cors.CustomMatchers.present;
      +import static io.helidon.webserver.cors.TestUtil.path;
      +import static org.hamcrest.CoreMatchers.containsString;
      +import static org.hamcrest.MatcherAssert.assertThat;
      +import static org.hamcrest.Matchers.is;
      +
      +public class TestHandlerRegistration {
      +
      +    static final String CORS4_CONTEXT_ROOT = "/cors4";
      +
      +    private static WebServer server;
      +    private WebClient client;
      +
      +
      +    @BeforeAll
      +    public static void startup() throws InterruptedException, ExecutionException, TimeoutException {
      +        server = TestUtil.startupServerWithApps();
      +    }
      +
      +    @BeforeEach
      +    public void startupClient() {
      +        client = TestUtil.startupClient(server);
      +    }
      +
      +    @AfterAll
      +    public static void shutdown() {
      +        TestUtil.shutdownServer(server);
      +    }
      +
      +        @Test
      +    void test4PreFlightAllowedHeaders2() throws ExecutionException, InterruptedException {
      +        WebClientRequestBuilder reqBuilder = client
      +                .method(Http.Method.OPTIONS.name())
      +                .path(CORS4_CONTEXT_ROOT);
      +
      +        Headers headers = reqBuilder.headers();
      +        headers.add(ORIGIN, "http://foo.bar");
      +        headers.add(ACCESS_CONTROL_REQUEST_METHOD, "PUT");
      +        headers.add(ACCESS_CONTROL_REQUEST_HEADERS, "X-foo, X-bar");
      +
      +        WebClientResponse res = reqBuilder
      +                .request()
      +                .toCompletableFuture()
      +                .get();
      +
      +        assertThat(res.status(), is(Http.Status.OK_200));
      +        assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_ORIGIN), present(is("http://foo.bar")));
      +        assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_METHODS), present(is("PUT")));
      +        assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-foo")));
      +        assertThat(res.headers().first(ACCESS_CONTROL_ALLOW_HEADERS), present(containsString("X-bar")));
      +    }
      +}
      diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      index aa35fbe5f9e..d7ca0b23092 100644
      --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java
      @@ -21,6 +21,7 @@
       import java.util.concurrent.TimeoutException;
       import java.util.function.Supplier;
       
      +import io.helidon.common.http.Http;
       import io.helidon.config.Config;
       import io.helidon.config.ConfigSources;
       import io.helidon.config.spi.ConfigSource;
      @@ -74,7 +75,19 @@ static Routing.Builder prepRouting() {
                                 new GreetService())
                       .register(OTHER_GREETING_PATH,
                                 CORSSupport.create(twoCORSConfig.get("cors-2-setup")),
      -                          new GreetService("Other Hello"));
      +                          new GreetService("Other Hello"))
      +                .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT,
      +                        CORSSupport.builder()
      +                                .allowOrigins("http://foo.bar", "http://bar.foo")
      +                                .allowMethods("PUT")
      +                                .build(),
      +                        (req, resp) -> resp.status(Http.Status.OK_200).send())
      +                .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT,
      +                        CORSSupport.builder()
      +                                .allowOrigins("*")
      +                                .allowMethods("GET")
      +                                .build(),
      +                        (req, resp) -> resp.status(Http.Status.OK_200).send());
       
               return builder;
           }
      
      From c790abc9da505bb6926df28566e25b8ac7d46435 Mon Sep 17 00:00:00 2001
      From: "tim.quinn@oracle.com" 
      Date: Sun, 12 Apr 2020 08:26:11 -0500
      Subject: [PATCH 072/100] Create a PathMatcher and store it with each added
       CrossOriginConfig, then use the matcher in finding matches on request paths
      
      ---
       .../internal/CrossOriginConfigAggregator.java | 90 +++++++++++++------
       1 file changed, 62 insertions(+), 28 deletions(-)
      
      diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java
      index e4215e71276..9facace2592 100644
      --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java
      +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java
      @@ -16,12 +16,13 @@
        */
       package io.helidon.webserver.cors.internal;
       
      -import java.util.HashMap;
      +import java.util.LinkedHashMap;
       import java.util.Map;
       import java.util.Optional;
       import java.util.function.Supplier;
       
       import io.helidon.config.Config;
      +import io.helidon.webserver.PathMatcher;
       import io.helidon.webserver.cors.CrossOriginConfig;
       
       import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize;
      @@ -33,10 +34,10 @@ public class CrossOriginConfigAggregator implements Setter crossOriginConfigs = new HashMap<>();
      +    private final Map crossOriginConfigMatchables = new LinkedHashMap<>();
       
           // Records the merged paths and configs added via the config method
      -    private final Map crossOriginConfigsAssembledFromConfigs = new HashMap<>();
      +    private final Map crossOriginConfigsAssembledFromConfigs = new LinkedHashMap<>();
       
           // Accumulates config via the setter methods from CrossOriginConfig
           private Optional crossOriginConfigBuilderOpt = Optional.empty();
      @@ -89,22 +90,27 @@ public CrossOriginConfigAggregator config(Config config) {
                   }
                   Config pathsNode = config.get(CrossOriginConfig.CORS_PATHS_CONFIG_KEY);
                   if (pathsNode.exists()) {
      -                crossOriginConfigsAssembledFromConfigs.putAll(pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper())
      -                        .get());
      +                pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper())
      +                        .get()
      +                        .entrySet()
      +                        .stream()
      +                        .forEach(entry -> crossOriginConfigsAssembledFromConfigs.put(entry.getKey(),
      +                                new CrossOriginConfigMatchable(entry.getKey(), entry.getValue())));
      +
                   }
               }
               return this;
           }
       
           /**
      -     * Adds cross origin information associated with a given path.
      +     * Adds cross origin information associated with a given pathExpr.
            *
      -     * @param path the path to which the cross origin information applies
      +     * @param pathExpr the pathExpr to which the cross origin information applies
            * @param crossOrigin the cross origin information
            * @return updated builder
            */
      -    public CrossOriginConfigAggregator addCrossOrigin(String path, CrossOriginConfig crossOrigin) {
      -        crossOriginConfigs.put(normalize(path), crossOrigin);
      +    public CrossOriginConfigAggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) {
      +        crossOriginConfigMatchables.put(normalize(pathExpr), new CrossOriginConfigMatchable(pathExpr, crossOrigin));
               return this;
           }
       
      @@ -164,30 +170,36 @@ public CrossOriginConfigAggregator maxAge(long maxAge) {
            * @return Optional for the matching config, or an empty Optional if none matched
            */
           Optional lookupCrossOrigin(String path, Supplier> secondaryLookup) {
      +
      +        Optional result = Optional.empty();
               String normalizedPath = normalize(path);
       
      -        // Check settings from config first, including wildcard.
      -        if (crossOriginConfigsAssembledFromConfigs.containsKey(normalizedPath)) {
      -            return Optional.of(crossOriginConfigsAssembledFromConfigs.get(normalizedPath));
      -        }
      -        if (crossOriginConfigsAssembledFromConfigs.containsKey(CONVENIENCE_PATH)) {
      -            return Optional.of(crossOriginConfigsAssembledFromConfigs.get(CONVENIENCE_PATH));
      -        }
      +        result = findFirst(crossOriginConfigsAssembledFromConfigs, normalizedPath)
      +                .or(() -> crossOriginConfigBuilderOpt
      +                            .map(CrossOriginConfig.Builder::build))
      +                .or(() -> findFirst(crossOriginConfigMatchables, normalizedPath))
      +                .or(secondaryLookup);
       
      -        // Check explicit settings using the CrossOriginConfig methods.
      -        if (crossOriginConfigBuilderOpt.isPresent()) {
      -            return Optional.of(crossOriginConfigBuilderOpt.get().build());
      -        }
      +        return result;
       
      -        // Check explicit settings using addCrossOriginConfig.
      -        if (crossOriginConfigs.containsKey(normalizedPath)) {
      -            return Optional.of(crossOriginConfigs.get(normalizedPath));
      -        }
      -        if (crossOriginConfigs.containsKey(CONVENIENCE_PATH)) {
      -            return Optional.of(crossOriginConfigs.get(CONVENIENCE_PATH));
      -        }
       
      -        return secondaryLookup.get();
      +    }
      +
      +    /**
      +     * Given a map from path expressions to matchables, finds the first map entry with a path matcher that accepts the provided
      +     * path.
      +     *
      +     * @param matchables map from pathExpressions to matchables
      +     * @param normalizedPath normalized path (from the request) to be matched
      +     * @return Optional of the CrossOriginConfig
      +     */
      +    private static Optional findFirst(Map matchables,
      +            String normalizedPath) {
      +        return matchables.values().stream()
      +                .filter(matchable -> matchable.matches(normalizedPath))
      +                .map(CrossOriginConfigMatchable::get)
      +                .findFirst();
      +
           }
       
           @Override
      @@ -208,4 +220,26 @@ private CrossOriginConfig.Builder crossOriginConfigBuilder() {
               return crossOriginConfigBuilderOpt.get();
           }
       
      +    /**
      +     * A composite of a {@code CrossOriginConfig} with a {@link PathMatcher} that processes the path expression with which the
      +     * {@code CrossOriginConfig} was added.
      +     */
      +    private static class CrossOriginConfigMatchable {
      +        private final CrossOriginConfig crossOriginConfig;
      +        private final PathMatcher matcher;
      +
      +        CrossOriginConfigMatchable(String pathExpr, CrossOriginConfig crossOriginConfig) {
      +            this.crossOriginConfig = crossOriginConfig;
      +            matcher = PathMatcher.create(pathExpr);
      +        }
      +
      +        boolean matches(String path) {
      +            return matcher.match(path).matches();
      +        }
      +
      +        CrossOriginConfig get() {
      +            return crossOriginConfig;
      +        }
      +    }
      +
       }
      
      From dd21f679209de2b94f734a90aac87487002f36b2 Mon Sep 17 00:00:00 2001
      From: "tim.quinn@oracle.com" 
      Date: Sun, 12 Apr 2020 18:06:20 -0500
      Subject: [PATCH 073/100] Remove internal subpackage; parameterize CORSSupport
       with types of request and response which the adapters wrap
      
      ---
       .../microprofile/cors/CrossOriginFilter.java  |  18 +-
       .../helidon/webserver/cors/CORSSupport.java   | 104 ++++++++----
       .../webserver/cors/CrossOriginConfig.java     |   5 +-
       .../CrossOriginConfigAggregator.java          |  13 +-
       .../{internal => }/CrossOriginHelper.java     | 154 ++----------------
       .../cors/{internal => }/LogHelper.java        |   7 +-
       .../webserver/cors/RequestAdapter.java        |  79 +++++++++
       .../webserver/cors/ResponseAdapter.java       |  67 ++++++++
       .../webserver/cors/SERequestAdapter.java      |   5 +-
       .../webserver/cors/SEResponseAdapter.java     |   9 +-
       .../webserver/cors/{internal => }/Setter.java |   4 +-
       .../webserver/cors/internal/package-info.java |  20 ---
       webserver/cors/src/main/java/module-info.java |   1 -
       13 files changed, 259 insertions(+), 227 deletions(-)
       rename webserver/cors/src/main/java/io/helidon/webserver/cors/{internal => }/CrossOriginConfigAggregator.java (95%)
       rename webserver/cors/src/main/java/io/helidon/webserver/cors/{internal => }/CrossOriginHelper.java (84%)
       rename webserver/cors/src/main/java/io/helidon/webserver/cors/{internal => }/LogHelper.java (95%)
       create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java
       create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java
       rename webserver/cors/src/main/java/io/helidon/webserver/cors/{internal => }/Setter.java (96%)
       delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java
      
      diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
      index a7066016a51..15d72d5f23e 100644
      --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
      +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java
      @@ -39,12 +39,11 @@
       import io.helidon.common.HelidonFeatures;
       import io.helidon.common.HelidonFlavor;
       import io.helidon.config.Config;
      +import io.helidon.webserver.cors.CORSSupport;
       import io.helidon.webserver.cors.CrossOriginConfig;
      -import io.helidon.webserver.cors.internal.CrossOriginConfigAggregator;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter;
       
      +import io.helidon.webserver.cors.RequestAdapter;
      +import io.helidon.webserver.cors.ResponseAdapter;
       import org.eclipse.microprofile.config.ConfigProvider;
       
       /**
      @@ -65,26 +64,25 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt
           @Context
           private ResourceInfo resourceInfo;
       
      -    private final CrossOriginHelper corsHelper;
      +    private final CORSSupport cors;
       
           CrossOriginFilter() {
               Config config = (Config) ConfigProvider.getConfig();
      -        corsHelper = CrossOriginHelper.builder()
      -                .aggregator(CrossOriginConfigAggregator.create()
      -                                .config(config.get(CORS_CONFIG_KEY)))
      +        CORSSupport.Builder b = CORSSupport.builder();
      +        cors = b.config(config.get(CORS_CONFIG_KEY))
                       .secondaryLookupSupplier(crossOriginFromAnnotationSupplier())
                       .build();
           }
       
           @Override
           public void filter(ContainerRequestContext requestContext) {
      -        Optional response = corsHelper.processRequest(new MPRequestAdapter(requestContext), new MPResponseAdapter());
      +        Optional response = cors.processRequest(new MPRequestAdapter(requestContext), new MPResponseAdapter());
               response.ifPresent(requestContext::abortWith);
           }
       
           @Override
           public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
      -        corsHelper.prepareResponse(new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext));
      +        cors.prepareResponse(new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext));
           }
       
           static class MPRequestAdapter implements RequestAdapter {
      diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java
      index a90fbf2811e..1bd904afe58 100644
      --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java
      +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java
      @@ -17,6 +17,7 @@
       package io.helidon.webserver.cors;
       
       import java.util.Optional;
      +import java.util.function.Supplier;
       
       import io.helidon.config.Config;
       import io.helidon.webserver.Handler;
      @@ -24,11 +25,6 @@
       import io.helidon.webserver.ServerRequest;
       import io.helidon.webserver.ServerResponse;
       import io.helidon.webserver.Service;
      -import io.helidon.webserver.cors.internal.CrossOriginConfigAggregator;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter;
      -import io.helidon.webserver.cors.internal.CrossOriginHelper.ResponseAdapter;
      -import io.helidon.webserver.cors.internal.Setter;
       
       /**
        * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon
      @@ -50,33 +46,42 @@
        *     If none of these sources is used, the {@code CORSSupport} applies defaults as described for
        *     {@link CrossOriginConfig}.
        * 

      + * + * @param type wrapped by RequestAdapter + * @param type wrapped by ResponseAdapter + * */ -public class CORSSupport implements Service, Handler { +public class CORSSupport implements Service, Handler { private final CrossOriginHelper helper; - private CORSSupport(Builder builder) { - CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder().aggregator(builder.aggregator); - helper = helperBuilder.build(); + private CORSSupport(Builder builder) { + helper = builder.helperBuilder.build(); } /** * Creates a {@code CORSSupport} which supports the default CORS set-up. * + * @param type of request wrapped by the request adapter + * @param type of response wrapper by the response adapter * @return the service */ - public static CORSSupport create() { - return builder().build(); + public static CORSSupport create() { + Builder b = builder(); + return b.build(); } /** * Returns a {@code CORSSupport} set up using the supplied {@link Config} node. * * @param config the config node containing CORS information + * @param type of request wrapped by the request adapter + * @param type of response wrapper by the response adapter * @return the initialized service */ - public static CORSSupport create(Config config) { - return builder().config(config).build(); + public static CORSSupport create(Config config) { + Builder b = builder(); + return b.config(config).build(); } /** @@ -84,8 +89,8 @@ public static CORSSupport create(Config config) { * * @return the builder */ - public static Builder builder() { - return new Builder(); + public static Builder builder() { + return new Builder<>(); } /** @@ -94,8 +99,9 @@ public static Builder builder() { * @param config node containing CORS information * @return builder initialized with the CORS set-up from the config */ - public static Builder builder(Config config) { - return builder().config(config); + public static Builder builder(Config config) { + Builder b = builder(); + return b.config(config); } @Override @@ -119,6 +125,28 @@ public void accept(ServerRequest request, ServerResponse response) { responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, responseAdapter)); } + /** + * Not for developer use. Submits a request adapter and response adapter for CORS processing. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the response + * @return Optional of the response type U; present if the response should be returned, empty if request processing should + * continue + */ + public Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + return helper.processRequest(requestAdapter, responseAdapter); + } + + /** + * Not for developer user. Gets a response ready to participate in the CORS protocol. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the reseponse + */ + public void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + helper.prepareResponse(requestAdapter, responseAdapter); + } + private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { helper.prepareResponse(requestAdapter, responseAdapter); @@ -129,16 +157,18 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques /** * Builder for {@code CORSSupport} instances. */ - public static class Builder implements io.helidon.common.Builder, Setter { + public static class Builder implements io.helidon.common.Builder>, + Setter> { - private final CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create(); + private final CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); + private final CrossOriginConfigAggregator aggregator = helperBuilder.aggregator(); - Builder() { + private Builder() { } @Override - public CORSSupport build() { - return new CORSSupport(this); + public CORSSupport build() { + return new CORSSupport<>(this); } /** @@ -148,7 +178,7 @@ public CORSSupport build() { * @param config the CORS config * @return the updated builder */ - public Builder config(Config config) { + public Builder config(Config config) { aggregator.config(config); return this; } @@ -159,7 +189,7 @@ public Builder config(Config config) { * @param value whether to use CORS support * @return updated builder */ - public Builder enabled(boolean value) { + public Builder enabled(boolean value) { aggregator.enabled(value); return this; } @@ -171,45 +201,57 @@ public Builder enabled(boolean value) { * @param crossOrigin the cross origin information * @return updated builder */ - public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { aggregator.addCrossOrigin(path, crossOrigin); return this; } @Override - public Builder allowOrigins(String... origins) { + public Builder allowOrigins(String... origins) { aggregator.allowOrigins(origins); return this; } @Override - public Builder allowHeaders(String... allowHeaders) { + public Builder allowHeaders(String... allowHeaders) { aggregator.allowHeaders(allowHeaders); return this; } @Override - public Builder exposeHeaders(String... exposeHeaders) { + public Builder exposeHeaders(String... exposeHeaders) { aggregator.exposeHeaders(exposeHeaders); return this; } @Override - public Builder allowMethods(String... allowMethods) { + public Builder allowMethods(String... allowMethods) { aggregator.allowMethods(allowMethods); return this; } @Override - public Builder allowCredentials(boolean allowCredentials) { + public Builder allowCredentials(boolean allowCredentials) { aggregator.allowCredentials(allowCredentials); return this; } @Override - public Builder maxAge(long maxAge) { + public Builder maxAge(long maxAge) { aggregator.maxAge(maxAge); return this; } + + /** + * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during + * look-up for a given request, none is found from the aggregator. + * + * @param secondaryLookupSupplier supplier of a CrossOriginConfig + * @return updated builder + */ + public Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { + helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); + return this; + } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index d3b2f0832ca..bdf5d8b1812 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -22,10 +22,9 @@ import java.util.function.Function; import io.helidon.config.Config; -import io.helidon.webserver.cors.internal.Setter; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.parseHeader; +import static io.helidon.webserver.cors.CrossOriginHelper.normalize; +import static io.helidon.webserver.cors.CrossOriginHelper.parseHeader; /** * Represents information about cross origin request sharing. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java similarity index 95% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java index 9facace2592..cd998952052 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/CrossOriginConfigAggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors.internal; +package io.helidon.webserver.cors; import java.util.LinkedHashMap; import java.util.Map; @@ -23,16 +23,15 @@ import io.helidon.config.Config; import io.helidon.webserver.PathMatcher; -import io.helidon.webserver.cors.CrossOriginConfig; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.normalize; +import static io.helidon.webserver.cors.CrossOriginHelper.normalize; /** - * Not for developer use. Collects CORS set-up information from various sources. + * Not for developer use. Collects CORS set-up information from various sources and looks up the relevant CORS + * information given a request's path. */ -public class CrossOriginConfigAggregator implements Setter { +class CrossOriginConfigAggregator implements Setter { - private static final String CONVENIENCE_PATH = "*"; // Records paths and configs added via addCrossOriginConfig private final Map crossOriginConfigMatchables = new LinkedHashMap<>(); @@ -52,7 +51,7 @@ public class CrossOriginConfigAggregator implements SetterNot for use by developers. @@ -59,7 +58,7 @@ * specific to the needs of CORS support. *

      */ -public class CrossOriginHelper { +class CrossOriginHelper { /** * Key for the node within the CORS config indicating whether CORS support is enabled. @@ -154,10 +153,7 @@ public enum RequestType { * @return new instance based on the config */ public static CrossOriginHelper create(Config config) { - CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create() - .config(config); - - return builder().aggregator(aggregator).build(); + return builder().config(config).build(); } /** @@ -169,9 +165,7 @@ public static CrossOriginHelper create() { return builder().build(); } - private final boolean isEnabled; private final CrossOriginConfigAggregator aggregator; -// private final Map crossOriginConfigs; private final Supplier> secondaryCrossOriginLookup; private CrossOriginHelper() { @@ -179,9 +173,7 @@ private CrossOriginHelper() { } private CrossOriginHelper(Builder builder) { - builder.validate(); - isEnabled = builder.isEnabled(); - aggregator = builder.aggregator(); + aggregator = builder.aggregator; secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; } @@ -199,17 +191,9 @@ public static Builder builder() { */ public static class Builder implements io.helidon.common.Builder { - private static final String NO_AGGREGATOR_MESSAGE = "CrossOriginHelper.Builder aggregator must be set but has not been"; - private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; - private Optional aggregatorOpt = Optional.empty(); - - void validate() { - if (aggregatorOpt.isEmpty()) { - throw new IllegalStateException(NO_AGGREGATOR_MESSAGE); - } - } + private final CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create(); /** * Sets the supplier for the secondary lookup of CORS information (typically not contained in @@ -224,20 +208,16 @@ public Builder secondaryLookupSupplier(Supplier> sec } /** - * Sets the aggregator to use for this builder. + * Adds cross-origin information via config. * - * @param aggregator the aggregator + * @param config config node containing CORS set-up information * @return updated builder */ - public Builder aggregator(CrossOriginConfigAggregator aggregator) { - aggregatorOpt = Optional.of(aggregator); + public Builder config(Config config) { + aggregator.config(config); return this; } - CrossOriginConfigAggregator aggregator() { - return aggregatorOpt.orElseThrow(() -> new IllegalStateException(NO_AGGREGATOR_MESSAGE)); - } - /** * Creates the {@code CrossOriginHelper}. * @@ -251,8 +231,8 @@ public CrossOriginHelper build() { return result; } - boolean isEnabled() { - return aggregatorOpt.map(CrossOriginConfigAggregator::isEnabled).orElse(false); + CrossOriginConfigAggregator aggregator() { + return aggregator; } } @@ -659,112 +639,4 @@ private static U forbid(RequestAdapter requestAdapter, ResponseAdapter return responseAdapter.forbidden(publicReason); } - /** - * Not for use by developers. - * - * Minimal abstraction of an HTTP request. - * - * @param type of the request wrapped by the adapter - */ - public interface RequestAdapter { - - /** - * - * @return possibly unnormalized path from the request - */ - String path(); - - /** - * Retrieves the first value for the specified header as a String. - * - * @param key header name to retrieve - * @return the first header value for the key - */ - Optional firstHeader(String key); - - /** - * Reports whether the specified header exists. - * - * @param key header name to check for - * @return whether the header exists among the request's headers - */ - boolean headerContainsKey(String key); - - /** - * Retrieves all header values for a given key as Strings. - * - * @param key header name to retrieve - * @return header values for the header; empty list if none - */ - List allHeaders(String key); - - /** - * Reports the method name for the request. - * - * @return the method name - */ - String method(); - - /** - * Processes the next handler/filter/request processor in the chain. - */ - void next(); - - /** - * Returns the request this adapter wraps. - * - * @return the request - */ - T request(); - } - - /** - * Not for use by developers. - * - * Minimal abstraction of an HTTP response. - * - *

      - * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} - * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the - * actual response. - *

      - * - * @param the type of the response wrapped by the adapter - */ - public interface ResponseAdapter { - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, String value); - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, Object value); - - /** - * Returns a response with the forbidden status and the specified error message, without any headers assigned - * using the {@code header} methods. - * - * @param message error message to use in setting the response status - * @return the factory - */ - T forbidden(String message); - - /** - * Returns a response with only the headers that were set on this adapter and the status set to OK. - * - * @return response instance - */ - T ok(); - } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java similarity index 95% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index c9a7a032b07..e418a19af31 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors.internal; +package io.helidon.webserver.cors; import java.util.AbstractMap; import java.util.ArrayList; @@ -25,13 +25,12 @@ import java.util.logging.Level; import io.helidon.common.http.Http; -import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestAdapter; -import io.helidon.webserver.cors.internal.CrossOriginHelper.RequestType; +import io.helidon.webserver.cors.CrossOriginHelper.RequestType; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.webserver.cors.internal.CrossOriginHelper.LOGGER; +import static io.helidon.webserver.cors.CrossOriginHelper.LOGGER; class LogHelper { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java new file mode 100644 index 00000000000..c54ae40fd9f --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +import java.util.List; +import java.util.Optional; + +/** + * Not for use by developers. + * + * Minimal abstraction of an HTTP request. + * + * @param type of the request wrapped by the adapter + */ +public interface RequestAdapter { + + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + + /** + * Processes the next handler/filter/request processor in the chain. + */ + void next(); + + /** + * Returns the request this adapter wraps. + * + * @return the request + */ + T request(); +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java new file mode 100644 index 00000000000..c46ac10b46c --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +/** + * Not for use by developers. + * + * Minimal abstraction of an HTTP response. + * + *

      + * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

      + * + * @param the type of the response wrapped by the adapter + */ +public interface ResponseAdapter { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, Object value); + + /** + * Returns a response with the forbidden status and the specified error message, without any headers assigned + * using the {@code header} methods. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns a response with only the headers that were set on this adapter and the status set to OK. + * + * @return response instance + */ + T ok(); +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index 3b418b7208f..c4a5c5ef66a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -20,12 +20,11 @@ import java.util.Optional; import io.helidon.webserver.ServerRequest; -import io.helidon.webserver.cors.internal.CrossOriginHelper; /** - * Helidon SE implementation of {@link CrossOriginHelper.RequestAdapter}. + * Helidon SE implementation of {@link RequestAdapter}. */ -class SERequestAdapter implements CrossOriginHelper.RequestAdapter { +class SERequestAdapter implements RequestAdapter { private final ServerRequest request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index 3550d7b8e25..13702d2dcf9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -18,12 +18,11 @@ import io.helidon.common.http.Http; import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.cors.internal.CrossOriginHelper; /** - * SE implementation of {@link CrossOriginHelper.ResponseAdapter}. + * SE implementation of {@link ResponseAdapter}. */ -class SEResponseAdapter implements CrossOriginHelper.ResponseAdapter { +class SEResponseAdapter implements ResponseAdapter { private final ServerResponse serverResponse; @@ -32,13 +31,13 @@ class SEResponseAdapter implements CrossOriginHelper.ResponseAdapter header(String key, String value) { + public ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public CrossOriginHelper.ResponseAdapter header(String key, Object value) { + public ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java similarity index 96% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java index 05af8ae6c86..4d9d739f3da 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/Setter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors.internal; +package io.helidon.webserver.cors; import io.helidon.webserver.cors.CORSSupport; @@ -24,7 +24,7 @@ * * @param the type of the implementing class so the fluid methods can return the correct type */ -public interface Setter { +interface Setter { /** * Sets the allowOrigins. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java deleted file mode 100644 index 72083662285..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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. - * - */ -/** - * Elements reserved for internal use. - */ -package io.helidon.webserver.cors.internal; diff --git a/webserver/cors/src/main/java/module-info.java b/webserver/cors/src/main/java/module-info.java index e7a86743ef7..482b698d841 100644 --- a/webserver/cors/src/main/java/module-info.java +++ b/webserver/cors/src/main/java/module-info.java @@ -26,5 +26,4 @@ requires io.helidon.webserver; exports io.helidon.webserver.cors; - exports io.helidon.webserver.cors.internal to io.helidon.microprofile.cors; } From e3f456d06e54418aca03dfd3e70102554ff07c42 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 12 Apr 2020 18:08:19 -0500 Subject: [PATCH 074/100] Fix a couple javadoc issues --- .../main/java/io/helidon/webserver/cors/CORSSupport.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 1bd904afe58..b175da0ebd2 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -63,7 +63,7 @@ private CORSSupport(Builder builder) { * Creates a {@code CORSSupport} which supports the default CORS set-up. * * @param type of request wrapped by the request adapter - * @param type of response wrapper by the response adapter + * @param type of response wrapped by the response adapter * @return the service */ public static CORSSupport create() { @@ -76,7 +76,7 @@ public static CORSSupport create() { * * @param config the config node containing CORS information * @param type of request wrapped by the request adapter - * @param type of response wrapper by the response adapter + * @param type of response wrapped by the response adapter * @return the initialized service */ public static CORSSupport create(Config config) { @@ -87,6 +87,8 @@ public static CORSSupport create(Config config) { /** * Creates a {@code Builder} for assembling a {@code CORSSupport}. * + * @param type of request wrapped by the request adapter + * @param type of response wrapped by the response adapter * @return the builder */ public static Builder builder() { @@ -98,6 +100,8 @@ public static Builder builder() { * * @param config node containing CORS information * @return builder initialized with the CORS set-up from the config + * @param type of request wrapped by the request adapter + * @param type of response wrapped by the response adapter */ public static Builder builder(Config config) { Builder b = builder(); From 729084583f3a79461fc56a50d3c9a4edb3b80e43 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 12 Apr 2020 18:14:59 -0500 Subject: [PATCH 075/100] Fix style check errors --- .../src/main/java/io/helidon/webserver/cors/CORSSupport.java | 3 +++ .../cors/src/main/java/io/helidon/webserver/cors/Setter.java | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index b175da0ebd2..2e30fbd9a57 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -160,6 +160,9 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques /** * Builder for {@code CORSSupport} instances. + * + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter */ public static class Builder implements io.helidon.common.Builder>, Setter> { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java index 4d9d739f3da..74c8b6c9285 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java @@ -16,8 +16,6 @@ */ package io.helidon.webserver.cors; -import io.helidon.webserver.cors.CORSSupport; - /** * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder} for assiging CORS-related * attributes. From bfb94a350954ce2a62991c58a1f8af76003c9bd5 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 12 Apr 2020 18:29:00 -0500 Subject: [PATCH 076/100] Move adapters into internal package to highlight the intent to restrict their use. --- .../microprofile/cors/CrossOriginFilter.java | 4 ++-- .../helidon/webserver/cors/CORSSupport.java | 2 ++ .../webserver/cors/CrossOriginHelper.java | 2 ++ .../io/helidon/webserver/cors/LogHelper.java | 1 + .../webserver/cors/SERequestAdapter.java | 1 + .../webserver/cors/SEResponseAdapter.java | 1 + .../cors/{ => internal}/RequestAdapter.java | 2 +- .../cors/{ => internal}/ResponseAdapter.java | 2 +- .../webserver/cors/internal/package-info.java | 21 +++++++++++++++++++ webserver/cors/src/main/java/module-info.java | 1 + 10 files changed, 33 insertions(+), 4 deletions(-) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{ => internal}/RequestAdapter.java (97%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{ => internal}/ResponseAdapter.java (97%) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 15d72d5f23e..20b0d54d1f7 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -41,9 +41,9 @@ import io.helidon.config.Config; import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; +import io.helidon.webserver.cors.internal.RequestAdapter; +import io.helidon.webserver.cors.internal.ResponseAdapter; -import io.helidon.webserver.cors.RequestAdapter; -import io.helidon.webserver.cors.ResponseAdapter; import org.eclipse.microprofile.config.ConfigProvider; /** diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 2e30fbd9a57..c7f5448f0ba 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -25,6 +25,8 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import io.helidon.webserver.cors.internal.RequestAdapter; +import io.helidon.webserver.cors.internal.ResponseAdapter; /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java index 6b62c70f499..3c776ed8eb2 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java @@ -33,6 +33,8 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.webserver.cors.LogHelper.Headers; +import io.helidon.webserver.cors.internal.RequestAdapter; +import io.helidon.webserver.cors.internal.ResponseAdapter; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index e418a19af31..50a273d7856 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -26,6 +26,7 @@ import io.helidon.common.http.Http; import io.helidon.webserver.cors.CrossOriginHelper.RequestType; +import io.helidon.webserver.cors.internal.RequestAdapter; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index c4a5c5ef66a..ebcb154ce53 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -20,6 +20,7 @@ import java.util.Optional; import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.cors.internal.RequestAdapter; /** * Helidon SE implementation of {@link RequestAdapter}. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index 13702d2dcf9..3bd6438e6e9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -18,6 +18,7 @@ import io.helidon.common.http.Http; import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.cors.internal.ResponseAdapter; /** * SE implementation of {@link ResponseAdapter}. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java similarity index 97% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java index c54ae40fd9f..8618278f6aa 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors; +package io.helidon.webserver.cors.internal; import java.util.List; import java.util.Optional; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java similarity index 97% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java index c46ac10b46c..b7727a2eff7 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.helidon.webserver.cors; +package io.helidon.webserver.cors.internal; /** * Not for use by developers. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java new file mode 100644 index 00000000000..e645e29ffbf --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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. + * + */ +/** + * Not for developer use. This package contains Helidon-internal classes that need to be public so the MP CORS module + * can use them but are not of use to developers. + */ +package io.helidon.webserver.cors.internal; diff --git a/webserver/cors/src/main/java/module-info.java b/webserver/cors/src/main/java/module-info.java index 482b698d841..e7a86743ef7 100644 --- a/webserver/cors/src/main/java/module-info.java +++ b/webserver/cors/src/main/java/module-info.java @@ -26,4 +26,5 @@ requires io.helidon.webserver; exports io.helidon.webserver.cors; + exports io.helidon.webserver.cors.internal to io.helidon.microprofile.cors; } From bd26c10313f0a56392feefcf6e2973af72f5a102 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 12 Apr 2020 19:29:56 -0500 Subject: [PATCH 077/100] Parameterize the CORSSupport.Builder so we can have a subclass of it in the internal package that knows about the secondary look-up supplier without advertising that on CORSSupport.Builder --- .../microprofile/cors/CrossOriginFilter.java | 4 +- .../helidon/webserver/cors/CORSSupport.java | 105 +++++++++++------- .../internal/InternalCORSSupportBuilder.java | 61 ++++++++++ 3 files changed, 129 insertions(+), 41 deletions(-) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 20b0d54d1f7..69483d89cd5 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -41,6 +41,7 @@ import io.helidon.config.Config; import io.helidon.webserver.cors.CORSSupport; import io.helidon.webserver.cors.CrossOriginConfig; +import io.helidon.webserver.cors.internal.InternalCORSSupportBuilder; import io.helidon.webserver.cors.internal.RequestAdapter; import io.helidon.webserver.cors.internal.ResponseAdapter; @@ -68,7 +69,8 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - CORSSupport.Builder b = CORSSupport.builder(); + + InternalCORSSupportBuilder b = CORSSupport.internalBuilder(); cors = b.config(config.get(CORS_CONFIG_KEY)) .secondaryLookupSupplier(crossOriginFromAnnotationSupplier()) .build(); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index c7f5448f0ba..5402219a7e2 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -17,7 +17,6 @@ package io.helidon.webserver.cors; import java.util.Optional; -import java.util.function.Supplier; import io.helidon.config.Config; import io.helidon.webserver.Handler; @@ -25,6 +24,7 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import io.helidon.webserver.cors.internal.InternalCORSSupportBuilder; import io.helidon.webserver.cors.internal.RequestAdapter; import io.helidon.webserver.cors.internal.ResponseAdapter; @@ -57,7 +57,7 @@ public class CORSSupport implements Service, Handler { private final CrossOriginHelper helper; - private CORSSupport(Builder builder) { + private > CORSSupport(Builder builder) { helper = builder.helperBuilder.build(); } @@ -66,10 +66,11 @@ private CORSSupport(Builder builder) { * * @param type of request wrapped by the request adapter * @param type of response wrapped by the response adapter + * @param type of the builder * @return the service */ - public static CORSSupport create() { - Builder b = builder(); + public static > CORSSupport create() { + Builder b = builder(); return b.build(); } @@ -79,10 +80,11 @@ public static CORSSupport create() { * @param config the config node containing CORS information * @param type of request wrapped by the request adapter * @param type of response wrapped by the response adapter + * @param type of the builder * @return the initialized service */ - public static CORSSupport create(Config config) { - Builder b = builder(); + public static > CORSSupport create(Config config) { + Builder b = builder(); return b.config(config).build(); } @@ -91,12 +93,24 @@ public static CORSSupport create(Config config) { * * @param type of request wrapped by the request adapter * @param type of response wrapped by the response adapter + * @param type of the builder * @return the builder */ - public static Builder builder() { + public static > Builder builder() { return new Builder<>(); } + /** + * Creates an internal builder - one that knows about the secondary cross-origin config supplier. + * + * @param type of request wrapped by the request adapter + * @param type of response wrapped by the response adapter + * @return the builder + */ + public static InternalCORSSupportBuilder internalBuilder() { + return InternalCORSSupportBuilder.create(); + } + /** * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. * @@ -104,9 +118,10 @@ public static Builder builder() { * @return builder initialized with the CORS set-up from the config * @param type of request wrapped by the request adapter * @param type of response wrapped by the response adapter + * @param type of the builder */ - public static Builder builder(Config config) { - Builder b = builder(); + public static > Builder builder(Config config) { + Builder b = builder(); return b.config(config); } @@ -165,14 +180,20 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques * * @param type of the request wrapped by the adapter * @param type of the response wrapped by the adapter + * @param type of the builder */ - public static class Builder implements io.helidon.common.Builder>, - Setter> { + public static class Builder> implements io.helidon.common.Builder>, + Setter> { private final CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); private final CrossOriginConfigAggregator aggregator = helperBuilder.aggregator(); - private Builder() { + protected Builder() { + } + + @SuppressWarnings("unchecked") + protected B me() { + return (B) this; } @Override @@ -187,9 +208,9 @@ public CORSSupport build() { * @param config the CORS config * @return the updated builder */ - public Builder config(Config config) { + public B config(Config config) { aggregator.config(config); - return this; + return me(); } /** @@ -198,9 +219,9 @@ public Builder config(Config config) { * @param value whether to use CORS support * @return updated builder */ - public Builder enabled(boolean value) { + public B enabled(boolean value) { aggregator.enabled(value); - return this; + return me(); } /** @@ -210,57 +231,61 @@ public Builder enabled(boolean value) { * @param crossOrigin the cross origin information * @return updated builder */ - public Builder addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + public B addCrossOrigin(String path, CrossOriginConfig crossOrigin) { aggregator.addCrossOrigin(path, crossOrigin); - return this; + return me(); } @Override - public Builder allowOrigins(String... origins) { + public B allowOrigins(String... origins) { aggregator.allowOrigins(origins); - return this; + return me(); } @Override - public Builder allowHeaders(String... allowHeaders) { + public B allowHeaders(String... allowHeaders) { aggregator.allowHeaders(allowHeaders); - return this; + return me(); } @Override - public Builder exposeHeaders(String... exposeHeaders) { + public B exposeHeaders(String... exposeHeaders) { aggregator.exposeHeaders(exposeHeaders); - return this; + return me(); } @Override - public Builder allowMethods(String... allowMethods) { + public B allowMethods(String... allowMethods) { aggregator.allowMethods(allowMethods); - return this; + return me(); } @Override - public Builder allowCredentials(boolean allowCredentials) { + public B allowCredentials(boolean allowCredentials) { aggregator.allowCredentials(allowCredentials); - return this; + return me(); } @Override - public Builder maxAge(long maxAge) { + public B maxAge(long maxAge) { aggregator.maxAge(maxAge); - return this; + return me(); } - /** - * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during - * look-up for a given request, none is found from the aggregator. - * - * @param secondaryLookupSupplier supplier of a CrossOriginConfig - * @return updated builder - */ - public Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { - helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); - return this; +// /** +// * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during +// * look-up for a given request, none is found from the aggregator. +// * +// * @param secondaryLookupSupplier supplier of a CrossOriginConfig +// * @return updated builder +// */ +// public Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { +// helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); +// return this; +// } + + protected CrossOriginHelper.Builder helperBuilder() { + return helperBuilder; } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java new file mode 100644 index 00000000000..3fc15045288 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors.internal; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.webserver.cors.CORSSupport; +import io.helidon.webserver.cors.CrossOriginConfig; + +/** + * Not for developer user. Creates a {@code CORSSupport.Builder} that knows about the secondary look-up supplier. Used + * from MP CORS. + * + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter + */ +public class InternalCORSSupportBuilder extends CORSSupport.Builder> { + + /** + * Creates a new instance. + * + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter + * @return the new builder + */ + public static InternalCORSSupportBuilder create() { + return new InternalCORSSupportBuilder<>(); + } + + InternalCORSSupportBuilder() { + } + + /** + * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during + * look-up for a given request, none is found from the aggregator. + * + * @param secondaryLookupSupplier supplier of a CrossOriginConfig + * @return updated builder + */ + public InternalCORSSupportBuilder secondaryLookupSupplier( + Supplier> secondaryLookupSupplier) { + helperBuilder().secondaryLookupSupplier(secondaryLookupSupplier); + return this; + } +} From 872c45fdde0802b4710c8eb6597dfc29ffa5962e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 06:14:22 -0500 Subject: [PATCH 078/100] Remove 'not for developer use' notes from classes that used to be public but now are not --- .../io/helidon/webserver/cors/CrossOriginConfigAggregator.java | 2 +- .../main/java/io/helidon/webserver/cors/CrossOriginHelper.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java index cd998952052..ef047940659 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java @@ -27,7 +27,7 @@ import static io.helidon.webserver.cors.CrossOriginHelper.normalize; /** - * Not for developer use. Collects CORS set-up information from various sources and looks up the relevant CORS + * Collects CORS set-up information from various sources and looks up the relevant CORS * information given a request's path. */ class CrossOriginConfigAggregator implements Setter { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java index 3c776ed8eb2..74460ec834d 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java @@ -49,8 +49,6 @@ import static io.helidon.webserver.cors.LogHelper.DECISION_LEVEL; /** - * Not for use by developers. - * * Centralizes internal logic common to both SE and MP CORS support for processing requests and preparing responses. * *

      This class is reserved for internal Helidon use. Do not use it from your applications. It might change or vanish at From 0baf3372b498ca6b672de2882dd9b10f6ca9d76a Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 09:34:43 -0500 Subject: [PATCH 079/100] Change overwriting behavior so the SE developer reigns. The order in which the devo invokes CrossOriginConfig.Builder methods to add settings determines the behavior, not any implicit preference of config over others. The MP code doesn't use the API other than to set config, so config always rules there. --- .../webserver/cors/CrossOriginConfig.java | 16 ++ .../cors/CrossOriginConfigAggregator.java | 163 ++++++++++++------ .../helidon/webserver/cors/package-info.java | 53 ++++-- 3 files changed, 161 insertions(+), 71 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index bdf5d8b1812..4428d6d3315 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -165,6 +165,22 @@ public static class Builder implements Setter, io.helidon.common.Builde private Builder() { } + /** + * Creates a new builder based on the values in an existing {@code CrossOriginConfig} object. + * + * @param original the existing cross-origin config object + * @return new Builder initialized from the existing object's settings + */ + public static Builder from(CrossOriginConfig original) { + return new Builder() + .allowCredentials(original.allowCredentials) + .allowHeaders(original.allowHeaders) + .allowMethods(original.allowMethods) + .allowOrigins(original.allowOrigins) + .exposeHeaders(original.exposeHeaders) + .maxAge(original.maxAge); + } + /** * Sets the allowOrigins. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java index ef047940659..2470484270a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java @@ -27,24 +27,41 @@ import static io.helidon.webserver.cors.CrossOriginHelper.normalize; /** - * Collects CORS set-up information from various sources and looks up the relevant CORS - * information given a request's path. + * Collects CORS set-up information from various sources and looks up the relevant CORS information given a request's path. + *

      + * The caller can build up the cross-config information over multiple invocations of the exposed methods. The behavior is that + * of a {@link LinkedHashMap}: + *

        + *
      • when storing cross-config information, the latest invocation that specifies the same path + * expression overwrites any preceding settings for the same path expression, and
      • + *
      • when matching against a request's path, the code checks the path matchers in the order + * they were added to the aggregator, whether by {@link #config} or {@link #addCrossOrigin} or the {@link Setter} + * methods. + *
      + *

      + *

      + * The {@code Setter} methods affect the so-called "pathless" entry. Those methods have no explicit path, so we record + * their settings in an entry with path expression {@value #PATHLESS_KEY} which matches everything. + *

      + *

      + * If the developer uses the {@link #config} or {@link #addCrossOrigin} methods along with the {@code Setter} + * methods, the results are predictable but might be confusing. The {@code config} and {@code addCrossOrigin} methods + * overwrite any entry with the same path expression, whereas the {@code Setter} methods update an existing + * entry with path {@value #PATHLESS_KEY}, creating one if needed. So, if the config or an {@code addCrossOrigin} + * invocation sets values for that same path expression then results can be surprising. + * path + *

      + * */ class CrossOriginConfigAggregator implements Setter { + // Key value for the map corresponding to the cross-origin config managed by the {@link Setter} methods + static final String PATHLESS_KEY = "{+}"; + // Records paths and configs added via addCrossOriginConfig private final Map crossOriginConfigMatchables = new LinkedHashMap<>(); - // Records the merged paths and configs added via the config method - private final Map crossOriginConfigsAssembledFromConfigs = new LinkedHashMap<>(); - - // Accumulates config via the setter methods from CrossOriginConfig - private Optional crossOriginConfigBuilderOpt = Optional.empty(); - - // To be enabled, there must be a "cors" config node. - private Optional isEnabledFromConfig = Optional.empty(); - - private boolean isEnabledFromAPI = true; + private Optional isEnabledOpt = Optional.empty(); /** * Factory method. @@ -59,12 +76,13 @@ private CrossOriginConfigAggregator() { } /** - * Reports whether the sources of CORS information have left CORS enabled or not. + * Reports whether the sources of CORS information have left CORS enabled or not. If there has been an explicit setting, + * use the most recent. Otherwise * * @return if CORS processing should be done */ public boolean isEnabled() { - return isEnabledFromConfig.orElse(isEnabledFromAPI); + return isEnabledOpt.orElse(true); } /** @@ -81,21 +99,22 @@ public CrossOriginConfigAggregator config(Config config) { if (config.exists()) { Config isEnabledNode = config.get("enabled"); if (isEnabledNode.exists()) { - // Last config setting wins. - isEnabledFromConfig = Optional.of(isEnabledNode.asBoolean().get()); + // Latest explicit setting wins. + enabled(isEnabledNode.asBoolean().get()); } else { - // If config exists but enabled is missing, default enabled is true. - isEnabledFromConfig = Optional.of(Boolean.TRUE); + /* + * If there is no pre-existing setting for isEnabled, because we are processing config and the default is + * {@code enabled: true} for config, set the aggregation to enabled. + */ + if (isEnabledOpt.isEmpty()) { + enabled(true); + } } Config pathsNode = config.get(CrossOriginConfig.CORS_PATHS_CONFIG_KEY); if (pathsNode.exists()) { pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()) .get() - .entrySet() - .stream() - .forEach(entry -> crossOriginConfigsAssembledFromConfigs.put(entry.getKey(), - new CrossOriginConfigMatchable(entry.getKey(), entry.getValue()))); - + .forEach(this::addCrossOrigin); } } return this; @@ -109,7 +128,7 @@ public CrossOriginConfigAggregator config(Config config) { * @return updated builder */ public CrossOriginConfigAggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) { - crossOriginConfigMatchables.put(normalize(pathExpr), new CrossOriginConfigMatchable(pathExpr, crossOrigin)); + crossOriginConfigMatchables.put(normalize(pathExpr), new FixedCrossOriginConfigMatchable(pathExpr, crossOrigin)); return this; } @@ -120,43 +139,43 @@ public CrossOriginConfigAggregator addCrossOrigin(String pathExpr, CrossOriginCo * @return updated builder */ public CrossOriginConfigAggregator enabled(boolean value) { - isEnabledFromAPI = value; + isEnabledOpt = Optional.of(value); return this; } @Override public CrossOriginConfigAggregator allowOrigins(String... origins) { - crossOriginConfigBuilder().allowOrigins(origins); + pathlessCrossOriginConfigBuilder().allowOrigins(origins); return this; } @Override public CrossOriginConfigAggregator allowHeaders(String... allowHeaders) { - crossOriginConfigBuilder().allowHeaders(allowHeaders); + pathlessCrossOriginConfigBuilder().allowHeaders(allowHeaders); return this; } @Override public CrossOriginConfigAggregator exposeHeaders(String... exposeHeaders) { - crossOriginConfigBuilder().exposeHeaders(exposeHeaders); + pathlessCrossOriginConfigBuilder().exposeHeaders(exposeHeaders); return this; } @Override public CrossOriginConfigAggregator allowMethods(String... allowMethods) { - crossOriginConfigBuilder().allowMethods(allowMethods); + pathlessCrossOriginConfigBuilder().allowMethods(allowMethods); return this; } @Override public CrossOriginConfigAggregator allowCredentials(boolean allowCredentials) { - crossOriginConfigBuilder().allowCredentials(allowCredentials); + pathlessCrossOriginConfigBuilder().allowCredentials(allowCredentials); return this; } @Override public CrossOriginConfigAggregator maxAge(long maxAge) { - crossOriginConfigBuilder().maxAge(maxAge); + pathlessCrossOriginConfigBuilder().maxAge(maxAge); return this; } @@ -173,15 +192,10 @@ Optional lookupCrossOrigin(String path, Supplier result = Optional.empty(); String normalizedPath = normalize(path); - result = findFirst(crossOriginConfigsAssembledFromConfigs, normalizedPath) - .or(() -> crossOriginConfigBuilderOpt - .map(CrossOriginConfig.Builder::build)) - .or(() -> findFirst(crossOriginConfigMatchables, normalizedPath)) + result = findFirst(crossOriginConfigMatchables, normalizedPath) .or(secondaryLookup); return result; - - } /** @@ -204,41 +218,86 @@ private static Optional findFirst(Map * - *

      Convenience API for the "/" path

      - * Sometimes you might want to prepare just one set of CORS information, for the "/" path. The Helidon CORS API provides a + *

      Convenience API for the "match-all" path

      + * Sometimes you might want to prepare just one set of CORS information to match any path. The Helidon CORS API provides a * short-cut for this. The {@code CORSSupport.Builder} class supports all the mutator methods from {@code CrossOriginConfig} - * such as {@code allowOrigins}, and on {@code CORSSupport.Builder} these methods implicitly affect the "/" path. + * such as {@code allowOrigins}, and on {@code CORSSupport.Builder} these methods implicitly affect the + * {@value io.helidon.webserver.cors.CrossOriginConfigAggregator#PATHLESS_KEY} path. (See the + * {@link io.helidon.webserver.PathMatcher} documentation.) + *

      * The following code *

        *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
      @@ -131,12 +134,12 @@
        * 
      * has the same effect as this more verbose version: *
      - *         CrossOriginConfig corsForCORS3= CrossOriginConfig.builder()
      + *         CrossOriginConfig configForAll = CrossOriginConfig.builder()
        *             .allowOrigins("http://foo.bar", "http://bar.foo")
        *             .allowMethods("DELETE", "PUT")
        *             .build();
        *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
      - *                 .addCrossOrigin("/", corsForCORS3);
      + *                 .addCrossOrigin("{+}", configForAll);
        * 
      *

      {@code CORSSupport} as a handler

      * The previous examples use a {@code CORSSupport} instance as a Helidon {@link io.helidon.webserver.Service} which you can @@ -179,23 +182,35 @@ * .register(CORSSupport.fromConfig()); * }
      *

      Resolving conflicting settings

      - * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupport.Builder} resolves conflicts CORS - * set-up this way: + * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupport.Builder} resolves conflicts in + * CORS set-up as if using a {@code Map} to store all the information: *
        - *
      • Multiple invocations of {@code CORSSupport.Builder.config} effectively merge the configured values which designate - * different paths into a single, unified configuration. - * The configured values provided by the latest invocation of {@code CORSSupport.Builder.config} will override any - * previously-set configuration values for a given path.
      • - *
      • Multiple uses of the CORS API other than the {@code config} method) for different paths are merged among - * themselves. The last invocation of the non-config API for a given path wins.
      • - *
      • Use of the convenience {@code CrossOriginConfig}-style methods on {@code CORSSupport.Builder} act as non-config - * programmatic settings for the "/" path.
      • - *
      • Configured values override ones set programmatically for a given path.
      • + *
      • Multiple invocations of the {@code CORSSupport.Builder} {@code config}, + * {@code addCrossOrigin}, and "match-any" methods (from {@link io.helidon.webserver.cors.Setter}) effectively merge + * values which designate different paths into a single, unified group of settings. + * The settings provided by the latest invocation of these methods override any previously-set values for a given path.
      • + *
      • Use of the convenience {@code CrossOriginConfig}-style methods defined by {@code Setter} affect the map entry with + * key {@value io.helidon.webserver.cors.CrossOriginConfigAggregator#PATHLESS_KEY}, updating any existing entry and + * creating one if needed. As a result, invoking {@code config} and {@code addCrossOriginConfig} methods with that + * path will overwrite any values set by earlier invocations of the convenience methods.
      • *
      - *

      Warning about internal classes

      *

      - * Note that {@code CrossOriginHelper}, while {@code public}, is not intended for use by developers. It is - * reserved for internal Helidon use and might change at any time. + * Each {@code CORSSupport} instance can be enabled or disabled, either through configuration or using the API. + * By default, when an application creates a new {@code CORSSupport.Builder} instance that builder's {@code build()} method will + * create an enabled {@code CORSSupport} object. Any subsequent explicit setting on the builder, either expressly set by an + * {@code enabled} entry in configuration passed to {@code CORSSupport.Builder.config} or set by invoking + * {@code CORSSupport.Builder.enabled} follows the familiar "latest-wins" approach. + *

      + *

      + * If the application uses a single {@code CORSSupport} instance, then the enabled setting for that instance governs the + * entire CORS implementation for the app's endpoints. + *

      + *

      Warning about internal classes and methods

      + *

      + * Note that everything in the {@code io.helidon.webserver.cors.internal} package, and any method with {@code internal} in + * its name, even if marked {@code public}, are intended for Helidon use. They are not + * intended for use by application developers but should be considered reserved for internal Helidon use and subject to change + * on short notice. *

      */ package io.helidon.webserver.cors; From 4b3405e0ef50f7cd4e83b7d47781c2afcb3a1371 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 10:43:06 -0500 Subject: [PATCH 080/100] Fix javadoc --- .../src/main/java/io/helidon/webserver/cors/CORSSupport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 5402219a7e2..9761091b135 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -39,7 +39,7 @@ *
    • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which * it applies, and
    • *
    • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the - * "/" path).
    • + * {@value CrossOriginConfigAggregator#PATHLESS_KEY} path). *
    *

    * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. From e68db0607cb6cc72424d41273616742653c87031 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 10:54:53 -0500 Subject: [PATCH 081/100] Clean up copyright notice --- .../java/io/helidon/microprofile/cors/CrossOriginFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 69483d89cd5..195adc8131b 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 4b5925fa5b48ff8c547481c8fd07e5a9f8b91a21 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 11:06:28 -0500 Subject: [PATCH 082/100] The requires for java.ws.rs should not need to be transitive --- microprofile/cors/src/main/java/module-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/module-info.java b/microprofile/cors/src/main/java/module-info.java index bbad13e0a35..437f928ea99 100644 --- a/microprofile/cors/src/main/java/module-info.java +++ b/microprofile/cors/src/main/java/module-info.java @@ -19,7 +19,7 @@ */ module io.helidon.microprofile.cors { - requires transitive java.ws.rs; + requires java.ws.rs; requires io.helidon.config; requires io.helidon.webserver.cors; From bf87039ea60b72edc0c07cd9dc46232b8ff08201 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 13 Apr 2020 15:16:12 -0500 Subject: [PATCH 083/100] Significant refactoring to reduce the proliferation of type variables; also remove the need for the 'internal' subpackage --- .../microprofile/cors/CORSSupportMP.java | 178 +++++++++++++ .../microprofile/cors/CrossOriginFilter.java | 166 +++---------- .../helidon/webserver/cors/CORSSupport.java | 233 ++++++++++-------- .../helidon/webserver/cors/CORSSupportSE.java | 56 +++++ .../webserver/cors/CrossOriginHelper.java | 5 +- .../io/helidon/webserver/cors/LogHelper.java | 2 +- .../webserver/cors/SERequestAdapter.java | 5 +- .../webserver/cors/SEResponseAdapter.java | 9 +- .../internal/InternalCORSSupportBuilder.java | 61 ----- .../cors/internal/RequestAdapter.java | 79 ------ .../cors/internal/ResponseAdapter.java | 67 ----- .../webserver/cors/internal/package-info.java | 21 -- .../helidon/webserver/cors/package-info.java | 59 ++--- webserver/cors/src/main/java/module-info.java | 1 - .../io/helidon/webserver/cors/TestUtil.java | 10 +- 15 files changed, 444 insertions(+), 508 deletions(-) create mode 100644 microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java new file mode 100644 index 00000000000..1ab324410e5 --- /dev/null +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.cors; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import io.helidon.webserver.cors.CORSSupport; +import io.helidon.webserver.cors.CrossOriginConfig; + +/** + * MP implementation of {@link CORSSupport}. + */ +class CORSSupportMP extends CORSSupport { + + /** + * + * @return a new builder of CORSSupportMP + */ + public static Builder builder() { + return new Builder(); + } + + private CORSSupportMP(Builder builder) { + super(builder); + } + + /** + * Not for developer use. Submits a request adapter and response adapter for CORS processing. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the response + * @return Optional of the response type U; present if the response should be returned, empty if request processing should + * continue + */ + @Override + protected Optional processRequest(RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { + return super.processRequest(requestAdapter, responseAdapter); + } + + /** + * Not for developer user. Gets a response ready to participate in the CORS protocol. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the reseponse + */ + @Override + protected void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + super.prepareResponse(requestAdapter, responseAdapter); + } + + static class Builder extends CORSSupport.Builder { + + @Override + public CORSSupportMP build() { + return new CORSSupportMP(this); + } + + @Override + protected Builder me() { + return this; + } + + @Override + protected Builder secondaryLookupSupplier( + Supplier> secondaryLookupSupplier) { + super.secondaryLookupSupplier(secondaryLookupSupplier); + return this; + } + } + + static class RequestAdapterMP implements RequestAdapter { + + private final ContainerRequestContext requestContext; + + RequestAdapterMP(ContainerRequestContext requestContext) { + this.requestContext = requestContext; + } + + @Override + public String path() { + return requestContext.getUriInfo().getPath(); + } + + @Override + public Optional firstHeader(String s) { + return Optional.ofNullable(requestContext.getHeaders().getFirst(s)); + } + + @Override + public boolean headerContainsKey(String s) { + return requestContext.getHeaders().containsKey(s); + } + + @Override + public List allHeaders(String s) { + return requestContext.getHeaders().get(s); + } + + @Override + public String method() { + return requestContext.getMethod(); + } + + @Override + public ContainerRequestContext request() { + return requestContext; + } + + @Override + public void next() { + } + } + + static class ResponseAdapterMP implements ResponseAdapter { + + private final MultivaluedMap headers; + + ResponseAdapterMP(ContainerResponseContext responseContext) { + headers = responseContext.getHeaders(); + } + + ResponseAdapterMP() { + headers = new MultivaluedHashMap<>(); + } + + @Override + public ResponseAdapter header(String key, String value) { + headers.add(key, value); + return this; + } + + @Override + public ResponseAdapter header(String key, Object value) { + headers.add(key, value); + return this; + } + + @Override + public Response forbidden(String message) { + return Response.status(Response.Status.FORBIDDEN).entity(message).build(); + } + + @Override + public Response ok() { + Response.ResponseBuilder builder = Response.ok(); + /* + * The Helidon CORS support code invokes ok() only for creating a CORS preflight response. In these cases no user + * code will have a chance to set headers in the response. That means we can use replaceAll here because the only + * headers needed in the response are the ones set using this adapter. + */ + builder.replaceAll(headers); + return builder.build(); + } + } +} diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 195adc8131b..a03b3109e26 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -18,9 +18,7 @@ import java.lang.reflect.Method; import java.util.Arrays; -import java.util.List; import java.util.Optional; -import java.util.function.Supplier; import javax.annotation.Priority; import javax.ws.rs.OPTIONS; @@ -32,18 +30,14 @@ import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; -import io.helidon.webserver.cors.CORSSupport; +import io.helidon.microprofile.cors.CORSSupportMP.RequestAdapterMP; +import io.helidon.microprofile.cors.CORSSupportMP.ResponseAdapterMP; import io.helidon.webserver.cors.CrossOriginConfig; -import io.helidon.webserver.cors.internal.InternalCORSSupportBuilder; -import io.helidon.webserver.cors.internal.RequestAdapter; -import io.helidon.webserver.cors.internal.ResponseAdapter; import org.eclipse.microprofile.config.ConfigProvider; @@ -65,145 +59,57 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private final CORSSupport cors; + private final CORSSupportMP cors; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - InternalCORSSupportBuilder b = CORSSupport.internalBuilder(); - cors = b.config(config.get(CORS_CONFIG_KEY)) - .secondaryLookupSupplier(crossOriginFromAnnotationSupplier()) + cors = CORSSupportMP.builder().config(config.get(CORS_CONFIG_KEY)) + .secondaryLookupSupplier(this::crossOriginFromAnnotationSupplier) .build(); } @Override public void filter(ContainerRequestContext requestContext) { - Optional response = cors.processRequest(new MPRequestAdapter(requestContext), new MPResponseAdapter()); + Optional response = cors.processRequest(new RequestAdapterMP(requestContext), new ResponseAdapterMP()); response.ifPresent(requestContext::abortWith); } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - cors.prepareResponse(new MPRequestAdapter(requestContext), new MPResponseAdapter(responseContext)); + cors.prepareResponse(new RequestAdapterMP(requestContext), new ResponseAdapterMP(responseContext)); } - static class MPRequestAdapter implements RequestAdapter { - - private final ContainerRequestContext requestContext; - - MPRequestAdapter(ContainerRequestContext requestContext) { - this.requestContext = requestContext; - } - - @Override - public String path() { - return requestContext.getUriInfo().getPath(); - } - - @Override - public Optional firstHeader(String s) { - return Optional.ofNullable(requestContext.getHeaders().getFirst(s)); - } - - @Override - public boolean headerContainsKey(String s) { - return requestContext.getHeaders().containsKey(s); - } - - @Override - public List allHeaders(String s) { - return requestContext.getHeaders().get(s); - } - - @Override - public String method() { - return requestContext.getMethod(); - } - - @Override - public ContainerRequestContext request() { - return requestContext; - } - - @Override - public void next() { - } - } - - static class MPResponseAdapter implements ResponseAdapter { - - private final MultivaluedMap headers; - - MPResponseAdapter(ContainerResponseContext responseContext) { - headers = responseContext.getHeaders(); - } - - MPResponseAdapter() { - headers = new MultivaluedHashMap<>(); - } - - @Override - public ResponseAdapter header(String key, String value) { - headers.add(key, value); - return this; - } - - @Override - public ResponseAdapter header(String key, Object value) { - headers.add(key, value); - return this; - } - - @Override - public Response forbidden(String message) { - return Response.status(Response.Status.FORBIDDEN).entity(message).build(); - } - - @Override - public Response ok() { - Response.ResponseBuilder builder = Response.ok(); - /* - * The Helidon CORS support code invokes ok() only for creating a CORS preflight response. In these cases no user - * code will have a chance to set headers in the response. That means we can use replaceAll here because the only - * headers needed in the response are the ones set using this adapter. - */ - builder.replaceAll(headers); - return builder.build(); - } - } - - Supplier> crossOriginFromAnnotationSupplier() { - - return () -> { - // If not found, inspect resource matched - Method resourceMethod = resourceInfo.getResourceMethod(); - Class resourceClass = resourceInfo.getResourceClass(); - - CrossOrigin corsAnnot; - OPTIONS optsAnnot = resourceMethod.getAnnotation(OPTIONS.class); - Path pathAnnot = resourceMethod.getAnnotation(Path.class); - if (optsAnnot != null) { - corsAnnot = resourceMethod.getAnnotation(CrossOrigin.class); - } else { - Optional optionsMethod = Arrays.stream(resourceClass.getDeclaredMethods()) - .filter(m -> { - OPTIONS optsAnnot2 = m.getAnnotation(OPTIONS.class); - if (optsAnnot2 != null) { - if (pathAnnot != null) { - Path pathAnnot2 = m.getAnnotation(Path.class); - return pathAnnot2 != null && pathAnnot.value() - .equals(pathAnnot2.value()); - } - return true; + Optional crossOriginFromAnnotationSupplier() { + + // If not found, inspect resource matched + Method resourceMethod = resourceInfo.getResourceMethod(); + Class resourceClass = resourceInfo.getResourceClass(); + + CrossOrigin corsAnnot; + OPTIONS optsAnnot = resourceMethod.getAnnotation(OPTIONS.class); + Path pathAnnot = resourceMethod.getAnnotation(Path.class); + if (optsAnnot != null) { + corsAnnot = resourceMethod.getAnnotation(CrossOrigin.class); + } else { + Optional optionsMethod = Arrays.stream(resourceClass.getDeclaredMethods()) + .filter(m -> { + OPTIONS optsAnnot2 = m.getAnnotation(OPTIONS.class); + if (optsAnnot2 != null) { + if (pathAnnot != null) { + Path pathAnnot2 = m.getAnnotation(Path.class); + return pathAnnot2 != null && pathAnnot.value() + .equals(pathAnnot2.value()); } - return false; - }) - .findFirst(); - corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) - .orElse(null); - } - return Optional.ofNullable(corsAnnot == null ? null : annotationToConfig(corsAnnot)); - }; + return true; + } + return false; + }) + .findFirst(); + corsAnnot = optionsMethod.map(m -> m.getAnnotation(CrossOrigin.class)) + .orElse(null); + } + return Optional.ofNullable(corsAnnot == null ? null : annotationToConfig(corsAnnot)); } private static CrossOriginConfig annotationToConfig(CrossOrigin crossOrigin) { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 9761091b135..7d583514f51 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -16,7 +16,9 @@ */ package io.helidon.webserver.cors; +import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import io.helidon.config.Config; import io.helidon.webserver.Handler; @@ -24,9 +26,6 @@ import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -import io.helidon.webserver.cors.internal.InternalCORSSupportBuilder; -import io.helidon.webserver.cors.internal.RequestAdapter; -import io.helidon.webserver.cors.internal.ResponseAdapter; /** * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon @@ -49,82 +48,15 @@ * {@link CrossOriginConfig}. *

    * - * @param type wrapped by RequestAdapter - * @param type wrapped by ResponseAdapter - * */ -public class CORSSupport implements Service, Handler { +public abstract class CORSSupport implements Service, Handler { private final CrossOriginHelper helper; - private > CORSSupport(Builder builder) { + protected > CORSSupport(Builder builder) { helper = builder.helperBuilder.build(); } - /** - * Creates a {@code CORSSupport} which supports the default CORS set-up. - * - * @param type of request wrapped by the request adapter - * @param type of response wrapped by the response adapter - * @param type of the builder - * @return the service - */ - public static > CORSSupport create() { - Builder b = builder(); - return b.build(); - } - - /** - * Returns a {@code CORSSupport} set up using the supplied {@link Config} node. - * - * @param config the config node containing CORS information - * @param type of request wrapped by the request adapter - * @param type of response wrapped by the response adapter - * @param type of the builder - * @return the initialized service - */ - public static > CORSSupport create(Config config) { - Builder b = builder(); - return b.config(config).build(); - } - - /** - * Creates a {@code Builder} for assembling a {@code CORSSupport}. - * - * @param type of request wrapped by the request adapter - * @param type of response wrapped by the response adapter - * @param type of the builder - * @return the builder - */ - public static > Builder builder() { - return new Builder<>(); - } - - /** - * Creates an internal builder - one that knows about the secondary cross-origin config supplier. - * - * @param type of request wrapped by the request adapter - * @param type of response wrapped by the response adapter - * @return the builder - */ - public static InternalCORSSupportBuilder internalBuilder() { - return InternalCORSSupportBuilder.create(); - } - - /** - * Creates a {@code Builder} initialized with the CORS information from the specified configuration node. - * - * @param config node containing CORS information - * @return builder initialized with the CORS set-up from the config - * @param type of request wrapped by the request adapter - * @param type of response wrapped by the response adapter - * @param type of the builder - */ - public static > Builder builder(Config config) { - Builder b = builder(); - return b.config(config); - } - @Override public void update(Routing.Rules rules) { if (helper.isActive()) { @@ -151,10 +83,12 @@ public void accept(ServerRequest request, ServerResponse response) { * * @param requestAdapter wrapper around the request * @param responseAdapter wrapper around the response + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter * @return Optional of the response type U; present if the response should be returned, empty if request processing should * continue */ - public Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + protected Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { return helper.processRequest(requestAdapter, responseAdapter); } @@ -163,8 +97,10 @@ public Optional processRequest(RequestAdapter requestAdapter, ResponseAdap * * @param requestAdapter wrapper around the request * @param responseAdapter wrapper around the reseponse + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter */ - public void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + protected void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { helper.prepareResponse(requestAdapter, responseAdapter); } @@ -178,12 +114,11 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques /** * Builder for {@code CORSSupport} instances. * - * @param type of the request wrapped by the adapter - * @param type of the response wrapped by the adapter + * @param specific subtype of {@code CORSSupport} the builder creates * @param type of the builder */ - public static class Builder> implements io.helidon.common.Builder>, - Setter> { + public abstract static class Builder> implements io.helidon.common.Builder, + Setter> { private final CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); private final CrossOriginConfigAggregator aggregator = helperBuilder.aggregator(); @@ -191,15 +126,10 @@ public static class Builder> implements io.heli protected Builder() { } - @SuppressWarnings("unchecked") - protected B me() { - return (B) this; - } + protected abstract B me(); @Override - public CORSSupport build() { - return new CORSSupport<>(this); - } + public abstract T build(); /** * Merges CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance @@ -272,20 +202,125 @@ public B maxAge(long maxAge) { return me(); } -// /** -// * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during -// * look-up for a given request, none is found from the aggregator. -// * -// * @param secondaryLookupSupplier supplier of a CrossOriginConfig -// * @return updated builder -// */ -// public Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { -// helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); -// return this; -// } - - protected CrossOriginHelper.Builder helperBuilder() { - return helperBuilder; + /** + * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during + * look-up for a given request, none is found from the aggregator. + * + * @param secondaryLookupSupplier supplier of a CrossOriginConfig + * @return updated builder + */ + protected Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { + helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); + return this; } } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP request. + * + * @param type of the request wrapped by the adapter + */ + protected interface RequestAdapter { + + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + + /** + * Processes the next handler/filter/request processor in the chain. + */ + void next(); + + /** + * Returns the request this adapter wraps. + * + * @return the request + */ + T request(); + } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP response. + * + *

    + * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

    + * + * @param the type of the response wrapped by the adapter + */ + protected interface ResponseAdapter { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, Object value); + + /** + * Returns a response with the forbidden status and the specified error message, without any headers assigned + * using the {@code header} methods. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns a response with only the headers that were set on this adapter and the status set to OK. + * + * @return response instance + */ + T ok(); + } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java new file mode 100644 index 00000000000..71aea329d35 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +/** + * SE implementation of {@link CORSSupport}. + */ +public class CORSSupportSE extends CORSSupport { + + private CORSSupportSE(Builder builder) { + super(builder); + } + + /** + * + * @return new builder for CORSSupportSE + */ + public static Builder builder() { + return new Builder(); + } + + /** + * + * @return new CORSSupportSE with default settings + */ + public static CORSSupportSE create() { + return builder().build(); + } + + public static class Builder extends CORSSupport.Builder { + + @Override + public CORSSupportSE build() { + return new CORSSupportSE(this); + } + + @Override + protected Builder me() { + return this; + } + } +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java index 74460ec834d..24f66213ce0 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java @@ -32,9 +32,9 @@ import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; import io.helidon.config.Config; +import io.helidon.webserver.cors.CORSSupport.RequestAdapter; +import io.helidon.webserver.cors.CORSSupport.ResponseAdapter; import io.helidon.webserver.cors.LogHelper.Headers; -import io.helidon.webserver.cors.internal.RequestAdapter; -import io.helidon.webserver.cors.internal.ResponseAdapter; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; @@ -638,5 +638,4 @@ private static U forbid(RequestAdapter requestAdapter, ResponseAdapter publicReason + (privateExplanation == null ? "" : "; " + privateExplanation.get()))); return responseAdapter.forbidden(publicReason); } - } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index 50a273d7856..8c2cc9545db 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -25,8 +25,8 @@ import java.util.logging.Level; import io.helidon.common.http.Http; +import io.helidon.webserver.cors.CORSSupport.RequestAdapter; import io.helidon.webserver.cors.CrossOriginHelper.RequestType; -import io.helidon.webserver.cors.internal.RequestAdapter; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index ebcb154ce53..9a1aac88bbf 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -20,12 +20,11 @@ import java.util.Optional; import io.helidon.webserver.ServerRequest; -import io.helidon.webserver.cors.internal.RequestAdapter; /** - * Helidon SE implementation of {@link RequestAdapter}. + * Helidon SE implementation of {@link CORSSupport.RequestAdapter}. */ -class SERequestAdapter implements RequestAdapter { +class SERequestAdapter implements CORSSupport.RequestAdapter { private final ServerRequest request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index 3bd6438e6e9..01b32cbe8da 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -18,12 +18,11 @@ import io.helidon.common.http.Http; import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.cors.internal.ResponseAdapter; /** - * SE implementation of {@link ResponseAdapter}. + * SE implementation of {@link CORSSupport.ResponseAdapter}. */ -class SEResponseAdapter implements ResponseAdapter { +class SEResponseAdapter implements CORSSupport.ResponseAdapter { private final ServerResponse serverResponse; @@ -32,13 +31,13 @@ class SEResponseAdapter implements ResponseAdapter { } @Override - public ResponseAdapter header(String key, String value) { + public CORSSupport.ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public ResponseAdapter header(String key, Object value) { + public CORSSupport.ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java deleted file mode 100644 index 3fc15045288..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/InternalCORSSupportBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.webserver.cors.internal; - -import java.util.Optional; -import java.util.function.Supplier; - -import io.helidon.webserver.cors.CORSSupport; -import io.helidon.webserver.cors.CrossOriginConfig; - -/** - * Not for developer user. Creates a {@code CORSSupport.Builder} that knows about the secondary look-up supplier. Used - * from MP CORS. - * - * @param type of the request wrapped by the adapter - * @param type of the response wrapped by the adapter - */ -public class InternalCORSSupportBuilder extends CORSSupport.Builder> { - - /** - * Creates a new instance. - * - * @param type of the request wrapped by the adapter - * @param type of the response wrapped by the adapter - * @return the new builder - */ - public static InternalCORSSupportBuilder create() { - return new InternalCORSSupportBuilder<>(); - } - - InternalCORSSupportBuilder() { - } - - /** - * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during - * look-up for a given request, none is found from the aggregator. - * - * @param secondaryLookupSupplier supplier of a CrossOriginConfig - * @return updated builder - */ - public InternalCORSSupportBuilder secondaryLookupSupplier( - Supplier> secondaryLookupSupplier) { - helperBuilder().secondaryLookupSupplier(secondaryLookupSupplier); - return this; - } -} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java deleted file mode 100644 index 8618278f6aa..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/RequestAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.webserver.cors.internal; - -import java.util.List; -import java.util.Optional; - -/** - * Not for use by developers. - * - * Minimal abstraction of an HTTP request. - * - * @param type of the request wrapped by the adapter - */ -public interface RequestAdapter { - - /** - * - * @return possibly unnormalized path from the request - */ - String path(); - - /** - * Retrieves the first value for the specified header as a String. - * - * @param key header name to retrieve - * @return the first header value for the key - */ - Optional firstHeader(String key); - - /** - * Reports whether the specified header exists. - * - * @param key header name to check for - * @return whether the header exists among the request's headers - */ - boolean headerContainsKey(String key); - - /** - * Retrieves all header values for a given key as Strings. - * - * @param key header name to retrieve - * @return header values for the header; empty list if none - */ - List allHeaders(String key); - - /** - * Reports the method name for the request. - * - * @return the method name - */ - String method(); - - /** - * Processes the next handler/filter/request processor in the chain. - */ - void next(); - - /** - * Returns the request this adapter wraps. - * - * @return the request - */ - T request(); -} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java deleted file mode 100644 index b7727a2eff7..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/ResponseAdapter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.webserver.cors.internal; - -/** - * Not for use by developers. - * - * Minimal abstraction of an HTTP response. - * - *

    - * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} - * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the - * actual response. - *

    - * - * @param the type of the response wrapped by the adapter - */ -public interface ResponseAdapter { - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, String value); - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, Object value); - - /** - * Returns a response with the forbidden status and the specified error message, without any headers assigned - * using the {@code header} methods. - * - * @param message error message to use in setting the response status - * @return the factory - */ - T forbidden(String message); - - /** - * Returns a response with only the headers that were set on this adapter and the status set to OK. - * - * @return response instance - */ - T ok(); -} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java deleted file mode 100644 index e645e29ffbf..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/internal/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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. - * - */ -/** - * Not for developer use. This package contains Helidon-internal classes that need to be public so the MP CORS module - * can use them but are not of use to developers. - */ -package io.helidon.webserver.cors.internal; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 2169a89ff4e..87da93efb2b 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -18,7 +18,7 @@ /** *

    Helidon SE CORS Support

    *

    - * Use {@link io.helidon.webserver.cors.CORSSupport} and its {@link io.helidon.webserver.cors.CORSSupport.Builder} to add CORS + * Use {@link io.helidon.webserver.cors.CORSSupportSE} and its {@link io.helidon.webserver.cors.CORSSupportSE.Builder} to add CORS * handling to resources in your application. *

    * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for @@ -46,7 +46,7 @@ *

    Finding and applying CORS configuration

    * Although Helidon prescribes the CORS config format, you can put it wherever you want in your application's configuration * file. Your application code will retrieve the CORS config from its location within your configuration and then use that - * config node with the {@link io.helidon.webserver.cors.CORSSupport.Builder} in preparing CORS support for your app. + * config node with the {@link io.helidon.webserver.cors.CORSSupportSE.Builder} in preparing CORS support for your app. * * If you set up this configuration *
    @@ -69,7 +69,7 @@
      *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build();
      *         Routing.Builder builder = Routing.builder()
      *                 .register("/myapp",
    - *                           CORSSupport.builder()
    + *                           CORSSupportSE.builder()
      *                                      .config(myAppConfig.get("my-cors"))
      *                                      .build(),
      *                           new MyApp());
    @@ -80,7 +80,7 @@
      *     
  • creating a {@link io.helidon.webserver.cors.CrossOriginConfig.Builder} instance,
  • *
  • operating on it to create the CORS set-up you want,
  • *
  • using the builder's {@code build()} method to create the {@code CrossOriginConfig} instance, and
  • - *
  • using the {@code CORSSupport.Builder} to associate a path with the {@code CrossOriginConfig} object.
  • + *
  • using the {@code CORSSupportSE.Builder} to associate a path with the {@code CrossOriginConfig} object.
  • *
*

* The next example shows creating CORS information and associating it with the path {@code /cors3} within the app. @@ -92,12 +92,12 @@ * * Routing.Builder builder = Routing.builder() * .register("/myapp", - * CORSSupport.builder() + * CORSSupportSE.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app * .build(), * new MyApp()); * - * Notice that you pass two services to the {@code register} method: the {@code CORSSupport} instance and your app + * Notice that you pass two services to the {@code register} method: the {@code CORSSupportSE} instance and your app * instance. Helidon will process requests to the path you specify with those services in that order. *

* Invoke {@code addCrossOrigin} multiple times to link more paths with CORS configuration. You can reuse one {@code @@ -105,11 +105,11 @@ *

*

* The following example shows how you can combine configuration and the API. To help with readability as things get more - * complicated, this example saves the {@code CORSSupport.Builder} in a variable rather than constructing it in-line when + * complicated, this example saves the {@code CORSSupportSE.Builder} in a variable rather than constructing it in-line when * invoking {@code register}: *

*
- *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
+ *         CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder()
  *                  .config(myAppConfig.get("my-cors"))
  *                  .addCrossOrigin("/cors3", corsFORCORS3);
  *
@@ -121,14 +121,14 @@
  *
  * 

Convenience API for the "match-all" path

* Sometimes you might want to prepare just one set of CORS information to match any path. The Helidon CORS API provides a - * short-cut for this. The {@code CORSSupport.Builder} class supports all the mutator methods from {@code CrossOriginConfig} - * such as {@code allowOrigins}, and on {@code CORSSupport.Builder} these methods implicitly affect the + * short-cut for this. The {@code CORSSupportSE.Builder} class supports all the mutator methods from {@code CrossOriginConfig} + * such as {@code allowOrigins}, and on {@code CORSSupportSE.Builder} these methods implicitly affect the * {@value io.helidon.webserver.cors.CrossOriginConfigAggregator#PATHLESS_KEY} path. (See the * {@link io.helidon.webserver.PathMatcher} documentation.) *

* The following code *

- *         CORSSupport.Builder corsBuilder = CORSSupport.builder()
+ *         CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder()
  *             .allowOrigins("http://foo.bar", "http://bar.foo")
  *             .allowMethods("DELETE", "PUT");
  * 
@@ -138,17 +138,17 @@ * .allowOrigins("http://foo.bar", "http://bar.foo") * .allowMethods("DELETE", "PUT") * .build(); - * CORSSupport.Builder corsBuilder = CORSSupport.builder() + * CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder() * .addCrossOrigin("{+}", configForAll); *
- *

{@code CORSSupport} as a handler

- * The previous examples use a {@code CORSSupport} instance as a Helidon {@link io.helidon.webserver.Service} which you can - * register with the routing rules. You can also use a {@code CORSSupport} object as a {@link io.helidon.webserver.Handler} in + *

{@code CORSSupportSE} as a handler

+ * The previous examples use a {@code CORSSupportSE} instance as a Helidon {@link io.helidon.webserver.Service} which you can + * register with the routing rules. You can also use a {@code CORSSupportSE} object as a {@link io.helidon.webserver.Handler} in * setting up the routing rules for an HTTP method and path. The next example sets up CORS processing for the {@code PUT} and * {@code OPTIONS} HTTP methods on the {@code /cors4} path within the app. The application code for both simply accepts the * request graciously and replies with success: *
{@code
- *         CORSSupport cors4Support = CORSSupport.builder()
+ *         CORSSupportSE cors4Support = CORSSupportSE.builder()
  *                 .allowOrigins("http://foo.bar", "http://bar.foo")
  *                 .allowMethods("PUT")
  *                 .build();
@@ -175,17 +175,17 @@
  *                      cors4Support,
  *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
  *                 .get("/cors4",
- *                      CORSSupport.builder()
+ *                      CORSSupportSE.builder()
  *                               .allowOrigins("*")
  *                               .minAge(-1),
  *                      (req, resp) -> resp.send("Hello, World!"))
- *                 .register(CORSSupport.fromConfig());
+ *                 .register(CORSSupportSE.fromConfig());
  * }
*

Resolving conflicting settings

- * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupport.Builder} resolves conflicts in + * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupportSE.Builder} resolves conflicts in * CORS set-up as if using a {@code Map} to store all the information: *
    - *
  • Multiple invocations of the {@code CORSSupport.Builder} {@code config}, + *
  • Multiple invocations of the {@code CORSSupportSE.Builder} {@code config}, * {@code addCrossOrigin}, and "match-any" methods (from {@link io.helidon.webserver.cors.Setter}) effectively merge * values which designate different paths into a single, unified group of settings. * The settings provided by the latest invocation of these methods override any previously-set values for a given path.
  • @@ -195,22 +195,15 @@ * path will overwrite any values set by earlier invocations of the convenience methods. *
*

- * Each {@code CORSSupport} instance can be enabled or disabled, either through configuration or using the API. - * By default, when an application creates a new {@code CORSSupport.Builder} instance that builder's {@code build()} method will - * create an enabled {@code CORSSupport} object. Any subsequent explicit setting on the builder, either expressly set by an - * {@code enabled} entry in configuration passed to {@code CORSSupport.Builder.config} or set by invoking - * {@code CORSSupport.Builder.enabled} follows the familiar "latest-wins" approach. + * Each {@code CORSSupportSE} instance can be enabled or disabled, either through configuration or using the API. + * By default, when an application creates a new {@code CORSSupportSE.Builder} instance that builder's {@code build()} method will + * create an enabled {@code CORSSupportSE} object. Any subsequent explicit setting on the builder, either expressly set by an + * {@code enabled} entry in configuration passed to {@code CORSSupportSE.Builder.config} or set by invoking + * {@code CORSSupportSE.Builder.enabled} follows the familiar "latest-wins" approach. *

*

- * If the application uses a single {@code CORSSupport} instance, then the enabled setting for that instance governs the + * If the application uses a single {@code CORSSupportSE} instance, then the enabled setting for that instance governs the * entire CORS implementation for the app's endpoints. *

- *

Warning about internal classes and methods

- *

- * Note that everything in the {@code io.helidon.webserver.cors.internal} package, and any method with {@code internal} in - * its name, even if marked {@code public}, are intended for Helidon use. They are not - * intended for use by application developers but should be considered reserved for internal Helidon use and subject to change - * on short notice. - *

*/ package io.helidon.webserver.cors; diff --git a/webserver/cors/src/main/java/module-info.java b/webserver/cors/src/main/java/module-info.java index e7a86743ef7..482b698d841 100644 --- a/webserver/cors/src/main/java/module-info.java +++ b/webserver/cors/src/main/java/module-info.java @@ -26,5 +26,4 @@ requires io.helidon.webserver; exports io.helidon.webserver.cors; - exports io.helidon.webserver.cors.internal to io.helidon.microprofile.cors; } diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index d7ca0b23092..c16910ceb61 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -61,7 +61,7 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CORSSupport.Builder corsSupportBuilder = CORSSupport.builder(); + CORSSupport.Builder corsSupportBuilder = CORSSupportSE.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -71,19 +71,19 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CORSSupport.builder().config(Config.create().get("cors-setup")).build(), + CORSSupportSE.builder().config(Config.create().get("cors-setup")).build(), new GreetService()) .register(OTHER_GREETING_PATH, - CORSSupport.create(twoCORSConfig.get("cors-2-setup")), + CORSSupportSE.builder().config(twoCORSConfig.get("cors-2-setup")).build(), new GreetService("Other Hello")) .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CORSSupport.builder() + CORSSupportSE.builder() .allowOrigins("http://foo.bar", "http://bar.foo") .allowMethods("PUT") .build(), (req, resp) -> resp.status(Http.Status.OK_200).send()) .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CORSSupport.builder() + CORSSupportSE.builder() .allowOrigins("*") .allowMethods("GET") .build(), From ecb7590e37724446c85a981c3925845e07f6c037 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 15 Apr 2020 14:20:57 -0500 Subject: [PATCH 084/100] Refactory things j-u-s-t a bit. --- .../microprofile/cors/CORSSupportMP.java | 2 +- ...nConfigAggregator.java => Aggregator.java} | 95 ++++----- .../helidon/webserver/cors/CORSSupport.java | 10 +- ...iginHelper.java => CORSSupportHelper.java} | 46 ++--- .../webserver/cors/CrossOriginConfig.java | 180 ++++++++-------- .../io/helidon/webserver/cors/Loader.java | 110 ++++++++++ .../io/helidon/webserver/cors/LogHelper.java | 4 +- .../cors/MappedCrossOriginConfig.java | 194 ++++++++++++++++++ .../io/helidon/webserver/cors/Setter.java | 21 +- .../helidon/webserver/cors/package-info.java | 12 +- .../webserver/cors/CrossOriginConfigTest.java | 125 +++++++++++ .../io/helidon/webserver/cors/TestUtil.java | 5 +- .../src/test/resources/configMapperTest.yaml | 43 ++++ 13 files changed, 666 insertions(+), 181 deletions(-) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CrossOriginConfigAggregator.java => Aggregator.java} (74%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CrossOriginHelper.java => CORSSupportHelper.java} (95%) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java create mode 100644 webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java create mode 100644 webserver/cors/src/test/resources/configMapperTest.yaml diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java index 1ab324410e5..2e55b571702 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java @@ -64,7 +64,7 @@ protected Optional processRequest(RequestAdapter requestAdapter, * Not for developer user. Gets a response ready to participate in the CORS protocol. * * @param requestAdapter wrapper around the request - * @param responseAdapter wrapper around the reseponse + * @param responseAdapter wrapper around the response */ @Override protected void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java similarity index 74% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 2470484270a..5559bcd058f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfigAggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -22,9 +22,10 @@ import java.util.function.Supplier; import io.helidon.config.Config; +import io.helidon.config.ConfigValue; import io.helidon.webserver.PathMatcher; -import static io.helidon.webserver.cors.CrossOriginHelper.normalize; +import static io.helidon.webserver.cors.CORSSupportHelper.normalize; /** * Collects CORS set-up information from various sources and looks up the relevant CORS information given a request's path. @@ -35,7 +36,7 @@ *
  • when storing cross-config information, the latest invocation that specifies the same path * expression overwrites any preceding settings for the same path expression, and
  • *
  • when matching against a request's path, the code checks the path matchers in the order - * they were added to the aggregator, whether by {@link #config} or {@link #addCrossOrigin} or the {@link Setter} + * they were added to the aggregator, whether by {@link #mappedConfig} or {@link #addCrossOrigin} or the {@link Setter} * methods. * *

    @@ -44,7 +45,7 @@ * their settings in an entry with path expression {@value #PATHLESS_KEY} which matches everything. *

    *

    - * If the developer uses the {@link #config} or {@link #addCrossOrigin} methods along with the {@code Setter} + * If the developer uses the {@link #mappedConfig} or {@link #addCrossOrigin} methods along with the {@code Setter} * methods, the results are predictable but might be confusing. The {@code config} and {@code addCrossOrigin} methods * overwrite any entry with the same path expression, whereas the {@code Setter} methods update an existing * entry with path {@value #PATHLESS_KEY}, creating one if needed. So, if the config or an {@code addCrossOrigin} @@ -53,7 +54,7 @@ *

    * */ -class CrossOriginConfigAggregator implements Setter { +class Aggregator implements Setter { // Key value for the map corresponding to the cross-origin config managed by the {@link Setter} methods static final String PATHLESS_KEY = "{+}"; @@ -61,18 +62,18 @@ class CrossOriginConfigAggregator implements Setter // Records paths and configs added via addCrossOriginConfig private final Map crossOriginConfigMatchables = new LinkedHashMap<>(); - private Optional isEnabledOpt = Optional.empty(); + private boolean isEnabled = true; /** * Factory method. * * @return new CrossOriginConfigAggregatpr */ - static CrossOriginConfigAggregator create() { - return new CrossOriginConfigAggregator(); + static Aggregator create() { + return new Aggregator(); } - private CrossOriginConfigAggregator() { + private Aggregator() { } /** @@ -82,7 +83,7 @@ private CrossOriginConfigAggregator() { * @return if CORS processing should be done */ public boolean isEnabled() { - return isEnabledOpt.orElse(true); + return isEnabled; } /** @@ -91,30 +92,19 @@ public boolean isEnabled() { * @param config {@code Config} node containing * @return updated builder */ - public CrossOriginConfigAggregator config(Config config) { - /* - * Merge the newly-provided config with what we've assembled so far. We do not merge the config for a given path; - * we add paths that are not already present and override paths that are there. - */ + public Aggregator mappedConfig(Config config) { + if (config.exists()) { - Config isEnabledNode = config.get("enabled"); - if (isEnabledNode.exists()) { - // Latest explicit setting wins. - enabled(isEnabledNode.asBoolean().get()); - } else { + ConfigValue mappedConfigValue = config.as(MappedCrossOriginConfig.Builder::from); + if (mappedConfigValue.isPresent()) { + MappedCrossOriginConfig mapped = mappedConfigValue.get().build(); /* - * If there is no pre-existing setting for isEnabled, because we are processing config and the default is - * {@code enabled: true} for config, set the aggregation to enabled. + * Merge the newly-provided config with what we've assembled so far. We do not merge the config for a given path; + * we add paths that are not already present and override paths that are there. */ - if (isEnabledOpt.isEmpty()) { - enabled(true); - } - } - Config pathsNode = config.get(CrossOriginConfig.CORS_PATHS_CONFIG_KEY); - if (pathsNode.exists()) { - pathsNode.as(new CrossOriginConfig.CrossOriginConfigMapper()) - .get() - .forEach(this::addCrossOrigin); + mapped.forEach(this::addCrossOrigin); + + isEnabled = mapped.isEnabled(); } } return this; @@ -127,7 +117,7 @@ public CrossOriginConfigAggregator config(Config config) { * @param crossOrigin the cross origin information * @return updated builder */ - public CrossOriginConfigAggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) { + public Aggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) { crossOriginConfigMatchables.put(normalize(pathExpr), new FixedCrossOriginConfigMatchable(pathExpr, crossOrigin)); return this; } @@ -138,43 +128,43 @@ public CrossOriginConfigAggregator addCrossOrigin(String pathExpr, CrossOriginCo * @param value whether CORS should be enabled * @return updated builder */ - public CrossOriginConfigAggregator enabled(boolean value) { - isEnabledOpt = Optional.of(value); + public Aggregator enabled(boolean value) { + isEnabled = value; return this; } @Override - public CrossOriginConfigAggregator allowOrigins(String... origins) { + public Aggregator allowOrigins(String... origins) { pathlessCrossOriginConfigBuilder().allowOrigins(origins); return this; } @Override - public CrossOriginConfigAggregator allowHeaders(String... allowHeaders) { + public Aggregator allowHeaders(String... allowHeaders) { pathlessCrossOriginConfigBuilder().allowHeaders(allowHeaders); return this; } @Override - public CrossOriginConfigAggregator exposeHeaders(String... exposeHeaders) { + public Aggregator exposeHeaders(String... exposeHeaders) { pathlessCrossOriginConfigBuilder().exposeHeaders(exposeHeaders); return this; } @Override - public CrossOriginConfigAggregator allowMethods(String... allowMethods) { + public Aggregator allowMethods(String... allowMethods) { pathlessCrossOriginConfigBuilder().allowMethods(allowMethods); return this; } @Override - public CrossOriginConfigAggregator allowCredentials(boolean allowCredentials) { + public Aggregator allowCredentials(boolean allowCredentials) { pathlessCrossOriginConfigBuilder().allowCredentials(allowCredentials); return this; } @Override - public CrossOriginConfigAggregator maxAge(long maxAge) { + public Aggregator maxAge(long maxAge) { pathlessCrossOriginConfigBuilder().maxAge(maxAge); return this; } @@ -183,16 +173,14 @@ public CrossOriginConfigAggregator maxAge(long maxAge) { * Looks for a matching CORS config entry for the specified path among the provided CORS configuration information, returning * an {@code Optional} of the matching {@code CrossOrigin} instance for the path, if any. * - * @param path the possibly unnormalized request path to check + * @param path the unnormalized request path to check * @param secondaryLookup Supplier for CrossOrigin used if none found in config * @return Optional for the matching config, or an empty Optional if none matched */ - Optional lookupCrossOrigin(String path, Supplier> secondaryLookup) { - - Optional result = Optional.empty(); - String normalizedPath = normalize(path); + Optional lookupCrossOrigin(String path, + Supplier> secondaryLookup) { - result = findFirst(crossOriginConfigMatchables, normalizedPath) + Optional result = findFirst(crossOriginConfigMatchables, normalize(path)) .or(secondaryLookup); return result; @@ -200,10 +188,10 @@ Optional lookupCrossOrigin(String path, Supplier findFirst(Map matchables, @@ -211,13 +199,14 @@ private static Optional findFirst(Map matchable.matches(normalizedPath)) .map(CrossOriginConfigMatchable::get) + .filter(CrossOriginConfig::isEnabled) .findFirst(); } @Override public String toString() { - return "CrossOriginConfigAggregator{" + return "Aggregator{" + "crossOriginConfigMatchables=" + crossOriginConfigMatchables + ", isEnabled=" + isEnabled() + '}'; @@ -260,8 +249,8 @@ private abstract static class CrossOriginConfigMatchable { this.matcher = PathMatcher.create(pathExpr); } - boolean matches(String path) { - return matcher.match(path).matches(); + boolean matches(String unnormalizedPath) { + return matcher.match(unnormalizedPath).matches(); } abstract CrossOriginConfig get(); @@ -290,6 +279,7 @@ CrossOriginConfig get() { private static class BuildableCrossOriginConfigMatchable extends CrossOriginConfigMatchable { private final CrossOriginConfig.Builder builder; + private CrossOriginConfig config = null; BuildableCrossOriginConfigMatchable(String pathExpr, CrossOriginConfig.Builder builder) { super(pathExpr); @@ -297,7 +287,10 @@ private static class BuildableCrossOriginConfigMatchable extends CrossOriginConf } CrossOriginConfig get() { - return builder.build(); + if (config == null) { + config = builder.build(); + } + return config; } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 7d583514f51..77acb801558 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -38,7 +38,7 @@ *
  • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which * it applies, and
  • *
  • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the - * {@value CrossOriginConfigAggregator#PATHLESS_KEY} path).
  • + * {@value Aggregator#PATHLESS_KEY} path). * *

    * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. @@ -51,7 +51,7 @@ */ public abstract class CORSSupport implements Service, Handler { - private final CrossOriginHelper helper; + private final CORSSupportHelper helper; protected > CORSSupport(Builder builder) { helper = builder.helperBuilder.build(); @@ -120,8 +120,8 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques public abstract static class Builder> implements io.helidon.common.Builder, Setter> { - private final CrossOriginHelper.Builder helperBuilder = CrossOriginHelper.builder(); - private final CrossOriginConfigAggregator aggregator = helperBuilder.aggregator(); + private final CORSSupportHelper.Builder helperBuilder = CORSSupportHelper.builder(); + private final Aggregator aggregator = helperBuilder.aggregator(); protected Builder() { } @@ -139,7 +139,7 @@ protected Builder() { * @return the updated builder */ public B config(Config config) { - aggregator.config(config); + aggregator.mappedConfig(config); return me(); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java similarity index 95% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java index 24f66213ce0..6d512c62869 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java @@ -58,19 +58,14 @@ * specific to the needs of CORS support. *

    */ -class CrossOriginHelper { - - /** - * Key for the node within the CORS config indicating whether CORS support is enabled. - */ - static final String CORS_ENABLED_CONFIG_KEY = "enabled"; +class CORSSupportHelper { static final String ORIGIN_DENIED = "CORS origin is denied"; static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; static final String METHOD_NOT_IN_ALLOWED_LIST = "CORS method is not in allowed list"; static final String HEADERS_NOT_IN_ALLOWED_LIST = "CORS headers not in allowed list"; - static final Logger LOGGER = Logger.getLogger(CrossOriginHelper.class.getName()); + static final Logger LOGGER = Logger.getLogger(CORSSupportHelper.class.getName()); private static final Supplier> EMPTY_SECONDARY_SUPPLIER = Optional::empty; @@ -152,7 +147,7 @@ public enum RequestType { * @param config Config node containing CORS set-up * @return new instance based on the config */ - public static CrossOriginHelper create(Config config) { + public static CORSSupportHelper create(Config config) { return builder().config(config).build(); } @@ -161,24 +156,24 @@ public static CrossOriginHelper create(Config config) { * * @return the new instance */ - public static CrossOriginHelper create() { + public static CORSSupportHelper create() { return builder().build(); } - private final CrossOriginConfigAggregator aggregator; + private final Aggregator aggregator; private final Supplier> secondaryCrossOriginLookup; - private CrossOriginHelper() { + private CORSSupportHelper() { this(builder()); } - private CrossOriginHelper(Builder builder) { + private CORSSupportHelper(Builder builder) { aggregator = builder.aggregator; secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; } /** - * Creates a builder for a new {@code CrossOriginHelper}. + * Creates a builder for a new {@code CORSSupportHelper}. * * @return initialized builder */ @@ -187,13 +182,13 @@ public static Builder builder() { } /** - * Builder class for {@code CrossOriginHelper}s. + * Builder class for {@code CORSSupportHelper}s. */ - public static class Builder implements io.helidon.common.Builder { + public static class Builder implements io.helidon.common.Builder { private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; - private final CrossOriginConfigAggregator aggregator = CrossOriginConfigAggregator.create(); + private final Aggregator aggregator = Aggregator.create(); /** * Sets the supplier for the secondary lookup of CORS information (typically not contained in @@ -214,24 +209,24 @@ public Builder secondaryLookupSupplier(Supplier> sec * @return updated builder */ public Builder config(Config config) { - aggregator.config(config); + aggregator.mappedConfig(config); return this; } /** - * Creates the {@code CrossOriginHelper}. + * Creates the {@code CORSSupportHelper}. * - * @return initialized {@code CrossOriginHelper} + * @return initialized {@code CORSSupportHelper} */ - public CrossOriginHelper build() { - CrossOriginHelper result = new CrossOriginHelper(this); + public CORSSupportHelper build() { + CORSSupportHelper result = new CORSSupportHelper(this); - LOGGER.config(() -> String.format("CrossOriginHelper configured as: %s", result.toString())); + LOGGER.config(() -> String.format("CORSSupportHelper configured as: %s", result.toString())); return result; } - CrossOriginConfigAggregator aggregator() { + Aggregator aggregator() { return aggregator; } } @@ -276,7 +271,8 @@ public Optional processRequest(RequestAdapter requestAdapter, Respo return Optional.empty(); } - Optional crossOrigin = aggregator.lookupCrossOrigin(requestAdapter.path(), secondaryCrossOriginLookup); + Optional crossOrigin = aggregator.lookupCrossOrigin(requestAdapter.path(), + secondaryCrossOriginLookup); RequestType requestType = requestType(requestAdapter); @@ -295,7 +291,7 @@ public Optional processRequest(RequestAdapter requestAdapter, Respo @Override public String toString() { - return String.format("CrossOriginHelper{isActive=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", + return String.format("CORSSupportHelper{isActive=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", isActive(), aggregator, secondaryCrossOriginLookup == EMPTY_SECONDARY_SUPPLIER ? "(not set)" : "(set)"); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 4428d6d3315..3305bae23bb 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -17,24 +17,44 @@ package io.helidon.webserver.cors; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.function.Function; import io.helidon.config.Config; -import static io.helidon.webserver.cors.CrossOriginHelper.normalize; -import static io.helidon.webserver.cors.CrossOriginHelper.parseHeader; +import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; /** * Represents information about cross origin request sharing. + * + * Applications can create instance in two ways: + *
      + *
    • using a {@code Builder} explicitly + *

      + * Obtain a suitable builder by: + *

      + *
        + *
      • explicitly getting a builder using {@link #builder()},
      • + *
      • invoking the static {@link Builder#from} method and + * passing an existing instance of {@code CrossOriginConfig}; the resulting {@code Builder} is + * intialized using the configuration node provided, or
      • + *
      • obtaining a {@link Config} instance and invoking {@code Config.as}, passing {@code Builder#from}
      • + *
      + * and then invoke methods on the builder, finally invoking the builder's {@code build} method to create the instance. + *
    • invoking the static {@link #from} method, passing a config node containing the cross-origin information to be + * converted. + *
    • + *
    + * + * @see MappedCrossOriginConfig + * */ -public class CrossOriginConfig /* implements CrossOrigin */ { +public class CrossOriginConfig { /** - * Default cache expiration in seconds. + * Key for the node within the CORS config that contains the list of path information. */ - public static final long DEFAULT_AGE = 3600; + public static final String CORS_PATHS_CONFIG_KEY = "paths"; + /** * Header Access-Control-Allow-Headers. */ @@ -67,11 +87,14 @@ public class CrossOriginConfig /* implements CrossOrigin */ { * Header Access-Control-Request-Method. */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** - * Key for the node within the CORS config that contains the list of path information. + * Default cache expiration in seconds. */ - public static final String CORS_PATHS_CONFIG_KEY = "paths"; + public static final long DEFAULT_AGE = 3600; + private final String pathPrefix; + private final boolean enabled; private final String[] allowOrigins; private final String[] allowHeaders; private final String[] exposeHeaders; @@ -80,6 +103,8 @@ public class CrossOriginConfig /* implements CrossOrigin */ { private final long maxAge; private CrossOriginConfig(Builder builder) { + this.pathPrefix = builder.pathPrefix; + this.enabled = builder.enabled; this.allowOrigins = builder.origins; this.allowHeaders = builder.allowHeaders; this.exposeHeaders = builder.exposeHeaders; @@ -89,15 +114,37 @@ private CrossOriginConfig(Builder builder) { } /** - * - * @return a new builder for cross origin config + * @return a new builder for basic cross origin config */ public static Builder builder() { return new Builder(); } /** + * Creates a new {@code CrossOriginConfig} instance using the provided config node. * + * @param config node containing cross-origin information + * @return new {@code Basic} instance based on the configuration + */ + public static CrossOriginConfig from(Config config) { + return Builder.from(config).build(); + } + + /** + * @return the configured path prefix; defaults to a "match-everything" pattern + */ + public String pathPrefix() { + return pathPrefix; + } + + /** + * @return whether this cross-origin config is enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** * @return the allowed origins */ public String[] allowOrigins() { @@ -105,7 +152,6 @@ public String[] allowOrigins() { } /** - * * @return the allowed headers */ public String[] allowHeaders() { @@ -113,7 +159,6 @@ public String[] allowHeaders() { } /** - * * @return headers OK to expose in responses */ public String[] exposeHeaders() { @@ -121,7 +166,6 @@ public String[] exposeHeaders() { } /** - * * @return allowed methods */ public String[] allowMethods() { @@ -129,7 +173,6 @@ public String[] allowMethods() { } /** - * * @return allowed credentials */ public boolean allowCredentials() { @@ -137,7 +180,6 @@ public boolean allowCredentials() { } /** - * * @return maximum age */ public long maxAge() { @@ -151,10 +193,13 @@ private static String[] copyOf(String[] strings) { /** * Builder for {@link CrossOriginConfig}. */ - public static class Builder implements Setter, io.helidon.common.Builder { + public static class Builder implements Setter, io.helidon.common.Builder, + Function { - private static final String[] ALLOW_ALL = {"*"}; + static final String[] ALLOW_ALL = {"*"}; + private String pathPrefix = PATHLESS_KEY; // not typically used except when inside a MappedCrossOriginConfig + private boolean enabled = true; private String[] origins = ALLOW_ALL; private String[] allowHeaders = ALLOW_ALL; private String[] exposeHeaders; @@ -173,6 +218,8 @@ private Builder() { */ public static Builder from(CrossOriginConfig original) { return new Builder() + .pathPrefix(original.pathPrefix) + .enabled(original.enabled) .allowCredentials(original.allowCredentials) .allowHeaders(original.allowHeaders) .allowMethods(original.allowMethods) @@ -182,70 +229,71 @@ public static Builder from(CrossOriginConfig original) { } /** - * Sets the allowOrigins. + * Creates a new {@code Builder}instance from the specified configuration. * - * @param origins the origin value(s) - * @return updated builder + * @param config node containing cross-origin information + * @return new {@code Builder} initialized from the config */ + public static Builder from(Config config) { + return Loader.Basic.builder(config); + } + @Override - public Builder allowOrigins(String... origins) { - this.origins = copyOf(origins); - return this; + public Builder apply(Config config) { + return from(config); } /** - * Sets the allow headers. + * Updates the path prefix for this cross-origin config. * - * @param allowHeaders the allow headers value(s) + * @param pathPrefix new path prefix * @return updated builder */ + public Builder pathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + return this; + } + + String pathPrefix() { + return pathPrefix; + } + + @Override + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + @Override + public Builder allowOrigins(String... origins) { + this.origins = copyOf(origins); + return this; + } + @Override public Builder allowHeaders(String... allowHeaders) { this.allowHeaders = copyOf(allowHeaders); return this; } - /** - * Sets the expose headers. - * - * @param exposeHeaders the expose headers value(s) - * @return updated builder - */ @Override public Builder exposeHeaders(String... exposeHeaders) { this.exposeHeaders = copyOf(exposeHeaders); return this; } - /** - * Sets the allow methods. - * - * @param allowMethods the allow method value(s) - * @return updated builder - */ @Override public Builder allowMethods(String... allowMethods) { this.allowMethods = copyOf(allowMethods); return this; } - /** - * Sets the allow credentials flag. - * - * @param allowCredentials the allow credentials flag - * @return updated builder - */ + @Override public Builder allowCredentials(boolean allowCredentials) { this.allowCredentials = allowCredentials; return this; } - /** - * Sets the maximum age. - * - * @param maxAge the maximum age - * @return updated builder - */ @Override public Builder maxAge(long maxAge) { this.maxAge = maxAge; @@ -257,36 +305,4 @@ public CrossOriginConfig build() { return new CrossOriginConfig(this); } } - - /** - * Functional interface for converting a Helidon config instance to a {@code CrossOriginConfig} instance. - */ - public static class CrossOriginConfigMapper implements Function> { - - @Override - public Map apply(Config config) { - Map result = new HashMap<>(); - int i = 0; - do { - Config item = config.get(Integer.toString(i++)); - if (!item.exists()) { - break; - } - Builder builder = new Builder(); - String path = item.get("path-prefix").as(String.class).orElse(null); - item.get("allow-origins").asList(String.class).ifPresent( - s -> builder.allowOrigins(parseHeader(s).toArray(new String[]{}))); - item.get("allow-methods").asList(String.class).ifPresent( - s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); - item.get("allow-headers").asList(String.class).ifPresent( - s -> builder.allowHeaders(parseHeader(s).toArray(new String[]{}))); - item.get("expose-headers").asList(String.class).ifPresent( - s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); - item.get("allow-credentials").as(Boolean.class).ifPresent(builder::allowCredentials); - item.get("max-age").as(Long.class).ifPresent(builder::maxAge); - result.put(normalize(path), builder.build()); - } while (true); - return result; - } - } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java new file mode 100644 index 00000000000..c7cbf8caa83 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +import io.helidon.config.Config; +import io.helidon.config.ConfigValue; + +import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; +import static io.helidon.webserver.cors.CORSSupportHelper.parseHeader; +import static io.helidon.webserver.cors.CrossOriginConfig.CORS_PATHS_CONFIG_KEY; + +/** + * Loads builders from config. Intended to be invoked from {@code apply} methods defined on the basic and mapped builder classes. + */ +class Loader { + + static class Basic { + + static CrossOriginConfig.Builder builder(Config config) { + return builder(CrossOriginConfig.builder(), config); + } + + static CrossOriginConfig.Builder builder(CrossOriginConfig.Builder builder, Config config) { + config.get("enabled") + .asBoolean() + .ifPresent(builder::enabled); + config.get("path-prefix") + .asString() + .ifPresent(builder::pathPrefix); + config.get("allow-origins") + .asList(String.class) + .ifPresent( + s -> builder.allowOrigins(parseHeader(s).toArray(new String[]{}))); + config.get("allow-methods") + .asList(String.class) + .ifPresent( + s -> builder.allowMethods(parseHeader(s).toArray(new String[]{}))); + config.get("allow-headers") + .asList(String.class) + .ifPresent( + s -> builder.allowHeaders(parseHeader(s).toArray(new String[]{}))); + config.get("expose-headers") + .asList(String.class) + .ifPresent( + s -> builder.exposeHeaders(parseHeader(s).toArray(new String[]{}))); + config.get("allow-credentials") + .as(Boolean.class) + .ifPresent(builder::allowCredentials); + config.get("max-age") + .as(Long.class) + .ifPresent(builder::maxAge); + return builder; + } + } + + static class Mapped { + + static MappedCrossOriginConfig.Builder builder(Config config) { + return builder(MappedCrossOriginConfig.builder(), config); + } + + static MappedCrossOriginConfig.Builder builder(MappedCrossOriginConfig.Builder builder, Config config) { + config.get("enabled").asBoolean().ifPresent(builder::enabled); + Config pathsNode = config.get(CORS_PATHS_CONFIG_KEY); + + CrossOriginConfig.Builder allPathsBuilder = null; + int i = 0; + do { + Config item = pathsNode.get(Integer.toString(i++)); + if (!item.exists()) { + break; + } + ConfigValue basicConfigValue = item.as(CrossOriginConfig.Builder::from); + if (!basicConfigValue.isPresent()) { + continue; + } + CrossOriginConfig.Builder basicBuilder = basicConfigValue.get(); + + /* + * We generally maintain the entries in insertion order, but insert any pathless one from config last so the + * process of matching request paths against paths in the mapped CORS instance will use any more specific path + * expressions before the wildcard. + */ + if (basicBuilder.pathPrefix().equals(PATHLESS_KEY)) { + allPathsBuilder = basicBuilder; + } else { + builder.put(basicBuilder.pathPrefix(), basicBuilder); + } + } while (true); + if (allPathsBuilder != null) { + builder.put(allPathsBuilder.pathPrefix(), allPathsBuilder); + } + return builder; + } + } +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index 8c2cc9545db..cead602aa3f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -26,12 +26,12 @@ import io.helidon.common.http.Http; import io.helidon.webserver.cors.CORSSupport.RequestAdapter; -import io.helidon.webserver.cors.CrossOriginHelper.RequestType; +import io.helidon.webserver.cors.CORSSupportHelper.RequestType; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; +import static io.helidon.webserver.cors.CORSSupportHelper.LOGGER; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; -import static io.helidon.webserver.cors.CrossOriginHelper.LOGGER; class LogHelper { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java new file mode 100644 index 00000000000..3eee24e2070 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import io.helidon.config.Config; + +import static io.helidon.webserver.cors.CORSSupportHelper.normalize; + +/** + * Cross-origin {@link CrossOriginConfig} instances linked to paths, plus an {@code enabled} setting. Most developers will not + * need to use this directly from their applications. + */ +public class MappedCrossOriginConfig implements Iterable> { + + private boolean isEnabled = true; + + private final Map buildables; + + /** + * Holds both a builder for and, later, the built {@code CrossOriginConfig} instances each of which are mapped to + * a path expression. + */ + private static class Buildable { + private final CrossOriginConfig.Builder builder; + private CrossOriginConfig crossOriginConfig; + + Buildable(CrossOriginConfig.Builder builder) { + this.builder = builder; + } + + /** + * Returns the instance, building it if needed. + * + * @return the built instance + */ + CrossOriginConfig get() { + if (crossOriginConfig == null) { + crossOriginConfig = builder.build(); + } + return crossOriginConfig; + } + } + + private MappedCrossOriginConfig(Builder builder) { + this.isEnabled = builder.enabledOpt.orElse(true); + buildables = builder.builders; + + // Force building to prevent any changes to the underlying builders that could cause surprising behavior later. + buildables.forEach((path, b) -> b.get()); + } + + /** + * Returns a new builder for creating a {@code CrossOriginConfig.Mapped} instance. + * + * @return the new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new {@code Mapped} instance using the provided configuration. + * + * @param config node containing {@code Mapped} cross-origin information + * @return new {@code Mapped} instance based on the config + */ + public static MappedCrossOriginConfig from(Config config) { + return Builder.from(config).build(); + } + + @Override + public Iterator> iterator() { + return new Iterator<>() { + + private final Iterator> it = buildables.entrySet().iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Map.Entry next() { + Map.Entry next = it.next(); + return new AbstractMap.SimpleEntry<>(next.getKey(), next.getValue().get()); + } + }; + } + + /** + * Invokes the specified consumer for each entry in the mapped CORS config. + * @param consumer accepts the path and the {@code CrossOriginConfig} for processing + */ + public void forEach(BiConsumer consumer) { + buildables.forEach((path, buildable) -> consumer.accept(path, buildable.get())); + } + + /** + * Finds the {@code CrossOriginConfig} associated with the given path expression, if any. + * + * @param pathExpr path expression to match on + * @return {@code Optional} of the corresponding basic cross-origin information + */ + public CrossOriginConfig get(String pathExpr) { + Buildable b = buildables.get(normalize(pathExpr)); + return b == null ? null : b.get(); + } + + /** + * Reports whether this instance is enabled. + * @return current enabled state + */ + public boolean isEnabled() { + return isEnabled; + } + + /** + * Fluent builder for {@code Mapped} cross-origin config. + */ + public static class Builder implements io.helidon.common.Builder, Function { + + private Optional enabledOpt = Optional.empty(); + private final Map builders = new HashMap<>(); + + private Builder() { + } + + /** + * Creates a new {@code Mapped.Builder} instance using the provided configuration. + * + * @param config node containing {@code Mapped} cross-origin information + * @return new {@code Mapped.Builder} based on the config + */ + public static Builder from(Config config) { + return Loader.Mapped.builder(config); + } + + @Override + public MappedCrossOriginConfig build() { + return new MappedCrossOriginConfig(this); + } + + + @Override + public Builder apply(Config config) { + return from(config); + } + + /** + * Sets whether the resulting {@code Mapped} cross-origin config should be enabled. + * + * @param enabled true to enable; false to disable + * @return updated builder + */ + public Builder enabled(boolean enabled) { + this.enabledOpt = Optional.of(enabled); + return this; + } + + /** + * Adds a new builder to the collection, associating it with the given path. + * + * @param path the path to link with the builder + * @param builder the builder to use in building the actual {@code CrossOriginConfig} instance + * @return updated builder + */ + public Builder put(String path, CrossOriginConfig.Builder builder) { + builders.put(normalize(path), new Buildable(builder)); + return this; + } + } +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java index 74c8b6c9285..38cf5d18914 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java @@ -23,11 +23,20 @@ * @param the type of the implementing class so the fluid methods can return the correct type */ interface Setter { + + /** + * Sets whether this config should be enabled or not. + * + * @param enabled true for this config to have effect; false for it to be ignored + * @return updated setter + */ + T enabled(boolean enabled); + /** * Sets the allowOrigins. * * @param origins the origin value(s) - * @return updated builder + * @return updated setter */ T allowOrigins(String... origins); @@ -35,7 +44,7 @@ interface Setter { * Sets the allow headers. * * @param allowHeaders the allow headers value(s) - * @return updated builder + * @return updated setter */ T allowHeaders(String... allowHeaders); @@ -43,7 +52,7 @@ interface Setter { * Sets the expose headers. * * @param exposeHeaders the expose headers value(s) - * @return updated builder + * @return updated setter */ T exposeHeaders(String... exposeHeaders); @@ -51,7 +60,7 @@ interface Setter { * Sets the allow methods. * * @param allowMethods the allow method value(s) - * @return updated builder + * @return updated setter */ T allowMethods(String... allowMethods); @@ -59,7 +68,7 @@ interface Setter { * Sets the allow credentials flag. * * @param allowCredentials the allow credentials flag - * @return updated builder + * @return updated setter */ T allowCredentials(boolean allowCredentials); @@ -67,7 +76,7 @@ interface Setter { * Sets the maximum age. * * @param maxAge the maximum age - * @return updated builder + * @return updated setter */ T maxAge(long maxAge); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 87da93efb2b..d806ef7229e 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -77,10 +77,10 @@ *

    The Helidon CORS API

    * You can define your application's CORS behavior programmatically -- together with configuration if you want -- by: *
      - *
    • creating a {@link io.helidon.webserver.cors.CrossOriginConfig.Builder} instance,
    • - *
    • operating on it to create the CORS set-up you want,
    • - *
    • using the builder's {@code build()} method to create the {@code CrossOriginConfig} instance, and
    • - *
    • using the {@code CORSSupportSE.Builder} to associate a path with the {@code CrossOriginConfig} object.
    • + *
    • creating a {@link io.helidon.webserver.cors.MappedCrossOriginConfig.Builder} instance,
    • + *
    • operating on the builder to prepare the CORS set-up you want,
    • + *
    • using the builder's {@code build()} method to create the {@code CrossOriginConfig.Mapped} instance, and
    • + *
    • using the {@code CORSSupportSE.Builder} to associate a path with the {@code CrossOriginConfig.Mapped} object.
    • *
    *

    * The next example shows creating CORS information and associating it with the path {@code /cors3} within the app. @@ -123,7 +123,7 @@ * Sometimes you might want to prepare just one set of CORS information to match any path. The Helidon CORS API provides a * short-cut for this. The {@code CORSSupportSE.Builder} class supports all the mutator methods from {@code CrossOriginConfig} * such as {@code allowOrigins}, and on {@code CORSSupportSE.Builder} these methods implicitly affect the - * {@value io.helidon.webserver.cors.CrossOriginConfigAggregator#PATHLESS_KEY} path. (See the + * {@value io.helidon.webserver.cors.Aggregator#PATHLESS_KEY} path. (See the * {@link io.helidon.webserver.PathMatcher} documentation.) *

    * The following code @@ -190,7 +190,7 @@ * values which designate different paths into a single, unified group of settings. * The settings provided by the latest invocation of these methods override any previously-set values for a given path. *

  • Use of the convenience {@code CrossOriginConfig}-style methods defined by {@code Setter} affect the map entry with - * key {@value io.helidon.webserver.cors.CrossOriginConfigAggregator#PATHLESS_KEY}, updating any existing entry and + * key {@value io.helidon.webserver.cors.Aggregator#PATHLESS_KEY}, updating any existing entry and * creating one if needed. As a result, invoking {@code config} and {@code addCrossOriginConfig} methods with that * path will overwrite any values set by earlier invocations of the convenience methods.
  • * diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java new file mode 100644 index 00000000000..6cfc2ac5500 --- /dev/null +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import io.helidon.config.MissingValueException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.webserver.cors.CrossOriginConfig.Builder.ALLOW_ALL; +import static io.helidon.webserver.cors.CrossOriginConfig.DEFAULT_AGE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class CrossOriginConfigTest { + + private final static String YAML_PATH = "/configMapperTest.yaml"; + + private static Config testConfig; + + @BeforeAll + public static void loadTestConfig() { + testConfig = TestUtil.minimalConfig(ConfigSources.classpath(YAML_PATH)); + } + + @Test + public void testNarrow() { + Config node = testConfig.get("narrow"); + assertThat(node, is(notNullValue())); + assertThat(node.exists(), is(true)); + CrossOriginConfig c = node.as(CrossOriginConfig::from).get(); + + assertThat(c.isEnabled(), is(true)); + assertThat(c.allowOrigins(), arrayContaining("http://foo.bar", "http://bar.foo")); + assertThat(c.allowMethods(), arrayContaining("DELETE", "PUT")); + assertThat(c.allowHeaders(), arrayContaining("X-bar", "X-foo")); + assertThat(c.exposeHeaders(), is(emptyArray())); + assertThat(c.allowCredentials(), is(true)); + assertThat(c.maxAge(), is(-1L)); + } + + @Test + public void testMissing() { + Assertions.assertThrows(MissingValueException.class, () -> { + CrossOriginConfig basic = testConfig.get("notThere").as(CrossOriginConfig::from).get(); + }); + } + + @Test + public void testWide() { + Config node = testConfig.get("wide"); + assertThat(node, is(notNullValue())); + assertThat(node.exists(), is(true)); + CrossOriginConfig b = node.as(CrossOriginConfig::from).get(); + + assertThat(b.isEnabled(), is(false)); + assertThat(b.allowOrigins(), arrayContaining(ALLOW_ALL)); + assertThat(b.allowMethods(), arrayContaining(ALLOW_ALL)); + assertThat(b.allowHeaders(), arrayContaining(ALLOW_ALL)); + assertThat(b.exposeHeaders(), is(emptyArray())); + assertThat(b.allowCredentials(), is(false)); + assertThat(b.maxAge(), is(DEFAULT_AGE)); + } + + @Test + public void testJustDisabled() { + Config node = testConfig.get("just-disabled"); + assertThat(node, is(notNullValue())); + assertThat(node.exists(), is(true)); + CrossOriginConfig b = node.as(CrossOriginConfig::from).get(); + + assertThat(b.isEnabled(), is(false)); + } + + @Test + public void testPaths() { + Config node = testConfig.get("cors-setup"); + assertThat(node, is(notNullValue())); + assertThat(node.exists(), is(true)); + MappedCrossOriginConfig m = node.as(MappedCrossOriginConfig::from).get(); + + assertThat(m.isEnabled(), is(true)); + + CrossOriginConfig b = m.get("/cors1"); + assertThat(b, notNullValue()); + assertThat(b.isEnabled(), is(true)); + assertThat(b.allowOrigins(), arrayContaining("*")); + assertThat(b.allowMethods(), arrayContaining("*")); + assertThat(b.allowHeaders(), arrayContaining("*")); + assertThat(b.allowCredentials(), is(false)); + assertThat(b.maxAge(), is(DEFAULT_AGE)); + + b = m.get("/cors2"); + assertThat(b, notNullValue()); + assertThat(b.isEnabled(), is(true)); + assertThat(b.allowOrigins(), arrayContaining("http://foo.bar", "http://bar.foo")); + assertThat(b.allowMethods(), arrayContaining("DELETE", "PUT")); + assertThat(b.allowHeaders(), arrayContaining("X-bar", "X-foo")); + assertThat(b.allowCredentials(), is(true)); + assertThat(b.maxAge(), is(-1L)); + + assertThat(m.get("/cors3"), nullValue()); + } +} diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index c16910ceb61..a743b5ef397 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -61,7 +61,7 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CORSSupport.Builder corsSupportBuilder = CORSSupportSE.builder(); + CORSSupportSE.Builder corsSupportBuilder = CORSSupportSE.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -92,8 +92,7 @@ static Routing.Builder prepRouting() { return builder; } - private static Config minimalConfig(Supplier configSource) { - Config.Builder configBuilder = Config.builder() + static Config minimalConfig(Supplier configSource) { Config.Builder configBuilder = Config.builder() .disableEnvironmentVariablesSource() .disableSystemPropertiesSource(); configBuilder.sources(configSource); diff --git a/webserver/cors/src/test/resources/configMapperTest.yaml b/webserver/cors/src/test/resources/configMapperTest.yaml new file mode 100644 index 00000000000..2059cd2a7e6 --- /dev/null +++ b/webserver/cors/src/test/resources/configMapperTest.yaml @@ -0,0 +1,43 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# 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. +# + +cors-setup: + enabled: true # or false or omitted + paths: + - path-prefix: /cors1 + allow-origins: ["*"] + allow-methods: ["*"] + - path-prefix: /cors2 + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-bar", "X-foo"] + allow-credentials: true + max-age: -1 + +narrow: + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["DELETE", "PUT"] + allow-headers: ["X-bar", "X-foo"] + allow-credentials: true + max-age: -1 + +wide: + enabled: false + allow-origins: ["*"] + allow-methods: ["*"] + +just-disabled: + enabled: false From f98144a5cdf4736e935471cb3ef10220d157bb29 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 15 Apr 2020 16:04:28 -0500 Subject: [PATCH 085/100] Revise some JavaDoc; add convenience method for SE developers to populate a CORSSupportSE from config, typically for use as a handler passed to Routing.Rules methods --- .../microprofile/cors/package-info.java | 28 +++++++++++++++---- .../io/helidon/webserver/cors/Aggregator.java | 11 ++++++++ .../helidon/webserver/cors/CORSSupport.java | 11 ++++++++ .../helidon/webserver/cors/CORSSupportSE.java | 20 +++++++++++++ .../helidon/webserver/cors/package-info.java | 15 +--------- .../io/helidon/webserver/cors/TestUtil.java | 12 ++++---- .../cors/src/test/resources/twoCORS.yaml | 4 +++ 7 files changed, 76 insertions(+), 25 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java index 69104e983bf..cc5d231d72b 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/package-info.java @@ -15,13 +15,31 @@ */ /** - *

    CORS implementation for Helidon MicroProfile.

    + *

    Helidon MP CORS Support

    * Adding the Helidon MP CORS module to your application enables CORS support automatically, implementing the configuration in * the {@value io.helidon.microprofile.cors.CrossOriginFilter#CORS_CONFIG_KEY} section of your MicroProfile configuration. - * - * See Helidon - * CORS Support for information about the CORS configuration format. + *

    + * Many MP developers will use the {@link io.helidon.microprofile.cors.CrossOrigin} annotation on the endpoint implementations in + * their code to set up the CORS behavior, but any values in configuration will override the annotations or set up CORS for + * endpoints without the annotation. + *

    + *

    + * Here is an example of the configuration format: + *

    + *
    + *   cors:
    + *     enabled: true # this is the default
    + *     paths:
    + *       - path-prefix: /cors1
    + *         allow-origins: ["*"]
    + *         allow-methods: ["*"]
    + *       - path-prefix: /cors2
    + *         allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *         allow-methods: ["DELETE", "PUT"]
    + *         allow-headers: ["X-bar", "X-foo"]
    + *         allow-credentials: true
    + *         max-age: -1
    + * 
    * */ package io.helidon.microprofile.cors; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 5559bcd058f..d15d9db62cc 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -122,6 +122,17 @@ public Aggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) return this; } + /** + * Adds cross origin information associated with the default path expression. + * + * @param crossOrigin the cross origin information + * @return updated builder + */ + public Aggregator addPathlessCrossOrigin(CrossOriginConfig crossOrigin) { + crossOriginConfigMatchables.put(PATHLESS_KEY, new FixedCrossOriginConfigMatchable(PATHLESS_KEY, crossOrigin)); + return this; + } + /** * Sets whether the app wants to enable CORS. * diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java index 77acb801558..405f3f43ed0 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java @@ -166,6 +166,17 @@ public B addCrossOrigin(String path, CrossOriginConfig crossOrigin) { return me(); } + /** + * Adds cross origin information associated with the default path. + * + * @param crossOrigin the cross origin information + * @return updated builder + */ + public B addCrossOrigin(CrossOriginConfig crossOrigin) { + aggregator.addPathlessCrossOrigin(crossOrigin); + return me(); + } + @Override public B allowOrigins(String... origins) { aggregator.allowOrigins(origins); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java index 71aea329d35..d54393c959b 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java @@ -16,6 +16,11 @@ */ package io.helidon.webserver.cors; +import io.helidon.config.Config; +import io.helidon.config.MissingValueException; + +import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; + /** * SE implementation of {@link CORSSupport}. */ @@ -41,6 +46,21 @@ public static CORSSupportSE create() { return builder().build(); } + /** + * Creates a new {@code CORSSupportSE} instance based on the provided configuration expected to match the basic + * {@code CrossOriginConfig} format. + * + * @param config node containing the cross-origin information + * @return initialized {@code CORSSupportSE} instance + */ + public static CORSSupportSE from(Config config) { + if (!config.exists()) { + throw MissingValueException.create(config.key()); + } + Builder builder = builder().addCrossOrigin(PATHLESS_KEY, CrossOriginConfig.from(config)); + return builder.build(); + } + public static class Builder extends CORSSupport.Builder { @Override diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index d806ef7229e..6a33beaebb9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -25,20 +25,7 @@ * your application another way. You can use Helidon configuration, the Helidon CORS API, or a combination. *

    Configuration

    *

    Format

    - * CORS configuration has two top-level items: - *
      - *
    • {@code enabled} - indicates whether CORS processing should be enabled or not; default {@code true}
    • - *
    • {@code paths} - contains a list of sub-items describing the CORS set-up for one path - *
        - *
      • {@code path-prefix} - the path this entry applies to
      • - *
      • {@code allow-origins} - array of origin URL strings
      • - *
      • {@code allow-methods} - array of method name strings (uppercase)
      • - *
      • {@code allow-headers} - array of header strings
      • - *
      • {@code expose-headers} - array of header strings
      • - *
      • {@code allow-credentials} - boolean
      • - *
      • {@code max-age} - long
      • - *
    • - *
    + * CORS configuration looks like this: *

    * The {@code enabled} setting allows configuration to completely disable CORS processing, regardless of other settings in * config or programmatic set-up of CORS in the application. diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index a743b5ef397..e01650a674d 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -39,7 +39,7 @@ public class TestUtil { static final String OTHER_GREETING_PATH = "/othergreet"; static WebServer startupServerWithApps() throws InterruptedException, ExecutionException, TimeoutException { - Routing.Builder routingBuilder = TestUtil.prepRouting(); + Routing.Builder routingBuilder = prepRouting(); return startServer(0, routingBuilder); } @@ -64,6 +64,9 @@ static Routing.Builder prepRouting() { CORSSupportSE.Builder corsSupportBuilder = CORSSupportSE.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); + /* + * Use the loaded config to build a CrossOriginConfig for /cors4. + */ /* * Load a specific config for "/othergreet." */ @@ -77,12 +80,9 @@ static Routing.Builder prepRouting() { CORSSupportSE.builder().config(twoCORSConfig.get("cors-2-setup")).build(), new GreetService("Other Hello")) .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CORSSupportSE.builder() - .allowOrigins("http://foo.bar", "http://bar.foo") - .allowMethods("PUT") - .build(), + CORSSupportSE.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode (req, resp) -> resp.status(Http.Status.OK_200).send()) - .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, + .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, // handler settings in-line CORSSupportSE.builder() .allowOrigins("*") .allowMethods("GET") diff --git a/webserver/cors/src/test/resources/twoCORS.yaml b/webserver/cors/src/test/resources/twoCORS.yaml index 38d38b5cbd4..788389a73ab 100644 --- a/webserver/cors/src/test/resources/twoCORS.yaml +++ b/webserver/cors/src/test/resources/twoCORS.yaml @@ -22,4 +22,8 @@ cors-2-setup: allow-credentials: true max-age: -1 +somewhat-restrictive: + allow-origins: ["http://foo.bar", "http://bar.foo"] + allow-methods: ["PUT"] + # Purposefully exclude /cors1. /cors3 information is added programmatically. From e0d148d8cdeabe8c426b1c268ae40ce7e073da2f Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 15 Apr 2020 17:18:15 -0500 Subject: [PATCH 086/100] Clean up package-info --- .../helidon/webserver/cors/package-info.java | 162 +++++------------- 1 file changed, 42 insertions(+), 120 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 6a33beaebb9..5223a1090ca 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -26,48 +26,59 @@ *

    Configuration

    *

    Format

    * CORS configuration looks like this: - *

    - * The {@code enabled} setting allows configuration to completely disable CORS processing, regardless of other settings in - * config or programmatic set-up of CORS in the application. - *

    + *
    + * enabled: true    # the default
    + * allow-origins: ["http://foo.bar", "http://bar.foo"]
    + * allow-methods: ["DELETE", "PUT"]
    + * allow-headers: ["X-bar", "X-foo"]
    + * allow-credentials: true
    + * max-age: -1
    + * 
    *

    Finding and applying CORS configuration

    * Although Helidon prescribes the CORS config format, you can put it wherever you want in your application's configuration * file. Your application code will retrieve the CORS config from its location within your configuration and then use that - * config node with the {@link io.helidon.webserver.cors.CORSSupportSE.Builder} in preparing CORS support for your app. + * config node to prepare CORS support for your app. * - * If you set up this configuration + * For example, if you set up this configuration *
    - *   my-cors:
    - *     paths:
    - *       - path-prefix: /cors1
    - *         allow-origins: ["*"]
    - *         allow-methods: ["*"]
    - *       - path-prefix: /cors2
    - *         allow-origins: ["http://foo.bar", "http://bar.foo"]
    - *         allow-methods: ["DELETE", "PUT"]
    - *         allow-headers: ["X-bar", "X-foo"]
    - *         allow-credentials: true
    - *         max-age: -1
    + * narrow:
    + *   allow-origins: ["http://foo.bar", "http://bar.foo"]
    + *   allow-methods: ["DELETE", "PUT"]
    + *   allow-headers: ["X-bar", "X-foo"]
    + *   allow-credentials: true
    + *   max-age: -1
    + *
    + * wide:
    + *   enabled: false
    + *   allow-origins: ["*"]
    + *   allow-methods: ["*"]
    + *
    + * just-disabled:
    + *   enabled: false
      * 
    *

    * in a resource called {@code myApp.yaml} then the following code would apply it to your app: *

    - *
    + *     
    {@code
      *         Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build();
      *         Routing.Builder builder = Routing.builder()
    - *                 .register("/myapp",
    - *                           CORSSupportSE.builder()
    - *                                      .config(myAppConfig.get("my-cors"))
    - *                                      .build(),
    - *                           new MyApp());
    - *     
    + * .any("/greet", + * CORSSupportSE.from(myAppConfig.get("narrow")), + * (req, resp) -> resp.status(Http.Status.OK_200).send()) + * .get("/greet", + * CORSSupportSE.from(myAppConfig.get("wide")), + * (req, resp) -> resp.status(Http.Status.OK_200).send("Hello, World!")); + * + * }
    + * This sets up more restrictive CORS behavior for more sensitive HTTP methods ({@code PUT} for example and more liberal CORS + * behavior for {@code GET}. + * *

    The Helidon CORS API

    * You can define your application's CORS behavior programmatically -- together with configuration if you want -- by: *
      - *
    • creating a {@link io.helidon.webserver.cors.MappedCrossOriginConfig.Builder} instance,
    • + *
    • creating a {@link io.helidon.webserver.cors.CrossOriginConfig.Builder} instance,
    • *
    • operating on the builder to prepare the CORS set-up you want,
    • - *
    • using the builder's {@code build()} method to create the {@code CrossOriginConfig.Mapped} instance, and
    • - *
    • using the {@code CORSSupportSE.Builder} to associate a path with the {@code CrossOriginConfig.Mapped} object.
    • + *
    • using the builder's {@code build()} method to create the {@code CrossOriginConfig} instance, and
    • *
    *

    * The next example shows creating CORS information and associating it with the path {@code /cors3} within the app. @@ -85,102 +96,17 @@ * new MyApp()); * * Notice that you pass two services to the {@code register} method: the {@code CORSSupportSE} instance and your app - * instance. Helidon will process requests to the path you specify with those services in that order. + * instance. Helidon will process requests to the path you specify with those services in that order. Also, note that you have + * to make sure you use the same path in this API call and in your {@code MyApp} service if you adjust the routing there. *

    * Invoke {@code addCrossOrigin} multiple times to link more paths with CORS configuration. You can reuse one {@code * CrossOriginConfig} object with more than one path if that meets your needs. *

    *

    - * The following example shows how you can combine configuration and the API. To help with readability as things get more - * complicated, this example saves the {@code CORSSupportSE.Builder} in a variable rather than constructing it in-line when - * invoking {@code register}: - *

    - *
    - *         CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder()
    - *                  .config(myAppConfig.get("my-cors"))
    - *                  .addCrossOrigin("/cors3", corsFORCORS3);
    - *
    - *         Routing.Builder builder = Routing.builder()
    - *                 .register("/myapp",
    - *                           corsBuilder.build(),
    - *                           new MyApp());
    - * 
    - * - *

    Convenience API for the "match-all" path

    - * Sometimes you might want to prepare just one set of CORS information to match any path. The Helidon CORS API provides a - * short-cut for this. The {@code CORSSupportSE.Builder} class supports all the mutator methods from {@code CrossOriginConfig} - * such as {@code allowOrigins}, and on {@code CORSSupportSE.Builder} these methods implicitly affect the - * {@value io.helidon.webserver.cors.Aggregator#PATHLESS_KEY} path. (See the - * {@link io.helidon.webserver.PathMatcher} documentation.) - *

    - * The following code - *

    - *         CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder()
    - *             .allowOrigins("http://foo.bar", "http://bar.foo")
    - *             .allowMethods("DELETE", "PUT");
    - * 
    - * has the same effect as this more verbose version: - *
    - *         CrossOriginConfig configForAll = CrossOriginConfig.builder()
    - *             .allowOrigins("http://foo.bar", "http://bar.foo")
    - *             .allowMethods("DELETE", "PUT")
    - *             .build();
    - *         CORSSupportSE.Builder corsBuilder = CORSSupportSE.builder()
    - *                 .addCrossOrigin("{+}", configForAll);
    - * 
    - *

    {@code CORSSupportSE} as a handler

    - * The previous examples use a {@code CORSSupportSE} instance as a Helidon {@link io.helidon.webserver.Service} which you can - * register with the routing rules. You can also use a {@code CORSSupportSE} object as a {@link io.helidon.webserver.Handler} in - * setting up the routing rules for an HTTP method and path. The next example sets up CORS processing for the {@code PUT} and - * {@code OPTIONS} HTTP methods on the {@code /cors4} path within the app. The application code for both simply accepts the - * request graciously and replies with success: - *
    {@code
    - *         CORSSupportSE cors4Support = CORSSupportSE.builder()
    - *                 .allowOrigins("http://foo.bar", "http://bar.foo")
    - *                 .allowMethods("PUT")
    - *                 .build();
    - *         Routing.Builder builder = Routing.builder()
    - *                 .put("/cors4",
    - *                      cors4Support,
    - *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
    - *                 .options("/cors4",
    - *                      cors4Support,
    - *                      (req, resp) -> resp.status(Http.Status.OK_200).send());
    - * }
    * Remember that the CORS protocol uses the {@code OPTIONS} HTTP method for preflight requests. If you use the handler-based - * methods on {@code Routing.Builder} be sure to invoke the {@code options} method as well to set up routing for {@code OPTIONS} - * requests. You could invoke the {@code any} method as a short-cut. - *

    - * You can do this multiple times and even combine it with service registrations: + * methods on {@code Routing.Builder} be sure to invoke the {@code options} method as well (or {code any}) to set up routing for + * {@code OPTIONS} requests. *

    - *
    {@code
    - *         Routing.Builder builder = Routing.builder()
    - *                 .put("/cors4",
    - *                      cors4Support,
    - *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
    - *                 .options("/cors4",
    - *                      cors4Support,
    - *                      (req, resp) -> resp.status(Http.Status.OK_200).send())
    - *                 .get("/cors4",
    - *                      CORSSupportSE.builder()
    - *                               .allowOrigins("*")
    - *                               .minAge(-1),
    - *                      (req, resp) -> resp.send("Hello, World!"))
    - *                 .register(CORSSupportSE.fromConfig());
    - * }
    - *

    Resolving conflicting settings

    - * With so many ways of preparing CORS information, conflicts can arise. The {@code CORSSupportSE.Builder} resolves conflicts in - * CORS set-up as if using a {@code Map} to store all the information: - *
      - *
    • Multiple invocations of the {@code CORSSupportSE.Builder} {@code config}, - * {@code addCrossOrigin}, and "match-any" methods (from {@link io.helidon.webserver.cors.Setter}) effectively merge - * values which designate different paths into a single, unified group of settings. - * The settings provided by the latest invocation of these methods override any previously-set values for a given path.
    • - *
    • Use of the convenience {@code CrossOriginConfig}-style methods defined by {@code Setter} affect the map entry with - * key {@value io.helidon.webserver.cors.Aggregator#PATHLESS_KEY}, updating any existing entry and - * creating one if needed. As a result, invoking {@code config} and {@code addCrossOriginConfig} methods with that - * path will overwrite any values set by earlier invocations of the convenience methods.
    • - *
    *

    * Each {@code CORSSupportSE} instance can be enabled or disabled, either through configuration or using the API. * By default, when an application creates a new {@code CORSSupportSE.Builder} instance that builder's {@code build()} method will @@ -188,9 +114,5 @@ * {@code enabled} entry in configuration passed to {@code CORSSupportSE.Builder.config} or set by invoking * {@code CORSSupportSE.Builder.enabled} follows the familiar "latest-wins" approach. *

    - *

    - * If the application uses a single {@code CORSSupportSE} instance, then the enabled setting for that instance governs the - * entire CORS implementation for the app's endpoints. - *

    */ package io.helidon.webserver.cors; From 1778c63925bd113705a55cf23092d00c9e90584f Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 06:32:07 -0500 Subject: [PATCH 087/100] Review comment: use camel case everywhere, even with acronyms --- ...{CORSSupportMP.java => CorsSupportMP.java} | 16 ++++----- .../microprofile/cors/CrossOriginFilter.java | 8 ++--- .../io/helidon/webserver/cors/Aggregator.java | 2 +- .../{CORSSupport.java => CorsSupport.java} | 18 +++++----- ...portHelper.java => CorsSupportHelper.java} | 34 +++++++++---------- ...{CORSSupportSE.java => CorsSupportSE.java} | 24 ++++++------- .../io/helidon/webserver/cors/Loader.java | 2 +- .../io/helidon/webserver/cors/LogHelper.java | 6 ++-- .../cors/MappedCrossOriginConfig.java | 2 +- .../webserver/cors/SERequestAdapter.java | 4 +-- .../webserver/cors/SEResponseAdapter.java | 8 ++--- .../io/helidon/webserver/cors/Setter.java | 2 +- .../helidon/webserver/cors/package-info.java | 20 +++++------ .../io/helidon/webserver/cors/TestUtil.java | 10 +++--- 14 files changed, 78 insertions(+), 78 deletions(-) rename microprofile/cors/src/main/java/io/helidon/microprofile/cors/{CORSSupportMP.java => CorsSupportMP.java} (93%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CORSSupport.java => CorsSupport.java} (94%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CORSSupportHelper.java => CorsSupportHelper.java} (96%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CORSSupportSE.java => CorsSupportSE.java} (72%) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java similarity index 93% rename from microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java rename to microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java index 2e55b571702..1c159001e50 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CORSSupportMP.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java @@ -26,23 +26,23 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import io.helidon.webserver.cors.CORSSupport; +import io.helidon.webserver.cors.CorsSupport; import io.helidon.webserver.cors.CrossOriginConfig; /** - * MP implementation of {@link CORSSupport}. + * MP implementation of {@link CorsSupport}. */ -class CORSSupportMP extends CORSSupport { +class CorsSupportMP extends CorsSupport { /** * - * @return a new builder of CORSSupportMP + * @return a new builder of CorsSupportMP */ public static Builder builder() { return new Builder(); } - private CORSSupportMP(Builder builder) { + private CorsSupportMP(Builder builder) { super(builder); } @@ -71,11 +71,11 @@ protected void prepareResponse(RequestAdapter requestAdapter, Response super.prepareResponse(requestAdapter, responseAdapter); } - static class Builder extends CORSSupport.Builder { + static class Builder extends CorsSupport.Builder { @Override - public CORSSupportMP build() { - return new CORSSupportMP(this); + public CorsSupportMP build() { + return new CorsSupportMP(this); } @Override diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index a03b3109e26..d4e4b411223 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -35,8 +35,8 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; -import io.helidon.microprofile.cors.CORSSupportMP.RequestAdapterMP; -import io.helidon.microprofile.cors.CORSSupportMP.ResponseAdapterMP; +import io.helidon.microprofile.cors.CorsSupportMP.RequestAdapterMP; +import io.helidon.microprofile.cors.CorsSupportMP.ResponseAdapterMP; import io.helidon.webserver.cors.CrossOriginConfig; import org.eclipse.microprofile.config.ConfigProvider; @@ -59,12 +59,12 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private final CORSSupportMP cors; + private final CorsSupportMP cors; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - cors = CORSSupportMP.builder().config(config.get(CORS_CONFIG_KEY)) + cors = CorsSupportMP.builder().config(config.get(CORS_CONFIG_KEY)) .secondaryLookupSupplier(this::crossOriginFromAnnotationSupplier) .build(); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index d15d9db62cc..f638e93958a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -25,7 +25,7 @@ import io.helidon.config.ConfigValue; import io.helidon.webserver.PathMatcher; -import static io.helidon.webserver.cors.CORSSupportHelper.normalize; +import static io.helidon.webserver.cors.CorsSupportHelper.normalize; /** * Collects CORS set-up information from various sources and looks up the relevant CORS information given a request's path. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java similarity index 94% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java index 405f3f43ed0..ef668ca1b20 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java @@ -31,7 +31,7 @@ * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon * services (such as OpenAPI and metrics). *

    - * The caller can set up the {@code CORSSupport} in a combination of these ways: + * The caller can set up the {@code CorsSupport} in a combination of these ways: *

    *
      *
    • from a {@link Config} node supplied programmatically,
    • @@ -44,16 +44,16 @@ * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. *

      *

      - * If none of these sources is used, the {@code CORSSupport} applies defaults as described for + * If none of these sources is used, the {@code CorsSupport} applies defaults as described for * {@link CrossOriginConfig}. *

      * */ -public abstract class CORSSupport implements Service, Handler { +public abstract class CorsSupport implements Service, Handler { - private final CORSSupportHelper helper; + private final CorsSupportHelper helper; - protected > CORSSupport(Builder builder) { + protected > CorsSupport(Builder builder) { helper = builder.helperBuilder.build(); } @@ -112,15 +112,15 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques } /** - * Builder for {@code CORSSupport} instances. + * Builder for {@code CorsSupport} instances. * - * @param specific subtype of {@code CORSSupport} the builder creates + * @param specific subtype of {@code CorsSupport} the builder creates * @param type of the builder */ - public abstract static class Builder> implements io.helidon.common.Builder, + public abstract static class Builder> implements io.helidon.common.Builder, Setter> { - private final CORSSupportHelper.Builder helperBuilder = CORSSupportHelper.builder(); + private final CorsSupportHelper.Builder helperBuilder = CorsSupportHelper.builder(); private final Aggregator aggregator = helperBuilder.aggregator(); protected Builder() { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java similarity index 96% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java index 6d512c62869..6f62a986762 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java @@ -32,8 +32,8 @@ import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; import io.helidon.config.Config; -import io.helidon.webserver.cors.CORSSupport.RequestAdapter; -import io.helidon.webserver.cors.CORSSupport.ResponseAdapter; +import io.helidon.webserver.cors.CorsSupport.RequestAdapter; +import io.helidon.webserver.cors.CorsSupport.ResponseAdapter; import io.helidon.webserver.cors.LogHelper.Headers; import static io.helidon.common.http.Http.Header.HOST; @@ -58,14 +58,14 @@ * specific to the needs of CORS support. *

      */ -class CORSSupportHelper { +class CorsSupportHelper { static final String ORIGIN_DENIED = "CORS origin is denied"; static final String ORIGIN_NOT_IN_ALLOWED_LIST = "CORS origin is not in allowed list"; static final String METHOD_NOT_IN_ALLOWED_LIST = "CORS method is not in allowed list"; static final String HEADERS_NOT_IN_ALLOWED_LIST = "CORS headers not in allowed list"; - static final Logger LOGGER = Logger.getLogger(CORSSupportHelper.class.getName()); + static final Logger LOGGER = Logger.getLogger(CorsSupportHelper.class.getName()); private static final Supplier> EMPTY_SECONDARY_SUPPLIER = Optional::empty; @@ -147,7 +147,7 @@ public enum RequestType { * @param config Config node containing CORS set-up * @return new instance based on the config */ - public static CORSSupportHelper create(Config config) { + public static CorsSupportHelper create(Config config) { return builder().config(config).build(); } @@ -156,24 +156,24 @@ public static CORSSupportHelper create(Config config) { * * @return the new instance */ - public static CORSSupportHelper create() { + public static CorsSupportHelper create() { return builder().build(); } private final Aggregator aggregator; private final Supplier> secondaryCrossOriginLookup; - private CORSSupportHelper() { + private CorsSupportHelper() { this(builder()); } - private CORSSupportHelper(Builder builder) { + private CorsSupportHelper(Builder builder) { aggregator = builder.aggregator; secondaryCrossOriginLookup = builder.secondaryCrossOriginLookup; } /** - * Creates a builder for a new {@code CORSSupportHelper}. + * Creates a builder for a new {@code CorsSupportHelper}. * * @return initialized builder */ @@ -182,9 +182,9 @@ public static Builder builder() { } /** - * Builder class for {@code CORSSupportHelper}s. + * Builder class for {@code CorsSupportHelper}s. */ - public static class Builder implements io.helidon.common.Builder { + public static class Builder implements io.helidon.common.Builder { private Supplier> secondaryCrossOriginLookup = EMPTY_SECONDARY_SUPPLIER; @@ -214,14 +214,14 @@ public Builder config(Config config) { } /** - * Creates the {@code CORSSupportHelper}. + * Creates the {@code CorsSupportHelper}. * - * @return initialized {@code CORSSupportHelper} + * @return initialized {@code CorsSupportHelper} */ - public CORSSupportHelper build() { - CORSSupportHelper result = new CORSSupportHelper(this); + public CorsSupportHelper build() { + CorsSupportHelper result = new CorsSupportHelper(this); - LOGGER.config(() -> String.format("CORSSupportHelper configured as: %s", result.toString())); + LOGGER.config(() -> String.format("CorsSupportHelper configured as: %s", result.toString())); return result; } @@ -291,7 +291,7 @@ public Optional processRequest(RequestAdapter requestAdapter, Respo @Override public String toString() { - return String.format("CORSSupportHelper{isActive=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", + return String.format("CorsSupportHelper{isActive=%s, crossOriginConfigs=%s, secondaryCrossOriginLookup=%s}", isActive(), aggregator, secondaryCrossOriginLookup == EMPTY_SECONDARY_SUPPLIER ? "(not set)" : "(set)"); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java similarity index 72% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java index d54393c959b..0edbbb8c57c 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CORSSupportSE.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java @@ -22,17 +22,17 @@ import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; /** - * SE implementation of {@link CORSSupport}. + * SE implementation of {@link CorsSupport}. */ -public class CORSSupportSE extends CORSSupport { +public class CorsSupportSE extends CorsSupport { - private CORSSupportSE(Builder builder) { + private CorsSupportSE(Builder builder) { super(builder); } /** * - * @return new builder for CORSSupportSE + * @return new builder for CorsSupportSE */ public static Builder builder() { return new Builder(); @@ -40,20 +40,20 @@ public static Builder builder() { /** * - * @return new CORSSupportSE with default settings + * @return new CorsSupportSE with default settings */ - public static CORSSupportSE create() { + public static CorsSupportSE create() { return builder().build(); } /** - * Creates a new {@code CORSSupportSE} instance based on the provided configuration expected to match the basic + * Creates a new {@code CorsSupportSE} instance based on the provided configuration expected to match the basic * {@code CrossOriginConfig} format. * * @param config node containing the cross-origin information - * @return initialized {@code CORSSupportSE} instance + * @return initialized {@code CorsSupportSE} instance */ - public static CORSSupportSE from(Config config) { + public static CorsSupportSE from(Config config) { if (!config.exists()) { throw MissingValueException.create(config.key()); } @@ -61,11 +61,11 @@ public static CORSSupportSE from(Config config) { return builder.build(); } - public static class Builder extends CORSSupport.Builder { + public static class Builder extends CorsSupport.Builder { @Override - public CORSSupportSE build() { - return new CORSSupportSE(this); + public CorsSupportSE build() { + return new CorsSupportSE(this); } @Override diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java index c7cbf8caa83..e6683ea9cdf 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java @@ -20,7 +20,7 @@ import io.helidon.config.ConfigValue; import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; -import static io.helidon.webserver.cors.CORSSupportHelper.parseHeader; +import static io.helidon.webserver.cors.CorsSupportHelper.parseHeader; import static io.helidon.webserver.cors.CrossOriginConfig.CORS_PATHS_CONFIG_KEY; /** diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index cead602aa3f..ef2c20f416f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -25,12 +25,12 @@ import java.util.logging.Level; import io.helidon.common.http.Http; -import io.helidon.webserver.cors.CORSSupport.RequestAdapter; -import io.helidon.webserver.cors.CORSSupportHelper.RequestType; +import io.helidon.webserver.cors.CorsSupport.RequestAdapter; +import io.helidon.webserver.cors.CorsSupportHelper.RequestType; import static io.helidon.common.http.Http.Header.HOST; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.webserver.cors.CORSSupportHelper.LOGGER; +import static io.helidon.webserver.cors.CorsSupportHelper.LOGGER; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; class LogHelper { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java index 3eee24e2070..ddf9390576c 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java @@ -26,7 +26,7 @@ import io.helidon.config.Config; -import static io.helidon.webserver.cors.CORSSupportHelper.normalize; +import static io.helidon.webserver.cors.CorsSupportHelper.normalize; /** * Cross-origin {@link CrossOriginConfig} instances linked to paths, plus an {@code enabled} setting. Most developers will not diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java index 9a1aac88bbf..bd969a8b52e 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java @@ -22,9 +22,9 @@ import io.helidon.webserver.ServerRequest; /** - * Helidon SE implementation of {@link CORSSupport.RequestAdapter}. + * Helidon SE implementation of {@link CorsSupport.RequestAdapter}. */ -class SERequestAdapter implements CORSSupport.RequestAdapter { +class SERequestAdapter implements CorsSupport.RequestAdapter { private final ServerRequest request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java index 01b32cbe8da..1fbe6d26953 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java @@ -20,9 +20,9 @@ import io.helidon.webserver.ServerResponse; /** - * SE implementation of {@link CORSSupport.ResponseAdapter}. + * SE implementation of {@link CorsSupport.ResponseAdapter}. */ -class SEResponseAdapter implements CORSSupport.ResponseAdapter { +class SEResponseAdapter implements CorsSupport.ResponseAdapter { private final ServerResponse serverResponse; @@ -31,13 +31,13 @@ class SEResponseAdapter implements CORSSupport.ResponseAdapter { } @Override - public CORSSupport.ResponseAdapter header(String key, String value) { + public CorsSupport.ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public CORSSupport.ResponseAdapter header(String key, Object value) { + public CorsSupport.ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java index 38cf5d18914..fe0ce3ede6f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java @@ -17,7 +17,7 @@ package io.helidon.webserver.cors; /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CORSSupport.Builder} for assiging CORS-related + * Defines common behavior between {@code CrossOriginConfig} and {@link CorsSupport.Builder} for assiging CORS-related * attributes. * * @param the type of the implementing class so the fluid methods can return the correct type diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 5223a1090ca..0fa76fdb66a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -18,7 +18,7 @@ /** *

      Helidon SE CORS Support

      *

      - * Use {@link io.helidon.webserver.cors.CORSSupportSE} and its {@link io.helidon.webserver.cors.CORSSupportSE.Builder} to add CORS + * Use {@link io.helidon.webserver.cors.CorsSupportSE} and its {@link io.helidon.webserver.cors.CorsSupportSE.Builder} to add CORS * handling to resources in your application. *

      * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for @@ -63,10 +63,10 @@ * Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build(); * Routing.Builder builder = Routing.builder() * .any("/greet", - * CORSSupportSE.from(myAppConfig.get("narrow")), + * CorsSupportSE.from(myAppConfig.get("narrow")), * (req, resp) -> resp.status(Http.Status.OK_200).send()) * .get("/greet", - * CORSSupportSE.from(myAppConfig.get("wide")), + * CorsSupportSE.from(myAppConfig.get("wide")), * (req, resp) -> resp.status(Http.Status.OK_200).send("Hello, World!")); * * } @@ -90,12 +90,12 @@ * * Routing.Builder builder = Routing.builder() * .register("/myapp", - * CORSSupportSE.builder() + * CorsSupportSE.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app * .build(), * new MyApp()); * - * Notice that you pass two services to the {@code register} method: the {@code CORSSupportSE} instance and your app + * Notice that you pass two services to the {@code register} method: the {@code CorsSupportSE} instance and your app * instance. Helidon will process requests to the path you specify with those services in that order. Also, note that you have * to make sure you use the same path in this API call and in your {@code MyApp} service if you adjust the routing there. *

      @@ -108,11 +108,11 @@ * {@code OPTIONS} requests. *

      *

      - * Each {@code CORSSupportSE} instance can be enabled or disabled, either through configuration or using the API. - * By default, when an application creates a new {@code CORSSupportSE.Builder} instance that builder's {@code build()} method will - * create an enabled {@code CORSSupportSE} object. Any subsequent explicit setting on the builder, either expressly set by an - * {@code enabled} entry in configuration passed to {@code CORSSupportSE.Builder.config} or set by invoking - * {@code CORSSupportSE.Builder.enabled} follows the familiar "latest-wins" approach. + * Each {@code CorsSupportSE} instance can be enabled or disabled, either through configuration or using the API. + * By default, when an application creates a new {@code CorsSupportSE.Builder} instance that builder's {@code build()} method will + * create an enabled {@code CorsSupportSE} object. Any subsequent explicit setting on the builder, either expressly set by an + * {@code enabled} entry in configuration passed to {@code CorsSupportSE.Builder.config} or set by invoking + * {@code CorsSupportSE.Builder.enabled} follows the familiar "latest-wins" approach. *

      */ package io.helidon.webserver.cors; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index e01650a674d..8c07c509cce 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -61,7 +61,7 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CORSSupportSE.Builder corsSupportBuilder = CORSSupportSE.builder(); + CorsSupportSE.Builder corsSupportBuilder = CorsSupportSE.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -74,16 +74,16 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CORSSupportSE.builder().config(Config.create().get("cors-setup")).build(), + CorsSupportSE.builder().config(Config.create().get("cors-setup")).build(), new GreetService()) .register(OTHER_GREETING_PATH, - CORSSupportSE.builder().config(twoCORSConfig.get("cors-2-setup")).build(), + CorsSupportSE.builder().config(twoCORSConfig.get("cors-2-setup")).build(), new GreetService("Other Hello")) .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CORSSupportSE.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode + CorsSupportSE.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode (req, resp) -> resp.status(Http.Status.OK_200).send()) .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, // handler settings in-line - CORSSupportSE.builder() + CorsSupportSE.builder() .allowOrigins("*") .allowMethods("GET") .build(), From 4238821f36fb5c9130cf26a94043d3b54d8f8426 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 06:34:13 -0500 Subject: [PATCH 088/100] Review comment: method in package-local class was public; change to package-local also --- .../main/java/io/helidon/microprofile/cors/CorsSupportMP.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java index 1c159001e50..965088b99ee 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java @@ -38,7 +38,7 @@ class CorsSupportMP extends CorsSupport { * * @return a new builder of CorsSupportMP */ - public static Builder builder() { + static Builder builder() { return new Builder(); } From 44f1dea4d6692c94b20d26bc643e6347ba74d8b9 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 06:51:51 -0500 Subject: [PATCH 089/100] More camel-casing changes --- ...{CorsSupportMP.java => CorsSupportMp.java} | 22 +++++++++---------- .../microprofile/cors/CrossOriginFilter.java | 12 +++++----- .../helidon/webserver/cors/CorsSupport.java | 4 ++-- ...{CorsSupportSE.java => CorsSupportSe.java} | 22 +++++++++---------- ...uestAdapter.java => RequestAdapterSe.java} | 4 ++-- ...nseAdapter.java => ResponseAdapterSe.java} | 4 ++-- .../helidon/webserver/cors/package-info.java | 20 ++++++++--------- ...actCORSTest.java => AbstractCorsTest.java} | 6 ++--- ...vice.java => AbstractCorsTestService.java} | 2 +- .../cors/{CORSTest.java => CorsTest.java} | 2 +- ...estServices.java => CorsTestServices.java} | 2 +- .../cors/TestHandlerRegistration.java | 2 -- ...RSConfigs.java => TestTwoCorsConfigs.java} | 2 +- .../io/helidon/webserver/cors/TestUtil.java | 14 ++++++------ 14 files changed, 58 insertions(+), 60 deletions(-) rename microprofile/cors/src/main/java/io/helidon/microprofile/cors/{CorsSupportMP.java => CorsSupportMp.java} (90%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{CorsSupportSE.java => CorsSupportSe.java} (76%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{SERequestAdapter.java => RequestAdapterSe.java} (93%) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{SEResponseAdapter.java => ResponseAdapterSe.java} (93%) rename webserver/cors/src/test/java/io/helidon/webserver/cors/{AbstractCORSTest.java => AbstractCorsTest.java} (98%) rename webserver/cors/src/test/java/io/helidon/webserver/cors/{AbstractCORSTestService.java => AbstractCorsTestService.java} (95%) rename webserver/cors/src/test/java/io/helidon/webserver/cors/{CORSTest.java => CorsTest.java} (98%) rename webserver/cors/src/test/java/io/helidon/webserver/cors/{CORSTestServices.java => CorsTestServices.java} (98%) rename webserver/cors/src/test/java/io/helidon/webserver/cors/{TestTwoCORSConfigs.java => TestTwoCorsConfigs.java} (97%) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java similarity index 90% rename from microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java rename to microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java index 965088b99ee..707daa428f0 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMP.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java @@ -32,17 +32,17 @@ /** * MP implementation of {@link CorsSupport}. */ -class CorsSupportMP extends CorsSupport { +class CorsSupportMp extends CorsSupport { /** * - * @return a new builder of CorsSupportMP + * @return a new builder of CorsSupportMp */ static Builder builder() { return new Builder(); } - private CorsSupportMP(Builder builder) { + private CorsSupportMp(Builder builder) { super(builder); } @@ -71,11 +71,11 @@ protected void prepareResponse(RequestAdapter requestAdapter, Response super.prepareResponse(requestAdapter, responseAdapter); } - static class Builder extends CorsSupport.Builder { + static class Builder extends CorsSupport.Builder { @Override - public CorsSupportMP build() { - return new CorsSupportMP(this); + public CorsSupportMp build() { + return new CorsSupportMp(this); } @Override @@ -91,11 +91,11 @@ protected Builder secondaryLookupSupplier( } } - static class RequestAdapterMP implements RequestAdapter { + static class RequestAdapterMp implements RequestAdapter { private final ContainerRequestContext requestContext; - RequestAdapterMP(ContainerRequestContext requestContext) { + RequestAdapterMp(ContainerRequestContext requestContext) { this.requestContext = requestContext; } @@ -134,15 +134,15 @@ public void next() { } } - static class ResponseAdapterMP implements ResponseAdapter { + static class ResponseAdapterMp implements ResponseAdapter { private final MultivaluedMap headers; - ResponseAdapterMP(ContainerResponseContext responseContext) { + ResponseAdapterMp(ContainerResponseContext responseContext) { headers = responseContext.getHeaders(); } - ResponseAdapterMP() { + ResponseAdapterMp() { headers = new MultivaluedHashMap<>(); } diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index d4e4b411223..44e19243c89 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -35,8 +35,8 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.config.Config; -import io.helidon.microprofile.cors.CorsSupportMP.RequestAdapterMP; -import io.helidon.microprofile.cors.CorsSupportMP.ResponseAdapterMP; +import io.helidon.microprofile.cors.CorsSupportMp.RequestAdapterMp; +import io.helidon.microprofile.cors.CorsSupportMp.ResponseAdapterMp; import io.helidon.webserver.cors.CrossOriginConfig; import org.eclipse.microprofile.config.ConfigProvider; @@ -59,25 +59,25 @@ class CrossOriginFilter implements ContainerRequestFilter, ContainerResponseFilt @Context private ResourceInfo resourceInfo; - private final CorsSupportMP cors; + private final CorsSupportMp cors; CrossOriginFilter() { Config config = (Config) ConfigProvider.getConfig(); - cors = CorsSupportMP.builder().config(config.get(CORS_CONFIG_KEY)) + cors = CorsSupportMp.builder().config(config.get(CORS_CONFIG_KEY)) .secondaryLookupSupplier(this::crossOriginFromAnnotationSupplier) .build(); } @Override public void filter(ContainerRequestContext requestContext) { - Optional response = cors.processRequest(new RequestAdapterMP(requestContext), new ResponseAdapterMP()); + Optional response = cors.processRequest(new RequestAdapterMp(requestContext), new ResponseAdapterMp()); response.ifPresent(requestContext::abortWith); } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { - cors.prepareResponse(new RequestAdapterMP(requestContext), new ResponseAdapterMP(responseContext)); + cors.prepareResponse(new RequestAdapterMp(requestContext), new ResponseAdapterMp(responseContext)); } Optional crossOriginFromAnnotationSupplier() { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java index ef668ca1b20..24c8e347b28 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java @@ -70,8 +70,8 @@ public void accept(ServerRequest request, ServerResponse response) { request.next(); return; } - RequestAdapter requestAdapter = new SERequestAdapter(request); - ResponseAdapter responseAdapter = new SEResponseAdapter(response); + RequestAdapter requestAdapter = new RequestAdapterSe(request); + ResponseAdapter responseAdapter = new ResponseAdapterSe(response); Optional responseOpt = helper.processRequest(requestAdapter, responseAdapter); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java similarity index 76% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java index 0edbbb8c57c..14a1d0fd5ac 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSE.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java @@ -24,15 +24,15 @@ /** * SE implementation of {@link CorsSupport}. */ -public class CorsSupportSE extends CorsSupport { +public class CorsSupportSe extends CorsSupport { - private CorsSupportSE(Builder builder) { + private CorsSupportSe(Builder builder) { super(builder); } /** * - * @return new builder for CorsSupportSE + * @return new builder for CorsSupportSe */ public static Builder builder() { return new Builder(); @@ -40,20 +40,20 @@ public static Builder builder() { /** * - * @return new CorsSupportSE with default settings + * @return new CorsSupportSe with default settings */ - public static CorsSupportSE create() { + public static CorsSupportSe create() { return builder().build(); } /** - * Creates a new {@code CorsSupportSE} instance based on the provided configuration expected to match the basic + * Creates a new {@code CorsSupportSe} instance based on the provided configuration expected to match the basic * {@code CrossOriginConfig} format. * * @param config node containing the cross-origin information - * @return initialized {@code CorsSupportSE} instance + * @return initialized {@code CorsSupportSe} instance */ - public static CorsSupportSE from(Config config) { + public static CorsSupportSe from(Config config) { if (!config.exists()) { throw MissingValueException.create(config.key()); } @@ -61,11 +61,11 @@ public static CorsSupportSE from(Config config) { return builder.build(); } - public static class Builder extends CorsSupport.Builder { + public static class Builder extends CorsSupport.Builder { @Override - public CorsSupportSE build() { - return new CorsSupportSE(this); + public CorsSupportSe build() { + return new CorsSupportSe(this); } @Override diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java similarity index 93% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java index bd969a8b52e..a895212395a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SERequestAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java @@ -24,11 +24,11 @@ /** * Helidon SE implementation of {@link CorsSupport.RequestAdapter}. */ -class SERequestAdapter implements CorsSupport.RequestAdapter { +class RequestAdapterSe implements CorsSupport.RequestAdapter { private final ServerRequest request; - SERequestAdapter(ServerRequest request) { + RequestAdapterSe(ServerRequest request) { this.request = request; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java similarity index 93% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java index 1fbe6d26953..5acbaf0a5ba 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/SEResponseAdapter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java @@ -22,11 +22,11 @@ /** * SE implementation of {@link CorsSupport.ResponseAdapter}. */ -class SEResponseAdapter implements CorsSupport.ResponseAdapter { +class ResponseAdapterSe implements CorsSupport.ResponseAdapter { private final ServerResponse serverResponse; - SEResponseAdapter(ServerResponse serverResponse) { + ResponseAdapterSe(ServerResponse serverResponse) { this.serverResponse = serverResponse; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 0fa76fdb66a..388c625f037 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -18,7 +18,7 @@ /** *

      Helidon SE CORS Support

      *

      - * Use {@link io.helidon.webserver.cors.CorsSupportSE} and its {@link io.helidon.webserver.cors.CorsSupportSE.Builder} to add CORS + * Use {@link io.helidon.webserver.cors.CorsSupportSe} and its {@link io.helidon.webserver.cors.CorsSupportSe.Builder} to add CORS * handling to resources in your application. *

      * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for @@ -63,10 +63,10 @@ * Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build(); * Routing.Builder builder = Routing.builder() * .any("/greet", - * CorsSupportSE.from(myAppConfig.get("narrow")), + * CorsSupportSe.from(myAppConfig.get("narrow")), * (req, resp) -> resp.status(Http.Status.OK_200).send()) * .get("/greet", - * CorsSupportSE.from(myAppConfig.get("wide")), + * CorsSupportSe.from(myAppConfig.get("wide")), * (req, resp) -> resp.status(Http.Status.OK_200).send("Hello, World!")); * * } @@ -90,12 +90,12 @@ * * Routing.Builder builder = Routing.builder() * .register("/myapp", - * CorsSupportSE.builder() + * CorsSupportSe.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app * .build(), * new MyApp()); * - * Notice that you pass two services to the {@code register} method: the {@code CorsSupportSE} instance and your app + * Notice that you pass two services to the {@code register} method: the {@code CorsSupportSe} instance and your app * instance. Helidon will process requests to the path you specify with those services in that order. Also, note that you have * to make sure you use the same path in this API call and in your {@code MyApp} service if you adjust the routing there. *

      @@ -108,11 +108,11 @@ * {@code OPTIONS} requests. *

      *

      - * Each {@code CorsSupportSE} instance can be enabled or disabled, either through configuration or using the API. - * By default, when an application creates a new {@code CorsSupportSE.Builder} instance that builder's {@code build()} method will - * create an enabled {@code CorsSupportSE} object. Any subsequent explicit setting on the builder, either expressly set by an - * {@code enabled} entry in configuration passed to {@code CorsSupportSE.Builder.config} or set by invoking - * {@code CorsSupportSE.Builder.enabled} follows the familiar "latest-wins" approach. + * Each {@code CorsSupportSe} instance can be enabled or disabled, either through configuration or using the API. + * By default, when an application creates a new {@code CorsSupportSe.Builder} instance that builder's {@code build()} method will + * create an enabled {@code CorsSupportSe} object. Any subsequent explicit setting on the builder, either expressly set by an + * {@code enabled} entry in configuration passed to {@code CorsSupportSe.Builder.config} or set by invoking + * {@code CorsSupportSe.Builder.enabled} follows the familiar "latest-wins" approach. *

      */ package io.helidon.webserver.cors; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java similarity index 98% rename from webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java index c411d22951c..da94429449b 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTest.java @@ -27,8 +27,8 @@ import java.util.concurrent.ExecutionException; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.webserver.cors.CORSTestServices.SERVICE_1; -import static io.helidon.webserver.cors.CORSTestServices.SERVICE_2; +import static io.helidon.webserver.cors.CorsTestServices.SERVICE_1; +import static io.helidon.webserver.cors.CorsTestServices.SERVICE_2; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; @@ -43,7 +43,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public abstract class AbstractCORSTest { +public abstract class AbstractCorsTest { abstract String contextRoot(); diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTestService.java similarity index 95% rename from webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTestService.java index ba8e480e80a..3f9381dd347 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCORSTestService.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/AbstractCorsTestService.java @@ -22,7 +22,7 @@ import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -abstract class AbstractCORSTestService implements Service { +abstract class AbstractCorsTestService implements Service { void ok(ServerRequest request, ServerResponse response) { response.status(Http.Status.OK_200.code()); diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTest.java similarity index 98% rename from webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTest.java index ffe71f2d58a..d18ddb83895 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTest.java @@ -39,7 +39,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class CORSTest extends AbstractCORSTest { +public class CorsTest extends AbstractCorsTest { private static final String CONTEXT_ROOT = "/greet"; private static WebServer server; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTestServices.java similarity index 98% rename from webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTestServices.java index e24bba1d522..40f935bc8e5 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/CORSTestServices.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CorsTestServices.java @@ -25,7 +25,7 @@ import io.helidon.webserver.Service; -class CORSTestServices { +class CorsTestServices { static final CORSTestService SERVICE_1 = new CORSTestService("/cors1"); static final CORSTestService SERVICE_2 = new CORSTestService("/cors2"); diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java index 01354222912..336384ecf84 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestHandlerRegistration.java @@ -31,11 +31,9 @@ import java.util.concurrent.TimeoutException; import static io.helidon.common.http.Http.Header.ORIGIN; -import static io.helidon.webserver.cors.CORSTestServices.SERVICE_1; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_MAX_AGE; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_HEADERS; import static io.helidon.webserver.cors.CrossOriginConfig.ACCESS_CONTROL_REQUEST_METHOD; import static io.helidon.webserver.cors.CustomMatchers.present; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCorsConfigs.java similarity index 97% rename from webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java rename to webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCorsConfigs.java index c675d10cd04..9d29b4a37fc 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCORSConfigs.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestTwoCorsConfigs.java @@ -32,7 +32,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public class TestTwoCORSConfigs extends AbstractCORSTest { +public class TestTwoCorsConfigs extends AbstractCorsTest { private static WebServer server; private WebClient client; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index 8c07c509cce..7911c5f0462 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -25,13 +25,13 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; import io.helidon.config.spi.ConfigSource; -import io.helidon.webserver.cors.CORSTestServices.CORSTestService; +import io.helidon.webserver.cors.CorsTestServices.CORSTestService; import io.helidon.webclient.WebClient; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.WebServer; -import static io.helidon.webserver.cors.CORSTestServices.SERVICE_3; +import static io.helidon.webserver.cors.CorsTestServices.SERVICE_3; public class TestUtil { @@ -61,7 +61,7 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CorsSupportSE.Builder corsSupportBuilder = CorsSupportSE.builder(); + CorsSupportSe.Builder corsSupportBuilder = CorsSupportSe.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -74,16 +74,16 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CorsSupportSE.builder().config(Config.create().get("cors-setup")).build(), + CorsSupportSe.builder().config(Config.create().get("cors-setup")).build(), new GreetService()) .register(OTHER_GREETING_PATH, - CorsSupportSE.builder().config(twoCORSConfig.get("cors-2-setup")).build(), + CorsSupportSe.builder().config(twoCORSConfig.get("cors-2-setup")).build(), new GreetService("Other Hello")) .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CorsSupportSE.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode + CorsSupportSe.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode (req, resp) -> resp.status(Http.Status.OK_200).send()) .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, // handler settings in-line - CorsSupportSE.builder() + CorsSupportSe.builder() .allowOrigins("*") .allowMethods("GET") .build(), From 1d717cb99dd40ac63010329e28239194ff8803f3 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 06:55:17 -0500 Subject: [PATCH 090/100] Review: add ContrainedTo(Runtime.SERVER) to CrossOriginAutoDiscoverable --- .../helidon/microprofile/cors/CrossOriginAutoDiscoverable.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java index 3e058320f9f..1dd90516e34 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java @@ -16,6 +16,8 @@ package io.helidon.microprofile.cors; +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; import javax.ws.rs.core.FeatureContext; import org.glassfish.jersey.internal.spi.AutoDiscoverable; @@ -23,6 +25,7 @@ /** * Class CrossOriginAutoDiscoverable. */ +@ConstrainedTo(RuntimeType.SERVER) public class CrossOriginAutoDiscoverable implements AutoDiscoverable { @Override From 30a9260efd4874277d6b18ea98813710d01d3837 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 06:58:07 -0500 Subject: [PATCH 091/100] Review: emphasize CrossOriginAutoDiscoverable is not for general use --- .../helidon/microprofile/cors/CrossOriginAutoDiscoverable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java index 1dd90516e34..564587905b1 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginAutoDiscoverable.java @@ -23,7 +23,7 @@ import org.glassfish.jersey.internal.spi.AutoDiscoverable; /** - * Class CrossOriginAutoDiscoverable. + * Not for use by developers. For Jersey auto-discovery support. */ @ConstrainedTo(RuntimeType.SERVER) public class CrossOriginAutoDiscoverable implements AutoDiscoverable { From ebfdf3286605a5f01f46bd9b8c65674fb9e8f5a5 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 07:05:23 -0500 Subject: [PATCH 092/100] Review: no public methods on package-private classes; those which implement interface methods remain public --- .../java/io/helidon/webserver/cors/Aggregator.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index f638e93958a..814d0bacea6 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -92,7 +92,7 @@ public boolean isEnabled() { * @param config {@code Config} node containing * @return updated builder */ - public Aggregator mappedConfig(Config config) { + Aggregator mappedConfig(Config config) { if (config.exists()) { ConfigValue mappedConfigValue = config.as(MappedCrossOriginConfig.Builder::from); @@ -117,7 +117,7 @@ public Aggregator mappedConfig(Config config) { * @param crossOrigin the cross origin information * @return updated builder */ - public Aggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) { + Aggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) { crossOriginConfigMatchables.put(normalize(pathExpr), new FixedCrossOriginConfigMatchable(pathExpr, crossOrigin)); return this; } @@ -128,17 +128,12 @@ public Aggregator addCrossOrigin(String pathExpr, CrossOriginConfig crossOrigin) * @param crossOrigin the cross origin information * @return updated builder */ - public Aggregator addPathlessCrossOrigin(CrossOriginConfig crossOrigin) { + Aggregator addPathlessCrossOrigin(CrossOriginConfig crossOrigin) { crossOriginConfigMatchables.put(PATHLESS_KEY, new FixedCrossOriginConfigMatchable(PATHLESS_KEY, crossOrigin)); return this; } - /** - * Sets whether the app wants to enable CORS. - * - * @param value whether CORS should be enabled - * @return updated builder - */ + @Override public Aggregator enabled(boolean value) { isEnabled = value; return this; From d309e99eb509fbea0201eb3aabd1b5702f2a5c96 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 07:08:08 -0500 Subject: [PATCH 093/100] Review: change Setter to CorsSetter --- .../java/io/helidon/webserver/cors/Aggregator.java | 14 +++++++------- .../cors/{Setter.java => CorsSetter.java} | 2 +- .../io/helidon/webserver/cors/CorsSupport.java | 2 +- .../helidon/webserver/cors/CrossOriginConfig.java | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename webserver/cors/src/main/java/io/helidon/webserver/cors/{Setter.java => CorsSetter.java} (98%) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 814d0bacea6..371d3b77831 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -36,27 +36,27 @@ *
    • when storing cross-config information, the latest invocation that specifies the same path * expression overwrites any preceding settings for the same path expression, and
    • *
    • when matching against a request's path, the code checks the path matchers in the order - * they were added to the aggregator, whether by {@link #mappedConfig} or {@link #addCrossOrigin} or the {@link Setter} + * they were added to the aggregator, whether by {@link #mappedConfig} or {@link #addCrossOrigin} or the {@link CorsSetter} * methods. *
    *

    *

    - * The {@code Setter} methods affect the so-called "pathless" entry. Those methods have no explicit path, so we record + * The {@code CorsSetter} methods affect the so-called "pathless" entry. Those methods have no explicit path, so we record * their settings in an entry with path expression {@value #PATHLESS_KEY} which matches everything. *

    *

    - * If the developer uses the {@link #mappedConfig} or {@link #addCrossOrigin} methods along with the {@code Setter} + * If the developer uses the {@link #mappedConfig} or {@link #addCrossOrigin} methods along with the {@code CorsSetter} * methods, the results are predictable but might be confusing. The {@code config} and {@code addCrossOrigin} methods - * overwrite any entry with the same path expression, whereas the {@code Setter} methods update an existing + * overwrite any entry with the same path expression, whereas the {@code CorsSetter} methods update an existing * entry with path {@value #PATHLESS_KEY}, creating one if needed. So, if the config or an {@code addCrossOrigin} * invocation sets values for that same path expression then results can be surprising. * path *

    * */ -class Aggregator implements Setter { +class Aggregator implements CorsSetter { - // Key value for the map corresponding to the cross-origin config managed by the {@link Setter} methods + // Key value for the map corresponding to the cross-origin config managed by the {@link CorsSetter} methods static final String PATHLESS_KEY = "{+}"; // Records paths and configs added via addCrossOriginConfig @@ -280,7 +280,7 @@ CrossOriginConfig get() { /** * Based on a {@code CrossOriginConfig.Builder}, primarily for supporting the "pathless" entry that can be updated by - * separate invocations of the {@link Setter} methods. + * separate invocations of the {@link CorsSetter} methods. */ private static class BuildableCrossOriginConfigMatchable extends CrossOriginConfigMatchable { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java similarity index 98% rename from webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java rename to webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java index fe0ce3ede6f..679f2d6de72 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Setter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java @@ -22,7 +22,7 @@ * * @param the type of the implementing class so the fluid methods can return the correct type */ -interface Setter { +interface CorsSetter { /** * Sets whether this config should be enabled or not. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java index 24c8e347b28..f940672ecc0 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java @@ -118,7 +118,7 @@ private void prepareCORSResponseAndContinue(RequestAdapter reques * @param type of the builder */ public abstract static class Builder> implements io.helidon.common.Builder, - Setter> { + CorsSetter> { private final CorsSupportHelper.Builder helperBuilder = CorsSupportHelper.builder(); private final Aggregator aggregator = helperBuilder.aggregator(); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 3305bae23bb..18f13336a01 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -193,7 +193,7 @@ private static String[] copyOf(String[] strings) { /** * Builder for {@link CrossOriginConfig}. */ - public static class Builder implements Setter, io.helidon.common.Builder, + public static class Builder implements CorsSetter, io.helidon.common.Builder, Function { static final String[] ALLOW_ALL = {"*"}; From 4bfc33ec3ef8dbe677771cf33caa734beca74dae Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 07:09:33 -0500 Subject: [PATCH 094/100] Fix typo --- .../src/main/java/io/helidon/webserver/cors/CorsSetter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java index 679f2d6de72..cc2e94fa3db 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java @@ -17,7 +17,7 @@ package io.helidon.webserver.cors; /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CorsSupport.Builder} for assiging CORS-related + * Defines common behavior between {@code CrossOriginConfig} and {@link CorsSupport.Builder} for assigning CORS-related * attributes. * * @param the type of the implementing class so the fluid methods can return the correct type From e49a2d4a1a062a566a47a2e0a1213c21fc3be56a Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 08:19:40 -0500 Subject: [PATCH 095/100] Review: Clean up the build and builder methods on CrossOriginConfig --- .../io/helidon/webserver/cors/Aggregator.java | 2 +- .../helidon/webserver/cors/CorsSupportSe.java | 2 +- .../webserver/cors/CrossOriginConfig.java | 97 +++++++++---------- .../io/helidon/webserver/cors/Loader.java | 2 +- .../webserver/cors/CrossOriginConfigTest.java | 8 +- 5 files changed, 55 insertions(+), 56 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 371d3b77831..77cd21606b8 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -233,7 +233,7 @@ private CrossOriginConfig.Builder pathlessCrossOriginConfigBuilder() { return ((BuildableCrossOriginConfigMatchable) matchable).builder; } else { // Convert the existing entry that has a fixed cross-origin config to a pre-initialized builder. - newBuilder = CrossOriginConfig.Builder.from(matchable.get()); + newBuilder = CrossOriginConfig.builder(matchable.get()); } } else { // No existing entry. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java index 14a1d0fd5ac..54fc0a2573a 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java @@ -57,7 +57,7 @@ public static CorsSupportSe from(Config config) { if (!config.exists()) { throw MissingValueException.create(config.key()); } - Builder builder = builder().addCrossOrigin(PATHLESS_KEY, CrossOriginConfig.from(config)); + Builder builder = builder().addCrossOrigin(PATHLESS_KEY, CrossOriginConfig.builder(config).build()); return builder.build(); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 18f13336a01..0362f23971d 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -17,7 +17,6 @@ package io.helidon.webserver.cors; import java.util.Arrays; -import java.util.function.Function; import io.helidon.config.Config; @@ -26,22 +25,24 @@ /** * Represents information about cross origin request sharing. * - * Applications can create instance in two ways: + * Applications can create a new instance in two ways: *
      - *
    • using a {@code Builder} explicitly + *
    • Use a {@code Builder} explicitly. *

      * Obtain a suitable builder by: *

      *
        - *
      • explicitly getting a builder using {@link #builder()},
      • - *
      • invoking the static {@link Builder#from} method and - * passing an existing instance of {@code CrossOriginConfig}; the resulting {@code Builder} is - * intialized using the configuration node provided, or
      • - *
      • obtaining a {@link Config} instance and invoking {@code Config.as}, passing {@code Builder#from}
      • + *
      • getting a new builder using the static {@link #builder()} method,
      • + *
      • initializing a builder from an existing {@code CrossOriginConfig} instance using the static + * {@link #builder(CrossOriginConfig)} method, or
      • + *
      • initializing a builder from a {@code Config} node, invoking {@link Config#as} using + * {@code corsConfig.as(CrossOriginConfig::builder).get()}
      • *
      - * and then invoke methods on the builder, finally invoking the builder's {@code build} method to create the instance. - *
    • invoking the static {@link #from} method, passing a config node containing the cross-origin information to be - * converted. + * and then invoke methods on the builder as needed. Finally invoke the builder's {@code build} method to create the + * instance. + *
    • Invoke the static {@link #build(Config)} method, passing a config node containing the cross-origin information to be + * converted. This is a convenience method equivalent to creating a builder using the config node and then invoking {@code + * build()}. *
    • *
    * @@ -121,13 +122,40 @@ public static Builder builder() { } /** - * Creates a new {@code CrossOriginConfig} instance using the provided config node. + * Creates a new {@code CrossOriginConfig.Builder} using the provided config node. * * @param config node containing cross-origin information - * @return new {@code Basic} instance based on the configuration + * @return new {@code CrossOriginConfig.Builder} instance based on the configuration */ - public static CrossOriginConfig from(Config config) { - return Builder.from(config).build(); + public static Builder builder(Config config) { + return Loader.Basic.builder(config); + } + + /** + * Initializes a new {@code CrossOriginConfig.Builder} from the values in an existing {@code CrossOriginConfig} object. + * + * @param original the existing cross-origin config object + * @return new Builder initialized from the existing object's settings + */ + public static Builder builder(CrossOriginConfig original) { + return new Builder() + .pathPrefix(original.pathPrefix) + .enabled(original.enabled) + .allowCredentials(original.allowCredentials) + .allowHeaders(original.allowHeaders) + .allowMethods(original.allowMethods) + .allowOrigins(original.allowOrigins) + .exposeHeaders(original.exposeHeaders) + .maxAge(original.maxAge); + } + + /** + * Creates a new {@code CrossOriginConfig} instance based on the provided configuration node. + * @param corsConfig node containing CORS information + * @return new {@code CrossOriginConfig} based on the configuration + */ + public static CrossOriginConfig build(Config corsConfig) { + return builder(corsConfig).build(); } /** @@ -193,8 +221,7 @@ private static String[] copyOf(String[] strings) { /** * Builder for {@link CrossOriginConfig}. */ - public static class Builder implements CorsSetter, io.helidon.common.Builder, - Function { + public static class Builder implements CorsSetter, io.helidon.common.Builder { static final String[] ALLOW_ALL = {"*"}; @@ -210,38 +237,10 @@ public static class Builder implements CorsSetter, io.helidon.common.Bu private Builder() { } - /** - * Creates a new builder based on the values in an existing {@code CrossOriginConfig} object. - * - * @param original the existing cross-origin config object - * @return new Builder initialized from the existing object's settings - */ - public static Builder from(CrossOriginConfig original) { - return new Builder() - .pathPrefix(original.pathPrefix) - .enabled(original.enabled) - .allowCredentials(original.allowCredentials) - .allowHeaders(original.allowHeaders) - .allowMethods(original.allowMethods) - .allowOrigins(original.allowOrigins) - .exposeHeaders(original.exposeHeaders) - .maxAge(original.maxAge); - } - - /** - * Creates a new {@code Builder}instance from the specified configuration. - * - * @param config node containing cross-origin information - * @return new {@code Builder} initialized from the config - */ - public static Builder from(Config config) { - return Loader.Basic.builder(config); - } - - @Override - public Builder apply(Config config) { - return from(config); - } +// @Override +// public Builder apply(Config config) { +// return builder(config); +// } /** * Updates the path prefix for this cross-origin config. diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java index e6683ea9cdf..67f79497a54 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java @@ -84,7 +84,7 @@ static MappedCrossOriginConfig.Builder builder(MappedCrossOriginConfig.Builder b if (!item.exists()) { break; } - ConfigValue basicConfigValue = item.as(CrossOriginConfig.Builder::from); + ConfigValue basicConfigValue = item.as(CrossOriginConfig::builder); if (!basicConfigValue.isPresent()) { continue; } diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java index 6cfc2ac5500..ba76557e804 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java @@ -49,7 +49,7 @@ public void testNarrow() { Config node = testConfig.get("narrow"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig c = node.as(CrossOriginConfig::from).get(); + CrossOriginConfig c = node.as(CrossOriginConfig::build).get(); assertThat(c.isEnabled(), is(true)); assertThat(c.allowOrigins(), arrayContaining("http://foo.bar", "http://bar.foo")); @@ -63,7 +63,7 @@ public void testNarrow() { @Test public void testMissing() { Assertions.assertThrows(MissingValueException.class, () -> { - CrossOriginConfig basic = testConfig.get("notThere").as(CrossOriginConfig::from).get(); + CrossOriginConfig basic = testConfig.get("notThere").as(CrossOriginConfig::build).get(); }); } @@ -72,7 +72,7 @@ public void testWide() { Config node = testConfig.get("wide"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig b = node.as(CrossOriginConfig::from).get(); + CrossOriginConfig b = node.as(CrossOriginConfig::build).get(); assertThat(b.isEnabled(), is(false)); assertThat(b.allowOrigins(), arrayContaining(ALLOW_ALL)); @@ -88,7 +88,7 @@ public void testJustDisabled() { Config node = testConfig.get("just-disabled"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig b = node.as(CrossOriginConfig::from).get(); + CrossOriginConfig b = node.as(CrossOriginConfig::build).get(); assertThat(b.isEnabled(), is(false)); } From 5ef7dc86e0004a5e58df898f0ebc6acec13d2346 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 08:30:33 -0500 Subject: [PATCH 096/100] Change maxAge to maxAgeSeconds --- .../microprofile/cors/CrossOriginFilter.java | 2 +- .../io/helidon/webserver/cors/Aggregator.java | 4 ++-- .../io/helidon/webserver/cors/CorsSetter.java | 4 ++-- .../io/helidon/webserver/cors/CorsSupport.java | 4 ++-- .../webserver/cors/CorsSupportHelper.java | 6 +++--- .../webserver/cors/CrossOriginConfig.java | 16 ++++++++-------- .../java/io/helidon/webserver/cors/Loader.java | 2 +- .../webserver/cors/CrossOriginConfigTest.java | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java index 44e19243c89..107d7c4b16c 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CrossOriginFilter.java @@ -119,7 +119,7 @@ private static CrossOriginConfig annotationToConfig(CrossOrigin crossOrigin) { .exposeHeaders(crossOrigin.exposeHeaders()) .allowMethods(crossOrigin.allowMethods()) .allowCredentials(crossOrigin.allowCredentials()) - .maxAge(crossOrigin.maxAge()) + .maxAgeSeconds(crossOrigin.maxAge()) .build(); } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 77cd21606b8..3702ca8c3db 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -170,8 +170,8 @@ public Aggregator allowCredentials(boolean allowCredentials) { } @Override - public Aggregator maxAge(long maxAge) { - pathlessCrossOriginConfigBuilder().maxAge(maxAge); + public Aggregator maxAgeSeconds(long maxAgeSeconds) { + pathlessCrossOriginConfigBuilder().maxAgeSeconds(maxAgeSeconds); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java index cc2e94fa3db..45eae538e6e 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java @@ -75,8 +75,8 @@ interface CorsSetter { /** * Sets the maximum age. * - * @param maxAge the maximum age + * @param maxAgeSeconds the maximum age * @return updated setter */ - T maxAge(long maxAge); + T maxAgeSeconds(long maxAgeSeconds); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java index f940672ecc0..99d4d28d724 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java @@ -208,8 +208,8 @@ public B allowCredentials(boolean allowCredentials) { } @Override - public B maxAge(long maxAge) { - aggregator.maxAge(maxAge); + public B maxAgeSeconds(long maxAgeSeconds) { + aggregator.maxAgeSeconds(maxAgeSeconds); return me(); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java index 6f62a986762..50d15e2f96c 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java @@ -539,9 +539,9 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, headers.add(ACCESS_CONTROL_ALLOW_METHODS, method); formatHeader(requestHeaders.toArray()).ifPresent( h -> headers.add(ACCESS_CONTROL_ALLOW_HEADERS, h)); - long maxAge = crossOrigin.maxAge(); - if (maxAge > 0) { - headers.add(ACCESS_CONTROL_MAX_AGE, maxAge, "maxAge > 0"); + long maxAgeSeconds = crossOrigin.maxAgeSeconds(); + if (maxAgeSeconds > 0) { + headers.add(ACCESS_CONTROL_MAX_AGE, maxAgeSeconds, "maxAgeSeconds > 0"); } headers.set(responseAdapter::header, "headers set on preflight request"); return responseAdapter.ok(); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 0362f23971d..4dedbd44ed9 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -101,7 +101,7 @@ public class CrossOriginConfig { private final String[] exposeHeaders; private final String[] allowMethods; private final boolean allowCredentials; - private final long maxAge; + private final long maxAgeSeconds; private CrossOriginConfig(Builder builder) { this.pathPrefix = builder.pathPrefix; @@ -111,7 +111,7 @@ private CrossOriginConfig(Builder builder) { this.exposeHeaders = builder.exposeHeaders; this.allowMethods = builder.allowMethods; this.allowCredentials = builder.allowCredentials; - this.maxAge = builder.maxAge; + this.maxAgeSeconds = builder.maxAgeSeconds; } /** @@ -146,7 +146,7 @@ public static Builder builder(CrossOriginConfig original) { .allowMethods(original.allowMethods) .allowOrigins(original.allowOrigins) .exposeHeaders(original.exposeHeaders) - .maxAge(original.maxAge); + .maxAgeSeconds(original.maxAgeSeconds); } /** @@ -210,8 +210,8 @@ public boolean allowCredentials() { /** * @return maximum age */ - public long maxAge() { - return maxAge; + public long maxAgeSeconds() { + return maxAgeSeconds; } private static String[] copyOf(String[] strings) { @@ -232,7 +232,7 @@ public static class Builder implements CorsSetter, io.helidon.common.Bu private String[] exposeHeaders; private String[] allowMethods = ALLOW_ALL; private boolean allowCredentials; - private long maxAge = DEFAULT_AGE; + private long maxAgeSeconds = DEFAULT_AGE; private Builder() { } @@ -294,8 +294,8 @@ public Builder allowCredentials(boolean allowCredentials) { } @Override - public Builder maxAge(long maxAge) { - this.maxAge = maxAge; + public Builder maxAgeSeconds(long maxAgeSeconds) { + this.maxAgeSeconds = maxAgeSeconds; return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java index 67f79497a54..2e66da81be3 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java @@ -62,7 +62,7 @@ static CrossOriginConfig.Builder builder(CrossOriginConfig.Builder builder, Conf .ifPresent(builder::allowCredentials); config.get("max-age") .as(Long.class) - .ifPresent(builder::maxAge); + .ifPresent(builder::maxAgeSeconds); return builder; } } diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java index ba76557e804..9160369d75d 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java @@ -57,7 +57,7 @@ public void testNarrow() { assertThat(c.allowHeaders(), arrayContaining("X-bar", "X-foo")); assertThat(c.exposeHeaders(), is(emptyArray())); assertThat(c.allowCredentials(), is(true)); - assertThat(c.maxAge(), is(-1L)); + assertThat(c.maxAgeSeconds(), is(-1L)); } @Test @@ -80,7 +80,7 @@ public void testWide() { assertThat(b.allowHeaders(), arrayContaining(ALLOW_ALL)); assertThat(b.exposeHeaders(), is(emptyArray())); assertThat(b.allowCredentials(), is(false)); - assertThat(b.maxAge(), is(DEFAULT_AGE)); + assertThat(b.maxAgeSeconds(), is(DEFAULT_AGE)); } @Test @@ -109,7 +109,7 @@ public void testPaths() { assertThat(b.allowMethods(), arrayContaining("*")); assertThat(b.allowHeaders(), arrayContaining("*")); assertThat(b.allowCredentials(), is(false)); - assertThat(b.maxAge(), is(DEFAULT_AGE)); + assertThat(b.maxAgeSeconds(), is(DEFAULT_AGE)); b = m.get("/cors2"); assertThat(b, notNullValue()); @@ -118,7 +118,7 @@ public void testPaths() { assertThat(b.allowMethods(), arrayContaining("DELETE", "PUT")); assertThat(b.allowHeaders(), arrayContaining("X-bar", "X-foo")); assertThat(b.allowCredentials(), is(true)); - assertThat(b.maxAge(), is(-1L)); + assertThat(b.maxAgeSeconds(), is(-1L)); assertThat(m.get("/cors3"), nullValue()); } From ca3f3bc1ac0af945158374c5971cd798af14c394 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 09:24:59 -0500 Subject: [PATCH 097/100] More clean-up of builders; MappedCrossOriginConfig this time --- .../io/helidon/webserver/cors/Aggregator.java | 2 +- .../cors/MappedCrossOriginConfig.java | 31 +++++++------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 3702ca8c3db..42d669a9fb3 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -95,7 +95,7 @@ public boolean isEnabled() { Aggregator mappedConfig(Config config) { if (config.exists()) { - ConfigValue mappedConfigValue = config.as(MappedCrossOriginConfig.Builder::from); + ConfigValue mappedConfigValue = config.as(MappedCrossOriginConfig::builder); if (mappedConfigValue.isPresent()) { MappedCrossOriginConfig mapped = mappedConfigValue.get().build(); /* diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java index ddf9390576c..5da9f70711d 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; -import java.util.function.Function; import io.helidon.config.Config; @@ -80,6 +79,16 @@ public static Builder builder() { return new Builder(); } + /** + * Creates a new {@code Mapped.Builder} instance using the provided configuration. + * + * @param config node containing {@code Mapped} cross-origin information + * @return new {@code Mapped.Builder} based on the config + */ + public static Builder builder(Config config) { + return Loader.Mapped.builder(config); + } + /** * Creates a new {@code Mapped} instance using the provided configuration. * @@ -87,7 +96,7 @@ public static Builder builder() { * @return new {@code Mapped} instance based on the config */ public static MappedCrossOriginConfig from(Config config) { - return Builder.from(config).build(); + return builder(config).build(); } @Override @@ -139,7 +148,7 @@ public boolean isEnabled() { /** * Fluent builder for {@code Mapped} cross-origin config. */ - public static class Builder implements io.helidon.common.Builder, Function { + public static class Builder implements io.helidon.common.Builder { private Optional enabledOpt = Optional.empty(); private final Map builders = new HashMap<>(); @@ -147,27 +156,11 @@ public static class Builder implements io.helidon.common.Builder Date: Fri, 17 Apr 2020 10:19:05 -0500 Subject: [PATCH 098/100] Improve toString in several places --- .../microprofile/cors/CorsSupportMp.java | 6 +++++ .../io/helidon/webserver/cors/Aggregator.java | 16 ++++++++++++++ .../webserver/cors/CrossOriginConfig.java | 22 ++++++++++++++----- .../webserver/cors/RequestAdapterSe.java | 5 +++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java index 707daa428f0..99c2f30afe1 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java @@ -132,6 +132,12 @@ public ContainerRequestContext request() { @Override public void next() { } + + @Override + public String toString() { + return String.format("RequestAdapterMp{path=%s, method=%s, headers=%s}", path(), method(), + requestContext.getHeaders()); + } } static class ResponseAdapterMp implements ResponseAdapter { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java index 42d669a9fb3..e5273fc4bc7 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Aggregator.java @@ -259,6 +259,10 @@ boolean matches(String unnormalizedPath) { return matcher.match(unnormalizedPath).matches(); } + PathMatcher matcher() { + return matcher; + } + abstract CrossOriginConfig get(); } @@ -276,6 +280,12 @@ private static class FixedCrossOriginConfigMatchable extends CrossOriginConfigMa CrossOriginConfig get() { return crossOriginConfig; } + + @Override + public String toString() { + return String.format("FixedCrossOriginConfigMatchable{matcher=%s, crossOriginConfig=%s}", + matcher(), crossOriginConfig); + } } /** @@ -298,5 +308,11 @@ CrossOriginConfig get() { } return config; } + + @Override + public String toString() { + return String.format("BuildableCrossOriginConfigMatchable{matcher=%s, builder=%s, config=%s}", + matcher(), builder, config); + } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index 4dedbd44ed9..a40acbe9e57 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -214,6 +214,15 @@ public long maxAgeSeconds() { return maxAgeSeconds; } + @Override + public String toString() { + return String.format("CrossOriginConfig{pathPrefix=%s, enabled=%b, origins=%s, allowHeaders=%s, exposeHeaders=%s, " + + "allowMethods=%s, allowCredentials=%b, maxAgeSeconds=%d", pathPrefix, enabled, + Arrays.toString(allowOrigins), + Arrays.toString(allowHeaders), Arrays.toString(exposeHeaders), Arrays.toString(allowMethods), + allowCredentials, maxAgeSeconds); + } + private static String[] copyOf(String[] strings) { return strings != null ? Arrays.copyOf(strings, strings.length) : new String[0]; } @@ -237,11 +246,6 @@ public static class Builder implements CorsSetter, io.helidon.common.Bu private Builder() { } -// @Override -// public Builder apply(Config config) { -// return builder(config); -// } - /** * Updates the path prefix for this cross-origin config. * @@ -303,5 +307,13 @@ public Builder maxAgeSeconds(long maxAgeSeconds) { public CrossOriginConfig build() { return new CrossOriginConfig(this); } + + @Override + public String toString() { + return String.format("Builder{pathPrefix=%s, enabled=%b, origins=%s, allowHeaders=%s, exposeHeaders=%s, " + + "allowMethods=%s, allowCredentials=%b, maxAgeSeconds=%d", pathPrefix, enabled, Arrays.toString(origins), + Arrays.toString(allowHeaders), Arrays.toString(exposeHeaders), Arrays.toString(allowMethods), + allowCredentials, maxAgeSeconds); + } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java index a895212395a..31fdce19f14 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java @@ -66,4 +66,9 @@ public void next() { public ServerRequest request() { return request; } + + @Override + public String toString() { + return String.format("RequestAdapterSe{path=%s, method=%s, headers=%s}", path(), method(), request.headers().toMap()); + } } From 9dbd735b66b0b6e0aca2ab8885a86e78fbe5628c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 17 Apr 2020 10:40:53 -0500 Subject: [PATCH 099/100] Slightly adjust how we do logging of CORS-related decision making --- .../webserver/cors/CorsSupportHelper.java | 17 +++++++---------- .../io/helidon/webserver/cors/LogHelper.java | 17 ++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java index 50d15e2f96c..3b45f806978 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java @@ -374,9 +374,7 @@ private static boolean isRequestTypeNormal(RequestAdapter requestAdapter, Optional hostOpt = requestAdapter.firstHeader(HOST); boolean result = originOpt.isEmpty() || (hostOpt.isPresent() && originOpt.get().contains("://" + hostOpt.get())); - if (!silent && LOGGER.isLoggable(DECISION_LEVEL)) { - LogHelper.isRequestTypeNormal(result, requestAdapter, originOpt, hostOpt); - } + LogHelper.logIsRequestTypeNormal(result, silent, requestAdapter, originOpt, hostOpt); return result; } @@ -390,9 +388,8 @@ private static RequestType inferCORSRequestType(RequestAdapter requestAda ? RequestType.PREFLIGHT : RequestType.CORS; - if (!silent && !LOGGER.isLoggable(DECISION_LEVEL)) { - LogHelper.inferCORSRequestType(result, requestAdapter, methodName, requestContainsAccessControlRequestMethodHeader); - } + LogHelper.logInferRequestType(result, silent, requestAdapter, methodName, + requestContainsAccessControlRequestMethodHeader); return result; } @@ -450,20 +447,20 @@ static void addCORSHeadersToResponse(CrossOriginConfig crossOrigin, .add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") .add(ACCESS_CONTROL_ALLOW_ORIGIN, origin) .add(Http.Header.VARY, ORIGIN) - .set(responseAdapter::header, "allow-credentials was set in CORS config"); + .setAndLog(responseAdapter::header, "allow-credentials was set in CORS config"); } else { List allowedOrigins = Arrays.asList(crossOrigin.allowOrigins()); new Headers() .add(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins.contains("*") ? "*" : origin) .add(Http.Header.VARY, ORIGIN) - .set(responseAdapter::header, "allow-credentials was not set in CORS config"); + .setAndLog(responseAdapter::header, "allow-credentials was not set in CORS config"); } // Add Access-Control-Expose-Headers if non-empty Headers headers = new Headers(); formatHeader(crossOrigin.exposeHeaders()).ifPresent( h -> headers.add(ACCESS_CONTROL_EXPOSE_HEADERS, h)); - headers.set(responseAdapter::header, "expose-headers was set in CORS config"); + headers.setAndLog(responseAdapter::header, "expose-headers was set in CORS config"); } /** @@ -543,7 +540,7 @@ static U processCORSPreFlightRequest(CrossOriginConfig crossOrigin, if (maxAgeSeconds > 0) { headers.add(ACCESS_CONTROL_MAX_AGE, maxAgeSeconds, "maxAgeSeconds > 0"); } - headers.set(responseAdapter::header, "headers set on preflight request"); + headers.setAndLog(responseAdapter::header, "headers set on preflight request"); return responseAdapter.ok(); } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index ef2c20f416f..0d05cecf3eb 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -60,14 +60,17 @@ Headers add(String key, Object value, String note) { return this; } - void set(BiConsumer consumer, String note) { + void setAndLog(BiConsumer consumer, String note) { headers.forEach(entry -> consumer.accept(entry.getKey(), entry.getValue())); LOGGER.log(DECISION_LEVEL, () -> note + ": " + headers + (notes == null ? "" : notes)); } } - static boolean isRequestTypeNormal(boolean result, RequestAdapter requestAdapter, Optional originOpt, - Optional hostOpt) { + static void logIsRequestTypeNormal(boolean result, boolean silent, RequestAdapter requestAdapter, + Optional originOpt, Optional hostOpt) { + if (silent || !LOGGER.isLoggable(DECISION_LEVEL)) { + return; + } // If no origin header or same as host, then just normal List reasonsWhyNormal = new ArrayList<>(); @@ -104,11 +107,13 @@ static boolean isRequestTypeNormal(boolean result, RequestAdapter request LOGGER.log(LogHelper.DECISION_LEVEL, () -> String.format("Request %s is cross-host: %s", requestAdapter, factorsWhyCrossHost)); } - return result; } - static RequestType inferCORSRequestType(RequestType result, RequestAdapter requestAdapter, String methodName, + static void logInferRequestType(RequestType result, boolean silent, RequestAdapter requestAdapter, String methodName, boolean requestContainsAccessControlRequestMethodHeader) { + if (silent || !LOGGER.isLoggable(DECISION_LEVEL)) { + return; + } List reasonsWhyCORS = new ArrayList<>(); // any reason is determinative List factorsWhyPreflight = new ArrayList<>(); // factors contribute but, individually, do not determine @@ -127,7 +132,5 @@ static RequestType inferCORSRequestType(RequestType result, RequestAdapter Date: Fri, 17 Apr 2020 11:32:09 -0500 Subject: [PATCH 100/100] Renaming to CorsSupport to ...Base and CorsSupportSe to CorsSupport; further clean-up of builder/config/'from' --- .../microprofile/cors/CorsSupportMp.java | 8 +- .../io/helidon/webserver/cors/CorsSetter.java | 2 +- .../helidon/webserver/cors/CorsSupport.java | 315 ++-------------- .../webserver/cors/CorsSupportBase.java | 337 ++++++++++++++++++ .../webserver/cors/CorsSupportHelper.java | 4 +- .../helidon/webserver/cors/CorsSupportSe.java | 76 ---- .../webserver/cors/CrossOriginConfig.java | 21 +- .../io/helidon/webserver/cors/Loader.java | 12 +- .../io/helidon/webserver/cors/LogHelper.java | 2 +- .../cors/MappedCrossOriginConfig.java | 18 +- .../webserver/cors/RequestAdapterSe.java | 4 +- .../webserver/cors/ResponseAdapterSe.java | 8 +- .../helidon/webserver/cors/package-info.java | 20 +- .../webserver/cors/CrossOriginConfigTest.java | 10 +- .../io/helidon/webserver/cors/TestUtil.java | 10 +- 15 files changed, 436 insertions(+), 411 deletions(-) create mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportBase.java delete mode 100644 webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java diff --git a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java index 99c2f30afe1..e7696712bb4 100644 --- a/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java +++ b/microprofile/cors/src/main/java/io/helidon/microprofile/cors/CorsSupportMp.java @@ -26,13 +26,13 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import io.helidon.webserver.cors.CorsSupport; +import io.helidon.webserver.cors.CorsSupportBase; import io.helidon.webserver.cors.CrossOriginConfig; /** - * MP implementation of {@link CorsSupport}. + * MP implementation of {@link CorsSupportBase}. */ -class CorsSupportMp extends CorsSupport { +class CorsSupportMp extends CorsSupportBase { /** * @@ -71,7 +71,7 @@ protected void prepareResponse(RequestAdapter requestAdapter, Response super.prepareResponse(requestAdapter, responseAdapter); } - static class Builder extends CorsSupport.Builder { + static class Builder extends CorsSupportBase.Builder { @Override public CorsSupportMp build() { diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java index 45eae538e6e..6c7dbc0d722 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSetter.java @@ -17,7 +17,7 @@ package io.helidon.webserver.cors; /** - * Defines common behavior between {@code CrossOriginConfig} and {@link CorsSupport.Builder} for assigning CORS-related + * Defines common behavior between {@code CrossOriginConfig} and {@link CorsSupportBase.Builder} for assigning CORS-related * attributes. * * @param the type of the implementing class so the fluid methods can return the correct type diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java index 99d4d28d724..0333dc2de11 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupport.java @@ -16,322 +16,61 @@ */ package io.helidon.webserver.cors; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; - import io.helidon.config.Config; -import io.helidon.webserver.Handler; -import io.helidon.webserver.Routing; -import io.helidon.webserver.ServerRequest; -import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.Service; +import io.helidon.config.MissingValueException; + +import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; /** - * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon - * services (such as OpenAPI and metrics). - *

    - * The caller can set up the {@code CorsSupport} in a combination of these ways: - *

    - *
      - *
    • from a {@link Config} node supplied programmatically,
    • - *
    • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which - * it applies, and
    • - *
    • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the - * {@value Aggregator#PATHLESS_KEY} path).
    • - *
    - *

    - * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. - *

    - *

    - * If none of these sources is used, the {@code CorsSupport} applies defaults as described for - * {@link CrossOriginConfig}. - *

    - * + * SE implementation of {@link CorsSupportBase}. */ -public abstract class CorsSupport implements Service, Handler { +public class CorsSupport extends CorsSupportBase { - private final CorsSupportHelper helper; - - protected > CorsSupport(Builder builder) { - helper = builder.helperBuilder.build(); - } - - @Override - public void update(Routing.Rules rules) { - if (helper.isActive()) { - rules.any(this); - } - } - - @Override - public void accept(ServerRequest request, ServerResponse response) { - if (!helper.isActive()) { - request.next(); - return; - } - RequestAdapter requestAdapter = new RequestAdapterSe(request); - ResponseAdapter responseAdapter = new ResponseAdapterSe(response); - - Optional responseOpt = helper.processRequest(requestAdapter, responseAdapter); - - responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, responseAdapter)); + private CorsSupport(Builder builder) { + super(builder); } /** - * Not for developer use. Submits a request adapter and response adapter for CORS processing. * - * @param requestAdapter wrapper around the request - * @param responseAdapter wrapper around the response - * @param type of the request wrapped by the adapter - * @param type of the response wrapped by the adapter - * @return Optional of the response type U; present if the response should be returned, empty if request processing should - * continue + * @return new builder for CorsSupport */ - protected Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - return helper.processRequest(requestAdapter, responseAdapter); + public static Builder builder() { + return new Builder(); } /** - * Not for developer user. Gets a response ready to participate in the CORS protocol. * - * @param requestAdapter wrapper around the request - * @param responseAdapter wrapper around the reseponse - * @param type of the request wrapped by the adapter - * @param type of the response wrapped by the adapter + * @return new CorsSupport with default settings */ - protected void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { - helper.prepareResponse(requestAdapter, responseAdapter); - } - - private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, - ResponseAdapter responseAdapter) { - helper.prepareResponse(requestAdapter, responseAdapter); - - requestAdapter.request().next(); + public static CorsSupport create() { + return builder().build(); } /** - * Builder for {@code CorsSupport} instances. + * Creates a new {@code CorsSupport} instance based on the provided configuration expected to match the basic + * {@code CrossOriginConfig} format. * - * @param specific subtype of {@code CorsSupport} the builder creates - * @param type of the builder + * @param config node containing the cross-origin information + * @return initialized {@code CorsSupport} instance */ - public abstract static class Builder> implements io.helidon.common.Builder, - CorsSetter> { - - private final CorsSupportHelper.Builder helperBuilder = CorsSupportHelper.builder(); - private final Aggregator aggregator = helperBuilder.aggregator(); - - protected Builder() { - } - - protected abstract B me(); - - @Override - public abstract T build(); - - /** - * Merges CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance - * from its own config. - * - * @param config the CORS config - * @return the updated builder - */ - public B config(Config config) { - aggregator.mappedConfig(config); - return me(); - } - - /** - * Sets whether CORS support should be enabled or not. - * - * @param value whether to use CORS support - * @return updated builder - */ - public B enabled(boolean value) { - aggregator.enabled(value); - return me(); - } - - /** - * Adds cross origin information associated with a given path. - * - * @param path the path to which the cross origin information applies - * @param crossOrigin the cross origin information - * @return updated builder - */ - public B addCrossOrigin(String path, CrossOriginConfig crossOrigin) { - aggregator.addCrossOrigin(path, crossOrigin); - return me(); - } - - /** - * Adds cross origin information associated with the default path. - * - * @param crossOrigin the cross origin information - * @return updated builder - */ - public B addCrossOrigin(CrossOriginConfig crossOrigin) { - aggregator.addPathlessCrossOrigin(crossOrigin); - return me(); - } - - @Override - public B allowOrigins(String... origins) { - aggregator.allowOrigins(origins); - return me(); - } - - @Override - public B allowHeaders(String... allowHeaders) { - aggregator.allowHeaders(allowHeaders); - return me(); + public static CorsSupport create(Config config) { + if (!config.exists()) { + throw MissingValueException.create(config.key()); } + Builder builder = builder().addCrossOrigin(PATHLESS_KEY, CrossOriginConfig.builder(config).build()); + return builder.build(); + } - @Override - public B exposeHeaders(String... exposeHeaders) { - aggregator.exposeHeaders(exposeHeaders); - return me(); - } + public static class Builder extends CorsSupportBase.Builder { @Override - public B allowMethods(String... allowMethods) { - aggregator.allowMethods(allowMethods); - return me(); + public CorsSupport build() { + return new CorsSupport(this); } @Override - public B allowCredentials(boolean allowCredentials) { - aggregator.allowCredentials(allowCredentials); - return me(); - } - - @Override - public B maxAgeSeconds(long maxAgeSeconds) { - aggregator.maxAgeSeconds(maxAgeSeconds); - return me(); - } - - /** - * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during - * look-up for a given request, none is found from the aggregator. - * - * @param secondaryLookupSupplier supplier of a CrossOriginConfig - * @return updated builder - */ - protected Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { - helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); + protected Builder me() { return this; } } - - /** - * Not for use by developers. - * - * Minimal abstraction of an HTTP request. - * - * @param type of the request wrapped by the adapter - */ - protected interface RequestAdapter { - - /** - * - * @return possibly unnormalized path from the request - */ - String path(); - - /** - * Retrieves the first value for the specified header as a String. - * - * @param key header name to retrieve - * @return the first header value for the key - */ - Optional firstHeader(String key); - - /** - * Reports whether the specified header exists. - * - * @param key header name to check for - * @return whether the header exists among the request's headers - */ - boolean headerContainsKey(String key); - - /** - * Retrieves all header values for a given key as Strings. - * - * @param key header name to retrieve - * @return header values for the header; empty list if none - */ - List allHeaders(String key); - - /** - * Reports the method name for the request. - * - * @return the method name - */ - String method(); - - /** - * Processes the next handler/filter/request processor in the chain. - */ - void next(); - - /** - * Returns the request this adapter wraps. - * - * @return the request - */ - T request(); - } - - /** - * Not for use by developers. - * - * Minimal abstraction of an HTTP response. - * - *

    - * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} - * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the - * actual response. - *

    - * - * @param the type of the response wrapped by the adapter - */ - protected interface ResponseAdapter { - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, String value); - - /** - * Arranges to add the specified header and value to the eventual response. - * - * @param key header name to add - * @param value header value to add - * @return the adapter - */ - ResponseAdapter header(String key, Object value); - - /** - * Returns a response with the forbidden status and the specified error message, without any headers assigned - * using the {@code header} methods. - * - * @param message error message to use in setting the response status - * @return the factory - */ - T forbidden(String message); - - /** - * Returns a response with only the headers that were set on this adapter and the status set to OK. - * - * @return response instance - */ - T ok(); - } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportBase.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportBase.java new file mode 100644 index 00000000000..0d7640ff929 --- /dev/null +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportBase.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.cors; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.webserver.Handler; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +/** + * A Helidon service and handler implementation that implements CORS, for both the application and for built-in Helidon + * services (such as OpenAPI and metrics). + *

    + * The caller can set up the {@code CorsSupportBase} in a combination of these ways: + *

    + *
      + *
    • from a {@link Config} node supplied programmatically,
    • + *
    • from one or more {@link CrossOriginConfig} objects supplied programmatically, each associated with a path to which + * it applies, and
    • + *
    • by setting individual CORS-related attributes on the {@link Builder} (which affects the CORS behavior for the + * {@value Aggregator#PATHLESS_KEY} path).
    • + *
    + *

    + * See the {@link Builder#build} method for how the builder resolves conflicts among these sources. + *

    + *

    + * If none of these sources is used, the {@code CorsSupportBase} applies defaults as described for + * {@link CrossOriginConfig}. + *

    + * + */ +public abstract class CorsSupportBase implements Service, Handler { + + private final CorsSupportHelper helper; + + protected > CorsSupportBase(Builder builder) { + helper = builder.helperBuilder.build(); + } + + @Override + public void update(Routing.Rules rules) { + if (helper.isActive()) { + rules.any(this); + } + } + + @Override + public void accept(ServerRequest request, ServerResponse response) { + if (!helper.isActive()) { + request.next(); + return; + } + RequestAdapter requestAdapter = new RequestAdapterSe(request); + ResponseAdapter responseAdapter = new ResponseAdapterSe(response); + + Optional responseOpt = helper.processRequest(requestAdapter, responseAdapter); + + responseOpt.ifPresentOrElse(ServerResponse::send, () -> prepareCORSResponseAndContinue(requestAdapter, responseAdapter)); + } + + /** + * Not for developer use. Submits a request adapter and response adapter for CORS processing. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the response + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter + * @return Optional of the response type U; present if the response should be returned, empty if request processing should + * continue + */ + protected Optional processRequest(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + return helper.processRequest(requestAdapter, responseAdapter); + } + + /** + * Not for developer user. Gets a response ready to participate in the CORS protocol. + * + * @param requestAdapter wrapper around the request + * @param responseAdapter wrapper around the reseponse + * @param type of the request wrapped by the adapter + * @param type of the response wrapped by the adapter + */ + protected void prepareResponse(RequestAdapter requestAdapter, ResponseAdapter responseAdapter) { + helper.prepareResponse(requestAdapter, responseAdapter); + } + + private void prepareCORSResponseAndContinue(RequestAdapter requestAdapter, + ResponseAdapter responseAdapter) { + helper.prepareResponse(requestAdapter, responseAdapter); + + requestAdapter.request().next(); + } + + /** + * Builder for {@code CorsSupportBase} instances. + * + * @param specific subtype of {@code CorsSupportBase} the builder creates + * @param type of the builder + */ + public abstract static class Builder> implements io.helidon.common.Builder, + CorsSetter> { + + private final CorsSupportHelper.Builder helperBuilder = CorsSupportHelper.builder(); + private final Aggregator aggregator = helperBuilder.aggregator(); + + protected Builder() { + } + + protected abstract B me(); + + @Override + public abstract T build(); + + /** + * Merges CORS config information. Typically, the app or component will retrieve the provided {@code Config} instance + * from its own config. + * + * @param config the CORS config + * @return the updated builder + */ + public B config(Config config) { + aggregator.mappedConfig(config); + return me(); + } + + /** + * Sets whether CORS support should be enabled or not. + * + * @param value whether to use CORS support + * @return updated builder + */ + public B enabled(boolean value) { + aggregator.enabled(value); + return me(); + } + + /** + * Adds cross origin information associated with a given path. + * + * @param path the path to which the cross origin information applies + * @param crossOrigin the cross origin information + * @return updated builder + */ + public B addCrossOrigin(String path, CrossOriginConfig crossOrigin) { + aggregator.addCrossOrigin(path, crossOrigin); + return me(); + } + + /** + * Adds cross origin information associated with the default path. + * + * @param crossOrigin the cross origin information + * @return updated builder + */ + public B addCrossOrigin(CrossOriginConfig crossOrigin) { + aggregator.addPathlessCrossOrigin(crossOrigin); + return me(); + } + + @Override + public B allowOrigins(String... origins) { + aggregator.allowOrigins(origins); + return me(); + } + + @Override + public B allowHeaders(String... allowHeaders) { + aggregator.allowHeaders(allowHeaders); + return me(); + } + + @Override + public B exposeHeaders(String... exposeHeaders) { + aggregator.exposeHeaders(exposeHeaders); + return me(); + } + + @Override + public B allowMethods(String... allowMethods) { + aggregator.allowMethods(allowMethods); + return me(); + } + + @Override + public B allowCredentials(boolean allowCredentials) { + aggregator.allowCredentials(allowCredentials); + return me(); + } + + @Override + public B maxAgeSeconds(long maxAgeSeconds) { + aggregator.maxAgeSeconds(maxAgeSeconds); + return me(); + } + + /** + * Not for developer use. Sets a back-up way to provide a {@code CrossOriginConfig} instance if, during + * look-up for a given request, none is found from the aggregator. + * + * @param secondaryLookupSupplier supplier of a CrossOriginConfig + * @return updated builder + */ + protected Builder secondaryLookupSupplier(Supplier> secondaryLookupSupplier) { + helperBuilder.secondaryLookupSupplier(secondaryLookupSupplier); + return this; + } + } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP request. + * + * @param type of the request wrapped by the adapter + */ + protected interface RequestAdapter { + + /** + * + * @return possibly unnormalized path from the request + */ + String path(); + + /** + * Retrieves the first value for the specified header as a String. + * + * @param key header name to retrieve + * @return the first header value for the key + */ + Optional firstHeader(String key); + + /** + * Reports whether the specified header exists. + * + * @param key header name to check for + * @return whether the header exists among the request's headers + */ + boolean headerContainsKey(String key); + + /** + * Retrieves all header values for a given key as Strings. + * + * @param key header name to retrieve + * @return header values for the header; empty list if none + */ + List allHeaders(String key); + + /** + * Reports the method name for the request. + * + * @return the method name + */ + String method(); + + /** + * Processes the next handler/filter/request processor in the chain. + */ + void next(); + + /** + * Returns the request this adapter wraps. + * + * @return the request + */ + T request(); + } + + /** + * Not for use by developers. + * + * Minimal abstraction of an HTTP response. + * + *

    + * Note to implementers: In some use cases, the CORS support code will invoke the {@code header} methods but not {@code ok} + * or {@code forbidden}. See to it that header values set on the adapter via the {@code header} methods are propagated to the + * actual response. + *

    + * + * @param the type of the response wrapped by the adapter + */ + protected interface ResponseAdapter { + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, String value); + + /** + * Arranges to add the specified header and value to the eventual response. + * + * @param key header name to add + * @param value header value to add + * @return the adapter + */ + ResponseAdapter header(String key, Object value); + + /** + * Returns a response with the forbidden status and the specified error message, without any headers assigned + * using the {@code header} methods. + * + * @param message error message to use in setting the response status + * @return the factory + */ + T forbidden(String message); + + /** + * Returns a response with only the headers that were set on this adapter and the status set to OK. + * + * @return response instance + */ + T ok(); + } +} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java index 3b45f806978..38522c5e131 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportHelper.java @@ -32,8 +32,8 @@ import io.helidon.common.HelidonFlavor; import io.helidon.common.http.Http; import io.helidon.config.Config; -import io.helidon.webserver.cors.CorsSupport.RequestAdapter; -import io.helidon.webserver.cors.CorsSupport.ResponseAdapter; +import io.helidon.webserver.cors.CorsSupportBase.RequestAdapter; +import io.helidon.webserver.cors.CorsSupportBase.ResponseAdapter; import io.helidon.webserver.cors.LogHelper.Headers; import static io.helidon.common.http.Http.Header.HOST; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java deleted file mode 100644 index 54fc0a2573a..00000000000 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsSupportSe.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * 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 io.helidon.webserver.cors; - -import io.helidon.config.Config; -import io.helidon.config.MissingValueException; - -import static io.helidon.webserver.cors.Aggregator.PATHLESS_KEY; - -/** - * SE implementation of {@link CorsSupport}. - */ -public class CorsSupportSe extends CorsSupport { - - private CorsSupportSe(Builder builder) { - super(builder); - } - - /** - * - * @return new builder for CorsSupportSe - */ - public static Builder builder() { - return new Builder(); - } - - /** - * - * @return new CorsSupportSe with default settings - */ - public static CorsSupportSe create() { - return builder().build(); - } - - /** - * Creates a new {@code CorsSupportSe} instance based on the provided configuration expected to match the basic - * {@code CrossOriginConfig} format. - * - * @param config node containing the cross-origin information - * @return initialized {@code CorsSupportSe} instance - */ - public static CorsSupportSe from(Config config) { - if (!config.exists()) { - throw MissingValueException.create(config.key()); - } - Builder builder = builder().addCrossOrigin(PATHLESS_KEY, CrossOriginConfig.builder(config).build()); - return builder.build(); - } - - public static class Builder extends CorsSupport.Builder { - - @Override - public CorsSupportSe build() { - return new CorsSupportSe(this); - } - - @Override - protected Builder me() { - return this; - } - } -} diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java index a40acbe9e57..33ea540799b 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CrossOriginConfig.java @@ -40,7 +40,7 @@ * * and then invoke methods on the builder as needed. Finally invoke the builder's {@code build} method to create the * instance. - *
  • Invoke the static {@link #build(Config)} method, passing a config node containing the cross-origin information to be + *
  • Invoke the static {@link #create(Config)} method, passing a config node containing the cross-origin information to be * converted. This is a convenience method equivalent to creating a builder using the config node and then invoking {@code * build()}. *
  • @@ -123,12 +123,16 @@ public static Builder builder() { /** * Creates a new {@code CrossOriginConfig.Builder} using the provided config node. + *

    + * Although this method is equivalent to {@code builder().config(config)} it conveniently combines those two steps for + * use as a method reference. + *

    * * @param config node containing cross-origin information * @return new {@code CrossOriginConfig.Builder} instance based on the configuration */ public static Builder builder(Config config) { - return Loader.Basic.builder(config); + return Loader.Basic.applyConfig(builder(), config); } /** @@ -154,7 +158,7 @@ public static Builder builder(CrossOriginConfig original) { * @param corsConfig node containing CORS information * @return new {@code CrossOriginConfig} based on the configuration */ - public static CrossOriginConfig build(Config corsConfig) { + public static CrossOriginConfig create(Config corsConfig) { return builder(corsConfig).build(); } @@ -303,6 +307,17 @@ public Builder maxAgeSeconds(long maxAgeSeconds) { return this; } + /** + * Augment or override existing settings using the provided {@code Config} node. + * + * @param corsConfig config node containing CORS information + * @return updated builder + */ + public Builder config(Config corsConfig) { + Loader.Basic.applyConfig(this, corsConfig); + return this; + } + @Override public CrossOriginConfig build() { return new CrossOriginConfig(this); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java index 2e66da81be3..5cd8a62226f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/Loader.java @@ -30,11 +30,7 @@ class Loader { static class Basic { - static CrossOriginConfig.Builder builder(Config config) { - return builder(CrossOriginConfig.builder(), config); - } - - static CrossOriginConfig.Builder builder(CrossOriginConfig.Builder builder, Config config) { + static CrossOriginConfig.Builder applyConfig(CrossOriginConfig.Builder builder, Config config) { config.get("enabled") .asBoolean() .ifPresent(builder::enabled); @@ -69,11 +65,11 @@ static CrossOriginConfig.Builder builder(CrossOriginConfig.Builder builder, Conf static class Mapped { - static MappedCrossOriginConfig.Builder builder(Config config) { - return builder(MappedCrossOriginConfig.builder(), config); + static MappedCrossOriginConfig.Builder applyConfig(Config config) { + return applyConfig(MappedCrossOriginConfig.builder(), config); } - static MappedCrossOriginConfig.Builder builder(MappedCrossOriginConfig.Builder builder, Config config) { + static MappedCrossOriginConfig.Builder applyConfig(MappedCrossOriginConfig.Builder builder, Config config) { config.get("enabled").asBoolean().ifPresent(builder::enabled); Config pathsNode = config.get(CORS_PATHS_CONFIG_KEY); diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java index 0d05cecf3eb..4ecf5e1ae0f 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/LogHelper.java @@ -25,7 +25,7 @@ import java.util.logging.Level; import io.helidon.common.http.Http; -import io.helidon.webserver.cors.CorsSupport.RequestAdapter; +import io.helidon.webserver.cors.CorsSupportBase.RequestAdapter; import io.helidon.webserver.cors.CorsSupportHelper.RequestType; import static io.helidon.common.http.Http.Header.HOST; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java index 5da9f70711d..bb488a7e621 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/MappedCrossOriginConfig.java @@ -81,12 +81,16 @@ public static Builder builder() { /** * Creates a new {@code Mapped.Builder} instance using the provided configuration. + *

    + * Although this method is equivalent to {@code builder().config(config)} it conveniently combines those two steps for + * use as a method reference. + *

    * * @param config node containing {@code Mapped} cross-origin information * @return new {@code Mapped.Builder} based on the config */ public static Builder builder(Config config) { - return Loader.Mapped.builder(config); + return builder().config(config); } /** @@ -95,7 +99,7 @@ public static Builder builder(Config config) { * @param config node containing {@code Mapped} cross-origin information * @return new {@code Mapped} instance based on the config */ - public static MappedCrossOriginConfig from(Config config) { + public static MappedCrossOriginConfig create(Config config) { return builder(config).build(); } @@ -183,5 +187,15 @@ public Builder put(String path, CrossOriginConfig.Builder builder) { builders.put(normalize(path), new Buildable(builder)); return this; } + + /** + * Applies data in the provided config node. + * + * @param corsConfig {@code Config} node containing CORS information + * @return updated builder + */ + public Builder config(Config corsConfig) { + return Loader.Mapped.applyConfig(corsConfig); + } } } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java index 31fdce19f14..c6c08e27db3 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/RequestAdapterSe.java @@ -22,9 +22,9 @@ import io.helidon.webserver.ServerRequest; /** - * Helidon SE implementation of {@link CorsSupport.RequestAdapter}. + * Helidon SE implementation of {@link CorsSupportBase.RequestAdapter}. */ -class RequestAdapterSe implements CorsSupport.RequestAdapter { +class RequestAdapterSe implements CorsSupportBase.RequestAdapter { private final ServerRequest request; diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java index 5acbaf0a5ba..a0d62558575 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/ResponseAdapterSe.java @@ -20,9 +20,9 @@ import io.helidon.webserver.ServerResponse; /** - * SE implementation of {@link CorsSupport.ResponseAdapter}. + * SE implementation of {@link CorsSupportBase.ResponseAdapter}. */ -class ResponseAdapterSe implements CorsSupport.ResponseAdapter { +class ResponseAdapterSe implements CorsSupportBase.ResponseAdapter { private final ServerResponse serverResponse; @@ -31,13 +31,13 @@ class ResponseAdapterSe implements CorsSupport.ResponseAdapter { } @Override - public CorsSupport.ResponseAdapter header(String key, String value) { + public CorsSupportBase.ResponseAdapter header(String key, String value) { serverResponse.headers().add(key, value); return this; } @Override - public CorsSupport.ResponseAdapter header(String key, Object value) { + public CorsSupportBase.ResponseAdapter header(String key, Object value) { serverResponse.headers().add(key, value.toString()); return this; } diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java index 388c625f037..c125e3b29d5 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/package-info.java @@ -18,7 +18,7 @@ /** *

    Helidon SE CORS Support

    *

    - * Use {@link io.helidon.webserver.cors.CorsSupportSe} and its {@link io.helidon.webserver.cors.CorsSupportSe.Builder} to add CORS + * Use {@link io.helidon.webserver.cors.CorsSupport} and its {@link io.helidon.webserver.cors.CorsSupport.Builder} to add CORS * handling to resources in your application. *

    * Because Helidon SE does not use annotation processing to identify endpoints, you need to provide the CORS information for @@ -63,10 +63,10 @@ * Config myAppConfig = Config.builder().sources(ConfigSources.classpath("myApp.yaml")).build(); * Routing.Builder builder = Routing.builder() * .any("/greet", - * CorsSupportSe.from(myAppConfig.get("narrow")), + * CorsSupport.create(myAppConfig.get("narrow")), * (req, resp) -> resp.status(Http.Status.OK_200).send()) * .get("/greet", - * CorsSupportSe.from(myAppConfig.get("wide")), + * CorsSupport.create(myAppConfig.get("wide")), * (req, resp) -> resp.status(Http.Status.OK_200).send("Hello, World!")); * * } @@ -90,12 +90,12 @@ * * Routing.Builder builder = Routing.builder() * .register("/myapp", - * CorsSupportSe.builder() + * CorsSupport.builder() * .addCrossOrigin("/cors3", corsForCORS3) // links the CORS info with a path within the app * .build(), * new MyApp()); * - * Notice that you pass two services to the {@code register} method: the {@code CorsSupportSe} instance and your app + * Notice that you pass two services to the {@code register} method: the {@code CorsSupport} instance and your app * instance. Helidon will process requests to the path you specify with those services in that order. Also, note that you have * to make sure you use the same path in this API call and in your {@code MyApp} service if you adjust the routing there. *

    @@ -108,11 +108,11 @@ * {@code OPTIONS} requests. *

    *

    - * Each {@code CorsSupportSe} instance can be enabled or disabled, either through configuration or using the API. - * By default, when an application creates a new {@code CorsSupportSe.Builder} instance that builder's {@code build()} method will - * create an enabled {@code CorsSupportSe} object. Any subsequent explicit setting on the builder, either expressly set by an - * {@code enabled} entry in configuration passed to {@code CorsSupportSe.Builder.config} or set by invoking - * {@code CorsSupportSe.Builder.enabled} follows the familiar "latest-wins" approach. + * Each {@code CorsSupport} instance can be enabled or disabled, either through configuration or using the API. + * By default, when an application creates a new {@code CorsSupport.Builder} instance that builder's {@code build()} method will + * create an enabled {@code CorsSupport} object. Any subsequent explicit setting on the builder, either expressly set by an + * {@code enabled} entry in configuration passed to {@code CorsSupport.Builder.config} or set by invoking + * {@code CorsSupport.Builder.enabled} follows the familiar "latest-wins" approach. *

    */ package io.helidon.webserver.cors; diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java index 9160369d75d..f4ce1055bfc 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/CrossOriginConfigTest.java @@ -49,7 +49,7 @@ public void testNarrow() { Config node = testConfig.get("narrow"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig c = node.as(CrossOriginConfig::build).get(); + CrossOriginConfig c = node.as(CrossOriginConfig::create).get(); assertThat(c.isEnabled(), is(true)); assertThat(c.allowOrigins(), arrayContaining("http://foo.bar", "http://bar.foo")); @@ -63,7 +63,7 @@ public void testNarrow() { @Test public void testMissing() { Assertions.assertThrows(MissingValueException.class, () -> { - CrossOriginConfig basic = testConfig.get("notThere").as(CrossOriginConfig::build).get(); + CrossOriginConfig basic = testConfig.get("notThere").as(CrossOriginConfig::create).get(); }); } @@ -72,7 +72,7 @@ public void testWide() { Config node = testConfig.get("wide"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig b = node.as(CrossOriginConfig::build).get(); + CrossOriginConfig b = node.as(CrossOriginConfig::create).get(); assertThat(b.isEnabled(), is(false)); assertThat(b.allowOrigins(), arrayContaining(ALLOW_ALL)); @@ -88,7 +88,7 @@ public void testJustDisabled() { Config node = testConfig.get("just-disabled"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - CrossOriginConfig b = node.as(CrossOriginConfig::build).get(); + CrossOriginConfig b = node.as(CrossOriginConfig::create).get(); assertThat(b.isEnabled(), is(false)); } @@ -98,7 +98,7 @@ public void testPaths() { Config node = testConfig.get("cors-setup"); assertThat(node, is(notNullValue())); assertThat(node.exists(), is(true)); - MappedCrossOriginConfig m = node.as(MappedCrossOriginConfig::from).get(); + MappedCrossOriginConfig m = node.as(MappedCrossOriginConfig::create).get(); assertThat(m.isEnabled(), is(true)); diff --git a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java index 7911c5f0462..b57930d1da1 100644 --- a/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java +++ b/webserver/cors/src/test/java/io/helidon/webserver/cors/TestUtil.java @@ -61,7 +61,7 @@ static Routing.Builder prepRouting() { /* * Use the default config for the service at "/greet" and then programmatically add the config for /cors3. */ - CorsSupportSe.Builder corsSupportBuilder = CorsSupportSe.builder(); + CorsSupport.Builder corsSupportBuilder = CorsSupport.builder(); corsSupportBuilder.addCrossOrigin(SERVICE_3.path(), cors3COC); /* @@ -74,16 +74,16 @@ static Routing.Builder prepRouting() { Routing.Builder builder = Routing.builder() .register(GREETING_PATH, - CorsSupportSe.builder().config(Config.create().get("cors-setup")).build(), + CorsSupport.builder().config(Config.create().get("cors-setup")).build(), new GreetService()) .register(OTHER_GREETING_PATH, - CorsSupportSe.builder().config(twoCORSConfig.get("cors-2-setup")).build(), + CorsSupport.builder().config(twoCORSConfig.get("cors-2-setup")).build(), new GreetService("Other Hello")) .any(TestHandlerRegistration.CORS4_CONTEXT_ROOT, - CorsSupportSe.from(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode + CorsSupport.create(twoCORSConfig.get("somewhat-restrictive")), // handler settings from config subnode (req, resp) -> resp.status(Http.Status.OK_200).send()) .get(TestHandlerRegistration.CORS4_CONTEXT_ROOT, // handler settings in-line - CorsSupportSe.builder() + CorsSupport.builder() .allowOrigins("*") .allowMethods("GET") .build(),