/* * Copyright (c) 2015 Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ovirt.engine.core.utils.servlet; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.ejb.EJB; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.commons.lang.StringUtils; import org.ebaysf.web.cors.CORSFilter; import org.ovirt.engine.core.common.config.ConfigCommon; import org.ovirt.engine.core.common.interfaces.BackendLocal; import org.ovirt.engine.core.common.queries.ConfigurationValues; import org.ovirt.engine.core.common.queries.GetConfigurationValueParameters; import org.ovirt.engine.core.common.queries.GetDefaultAllowedOriginsQueryParameters; import org.ovirt.engine.core.common.queries.VdcQueryReturnValue; import org.ovirt.engine.core.common.queries.VdcQueryType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This filter implements CORS (Cross Site Resource Sharing). In does so delegating all the hard work to an existing * filter developed by eBay (see <a href="https://github.com/ebay/cors-filter">here</a>). The only purpose of this * class is to get the configuration from the backend and pass it to the eBay filter. */ @SuppressWarnings("unused") public class CORSSupportFilter implements Filter { /** * The log used by the filter. */ private static final Logger log = LoggerFactory.getLogger(CORSSupportFilter.class); private static final String DEFAULT_ORIGINS_SUFFIXES = "defaultOriginsSuffixes"; private static final Set<String> EMPTY_SET = new HashSet<>(); /** * We need access to the backend in order to get the values of the configuration parameters. */ @EJB(lookup = "java:global/engine/bll/Backend!org.ovirt.engine.core.common.interfaces.BackendLocal") private BackendLocal backend; /** * We must perform lazy initialization because the application server runs the {@code init} method in one of the * MSC (Modular Service Container) threads, and it may happen that it decides to run initialization of the RESTAPI * and the backend in the same thread, first the RESTAPI and then the backend. If this happens any attempt to lookup * the backend bean will cause a deadlock. So we use this flag to indicate if initialization has been performed * already, and to perform it lazily as needed. */ private volatile boolean initialized = false; /** * We need to save the filter configuration, for use during lazy initialization. */ private FilterConfig config; /** * This is the filter that perform all the actual work, we just delegate calls to it. */ private Filter delegate; /** * Time when previous call of createDelegate() happened (in ms). */ private long lastInitializationTime = 0L; /** * The createDelegate() is not called more then once per time period of delayBeforeReinit (in ms). */ private long delayBeforeReinit = 60 * 1000L; /** * True, if the CORSSupport is allowed in the configuration (by engine-config). * * If true, the filter will handle CORS request headers accordingly. * Otherwise, CORS is disabled. */ private Boolean enabled; /** * True, if the CORSAllowDefaultOrigins is allowed in the configuration (by engine-config). * * If true nad CORS is enabled, the filter will consider all configured hosts as allowed origins. * Otherwise, only values from CORSAllowedOrigins are taken. */ private Boolean enabledDefaultOrigins; /** * Suffixes to be appended to all default origins. Usually port(s) can be provided. * Effective only if the enabledDefaultOrigins is true. * * Can be configured via `engine-config -s CORSDefaultOriginSuffixes=[comma separated list]` * Example: * engine-config -s 'CORSDefaultOriginSuffixes=:9090,:1234' */ private Set<String> defaultOriginsSuffixes; /** * Keep previous value to detect change. For optimization only. */ private Set<String> oldAllowedOrigins; @Override public void init(final FilterConfig config) throws ServletException { this.config = config; this.enabled = (Boolean) getBackendParameter(ConfigurationValues.CORSSupport); this.enabledDefaultOrigins = (Boolean) getBackendParameter(ConfigurationValues.CORSAllowDefaultOrigins); String sufficesFromConf = StringUtils.defaultString( (String) getBackendParameter(ConfigurationValues.CORSDefaultOriginSuffixes), ""); this.defaultOriginsSuffixes = new HashSet<>(Arrays.asList(sufficesFromConf.split(","))); } @Override public void destroy() { if (delegate != null) { delegate.destroy(); } } private void createDelegate() throws ServletException { // Check if the CORS support is enabled in configuration: if (enabled == null || !enabled) { log.info("CORS support is disabled."); return; } // Get the allowed origins from the backend configuration: final String allowedOriginsConfig = (String) getBackendParameter(ConfigurationValues.CORSAllowedOrigins); final Set<String> allowedDefaultOrigins = getDefaultAllowedOrigins(); final String allowedOrigins = mergeOrigins(allowedOriginsConfig, allowedDefaultOrigins); if (StringUtils.isEmpty(allowedOrigins)) { log.warn( "The CORS support has been enabled, but the list of allowed origins is empty. This means that CORS " + "support will actually be disabled." ); return; } log.info("CORS support is enabled for origins \"{}\".", allowedOrigins); if (delegate == null || !allowedDefaultOrigins.equals(oldAllowedOrigins)) { // Create new CORSFilter() only if needed oldAllowedOrigins = allowedDefaultOrigins; // Populate the parameters for the delegate: final Map<String, String> parameters = new HashMap<>(); parameters.put(CORSFilter.PARAM_CORS_ALLOWED_METHODS, "GET,POST,PUT,DELETE"); parameters.put(CORSFilter.PARAM_CORS_ALLOWED_HEADERS, "Accept,Authorization,Content-Type"); parameters.put(CORSFilter.PARAM_CORS_ALLOWED_ORIGINS, allowedOrigins); // Add all the parameters of this filter to those passed to the delegate, so that the user can override the // configuration modifying the web.xml file: final Enumeration<String> names = config.getInitParameterNames(); while (names.hasMoreElements()) { final String name = names.nextElement(); final String value = config.getInitParameter(name); parameters.put(name, value); } // Create the delegate and initialize with the prepared parameters: delegate = new CORSFilter(); delegate.init( new FilterConfig() { @Override public String getFilterName() { return config.getFilterName(); } @Override public ServletContext getServletContext() { return config.getServletContext(); } @Override public String getInitParameter(String name) { return parameters.get(name); } @Override public Enumeration<String> getInitParameterNames() { return Collections.enumeration(parameters.keySet()); } } ); } } private String mergeOrigins(String fromConfig, Set<String> fromDefault) { if ("*".equals(fromConfig)) { return fromConfig; } if (StringUtils.isEmpty(fromConfig)) { return StringUtils.join(fromDefault, ','); } return fromConfig + "," + StringUtils.join(fromDefault, ','); } private Object getBackendParameter(final ConfigurationValues key) throws ServletException { final GetConfigurationValueParameters parameters = new GetConfigurationValueParameters(); parameters.setConfigValue(key); parameters.setVersion(ConfigCommon.defaultConfigurationVersion); VdcQueryReturnValue value = backend.runPublicQuery(VdcQueryType.GetConfigurationValue, parameters); if (!value.getSucceeded()) { throw new ServletException("Can't get value of backend parameter \"" + key + "\"."); } return value.getReturnValue(); } private Set<String> getDefaultAllowedOrigins() throws ServletException { if (this.enabledDefaultOrigins) { GetDefaultAllowedOriginsQueryParameters parameters = new GetDefaultAllowedOriginsQueryParameters(); parameters.addSuffixes(defaultOriginsSuffixes); VdcQueryReturnValue value = backend.runPublicQuery(VdcQueryType.GetDefaultAllowedOrigins, parameters); if (!value.getSucceeded()) { throw new ServletException("Can't get list of default origins"); } if (log.isDebugEnabled()) { log.debug("Origins allowed by default: {}", StringUtils.join((Set<String>) value.getReturnValue(), ',')); } return value.getReturnValue(); } return EMPTY_SET; } public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { // Result of GetDefaultAllowedOriginsQuery might vary in time, so // reinitialize if needed - new CORSFilter needs to be created with new params if (delegate != null) { // is CORSSupport enabled by engine-config ? if (initialized) { // for performance reasons, check for changes at most once per time period long now = System.currentTimeMillis(); if (lastInitializationTime + delayBeforeReinit < now) { synchronized (this) { initialized = false; // force reinitialization } lastInitializationTime = now; } } } // Perform lazy initialization, if needed: if (!initialized) { synchronized (this) { if (!initialized) { createDelegate(); initialized = true; } } } // Do the filtering if needed: if (delegate != null) { delegate.doFilter(request, response, chain); } else { chain.doFilter(request, response); } } }