001/*
002 * (C) Copyright 2013-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Arnaud Kervern
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.platform.web.common.requestcontroller.service;
021
022import java.io.Serializable;
023import java.util.Dictionary;
024import java.util.Enumeration;
025import java.util.Hashtable;
026import java.util.regex.Pattern;
027
028import javax.servlet.Filter;
029import javax.servlet.FilterConfig;
030import javax.servlet.ServletContext;
031import javax.servlet.ServletException;
032
033import org.apache.commons.lang3.StringUtils;
034import org.nuxeo.common.xmap.annotation.XNode;
035import org.nuxeo.common.xmap.annotation.XObject;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.runtime.api.Framework;
038
039import com.thetransactioncompany.cors.CORSFilter;
040
041import static org.apache.commons.lang3.StringUtils.isEmpty;
042
043/**
044 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
045 * @since 5.7.2
046 */
047@XObject(value = "corsConfig")
048public class NuxeoCorsFilterDescriptor implements Serializable, Cloneable {
049
050    private static final String PROPERTIES_PREFIX = "cors.";
051
052    @XNode("@name")
053    protected String name;
054
055    @XNode("@enabled")
056    protected Boolean enabled = true;
057
058    @XNode("@allowGenericHttpRequests")
059    protected Boolean allowGenericHttpRequests = true;
060
061    @XNode("@allowOrigin")
062    protected String allowOrigin;
063
064    @XNode("@allowSubdomains")
065    protected boolean allowSubdomains = false;
066
067    @XNode("@supportedMethods")
068    protected String supportedMethods;
069
070    @XNode("@supportedHeaders")
071    protected String supportedHeaders;
072
073    @XNode("@exposedHeaders")
074    protected String exposedHeaders;
075
076    @XNode("@supportsCredentials")
077    protected Boolean supportsCredentials = true;
078
079    @XNode("@maxAge")
080    protected int maxAge = -1;
081
082    protected Pattern pattern;
083
084    @XNode("pattern")
085    public void setPattern(String patternString) {
086        patternString = Framework.expandVars(patternString);
087        if (!StringUtils.isBlank(patternString)) {
088            pattern = Pattern.compile(patternString);
089        }
090    }
091
092    // volatile for double-checked locking
093    protected volatile CORSFilter filter;
094
095    public CORSFilter getFilter() {
096        if (filter == null) {
097            synchronized (this) {
098                if (filter == null) {
099                    CORSFilter corsFilter = new CORSFilter();
100                    try {
101                        corsFilter.init(buildFilterConfig());
102                    } catch (ServletException e) {
103                        throw new NuxeoException(e);
104                    }
105                    filter = corsFilter;
106                }
107            }
108        }
109        return filter;
110    }
111
112    protected FilterConfig buildFilterConfig() {
113        Dictionary<String, String> parameters = buildDictionary();
114
115        return new FilterConfig() {
116            @Override
117            public String getFilterName() {
118                return "NuxeoCorsFilterDescriptor";
119            }
120
121            @Override
122            public ServletContext getServletContext() {
123                // Not used with @see CORSFilter
124                return null;
125            }
126
127            @Override
128            public String getInitParameter(String name) {
129                return parameters.get(name);
130            }
131
132            @Override
133            public Enumeration getInitParameterNames() {
134                return parameters.keys();
135            }
136        };
137    }
138
139    public NuxeoCorsFilterDescriptor clone() throws CloneNotSupportedException {
140        NuxeoCorsFilterDescriptor n = new NuxeoCorsFilterDescriptor();
141        n.name = name;
142        n.allowGenericHttpRequests = allowGenericHttpRequests;
143        n.allowOrigin = allowOrigin;
144        n.allowSubdomains = allowSubdomains;
145        n.supportedMethods = supportedMethods;
146        n.supportedHeaders = supportedHeaders;
147        n.exposedHeaders = exposedHeaders;
148        n.supportsCredentials = supportsCredentials;
149        n.maxAge = maxAge;
150        n.pattern = pattern;
151        return n;
152    }
153
154    public void merge(NuxeoCorsFilterDescriptor o) {
155        allowGenericHttpRequests = o.allowGenericHttpRequests;
156        supportsCredentials = o.supportsCredentials;
157        allowSubdomains = o.allowSubdomains;
158
159        if (!StringUtils.isEmpty(o.allowOrigin)) {
160            allowOrigin = o.allowOrigin;
161        }
162
163        if (!StringUtils.isEmpty(o.supportedMethods)) {
164            supportedMethods = o.supportedMethods;
165        }
166
167        if (!StringUtils.isEmpty(o.supportedHeaders)) {
168            supportedHeaders = o.supportedHeaders;
169        }
170
171        if (!StringUtils.isEmpty(o.exposedHeaders)) {
172            exposedHeaders = o.exposedHeaders;
173        }
174
175        if (maxAge == -1) {
176            maxAge = o.maxAge;
177        }
178
179        if (o.pattern != null) {
180            pattern = o.pattern;
181        }
182
183        filter = null; // recomputed on first access
184    }
185
186    protected Dictionary<String, String> buildDictionary() {
187        Dictionary<String, String> params = new Hashtable<>();
188        params.put(PROPERTIES_PREFIX + "allowGenericHttpRequests", Boolean.toString(allowGenericHttpRequests));
189
190        if (!isEmpty(allowOrigin)) {
191            params.put(PROPERTIES_PREFIX + "allowOrigin", allowOrigin);
192        }
193
194        params.put(PROPERTIES_PREFIX + "allowSubdomains", Boolean.toString(allowSubdomains));
195
196        if (!isEmpty(supportedMethods)) {
197            params.put(PROPERTIES_PREFIX + "supportedMethods", supportedMethods);
198        }
199
200        if (!isEmpty(supportedHeaders)) {
201            params.put(PROPERTIES_PREFIX + "supportedHeaders", supportedHeaders);
202        }
203
204        if (!isEmpty(exposedHeaders)) {
205            params.put(PROPERTIES_PREFIX + "exposedHeaders", exposedHeaders);
206        }
207
208        params.put(PROPERTIES_PREFIX + "supportsCredentials", Boolean.toString(supportsCredentials));
209        params.put(PROPERTIES_PREFIX + "maxAge", Integer.toString(maxAge));
210
211        return params;
212    }
213}