1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.handler.codec.http.cors;
17
18 import io.netty.buffer.Unpooled;
19 import io.netty.channel.ChannelDuplexHandler;
20 import io.netty.channel.ChannelFuture;
21 import io.netty.channel.ChannelFutureListener;
22 import io.netty.channel.ChannelHandlerContext;
23 import io.netty.channel.ChannelPromise;
24 import io.netty.handler.codec.http.DefaultFullHttpResponse;
25 import io.netty.handler.codec.http.HttpContent;
26 import io.netty.handler.codec.http.HttpHeaderNames;
27 import io.netty.handler.codec.http.HttpHeaderValues;
28 import io.netty.handler.codec.http.HttpHeaders;
29 import io.netty.handler.codec.http.DefaultHttpHeadersFactory;
30 import io.netty.handler.codec.http.HttpRequest;
31 import io.netty.handler.codec.http.HttpResponse;
32 import io.netty.handler.codec.http.HttpUtil;
33 import io.netty.util.ReferenceCountUtil;
34 import io.netty.util.internal.logging.InternalLogger;
35 import io.netty.util.internal.logging.InternalLoggerFactory;
36
37 import java.util.Collections;
38 import java.util.List;
39
40 import static io.netty.handler.codec.http.HttpMethod.OPTIONS;
41 import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
42 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
43 import static io.netty.util.ReferenceCountUtil.release;
44 import static io.netty.util.internal.ObjectUtil.checkNonEmpty;
45 import static io.netty.util.internal.ObjectUtil.checkNotNull;
46
47
48
49
50
51
52
53
54
55
56
57 public class CorsHandler extends ChannelDuplexHandler {
58
59 private static final InternalLogger logger = InternalLoggerFactory.getInstance(CorsHandler.class);
60 private static final String ANY_ORIGIN = "*";
61 private static final String NULL_ORIGIN = "null";
62 private CorsConfig config;
63
64 private HttpRequest request;
65 private final List<CorsConfig> configList;
66 private final boolean isShortCircuit;
67 private final boolean varyOnOrigin;
68 private boolean consumeContent;
69
70
71
72
73 public CorsHandler(final CorsConfig config) {
74 this(Collections.singletonList(checkNotNull(config, "config")), config.isShortCircuit());
75 }
76
77
78
79
80
81
82
83
84 public CorsHandler(final List<CorsConfig> configList, boolean isShortCircuit) {
85 checkNonEmpty(configList, "configList");
86 this.configList = configList;
87 this.isShortCircuit = isShortCircuit;
88 this.varyOnOrigin = isCorsSupportEnabled();
89 }
90
91 private boolean isCorsSupportEnabled() {
92 for (CorsConfig config : configList) {
93 if (config.isCorsSupportEnabled()) {
94 return true;
95 }
96 }
97 return false;
98 }
99
100 @Override
101 public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
102 if (msg instanceof HttpRequest) {
103 request = (HttpRequest) msg;
104 final String origin = request.headers().get(HttpHeaderNames.ORIGIN);
105 config = getForOrigin(origin);
106 if (isPreflightRequest(request)) {
107 handlePreflight(ctx, request);
108
109
110 consumeContent = true;
111 return;
112 }
113 if (isShortCircuit && !(origin == null || config != null)) {
114 forbidden(ctx, request);
115 consumeContent = true;
116 return;
117 }
118
119
120 consumeContent = false;
121 ctx.fireChannelRead(msg);
122 return;
123 }
124
125 if (consumeContent && (msg instanceof HttpContent)) {
126 ReferenceCountUtil.release(msg);
127 return;
128 }
129
130 ctx.fireChannelRead(msg);
131 }
132
133 private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
134 final HttpResponse response = new DefaultFullHttpResponse(
135 request.protocolVersion(),
136 OK,
137 Unpooled.buffer(0),
138 DefaultHttpHeadersFactory.headersFactory().withCombiningHeaders(true),
139 DefaultHttpHeadersFactory.trailersFactory().withCombiningHeaders(true));
140 if (setOrigin(response)) {
141 setAllowMethods(response);
142 setAllowHeaders(response);
143 setAllowCredentials(response);
144 setMaxAge(response);
145 setPreflightHeaders(response);
146 setAllowPrivateNetwork(response);
147 }
148 if (!response.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) {
149 response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
150 }
151 release(request);
152 respond(ctx, request, response);
153 }
154
155
156
157
158
159
160
161 private void setPreflightHeaders(final HttpResponse response) {
162 response.headers().add(config.preflightResponseHeaders());
163 }
164
165 private CorsConfig getForOrigin(String requestOrigin) {
166 for (CorsConfig corsConfig : configList) {
167 if (corsConfig.isAnyOriginSupported()) {
168 return corsConfig;
169 }
170 if (corsConfig.origins().contains(requestOrigin)) {
171 return corsConfig;
172 }
173 if (corsConfig.isNullOriginAllowed() && NULL_ORIGIN.equals(requestOrigin)) {
174 return corsConfig;
175 }
176 }
177 return null;
178 }
179
180 private boolean setOrigin(final HttpResponse response) {
181 final String origin = request.headers().get(HttpHeaderNames.ORIGIN);
182 if (origin != null && config != null) {
183 if (NULL_ORIGIN.equals(origin) && config.isNullOriginAllowed()) {
184 setNullOrigin(response);
185 setVaryHeader(response);
186 return true;
187 }
188 if (config.isAnyOriginSupported()) {
189 if (config.isCredentialsAllowed()) {
190 echoRequestOrigin(response);
191 setVaryHeader(response);
192 } else {
193 setAnyOrigin(response);
194 }
195 return true;
196 }
197 if (config.origins().contains(origin)) {
198 setOrigin(response, origin);
199 setVaryHeader(response);
200 return true;
201 }
202 logger.debug("Request origin [{}]] was not among the configured origins [{}]", origin, config.origins());
203 }
204 return false;
205 }
206
207 private void echoRequestOrigin(final HttpResponse response) {
208 setOrigin(response, request.headers().get(HttpHeaderNames.ORIGIN));
209 }
210
211 private static void setVaryHeader(final HttpResponse response) {
212 if (!response.headers().containsValue(HttpHeaderNames.VARY, HttpHeaderNames.ORIGIN, true)) {
213 response.headers().add(HttpHeaderNames.VARY, HttpHeaderNames.ORIGIN);
214 }
215 }
216
217 private static void setAnyOrigin(final HttpResponse response) {
218 setOrigin(response, ANY_ORIGIN);
219 }
220
221 private static void setNullOrigin(final HttpResponse response) {
222 setOrigin(response, NULL_ORIGIN);
223 }
224
225 private static void setOrigin(final HttpResponse response, final String origin) {
226 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
227 }
228
229 private void setAllowCredentials(final HttpResponse response) {
230 if (config.isCredentialsAllowed()
231 && !response.headers().get(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN).equals(ANY_ORIGIN)) {
232 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
233 }
234 }
235
236 private static boolean isPreflightRequest(final HttpRequest request) {
237 final HttpHeaders headers = request.headers();
238 return OPTIONS.equals(request.method()) &&
239 headers.contains(HttpHeaderNames.ORIGIN) &&
240 headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
241 }
242
243 private void setExposeHeaders(final HttpResponse response) {
244 if (!config.exposedHeaders().isEmpty()) {
245 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, config.exposedHeaders());
246 }
247 }
248
249 private void setAllowMethods(final HttpResponse response) {
250 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, config.allowedRequestMethods());
251 }
252
253 private void setAllowHeaders(final HttpResponse response) {
254 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, config.allowedRequestHeaders());
255 }
256
257 private void setMaxAge(final HttpResponse response) {
258 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, config.maxAge());
259 }
260
261 private void setAllowPrivateNetwork(final HttpResponse response) {
262 if (request.headers().contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK)) {
263 if (config.isPrivateNetworkAllowed()) {
264 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, "true");
265 } else {
266 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, "false");
267 }
268 }
269 }
270
271 @Override
272 public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise)
273 throws Exception {
274 if (msg instanceof HttpResponse) {
275 final HttpResponse response = (HttpResponse) msg;
276 if (config != null && config.isCorsSupportEnabled() && setOrigin(response)) {
277 setAllowCredentials(response);
278 setExposeHeaders(response);
279 }
280 if (varyOnOrigin) {
281 setVaryHeader(response);
282 }
283 }
284 ctx.write(msg, promise);
285 }
286
287 private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) {
288 HttpResponse response = new DefaultFullHttpResponse(
289 request.protocolVersion(), FORBIDDEN, ctx.alloc().buffer(0));
290 response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
291 release(request);
292 respond(ctx, request, response);
293 }
294
295 private static void respond(
296 final ChannelHandlerContext ctx,
297 final HttpRequest request,
298 final HttpResponse response) {
299
300 final boolean keepAlive = HttpUtil.isKeepAlive(request);
301
302 HttpUtil.setKeepAlive(response, keepAlive);
303
304 final ChannelFuture future = ctx.writeAndFlush(response);
305 if (!keepAlive) {
306 future.addListener(ChannelFutureListener.CLOSE);
307 }
308 }
309 }