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.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   * Handles <a href="https://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
43   * <p>
44   * This handler can be configured using one or more {@link CorsConfig}, please
45   * refer to this class for details about the configuration options available.
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       * Creates a new instance with a single {@link CorsConfig}.
60       */
61      public CorsHandler(final CorsConfig config) {
62          this(Collections.singletonList(requireNonNull(config, "config")), config.isShortCircuit());
63      }
64  
65      /**
66       * Creates a new instance with the specified config list. If more than one
67       * config matches a certain origin, the first in the List will be used.
68       *
69       * @param configList     List of {@link CorsConfig}
70       * @param isShortCircuit Same as {@link CorsConfig#isShortCircuit} but applicable to all supplied configs.
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      * This is a non CORS specification feature which enables the setting of preflight
118      * response headers that might be required by intermediaries.
119      *
120      * @param response the HttpResponse to which the preflight response headers should be added.
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 }