Skip to content

Commit e20db7e

Browse files
Add pekko-http-cors module
Co-Authored-By: Lomig Mégard <[email protected]>
1 parent f4c813c commit e20db7e

File tree

27 files changed

+2054
-2
lines changed

27 files changed

+2054
-2
lines changed

NOTICE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ Copyright (c) 2011-2023 Lightbend, Inc.
2121
Scala includes software developed at
2222
LAMP/EPFL (https://lamp.epfl.ch/) and
2323
Lightbend, Inc. (https://www.lightbend.com/).
24+
25+
---------------
26+
27+
pekko-http-cors contains code that was donated by Lomig Mégard to the Apache Software Foundation
28+
via a Software Grant Agreement <https://www.apache.org/licenses/contributor-agreements.html#grants>
29+
See <https://github.com/lomigmegard/pekko-http-cors/issues/33>.

build.sbt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ lazy val userProjects: Seq[ProjectReference] = List[ProjectReference](
5555
http2Tests,
5656
http,
5757
httpCaching,
58+
httpCors,
5859
httpTestkit,
5960
httpMarshallersScala,
6061
httpMarshallersJava,
@@ -250,7 +251,7 @@ lazy val httpTests = project("http-tests")
250251

251252
lazy val httpJmhBench = project("http-bench-jmh")
252253
.settings(commonSettings)
253-
.dependsOn(http, http2Tests % "compile->compile,test")
254+
.dependsOn(http, httpCors, http2Tests % "compile->compile,test")
254255
.addPekkoModuleDependency("pekko-stream")
255256
.enablePlugins(JmhPlugin)
256257
.enablePlugins(NoPublish) // don't release benchs
@@ -300,6 +301,17 @@ lazy val httpCaching = project("http-caching")
300301
.dependsOn(http, httpCore, httpTestkit % "test")
301302
.enablePlugins(BootstrapGenjavadoc)
302303

304+
lazy val httpCors = project("http-cors")
305+
.settings(
306+
name := "pekko-http-cors")
307+
.settings(commonSettings)
308+
.settings(AutomaticModuleName.settings("pekko.http.cors"))
309+
.addPekkoModuleDependency("pekko-stream", "provided")
310+
.addPekkoModuleDependency("pekko-stream-testkit", "provided")
311+
.settings(Dependencies.httpCors)
312+
.dependsOn(http, httpCore, httpTestkit % "test")
313+
.enablePlugins(BootstrapGenjavadoc)
314+
303315
def project(moduleName: String) =
304316
Project(id = moduleName, base = file(moduleName)).settings(
305317
name := s"pekko-$moduleName")
@@ -389,7 +401,7 @@ lazy val docs = project("docs")
389401
.addPekkoModuleDependency("pekko-stream-testkit", "provided", PekkoDependency.docs)
390402
.addPekkoModuleDependency("pekko-actor-testkit-typed", "provided", PekkoDependency.docs)
391403
.dependsOn(
392-
httpCore, http, httpXml, http2Tests, httpMarshallersJava, httpMarshallersScala, httpCaching,
404+
httpCore, http, httpXml, http2Tests, httpMarshallersJava, httpMarshallersScala, httpCaching, httpCors,
393405
httpTests % "compile;test->test", httpTestkit % "compile;test->test", httpScalafixRules % ScalafixConfig)
394406
.settings(Dependencies.docs)
395407
.settings(

docs/src/main/paradox/common/cors.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Cors
2+
3+
Apache Pekko HTTP's cors module provides support for the [W3C cors standard](https://www.w3.org/TR/cors/)
4+
5+
## Dependency
6+
7+
To use Apache Pekko HTTP Cors, add the module to your project:
8+
9+
@@dependency [sbt,Gradle,Maven] {
10+
bomGroup2="org.apache.pekko" bomArtifact2="pekko-http-bom_$scala.binary.version$" bomVersionSymbols2="PekkoHttpVersion"
11+
symbol="PekkoHttpVersion"
12+
value="$project.version$"
13+
group="org.apache.pekko"
14+
artifact="pekko-http-cors_$scala.binary.version$"
15+
version="PekkoHttpVersion"
16+
}
17+
18+
## Quick Start
19+
The simplest way to enable CORS in your application is to use the `cors` directive.
20+
Settings are passed as a parameter to the directive, with your overrides loaded from the `application.conf`.
21+
22+
@@@ div { .group-scala }
23+
```scala
24+
import org.apache.pekko.http.cors.scaladsl.CorsDirectives._
25+
26+
val route: Route = cors() {
27+
complete(...)
28+
}
29+
```
30+
@@@
31+
@@@ div { .group-java }
32+
```java
33+
import static org.apache.pekko.http.cors.javadsl.CorsDirectives.*;
34+
35+
final Route route = cors(() -> {
36+
complete(...)
37+
})
38+
```
39+
@@@
40+
41+
The settings can be updated programmatically too.
42+
@@@ div { .group-scala }
43+
```scala
44+
val settings = CorsSettings(...).withAllowGenericHttpRequests(false)
45+
val strictRoute: Route = cors(settings) {
46+
complete(...)
47+
}
48+
```
49+
@@@
50+
@@@ div { .group-java }
51+
```java
52+
final CorsSettings settings = CorsSettings.create(...).withAllowGenericHttpRequests(false);
53+
final Route route = cors(settings, () -> {
54+
complete(...)
55+
});
56+
```
57+
@@@
58+
59+
## Rejection
60+
The CORS directives can reject requests using the `CorsRejection` class. Requests can be either malformed or not allowed to access the resource.
61+
62+
A rejection handler is provided by the library to return meaningful HTTP responses. Read the pekko documentation (link TODO) to learn more about rejections, or if you need to write your own handler.
63+
64+
Scala
65+
: @@snip [CorsServerExample.scala](/docs/src/test/scala/docs/http/scaladsl/server/cors/CorsServerExample.scala) { #cors-server-example }
66+
67+
Java
68+
: @@snip [CorsServerExample.java](/docs/src/test/java/docs/http/javadsl/server/cors/CorsServerExample.java) { #cors-server-example }
69+
70+
#### allowGenericHttpRequests / getAllowGenericHttpRequests
71+
If `true`, allow generic requests (that are outside the scope of the specification) to pass through the directive. Else, strict CORS filtering is applied and any invalid request will be rejected.
72+
73+
#### allowCredentials / getAllowCredentials
74+
Indicates whether the resource supports user credentials. If `true`, the header `Access-Control-Allow-Credentials` is set in the response, indicating the actual request can include user credentials.
75+
76+
Examples of user credentials are: cookies, HTTP authentication or client-side certificates.
77+
78+
#### allowedOrigins / getAllowedOrigins
79+
List of origins that the CORS filter must allow. Can also be set to `*` to allow access to the resource from any origin. Controls the content of the `Access-Control-Allow-Origin` response header:
80+
* if parameter is `*` **and** credentials are not allowed, a `*` is set in `Access-Control-Allow-Origin`.
81+
* otherwise, the origins given in the `Origin` request header are echoed.
82+
83+
Hostname starting with `*.` will match any sub-domain. The scheme and the port are always strictly matched.
84+
85+
The actual or preflight request is rejected if any of the origins from the request is not allowed.
86+
87+
#### allowedHeaders / getAllowedHeaders
88+
List of request headers that can be used when making an actual request. Controls the content of the `Access-Control-Allow-Headers` header in a preflight response:
89+
* if parameter is `*`, the headers from `Access-Control-Request-Headers` are echoed.
90+
* otherwise the parameter list is returned as part of the header.
91+
92+
#### allowedMethods / getAllowedMethods
93+
List of methods that can be used when making an actual request. The list is returned as part of the `Access-Control-Allow-Methods` preflight response header.
94+
95+
The preflight request will be rejected if the `Access-Control-Request-Method` header's method is not part of the list.
96+
97+
#### exposedHeaders / getAllowedMethods
98+
List of headers (other than [simple response headers](https://www.w3.org/TR/cors/#simple-response-header)) that browsers are allowed to access. If not empty, this list is returned as part of the `Access-Control-Expose-Headers` header in the actual response.
99+
100+
#### maxAge / getMaxAge
101+
When set, the amount of seconds the browser is allowed to cache the results of a preflight request. This value is returned as part of the `Access-Control-Max-Age` preflight response header. If @scala[`None`]@java[`OptionalLong.empty()`], the header is not added to the preflight response.
102+
103+
## Benchmarks
104+
105+
Benchmarks for Apache Pekko cors are located within the @github[http-bench-jmh project](/http-bench-jmh/src/main/scala/org/apache/pekko/http/cors/CorsBenchmark.scala) along with
106+
@github[instructions](/http-bench-jmh/README.md) on how to run them.
107+
108+
Please look at the original project [Akka Http Cors](https://github.com/lomigmegard/akka-http-cors) for previous benchmarks with Akka Http.

docs/src/main/paradox/common/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ which are specific to one side only.
2020
* [xml-support](sse-support.md)
2121
* [timeouts](timeouts.md)
2222
* [caching](caching.md)
23+
* [cors](cors.md)
2324

2425
@@@

docs/src/main/paradox/configuration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ pekko-http
1515
pekko-http-caching
1616
: @@snip [reference.conf](/http-caching/src/main/resources/reference.conf)
1717

18+
pekko-http-cors
19+
: @@snip [reference.conf](/http-cors/src/main/resources/reference.conf)
20+
1821
The other Apache Pekko HTTP modules do not offer any configuration via [Typesafe Config](https://github.com/lightbend/config).

docs/src/main/paradox/migration-guide/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@
1111
* Where class names have "Akka" in the name, the Pekko ones have "Pekko" - e.g. PekkoException instead of AkkaException
1212
* Configs in `application.conf` use "pekko" prefix instead of "akka"
1313
* We will soon provide a more detailed guide for migrating from Akka HTTP v10.2 to Apache Pekko HTTP
14+
* If you happen to be using [akka-http-cors](https://github.com/lomigmegard/akka-http-cors) this has been directly integrated into
15+
Apache Pekko Http which means you should use the @ref:[pekko-http-cors artifact](../common/cors.md) instead. Note that the root configuration naming
16+
has also been updated (i.e. changing from `akka-http-cors` to `pekko.http.cors`) to make it consistent with the rest of Apache Pekko Http.
17+
* In addition there is a single breaking change, the return type of the `org.apache.pekko.http.cors.javadsl.settings.CorsSettings.getMaxAge`
18+
method has been changed from `java.util.Optional<Long>` to `java.util.OptionalLong` since it's more idiomatic.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package docs.http.javadsl.server.cors;
19+
20+
// #cors-server-example
21+
import org.apache.pekko.http.javadsl.model.StatusCodes;
22+
import org.apache.pekko.http.javadsl.server.*;
23+
24+
import java.util.NoSuchElementException;
25+
import java.util.concurrent.ExecutionException;
26+
import java.util.function.Function;
27+
import java.util.function.Supplier;
28+
29+
import static org.apache.pekko.http.cors.javadsl.CorsDirectives.cors;
30+
import static org.apache.pekko.http.cors.javadsl.CorsDirectives.corsRejectionHandler;
31+
32+
public class CorsServerExample extends HttpApp {
33+
34+
public static void main(String[] args) throws ExecutionException, InterruptedException {
35+
final CorsServerExample app = new CorsServerExample();
36+
app.startServer("127.0.0.1", 9000);
37+
}
38+
39+
protected Route routes() {
40+
41+
// Your CORS settings are loaded from `application.conf`
42+
43+
// Your rejection handler
44+
final RejectionHandler rejectionHandler =
45+
corsRejectionHandler().withFallback(RejectionHandler.defaultHandler());
46+
47+
// Your exception handler
48+
final ExceptionHandler exceptionHandler =
49+
ExceptionHandler.newBuilder()
50+
.match(
51+
NoSuchElementException.class,
52+
ex -> complete(StatusCodes.NOT_FOUND, ex.getMessage()))
53+
.build();
54+
55+
// Combining the two handlers only for convenience
56+
final Function<Supplier<Route>, Route> handleErrors =
57+
inner ->
58+
Directives.allOf(
59+
s -> handleExceptions(exceptionHandler, s),
60+
s -> handleRejections(rejectionHandler, s),
61+
inner);
62+
63+
// Note how rejections and exceptions are handled *before* the CORS directive (in the inner
64+
// route).
65+
// This is required to have the correct CORS headers in the response even when an error occurs.
66+
return handleErrors.apply(
67+
() ->
68+
cors(
69+
() ->
70+
handleErrors.apply(
71+
() ->
72+
concat(
73+
path("ping", () -> complete("pong")),
74+
path(
75+
"pong",
76+
() ->
77+
failWith(
78+
new NoSuchElementException(
79+
"pong not found, try with ping")))))));
80+
}
81+
}
82+
// #cors-server-example
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package docs.http.scaladsl.server.cors
19+
20+
// #cors-server-example
21+
import org.apache.pekko
22+
import pekko.http.scaladsl.model.StatusCodes
23+
import pekko.http.scaladsl.server._
24+
25+
object CorsServerExample extends HttpApp {
26+
def main(args: Array[String]): Unit = {
27+
CorsServerExample.startServer("127.0.0.1", 9000)
28+
}
29+
30+
protected def routes: Route = {
31+
import pekko.http.cors.scaladsl.CorsDirectives._
32+
33+
// Your CORS settings are loaded from `application.conf`
34+
35+
// Your rejection handler
36+
val rejectionHandler = corsRejectionHandler.withFallback(RejectionHandler.default)
37+
38+
// Your exception handler
39+
val exceptionHandler = ExceptionHandler { case e: NoSuchElementException =>
40+
complete(StatusCodes.NotFound -> e.getMessage)
41+
}
42+
43+
// Combining the two handlers only for convenience
44+
val handleErrors = handleRejections(rejectionHandler) & handleExceptions(exceptionHandler)
45+
46+
// Note how rejections and exceptions are handled *before* the CORS directive (in the inner route).
47+
// This is required to have the correct CORS headers in the response even when an error occurs.
48+
handleErrors {
49+
cors() {
50+
handleErrors {
51+
path("ping") {
52+
complete("pong")
53+
} ~
54+
path("pong") {
55+
failWith(new NoSuchElementException("pong not found, try with ping"))
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}
62+
// #cors-server-example

0 commit comments

Comments
 (0)