/* * 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.v11; import javax.security.cert.X509Certificate; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompException; import org.apache.activemq.artemis.core.protocol.stomp.FrameEventListener; import org.apache.activemq.artemis.core.protocol.stomp.SimpleBytes; import org.apache.activemq.artemis.core.protocol.stomp.Stomp; import org.apache.activemq.artemis.core.protocol.stomp.StompConnection; import org.apache.activemq.artemis.core.protocol.stomp.StompDecoder; import org.apache.activemq.artemis.core.protocol.stomp.StompFrame; import org.apache.activemq.artemis.core.protocol.stomp.VersionedStompFrameHandler; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl; import org.apache.activemq.artemis.core.server.ActiveMQScheduledComponent; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry; import org.apache.activemq.artemis.utils.CertificateUtil; import org.apache.activemq.artemis.utils.ExecutorFactory; import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE; public class StompFrameHandlerV11 extends VersionedStompFrameHandler implements FrameEventListener { protected static final char ESC_CHAR = '\\'; private HeartBeater heartBeater; public StompFrameHandlerV11(StompConnection connection, ScheduledExecutorService scheduledExecutorService, ExecutorFactory executorFactory) { super(connection, scheduledExecutorService, executorFactory); connection.addStompEventListener(this); decoder = new StompDecoderV11(this); decoder.init(); } public ActiveMQScheduledComponent getHeartBeater() { return heartBeater; } @Override public StompFrame onConnect(StompFrame frame) { StompFrame response = null; Map<String, String> headers = frame.getHeadersMap(); String login = headers.get(Stomp.Headers.Connect.LOGIN); String passcode = headers.get(Stomp.Headers.Connect.PASSCODE); String clientID = headers.get(Stomp.Headers.Connect.CLIENT_ID); String requestID = headers.get(Stomp.Headers.Connect.REQUEST_ID); X509Certificate[] certificates = null; if (connection.getTransportConnection() instanceof NettyConnection) { certificates = CertificateUtil.getCertsFromChannel(((NettyConnection) connection.getTransportConnection()).getChannel()); } try { if (connection.validateUser(login, passcode, certificates)) { connection.setClientID(clientID); connection.setValid(true); response = this.createStompFrame(Stomp.Responses.CONNECTED); // version response.addHeader(Stomp.Headers.Connected.VERSION, connection.getVersion()); // session response.addHeader(Stomp.Headers.Connected.SESSION, connection.getID().toString()); // server response.addHeader(Stomp.Headers.Connected.SERVER, connection.getActiveMQServerName()); if (requestID != null) { response.addHeader(Stomp.Headers.Connected.RESPONSE_ID, requestID); } // heart-beat. We need to start after connected frame has been sent. // otherwise the client may receive heart-beat before it receives // connected frame. String heartBeat = headers.get(Stomp.Headers.Connect.HEART_BEAT); if (heartBeat != null) { handleHeartBeat(heartBeat); if (heartBeater == null) { response.addHeader(Stomp.Headers.Connected.HEART_BEAT, "0,0"); } else { response.addHeader(Stomp.Headers.Connected.HEART_BEAT, heartBeater.serverPingPeriod + "," + heartBeater.clientPingResponse); } } } else { // not valid response = createStompFrame(Stomp.Responses.ERROR); response.setNeedsDisconnect(true); response.addHeader(Stomp.Headers.CONTENT_TYPE, "text/plain"); String responseText = "Security Error occurred: User name [" + login + "] or password is invalid"; response.setBody(responseText); response.addHeader(Stomp.Headers.Error.MESSAGE, responseText); } } catch (ActiveMQStompException e) { response = e.getFrame(); } return response; } private void handleHeartBeat(String heartBeatHeader) throws ActiveMQStompException { String[] params = heartBeatHeader.split(","); if (params.length != 2) { throw new ActiveMQStompException(connection, "Incorrect heartbeat header " + heartBeatHeader); } //client ping long minPingInterval = Long.valueOf(params[0]); //client receive ping long minAcceptInterval = Long.valueOf(params[1]); if (heartBeater == null) { heartBeater = new HeartBeater(scheduledExecutorService, executorFactory.getExecutor(), minPingInterval, minAcceptInterval); } } @Override public StompFrame onDisconnect(StompFrame frame) { disconnect(); return null; } @Override protected void disconnect() { if (this.heartBeater != null) { heartBeater.shutdown(); } } @Override public StompFrame onUnsubscribe(StompFrame request) { StompFrame response = null; //unsubscribe in 1.1 only needs id header String id = request.getHeader(Stomp.Headers.Unsubscribe.ID); String durableSubscriptionName = request.getHeader(Stomp.Headers.Unsubscribe.DURABLE_SUBSCRIBER_NAME); if (durableSubscriptionName == null) { durableSubscriptionName = request.getHeader(Stomp.Headers.Unsubscribe.DURABLE_SUBSCRIPTION_NAME); } String subscriptionID = null; if (id != null) { subscriptionID = id; } else if (durableSubscriptionName == null) { response = BUNDLE.needSubscriptionID().setHandler(this).getFrame(); return response; } try { connection.unsubscribe(subscriptionID, durableSubscriptionName); } catch (ActiveMQStompException e) { response = e.getFrame(); } return response; } @Override public StompFrame onAck(StompFrame request) { StompFrame response = null; String messageID = request.getHeader(Stomp.Headers.Ack.MESSAGE_ID); String txID = request.getHeader(Stomp.Headers.TRANSACTION); String subscriptionID = request.getHeader(Stomp.Headers.Ack.SUBSCRIPTION); if (txID != null) { ActiveMQServerLogger.LOGGER.stompTXAckNorSupported(); } if (subscriptionID == null) { response = BUNDLE.needSubscriptionID().setHandler(this).getFrame(); return response; } try { connection.acknowledge(messageID, subscriptionID); } catch (ActiveMQStompException e) { response = e.getFrame(); } return response; } @Override public StompFrame onStomp(StompFrame request) { if (!connection.isValid()) { return onConnect(request); } return null; } @Override public StompFrame onNack(StompFrame request) { //this eventually means discard the message (it never be redelivered again). //we can consider supporting redeliver to a different sub. return onAck(request); } @Override public void replySent(StompFrame reply) { if (reply.getCommand().equals(Stomp.Responses.CONNECTED)) { //kick off the pinger startHeartBeat(); } if (reply.needsDisconnect()) { connection.disconnect(false); } else { //update ping if (heartBeater != null) { heartBeater.pinged(); } } } private void startHeartBeat() { if (heartBeater != null && heartBeater.serverPingPeriod != 0) { heartBeater.start(); } } public StompFrame createPingFrame() { StompFrame frame = createStompFrame(Stomp.Commands.STOMP); frame.setPing(true); return frame; } /* * HeartBeater functions: * (a) server ping: if server hasn't sent any frame within serverPingPeriod interval, send a ping * (b) configure connection ttl so that org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl.FailureCheckAndFlushThread * can deal with closing connections which go stale */ private class HeartBeater extends ActiveMQScheduledComponent { private static final int MIN_SERVER_PING = 500; long serverPingPeriod = 0; long clientPingResponse; volatile boolean shutdown = false; AtomicLong lastPingTimestamp = new AtomicLong(0); ConnectionEntry connectionEntry; private HeartBeater(ScheduledExecutorService scheduledExecutorService, Executor executor, final long clientPing, final long clientAcceptPing) { super(scheduledExecutorService, executor, clientAcceptPing > MIN_SERVER_PING ? clientAcceptPing : MIN_SERVER_PING, TimeUnit.MILLISECONDS, false); if (clientAcceptPing != 0) { serverPingPeriod = super.getPeriod(); } connectionEntry = ((RemotingServiceImpl) connection.getManager().getServer().getRemotingService()).getConnectionEntry(connection.getID()); if (connectionEntry != null) { String heartBeatToTtlModifierStr = (String) connection.getAcceptorUsed().getConfiguration().get(TransportConstants.HEART_BEAT_TO_CONNECTION_TTL_MODIFIER); double heartBeatToTtlModifier = heartBeatToTtlModifierStr == null ? 2 : Double.valueOf(heartBeatToTtlModifierStr); // the default response to the client clientPingResponse = (long) (connectionEntry.ttl / heartBeatToTtlModifier); if (clientPing != 0) { clientPingResponse = clientPing; String ttlMaxStr = (String) connection.getAcceptorUsed().getConfiguration().get(TransportConstants.CONNECTION_TTL_MAX); long ttlMax = ttlMaxStr == null ? Long.MAX_VALUE : Long.valueOf(ttlMaxStr); String ttlMinStr = (String) connection.getAcceptorUsed().getConfiguration().get(TransportConstants.CONNECTION_TTL_MIN); long ttlMin = ttlMinStr == null ? 1000 : Long.valueOf(ttlMinStr); /* The connection's TTL should be one of the following: * 1) clientPing * heartBeatToTtlModifier * 2) ttlMin * 3) ttlMax */ long connectionTtl = (long) (clientPing * heartBeatToTtlModifier); if (connectionTtl < ttlMin) { connectionTtl = ttlMin; clientPingResponse = (long) (ttlMin / heartBeatToTtlModifier); } else if (connectionTtl > ttlMax) { connectionTtl = ttlMax; clientPingResponse = (long) (ttlMax / heartBeatToTtlModifier); } if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) { ActiveMQServerLogger.LOGGER.debug("Setting STOMP client TTL to: " + connectionTtl); } connectionEntry.ttl = connectionTtl; } } } public void shutdown() { this.stop(); } public void pinged() { lastPingTimestamp.set(System.currentTimeMillis()); } @Override public void run() { lastPingTimestamp.set(System.currentTimeMillis()); connection.ping(createPingFrame()); } } @Override public void requestAccepted(StompFrame request) { } @Override public StompFrame createStompFrame(String command) { return new StompFrameV11(command); } @Override public void initDecoder(VersionedStompFrameHandler existingHandler) { decoder.init(existingHandler.getDecoder()); } protected class StompDecoderV11 extends StompDecoder { protected boolean isEscaping = false; protected SimpleBytes holder = new SimpleBytes(1024); public StompDecoderV11(StompFrameHandlerV11 handler) { super(handler); } @Override public void init(StompDecoder decoder) { this.data = decoder.data; this.workingBuffer = decoder.workingBuffer; this.pos = decoder.pos; this.command = decoder.command; } @Override public void init() { super.init(); isEscaping = false; holder.reset(); } @Override protected boolean parseCommand() throws ActiveMQStompException { int offset = 0; boolean nextChar = false; //check for ping // Some badly behaved STOMP clients add a \n *after* the terminating NUL char at the end of the // STOMP frame this can manifest as an extra \n at the beginning when the // next STOMP frame is read - we need to deal with this. // Besides, Stomp 1.2 allows for extra EOLs after NULL (i.e. // either "[\r]\n"s or "\n"s) while (true) { if (workingBuffer[offset] == NEW_LINE) { //client ping nextChar = false; } else if (workingBuffer[offset] == CR) { if (nextChar) throw BUNDLE.invalidTwoCRs().setHandler(handler); nextChar = true; } else { break; } offset++; if (offset == data) return false; //no more bytes } if (nextChar) { throw BUNDLE.badCRs().setHandler(handler); } //if some EOLs have been processed, drop those bytes before parsing command if (offset > 0) { System.arraycopy(workingBuffer, offset, workingBuffer, 0, data - offset); data = data - offset; offset = 0; } if (data < 4) { // Need at least four bytes to identify the command // - up to 3 bytes for the command name + potentially another byte for a leading \n return false; } byte b = workingBuffer[offset]; switch (b) { case A: { if (workingBuffer[offset + 1] == StompDecoder.B) { if (!tryIncrement(offset + COMMAND_ABORT_LENGTH + eolLen)) { return false; } // ABORT command = COMMAND_ABORT; } else { if (!tryIncrement(offset + COMMAND_ACK_LENGTH + eolLen)) { return false; } // ACK command = COMMAND_ACK; } break; } case B: { if (!tryIncrement(offset + COMMAND_BEGIN_LENGTH + eolLen)) { return false; } // BEGIN command = COMMAND_BEGIN; break; } case C: { if (workingBuffer[offset + 2] == M) { if (!tryIncrement(offset + COMMAND_COMMIT_LENGTH + eolLen)) { return false; } // COMMIT command = COMMAND_COMMIT; } else if (workingBuffer[offset + 7] == E) { if (!tryIncrement(offset + COMMAND_CONNECTED_LENGTH + eolLen)) { return false; } // CONNECTED command = COMMAND_CONNECTED; } else { if (!tryIncrement(offset + COMMAND_CONNECT_LENGTH + eolLen)) { return false; } // CONNECT command = COMMAND_CONNECT; } break; } case D: { if (!tryIncrement(offset + COMMAND_DISCONNECT_LENGTH + eolLen)) { return false; } // DISCONNECT command = COMMAND_DISCONNECT; break; } case R: { if (!tryIncrement(offset + COMMAND_RECEIPT_LENGTH + eolLen)) { return false; } // RECEIPT command = COMMAND_RECEIPT; break; } /**** added by meddy, 27 april 2011, handle header parser for reply to websocket protocol ****/ case E: { if (!tryIncrement(offset + COMMAND_ERROR_LENGTH + eolLen)) { return false; } // ERROR command = COMMAND_ERROR; break; } case M: { if (!tryIncrement(offset + COMMAND_MESSAGE_LENGTH + eolLen)) { return false; } // MESSAGE command = COMMAND_MESSAGE; break; } /**** end ****/ case S: { if (workingBuffer[offset + 1] == E) { if (!tryIncrement(offset + COMMAND_SEND_LENGTH + eolLen)) { return false; } // SEND command = COMMAND_SEND; } else if (workingBuffer[offset + 1] == U) { if (!tryIncrement(offset + COMMAND_SUBSCRIBE_LENGTH + eolLen)) { return false; } // SUBSCRIBE command = COMMAND_SUBSCRIBE; } else { if (!tryIncrement(offset + StompDecoder.COMMAND_STOMP_LENGTH + eolLen)) { return false; } // SUBSCRIBE command = COMMAND_STOMP; } break; } case U: { if (!tryIncrement(offset + COMMAND_UNSUBSCRIBE_LENGTH + eolLen)) { return false; } // UNSUBSCRIBE command = COMMAND_UNSUBSCRIBE; break; } case N: { if (!tryIncrement(offset + COMMAND_NACK_LENGTH + eolLen)) { return false; } //NACK command = COMMAND_NACK; break; } default: { throwInvalid(); } } checkEol(); return true; } protected void checkEol() throws ActiveMQStompException { if (workingBuffer[pos - 1] != NEW_LINE) { throwInvalid(); } } protected void throwUndefinedEscape(byte b) throws ActiveMQStompException { ActiveMQStompException error = BUNDLE.undefinedEscapeSequence(new String(new char[]{ESC_CHAR, (char) b})).setHandler(handler); error.setCode(ActiveMQStompException.UNDEFINED_ESCAPE); throw error; } @Override protected boolean parseHeaders() throws ActiveMQStompException { outer: while (true) { byte b = workingBuffer[pos++]; switch (b) { //escaping case ESC_CHAR: { if (isEscaping) { //this is a backslash holder.append(b); isEscaping = false; } else { //begin escaping isEscaping = true; } break; } case HEADER_SEPARATOR: { if (inHeaderName) { headerName = holder.getString(); holder.reset(); inHeaderName = false; headerValueWhitespace = true; } whiteSpaceOnly = false; break; } case StompDecoder.LN: { if (isEscaping) { holder.append(StompDecoder.NEW_LINE); isEscaping = false; } else { holder.append(b); } break; } case StompDecoder.c: { if (isEscaping) { holder.append(StompDecoder.HEADER_SEPARATOR); isEscaping = false; } else { holder.append(b); } break; } case StompDecoder.NEW_LINE: { if (whiteSpaceOnly) { // Headers are terminated by a blank line readingHeaders = false; break outer; } String headerValue = holder.getString(); holder.reset(); headers.put(headerName, headerValue); if (headerName.equals(Stomp.Headers.CONTENT_LENGTH)) { contentLength = Integer.parseInt(headerValue); } if (headerName.equals(Stomp.Headers.CONTENT_TYPE)) { contentType = headerValue; } whiteSpaceOnly = true; inHeaderName = true; headerValueWhitespace = false; break; } default: { whiteSpaceOnly = false; headerValueWhitespace = false; if (isEscaping) { throwUndefinedEscape(b); } holder.append(b); } } if (pos == data) { // Run out of data return false; } } return true; } @Override protected StompFrame parseBody() throws ActiveMQStompException { byte[] content = null; if (contentLength != -1) { if (pos + contentLength + 1 > data) { // Need more bytes } else { content = new byte[contentLength]; System.arraycopy(workingBuffer, pos, content, 0, contentLength); pos += contentLength; //drain all the rest if (bodyStart == -1) { bodyStart = pos; } while (pos < data) { if (workingBuffer[pos++] == 0) { break; } } } } else { // Need to scan for terminating NUL if (bodyStart == -1) { bodyStart = pos; } while (pos < data) { if (workingBuffer[pos++] == 0) { content = new byte[pos - bodyStart - 1]; System.arraycopy(workingBuffer, bodyStart, content, 0, content.length); break; } } } if (content != null) { if (data > pos) { if (workingBuffer[pos] == NEW_LINE) pos++; if (data > pos) // More data still in the buffer from the next packet System.arraycopy(workingBuffer, pos, workingBuffer, 0, data - pos); } data = data - pos; // reset StompFrame ret = new StompFrameV11(command, headers, content); init(); return ret; } else { return null; } } } }