/*
* 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.protocol.amqp.proton;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import io.netty.buffer.ByteBuf;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPConnectionCallback;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException;
import org.apache.activemq.artemis.protocol.amqp.proton.handler.EventHandler;
import org.apache.activemq.artemis.protocol.amqp.proton.handler.ExtCapability;
import org.apache.activemq.artemis.protocol.amqp.proton.handler.ProtonHandler;
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.VersionLoader;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.transaction.Coordinator;
import org.apache.qpid.proton.amqp.transport.ErrorCondition;
import org.apache.qpid.proton.engine.Connection;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Link;
import org.apache.qpid.proton.engine.Receiver;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.engine.Session;
import org.apache.qpid.proton.engine.Transport;
import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.FAILOVER_SERVER_LIST;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.HOSTNAME;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.NETWORK_HOST;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.PORT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.SCHEME;
public class AMQPConnectionContext extends ProtonInitializable implements EventHandler {
private static final Logger log = Logger.getLogger(AMQPConnectionContext.class);
public static final Symbol CONNECTION_OPEN_FAILED = Symbol.valueOf("amqp:connection-establishment-failed");
public static final String AMQP_CONTAINER_ID = "amqp-container-id";
protected final ProtonHandler handler;
protected AMQPConnectionCallback connectionCallback;
private final String containerId;
private final Map<Symbol, Object> connectionProperties = new HashMap<>();
private final ScheduledExecutorService scheduledPool;
private final Map<Session, AMQPSessionContext> sessions = new ConcurrentHashMap<>();
private final ProtonProtocolManager protocolManager;
public AMQPConnectionContext(ProtonProtocolManager protocolManager,
AMQPConnectionCallback connectionSP,
String containerId,
int idleTimeout,
int maxFrameSize,
int channelMax,
ScheduledExecutorService scheduledPool) {
this.protocolManager = protocolManager;
this.connectionCallback = connectionSP;
this.containerId = (containerId != null) ? containerId : UUID.randomUUID().toString();
connectionProperties.put(AmqpSupport.PRODUCT, "apache-activemq-artemis");
connectionProperties.put(AmqpSupport.VERSION, VersionLoader.getVersion().getFullVersion());
this.scheduledPool = scheduledPool;
connectionCallback.setConnection(this);
this.handler = new ProtonHandler(protocolManager.getServer().getExecutorFactory().getExecutor());
handler.addEventHandler(this);
Transport transport = handler.getTransport();
transport.setEmitFlowEventOnSend(false);
if (idleTimeout > 0) {
transport.setIdleTimeout(idleTimeout);
}
transport.setChannelMax(channelMax);
transport.setMaxFrameSize(maxFrameSize);
}
protected AMQPSessionContext newSessionExtension(Session realSession) throws ActiveMQAMQPException {
AMQPSessionCallback sessionSPI = connectionCallback.createSessionCallback(this);
AMQPSessionContext protonSession = new AMQPSessionContext(sessionSPI, this, realSession);
return protonSession;
}
public SASLResult getSASLResult() {
return handler.getSASLResult();
}
public void inputBuffer(ByteBuf buffer) {
if (log.isTraceEnabled()) {
ByteUtil.debugFrame(log, "Buffer Received ", buffer);
}
handler.inputBuffer(buffer);
}
public void destroy() {
connectionCallback.close();
}
public boolean isSyncOnFlush() {
return false;
}
public boolean tryLock(long time, TimeUnit timeUnit) {
return handler.tryLock(time, timeUnit);
}
public void lock() {
handler.lock();
}
public void unlock() {
handler.unlock();
}
public int capacity() {
return handler.capacity();
}
public void flush() {
handler.flush();
}
public void close(ErrorCondition errorCondition) {
handler.close(errorCondition);
}
protected AMQPSessionContext getSessionExtension(Session realSession) throws ActiveMQAMQPException {
AMQPSessionContext sessionExtension = sessions.get(realSession);
if (sessionExtension == null) {
// how this is possible? Log a warn here
sessionExtension = newSessionExtension(realSession);
realSession.setContext(sessionExtension);
sessions.put(realSession, sessionExtension);
}
return sessionExtension;
}
protected boolean validateConnection(Connection connection) {
return connectionCallback.validateConnection(connection, handler.getSASLResult());
}
public boolean checkDataReceived() {
return handler.checkDataReceived();
}
public long getCreationTime() {
return handler.getCreationTime();
}
public String getRemoteContainer() {
return handler.getConnection().getRemoteContainer();
}
public String getPubSubPrefix() {
return null;
}
protected void initInternal() throws Exception {
}
protected void remoteLinkOpened(Link link) throws Exception {
AMQPSessionContext protonSession = getSessionExtension(link.getSession());
link.setSource(link.getRemoteSource());
link.setTarget(link.getRemoteTarget());
if (link instanceof Receiver) {
Receiver receiver = (Receiver) link;
if (link.getRemoteTarget() instanceof Coordinator) {
Coordinator coordinator = (Coordinator) link.getRemoteTarget();
protonSession.addTransactionHandler(coordinator, receiver);
} else {
protonSession.addReceiver(receiver);
}
} else {
Sender sender = (Sender) link;
protonSession.addSender(sender);
}
}
public Symbol[] getConnectionCapabilitiesOffered() {
URI tc = connectionCallback.getFailoverList();
if (tc != null) {
Map<Symbol, Object> hostDetails = new HashMap<>();
hostDetails.put(NETWORK_HOST, tc.getHost());
boolean isSSL = tc.getQuery().contains(TransportConstants.SSL_ENABLED_PROP_NAME + "=true");
if (isSSL) {
hostDetails.put(SCHEME, "amqps");
} else {
hostDetails.put(SCHEME, "amqp");
}
hostDetails.put(HOSTNAME, tc.getHost());
hostDetails.put(PORT, tc.getPort());
connectionProperties.put(FAILOVER_SERVER_LIST, Arrays.asList(hostDetails));
}
return ExtCapability.getCapabilities();
}
public void open(Map<Symbol, Object> connectionProperties) {
handler.open(containerId, connectionProperties);
}
public String getContainer() {
return containerId;
}
public void addEventHandler(EventHandler eventHandler) {
handler.addEventHandler(eventHandler);
}
public ProtonProtocolManager getProtocolManager() {
return protocolManager;
}
public int getAmqpLowCredits() {
if (protocolManager != null) {
return protocolManager.getAmqpLowCredits();
} else {
// this is for tests only...
return 30;
}
}
public int getAmqpCredits() {
if (protocolManager != null) {
return protocolManager.getAmqpCredits();
} else {
// this is for tests only...
return 100;
}
}
@Override
public void onInit(Connection connection) throws Exception {
}
@Override
public void onLocalOpen(Connection connection) throws Exception {
}
@Override
public void onLocalClose(Connection connection) throws Exception {
}
@Override
public void onFinal(Connection connection) throws Exception {
}
@Override
public void onInit(Session session) throws Exception {
}
@Override
public void onFinal(Session session) throws Exception {
}
@Override
public void onInit(Link link) throws Exception {
}
@Override
public void onLocalOpen(Link link) throws Exception {
}
@Override
public void onLocalClose(Link link) throws Exception {
}
@Override
public void onFinal(Link link) throws Exception {
}
@Override
public void onAuthInit(ProtonHandler handler, Connection connection, boolean sasl) {
if (sasl) {
handler.createServerSASL(connectionCallback.getSASLMechnisms());
} else {
if (!connectionCallback.isSupportsAnonymous()) {
connectionCallback.sendSASLSupported();
connectionCallback.close();
handler.close(null);
}
}
}
@Override
public void onTransport(Transport transport) {
handler.flushBytes();
}
@Override
public void pushBytes(ByteBuf bytes) {
connectionCallback.onTransport(bytes, this);
}
@Override
public boolean flowControl(ReadyListener readyListener) {
return connectionCallback.isWritable(readyListener);
}
@Override
public void onRemoteOpen(Connection connection) throws Exception {
lock();
try {
try {
initInternal();
} catch (Exception e) {
log.error("Error init connection", e);
}
if (!validateConnection(connection)) {
connection.close();
} else {
connection.setContext(AMQPConnectionContext.this);
connection.setContainer(containerId);
connection.setProperties(connectionProperties);
connection.setOfferedCapabilities(getConnectionCapabilitiesOffered());
connection.open();
}
} finally {
unlock();
}
initialise();
/*
* This can be null which is in effect an empty map, also we really don't need to check this for in bound connections
* but its here in case we add support for outbound connections.
* */
if (connection.getRemoteProperties() == null || !connection.getRemoteProperties().containsKey(CONNECTION_OPEN_FAILED)) {
long nextKeepAliveTime = handler.tick(true);
if (nextKeepAliveTime > 0 && scheduledPool != null) {
scheduledPool.schedule(new Runnable() {
@Override
public void run() {
long rescheduleAt = (handler.tick(false) - TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
if (rescheduleAt > 0) {
scheduledPool.schedule(this, rescheduleAt, TimeUnit.MILLISECONDS);
}
}
}, (nextKeepAliveTime - TimeUnit.NANOSECONDS.toMillis(System.nanoTime())), TimeUnit.MILLISECONDS);
}
}
}
@Override
public void onRemoteClose(Connection connection) {
lock();
try {
connection.close();
connection.free();
} finally {
unlock();
}
for (AMQPSessionContext protonSession : sessions.values()) {
protonSession.close();
}
sessions.clear();
// We must force write the channel before we actually destroy the connection
handler.flushBytes();
destroy();
}
@Override
public void onLocalOpen(Session session) throws Exception {
getSessionExtension(session);
}
@Override
public void onRemoteOpen(Session session) throws Exception {
getSessionExtension(session).initialise();
lock();
try {
session.open();
} finally {
unlock();
}
}
@Override
public void onLocalClose(Session session) throws Exception {
}
@Override
public void onRemoteClose(Session session) throws Exception {
lock();
try {
session.close();
session.free();
} finally {
unlock();
}
AMQPSessionContext sessionContext = (AMQPSessionContext) session.getContext();
if (sessionContext != null) {
sessionContext.close();
sessions.remove(session);
session.setContext(null);
}
}
@Override
public void onRemoteOpen(Link link) throws Exception {
remoteLinkOpened(link);
}
@Override
public void onFlow(Link link) throws Exception {
if (link.getContext() != null) {
((ProtonDeliveryHandler) link.getContext()).onFlow(link.getCredit(), link.getDrain());
}
}
@Override
public void onRemoteClose(Link link) throws Exception {
lock();
try {
link.close();
link.free();
} finally {
unlock();
}
ProtonDeliveryHandler linkContext = (ProtonDeliveryHandler) link.getContext();
if (linkContext != null) {
linkContext.close(true);
}
}
@Override
public void onRemoteDetach(Link link) throws Exception {
lock();
try {
link.detach();
link.free();
} finally {
unlock();
}
}
@Override
public void onLocalDetach(Link link) throws Exception {
Object context = link.getContext();
if (context instanceof ProtonServerSenderContext) {
ProtonServerSenderContext senderContext = (ProtonServerSenderContext) context;
senderContext.close(false);
}
}
@Override
public void onDelivery(Delivery delivery) throws Exception {
ProtonDeliveryHandler handler = (ProtonDeliveryHandler) delivery.getLink().getContext();
if (handler != null) {
handler.onMessage(delivery);
} else {
log.warn("Handler is null, can't delivery " + delivery, new Exception("tracing location"));
}
}
}