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

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; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy