/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses 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 org.apache.activemq.artemis.core.protocol.stomp; import javax.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQBuffers; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException; import org.apache.activemq.artemis.api.core.ICoreMessage; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.core.message.impl.CoreMessage; import org.apache.activemq.artemis.core.protocol.stomp.v10.StompFrameHandlerV10; import org.apache.activemq.artemis.core.protocol.stomp.v12.StompFrameHandlerV12; import org.apache.activemq.artemis.core.remoting.CloseListener; import org.apache.activemq.artemis.core.remoting.FailureListener; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.ServerSession; import org.apache.activemq.artemis.core.server.impl.AddressInfo; import org.apache.activemq.artemis.core.settings.impl.AddressSettings; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Acceptor; import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.ReadyListener; import org.apache.activemq.artemis.utils.ConfigurationHelper; import org.apache.activemq.artemis.utils.ExecutorFactory; import org.apache.activemq.artemis.utils.VersionLoader; import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE; public final class StompConnection implements RemotingConnection { protected static final String CONNECTION_ID_PROP = "__AMQ_CID"; private static final String SERVER_NAME = "ActiveMQ-Artemis/" + VersionLoader.getVersion().getFullVersion() + " ActiveMQ Artemis Messaging Engine"; private final StompProtocolManager manager; private final Connection transportConnection; private String login; private String passcode; private String clientID; //this means login is valid. (stomp connection ok) private boolean valid; private boolean destroyed = false; private final long creationTime; private final Acceptor acceptorUsed; private final List<FailureListener> failureListeners = new CopyOnWriteArrayList<>(); private final List<CloseListener> closeListeners = new CopyOnWriteArrayList<>(); private final Object failLock = new Object(); private boolean dataReceived; private final boolean enableMessageID; private final int minLargeMessageSize; private StompVersions version; private VersionedStompFrameHandler frameHandler; //this means the version negotiation done. private boolean initialized; private FrameEventListener stompListener; private final Object sendLock = new Object(); private final ScheduledExecutorService scheduledExecutorService; private final ExecutorFactory factory; @Override public boolean isSupportReconnect() { return false; } public VersionedStompFrameHandler getStompVersionHandler() { return frameHandler; } public StompFrame decode(ActiveMQBuffer buffer) throws ActiveMQStompException { StompFrame frame = null; try { frame = frameHandler.decode(buffer); } catch (ActiveMQStompException e) { switch (e.getCode()) { case ActiveMQStompException.INVALID_EOL_V10: if (version != null) throw e; frameHandler = new StompFrameHandlerV12(this, scheduledExecutorService, factory); buffer.resetReaderIndex(); frame = decode(buffer); break; case ActiveMQStompException.INVALID_COMMAND: case ActiveMQStompException.UNDEFINED_ESCAPE: frameHandler.onError(e); break; default: throw e; } } return frame; } @Override public boolean isWritable(ReadyListener callback) { return transportConnection.isWritable(callback); } public boolean hasBytes() { return frameHandler.hasBytes(); } StompConnection(final Acceptor acceptorUsed, final Connection transportConnection, final StompProtocolManager manager, final ScheduledExecutorService scheduledExecutorService, final ExecutorFactory factory) { this.scheduledExecutorService = scheduledExecutorService; this.factory = factory; this.transportConnection = transportConnection; this.manager = manager; this.frameHandler = new StompFrameHandlerV10(this, scheduledExecutorService, factory); this.creationTime = System.currentTimeMillis(); this.acceptorUsed = acceptorUsed; this.enableMessageID = ConfigurationHelper.getBooleanProperty(TransportConstants.STOMP_ENABLE_MESSAGE_ID, false, acceptorUsed.getConfiguration()); this.minLargeMessageSize = ConfigurationHelper.getIntProperty(TransportConstants.STOMP_MIN_LARGE_MESSAGE_SIZE, ActiveMQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE, acceptorUsed.getConfiguration()); } @Override public void addFailureListener(final FailureListener listener) { if (listener == null) { throw new IllegalStateException("FailureListener cannot be null"); } failureListeners.add(listener); } @Override public boolean removeFailureListener(final FailureListener listener) { if (listener == null) { throw new IllegalStateException("FailureListener cannot be null"); } return failureListeners.remove(listener); } @Override public void addCloseListener(final CloseListener listener) { if (listener == null) { throw new IllegalStateException("CloseListener cannot be null"); } closeListeners.add(listener); } @Override public boolean removeCloseListener(final CloseListener listener) { if (listener == null) { throw new IllegalStateException("CloseListener cannot be null"); } return closeListeners.remove(listener); } @Override public List<CloseListener> removeCloseListeners() { List<CloseListener> ret = new ArrayList<>(closeListeners); closeListeners.clear(); return ret; } @Override public List<FailureListener> removeFailureListeners() { List<FailureListener> ret = new ArrayList<>(failureListeners); failureListeners.clear(); return ret; } @Override public void setCloseListeners(List<CloseListener> listeners) { closeListeners.clear(); closeListeners.addAll(listeners); } @Override public void setFailureListeners(final List<FailureListener> listeners) { failureListeners.clear(); failureListeners.addAll(listeners); } protected synchronized void setDataReceived() { dataReceived = true; } @Override public synchronized boolean checkDataReceived() { boolean res = dataReceived; dataReceived = false; return res; } // TODO this should take a type - send or receive so it knows whether to check the address or the queue public void checkDestination(String destination) throws ActiveMQStompException { if (!manager.destinationExists(getSession().getCoreSession().removePrefix(SimpleString.toSimpleString(destination)).toString())) { throw BUNDLE.destinationNotExist(destination).setHandler(frameHandler); } } public boolean autoCreateDestinationIfPossible(String queue, RoutingType routingType) throws ActiveMQStompException { boolean result = false; ServerSession session = getSession().getCoreSession(); try { if (manager.getServer().getAddressInfo(SimpleString.toSimpleString(queue)) == null) { AddressSettings addressSettings = manager.getServer().getAddressSettingsRepository().getMatch(queue); if (routingType != null && routingType == RoutingType.MULTICAST && addressSettings.isAutoCreateAddresses()) { session.createAddress(SimpleString.toSimpleString(queue), RoutingType.MULTICAST, true); result = true; } else { if (addressSettings.isAutoCreateAddresses()) { session.createAddress(SimpleString.toSimpleString(queue), RoutingType.ANYCAST, true); result = true; } if (addressSettings.isAutoCreateQueues()) { session.createQueue(SimpleString.toSimpleString(queue), SimpleString.toSimpleString(queue), RoutingType.ANYCAST, null, false, true, true); result = true; } } } } catch (ActiveMQQueueExistsException e) { // ignore } catch (Exception e) { throw new ActiveMQStompException(e.getMessage(), e).setHandler(frameHandler); } return result; } public void checkRoutingSemantics(String destination, RoutingType routingType) throws ActiveMQStompException { AddressInfo addressInfo = manager.getServer().getAddressInfo(getSession().getCoreSession().removePrefix(SimpleString.toSimpleString(destination))); // may be null here if, for example, the management address is being checked if (addressInfo != null) { Set<RoutingType> actualDeliveryModesOfAddress = addressInfo.getRoutingTypes(); if (routingType != null && !actualDeliveryModesOfAddress.contains(routingType)) { throw BUNDLE.illegalSemantics(routingType.toString(), actualDeliveryModesOfAddress.toString()); } } } @Override public ActiveMQBuffer createTransportBuffer(int size) { return ActiveMQBuffers.dynamicBuffer(size); } @Override public void destroy() { synchronized (failLock) { if (destroyed) { return; } } destroyed = true; internalClose(); synchronized (sendLock) { callClosingListeners(); } } public Acceptor getAcceptorUsed() { return acceptorUsed; } private void internalClose() { transportConnection.close(); manager.cleanup(this); } @Override public void fail(final ActiveMQException me) { synchronized (failLock) { if (destroyed) { return; } StompFrame frame = frameHandler.createStompFrame(Stomp.Responses.ERROR); frame.addHeader(Stomp.Headers.Error.MESSAGE, me.getMessage()); sendFrame(frame); destroyed = true; } ActiveMQServerLogger.LOGGER.connectionFailureDetected(me.getMessage(), me.getType()); if (frameHandler != null) { frameHandler.disconnect(); } // Then call the listeners callFailureListeners(me); callClosingListeners(); internalClose(); } @Override public void fail(final ActiveMQException me, String scaleDownTargetNodeID) { fail(me); } @Override public void flush() { } @Override public List<FailureListener> getFailureListeners() { // we do not return the listeners otherwise the remoting service // would NOT destroy the connection. return Collections.emptyList(); } @Override public Object getID() { return transportConnection.getID(); } @Override public String getRemoteAddress() { return transportConnection.getRemoteAddress(); } @Override public long getCreationTime() { return creationTime; } @Override public Connection getTransportConnection() { return transportConnection; } @Override public boolean isClient() { return false; } @Override public boolean isDestroyed() { return destroyed; } @Override public void bufferReceived(Object connectionID, ActiveMQBuffer buffer) { manager.handleBuffer(this, buffer); } public String getLogin() { return login; } public String getPasscode() { return passcode; } public void setClientID(String clientID) { this.clientID = clientID; } public String getClientID() { return clientID; } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } private void callFailureListeners(final ActiveMQException me) { final List<FailureListener> listenersClone = new ArrayList<>(failureListeners); for (final FailureListener listener : listenersClone) { try { listener.connectionFailed(me, false); } catch (final Throwable t) { // Failure of one listener to execute shouldn't prevent others // from // executing ActiveMQServerLogger.LOGGER.errorCallingFailureListener(t); } } } private void callClosingListeners() { final List<CloseListener> listenersClone = new ArrayList<>(closeListeners); for (final CloseListener listener : listenersClone) { try { listener.connectionClosed(); } catch (final Throwable t) { // Failure of one listener to execute shouldn't prevent others // from // executing ActiveMQServerLogger.LOGGER.errorCallingFailureListener(t); } } } /* * accept-version value takes form of "v1,v2,v3..." * we need to return the highest supported version */ public void negotiateVersion(StompFrame frame) throws ActiveMQStompException { String acceptVersion = frame.getHeader(Stomp.Headers.ACCEPT_VERSION); if (acceptVersion == null) { this.version = StompVersions.V1_0; } else { StringTokenizer tokenizer = new StringTokenizer(acceptVersion, ","); Set<String> requestVersions = new HashSet<>(tokenizer.countTokens()); while (tokenizer.hasMoreTokens()) { requestVersions.add(tokenizer.nextToken()); } if (requestVersions.contains(StompVersions.V1_2.toString())) { this.version = StompVersions.V1_2; } else if (requestVersions.contains(StompVersions.V1_1.toString())) { this.version = StompVersions.V1_1; } else if (requestVersions.contains(StompVersions.V1_0.toString())) { this.version = StompVersions.V1_0; } else { //not a supported version! ActiveMQStompException error = BUNDLE.versionNotSupported(acceptVersion).setHandler(frameHandler); error.addHeader(Stomp.Headers.Error.VERSION, manager.getSupportedVersionsAsErrorVersion()); error.addHeader(Stomp.Headers.CONTENT_TYPE, "text/plain"); error.setBody("Supported protocol versions are " + manager.getSupportedVersionsAsString()); error.setDisconnect(true); throw error; } } if (this.version != (StompVersions.V1_0)) { VersionedStompFrameHandler newHandler = VersionedStompFrameHandler.getHandler(this, this.version, scheduledExecutorService, factory); newHandler.initDecoder(this.frameHandler); this.frameHandler = newHandler; } this.initialized = true; } //reject if the host doesn't match public void setHost(String host) throws ActiveMQStompException { if (host == null) { ActiveMQStompException error = BUNDLE.nullHostHeader().setHandler(frameHandler); error.setBody(BUNDLE.hostCannotBeNull()); throw error; } String localHost = manager.getVirtualHostName(); if (!host.equals(localHost)) { ActiveMQStompException error = BUNDLE.hostNotMatch().setHandler(frameHandler); error.setBody(BUNDLE.hostNotMatchDetails(host)); throw error; } } public void handleFrame(StompFrame request) { StompFrame reply = null; if (stompListener != null) { stompListener.requestAccepted(request); } String cmd = request.getCommand(); try { if (isDestroyed()) { throw BUNDLE.connectionDestroyed().setHandler(frameHandler); } if (!initialized) { if (!(Stomp.Commands.CONNECT.equals(cmd) || Stomp.Commands.STOMP.equals(cmd))) { throw BUNDLE.connectionNotEstablished().setHandler(frameHandler); } //decide version negotiateVersion(request); } reply = frameHandler.handleFrame(request); } catch (ActiveMQStompException e) { reply = e.getFrame(); } if (reply != null) { sendFrame(reply); } if (Stomp.Commands.DISCONNECT.equals(cmd)) { this.disconnect(false); } } public void sendFrame(StompFrame frame) { manager.sendReply(this, frame); } public boolean validateUser(final String login, final String pass, final X509Certificate[] certificates) { this.valid = manager.validateUser(login, pass, certificates); if (valid) { this.login = login; this.passcode = pass; } return valid; } public CoreMessage createServerMessage() { return manager.createServerMessage(); } public StompSession getSession() throws ActiveMQStompException { return getSession(null); } public StompSession getSession(String txID) throws ActiveMQStompException { StompSession session = null; try { if (txID == null) { session = manager.getSession(this); } else { session = manager.getTransactedSession(this, txID); } } catch (Exception e) { throw BUNDLE.errorGetSession(e).setHandler(frameHandler); } return session; } protected void validate() throws ActiveMQStompException { if (!this.valid) { throw BUNDLE.invalidConnection().setHandler(frameHandler); } } protected void sendServerMessage(ICoreMessage message, String txID) throws ActiveMQStompException { StompSession stompSession = getSession(txID); if (stompSession.isNoLocal()) { message.putStringProperty(CONNECTION_ID_PROP, getID().toString()); } if (isEnableMessageID()) { message.putStringProperty("amqMessageId", "STOMP" + message.getMessageID()); } try { if (minLargeMessageSize == -1 || (message.getBodyBuffer().writerIndex() < minLargeMessageSize)) { stompSession.sendInternal(message, false); } else { stompSession.sendInternalLarge((CoreMessage)message, false); } } catch (Exception e) { throw BUNDLE.errorSendMessage(message, e).setHandler(frameHandler); } } @Override public void disconnect(final boolean criticalError) { disconnect(null, criticalError); } @Override public void disconnect(String scaleDownNodeID, final boolean criticalError) { destroy(); } protected void beginTransaction(String txID) throws ActiveMQStompException { try { manager.beginTransaction(this, txID); } catch (ActiveMQStompException e) { throw e; } catch (Exception e) { throw BUNDLE.errorBeginTx(txID, e).setHandler(frameHandler); } } public void commitTransaction(String txID) throws ActiveMQStompException { try { manager.commitTransaction(this, txID); } catch (Exception e) { throw BUNDLE.errorCommitTx(txID, e).setHandler(frameHandler); } } public void abortTransaction(String txID) throws ActiveMQStompException { try { manager.abortTransaction(this, txID); } catch (ActiveMQStompException e) { throw e; } catch (Exception e) { throw BUNDLE.errorAbortTx(txID, e).setHandler(frameHandler); } } void subscribe(String destination, String selector, String ack, String id, String durableSubscriptionName, boolean noLocal, RoutingType subscriptionType) throws ActiveMQStompException { autoCreateDestinationIfPossible(destination, subscriptionType); checkDestination(destination); checkRoutingSemantics(destination, subscriptionType); if (noLocal) { String noLocalFilter = CONNECTION_ID_PROP + " <> '" + getID().toString() + "'"; if (selector == null) { selector = noLocalFilter; } else { selector += " AND " + noLocalFilter; } } if (ack == null) { ack = Stomp.Headers.Subscribe.AckModeValues.AUTO; } String subscriptionID = null; if (id != null) { subscriptionID = id; } else { if (destination == null) { throw BUNDLE.noDestination().setHandler(frameHandler); } subscriptionID = "subscription/" + destination; } try { manager.subscribe(this, subscriptionID, durableSubscriptionName, destination, selector, ack, noLocal); } catch (ActiveMQStompException e) { throw e; } catch (Exception e) { throw BUNDLE.errorCreatingSubscription(subscriptionID, e).setHandler(frameHandler); } } public void unsubscribe(String subscriptionID, String durableSubscriptionName) throws ActiveMQStompException { try { manager.unsubscribe(this, subscriptionID, durableSubscriptionName); } catch (ActiveMQStompException e) { throw e; } catch (Exception e) { throw BUNDLE.errorUnsubscribing(subscriptionID, e).setHandler(frameHandler); } } public void acknowledge(String messageID, String subscriptionID) throws ActiveMQStompException { try { manager.acknowledge(this, messageID, subscriptionID); } catch (ActiveMQStompException e) { throw e; } catch (Exception e) { throw BUNDLE.errorAck(messageID, e).setHandler(frameHandler); } } public String getVersion() { return String.valueOf(version); } public String getActiveMQServerName() { return SERVER_NAME; } public StompFrame createStompMessage(ICoreMessage serverMessage, ActiveMQBuffer bodyBuffer, StompSubscription subscription, int deliveryCount) throws Exception { return frameHandler.createMessageFrame(serverMessage, bodyBuffer, subscription, deliveryCount); } public void addStompEventListener(FrameEventListener listener) { this.stompListener = listener; } //send a ping stomp frame public void ping(StompFrame pingFrame) { manager.sendReply(this, pingFrame); } public void physicalSend(StompFrame frame) throws Exception { ActiveMQBuffer buffer = frame.toActiveMQBuffer(); synchronized (sendLock) { getTransportConnection().write(buffer, false, false); } if (stompListener != null) { stompListener.replySent(frame); } } public VersionedStompFrameHandler getFrameHandler() { return this.frameHandler; } public boolean isEnableMessageID() { return enableMessageID; } public int getMinLargeMessageSize() { return minLargeMessageSize; } public StompProtocolManager getManager() { return manager; } @Override public void killMessage(SimpleString nodeID) { //unsupported } @Override public boolean isSupportsFlowControl() { return false; } }