ContextUtils.java

/**
 * 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.cxf.ws.addressing;


import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.cxf.Bus;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.common.util.SystemPropertyAction;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.ConduitInitiator;
import org.apache.cxf.transport.ConduitInitiatorManager;
import org.apache.cxf.transport.Destination;
import org.apache.cxf.transport.MessageObserver;

import static org.apache.cxf.ws.addressing.JAXWSAConstants.ADDRESSING_PROPERTIES_INBOUND;
import static org.apache.cxf.ws.addressing.JAXWSAConstants.ADDRESSING_PROPERTIES_OUTBOUND;
import static org.apache.cxf.ws.addressing.JAXWSAConstants.CLIENT_ADDRESSING_PROPERTIES;

/**
 * Holder for utility methods relating to contexts.
 */
public final class ContextUtils {

    public static final ObjectFactory WSA_OBJECT_FACTORY = new ObjectFactory();
    public static final String ACTION = ContextUtils.class.getName() + ".ACTION";

    /**
     * System property whose value overrides the comma-separated list of URI scheme
     * prefixes permitted in wsa:ReplyTo / wsa:FaultTo decoupled-destination addresses.
     * Example: {@code -Dorg.apache.cxf.ws.addressing.decoupled.allowedSchemes=http://,https://,jms:}
     */
    public static final String ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY =
        "org.apache.cxf.ws.addressing.decoupled.allowedSchemes";

    /**
     * System property that enables WS-Addressing decoupled destinations (non-anonymous
     * wsa:ReplyTo / wsa:FaultTo). Defaults to {@code false} ��� decoupled destinations
     * are <em>disabled</em> by default to prevent SSRF. Set to {@code true} only when
     * your deployment legitimately requires decoupled WS-Addressing callbacks.
     */
    public static final String WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY =
        "org.apache.cxf.ws.addressing.decoupled.enabled";

    /**
     * Exchange property key that higher-level protocols (e.g. WS-RM) may set to
     * {@code Boolean.TRUE} to pre-approve decoupled wsa:ReplyTo / wsa:FaultTo
     * destinations for a specific exchange, bypassing the global
     * {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY} opt-in flag.
     *
     * <p>The URI-scheme allowlist ({@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} /
     * {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}) is still enforced even when
     * this flag is set, to prevent reaching dangerous URI types (e.g. {@code file://}).
     *
     * <p>Only trusted CXF modules should set this property.
     */
    public static final String DECOUPLED_DESTINATION_APPROVED_PROPERTY =
        "org.apache.cxf.ws.addressing.decoupled.approved";

    /**
     * Default set of URI scheme prefixes permitted in wsa:ReplyTo / wsa:FaultTo
     * decoupled-destination addresses. Schemes that can open arbitrary filesystem or
     * OS resources (file://, corba:, IOR:, etc.) are intentionally excluded.
     */
    public static final Set<String> DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES =
        Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
            "http://", "https://",
            "jms://", "jms:",
            "ws://", "wss://",
            "hc://", "hc5://",
            "local://"
        )));

    private static final EndpointReferenceType NONE_ENDPOINT_REFERENCE = new EndpointReferenceType();

    private static final Logger LOG = LogUtils.getL7dLogger(ContextUtils.class);

    /**
     * Used to fabricate a Uniform Resource Name from a UUID string
     */
    private static final String URN_UUID = "urn:uuid:";

    /**
     * Used by MAPAggregator to cache bad MAP fault name
     */
    private static final String MAP_FAULT_NAME_PROPERTY =
        "org.apache.cxf.ws.addressing.map.fault.name";

    /**
     * Used by MAPAggregator to cache bad MAP fault reason
     */
    private static final String MAP_FAULT_REASON_PROPERTY =
        "org.apache.cxf.ws.addressing.map.fault.reason";

    /**
     * Indicates a partial response has already been sent
     */
    private static final String PARTIAL_RESPONSE_SENT_PROPERTY =
        "org.apache.cxf.ws.addressing.partial.response.sent";

    static {
        NONE_ENDPOINT_REFERENCE.setAddress(new AttributedURIType());
        NONE_ENDPOINT_REFERENCE.getAddress().setValue(Names.WSA_NONE_ADDRESS);
    }

   /**
    * Prevents instantiation.
    */
    private ContextUtils() {
    }

   /**
    * Determine if message is outbound.
    *
    * @param message the current Message
    * @return true iff the message direction is outbound
    */
    public static boolean isOutbound(Message message) {
        return message != null
               && message.getExchange() != null
               && (message == message.getExchange().getOutMessage()
                   || message == message.getExchange().getOutFaultMessage());
    }

   /**
    * Determine if message is fault.
    *
    * @param message the current Message
    * @return true iff the message is a fault
    */
    public static boolean isFault(Message message) {
        return message != null
               && message.getExchange() != null
               && (message == message.getExchange().getInFaultMessage()
                   || message == message.getExchange().getOutFaultMessage());
    }

   /**
    * Determine if current messaging role is that of requestor.
    *
    * @param message the current Message
    * @return true if the current messaging role is that of requestor
    */
    public static boolean isRequestor(Message message) {
        Boolean requestor = (Boolean)message.get(Message.REQUESTOR_ROLE);
        return requestor != null && requestor.booleanValue();
    }

    /**
     * Get appropriate property name for message addressing properties.
     *
     * @param isRequestor true if the current messaging role is that of
     * requestor
     * @param isProviderContext true if the binding provider request context
     * available to the client application as opposed to the message context
     * visible to handlers
     * @param isOutbound true if the message is outbound
     * @return the property name to use when caching the MAPs in the context
     */
    public static String getMAPProperty(boolean isRequestor,
                                        boolean isProviderContext,
                                        boolean isOutbound) {
        if (isRequestor && isProviderContext) {
            return CLIENT_ADDRESSING_PROPERTIES;
        }
        return isOutbound
            ? ADDRESSING_PROPERTIES_OUTBOUND
            : ADDRESSING_PROPERTIES_INBOUND;
    }

    /**
     * Store MAPs in the message.
     *
     * @param message the current message
     * @param isOutbound true if the message is outbound
     */
    public static void storeMAPs(AddressingProperties maps,
                                 Message message,
                                 boolean isOutbound) {
        storeMAPs(maps, message, isOutbound, isRequestor(message), false);
    }

    /**
     * Store MAPs in the message.
     *
     * @param maps the MAPs to store
     * @param message the current message
     * @param isOutbound true if the message is outbound
     * @param isRequestor true if the current messaging role is that of requestor
     */
    public static void storeMAPs(AddressingProperties maps,
                                 Message message,
                                 boolean isOutbound,
                                 boolean isRequestor) {
        storeMAPs(maps, message, isOutbound, isRequestor, false);
    }

    /**
     * Store MAPs in the message.
     *
     * @param maps the MAPs to store
     * @param message the current message
     * @param isOutbound true if the message is outbound
     * @param isRequestor true if the current messaging role is that of requestor
     * @param isProviderContext true if the binding provider request context
     */
    public static void storeMAPs(AddressingProperties maps,
                                 Message message,
                                 boolean isOutbound,
                                 boolean isRequestor,
                                 boolean isProviderContext) {
        if (maps != null) {
            String mapProperty = getMAPProperty(isRequestor, isProviderContext, isOutbound);
            LOG.log(Level.FINE,
                    "associating MAPs with context property {0}",
                    mapProperty);
            message.put(mapProperty, maps);
        }
    }

    /**
     * @param message the current message
     * @param isProviderContext true if the binding provider request context
     * available to the client application as opposed to the message context
     * visible to handlers
     * @param isOutbound true if the message is outbound
     * @return the current addressing properties
     */
    public static AddressingProperties retrieveMAPs(
                                                   Message message,
                                                   boolean isProviderContext,
                                                   boolean isOutbound) {
        return retrieveMAPs(message, isProviderContext, isOutbound, true);
    }

    /**
     * @param message the current message
     * @param isProviderContext true if the binding provider request context
     * available to the client application as opposed to the message context
     * visible to handlers
     * @param isOutbound true if the message is outbound
     * @param warnIfMissing log a warning  message if properties cannot be retrieved
     * @return the current addressing properties
     */
    public static AddressingProperties retrieveMAPs(
                                                   Message message,
                                                   boolean isProviderContext,
                                                   boolean isOutbound,
                                                   boolean warnIfMissing) {
        boolean isRequestor = ContextUtils.isRequestor(message);
        String mapProperty =
            ContextUtils.getMAPProperty(isRequestor,
                                        isProviderContext,
                                        isOutbound);
        LOG.log(Level.FINE,
                "retrieving MAPs from context property {0}",
                mapProperty);

        AddressingProperties maps =
            (AddressingProperties)message.get(mapProperty);
        if (maps == null && isOutbound && !isRequestor
            && message.getExchange() != null && message.getExchange().getInMessage() != null) {
            maps = (AddressingProperties)message.getExchange().getInMessage().get(mapProperty);
        }

        if (maps != null) {
            LOG.log(Level.FINE, "current MAPs {0}", maps);
        } else if (!isProviderContext) {
            LogUtils.log(LOG, warnIfMissing ? Level.WARNING : Level.FINE,
                "MAPS_RETRIEVAL_FAILURE_MSG");
        }
        return maps;
    }

    /**
     * Helper method to get an attributed URI.
     *
     * @param uri the URI
     * @return an AttributedURIType encapsulating the URI
     */
    public static AttributedURIType getAttributedURI(String uri) {
        AttributedURIType attributedURI =
            WSA_OBJECT_FACTORY.createAttributedURIType();
        attributedURI.setValue(uri);
        return attributedURI;
    }

    /**
     * Helper method to get a RealtesTo instance.
     *
     * @param uri the related URI
     * @return a RelatesToType encapsulating the URI
     */
    public static RelatesToType getRelatesTo(String uri) {
        RelatesToType relatesTo =
            WSA_OBJECT_FACTORY.createRelatesToType();
        relatesTo.setValue(uri);
        return relatesTo;
    }

    private static boolean startsWith(String value, String ref) {
        if (StringUtils.isEmpty(value)) {
            return false;
        }
        return value.startsWith(ref);
    }

    /**
     * Helper method to determine if an EPR address is generic (either null,
     * none or anonymous).
     *
     * @param ref the EPR under test
     * @return true if the address is generic
     */
    public static boolean isGenericAddress(EndpointReferenceType ref) {
        return ref == null
               || ref.getAddress() == null
               || startsWith(ref.getAddress().getValue(), Names.WSA_ANONYMOUS_ADDRESS)
               || startsWith(ref.getAddress().getValue(), Names.WSA_NONE_ADDRESS);
    }

    /**
     * Helper method to determine if an EPR address is anon (either null,
     * anonymous).
     *
     * @param ref the EPR under test
     * @return true if the address is generic
     */
    public static boolean isAnonymousAddress(EndpointReferenceType ref) {
        return ref == null
               || ref.getAddress() == null
               || startsWith(ref.getAddress().getValue(), Names.WSA_ANONYMOUS_ADDRESS);
    }

    /**
     * Helper method to determine if an EPR address is none.
     *
     * @param ref the EPR under test
     * @return true if the address is generic
     */
    public static boolean isNoneAddress(EndpointReferenceType ref) {
        return ref != null
               && ref.getAddress() != null
               && Names.WSA_NONE_ADDRESS.equals(ref.getAddress().getValue());
    }

    /**
     * Helper method to determine if an MAPs Action is empty (a null action
     * is considered empty, whereas a zero length action suppresses
     * the propagation of the Action property).
     *
     * @param maps the MAPs Action under test
     * @return true if the Action is empty
     */
    public static boolean hasEmptyAction(AddressingProperties maps) {
        boolean empty = maps.getAction() == null;
        if (maps.getAction() != null
            && maps.getAction().getValue().isEmpty()) {
            maps.setAction(null);
            empty = false;
        }
        return empty;
    }


    /**
     * Propagate inbound MAPs onto full reponse & fault messages.
     *
     * @param inMAPs the inbound MAPs
     * @param exchange the current Exchange
     */
    public static void propogateReceivedMAPs(AddressingProperties inMAPs,
                                              Exchange exchange) {
        if (exchange.getOutMessage() == null) {
            exchange.setOutMessage(createMessage(exchange));
        }
        propogateReceivedMAPs(inMAPs, exchange.getOutMessage());
        if (exchange.getOutFaultMessage() == null) {
            exchange.setOutFaultMessage(createMessage(exchange));
        }
        propogateReceivedMAPs(inMAPs, exchange.getOutFaultMessage());
    }

    /**
     * Propogate inbound MAPs onto reponse message if applicable
     * (not applicable for oneways).
     *
     * @param inMAPs the inbound MAPs
     * @param responseMessage
     */
    public static void propogateReceivedMAPs(AddressingProperties inMAPs,
                                             Message responseMessage) {
        if (responseMessage != null) {
            storeMAPs(inMAPs, responseMessage, false, false, false);
        }
    }


    /**
     * Store bad MAP fault name in the message.
     *
     * @param faultName the fault name to store
     * @param message the current message
     */
    public static void storeMAPFaultName(String faultName,
                                         Message message) {
        message.put(MAP_FAULT_NAME_PROPERTY, faultName);
    }

    /**
     * Retrieve MAP fault name from the message.
     *
     * @param message the current message
     * @return the retrieved fault name
     */
    public static String retrieveMAPFaultName(Message message) {
        return (String)message.get(MAP_FAULT_NAME_PROPERTY);
    }

    /**
     * Store MAP fault reason in the message.
     *
     * @param reason the fault reason to store
     * @param message the current message
     */
    public static void storeMAPFaultReason(String reason,
                                           Message message) {
        message.put(MAP_FAULT_REASON_PROPERTY, reason);
    }

    /**
     * Retrieve MAP fault reason from the message.
     *
     * @param message the current message
     * @return the retrieved fault reason
     */
    public static String retrieveMAPFaultReason(Message message) {
        return (String)message.get(MAP_FAULT_REASON_PROPERTY);
    }

    /**
     * Store an indication that a partial response has been sent.
     * Relavant if *both* the replyTo & faultTo are decoupled,
     * and a fault occurs, then we would already have sent the
     * partial response (pre-dispatch) for the replyTo, so
     * no need to send again.
     *
     * @param message the current message
     */
    public static void storePartialResponseSent(Message message) {
        message.put(PARTIAL_RESPONSE_SENT_PROPERTY, Boolean.TRUE);
    }

    /**
     * Retrieve indication that a partial response has been sent.
     *
     * @param message the current message
     * @return the retrieved indication that a partial response
     * has been sent
     */
    public static boolean retrievePartialResponseSent(Message message) {
        Boolean ret = (Boolean)message.get(PARTIAL_RESPONSE_SENT_PROPERTY);
        return ret != null && ret.booleanValue();
    }

    /**
     * Store indication that a deferred uncorrelated message abort is
     * supported
     *
     * @param message the current message
     */
    public static void storeDeferUncorrelatedMessageAbort(Message message) {
        if (message.getExchange() != null) {
            message.getExchange().put("defer.uncorrelated.message.abort", Boolean.TRUE);
        }
    }

    /**
     * Retrieve indication that a deferred uncorrelated message abort is
     * supported
     *
     * @param message the current message
     * @return the retrieved indication
     */
    public static boolean retrieveDeferUncorrelatedMessageAbort(Message message) {
        Boolean ret = message.getExchange() != null
                      ? (Boolean)message.getExchange().get("defer.uncorrelated.message.abort")
                      : null;
        return ret != null && ret.booleanValue();
    }

    /**
     * Store indication that a deferred uncorrelated message abort should
     * occur
     *
     * @param message the current message
     */
    public static void storeDeferredUncorrelatedMessageAbort(Message message) {
        if (message.getExchange() != null) {
            message.getExchange().put("deferred.uncorrelated.message.abort", Boolean.TRUE);
        }
    }

    /**
     * Retrieve indication that a deferred uncorrelated message abort should
     * occur.
     *
     * @param message the current message
     * @return the retrieved indication
     */
    public static boolean retrieveDeferredUncorrelatedMessageAbort(Message message) {
        Boolean ret = message.getExchange() != null
                      ? (Boolean)message.getExchange().get("deferred.uncorrelated.message.abort")
                      : null;
        return ret != null && ret.booleanValue();
    }

    /**
     * Retrieve indication that an async post-response service invocation
     * is required.
     *
     * @param message the current message
     * @return the retrieved indication that an async post-response service
     * invocation is required.
     */
    public static boolean retrieveAsyncPostResponseDispatch(Message message) {
        Boolean ret = (Boolean)message.get(Message.ASYNC_POST_RESPONSE_DISPATCH);
        return ret != null && ret.booleanValue();
    }

    /**
     * @return a generated UUID
     */
    public static String generateUUID() {
        return URN_UUID + UUID.randomUUID();
    }

    /**
     * Retreive Conduit from Exchange if not already available
     *
     * @param conduit the current value for the Conduit
     * @param message the current message
     * @return the Conduit if available
     */
    public static Conduit getConduit(Conduit conduit, Message message) {
        if (conduit == null) {
            Exchange exchange = message.getExchange();
            conduit = exchange != null ? exchange.getConduit(message) : null;
        }
        return conduit;
    }

    public static EndpointReferenceType getNoneEndpointReference() {
        return NONE_ENDPOINT_REFERENCE;
    }

    public static void applyReferenceParam(EndpointReferenceType toEpr, Object el) {
        if (null == toEpr.getReferenceParameters()) {
            toEpr.setReferenceParameters(WSA_OBJECT_FACTORY.createReferenceParametersType());
        }
        toEpr.getReferenceParameters().getAny().add(el);
    }

    /**
     * Create a Binding specific Message.
     *
     * @param exchange the current exchange
     * @return the Method from the BindingOperationInfo
     */
    public static Message createMessage(Exchange exchange) {
        Endpoint ep = exchange.getEndpoint();
        Message msg = null;
        if (ep != null) {
            msg = new MessageImpl();
            msg.setExchange(exchange);
            if (ep.getBinding() != null) {
                msg = ep.getBinding().createMessage(msg);
            }
        }
        return msg;
    }

    /**
     * Returns {@code true} if {@code uri} is permitted as a wsa:ReplyTo / wsa:FaultTo
     * decoupled-destination address for the <em>WS-Addressing</em> path.
     *
     * <p>WS-Addressing decoupled destinations are <em>disabled by default</em> to
     * prevent SSRF: any attacker who can craft a SOAP request can otherwise force
     * the server to open an outbound connection to any URL. Enable with
     * {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY}{@code =true}.
     *
     * <p>When enabled, the URI must start with a prefix from
     * {@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} or
     * {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}.
     */
    public static boolean isDecoupledDestinationAllowed(String uri) {
        if (uri == null) {
            return false;
        }
        if (!Boolean.parseBoolean(
                System.getProperty(WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "false"))) {
            return false;
        }
        return isDecoupledDestinationSchemeAllowed(uri);
    }

    /**
     * Log a message after {@link #isDecoupledDestinationAllowed(String)} returned {@code false}.
     *
     * @param logger the {@link Logger} to pass the message to
     * @param level log {@link Level} on which the message should be logged
     * @param destinationUri the URI banned due to the configuration
     */
    public static void logDisallowedDecoupledDestinationScheme(Logger logger, Level level, String destinationUri) {
        logger.logrb(level,
                AddressingConstants.BUNDLE,
                "DISALLOWED_DECOUPLED_DESTINATION_SCHEME",
                new Object[] {
                    destinationUri,
                    ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
                });
    }

    /**
     * Log a message after {@link #isDecoupledDestinationAllowed(String)} returned {@code false}
     * for messages having {@value #DECOUPLED_DESTINATION_APPROVED_PROPERTY} set to {@code true}.
     *
     * @param logger the {@link Logger} to pass the message to
     * @param level log {@link Level} on which the message should be logged
     * @param destinationUri the URI banned due to the configuration
     */
    public static void logRejectedDecoupledDestination(Logger logger, Level level, String destinationUri) {
        logger.logrb(level,
                AddressingConstants.BUNDLE,
                "REJECTED_DECOUPLED_DESTINATION",
                new Object[] {
                    destinationUri,
                    WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY,
                    ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
                });
    }

    /**
     * Log a message after {@link #isDecoupledDestinationSchemeAllowed(String)} returned {@code false}
     * for messages having {@value #DECOUPLED_DESTINATION_APPROVED_PROPERTY} set to {@code true}.
     *
     * @param logger the {@link Logger} to pass the message to
     * @param level log {@link Level} on which the message should be logged
     * @param destinationUri the URI banned due to the configuration
     */
    public static void logDecoupledFaultToSchemeNotAllowed(Logger logger, Level level, String destinationUri) {
        logger.logrb(level,
                AddressingConstants.BUNDLE,
                "DECOUPLED_FAULT_TO_SCHEME_NOT_ALLOWED",
                new Object[] {
                    destinationUri,
                    ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
                });
    }

    /**
     * Log a message after {@link #isDecoupledDestinationAllowed(String)} returned {@code false}
     * for {@code FaultTo} URI.
     *
     * @param logger the {@link Logger} to pass the message to
     * @param level log {@link Level} on which the message should be logged
     * @param destinationUri the URI banned due to the configuration
     */
    public static void logDecoupledFaultToNotAllowed(Logger logger, Level level, String destinationUri) {
        logger.logrb(level,
                AddressingConstants.BUNDLE,
                "DECOUPLED_FAULT_TO_NOT_ALLOWED",
                new Object[] {
                    destinationUri,
                    WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY,
                    ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
                });
    }

    /**
     * Create a SOAP Fault reason after {@link #isDecoupledDestinationAllowed(String)} returned {@code false}.
     *
     * @param destinationUri the URI banned due to the configuration
     * @return a formatted message
     */
    public static String formatDecoupledReplyToNotPermittedMessage(String destinationUri) {
        return MessageFormat.format(
                AddressingConstants.BUNDLE.getString("DECOUPLED_REPLY_TO_NOT_PERMITTED"),
                destinationUri,
                WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY,
                ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY);
    }

    /**
     * Create a SOAP Fault reason after {@link #isDecoupledDestinationSchemeAllowed(String)} returned {@code false}.
     *
     * @param destinationUri the URI banned due to the configuration
     * @return a formatted message
     */
    public static String formatDecoupledReplyToSchemeNotPermittedMessage(String destinationUri) {
        return MessageFormat.format(
                AddressingConstants.BUNDLE.getString("DECOUPLED_REPLY_TO_SCHEME_NOT_PERMITTED"),
                destinationUri,
                ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY);
    }

    /**
     * Validates the URI scheme for {@link #isDecoupledDestinationAllowed}.
     */
    public static boolean isDecoupledDestinationSchemeAllowed(String uri) {
        if (uri == null) {
            return false;
        }
        // Normalize the URI to lowercase for case-insensitive scheme comparison
        // (RFC 3986 ��3.1: scheme is case-insensitive).
        String normalizedUri = uri.toLowerCase(Locale.ROOT);
        String prop = SystemPropertyAction.getPropertyOrNull(ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY);
        Set<String> allowed = (prop != null)
            ? new LinkedHashSet<>(Arrays.asList(prop.split(",")))
            : DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES;
        for (String prefix : allowed) {
            String trimmedPrefix = prefix.trim().toLowerCase(Locale.ROOT);
            // Skip empty tokens that arise from leading/trailing/consecutive commas in
            // the system property (e.g. ",http://" or "http://,,https://").
            // An empty prefix would make startsWith("") return true for every URI,
            // effectively disabling the SSRF allowlist.
            if (trimmedPrefix.isEmpty()) {
                continue;
            }
            if (normalizedUri.startsWith(trimmedPrefix)) {
                return true;
            }
        }
        return false;
    }

    public static Destination createDecoupledDestination(Exchange exchange,
                                                         final EndpointReferenceType reference) {
        final EndpointInfo ei = exchange.getEndpoint().getEndpointInfo();
        return new DecoupledDestination(ei, reference);
    }

    private static final class DecoupledDestination implements Destination {
        private final EndpointInfo ei;
        private final EndpointReferenceType reference;

        DecoupledDestination(EndpointInfo ei, EndpointReferenceType reference) {
            this.ei = ei;
            this.reference = reference;
        }

        public EndpointReferenceType getAddress() {
            return reference;
        }

        public Conduit getBackChannel(Message inMessage) throws IOException {
            if (isNoneAddress(reference)) {
                return null;
            }
            final String destinationUri = reference.getAddress().getValue();
            boolean approved = Boolean.TRUE.equals(
                inMessage.getExchange().get(DECOUPLED_DESTINATION_APPROVED_PROPERTY));
            if (approved) {
                // Higher-level protocol (e.g. WS-RM) pre-approved decoupled addressing
                // for this exchange; still enforce the scheme allowlist.
                if (!isDecoupledDestinationSchemeAllowed(destinationUri)) {
                    logDisallowedDecoupledDestinationScheme(LOG, Level.WARNING, destinationUri);
                    return null;
                }
            } else if (!isDecoupledDestinationAllowed(destinationUri)) {
                logRejectedDecoupledDestination(LOG, Level.WARNING, destinationUri);
                return null;
            }
            Bus bus = inMessage.getExchange().getBus();
            //this is a response targeting a decoupled endpoint.   Treat it as a oneway so
            //we don't wait for a response.
            inMessage.getExchange().setOneWay(true);
            ConduitInitiator conduitInitiator
                = bus.getExtension(ConduitInitiatorManager.class)
                    .getConduitInitiatorForUri(destinationUri);
            if (conduitInitiator != null) {
                Conduit c = conduitInitiator.getConduit(ei, reference, bus);
                //ensure decoupled back channel input stream is closed
                c.setMessageObserver(new MessageObserver() {
                    public void onMessage(Message m) {
                        InputStream is = m.getContent(InputStream.class);
                        if (is != null) {
                            try {
                                is.close();
                            } catch (Exception e) {
                                //ignore
                            }
                        }
                    }
                });
                return c;
            }
            return null;
        }

        public MessageObserver getMessageObserver() {
            return null;
        }

        public void shutdown() {
        }

        public void setMessageObserver(MessageObserver observer) {
        }
    }
}