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    * http://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.channel.ChannelFutureListener;
19  import io.netty.channel.ChannelHandlerAdapter;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelPromise;
22  import io.netty.handler.codec.http.DefaultFullHttpResponse;
23  import io.netty.handler.codec.http.HttpHeaderNames;
24  import io.netty.handler.codec.http.HttpHeaders;
25  import io.netty.handler.codec.http.HttpRequest;
26  import io.netty.handler.codec.http.HttpResponse;
27  import io.netty.util.internal.logging.InternalLogger;
28  import io.netty.util.internal.logging.InternalLoggerFactory;
29  
30  import static io.netty.handler.codec.http.HttpMethod.*;
31  import static io.netty.handler.codec.http.HttpResponseStatus.*;
32  import static io.netty.util.ReferenceCountUtil.*;
33  
34  /**
35   * Handles <a href="http://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
36   * <p>
37   * This handler can be configured using a {@link CorsConfig}, please
38   * refer to this class for details about the configuration options available.
39   */
40  public class CorsHandler extends ChannelHandlerAdapter {
41  
42      private static final InternalLogger logger = InternalLoggerFactory.getInstance(CorsHandler.class);
43      private static final String ANY_ORIGIN = "*";
44      private final CorsConfig config;
45  
46      private HttpRequest request;
47  
48      public CorsHandler(final CorsConfig config) {
49          this.config = config;
50      }
51  
52      @Override
53      public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
54          if (config.isCorsSupportEnabled() && msg instanceof HttpRequest) {
55              request = (HttpRequest) msg;
56              if (isPreflightRequest(request)) {
57                  handlePreflight(ctx, request);
58                  return;
59              }
60              if (config.isShortCurcuit() && !validateOrigin()) {
61                  forbidden(ctx, request);
62                  return;
63              }
64          }
65          ctx.fireChannelRead(msg);
66      }
67  
68      private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
69          final HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), OK, true, true);
70          if (setOrigin(response)) {
71              setAllowMethods(response);
72              setAllowHeaders(response);
73              setAllowCredentials(response);
74              setMaxAge(response);
75              setPreflightHeaders(response);
76          }
77          release(request);
78          ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
79      }
80  
81      /**
82       * This is a non CORS specification feature which enables the setting of preflight
83       * response headers that might be required by intermediaries.
84       *
85       * @param response the HttpResponse to which the preflight response headers should be added.
86       */
87      private void setPreflightHeaders(final HttpResponse response) {
88          response.headers().add(config.preflightResponseHeaders());
89      }
90  
91      private boolean setOrigin(final HttpResponse response) {
92          final CharSequence origin = request.headers().get(HttpHeaderNames.ORIGIN);
93          if (origin != null) {
94              if ("null".equals(origin) && config.isNullOriginAllowed()) {
95                  setAnyOrigin(response);
96                  return true;
97              }
98              if (config.isAnyOriginSupported()) {
99                  if (config.isCredentialsAllowed()) {
100                     echoRequestOrigin(response);
101                     setVaryHeader(response);
102                 } else {
103                     setAnyOrigin(response);
104                 }
105                 return true;
106             }
107             if (config.origins().contains(origin)) {
108                 setOrigin(response, origin);
109                 setVaryHeader(response);
110                 return true;
111             }
112             logger.debug("Request origin [" + origin + "] was not among the configured origins " + config.origins());
113         }
114         return false;
115     }
116 
117     private boolean validateOrigin() {
118         if (config.isAnyOriginSupported()) {
119             return true;
120         }
121 
122         final CharSequence origin = request.headers().get(HttpHeaderNames.ORIGIN);
123         if (origin == null) {
124             // Not a CORS request so we cannot validate it. It may be a non CORS request.
125             return true;
126         }
127 
128         if ("null".equals(origin) && config.isNullOriginAllowed()) {
129             return true;
130         }
131 
132         return config.origins().contains(origin);
133     }
134 
135     private void echoRequestOrigin(final HttpResponse response) {
136         setOrigin(response, request.headers().get(HttpHeaderNames.ORIGIN));
137     }
138 
139     private static void setVaryHeader(final HttpResponse response) {
140         response.headers().set(HttpHeaderNames.VARY, HttpHeaderNames.ORIGIN);
141     }
142 
143     private static void setAnyOrigin(final HttpResponse response) {
144         setOrigin(response, ANY_ORIGIN);
145     }
146 
147     private static void setOrigin(final HttpResponse response, final CharSequence origin) {
148         response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
149     }
150 
151     private void setAllowCredentials(final HttpResponse response) {
152         if (config.isCredentialsAllowed()
153                 && !response.headers().get(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN).equals(ANY_ORIGIN)) {
154             response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
155         }
156     }
157 
158     private static boolean isPreflightRequest(final HttpRequest request) {
159         final HttpHeaders headers = request.headers();
160         return request.method().equals(OPTIONS) &&
161                 headers.contains(HttpHeaderNames.ORIGIN) &&
162                 headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
163     }
164 
165     private void setExposeHeaders(final HttpResponse response) {
166         if (!config.exposedHeaders().isEmpty()) {
167             response.headers().set(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, config.exposedHeaders());
168         }
169     }
170 
171     private void setAllowMethods(final HttpResponse response) {
172         response.headers().setObject(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, config.allowedRequestMethods());
173     }
174 
175     private void setAllowHeaders(final HttpResponse response) {
176         response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, config.allowedRequestHeaders());
177     }
178 
179     private void setMaxAge(final HttpResponse response) {
180         response.headers().setLong(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, config.maxAge());
181     }
182 
183     @Override
184     public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise)
185             throws Exception {
186         if (config.isCorsSupportEnabled() && msg instanceof HttpResponse) {
187             final HttpResponse response = (HttpResponse) msg;
188             if (setOrigin(response)) {
189                 setAllowCredentials(response);
190                 setAllowHeaders(response);
191                 setExposeHeaders(response);
192             }
193         }
194         ctx.writeAndFlush(msg, promise);
195     }
196 
197     @Override
198     public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
199         logger.error("Caught error in CorsHandler", cause);
200         ctx.fireExceptionCaught(cause);
201     }
202 
203     private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) {
204         ctx.writeAndFlush(new DefaultFullHttpResponse(request.protocolVersion(), FORBIDDEN))
205                 .addListener(ChannelFutureListener.CLOSE);
206         release(request);
207     }
208 }
209