ws.wamp.jawampa.WampRouter Maven / Gradle / Ivy
/*
* Copyright 2014 Matthias Einwag
*
* The jawampa authors license 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 ws.wamp.jawampa;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import rx.Observable;
import rx.Scheduler;
import rx.schedulers.Schedulers;
import rx.subjects.AsyncSubject;
import ws.wamp.jawampa.WampMessages.*;
import ws.wamp.jawampa.connection.ICompletionCallback;
import ws.wamp.jawampa.connection.IConnectionController;
import ws.wamp.jawampa.connection.IWampConnection;
import ws.wamp.jawampa.connection.IWampConnectionAcceptor;
import ws.wamp.jawampa.connection.IWampConnectionFuture;
import ws.wamp.jawampa.connection.IWampConnectionListener;
import ws.wamp.jawampa.connection.IWampConnectionPromise;
import ws.wamp.jawampa.connection.QueueingConnectionController;
import ws.wamp.jawampa.connection.WampConnectionPromise;
import ws.wamp.jawampa.internal.IdGenerator;
import ws.wamp.jawampa.internal.IdValidator;
import ws.wamp.jawampa.internal.RealmConfig;
import ws.wamp.jawampa.internal.UriValidator;
import ws.wamp.jawampa.internal.Version;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* The {@link WampRouter} provides Dealer and Broker functionality for the WAMP
* protocol.
*/
public class WampRouter {
final static Set SUPPORTED_CLIENT_ROLES;
static {
SUPPORTED_CLIENT_ROLES = new HashSet();
SUPPORTED_CLIENT_ROLES.add(WampRoles.Caller);
SUPPORTED_CLIENT_ROLES.add(WampRoles.Callee);
SUPPORTED_CLIENT_ROLES.add(WampRoles.Publisher);
SUPPORTED_CLIENT_ROLES.add(WampRoles.Subscriber);
}
/** Represents a realm that is exposed through the router */
static class Realm {
final RealmConfig config;
final ObjectNode welcomeDetails;
final Map channelsBySessionId = new HashMap();
final Map procedures = new HashMap();
// Fields that are used for implementing subscription functionality
final EnumMap> subscriptionsByFlags
= new EnumMap>(SubscriptionFlags.class);
final Map subscriptionsById = new HashMap();
long lastUsedSubscriptionId = IdValidator.MIN_VALID_ID;
public Realm(RealmConfig config) {
this.config = config;
subscriptionsByFlags.put(SubscriptionFlags.Exact, new HashMap());
subscriptionsByFlags.put(SubscriptionFlags.Prefix, new HashMap());
subscriptionsByFlags.put(SubscriptionFlags.Wildcard, new HashMap());
// Expose the roles that are configured for the realm
ObjectMapper objectMapper = new ObjectMapper();
welcomeDetails = objectMapper.createObjectNode();
welcomeDetails.put("agent", Version.getVersion());
ObjectNode routerRoles = welcomeDetails.putObject("roles");
for (WampRoles role : config.roles) {
ObjectNode roleNode = routerRoles.putObject(role.toString());
if (role == WampRoles.Publisher) {
ObjectNode featuresNode = roleNode.putObject("features");
featuresNode.put("publisher_exclusion", true);
} else if (role == WampRoles.Subscriber) {
ObjectNode featuresNode = roleNode.putObject("features");
featuresNode.put("pattern_based_subscription", true);
}
}
}
void includeChannel(ClientHandler channel, long sessionId, Set roles) {
channel.realm = this;
channel.sessionId = sessionId;
channel.roles = roles;
channelsBySessionId.put(sessionId, channel);
}
void removeChannel(ClientHandler channel, boolean removeFromList) {
if (channel.realm == null) return;
if (channel.subscriptionsById != null) {
// Remove the channels subscriptions from our subscription table
for (Subscription sub : channel.subscriptionsById.values()) {
sub.subscribers.remove(channel);
if (sub.subscribers.isEmpty()) {
// Subscription is no longer used by any client
subscriptionsByFlags.get(sub.flags).remove(sub.topic);
subscriptionsById.remove(sub.subscriptionId);
}
}
channel.subscriptionsById.clear();
channel.subscriptionsById = null;
}
if (channel.providedProcedures != null) {
// Remove the clients procedures from our procedure table
for (Procedure proc : channel.providedProcedures.values()) {
// Clear all pending invocations and thereby inform other clients
// that the proc has gone away
for (Invocation invoc : proc.pendingCalls) {
if (invoc.caller.state != RouterHandlerState.Open) continue;
ErrorMessage errMsg = new ErrorMessage(CallMessage.ID, invoc.callRequestId,
null, ApplicationError.NO_SUCH_PROCEDURE, null, null);
invoc.caller.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
}
proc.pendingCalls.clear();
// Remove the procedure from the realm
procedures.remove(proc.procName);
}
channel.providedProcedures = null;
channel.pendingInvocations = null;
}
if (removeFromList) {
channelsBySessionId.remove(channel.sessionId);
}
channel.realm = null;
channel.roles.clear();
channel.roles = null;
channel.sessionId = 0;
}
}
static class Procedure {
final String procName;
final ClientHandler provider;
final long registrationId;
final List pendingCalls = new ArrayList();
public Procedure(String name, ClientHandler provider, long registrationId) {
this.procName = name;
this.provider = provider;
this.registrationId = registrationId;
}
}
static class Invocation {
Procedure procedure;
long callRequestId;
ClientHandler caller;
long invocationRequestId;
}
static class Subscription {
final String topic;
final SubscriptionFlags flags;
final String components[]; // non-null only for wildcard type
final long subscriptionId;
final Set subscribers;
public Subscription(String topic, SubscriptionFlags flags, long subscriptionId) {
this.topic = topic;
this.flags = flags;
this.components = flags == SubscriptionFlags.Wildcard ? topic.split("\\.", -1) : null;
this.subscriptionId = subscriptionId;
this.subscribers = new HashSet();
}
}
final ScheduledExecutorService eventLoop;
final Scheduler scheduler;
final ObjectMapper objectMapper = new ObjectMapper();
boolean isDisposed = false;
AsyncSubject closedFuture = AsyncSubject.create();
final Map realms;
final Set idleChannels;
/** The number of connections that have to be closed. This is important for shutdown */
int connectionsToClose = 0;
/**
* Returns the (singlethreaded) EventLoop on which this router is running.
* This is required by other Netty ChannelHandlers that want to forward messages
* to the router.
*/
public ScheduledExecutorService eventLoop() {
return eventLoop;
}
/**
* Returns the Jackson {@link ObjectMapper} that is used for JSON serialization,
* deserialization and object mapping by this router.
*/
public ObjectMapper objectMapper() {
return objectMapper;
}
WampRouter(Map realms) {
// Populate the realms from the configuration
this.realms = new HashMap();
for (Map.Entry e : realms.entrySet()) {
Realm info = new Realm(e.getValue());
this.realms.put(e.getKey(), info);
}
// Create an eventloop and the RX scheduler on top of it
this.eventLoop = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "WampRouterEventLoop");
t.setDaemon(true);
return t;
}
});
this.scheduler = Schedulers.from(eventLoop);
idleChannels = new HashSet();
}
/**
* Tries to schedule a runnable on the underlying executor.
* Rejected executions will be suppressed.
* This is useful for cases when the clients EventLoop is shut down before
* the EventLoop of the underlying connection.
*
* @param action The action to schedule.
*/
void tryScheduleAction(Runnable action) {
try {
eventLoop.submit(action);
} catch (RejectedExecutionException e) {}
}
private ICompletionCallback onConnectionClosed = new ICompletionCallback() {
@Override
public void onCompletion(IWampConnectionFuture future) {
tryScheduleAction(new Runnable() {
@Override
public void run() {
connectionsToClose -= 1;
if (isDisposed && connectionsToClose == 0) {
eventLoop.shutdown();
closedFuture.onNext(null);
closedFuture.onCompleted();
}
}
});
}
};
/**
* Increases the number of connections to close and starts to asynchronously
* close it. When this has happened {@link WampRouter#onConnectionClosed} will be called.
*/
private void closeConnection(IConnectionController controller, boolean sendRemaining) {
connectionsToClose += 1;
WampConnectionPromise promise =
new WampConnectionPromise(onConnectionClosed, null);
controller.close(sendRemaining, promise);
}
/**
* Closes the router.
* This will shut down all realm that are registered to the router.
* All connections to clients on the realm will be closed.
* However pending calls will be completed through an error message
* as far as possible.
* @return Returns an observable that completes when the router is completely shut down.
*/
public Observable close() {
if (eventLoop.isShutdown()) return closedFuture;
tryScheduleAction(new Runnable() {
@Override
public void run() {
if (isDisposed) return;
isDisposed = true;
// Close all currently connected channels
for (IConnectionController con : idleChannels) closeConnection(con, true);
idleChannels.clear();
for (Realm ri : realms.values()) {
for (ClientHandler channel : ri.channelsBySessionId.values()) {
ri.removeChannel(channel, false);
channel.markAsClosed();
GoodbyeMessage goodbye = new GoodbyeMessage(null, ApplicationError.SYSTEM_SHUTDOWN);
channel.controller.sendMessage(goodbye, IWampConnectionPromise.Empty);
closeConnection(channel.controller, true);
}
ri.channelsBySessionId.clear();
}
// close is asynchronous. It will wait until all connections are closed
// Afterwards the eventLoop will be shutDown.
}
});
return closedFuture;
}
enum RouterHandlerState {
Open,
Closed
}
IWampConnectionAcceptor connectionAcceptor = new IWampConnectionAcceptor() {
@Override
public IWampConnectionListener createNewConnectionListener() {
ClientHandler newHandler = new ClientHandler();
IConnectionController newController = new QueueingConnectionController(eventLoop, newHandler);
newHandler.controller = newController;
return newController;
}
@Override
public void acceptNewConnection(final IWampConnection newConnection,
final IWampConnectionListener connectionListener) {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
if (connectionListener == null
|| !(connectionListener instanceof QueueingConnectionController)
|| newConnection == null) {
// This is always true if the transport provider does not manipulate the structure
// that was sent by the router
if (newConnection != null) newConnection.close(false, IWampConnectionPromise.Empty);
return;
}
QueueingConnectionController controller = (QueueingConnectionController)connectionListener;
controller.setConnection(newConnection);
if (isDisposed) {
// Got an incoming connection after the router has already shut down.
// Therefore we close the connection
closeConnection(controller, false);
} else {
// Store the controller
idleChannels.add(controller);
}
}
});
} catch (RejectedExecutionException e) {
// Close the connection
// Defer the operation to avoid a cyclic call from the new connection
// to this method and back
Runnable r = new Runnable () {
@Override
public void run() {
newConnection.close(false, IWampConnectionPromise.Empty);
}
};
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(r);
executor.shutdown();
}
}
};
/**
* Returns the {@link IWampConnectionAcceptor} interface that the router
* provides in order to be able to accept new connection.
*/
public IWampConnectionAcceptor connectionAcceptor() {
return connectionAcceptor;
}
class ClientHandler implements IWampConnectionListener {
IConnectionController controller;
public RouterHandlerState state = RouterHandlerState.Open;
long sessionId;
Realm realm;
Set roles;
/**
* Procedures that this channel provides.
* Key is the registration ID, Value is the procedure
*/
Map providedProcedures;
Map pendingInvocations;
/** The Set of subscriptions to which this channel is subscribed */
Map subscriptionsById;
long lastUsedId = IdValidator.MIN_VALID_ID;
void markAsClosed() {
state = RouterHandlerState.Closed;
}
public ClientHandler() {
}
@Override
public void transportClosed() {
// Handle in the same way as a close due to an error
transportError(null);
}
@Override
public void transportError(Throwable cause) {
if (isDisposed || state != RouterHandlerState.Open) return;
if (realm != null) {
closeActiveClient(ClientHandler.this, null);
} else {
closePassiveClient(ClientHandler.this);
}
}
@Override
public void messageReceived(final WampMessage message) {
if (isDisposed || state != RouterHandlerState.Open) return;
if (realm == null) {
onMessageFromUnregisteredChannel(ClientHandler.this, message);
} else {
onMessageFromRegisteredChannel(ClientHandler.this, message);
}
}
}
private void onMessageFromRegisteredChannel(ClientHandler handler, WampMessage msg) {
// TODO: Validate roles for all relevant messages
if (msg instanceof HelloMessage || msg instanceof WelcomeMessage) {
// The client sent hello but it was already registered -> This is an error
// If the client sends welcome it's also an error
closeActiveClient(handler, new GoodbyeMessage(null, ApplicationError.INVALID_ARGUMENT));
} else if (msg instanceof AbortMessage || msg instanceof GoodbyeMessage) {
// The client wants to leave the realm
// Remove the channel from the realm
handler.realm.removeChannel(handler, true);
// But add it to the list of passive channels
idleChannels.add(handler.controller);
// Echo the message in case of goodbye
if (msg instanceof GoodbyeMessage) {
GoodbyeMessage reply = new GoodbyeMessage(null, ApplicationError.GOODBYE_AND_OUT);
handler.controller.sendMessage(reply, IWampConnectionPromise.Empty);
}
} else if (msg instanceof CallMessage) {
// The client wants to call a remote function
// Verify the message
CallMessage call = (CallMessage) msg;
String err = null;
if (!UriValidator.tryValidate(call.procedure, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
if (err == null && !(IdValidator.isValidId(call.requestId))) {
// Client sent an invalid request ID
err = ApplicationError.INVALID_ARGUMENT;
}
Procedure proc = null;
if (err == null) {
proc = handler.realm.procedures.get(call.procedure);
if (proc == null) err = ApplicationError.NO_SUCH_PROCEDURE;
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(CallMessage.ID, call.requestId,
null, err, null, null);
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
return;
}
// Everything checked, we can forward the call to the provider
Invocation invoc = new Invocation();
invoc.callRequestId = call.requestId;
invoc.caller = handler;
invoc.procedure = proc;
invoc.invocationRequestId = IdGenerator.newLinearId(proc.provider.lastUsedId,
proc.provider.pendingInvocations);
proc.provider.lastUsedId = invoc.invocationRequestId;
// Store the invocation
proc.provider.pendingInvocations.put(invoc.invocationRequestId, invoc);
// Store the call in the procedure to return error if client unregisters
proc.pendingCalls.add(invoc);
// And send it to the provider
InvocationMessage imsg = new InvocationMessage(invoc.invocationRequestId,
proc.registrationId, null, call.arguments, call.argumentsKw);
proc.provider.controller.sendMessage(imsg, IWampConnectionPromise.Empty);
} else if (msg instanceof YieldMessage) {
// The clients sends as the result of an RPC
// Verify the message
YieldMessage yield = (YieldMessage) msg;
if (!(IdValidator.isValidId(yield.requestId))) return;
// Look up the invocation to find the original caller
if (handler.pendingInvocations == null) return; // If a client send a yield without an invocation, return
Invocation invoc = handler.pendingInvocations.get(yield.requestId);
if (invoc == null) return; // There is no invocation pending under this ID
handler.pendingInvocations.remove(yield.requestId);
invoc.procedure.pendingCalls.remove(invoc);
// Send the result to the original caller
ResultMessage result = new ResultMessage(invoc.callRequestId, null, yield.arguments, yield.argumentsKw);
invoc.caller.controller.sendMessage(result, IWampConnectionPromise.Empty);
} else if (msg instanceof ErrorMessage) {
ErrorMessage err = (ErrorMessage) msg;
if (!(IdValidator.isValidId(err.requestId))) {
return;
}
if (err.requestType == InvocationMessage.ID) {
if (!UriValidator.tryValidate(err.error, handler.realm.config.useStrictUriValidation)) {
// The Message provider has sent us an invalid URI for the error string
// We better don't forward it but instead close the connection, which will
// give the original caller an unknown message error
closeActiveClient(handler, new GoodbyeMessage(null, ApplicationError.INVALID_ARGUMENT));
return;
}
// Look up the invocation to find the original caller
if (handler.pendingInvocations == null) return; // if an error is send before an invocation, do not do anything
Invocation invoc = handler.pendingInvocations.get(err.requestId);
if (invoc == null) return; // There is no invocation pending under this ID
handler.pendingInvocations.remove(err.requestId);
invoc.procedure.pendingCalls.remove(invoc);
// Send the result to the original caller
ErrorMessage fwdError = new ErrorMessage(CallMessage.ID, invoc.callRequestId,
null, err.error, err.arguments, err.argumentsKw);
invoc.caller.controller.sendMessage(fwdError, IWampConnectionPromise.Empty);
}
// else TODO: Are there any other possibilities where a client could return ERROR
} else if (msg instanceof RegisterMessage) {
// The client wants to register a procedure
// Verify the message
RegisterMessage reg = (RegisterMessage) msg;
String err = null;
if (!UriValidator.tryValidate(reg.procedure, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
if (err == null && !(IdValidator.isValidId(reg.requestId))) {
// Client sent an invalid request ID
err = ApplicationError.INVALID_ARGUMENT;
}
Procedure proc = null;
if (err == null) {
proc = handler.realm.procedures.get(reg.procedure);
if (proc != null) err = ApplicationError.PROCEDURE_ALREADY_EXISTS;
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(RegisterMessage.ID, reg.requestId,
null, err, null, null);
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
return;
}
// Everything checked, we can register the caller as the procedure provider
long registrationId = IdGenerator.newLinearId(handler.lastUsedId, handler.providedProcedures);
handler.lastUsedId = registrationId;
Procedure procInfo = new Procedure(reg.procedure, handler, registrationId);
// Insert new procedure
handler.realm.procedures.put(reg.procedure, procInfo);
if (handler.providedProcedures == null) {
handler.providedProcedures = new HashMap();
handler.pendingInvocations = new HashMap();
}
handler.providedProcedures.put(procInfo.registrationId, procInfo);
RegisteredMessage response = new RegisteredMessage(reg.requestId, procInfo.registrationId);
handler.controller.sendMessage(response, IWampConnectionPromise.Empty);
} else if (msg instanceof UnregisterMessage) {
// The client wants to unregister a procedure
// Verify the message
UnregisterMessage unreg = (UnregisterMessage) msg;
String err = null;
if (!(IdValidator.isValidId(unreg.requestId))
|| !(IdValidator.isValidId(unreg.registrationId))) {
// Client sent an invalid request or registration ID
err = ApplicationError.INVALID_ARGUMENT;
}
Procedure proc = null;
if (err == null) {
if (handler.providedProcedures != null) {
proc = handler.providedProcedures.get(unreg.registrationId);
}
// Check whether the procedure exists AND if the caller is the owner
// If the caller is not the owner it might be an attack, so we don't
// disclose that the procedure exists.
if (proc == null) {
err = ApplicationError.NO_SUCH_REGISTRATION;
}
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(UnregisterMessage.ID, unreg.requestId,
null, err, null, null);
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
return;
}
// Mark pending calls to this procedure as failed
for (Invocation invoc : proc.pendingCalls) {
handler.pendingInvocations.remove(invoc.invocationRequestId);
if (invoc.caller.state == RouterHandlerState.Open) {
ErrorMessage errMsg = new ErrorMessage(CallMessage.ID, invoc.callRequestId,
null, ApplicationError.NO_SUCH_PROCEDURE, null, null);
invoc.caller.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
}
}
proc.pendingCalls.clear();
// Remove the procedure from the realm and the handler
handler.realm.procedures.remove(proc.procName);
handler.providedProcedures.remove(proc.registrationId);
if (handler.providedProcedures.size() == 0) {
handler.providedProcedures = null;
handler.pendingInvocations = null;
}
// Send the acknowledge
UnregisteredMessage response = new UnregisteredMessage(unreg.requestId);
handler.controller.sendMessage(response, IWampConnectionPromise.Empty);
} else if (msg instanceof SubscribeMessage) {
// The client wants to subscribe to a procedure
// Verify the message
SubscribeMessage sub = (SubscribeMessage) msg;
String err = null;
// Find subscription match type
SubscriptionFlags flags = SubscriptionFlags.Exact;
if (sub.options != null) {
JsonNode match = sub.options.get("match");
if (match != null) {
String matchValue = match.asText();
if ("prefix".equals(matchValue)) {
flags = SubscriptionFlags.Prefix;
} else if ("wildcard".equals(matchValue)) {
flags = SubscriptionFlags.Wildcard;
}
}
}
if (flags == SubscriptionFlags.Exact) {
if (!UriValidator.tryValidate(sub.topic, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
} else if (flags == SubscriptionFlags.Prefix) {
if (!UriValidator.tryValidatePrefix(sub.topic, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
} else if (flags == SubscriptionFlags.Wildcard) {
if (!UriValidator.tryValidateWildcard(sub.topic, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
}
if (err == null && !(IdValidator.isValidId(sub.requestId))) {
// Client sent an invalid request ID
err = ApplicationError.INVALID_ARGUMENT;
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(SubscribeMessage.ID, sub.requestId,
null, err, null, null);
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
return;
}
// Create a new subscription map for the client if it was not subscribed before
if (handler.subscriptionsById == null) {
handler.subscriptionsById = new HashMap();
}
// Search if a subscription from any client on the realm to this topic exists
Map subscriptionMap = handler.realm.subscriptionsByFlags.get(flags);
Subscription subscription = subscriptionMap.get(sub.topic);
if (subscription == null) {
// No client was subscribed to this URI up to now
// Create a new subscription id
long subscriptionId = IdGenerator.newLinearId(handler.realm.lastUsedSubscriptionId,
handler.realm.subscriptionsById);
handler.realm.lastUsedSubscriptionId = subscriptionId;
// Create and add the new subscription
subscription = new Subscription(sub.topic, flags, subscriptionId);
subscriptionMap.put(sub.topic, subscription);
handler.realm.subscriptionsById.put(subscriptionId, subscription);
}
// We check if the client is already subscribed to this topic by trying to add the
// new client as a receiver. If the client is already a receiver we do nothing
// (already subscribed and already stored in handler.subscriptionsById). Calling
// add to check and add is more efficient than checking with contains first.
// If the client was already subscribed this will return the same subscriptionId
// than as for the last subscription.
// See discussion in https://groups.google.com/forum/#!topic/wampws/kC878Ngc9Z0
if (subscription.subscribers.add(handler)) {
// Add the subscription on the client
handler.subscriptionsById.put(subscription.subscriptionId, subscription);
}
SubscribedMessage response = new SubscribedMessage(sub.requestId, subscription.subscriptionId);
handler.controller.sendMessage(response, IWampConnectionPromise.Empty);
} else if (msg instanceof UnsubscribeMessage) {
// The client wants to cancel a subscription
// Verify the message
UnsubscribeMessage unsub = (UnsubscribeMessage) msg;
String err = null;
if (!(IdValidator.isValidId(unsub.requestId))
|| !(IdValidator.isValidId(unsub.subscriptionId))) {
// Client sent an invalid request or registration ID
err = ApplicationError.INVALID_ARGUMENT;
}
Subscription s = null;
if (err == null) {
// Check whether such a subscription exists and fetch the topic name
if (handler.subscriptionsById != null) {
s = handler.subscriptionsById.get(unsub.subscriptionId);
}
if (s == null) {
err = ApplicationError.NO_SUCH_SUBSCRIPTION;
}
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(UnsubscribeMessage.ID, unsub.requestId,
null, err, null, null);
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
return;
}
// Remove the channel as an receiver from the subscription
s.subscribers.remove(handler);
// Remove the subscription from the handler
handler.subscriptionsById.remove(s.subscriptionId);
if (handler.subscriptionsById.isEmpty()) {
handler.subscriptionsById = null;
}
// Remove the subscription from the realm if no subscriber is left
if (s.subscribers.isEmpty()) {
handler.realm.subscriptionsByFlags.get(s.flags).remove(s.topic);
handler.realm.subscriptionsById.remove(s.subscriptionId);
}
// Send the acknowledge
UnsubscribedMessage response = new UnsubscribedMessage(unsub.requestId);
handler.controller.sendMessage(response, IWampConnectionPromise.Empty);
} else if (msg instanceof PublishMessage) {
// The client wants to publish something to all subscribers (apart from himself)
PublishMessage pub = (PublishMessage) msg;
// Check whether the client wants an acknowledgement for the publication
// Default is no
boolean sendAcknowledge = false;
JsonNode ackOption = pub.options.get("acknowledge");
if (ackOption != null && ackOption.asBoolean() == true)
sendAcknowledge = true;
String err = null;
if (!UriValidator.tryValidate(pub.topic, handler.realm.config.useStrictUriValidation)) {
// Client sent an invalid URI
err = ApplicationError.INVALID_URI;
}
if (err == null && !(IdValidator.isValidId(pub.requestId))) {
// Client sent an invalid request ID
err = ApplicationError.INVALID_ARGUMENT;
}
if (err != null) { // If we have an error send that to the client
ErrorMessage errMsg = new ErrorMessage(PublishMessage.ID, pub.requestId,
null, err, null, null);
if (sendAcknowledge) {
handler.controller.sendMessage(errMsg, IWampConnectionPromise.Empty);
}
return;
}
long publicationId = IdGenerator.newRandomId(null); // Store that somewhere?
// Get the subscriptions for this topic on the realm
Subscription exactSubscription = handler.realm.subscriptionsByFlags.get(SubscriptionFlags.Exact).get(pub.topic);
if (exactSubscription != null) {
publishEvent(handler, pub, publicationId, exactSubscription);
}
Map prefixSubscriptionMap = handler.realm.subscriptionsByFlags.get(SubscriptionFlags.Prefix);
for (Subscription prefixSubscription : prefixSubscriptionMap.values()) {
if (pub.topic.startsWith(prefixSubscription.topic)) {
publishEvent(handler, pub, publicationId, prefixSubscription);
}
}
Map wildcardSubscriptionMap = handler.realm.subscriptionsByFlags.get(SubscriptionFlags.Wildcard);
String[] components = pub.topic.split("\\.", -1);
for (Subscription wildcardSubscription : wildcardSubscriptionMap.values()) {
boolean matched = true;
if (components.length == wildcardSubscription.components.length) {
for (int i=0; i < components.length; i++) {
if (wildcardSubscription.components[i].length() > 0
&& !components[i].equals(wildcardSubscription.components[i])) {
matched = false;
break;
}
}
}else
matched = false;
if (matched) {
publishEvent(handler, pub, publicationId, wildcardSubscription);
}
}
if (sendAcknowledge) {
PublishedMessage response = new PublishedMessage(pub.requestId, publicationId);
handler.controller.sendMessage(response, IWampConnectionPromise.Empty);
}
}
}
private void publishEvent(ClientHandler publisher, PublishMessage pub, long publicationId, Subscription subscription){
ObjectNode details = null;
if (subscription.flags != SubscriptionFlags.Exact) {
details = objectMapper.createObjectNode();
details.put("topic", pub.topic);
}
EventMessage ev = new EventMessage(subscription.subscriptionId, publicationId,
details, pub.arguments, pub.argumentsKw);
for (ClientHandler receiver : subscription.subscribers) {
if (receiver == publisher ) { // Potentially skip the publisher
boolean skipPublisher = true;
if (pub.options != null) {
JsonNode excludeMeNode = pub.options.get("exclude_me");
if (excludeMeNode != null) {
skipPublisher = excludeMeNode.asBoolean(true);
}
}
if (skipPublisher) continue;
}
// Publish the event to the subscriber
receiver.controller.sendMessage(ev, IWampConnectionPromise.Empty);
}
}
private void onMessageFromUnregisteredChannel(ClientHandler channelHandler, WampMessage msg)
{
// Only HELLO is allowed when a channel is not registered
if (!(msg instanceof HelloMessage)) {
// Close the connection
closePassiveClient(channelHandler);
return;
}
HelloMessage hello = (HelloMessage) msg;
String errorMsg = null;
Realm realm = null;
if (!UriValidator.tryValidate(hello.realm, false)) {
errorMsg = ApplicationError.INVALID_URI;
} else {
realm = realms.get(hello.realm);
if (realm == null) {
errorMsg = ApplicationError.NO_SUCH_REALM;
}
}
if (errorMsg != null) {
AbortMessage abort = new AbortMessage(null, errorMsg);
channelHandler.controller.sendMessage(abort, IWampConnectionPromise.Empty);
return;
}
Set roles = new HashSet();
boolean hasUnsupportedRoles = false;
JsonNode n = hello.details.get("roles");
if (n != null && n.isObject()) {
ObjectNode rolesNode = (ObjectNode) n;
Iterator roleKeys = rolesNode.fieldNames();
while (roleKeys.hasNext()) {
WampRoles role = WampRoles.fromString(roleKeys.next());
if (!SUPPORTED_CLIENT_ROLES.contains(role)) hasUnsupportedRoles = true;
if (role != null) roles.add(role);
}
}
if (roles.size() == 0 || hasUnsupportedRoles) {
AbortMessage abort = new AbortMessage(null, ApplicationError.NO_SUCH_ROLE);
channelHandler.controller.sendMessage(abort, IWampConnectionPromise.Empty);
return;
}
long sessionId = IdGenerator.newRandomId(realm.channelsBySessionId);
// Include the channel into the realm
realm.includeChannel(channelHandler, sessionId, roles);
// Remove the channel from the idle channel list - It is no longer idle
idleChannels.remove(channelHandler.controller);
// Respond with the WELCOME message
WelcomeMessage welcome = new WelcomeMessage(channelHandler.sessionId, realm.welcomeDetails);
channelHandler.controller.sendMessage(welcome, IWampConnectionPromise.Empty);
}
private void closeActiveClient(ClientHandler channel, WampMessage closeMessage) {
if (channel == null) return;
channel.realm.removeChannel(channel, true);
channel.markAsClosed();
if (channel.controller != null) {
if (closeMessage != null)
channel.controller.sendMessage(closeMessage, IWampConnectionPromise.Empty);
closeConnection(channel.controller, true);
}
}
private void closePassiveClient(ClientHandler channelHandler) {
idleChannels.remove(channelHandler.controller);
channelHandler.markAsClosed();
closeConnection(channelHandler.controller, false);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy