org.cometd.oort.Oort Maven / Gradle / Ivy
/*
* Copyright (c) 2010 the original author or authors.
*
* 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.cometd.oort;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.BayeuxServer.Extension;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerMessage.Mutable;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.common.HashMapMessage;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.component.AggregateLifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.websocket.WebSocketClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Oort is the cluster manager that links one CometD server to a set of other CometD servers.
* The Oort instance is created and configured by either {@link OortMulticastConfigServlet} or
* {@link OortStaticConfigServlet}.
* This class maintains a collection of {@link OortComet} instances to each
* CometD server, created by calls to {@link #observeComet(String)}.
* The key configuration parameter is the Oort URL, which is
* full public URL of the CometD servlet to which the Oort instance is bound,
* for example: http://myserver:8080/context/cometd
.
* Oort instances can be configured with a shared {@link #setSecret(String) secret}, which allows
* the Oort instance to distinguish handshakes coming from remote clients from handshakes coming from
* other Oort comets: the firsts may be subject to a stricter authentication policy than the seconds.
*
* @see OortMulticastConfigServlet
* @see OortStaticConfigServlet
*/
public class Oort extends AggregateLifeCycle
{
public final static String OORT_ATTRIBUTE = Oort.class.getName();
public static final String EXT_OORT_FIELD = "org.cometd.oort";
public static final String EXT_OORT_URL_FIELD = "oortURL";
public static final String EXT_OORT_SECRET_FIELD = "oortSecret";
public static final String EXT_COMET_URL_FIELD = "cometURL";
public static final String OORT_CLOUD_CHANNEL = "/oort/cloud";
private static final String COMET_URL_ATTRIBUTE = EXT_OORT_FIELD + "." + EXT_COMET_URL_FIELD;
private final ConcurrentMap _knownComets = new ConcurrentHashMap();
private final Map _incomingComets = new ConcurrentHashMap();
private final ConcurrentMap _channels = new ConcurrentHashMap();
private final Extension _oortExtension = new OortExtension();
private ServerChannel.MessageListener _cloudListener = new CloudListener();
private final BayeuxServer _bayeux;
private final String _url;
private final Logger _logger;
private final ThreadPool _threadPool;
private final HttpClient _httpClient;
private final WebSocketClientFactory _wsFactory;
private final LocalSession _oortSession;
private String _secret;
private boolean _debug;
private boolean _clientDebug;
public Oort(BayeuxServer bayeux, String url)
{
_bayeux = bayeux;
_url = url;
_logger = LoggerFactory.getLogger(getClass().getName() + "-" + _url);
_debug = String.valueOf(BayeuxServerImpl.DEBUG_LOG_LEVEL).equals(bayeux.getOption(BayeuxServerImpl.LOG_LEVEL));
_threadPool = new QueuedThreadPool();
addBean(_threadPool);
_httpClient = new HttpClient();
_httpClient.setThreadPool(_threadPool);
addBean(_httpClient);
_wsFactory=new WebSocketClientFactory(_threadPool);
addBean(_wsFactory);
_oortSession = bayeux.newLocalSession("oort");
_secret = Long.toHexString(new SecureRandom().nextLong());
}
@Override
protected void doStart() throws Exception
{
super.doStart();
_bayeux.addExtension(_oortExtension);
_bayeux.createIfAbsent(OORT_CLOUD_CHANNEL, new ConfigurableServerChannel.Initializer()
{
public void configureChannel(ConfigurableServerChannel channel)
{
channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
_cloudListener = new CloudListener();
channel.addListener(_cloudListener);
}
});
_oortSession.handshake();
}
@Override
protected void doStop() throws Exception
{
_oortSession.disconnect();
for (OortComet comet : _knownComets.values())
comet.disconnect(1000);
_knownComets.clear();
_incomingComets.clear();
_channels.clear();
ServerChannel oortCloudChannel = _bayeux.getChannel(OORT_CLOUD_CHANNEL);
if (oortCloudChannel != null)
{
oortCloudChannel.removeListener(_cloudListener);
oortCloudChannel.removeAuthorizer(GrantAuthorizer.GRANT_ALL);
}
_bayeux.removeExtension(_oortExtension);
super.doStop();
}
public BayeuxServer getBayeuxServer()
{
return _bayeux;
}
/**
* @return the public absolute URL of the Oort CometD server
*/
public String getURL()
{
return _url;
}
public String getSecret()
{
return _secret;
}
public void setSecret(String secret)
{
this._secret = secret;
}
public boolean isDebugEnabled()
{
return _debug;
}
public void setDebugEnabled(boolean debug)
{
_debug = debug;
}
private void debug(String message, Object... args)
{
if (isDebugEnabled())
_logger.info(message, args);
else
_logger.debug(message, args);
}
public boolean isClientDebugEnabled()
{
return _clientDebug;
}
public void setClientDebugEnabled(boolean clientDebugEnabled)
{
_clientDebug = clientDebugEnabled;
for (OortComet comet : _knownComets.values())
comet.setDebugEnabled(clientDebugEnabled);
}
/**
* Connects (if not already connected) and observes another CometD server
* (identified by the given URL) via a {@link OortComet} instance.
*
* @param cometURL the CometD url to observe
* @return The {@link OortComet} instance associated to the CometD server identified by the URL
*/
public OortComet observeComet(String cometURL)
{
try
{
URI uri = new URI(cometURL);
if (uri.getScheme() == null)
throw new IllegalArgumentException("Missing protocol in comet URL " + cometURL);
if (uri.getHost() == null)
throw new IllegalArgumentException("Missing host in comet URL " + cometURL);
}
catch (URISyntaxException x)
{
throw new IllegalArgumentException(x);
}
if (_url.equals(cometURL))
return null;
OortComet comet = newOortComet(cometURL);
OortComet existing = _knownComets.putIfAbsent(cometURL, comet);
if (existing != null)
return existing;
debug("Connecting to comet {}", cometURL);
String b64Secret = encodeSecret(getSecret());
Message.Mutable fields = new HashMapMessage();
Map ext = fields.getExt(true);
Map oortExt = new HashMap(3);
ext.put(EXT_OORT_FIELD, oortExt);
oortExt.put(EXT_OORT_URL_FIELD, getURL());
oortExt.put(EXT_OORT_SECRET_FIELD, b64Secret);
oortExt.put(EXT_COMET_URL_FIELD, cometURL);
connectComet(comet, fields);
return comet;
}
protected OortComet newOortComet(String cometURL)
{
return new OortComet(this, cometURL);
}
protected String encodeSecret(String secret)
{
try
{
MessageDigest digest = MessageDigest.getInstance("SHA-1");
return new String(B64Code.encode(digest.digest(secret.getBytes("UTF-8"))));
}
catch (Exception x)
{
throw new IllegalArgumentException(x);
}
}
protected void connectComet(OortComet comet, Message.Mutable fields)
{
comet.handshake(fields);
}
public OortComet deobserveComet(String cometURL)
{
if (_url.equals(cometURL))
return null;
OortComet comet = _knownComets.remove(cometURL);
if (comet != null)
{
debug("Disconnecting from comet {}", cometURL);
comet.disconnect();
}
return comet;
}
/**
* Callback method invoked when a comet joins this Oort instance and communicates
* the other comets linked to it, so that this Oort instance can connect to those
* comets as well.
*
* @param comets the Oort server URLs to connect to
*/
protected void cometsJoined(Set comets)
{
for (String comet : comets)
{
if (!_url.equals(comet) && !_knownComets.containsKey(comet))
observeComet(comet);
}
}
/**
* @return the set of known Oort comet servers URLs.
*/
public Set getKnownComets()
{
return new HashSet(_knownComets.keySet());
}
/**
* @param cometURL the URL of a Oort comet
* @return the OortComet instance connected with the Oort comet with the given URL
*/
public OortComet getComet(String cometURL)
{
return _knownComets.get(cometURL);
}
/**
* Observes the given channel, registering to receive messages from
* the Oort comets connected to this Oort instance.
* Once observed, all {@link OortComet} instances subscribe
* to the channel and will repeat any messages published to
* the local channel (with loop prevention), so that the
* messages are distributed to all Oort comet servers.
*
* @param channelName the channel to observe
*/
public void observeChannel(String channelName)
{
ChannelId channelId = new ChannelId(channelName);
if (channelId.isMeta() || channelId.isService())
throw new IllegalArgumentException("Channel " + channelName + " cannot be observed because is not a broadcast channel");
if (_channels.putIfAbsent(channelName, Boolean.TRUE) == null)
{
Set observedChannels = getObservedChannels();
for (OortComet comet : _knownComets.values())
comet.subscribe(observedChannels);
}
}
public void deobserveChannel(String channelId)
{
if (_channels.remove(channelId) != null)
{
for (OortComet comet : _knownComets.values())
comet.unsubscribe(channelId);
}
}
/**
* @param session the server session to test
* @return whether the given server session is one of those created by the Oort internal working
* @see #isOortHandshake(Message)
*/
public boolean isOort(ServerSession session)
{
String id = session.getId();
if (id.equals(_oortSession.getId()))
return true;
if (_incomingComets.containsKey(id))
return true;
for (OortComet oc : _knownComets.values())
{
if (id.equals(oc.getId()))
return true;
}
return false;
}
/**
* @param handshake the handshake message to test
* @return whether the given handshake message is coming from another Oort comet
* that has been configured with the same {@link #setSecret(String) secret}
* @see #isOort(ServerSession)
*/
public boolean isOortHandshake(Message handshake)
{
if (!Channel.META_HANDSHAKE.equals(handshake.getChannel()))
return false;
Map ext = handshake.getExt();
if (ext == null)
return false;
Object oortExtObject = ext.get(EXT_OORT_FIELD);
if (!(oortExtObject instanceof Map))
return false;
@SuppressWarnings("unchecked")
Map oortExt = (Map)oortExtObject;
String cometURL = (String)oortExt.get(EXT_COMET_URL_FIELD);
if (!getURL().equals(cometURL))
return false;
String b64RemoteSecret = (String)oortExt.get(EXT_OORT_SECRET_FIELD);
String b64LocalSecret = encodeSecret(getSecret());
return b64LocalSecret.equals(b64RemoteSecret);
}
public String toString()
{
return _url;
}
/**
* Called to register the details of a successful handshake from another Oort comet.
*
* @param cometURL the remote Oort URL
* @param cometSecret the remote Oort secret
* @param session the server session that represent the connection with the remote Oort comet
*/
protected void incomingCometHandshake(String cometURL, String cometSecret, ServerSession session)
{
debug("Incoming comet handshake from comet {} with {}", cometURL, session.getId());
if (!_knownComets.containsKey(cometURL))
{
debug("Comet {} is unknown, establishing connection", cometURL);
observeComet(cometURL);
}
else
{
debug("Comet {} is already known", cometURL);
}
session.setAttribute(COMET_URL_ATTRIBUTE, cometURL);
_incomingComets.put(session.getId(), session);
// Be notified when the remote comet stops
session.addListener(new OortCometDisconnectListener(cometURL));
// Prevent loops in sending/receiving messages
session.addListener(new OortCometLoopListener());
}
/**
* Extension that detects incoming handshakes from other Oort servers.
*
* @see Oort#incomingCometHandshake(String, String, ServerSession)
*/
protected class OortExtension implements Extension
{
public boolean rcv(ServerSession from, Mutable message)
{
return true;
}
public boolean rcvMeta(ServerSession from, Mutable message)
{
return true;
}
public boolean send(ServerSession from, ServerSession to, Mutable message)
{
return true;
}
public boolean sendMeta(ServerSession to, Mutable message)
{
// Skip local sessions
if (to != null && Channel.META_HANDSHAKE.equals(message.getChannel()) && message.isSuccessful())
{
Map ext = message.getAssociated().getExt();
if (ext != null)
{
Object oortExtObject = ext.get(EXT_OORT_FIELD);
if (oortExtObject instanceof Map)
{
@SuppressWarnings("unchecked")
Map oortExt = (Map)oortExtObject;
String cometURL = (String)oortExt.get(EXT_COMET_URL_FIELD);
if (getURL().equals(cometURL))
{
// Read incoming information
String remoteOortURL = (String)oortExt.get(EXT_OORT_URL_FIELD);
String remoteOortSecret = (String)oortExt.get(EXT_OORT_SECRET_FIELD);
incomingCometHandshake(remoteOortURL, remoteOortSecret, to);
}
}
}
}
return true;
}
}
protected void joinComets(String cometURL, Message message)
{
Object data = message.getData();
Object[] array = data instanceof List ? ((List)data).toArray() : (Object[])data;
Set comets = new HashSet();
for (Object o : array)
comets.add(o.toString());
debug("Received comets {} from {}", comets, cometURL);
cometsJoined(comets);
}
/**
* This listener handles messages sent to /oort/cloud
that contains the list of comets
* connected to the Oort that just joined the cloud.
* For example, if comets A and B are connected, and if comets C and D are connected, when connecting
* A and C, a message is sent from A to C on /oort/cloud
containing the comets connected
* to A (in this case B). When C receives this message, it knows it has to connect to B also.
*/
protected class CloudListener implements ServerChannel.MessageListener
{
public boolean onMessage(ServerSession from, ServerChannel channel, Mutable msg)
{
if (!from.isLocalSession())
{
String cometURL = (String)from.getAttribute(COMET_URL_ATTRIBUTE);
joinComets(cometURL, msg);
}
return true;
}
}
public ThreadPool getThreadPool()
{
return _threadPool;
}
public WebSocketClientFactory getWebSocketClientFactory()
{
return _wsFactory;
}
public HttpClient getHttpClient()
{
return _httpClient;
}
protected Logger getLogger()
{
return _logger;
}
public Set getObservedChannels()
{
return new HashSet(_channels.keySet());
}
/**
* @return the oortSession
*/
public LocalSession getOortSession()
{
return _oortSession;
}
/**
* Listener that detect when a server session is removed (means that the remote
* comet disconnected), and disconnects the OortComet associated.
*/
private class OortCometDisconnectListener implements ServerSession.RemoveListener
{
private final String cometURL;
public OortCometDisconnectListener(String cometURL)
{
this.cometURL = cometURL;
}
public void removed(ServerSession session, boolean timeout)
{
ServerSession removed = _incomingComets.remove(session.getId());
if (removed != null)
{
_logger.info("Disconnected from comet {} with session {}", cometURL, removed);
OortComet oortComet = _knownComets.remove(cometURL);
if (oortComet != null)
oortComet.disconnect();
}
}
}
private class OortCometLoopListener implements ServerSession.MessageListener
{
public boolean onMessage(ServerSession to, ServerSession from, ServerMessage message)
{
// Prevent loops by not delivering a message from self or Oort session to remote Oort comets
if (to.getId().equals(from.getId()) || isOort(from))
{
debug("{} --| {} {}", from, to, message);
return false;
}
debug("{} --> {} {}", from, to, message);
return true;
}
}
}