![JAR search and dependency download from the Maven repository](/logo.png)
org.atmosphere.stomp.interceptor.ConnectInterceptor Maven / Gradle / Ivy
/*
* Copyright 2014 Jeanfrancois Arcand
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.atmosphere.stomp.interceptor;
import org.atmosphere.cpr.Action;
import org.atmosphere.cpr.AtmosphereConfig;
import org.atmosphere.cpr.AtmosphereFramework;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter;
import org.atmosphere.cpr.AtmosphereResourceImpl;
import org.atmosphere.cpr.HeartbeatAtmosphereResourceEvent;
import org.atmosphere.cpr.packages.StompEndpointProcessor;
import org.atmosphere.interceptor.HeartbeatInterceptor;
import org.atmosphere.stomp.StompInterceptor;
import org.atmosphere.stomp.protocol.Frame;
import org.atmosphere.stomp.protocol.Header;
import org.atmosphere.util.Version;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
*
* Evaluates the {@link org.atmosphere.stomp.protocol.Action#CONNECT connection} frame.
*
*
*
* Key features are the heartbeat negotiation and the user authentication when associated headers are specified in the
* frame.
*
*
* @author Guillaume DROUET
* @version 1.0
* @since 0.2
*/
public class ConnectInterceptor extends HeartbeatInterceptor implements StompInterceptor {
/**
* The padding data for STOMP heartbeat.
*/
public static final byte[] STOMP_HEARTBEAT_DATA = new byte[]{0x0A,};
/**
* The default supported version.
*/
public static final float DEFAULT_VERSION = 1.0f;
/**
* The highest version we currently support.
*/
public static final float HIGHEST_VERSION = 1.1f;
/**
* Supported versions.
*/
public static final String VERSIONS = String.format("%f,%f", DEFAULT_VERSION, HIGHEST_VERSION);
/**
* Server name sent to client.
*/
private static final String SERVER = "Atmosphere/" + Version.getDotedVersion();
/**
* Heartbeat desired by client.
*/
private ThreadLocal desiredHeartbeat = new ThreadLocal();
/**
* {@inheritDoc}
*/
@Override
public void configure(final AtmosphereConfig config) {
super.configure(config);
// Atmosphere.js does not like empty strings, works fine with "hb".
paddingText(/*STOMP_HEARTBEAT_DATA*/"hb".getBytes());
}
/**
* {@inheritDoc}
*/
@Override
protected int extractHeartbeatInterval(final AtmosphereResourceImpl resource) {
return desiredHeartbeat.get();
}
/**
* {@inheritDoc}
*/
@Override
public Action inspect(final AtmosphereFramework framework, final Frame frame, final FrameInterceptor.StompAtmosphereResource r) {
try {
// Hack: we suspect a heartbeat here
if (org.atmosphere.stomp.protocol.Action.NULL.equals(frame.getAction())) {
// Dispatch an event to notify that a heartbeat has been intercepted
// TODO: see https://github.com/Atmosphere/atmosphere/issues/1561
final AtmosphereResourceEvent event = new HeartbeatAtmosphereResourceEvent(AtmosphereResourceImpl.class.cast(r.getResource()));
r.getResource().addEventListener(new AtmosphereResourceEventListenerAdapter.OnHeartbeat() {
@Override
public void onHeartbeat(AtmosphereResourceEvent event) {
StompEndpointProcessor.invokeOnHeartbeat(event);
}
});
// Fire event
r.getResource().notifyListeners(event);
desiredHeartbeat.set(0);
return inspect(r.getResource());
}
// Send headers response to client
final Map headers = new HashMap();
// Protocol negotiation
final float version = parseVersion(frame.getHeaders().get(Header.ACCEPT_VERSION));
// No version in common between server and client
if (version == -1) {
headers.put(Header.VERSION, VERSIONS);
r.write(org.atmosphere.stomp.protocol.Action.ERROR, headers, "Supported protocol versions are " + VERSIONS);
return Action.CANCELLED;
} else {
// Extracts heartbeat then clock
final Integer[] intervals = parseHeartBeat(frame.getHeaders().get(Header.HEART_BEAT));
// Extract the desired heartbeat interval
// Won't be applied if lower than config value
int serverInterval = intervals[1];
if (serverInterval != 0) {
serverInterval = Math.max((int) TimeUnit.SECONDS.convert(serverInterval, TimeUnit.MILLISECONDS), heartbeatFrequencyInSeconds());
} else {
serverInterval = 0;
}
desiredHeartbeat.set(serverInterval);
final Action retval = inspect(r.getResource());
headers.put(Header.VERSION, String.valueOf(version));
headers.put(Header.SESSION, r.getResource().uuid());
headers.put(Header.SERVER, SERVER);
headers.put(Header.HEART_BEAT, TimeUnit.MILLISECONDS.convert(serverInterval, TimeUnit.SECONDS) + "," + intervals[0]);
r.write(org.atmosphere.stomp.protocol.Action.CONNECTED, headers);
return retval;
}
} finally {
desiredHeartbeat.remove();
}
}
/**
*
* Parse the given header value to extract the most appropriate version sent by client. If value is {@code null},
* then {@link #DEFAULT_VERSION} is returned. Otherwise, the method looks for the highest version supported by
* both client and server. If no version are in common, -1 is returned and an error should be sent
*
*
* @param acceptVersion the header value
* @return the extracted version
*/
private float parseVersion(final String acceptVersion) {
float retval;
if (acceptVersion != null) {
retval = -1;
final String[] versions = acceptVersion.split(",");
for (final String version : versions) {
final float clientVersion = Float.parseFloat(version);
if (clientVersion == DEFAULT_VERSION || clientVersion == HIGHEST_VERSION) {
retval = Math.max(retval, clientVersion);
}
}
} else {
retval = DEFAULT_VERSION;
}
return retval;
}
/**
*
* Extracts the heartbeat from the given header value. The value contains two integers. The first one is the
* heartbeat interval in milliseconds the sender of the header can assume. The second one is what he wants to
* receive.
*
*
*
* If the value is {@code null} then 0 will be returned for each direction do disable heartbeat.
*
*
* @param heartbeat the header value
* @return the parsed array
*/
private Integer[] parseHeartBeat(final String heartbeat) {
if (heartbeat == null) {
return new Integer[]{0, 0,};
} else {
final String[] arr = heartbeat.split(",");
return new Integer[]{Integer.parseInt(arr[0]), Integer.parseInt(arr[1]),};
}
}
}