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.util.Dictionary;
023import java.util.Enumeration;
024import java.util.Hashtable;
025import java.util.regex.Pattern;
026
027import javax.servlet.FilterConfig;
028import javax.servlet.ServletContext;
029import javax.servlet.ServletException;
030
031import org.apache.commons.lang3.StringUtils;
032import org.nuxeo.common.xmap.annotation.XNode;
033import org.nuxeo.common.xmap.annotation.XObject;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.runtime.api.Framework;
036
037import com.thetransactioncompany.cors.CORSFilter;
038
039import static org.apache.commons.lang3.StringUtils.isEmpty;
040
041/**
042 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
043 * @since 5.7.2
044 */
045@XObject(value = "corsConfig")
046public class NuxeoCorsFilterDescriptor implements Cloneable {
047
048    private static final String PROPERTIES_PREFIX = "cors.";
049
050    @XNode("@name")
051    protected String name;
052
053    @XNode("@enabled")
054    protected Boolean enabled = true;
055
056    @XNode("@allowGenericHttpRequests")
057    protected Boolean allowGenericHttpRequests = true;
058
059    @XNode("@allowOrigin")
060    protected String allowOrigin;
061
062    @XNode("@allowSubdomains")
063    protected boolean allowSubdomains = false;
064
065    @XNode("@supportedMethods")
066    protected String supportedMethods;
067
068    @XNode("@supportedHeaders")
069    protected String supportedHeaders;
070
071    @XNode("@exposedHeaders")
072    protected String exposedHeaders;
073
074    @XNode("@supportsCredentials")
075    protected Boolean supportsCredentials = true;
076
077    @XNode("@maxAge")
078    protected int maxAge = -1;
079
080    protected Pattern pattern;
081
082    @XNode("pattern")
083    public void setPattern(String patternString) {
084        patternString = Framework.expandVars(patternString);
085        if (!StringUtils.isBlank(patternString)) {
086            pattern = Pattern.compile(patternString);
087        }
088    }
089
090    // volatile for double-checked locking
091    protected volatile CORSFilter filter;
092
093    public CORSFilter getFilter() {
094        if (filter == null) {
095            synchronized (this) {
096                if (filter == null) {
097                    CORSFilter corsFilter = new CORSFilter();
098                    try {
099                        corsFilter.init(buildFilterConfig());
100                    } catch (ServletException e) {
101                        throw new NuxeoException(e);
102                    }
103                    filter = corsFilter;
104                }
105            }
106        }
107        return filter;
108    }
109
110    protected FilterConfig buildFilterConfig() {
111        Dictionary<String, String> parameters = buildDictionary();
112
113        return new FilterConfig() {
114            @Override
115            public String getFilterName() {
116                return "NuxeoCorsFilterDescriptor";
117            }
118
119            @Override
120            public ServletContext getServletContext() {
121                // Not used with @see CORSFilter
122                return null;
123            }
124
125            @Override
126            public String getInitParameter(String name) {
127                return parameters.get(name);
128            }
129
130            @Override
131            public Enumeration<String> getInitParameterNames() {
132                return parameters.keys();
133            }
134        };
135    }
136
137    @Override
138    public NuxeoCorsFilterDescriptor clone() throws CloneNotSupportedException {
139        NuxeoCorsFilterDescriptor n = new NuxeoCorsFilterDescriptor();
140        n.name = name;
141        n.allowGenericHttpRequests = allowGenericHttpRequests;
142        n.allowOrigin = allowOrigin;
143        n.allowSubdomains = allowSubdomains;
144        n.supportedMethods = supportedMethods;
145        n.supportedHeaders = supportedHeaders;
146        n.exposedHeaders = exposedHeaders;
147        n.supportsCredentials = supportsCredentials;
148        n.maxAge = maxAge;
149        n.pattern = pattern;
150        return n;
151    }
152
153    public void merge(NuxeoCorsFilterDescriptor o) {
154        allowGenericHttpRequests = o.allowGenericHttpRequests;
155        supportsCredentials = o.supportsCredentials;
156        allowSubdomains = o.allowSubdomains;
157
158        if (!StringUtils.isEmpty(o.allowOrigin)) {
159            allowOrigin = o.allowOrigin;
160        }
161
162        if (!StringUtils.isEmpty(o.supportedMethods)) {
163            supportedMethods = o.supportedMethods;
164        }
165
166        if (!StringUtils.isEmpty(o.supportedHeaders)) {
167            supportedHeaders = o.supportedHeaders;
168        }
169
170        if (!StringUtils.isEmpty(o.exposedHeaders)) {
171            exposedHeaders = o.exposedHeaders;
172        }
173
174        if (maxAge == -1) {
175            maxAge = o.maxAge;
176        }
177
178        if (o.pattern != null) {
179            pattern = o.pattern;
180        }
181
182        filter = null; // recomputed on first access
183    }
184
185    protected Dictionary<String, String> buildDictionary() {
186        Dictionary<String, String> params = new Hashtable<>();
187        params.put(PROPERTIES_PREFIX + "allowGenericHttpRequests", Boolean.toString(allowGenericHttpRequests));
188
189        if (!isEmpty(allowOrigin)) {
190            params.put(PROPERTIES_PREFIX + "allowOrigin", allowOrigin);
191        }
192
193        params.put(PROPERTIES_PREFIX + "allowSubdomains", Boolean.toString(allowSubdomains));
194
195        if (!isEmpty(supportedMethods)) {
196            params.put(PROPERTIES_PREFIX + "supportedMethods", supportedMethods);
197        }
198
199        if (!isEmpty(supportedHeaders)) {
200            params.put(PROPERTIES_PREFIX + "supportedHeaders", supportedHeaders);
201        }
202
203        if (!isEmpty(exposedHeaders)) {
204            params.put(PROPERTIES_PREFIX + "exposedHeaders", exposedHeaders);
205        }
206
207        params.put(PROPERTIES_PREFIX + "supportsCredentials", Boolean.toString(supportsCredentials));
208        params.put(PROPERTIES_PREFIX + "maxAge", Integer.toString(maxAge));
209
210        return params;
211    }
212}