View Javadoc
1   /**
2    * This file Copyright (c) 2020 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cors;
35  
36  import info.magnolia.cms.filters.AbstractMgnlFilter;
37  import info.magnolia.cms.filters.MgnlFilter;
38  
39  import java.io.IOException;
40  import java.net.MalformedURLException;
41  import java.net.URI;
42  import java.net.URISyntaxException;
43  import java.net.URL;
44  import java.util.Optional;
45  
46  import javax.servlet.FilterChain;
47  import javax.servlet.ServletException;
48  import javax.servlet.http.HttpServletRequest;
49  import javax.servlet.http.HttpServletResponse;
50  
51  import org.apache.commons.lang3.StringUtils;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import com.machinezoo.noexception.Exceptions;
56  
57  /**
58   * Filter capable of handling CORS and CORS pre-flight requests.
59   * <p>
60   * In cases where CORS request arrives, but is invalid, error 403 is returned by the filter.
61   * <p>
62   * In case of request, that is not CORS, this filter just delegates through the filter chain.
63   */
64  public abstract class AbstractCorsFilter extends AbstractMgnlFilter {
65  
66      public static final String OPTIONS_METHOD = "OPTIONS";
67  
68      private static final Logger log = LoggerFactory.getLogger(AbstractCorsFilter.class);
69  
70      @Override
71      public void doFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws IOException, ServletException {
72          final String current = currentHost(request);
73          final RequestType requestType = RequestType.from(current, request);
74          try {
75              switch (requestType) {
76              case NOT_CORS:
77                  filterChain.doFilter(request, response);
78                  break;
79              case PRE_FLIGHT:
80                  final Optional<MgnlFilter> corsResponseFilter = getCorsResponseFilter();
81                  if (corsResponseFilter.isPresent()) {
82                      corsResponseFilter.get().doFilter(request, response, filterChain);
83                  } else {
84                      filterChain.doFilter(request, response);
85                  }
86                  break;
87              case CORS:
88                  getCorsResponseFilter().ifPresent(Exceptions.wrap().consumer(filter -> filter.doFilter(request, response, filterChain)));
89                  filterChain.doFilter(request, response);
90                  break;
91              default:
92                  handleInvalid(response);
93                  break;
94              }
95          } catch (CorsException e) {
96              log.warn("CORS failed due to: {}", e.getMessage());
97              handleInvalid(response);
98          }
99      }
100 
101     protected abstract Optional<MgnlFilter> getCorsResponseFilter();
102 
103     private String currentHost(final HttpServletRequest request) throws MalformedURLException {
104         final URL url = new URL(request.getRequestURL().toString());
105         final String protocol = url.getProtocol();
106         final String authority = url.getAuthority();
107         return String.format("%s://%s", protocol, authority);
108     }
109 
110     private void handleInvalid(final HttpServletResponse response) {
111         response.setContentType("text/plain");
112         response.setStatus(HttpServletResponse.SC_FORBIDDEN);
113         response.resetBuffer();
114     }
115 
116     /**
117      * Type of the request determined from the incoming {@link HttpServletRequest}.
118      */
119     public enum RequestType {
120         CORS, PRE_FLIGHT, NOT_CORS, INVALID_CORS;
121 
122         public static RequestType from(final String currentHost, final HttpServletRequest request) {
123             final String origin = request.getHeader(Headers.ORIGIN.getName());
124             if (origin == null) {
125                 return RequestType.NOT_CORS;
126             } else if (origin.isEmpty() || !isValidOrigin(origin)) {
127                 return RequestType.INVALID_CORS;
128             } else if (isSameOrigin(currentHost, origin)) {
129                 return RequestType.NOT_CORS;
130             } else {
131                 final String method = request.getMethod();
132                 if (method != null) {
133                     if (OPTIONS_METHOD.equals(method)) {
134                         final String accessControlRequestMethodHeader = request.getHeader(Headers.ACCESS_CONTROL_REQUEST_METHOD.getName());
135                         if (StringUtils.isNotBlank(accessControlRequestMethodHeader)) {
136                             return RequestType.PRE_FLIGHT;
137                         } else if (accessControlRequestMethodHeader != null && accessControlRequestMethodHeader.isEmpty()) {
138                             return RequestType.INVALID_CORS;
139                         } else {
140                             return RequestType.CORS;
141                         }
142                     } else {
143                         return RequestType.CORS;
144                     }
145                 }
146             }
147             return RequestType.INVALID_CORS;
148         }
149 
150         private static boolean isValidOrigin(final String origin) {
151             if (StringUtils.contains(origin, '%')) {
152                 return false;
153             }
154             final URI uri;
155             try {
156                 uri = new URI(origin);
157             } catch (URISyntaxException e) {
158                 return false;
159             }
160             return uri.getScheme() != null;
161         }
162 
163         private static boolean isSameOrigin(final String currentUri, final String origin) {
164             return currentUri.equals(origin);
165         }
166     }
167 
168     /**
169      * CORS related headers.
170      */
171     public enum Headers {
172         ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"),
173         ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"),
174         ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"),
175         ACCESS_CONTROL_ALLOW_CREDENTIALS("Access-Control-Allow-Credentials"),
176         ACCESS_CONTROL_MAX_AGE("Access-Control-Max-Age"),
177         ACCESS_CONTROL_ALLOW_METHODS("Access-Control-Allow-Methods"),
178         ACCESS_CONTROL_ALLOW_HEADERS("Access-Control-Allow-Headers"),
179         ORIGIN("Origin"),
180         VARY("Vary");
181 
182         private final String headerName;
183 
184         Headers(final String headerName) {
185             this.headerName = headerName;
186         }
187 
188         public String getName() {
189             return headerName;
190         }
191     }
192 }