Skip to content

Commit bc6dd7f

Browse files
committed
Finalize cors implementation + cors doc
1 parent e127398 commit bc6dd7f

10 files changed

Lines changed: 317 additions & 68 deletions

File tree

docs/asciidoc/cors.adoc

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
== CORS
2+
3+
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[Cross-Origin Resource Sharing (CORS)] is a mechanism that uses additional HTTP headers to tell a
4+
browser to let a web application running at one origin (domain) have permission to access selected
5+
resources from a server at a different origin. A web application executes a cross-origin HTTP
6+
request when it requests a resource that has a different origin (domain, protocol, or port) than
7+
its own origin.
8+
9+
Jooby supports CORS out of the box. By default, **CORS requests will be rejected**.
10+
To enable processing of CORS requests, use the javadoc:CorsHandler[]:
11+
12+
.CorsExample
13+
[source, java, role = "primary"]
14+
----
15+
import io.jooby.Jooby;
16+
import io.jooby.CorsHandler;
17+
...
18+
{
19+
20+
decorator(new CorsHandler()); <1>
21+
22+
path("/api", () -> {
23+
// API methods
24+
});
25+
}
26+
----
27+
28+
.Kotlin
29+
[source, kotlin, role = "secondary"]
30+
----
31+
import io.jooby.Jooby
32+
import io.jooby.CorsHandler
33+
...
34+
{
35+
decorator(CorsHandler()) <1>
36+
37+
path("/api") {
38+
// API methods
39+
}
40+
}
41+
----
42+
43+
<1> Install CorsHandler with defaults options
44+
45+
Default options are:
46+
47+
- origin: `*`
48+
- credentials: `true`
49+
- allowed methods: `GET`, `POST`
50+
- allowed headers: `X-Requested-With`, `Content-Type`, `Accept` and `Origin`
51+
- max age: `30m`;
52+
53+
To customize default options use javadoc:Cors[]:
54+
55+
.Cors options
56+
[source, java, role = "primary"]
57+
----
58+
import io.jooby.Jooby;
59+
import io.jooby.CorsHandler;
60+
...
61+
{
62+
Cors cors = new Cors()
63+
.setMethods("GET", "POST", "PUT"); <1>
64+
65+
decorator(new CorsHandler(cors)); <2>
66+
67+
path("/api", () -> {
68+
// API methods
69+
});
70+
}
71+
----
72+
73+
.Kotlin
74+
[source, kotlin, role = "secondary"]
75+
----
76+
import io.jooby.Jooby
77+
import io.jooby.CorsHandler
78+
import io.jooby.cors
79+
...
80+
{
81+
val cors = cors {
82+
methods = listOf("GET", "POST", "PUT") <1>
83+
}
84+
decorator(CorsHandler(cors)) <2>
85+
86+
path("/api") {
87+
// API methods
88+
}
89+
}
90+
----
91+
92+
<1> Specify allowed methods
93+
<2> Pass cors options to cors handler
94+
95+
Optionally cors options can be specified in the application configuration file:
96+
97+
.application.conf
98+
[source,json]
99+
----
100+
cors {
101+
origin: "*"
102+
credentials: true
103+
methods: [GET, POST],
104+
headers: [Content-Type],
105+
maxAge: 30m
106+
exposedHeaders: [Custom-Header]
107+
}
108+
----
109+
110+
.Loading options
111+
[source, java, role = "primary"]
112+
----
113+
import io.jooby.Jooby;
114+
import io.jooby.CorsHandler;
115+
...
116+
{
117+
Cors cors = Cors.from(getConfig()); <1>
118+
119+
decorator(new CorsHandler(cors));
120+
121+
path("/api", () -> {
122+
// API methods
123+
});
124+
}
125+
----
126+
127+
.Kotlin
128+
[source, kotlin, role = "secondary"]
129+
----
130+
import io.jooby.Jooby
131+
import io.jooby.CorsHandler
132+
...
133+
{
134+
val cors = Cors.from(config) <1>
135+
decorator(CorsHandler(cors))
136+
137+
path("/api") {
138+
// API methods
139+
}
140+
}
141+
----
142+
143+
<1> Load cors options from application configuration file

