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