/* * 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.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Executor; import io.netty.channel.ChannelPipeline; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; import org.apache.activemq.artemis.api.core.BaseInterceptor; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.core.io.IOCallback; import org.apache.activemq.artemis.core.message.impl.CoreMessage; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyServerConnection; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.ServerSession; import org.apache.activemq.artemis.spi.core.protocol.AbstractProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManagerFactory; 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.security.ActiveMQSecurityManager; import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager2; import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager3; import org.apache.activemq.artemis.utils.UUIDGenerator; import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE; /** * StompProtocolManager */ public class StompProtocolManager extends AbstractProtocolManager<StompFrame, StompFrameInterceptor, StompConnection> { private static final List<String> websocketRegistryNames = Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp"); private final ActiveMQServer server; private final StompProtocolManagerFactory factory; private final Executor executor; private final Map<String, StompSession> transactedSessions = new HashMap<>(); // key => connection ID, value => Stomp session private final Map<Object, StompSession> sessions = new HashMap<>(); private final List<StompFrameInterceptor> incomingInterceptors; private final List<StompFrameInterceptor> outgoingInterceptors; // Static -------------------------------------------------------- // Constructors -------------------------------------------------- StompProtocolManager(final StompProtocolManagerFactory factory, final ActiveMQServer server, final List<StompFrameInterceptor> incomingInterceptors, final List<StompFrameInterceptor> outgoingInterceptors) { this.factory = factory; this.server = server; this.executor = server.getExecutorFactory().getExecutor(); this.incomingInterceptors = incomingInterceptors; this.outgoingInterceptors = outgoingInterceptors; } @Override public boolean acceptsNoHandshake() { return false; } @Override public ProtocolManagerFactory<StompFrameInterceptor> getFactory() { return factory; } @Override public void updateInterceptors(List<BaseInterceptor> incoming, List<BaseInterceptor> outgoing) { this.incomingInterceptors.clear(); this.incomingInterceptors.addAll(getFactory().filterInterceptors(incoming)); this.outgoingInterceptors.clear(); this.outgoingInterceptors.addAll(getFactory().filterInterceptors(outgoing)); } @Override public ConnectionEntry createConnectionEntry(final Acceptor acceptorUsed, final Connection connection) { StompConnection conn = new StompConnection(acceptorUsed, connection, this, server.getScheduledPool(), server.getExecutorFactory()); // Note that STOMP 1.0 has no heartbeat, so if connection ttl is non zero, data must continue to be sent or connection // will be timed out and closed! String ttlStr = (String) acceptorUsed.getConfiguration().get(TransportConstants.CONNECTION_TTL); Long ttl = ttlStr == null ? null : Long.valueOf(ttlStr); if (ttl != null) { if (ttl > 0) { return new ConnectionEntry(conn, null, System.currentTimeMillis(), ttl); } throw BUNDLE.negativeConnectionTTL(ttl); } ttl = server.getConfiguration().getConnectionTTLOverride(); if (ttl != -1) { return new ConnectionEntry(conn, null, System.currentTimeMillis(), ttl); } else { // Default to 1 minute - which is same as core protocol return new ConnectionEntry(conn, null, System.currentTimeMillis(), 1 * 60 * 1000); } } @Override public void removeHandler(String name) { } @Override public void handleBuffer(final RemotingConnection connection, final ActiveMQBuffer buffer) { StompConnection conn = (StompConnection) connection; conn.setDataReceived(); do { StompFrame request; try { request = conn.decode(buffer); } catch (Exception e) { ActiveMQServerLogger.LOGGER.errorDecodingPacket(e); return; } if (request == null) { break; } try { invokeInterceptors(this.incomingInterceptors, request, conn); conn.handleFrame(request); } finally { server.getStorageManager().clearContext(); } } while (conn.hasBytes()); } @Override public void addChannelHandlers(ChannelPipeline pipeline) { } @Override public boolean isProtocol(byte[] array) { String frameStart = new String(array, StandardCharsets.US_ASCII); return frameStart.startsWith(Stomp.Commands.CONNECT) || frameStart.startsWith(Stomp.Commands.STOMP); } @Override public void handshake(NettyServerConnection connection, ActiveMQBuffer buffer) { //Todo move handshake to here } @Override public List<String> websocketSubprotocolIdentifiers() { return websocketRegistryNames; } // Public -------------------------------------------------------- public boolean send(final StompConnection connection, final StompFrame frame) { if (ActiveMQServerLogger.LOGGER.isTraceEnabled()) { ActiveMQServerLogger.LOGGER.trace("sent " + frame); } invokeInterceptors(this.outgoingInterceptors, frame, connection); synchronized (connection) { if (connection.isDestroyed()) { ActiveMQStompProtocolLogger.LOGGER.connectionClosed(connection); return false; } try { connection.physicalSend(frame); } catch (Exception e) { ActiveMQStompProtocolLogger.LOGGER.errorSendingFrame(e, frame); return false; } return true; } } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- public StompSession getSession(StompConnection connection) throws Exception { StompSession stompSession = sessions.get(connection.getID()); if (stompSession == null) { stompSession = new StompSession(connection, this, server.getStorageManager().newContext(server.getExecutorFactory().getExecutor())); String name = UUIDGenerator.getInstance().generateStringUUID(); ServerSession session = server.createSession(name, connection.getLogin(), connection.getPasscode(), ActiveMQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE, connection, true, false, false, false, null, stompSession, true, server.newOperationContext(), getPrefixes()); stompSession.setServerSession(session); sessions.put(connection.getID(), stompSession); } server.getStorageManager().setContext(stompSession.getContext()); return stompSession; } public StompSession getTransactedSession(StompConnection connection, String txID) throws Exception { StompSession stompSession = transactedSessions.get(txID); if (stompSession == null) { stompSession = new StompSession(connection, this, server.getStorageManager().newContext(executor)); String name = UUIDGenerator.getInstance().generateStringUUID(); ServerSession session = server.createSession(name, connection.getLogin(), connection.getPasscode(), ActiveMQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE, connection, false, false, false, false, null, stompSession, true, server.newOperationContext(), getPrefixes()); stompSession.setServerSession(session); transactedSessions.put(txID, stompSession); } server.getStorageManager().setContext(stompSession.getContext()); return stompSession; } public void cleanup(final StompConnection connection) { connection.setValid(false); // Close the session outside of the lock on the StompConnection, otherwise it could dead lock this.executor.execute(new Runnable() { @Override public void run() { StompSession session = sessions.remove(connection.getID()); if (session != null) { try { session.getCoreSession().stop(); session.getCoreSession().rollback(true); session.getCoreSession().close(false); } catch (Exception e) { ActiveMQServerLogger.LOGGER.errorCleaningStompConn(e); } } // removed the transacted session belonging to the connection Iterator<Entry<String, StompSession>> iterator = transactedSessions.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, StompSession> entry = iterator.next(); if (entry.getValue().getConnection() == connection) { ServerSession serverSession = entry.getValue().getCoreSession(); try { serverSession.rollback(true); serverSession.close(false); } catch (Exception e) { ActiveMQServerLogger.LOGGER.errorCleaningStompConn(e); } iterator.remove(); } } } }); } public void sendReply(final StompConnection connection, final StompFrame frame) { server.getStorageManager().afterCompleteOperations(new IOCallback() { @Override public void onError(final int errorCode, final String errorMessage) { ActiveMQServerLogger.LOGGER.errorProcessingIOCallback(errorCode, errorMessage); ActiveMQStompException e = new ActiveMQStompException("Error sending reply", ActiveMQExceptionType.createException(errorCode, errorMessage)).setHandler(connection.getFrameHandler()); StompFrame error = e.getFrame(); send(connection, error); } @Override public void done() { send(connection, frame); } }); } public String getSupportedVersionsAsString() { String versions = ""; for (StompVersions version : StompVersions.values()) { versions += " v" + version; } return versions.substring(1); } public String getSupportedVersionsAsErrorVersion() { String versions = ""; for (StompVersions version : StompVersions.values()) { versions += "," + version; } return versions.substring(1); } public String getVirtualHostName() { return "activemq"; } public boolean validateUser(String login, String passcode, X509Certificate[] certificates) { boolean validated = true; ActiveMQSecurityManager sm = server.getSecurityManager(); if (sm != null && server.getConfiguration().isSecurityEnabled()) { if (sm instanceof ActiveMQSecurityManager3) { validated = ((ActiveMQSecurityManager3) sm).validateUser(login, passcode, certificates) != null; } else if (sm instanceof ActiveMQSecurityManager2) { validated = ((ActiveMQSecurityManager2) sm).validateUser(login, passcode, certificates); } else { validated = sm.validateUser(login, passcode); } } return validated; } public CoreMessage createServerMessage() { return new CoreMessage(server.getStorageManager().generateID(), 512); } public void commitTransaction(StompConnection connection, String txID) throws Exception { StompSession session = getTransactedSession(connection, txID); if (session == null) { throw new ActiveMQStompException(connection, "No transaction started: " + txID); } transactedSessions.remove(txID); session.getCoreSession().commit(); } public void abortTransaction(StompConnection connection, String txID) throws Exception { StompSession session = getTransactedSession(connection, txID); if (session == null) { throw new ActiveMQStompException(connection, "No transaction started: " + txID); } transactedSessions.remove(txID); session.getCoreSession().rollback(false); } // Inner classes ------------------------------------------------- public void subscribe(StompConnection connection, String subscriptionID, String durableSubscriptionName, String destination, String selector, String ack, boolean noLocal) throws Exception { StompSession stompSession = getSession(connection); stompSession.setNoLocal(noLocal); if (stompSession.containsSubscription(subscriptionID)) { throw new ActiveMQStompException(connection, "There already is a subscription for: " + subscriptionID + ". Either use unique subscription IDs or do not create multiple subscriptions for the same destination"); } long consumerID = server.getStorageManager().generateID(); stompSession.addSubscription(consumerID, subscriptionID, connection.getClientID(), durableSubscriptionName, destination, selector, ack); } public void unsubscribe(StompConnection connection, String subscriptionID, String durableSubscriberName) throws Exception { StompSession stompSession = getSession(connection); boolean unsubscribed = stompSession.unsubscribe(subscriptionID, durableSubscriberName, connection.getClientID()); if (!unsubscribed) { throw new ActiveMQStompException(connection, "Cannot unsubscribe as no subscription exists for id: " + subscriptionID); } } public void acknowledge(StompConnection connection, String messageID, String subscriptionID) throws Exception { StompSession stompSession = getSession(connection); stompSession.acknowledge(messageID, subscriptionID); } public void beginTransaction(StompConnection connection, String txID) throws Exception { ActiveMQServerLogger.LOGGER.stompBeginTX(txID); if (transactedSessions.containsKey(txID)) { ActiveMQServerLogger.LOGGER.stompErrorTXExists(txID); throw new ActiveMQStompException(connection, "Transaction already started: " + txID); } // create the transacted session getTransactedSession(connection, txID); } public boolean destinationExists(String destination) { if (server.getManagementService().getManagementAddress().toString().equals(destination)) { return true; } return server.getPostOffice().getAddressInfo(SimpleString.toSimpleString(destination)) != null; } public ActiveMQServer getServer() { return server; } }