docs/asciidoc/error-handler.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ import io.jooby.Medio.html
163163

164164
In addition to the generic/global error handler you can catch specific status code:
165165

166-
.Satus Code Error Handler
166+
.Status Code Error Handler
167167
[source, java, role = "primary"]
168168
----
169169
import static io.jooby.StatusCode.NOT_FOUND;

docs/asciidoc/index.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ include::mvc-api.adoc[]
226226

227227
include::error-handler.adoc[]
228228

229+
include::cors.adoc[]
230+
229231
include::configuration.adoc[]
230232

231233
include::testing.adoc[]

jooby/src/main/java/io/jooby/Cors.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*
3030
* <pre>
3131
* {
32-
* before(new Cors());
32+
* decorator(new CorsHandler());
3333
* }
3434
* </pre>
3535
*
@@ -76,7 +76,7 @@ public boolean test(final T value) {
7676

7777
private Duration maxAge;
7878

79-
private List<String> exposedHeaders;
79+
private List<String> exposedHeaders = Collections.emptyList();
8080

8181
/**
8282
* Creates default {@link Cors}. Default options are:
@@ -96,7 +96,6 @@ public Cors() {
9696
setMethods("GET", "POST");
9797
setHeaders("X-Requested-With", "Content-Type", "Accept", "Origin");
9898
setMaxAge(Duration.ofMinutes(30));
99-
setExposedHeaders();
10099
}
101100

102101
/**

jooby/src/main/java/io/jooby/CorsHandler.java

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55
import java.util.List;
66
import java.util.stream.Collectors;
77

8-
import org.slf4j.Logger;
9-
import org.slf4j.LoggerFactory;
10-
118
import javax.annotation.Nonnull;
129

1310
/**
1411
* Handle preflight and simple CORS requests. CORS options are set via: {@link Cors}.
1512
*
1613
* @author edgar
17-
* @since 0.8.0
14+
* @since 2.0.4
1815
* @see Cors
1916
*/
20-
public class CorsHandler implements Route.Before {
17+
public class CorsHandler implements Route.Decorator {
2118

2219
private static final String ORIGIN = "Origin";
2320

@@ -39,9 +36,6 @@ public class CorsHandler implements Route.Before {
3936

4037
private static final String AC_ALLOW_METHODS = "Access-Control-Allow-Methods";
4138

42-
/** The logging system. */
43-
private final Logger log = LoggerFactory.getLogger(Cors.class);
44-
4539
private final Cors options;
4640

4741
/**
@@ -57,53 +51,68 @@ public CorsHandler() {
5751
this(new Cors());
5852
}
5953

60-
@Override public void apply(@Nonnull Context ctx) throws Exception {
61-
Value origin = ctx.header("Origin");
62-
if (!origin.isMissing()) {
63-
cors(options, ctx, origin.value());
64-
}
65-
}
66-
67-
private void cors(final Cors options, final Context ctx, final String origin) throws Exception {
68-
if (options.allowOrigin(origin)) {
69-
log.debug("allowed origin: {}", origin);
70-
if (preflight(ctx)) {
71-
log.debug("handling preflight for: {}", origin);
72-
preflight(options, ctx, origin);
73-
} else {
74-
log.debug("handling simple options for: {}", origin);
75-
if ("null".equals(origin)) {
76-
ctx.setResponseHeader(AC_ALLOW_ORIGIN, ANY_ORIGIN);
77-
} else {
78-
ctx.setResponseHeader(AC_ALLOW_ORIGIN, origin);
79-
if (!options.anyHeader()) {
80-
ctx.setResponseHeader("Vary", ORIGIN);
81-
}
82-
if (options.getUseCredentials()) {
83-
ctx.setResponseHeader(AC_ALLOW_CREDENTIALS, true);
84-
}
85-
if (!options.getExposedHeaders().isEmpty()) {
86-
ctx.setResponseHeader(AC_EXPOSE_HEADERS, options.getExposedHeaders().stream().collect(Collectors.joining()));
54+
@Nonnull @Override public Route.Handler apply(@Nonnull Route.Handler next) {
55+
return ctx -> {
56+
String origin = ctx.header("Origin").valueOrNull();
57+
if (origin != null && options.allowOrigin(origin)) {
58+
if (isPreflight(ctx)) {
59+
if (preflight(ctx, options, origin)) {
60+
return ctx;
61+
} else {
62+
return ctx.send(StatusCode.FORBIDDEN);
8763
}
64+
} else if (isSimple(ctx)) {
65+
simple(ctx, options, origin);
66+
} else {
67+
return ctx.send(StatusCode.FORBIDDEN);
8868
}
8969
}
70+
return next.apply(ctx);
71+
};
72+
}
73+
74+
private boolean isSimple(Context ctx) {
75+
return ctx.getMethod().equals(Router.GET)
76+
|| ctx.getMethod().equals(Router.POST)
77+
|| ctx.getMethod().equals(Router.HEAD);
78+
}
79+
80+
private void simple(final Context ctx, final Cors options, final String origin) throws Exception {
81+
if ("null".equals(origin)) {
82+
ctx.setResponseHeader(AC_ALLOW_ORIGIN, ANY_ORIGIN);
83+
} else {
84+
ctx.setResponseHeader(AC_ALLOW_ORIGIN, origin);
85+
if (!options.anyHeader()) {
86+
ctx.setResponseHeader("Vary", ORIGIN);
87+
}
88+
if (options.getUseCredentials()) {
89+
ctx.setResponseHeader(AC_ALLOW_CREDENTIALS, true);
90+
}
91+
if (!options.getExposedHeaders().isEmpty()) {
92+
ctx.setResponseHeader(AC_EXPOSE_HEADERS,
93+
options.getExposedHeaders().stream().collect(Collectors.joining()));
94+
}
9095
}
9196
}
9297

93-
private boolean preflight(final Context ctx) {
94-
return ctx.getMethod().equalsIgnoreCase("OPTIONS") && !ctx.header(AC_REQUEST_METHOD)
95-
.isMissing();
98+
@Nonnull @Override public Route.Decorator setRoute(@Nonnull Route route) {
99+
route.setHttpOptions(true);
100+
return this;
101+
}
102+
103+
private boolean isPreflight(final Context ctx) {
104+
return ctx.getMethod().equals(Router.OPTIONS) && !ctx.header(AC_REQUEST_METHOD).isMissing();
96105
}
97106

98-
private void preflight(final Cors options, final Context ctx, final String origin) {
107+
private boolean preflight(final Context ctx, final Cors options, final String origin) {
99108
/**
100109
* Allowed method
101110
*/
102111
boolean allowMethod = ctx.header(AC_REQUEST_METHOD).toOptional()
103112
.map(options::allowMethod)
104113
.orElse(false);
105114
if (!allowMethod) {
106-
return;
115+
return false;
107116
}
108117

109118
/**
@@ -113,7 +122,7 @@ private void preflight(final Cors options, final Context ctx, final String origi
113122
Arrays.asList(header.split("\\s*,\\s*"))
114123
).orElse(Collections.emptyList());
115124
if (!options.allowHeaders(headers)) {
116-
return;
125+
return false;
117126
}
118127

119128
/**
@@ -145,5 +154,6 @@ private void preflight(final Cors options, final Context ctx, final String origi
145154
}
146155

147156
ctx.send(StatusCode.OK);
157+
return true;
148158
}
149159
}

0 commit comments

Comments
 (0)