blob: bb31b39b7d1c5b57b1c692da0e5512b4fb51d3e8 [file] [log] [blame]
/*
* 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 <code>BaseHTTPEndpoint</code>.
*/
public BaseHTTPEndpoint()
{
this(false);
}
/**
* Constructs a <code>BaseHTTPEndpoint</code> with the specified management setting.
*
* @param enableManagement <code>true</code> if the <code>BaseHTTPEndpoint</code>
* is manageable; otherwise <code>false</code>.
*/
public BaseHTTPEndpoint(boolean enableManagement)
{
super(enableManagement);
}
//--------------------------------------------------------------------------
//
// Initialize, validate, start, and stop methods.
//
//--------------------------------------------------------------------------
/**
* Initializes the <code>Endpoint</code> with the properties.
* If subclasses override this method, they must call <code>super.initialize()</code>.
*
* @param id The ID of the <code>Endpoint</code>.
* @param properties Properties for the <code>Endpoint</code>.
*/
@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 <code>Endpoint</code> 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<HttpHeader> httpResponseHeaders;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// addNoCacheHeaders
//----------------------------------
protected boolean addNoCacheHeaders = true;
/**
* Retrieves the <code>add-no-cache-headers</code> property.
*
* @return <code>true</code> if <code>add-no-cache-headers</code> is enabled;
* <code>false</code> otherwise.
*/
public boolean isAddNoCacheHeaders()
{
return addNoCacheHeaders;
}
/**
* Sets the <code>add-no-cache-headers</code> property.
*
* @param addNoCacheHeaders The <code>add-no-cache-headers</code> 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 <code>false</code>.
*
* @return <code>true</code> if the server session will be invalidated
* when a client channel disconnects, <code>false</code> otherwise.
*/
public boolean isInvalidateSessionOnDisconnect()
{
return invalidateSessionOnDisconnect;
}
/**
* Determines whether to invalidate the server session for a client
* that disconnects its channel.
* The default is <code>false</code>.
*
* @param value <code>true</code> to invalidate the server session for a client
* that disconnects its channel, <code>false</code> otherwise.
*/
public void setInvalidateSessionOnDisconnect(boolean value)
{
invalidateSessionOnDisconnect = value;
}
//----------------------------------
// redirectURL
//----------------------------------
protected String redirectURL;
/**
* Retrieves the <code>redirect-url</code> property.
*
* @return The <code>redirect-url</code> property.
*/
public String getRedirectURL()
{
return redirectURL;
}
/**
* Sets the <code>redirect-url</code> property.
*
* @param redirectURL The <code>redirect-url</code> 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 <code>true</code>.
*
* @return <code>true</code> 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 <code>ConfigMap</code> of endpoint properties that the client
* needs. This includes properties from <code>super.describeEndpoint</code>
* and additional <code>BaseHTTPEndpoint</code> 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<FlexSession> sessions = null;
if (!duplicateSessionDetected)
{
sessions = flexClient.getFlexSessions();
int n = sessions.size();
if (n > 1)
{
List<HttpFlexSession> httpFlexSessions = new ArrayList<HttpFlexSession>();
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<HttpFlexSession> httpFlexSessions = (List<HttpFlexSession>)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<MessageClient> 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<String> headers = httpResponseHeaders.getPropertyAsList(HEADER_ATTR, null);
if (headers == null || headers.isEmpty())
return;
if (this.httpResponseHeaders == null)
this.httpResponseHeaders = new ArrayList<HttpHeader>();
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;
}
}