All Downloads are FREE. Search and download functionalities are using the official Maven repository.

flex.messaging.endpoints.BaseHTTPEndpoint Maven / Gradle / Ivy

/*
 * 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 flex.messaging.endpoints;

import flex.management.runtime.messaging.endpoints.EndpointControl;
import flex.messaging.FlexContext;
import flex.messaging.FlexSession;
import flex.messaging.HttpFlexSession;
import flex.messaging.MessageClient;
import flex.messaging.client.FlexClient;
import flex.messaging.config.ConfigMap;
import flex.messaging.config.ConfigurationConstants;
import flex.messaging.endpoints.amf.AMFFilter;
import flex.messaging.io.MessageIOConstants;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.log.HTTPRequestLog;
import flex.messaging.messages.CommandMessage;
import flex.messaging.messages.Message;
import flex.messaging.util.SettingsReplaceUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Abstract base class for all the HTTP-based endpoints.
 */
public abstract class BaseHTTPEndpoint extends AbstractEndpoint
{
    //--------------------------------------------------------------------------
    //
    // Public Static Constants
    //
    //--------------------------------------------------------------------------

    /**
     * The secure and insecure URL schemes for the HTTP endpoint.
     */
    public static final String HTTP_PROTOCOL_SCHEME = "http";
    public static final String HTTPS_PROTOCOL_SCHEME = "https";

    //--------------------------------------------------------------------------
    //
    // Private Static Constants
    //
    //--------------------------------------------------------------------------

    private static final String ADD_NO_CACHE_HEADERS = "add-no-cache-headers";
    private static final String REDIRECT_URL = "redirect-url";
    private static final String INVALIDATE_SESSION_ON_DISCONNECT = "invalidate-session-on-disconnect";
    private static final String HTTP_RESPONSE_HEADERS = "http-response-headers";
    private static final String HEADER_ATTR = "header";

    private static final String HEADER_NAME_ORIGIN = "Origin";
    private static final String ACCESS_CONTROL = "Access-Control-";
    private static final String SESSION_REWRITING_ENABLED = "session-rewriting-enabled";

    private static final int ERR_MSG_DUPLICATE_SESSIONS_DETECTED = 10035;
    private static final String REQUEST_ATTR_DUPLICATE_SESSION_FLAG = "flex.messaging.request.DuplicateSessionDetected";

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * Constructs an unmanaged BaseHTTPEndpoint.
     */
    public BaseHTTPEndpoint()
    {
        this(false);
    }

    /**
     * Constructs a BaseHTTPEndpoint with the specified management setting.
     *
     * @param enableManagement true if the BaseHTTPEndpoint
     * is manageable; otherwise false.
     */
    public BaseHTTPEndpoint(boolean enableManagement)
    {
        super(enableManagement);
    }

    //--------------------------------------------------------------------------
    //
    // Initialize, validate, start, and stop methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Initializes the Endpoint with the properties.
     * If subclasses override this method, they must call super.initialize().
     *
     * @param id The ID of the Endpoint.
     * @param properties Properties for the Endpoint.
     */
    @Override public void initialize(String id, ConfigMap properties)
    {
        super.initialize(id, properties);

        if (properties == null || properties.size() == 0)
            return;

        // General HTTP props.
        addNoCacheHeaders = properties.getPropertyAsBoolean(ADD_NO_CACHE_HEADERS, true);
        redirectURL = properties.getPropertyAsString(REDIRECT_URL, null);
        invalidateSessionOnDisconnect = properties.getPropertyAsBoolean(INVALIDATE_SESSION_ON_DISCONNECT, false);
        loginAfterDisconnect = properties.getPropertyAsBoolean(ConfigurationConstants.LOGIN_AFTER_DISCONNECT_ELEMENT, false);
        sessionRewritingEnabled = properties.getPropertyAsBoolean(SESSION_REWRITING_ENABLED, true);
        initializeHttpResponseHeaders(properties);
        validateEndpointProtocol();
    }

    /**
     * Starts the Endpoint by creating a filter chain and setting
     * up serializers and deserializers.
     */
    @Override public void start()
    {
        if (isStarted())
            return;

        super.start();

        filterChain = createFilterChain();
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     * Controller used to manage this endpoint.
     */
    protected EndpointControl controller;

    /**
     * AMF processing filter chain used by this endpoint.
     */
    protected AMFFilter filterChain;

    /**
     * Headers to add to the HTTP response.
     */
    protected List httpResponseHeaders;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  addNoCacheHeaders
    //----------------------------------

    protected boolean addNoCacheHeaders = true;

    /**
     * Retrieves the add-no-cache-headers property.
     *
     * @return true if add-no-cache-headers is enabled;
     * false otherwise.
     */
    public boolean isAddNoCacheHeaders()
    {
        return addNoCacheHeaders;
    }

    /**
     * Sets the add-no-cache-headers property.
     *
     * @param addNoCacheHeaders The add-no-cache-headers property.
     */
    public void setAddNoCacheHeaders(boolean addNoCacheHeaders)
    {
        this.addNoCacheHeaders = addNoCacheHeaders;
    }

    //----------------------------------
    //  loginAfterDisconnect
    //----------------------------------

    /**
     *
     * This is a property used on the client.
     */
    protected boolean loginAfterDisconnect;

    //----------------------------------
    //  invalidateSessionOnDisconnect
    //----------------------------------

    protected boolean invalidateSessionOnDisconnect;

    /**
     * Indicates whether the server session will be invalidated
     * when a client channel disconnects.
     * The default is false.
     *
     * @return true if the server session will be invalidated
     *         when a client channel disconnects, false otherwise.
     */
    public boolean isInvalidateSessionOnDisconnect()
    {
        return invalidateSessionOnDisconnect;
    }

    /**
     * Determines whether to invalidate the server session for a client
     * that disconnects its channel.
     * The default is false.
     *
     * @param value true to invalidate the server session for a client
     *              that disconnects its channel, false otherwise.
     */
    public void setInvalidateSessionOnDisconnect(boolean value)
    {
        invalidateSessionOnDisconnect = value;
    }

    //----------------------------------
    //  redirectURL
    //----------------------------------

    protected String redirectURL;

    /**
     * Retrieves the redirect-url property.
     *
     * @return The redirect-url property.
     */
    public String getRedirectURL()
    {
        return redirectURL;
    }

    /**
     * Sets the redirect-url property.
     *
     * @param redirectURL The redirect-url property.
     */
    public void setRedirectURL(String redirectURL)
    {
        this.redirectURL = redirectURL;
    }

    //----------------------------------
    //  sessionRewritingEnabled
    //----------------------------------

    protected boolean sessionRewritingEnabled = true;

    /**
     * Indicates whether the server will fall back on rewriting URLs to include
     * session identifiers in the URL when HTTP session cookies are not allowed
     * on the client. The default is true.
     *
     * @return true if the session rewriting is enabled.
     */
    public boolean isSessionRewritingEnabled()
    {
        return sessionRewritingEnabled;
    }

    /**
     * Sets whether the session rewriting is enabled.
     *
     * @param value The session writing enabled value.
     */
    public void setSessionRewritingEnabled(boolean value)
    {
        sessionRewritingEnabled = value;
    }

    //--------------------------------------------------------------------------
    //
    // Public Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Handle AMF/AMFX encoded messages sent over HTTP.
     *
     * @param req The original servlet request.
     * @param res The active servlet response.
     */
    @Override
    public void service(HttpServletRequest req, HttpServletResponse res)
    {
        super.service(req, res);

        try
        {
            // Setup serialization and type marshalling contexts
            setThreadLocals();

            // Create a context for this request
            ActionContext context = new ActionContext();

            // Pass endpoint's mpi settings to the context so that it knows what level of
            // performance metrics should be gathered during serialization/deserialization
            context.setRecordMessageSizes(isRecordMessageSizes());
            context.setRecordMessageTimes(isRecordMessageTimes());

            // Send invocation through filter chain, which ends at the MessageBroker
            filterChain.invoke(context);

            // After serialization completes, increment endpoint byte counters,
            // if the endpoint is managed
            if (isManaged())
            {
                controller.addToBytesDeserialized(context.getDeserializedBytes());
                controller.addToBytesSerialized(context.getSerializedBytes());
            }

            if (context.getStatus() != MessageIOConstants.STATUS_NOTAMF)
            {
                if (addNoCacheHeaders)
                    addNoCacheHeaders(req, res);

                addHeadersToResponse(req, res);

                ByteArrayOutputStream outBuffer = context.getResponseOutput();

                res.setContentType(getResponseContentType());

                res.setContentLength(outBuffer.size());
                outBuffer.writeTo(res.getOutputStream());
                res.flushBuffer();
            }
            else
            {
                // Not an AMF request, probably viewed in a browser
                if (redirectURL != null)
                {
                    try
                    {
                        //Check for redirect URL context-root token
                        redirectURL = SettingsReplaceUtil.replaceContextPath(redirectURL, req.getContextPath());
                        res.sendRedirect(redirectURL);
                    }
                    catch (IllegalStateException alreadyFlushed)
                    {
                        // ignore
                    }
                }
            }
        }
        catch (IOException ioe)
        {
            // This happens when client closes the connection, log it at info level
            log.info(ioe.getMessage());
            // Store exception information for latter logging
            req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, ioe.toString());
        }
        catch (Throwable t)
        {
            log.error(t.getMessage(), t);
            // Store exception information for latter logging
            req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, t.toString());
        }
        finally
        {
            clearThreadLocals();
        }
    }


    /**
     *
     * Returns a ConfigMap of endpoint properties that the client
     * needs. This includes properties from super.describeEndpoint
     * and additional BaseHTTPEndpoint specific properties under
     * "properties" key.
     */
    @Override
    public ConfigMap describeEndpoint()
    {
        ConfigMap endpointConfig = super.describeEndpoint();

        if (loginAfterDisconnect)
        {
            ConfigMap loginAfterDisconnect = new ConfigMap();
            // Adding as a value rather than attribute to the parent
            loginAfterDisconnect.addProperty(EMPTY_STRING, TRUE_STRING);

            ConfigMap properties = endpointConfig.getPropertyAsMap(PROPERTIES_ELEMENT, null);
            if (properties == null)
            {
                properties = new ConfigMap();
                endpointConfig.addProperty(PROPERTIES_ELEMENT, properties);
            }
            properties.addProperty(ConfigurationConstants.LOGIN_AFTER_DISCONNECT_ELEMENT, loginAfterDisconnect);
        }

        return endpointConfig;
    }

    /**
     * Overrides to guard against duplicate HTTP-based sessions for the same FlexClient
     * which will occur if the remote host has disabled session cookies.
     *
     * @see AbstractEndpoint#setupFlexClient(String)
     */
    @Override
    public FlexClient setupFlexClient(String id)
    {
        FlexClient flexClient = super.setupFlexClient(id);

        // Scan for duplicate HTTP-sessions and if found, invalidate them and throw a MessageException.
        // A request attribute is used to deal with batched AMF messages that arrive in a single request by trigger multiple passes through this method.
        boolean duplicateSessionDetected = (FlexContext.getHttpRequest().getAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG) != null);

        List sessions = null;
        if (!duplicateSessionDetected)
        {
            sessions = flexClient.getFlexSessions();
            int n = sessions.size();
            if (n > 1)
            {
                List httpFlexSessions = new ArrayList();
                for (int i = 0; i < n; i++)
                {
                    FlexSession currentSession = sessions.get(i);
                    if (currentSession instanceof HttpFlexSession)
                        httpFlexSessions.add((HttpFlexSession)currentSession);
                    if (httpFlexSessions.size() > 1)
                    {
                        FlexContext.getHttpRequest().setAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG, httpFlexSessions);
                        duplicateSessionDetected = true;
                        break;
                    }
                }
            }
        }

        // If more than one was found, remote host isn't using session cookies. Kill all duplicate sessions and return an error.
        // Simplest to just re-scan the list given that it will be very short, but use an iterator for concurrent modification.
        if (duplicateSessionDetected)
        {
            Object attributeValue = FlexContext.getHttpRequest().getAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG);
            String newSessionId = null;
            String oldSessionId = null;
            if (attributeValue != null)
            {
                @SuppressWarnings("unchecked")
                List httpFlexSessions = (List)attributeValue;
                oldSessionId = httpFlexSessions.get(0).getId();
                newSessionId = httpFlexSessions.get(1).getId();
            }

            if (sessions != null)
            {
                for (FlexSession session : sessions)
                {
                    if (session instanceof HttpFlexSession)
                    {
                        session.invalidate();
                    }
                }
            }

            // Return an error to the client.

            DuplicateSessionException e = new DuplicateSessionException();
            // Duplicate HTTP-based FlexSession error: A request for FlexClient ''{0}'' arrived over a new FlexSession ''{1}'', but FlexClient is already associated with FlexSession ''{2}'', therefore it cannot be associated with the new session.
            e.setMessage(ERR_MSG_DUPLICATE_SESSIONS_DETECTED, new Object[]{flexClient.getId(), newSessionId, oldSessionId});
            throw e;
        }

        return flexClient;
    }

    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Adds custom headers specified in the config to the HTTP response. The only
     * exception is that access control headers (Access-Control-*) are sent only
     * if there is an Origin header in the request.
     *
     * @param request The HTTP request.
     * @param response The HTTP response.
     */
    protected void addHeadersToResponse(HttpServletRequest request, HttpServletResponse response)
    {
        if (httpResponseHeaders == null || httpResponseHeaders.isEmpty())
            return;

        String origin = request.getHeader(HEADER_NAME_ORIGIN);
        boolean originHeaderExists = origin != null && origin.length() != 0;

        for (HttpHeader header : httpResponseHeaders)
        {
            if (header.name.startsWith(ACCESS_CONTROL) && !originHeaderExists)
                continue;

            response.addHeader(header.name, header.value);
        }
    }

    /**
     * Create the gateway filters that transform action requests
     * and responses.
     */
    protected abstract AMFFilter createFilterChain();

    /**
     * Returns the content type used by the connection handler to set on the
     * HTTP response. Subclasses should either return MessageIOConstants.AMF_CONTENT_TYPE
     * or MessageIOConstants.XML_CONTENT_TYPE.
     */
    protected abstract String getResponseContentType();

    /**
     * Returns https which is the secure protocol scheme for the endpoint.
     *
     * @return https.
     */
    @Override protected String getSecureProtocolScheme()
    {
        return HTTPS_PROTOCOL_SCHEME;
    }

    /**
     * Returns http which is the insecure protocol scheme for the endpoint.
     *
     * @return http.
     */
    @Override protected String getInsecureProtocolScheme()
    {
        return HTTP_PROTOCOL_SCHEME;
    }

    /**
     * @see flex.messaging.endpoints.AbstractEndpoint#handleChannelDisconnect(CommandMessage)
     */
    @Override protected Message handleChannelDisconnect(CommandMessage disconnectCommand)
    {
        HttpFlexSession session = (HttpFlexSession)FlexContext.getFlexSession();
        FlexClient flexClient = FlexContext.getFlexClient();

        // Shut down any subscriptions established over this channel/endpoint
        // for this specific FlexClient.
        if (flexClient.isValid())
        {
            String endpointId = getId();
            List messageClients = flexClient.getMessageClients();
            for (MessageClient messageClient : messageClients)
            {
                if (messageClient.getEndpointId().equals(endpointId))
                {
                    messageClient.setClientChannelDisconnected(true);
                    messageClient.invalidate();
                }
            }
        }

        // And optionally invalidate the session.
        if (session.isValid() && isInvalidateSessionOnDisconnect())
            session.invalidate(false /* don't recreate */);

        return super.handleChannelDisconnect(disconnectCommand);
    }

    protected void initializeHttpResponseHeaders(ConfigMap properties)
    {
        if (!properties.containsKey(HTTP_RESPONSE_HEADERS))
            return;

        ConfigMap httpResponseHeaders = properties.getPropertyAsMap(HTTP_RESPONSE_HEADERS, null);
        if (httpResponseHeaders == null)
            return;

        @SuppressWarnings("unchecked")
        List headers = httpResponseHeaders.getPropertyAsList(HEADER_ATTR, null);
        if (headers == null || headers.isEmpty())
            return;

        if (this.httpResponseHeaders == null)
            this.httpResponseHeaders = new ArrayList();

        for (String header : headers)
        {
            int colonIndex = header.indexOf(":");
            String name = header.substring(0, colonIndex).trim();
            String value = header.substring(colonIndex + 1).trim();
            this.httpResponseHeaders.add(new HttpHeader(name, value));
        }
    }

    //--------------------------------------------------------------------------
    //
    // Nested Classes
    //
    //--------------------------------------------------------------------------

    /**
     * Helper class used for headers in the HTTP request/response.
     */
    static class HttpHeader
    {
        public HttpHeader(String name, String value)
        {
            this.name = name;
            this.value = value;
        }
        public final String name;
        public final String value;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy