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

org.cometd.oort.Oort Maven / Gradle / Ivy

There is a newer version: 8.0.6
Show newest version
/*
 * Copyright (c) 2008-2019 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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.BayeuxServer.Extension;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage.Mutable;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.client.ext.AckExtension;
import org.cometd.client.http.jetty.JettyHttpClientTransport;
import org.cometd.client.transport.ClientTransport;
import org.cometd.client.websocket.javax.WebSocketTransport;
import org.cometd.common.JSONContext;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.ext.AcknowledgedMessagesExtension;
import org.cometd.server.ext.BinaryExtension;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.DumpableCollection;
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: {@code 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 */ @ManagedObject("CometD cloud node") public class Oort extends ContainerLifeCycle { 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_ID_FIELD = "oortId"; public static final String EXT_OORT_SECRET_FIELD = "oortSecret"; public static final String EXT_COMET_URL_FIELD = "cometURL"; public static final String EXT_OORT_ALIAS_URL_FIELD = "oortAliasURL"; public static final String OORT_CLOUD_CHANNEL = "/oort/cloud"; public static final String OORT_SERVICE_CHANNEL = "/service/oort"; static final String COMET_URL_ATTRIBUTE = EXT_OORT_FIELD + "." + EXT_COMET_URL_FIELD; private final ConcurrentMap _channels = new ConcurrentHashMap<>(); private final CopyOnWriteArrayList _cometListeners = new CopyOnWriteArrayList<>(); private final ServerChannel.MessageListener _cloudListener = new CloudListener(); private final List _transportFactories = new ArrayList<>(); private final BayeuxServer _bayeux; private final String _url; private final String _id; private final Logger _logger; private final LocalSession _oortSession; private final OortMembership _membership; private ScheduledExecutorService _scheduler; private String _secret; private boolean _ackExtensionEnabled = true; private Extension _ackExtension; private boolean _binaryExtensionEnabled; private Extension _serverBinaryExtension; private ClientSession.Extension _binaryExtension; private JSONContext.Client _jsonContext; public Oort(BayeuxServer bayeux, String url) { _bayeux = bayeux; _url = url; _id = UUID.randomUUID().toString(); _logger = LoggerFactory.getLogger(getClass().getName() + "." + replacePunctuation(_url, '_')); _oortSession = bayeux.newLocalSession("oort"); _membership = new OortMembership(this); addBean(_membership); _secret = Long.toHexString(new SecureRandom().nextLong()); } @Override protected void doStart() throws Exception { ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); scheduler.setRemoveOnCancelPolicy(true); _scheduler = scheduler; if (_transportFactories.isEmpty()) { _transportFactories.add(new WebSocketTransport.Factory()); _transportFactories.add(new JettyHttpClientTransport.Factory(new HttpClient())); } for (ClientTransport.Factory factory : _transportFactories) { addBean(factory); } if (isAckExtensionEnabled()) { boolean present = false; for (Extension extension : _bayeux.getExtensions()) { if (extension instanceof AcknowledgedMessagesExtension) { present = true; break; } } if (!present) { _bayeux.addExtension(_ackExtension = new AcknowledgedMessagesExtension()); } } if (isBinaryExtensionEnabled()) { _oortSession.addExtension(_binaryExtension = new org.cometd.client.ext.BinaryExtension()); boolean present = false; for (Extension extension : _bayeux.getExtensions()) { if (extension instanceof BinaryExtension) { present = true; break; } } if (!present) { _bayeux.addExtension(_serverBinaryExtension = new BinaryExtension()); } } ServerChannel oortCloudChannel = _bayeux.createChannelIfAbsent(OORT_CLOUD_CHANNEL).getReference(); oortCloudChannel.addAuthorizer(GrantAuthorizer.GRANT_ALL); oortCloudChannel.addListener(_cloudListener); _oortSession.handshake(); super.doStart(); } @Override protected void doStop() throws Exception { super.doStop(); _oortSession.disconnect(); _oortSession.removeExtension(_binaryExtension); ServerChannel channel = _bayeux.getChannel(OORT_CLOUD_CHANNEL); if (channel != null) { channel.removeListener(_cloudListener); channel.removeAuthorizer(GrantAuthorizer.GRANT_ALL); } Extension binaryExtension = _serverBinaryExtension; _serverBinaryExtension = null; if (binaryExtension != null) { _bayeux.removeExtension(binaryExtension); } Extension ackExtension = _ackExtension; _ackExtension = null; if (ackExtension != null) { _bayeux.removeExtension(ackExtension); } _channels.clear(); _scheduler.shutdown(); for (ClientTransport.Factory factory : _transportFactories) { removeBean(factory); } } protected ScheduledExecutorService getScheduler() { return _scheduler; } @ManagedAttribute(value = "The BayeuxServer of this Oort", readonly = true) public BayeuxServer getBayeuxServer() { return _bayeux; } /** * @return the public absolute URL of the Oort CometD server */ @ManagedAttribute(value = "The URL of this Oort", readonly = true) public String getURL() { return _url; } @ManagedAttribute(value = "The unique ID of this Oort", readonly = true) public String getId() { return _id; } @ManagedAttribute("The secret of this Oort") public String getSecret() { return _secret; } public void setSecret(String secret) { this._secret = secret; } @ManagedAttribute("Whether the acknowledgement extension is enabled") public boolean isAckExtensionEnabled() { return _ackExtensionEnabled; } public void setAckExtensionEnabled(boolean value) { _ackExtensionEnabled = value; } @ManagedAttribute("Whether the binary extension is enabled") public boolean isBinaryExtensionEnabled() { return _binaryExtensionEnabled; } public void setBinaryExtensionEnabled(boolean value) { _binaryExtensionEnabled = value; } public JSONContext.Client getJSONContextClient() { return _jsonContext; } public void setJSONContextClient(JSONContext.Client jsonContext) { _jsonContext = jsonContext; } public List getClientTransportFactories() { return _transportFactories; } public void setClientTransportFactories(List factories) { _transportFactories.clear(); _transportFactories.addAll(factories); } /** *

Connects (if not already connected) and observes another Oort instance * (identified by the given URL) via a {@link OortComet} instance.

* * @param cometURL the Oort URL to observe * @return The {@link OortComet} instance associated to the Oort instance identified by the URL * or null if the given Oort URL represent this Oort instance */ public OortComet observeComet(String cometURL) { return _membership.observeComet(cometURL); } protected OortComet newOortComet(String cometURL) { Map options = new HashMap<>(2); options.put(ClientTransport.SCHEDULER_OPTION, _scheduler); JSONContext.Client jsonContext = getJSONContextClient(); if (jsonContext != null) { options.put(ClientTransport.JSON_CONTEXT_OPTION, jsonContext); } String maxMessageSizeOption = ClientTransport.MAX_MESSAGE_SIZE_OPTION; Object option = _bayeux.getOption(maxMessageSizeOption); if (option != null) { options.put(maxMessageSizeOption, option); } maxMessageSizeOption = WebSocketTransport.PREFIX + "." + ClientTransport.MAX_MESSAGE_SIZE_OPTION; option = _bayeux.getOption(maxMessageSizeOption); if (option != null) { options.put(maxMessageSizeOption, option); } String idleTimeoutOption = WebSocketTransport.PREFIX + "." + WebSocketTransport.IDLE_TIMEOUT_OPTION; option = _bayeux.getOption(idleTimeoutOption); if (option != null) { options.put(idleTimeoutOption, option); } String maxNetworkDelayOption = ClientTransport.MAX_NETWORK_DELAY_OPTION; option = _bayeux.getOption(maxNetworkDelayOption); if (option != null) { options.put(maxNetworkDelayOption, option); } List transports = new ArrayList<>(); for (ClientTransport.Factory factory : getClientTransportFactories()) { transports.add(factory.newClientTransport(cometURL, options)); } ClientTransport transport = transports.get(0); int size = transports.size(); ClientTransport[] otherTransports = transports.subList(1, size).toArray(new ClientTransport[0]); return newOortComet(cometURL, transport, otherTransports); } protected OortComet newOortComet(String cometURL, ClientTransport transport, ClientTransport[] otherTransports) { return new OortComet(this, cometURL, getScheduler(), transport, otherTransports); } protected void configureOortComet(OortComet oortComet) { if (isAckExtensionEnabled()) { boolean present = false; for (ClientSession.Extension extension : oortComet.getExtensions()) { if (extension instanceof AckExtension) { present = true; break; } } if (!present) { oortComet.addExtension(new AckExtension()); } } if (isBinaryExtensionEnabled()) { boolean present = false; for (ClientSession.Extension extension : oortComet.getExtensions()) { if (extension instanceof org.cometd.client.ext.BinaryExtension) { present = true; break; } } if (!present) { oortComet.addExtension(new org.cometd.client.ext.BinaryExtension()); } } } protected String encodeSecret(String secret) { try { MessageDigest digest = MessageDigest.getInstance("SHA-512"); byte[] bytes = digest.digest(secret.getBytes(StandardCharsets.UTF_8)); return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); } catch (Exception x) { throw new IllegalArgumentException(x); } } OortComet createOortComet(String cometURL) { return _membership.createOortComet(cometURL); } void connectComet(OortComet comet) { connectComet(comet, newOortHandshakeFields(comet.getURL(), null)); } protected void connectComet(OortComet comet, Map fields) { comet.handshake(fields); } public OortComet deobserveComet(String cometURL) { return _membership.deobserveComet(cometURL); } /** * @return the set of known Oort comet servers URLs. */ @ManagedAttribute(value = "URLs of known Oorts in the cluster", readonly = true) public Set getKnownComets() { return _membership.getKnownComets(); } /** * @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 _membership.getComet(cometURL); } /** * @param cometURL the URL of a Oort comet * @return the OortComet instance connecting or connected with the Oort comet with the given URL */ protected OortComet findComet(String cometURL) { return _membership.findComet(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 */ @ManagedOperation(value = "Observes the given channel", impact = "ACTION") public void observeChannel(@Name(value = "channel", description = "The channel to observe") String channelName) { if (_logger.isDebugEnabled()) { _logger.debug("Observing channel {}", channelName); } if (!ChannelId.isBroadcast(channelName)) { throw new IllegalArgumentException("Channel " + channelName + " cannot be observed because is not a broadcast channel"); } if (_channels.putIfAbsent(channelName, Boolean.TRUE) == null) { Set observedChannels = getObservedChannels(); _membership.observeChannels(observedChannels); } } @ManagedOperation(value = "Deobserves the given channel", impact = "ACTION") public void deobserveChannel(@Name(value = "channel", description = "The channel to deobserve") String channelId) { if (_channels.remove(channelId) != null) { _membership.deobserveChannel(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) { if (session == null) { return false; } String id = session.getId(); if (id.equals(_oortSession.getId())) { return true; } if (_membership.containsServerSession(session)) { return true; } return session.getAttribute(COMET_URL_ATTRIBUTE) != null; } /** * @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); } protected Map newOortHandshakeFields(String cometURL, String oortAliasURL) { Map fields = new HashMap<>(1); Map ext = new HashMap<>(1); fields.put(Message.EXT_FIELD, ext); Map oortExt = new HashMap<>(4); ext.put(EXT_OORT_FIELD, oortExt); oortExt.put(EXT_OORT_URL_FIELD, getURL()); oortExt.put(EXT_OORT_ID_FIELD, getId()); String b64Secret = encodeSecret(getSecret()); oortExt.put(EXT_OORT_SECRET_FIELD, b64Secret); oortExt.put(EXT_COMET_URL_FIELD, cometURL); if (oortAliasURL != null) { oortExt.put(EXT_OORT_ALIAS_URL_FIELD, oortAliasURL); } return fields; } /** * @param oortURL the comet URL to check for connection * @return whether the given comet is connected to this comet */ protected boolean isCometConnected(String oortURL) { return _membership.isCometConnected(oortURL); } /** * Registers the given listener to be notified of comet events. * * @param listener the listener to add * @see #removeCometListener(CometListener) */ public void addCometListener(CometListener listener) { _cometListeners.add(listener); } /** * Deregisters the given listener from being notified of comet events. * * @param listener the listener to remove * @see #addCometListener(CometListener) */ public void removeCometListener(CometListener listener) { _cometListeners.remove(listener); } /** * Deregisters all comet listeners. * * @see #addCometListener(CometListener) * @see #removeCometListener(CometListener) */ public void removeCometListeners() { _cometListeners.clear(); } void notifyCometJoined(String remoteOortId, String remoteOortURL) { if (_logger.isDebugEnabled()) { _logger.debug("Comet joined: {}|{}", remoteOortId, remoteOortURL); } CometListener.Event event = new CometListener.Event(this, remoteOortId, remoteOortURL); for (CometListener cometListener : _cometListeners) { try { cometListener.cometJoined(event); } catch (Throwable x) { _logger.info("Exception while invoking listener " + cometListener, x); } } } void notifyCometLeft(String remoteOortId, String remoteOortURL) { if (_logger.isDebugEnabled()) { _logger.debug("Comet left: {}|{}", remoteOortId, remoteOortURL); } CometListener.Event event = new CometListener.Event(this, remoteOortId, remoteOortURL); for (CometListener cometListener : _cometListeners) { try { cometListener.cometLeft(event); } catch (Throwable x) { _logger.info("Exception while invoking listener " + cometListener, x); } } } protected void joinComets(Message message) { Object data = message.getData(); Object[] array = data instanceof List ? ((List)data).toArray() : (Object[])data; for (Object element : array) { observeComet((String)element); } } public Set getObservedChannels() { return new HashSet<>(_channels.keySet()); } List knownOortIds() { return _membership.knownOortIds(); } /** * @return the oortSession */ public LocalSession getOortSession() { return _oortSession; } protected static String replacePunctuation(String source, char replacement) { String replaced = source.replaceAll("[^\\p{Alnum}]", String.valueOf(replacement)); // Compact multiple consecutive replacement chars return replaced.replaceAll("(" + replacement + ")\\1+", "$1"); } @Override public void dump(Appendable out, String indent) throws IOException { dumpObjects(out, indent, new DumpableCollection("observed channels", _channels.keySet())); } @Override public String toString() { return String.format("%s[%s]", getClass().getSimpleName(), getURL()); } /** *

This listener handles messages sent to {@code /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 {@code /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 { @Override public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message) { if (!from.isLocalSession()) { joinComets(message); } return true; } } /** *

Listener interface that gets notified of comet events, that is when a new * comet joins the cloud or when a comet leaves the cloud.

*

If oortA and oortB form a cloud, when oortC joins to the cloud, both * oortA and oortB receive one event of type "comet joined", signaling that oortC * joined the cloud.

*

If, later, oortB leaves the cloud, then both oortA and oortC receive one * event of type "comet left" signaling that oortB left the cloud.

*/ public interface CometListener extends EventListener { /** * Callback method invoked when a new comet joins the cloud * * @param event the comet event */ public default void cometJoined(Event event) { } /** * Callback method invoked when a comet leaves the cloud * * @param event the comet event */ public default void cometLeft(Event event) { } /** * Comet event object delivered to {@link CometListener} methods. */ public static class Event extends EventObject { private final String cometId; private final String cometURL; public Event(Oort source, String cometId, String cometURL) { super(source); this.cometId = cometId; this.cometURL = cometURL; } /** * @return the local Oort object */ public Oort getOort() { return (Oort)getSource(); } /** * @return the ID of the comet that generated the event */ public String getCometId() { return cometId; } /** * @return the URL of the comet that generated the event */ public String getCometURL() { return cometURL; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy