From a786850a96a5e2ab3fa7e5d39fd5b786af456df3 Mon Sep 17 00:00:00 2001 From: Arunan Sugunakumar Date: Tue, 7 Oct 2025 12:01:59 +0530 Subject: [PATCH] Introduce CORS support for REST API The cors can be configured using the following properties in synapse.properties. ``` synapse.rest.CORSConfig.enabled=true synapse.rest.CORSConfig.Access-Control-Allow-Origin=http://localhost:3000,https://example.com synapse.rest.CORSConfig.Access-Control-Allow-Headers=Content-Type,Authorization,X-Requested-With ``` --- .../config/SynapsePropertiesLoader.java | 21 +++ .../mediators/builtin/RespondMediator.java | 11 ++ .../apache/synapse/rest/RESTConstants.java | 29 ++++ .../org/apache/synapse/rest/Resource.java | 13 ++ .../synapse/rest/cors/CORSConfiguration.java | 50 +++++++ .../apache/synapse/rest/cors/CORSHelper.java | 141 ++++++++++++++++++ .../rest/cors/SynapseCORSConfiguration.java | 116 ++++++++++++++ repository/conf/synapse.properties | 11 ++ 8 files changed, 392 insertions(+) create mode 100644 modules/core/src/main/java/org/apache/synapse/rest/cors/CORSConfiguration.java create mode 100644 modules/core/src/main/java/org/apache/synapse/rest/cors/CORSHelper.java create mode 100644 modules/core/src/main/java/org/apache/synapse/rest/cors/SynapseCORSConfiguration.java diff --git a/modules/core/src/main/java/org/apache/synapse/config/SynapsePropertiesLoader.java b/modules/core/src/main/java/org/apache/synapse/config/SynapsePropertiesLoader.java index 0e27b2a46..f0735891e 100644 --- a/modules/core/src/main/java/org/apache/synapse/config/SynapsePropertiesLoader.java +++ b/modules/core/src/main/java/org/apache/synapse/config/SynapsePropertiesLoader.java @@ -95,4 +95,25 @@ public static Properties reloadSynapseProperties() { public static String getPropertyValue(String key, String defaultValue) { return MiscellaneousUtil.getProperty(loadSynapseProperties(), key, defaultValue); } + + /** + * Get the boolean value of the property from the synapse properties. + * + * @param name name of the config property + * @param def default value to return if the property is not set + * @return the value of the property to be used + */ + public static Boolean getBooleanProperty(String name, Boolean def) { + String val = MiscellaneousUtil.getProperty(loadSynapseProperties(), name, String.valueOf(def)); + if (val == null) { + if (log.isDebugEnabled()) { + log.debug("Parameter : " + name + " is not defined in the synapse.properties file."); + } + return def; + } + if (log.isDebugEnabled()) { + log.debug("synapse.properties parameter : " + name + " = " + val); + } + return Boolean.valueOf(val); + } } diff --git a/modules/core/src/main/java/org/apache/synapse/mediators/builtin/RespondMediator.java b/modules/core/src/main/java/org/apache/synapse/mediators/builtin/RespondMediator.java index 2cc5e7e64..282d77b94 100644 --- a/modules/core/src/main/java/org/apache/synapse/mediators/builtin/RespondMediator.java +++ b/modules/core/src/main/java/org/apache/synapse/mediators/builtin/RespondMediator.java @@ -20,9 +20,12 @@ package org.apache.synapse.mediators.builtin; import org.apache.synapse.MessageContext; +import org.apache.synapse.SynapseConstants; import org.apache.synapse.SynapseLog; import org.apache.synapse.core.axis2.Axis2Sender; import org.apache.synapse.mediators.AbstractMediator; +import org.apache.synapse.rest.cors.CORSHelper; +import org.apache.synapse.rest.cors.SynapseCORSConfiguration; /** * Halts further processing/mediation of the current message and return the current message back to client @@ -51,6 +54,14 @@ public boolean mediate(MessageContext synCtx) { synCtx.setTo(null); synCtx.setResponse(true); + + // if this is not a response from a proxy service + String proxyName = (String) synCtx.getProperty(SynapseConstants.PROXY_SERVICE); + if (proxyName == null || proxyName.isEmpty()) { + // Add CORS headers for API response + CORSHelper.handleCORSHeadersForResponse(SynapseCORSConfiguration.getInstance(), synCtx); + } + Axis2Sender.sendBack(synCtx); if (isTraceOrDebugEnabled) { diff --git a/modules/core/src/main/java/org/apache/synapse/rest/RESTConstants.java b/modules/core/src/main/java/org/apache/synapse/rest/RESTConstants.java index ab20062cd..1ab252cb3 100644 --- a/modules/core/src/main/java/org/apache/synapse/rest/RESTConstants.java +++ b/modules/core/src/main/java/org/apache/synapse/rest/RESTConstants.java @@ -53,4 +53,33 @@ public static enum METHODS { /** The Synapse MC property that marks if the message was denied on the accessed transport */ public static final String REST_API_TRANSPORT_DENIED = "REST_API_TRANSPORT_DENIED"; + public static final String CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + public static final String CORS_HEADER_ACCESS_CTL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + public static final String CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + public static final String CORS_HEADER_ORIGIN = "Origin"; + /** + * CORS related configuration in synapse.properties + */ + // enable/disable CORS support + public static final String CORS_CONFIGURATION_ENABLED = "synapse.rest.CORSConfig.enabled"; + // List of allowed origins (comma separated) + public static final String CORS_CONFIGURATION_ACCESS_CTL_ALLOW_ORIGIN = + "synapse.rest.CORSConfig.Access-Control-Allow-Origin"; + // List of allowed headers (comma separated) + public static final String CORS_CONFIGURATION_ACCESS_CTL_ALLOW_HEADERS = + "synapse.rest.CORSConfig.Access-Control-Allow-Headers"; + + /** + * Constant prefix for rest related internal properties + */ + public static final String _SYNAPSE_INTERNAL_ = "_SYNAPSE_INTERNAL_REST_"; + + public static final String INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN = + _SYNAPSE_INTERNAL_ + "Access-Control-Allow-Origin"; + public static final String INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_METHODS = + _SYNAPSE_INTERNAL_+ "Access-Control-Allow-Methods"; + public static final String INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS = + _SYNAPSE_INTERNAL_+ "Access-Control-Allow-Headers"; + public static final String INTERNAL_CORS_HEADER_ORIGIN = _SYNAPSE_INTERNAL_+ "Origin"; + } diff --git a/modules/core/src/main/java/org/apache/synapse/rest/Resource.java b/modules/core/src/main/java/org/apache/synapse/rest/Resource.java index 3f6c4aec5..126a246c2 100644 --- a/modules/core/src/main/java/org/apache/synapse/rest/Resource.java +++ b/modules/core/src/main/java/org/apache/synapse/rest/Resource.java @@ -32,6 +32,8 @@ import org.apache.synapse.core.axis2.Axis2Sender; import org.apache.synapse.mediators.MediatorFaultHandler; import org.apache.synapse.mediators.base.SequenceMediator; +import org.apache.synapse.rest.cors.CORSHelper; +import org.apache.synapse.rest.cors.SynapseCORSConfiguration; import org.apache.synapse.rest.dispatch.DispatcherHelper; import org.apache.synapse.transport.nhttp.NhttpConstants; @@ -268,6 +270,10 @@ void process(MessageContext synCtx) { String method = (String) synCtx.getProperty(RESTConstants.REST_METHOD); if (RESTConstants.METHOD_OPTIONS.equals(method) && sendOptions(synCtx)) { return; + } else { + // Handle CORS for other HTTP Methods + CORSHelper.handleCORSHeaders(SynapseCORSConfiguration.getInstance(), + synCtx, getSupportedMethods(), false); } synCtx.setProperty(RESTConstants.SYNAPSE_RESOURCE, name); @@ -291,6 +297,9 @@ void process(MessageContext synCtx) { } } } + } else { + // Add CORS headers for response message + CORSHelper.handleCORSHeadersForResponse(SynapseCORSConfiguration.getInstance(), synCtx); } SequenceMediator sequence = synCtx.isResponse() ? outSequence : inSequence; @@ -357,6 +366,8 @@ private boolean sendOptions(MessageContext synCtx) { synCtx.setResponse(true); synCtx.setTo(null); transportHeaders.put(HttpHeaders.ALLOW, getSupportedMethods()); + CORSHelper.handleCORSHeaders(SynapseCORSConfiguration.getInstance(), + synCtx, getSupportedMethods(),true); Axis2Sender.sendBack(synCtx); return true; } else { @@ -370,6 +381,8 @@ private boolean sendOptions(MessageContext synCtx) { synCtx.setResponse(true); synCtx.setTo(null); transportHeaders.put(HttpHeaders.ALLOW, getSupportedMethods()); + CORSHelper.handleCORSHeaders(SynapseCORSConfiguration.getInstance(), + synCtx, getSupportedMethods(), true); Axis2Sender.sendBack(synCtx); return true; } diff --git a/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSConfiguration.java b/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSConfiguration.java new file mode 100644 index 000000000..630a2361c --- /dev/null +++ b/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSConfiguration.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.synapse.rest.cors; + +import java.util.Set; + +/** + * {@code CORSConfiguration} is the interface that need to be implemented in order hold the CORS configuration information + */ +public interface CORSConfiguration { + + /** + * Returns allowed origins in the configuration. + * + * @return allowed origins + */ + Set getAllowedOrigins(); + + /** + * Returns allowed headers in the configuration. + * + * @return allowed headers + */ + String getAllowedHeaders(); + + /** + * Returns if CORS is enabled. + * + * @return boolean enabled + */ + boolean isEnabled(); + +} diff --git a/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSHelper.java b/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSHelper.java new file mode 100644 index 000000000..c590daf5d --- /dev/null +++ b/modules/core/src/main/java/org/apache/synapse/rest/cors/CORSHelper.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.synapse.rest.cors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpStatus; +import org.apache.synapse.MessageContext; +import org.apache.synapse.core.axis2.Axis2MessageContext; +import org.apache.synapse.rest.RESTConstants; +import org.apache.synapse.transport.passthru.PassThroughConstants; + +import java.util.Map; +import java.util.Set; + +/** + * This class provides util functions for all CORS related activities. + */ +public class CORSHelper { + + private static final Log log = LogFactory.getLog(CORSHelper.class); + + /** + * Function to retrieve allowed origin header string + * + * @param origin Received origin + * @param allowedOrigins allowed origin set + * @return + */ + public static String getAllowedOrigins(String origin, Set allowedOrigins) { + + if (allowedOrigins.contains("*")) { + return "*"; + } else if (allowedOrigins.contains(origin)) { + return origin; + } else { + return ""; + } + } + + /** + * Functions to handle CORS Headers + * + * @param synCtx Synapse message context + * @param corsConfiguration of the API + * @param supportedMethods + * @param updateHeaders Boolean + */ + public static void handleCORSHeaders(CORSConfiguration corsConfiguration, MessageContext synCtx, + String supportedMethods, boolean updateHeaders) { + + if (corsConfiguration.isEnabled()) { + org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); + Map transportHeaders = (Map) msgCtx.getProperty( + org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS); + if (transportHeaders != null) { + String allowedOrigin = getAllowedOrigins(transportHeaders.get(RESTConstants.CORS_HEADER_ORIGIN), + corsConfiguration.getAllowedOrigins()); + if (updateHeaders) { + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_METHODS, supportedMethods); + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN, allowedOrigin); + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS, + corsConfiguration.getAllowedHeaders()); + } + + synCtx.setProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_METHODS, supportedMethods); + synCtx.setProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN, allowedOrigin); + synCtx.setProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS, + corsConfiguration.getAllowedHeaders()); + synCtx.setProperty(RESTConstants.INTERNAL_CORS_HEADER_ORIGIN, + transportHeaders.get(RESTConstants.CORS_HEADER_ORIGIN)); + + // If the request origin is not allowed, set the status code to 403 + if (isOptionsRequest(synCtx) && allowedOrigin.isEmpty()) { + ((Axis2MessageContext) synCtx).getAxis2MessageContext() + .setProperty(PassThroughConstants.HTTP_SC, HttpStatus.SC_FORBIDDEN); + } + } + } + + } + + /** + * Function to set CORS headers to response message transport headers extracting from synapse message context + * + * @param synCtx + * @param corsConfiguration of the API + */ + public static void handleCORSHeadersForResponse(CORSConfiguration corsConfiguration, MessageContext synCtx) { + + if (corsConfiguration.isEnabled()) { + org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); + Map transportHeaders = (Map) msgCtx.getProperty( + org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS); + if (transportHeaders != null) { + if (synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_METHODS) != null) { + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_METHODS, + (String) synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_METHODS)); + } + + if (synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN) != null) { + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN, + (String) synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_ORIGIN)); + } + + if (synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS) != null) { + transportHeaders.put(RESTConstants.CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS, + (String) synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ACCESS_CTL_ALLOW_HEADERS)); + } + + if (synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ORIGIN) != null) { + transportHeaders.put(RESTConstants.CORS_HEADER_ORIGIN, + (String) synCtx.getProperty(RESTConstants.INTERNAL_CORS_HEADER_ORIGIN)); + } + } + } + } + + private static boolean isOptionsRequest(MessageContext synCtx) { + + String method = (String) synCtx.getProperty(RESTConstants.REST_METHOD); + return RESTConstants.METHOD_OPTIONS.equals(method); + } +} diff --git a/modules/core/src/main/java/org/apache/synapse/rest/cors/SynapseCORSConfiguration.java b/modules/core/src/main/java/org/apache/synapse/rest/cors/SynapseCORSConfiguration.java new file mode 100644 index 000000000..c4bd3dfac --- /dev/null +++ b/modules/core/src/main/java/org/apache/synapse/rest/cors/SynapseCORSConfiguration.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.synapse.rest.cors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.synapse.SynapseException; +import org.apache.synapse.config.SynapsePropertiesLoader; +import org.apache.synapse.rest.RESTConstants; + +import java.net.InterfaceAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +/** + * This class holds CORS configurations made in synapse.properties file + */ +public class SynapseCORSConfiguration implements CORSConfiguration { + + private static Log LOG = LogFactory.getLog(SynapseCORSConfiguration.class); + private static SynapseCORSConfiguration corsConfigs = null; + + private boolean enabled; + private Set allowedOrigins = new HashSet<>(); + private String allowedHeaders; + private static final String LOCAL_HOST = "localhost"; + + private SynapseCORSConfiguration() { + enabled = SynapsePropertiesLoader.getBooleanProperty(RESTConstants.CORS_CONFIGURATION_ENABLED, false); + if (!enabled) { + return; // no need to do the rest if cors is not enabled. + } + //Retrieve allowed origin list + String allowedOriginListStr = SynapsePropertiesLoader.getPropertyValue( + RESTConstants.CORS_CONFIGURATION_ACCESS_CTL_ALLOW_ORIGIN, null); + if (allowedOriginListStr != null) { + String[] originList = allowedOriginListStr.split(","); + for (String origin : originList) { + String trimmedOrigin = origin.trim(); + allowedOrigins.add(trimmedOrigin); + if (!trimmedOrigin.contains(LOCAL_HOST)) { + continue; + } + try { + URL url = new URL(trimmedOrigin); + if (url.getHost().equals(LOCAL_HOST)) { + // Add localhost IPs as allowed origin + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface nInterface = networkInterfaces.nextElement(); + for (InterfaceAddress iAddr : nInterface.getInterfaceAddresses()) { + URL localUrl = new URL(url.getProtocol(), + iAddr.getAddress().getHostAddress(), url.getPort(), ""); + allowedOrigins.add(localUrl.toString()); + } + } + } + } catch (MalformedURLException e) { + throw new SynapseException("Provided origin URL " + trimmedOrigin + " is malformed", e); + } catch (SocketException e) { + throw new SynapseException("Error occurred while retrieving network interfaces", e); + } + } + } + + //Retrieve allowed headers + allowedHeaders = SynapsePropertiesLoader.getPropertyValue( + RESTConstants.CORS_CONFIGURATION_ACCESS_CTL_ALLOW_HEADERS, ""); + } + + public static SynapseCORSConfiguration getInstance() { + if (corsConfigs != null) { + return corsConfigs; + } + //init CORS configurations + corsConfigs = new SynapseCORSConfiguration(); + return corsConfigs; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public Set getAllowedOrigins() { + return allowedOrigins; + } + + @Override + public String getAllowedHeaders() { + return allowedHeaders; + } +} diff --git a/repository/conf/synapse.properties b/repository/conf/synapse.properties index e3d43d594..576adc15f 100644 --- a/repository/conf/synapse.properties +++ b/repository/conf/synapse.properties @@ -149,6 +149,17 @@ synapse.jmx.jndiPort=0 # synapse.wsdl.resolver=samples.userguide.UserDefinedWSDLResolver # synapse.schema.resolver=samples.userguide.UserDefinedXmlSchemaURIResolver +# +################################################################################ +# CORS Configuration +################################################################################ +# Enable or disable CORS support +#synapse.rest.CORSConfig.enabled=true +# Comma separated list of allowed origins. Use * to allow all origins +#synapse.rest.CORSConfig.Access-Control-Allow-Origin=http://localhost:3000,https://example.com +# Comma separated list of allowed headers +#synapse.rest.CORSConfig.Access-Control-Allow-Headers=Content-Type,Authorization,X-Requested-With + # ################################################################################ # Beanstalk Configuration - Used primarily by the EJB Mediator.