View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version
5    * 2.0 (the "License"); you may not use this file except in compliance with the
6    * License. You may obtain a copy of the License at:
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
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   * Handles <a href="https://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
49   * <p>
50   * This handler can be configured using one or more {@link CorsConfig}, please
51   * refer to this class for details about the configuration options available.
52   * <p>
53   * If CORS support is enabled for at least one {@link CorsConfig}, every response written
54   * by this handler will carry {@code Vary: Origin}, including responses to requests without
55   * {@code Origin} header or with an origin that is not allowed.
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       * Creates a new instance with a single {@link CorsConfig}.
72       */
73      public CorsHandler(final CorsConfig config) {
74          this(Collections.singletonList(checkNotNull(config, "config")), config.isShortCircuit());
75      }
76  
77      /**
78       * Creates a new instance with the specified config list. If more than one
79       * config matches a certain origin, the first in the List will be used.
80       *
81       * @param configList     List of {@link CorsConfig}
82       * @param isShortCircuit Same as {@link CorsConfig#isShortCircuit} but applicable to all supplied configs.
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                 // Enable consumeContent so that all following HttpContent
109                 // for this request will be released and not propagated downstream.
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             // This request is forwarded, stop discarding
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      * This is a non CORS specification feature which enables the setting of preflight
157      * response headers that might be required by intermediaries.
158      *
159      * @param response the HttpResponse to which the preflight response headers should be added.
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 }