/* * 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.transport.ws; import java.io.IOException; import java.nio.ByteBuffer; import java.security.cert.X509Certificate; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.broker.BrokerServiceAware; import org.apache.activemq.transport.Transport; import org.apache.activemq.transport.TransportSupport; import org.apache.activemq.transport.ws.WSTransport.WSTransportSink; import org.apache.activemq.util.IOExceptionSupport; import org.apache.activemq.util.IntrospectionSupport; import org.apache.activemq.util.ServiceStopper; import org.apache.activemq.wireformat.WireFormat; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A proxy class that manages sending WebSocket events to the wrapped protocol level * WebSocket Transport. */ public final class WSTransportProxy extends TransportSupport implements Transport, WebSocketListener, BrokerServiceAware, WSTransportSink { private static final Logger LOG = LoggerFactory.getLogger(WSTransportProxy.class); private final int ORDERLY_CLOSE_TIMEOUT = 10; private final ReentrantLock protocolLock = new ReentrantLock(); private final CountDownLatch socketTransportStarted = new CountDownLatch(1); private final String remoteAddress; private final Transport transport; private final WSTransport wsTransport; private Session session; /** * Create a WebSocket Transport Proxy instance that will pass * along WebSocket event to the underlying protocol level transport. * * @param remoteAddress * the provided remote address to report being connected to. * @param transport * The protocol level WebSocket Transport */ public WSTransportProxy(String remoteAddress, Transport transport) { this.remoteAddress = remoteAddress; this.transport = transport; this.wsTransport = transport.narrow(WSTransport.class); if (wsTransport == null) { throw new IllegalArgumentException("Provided Transport does not contains a WSTransport implementation"); } else { wsTransport.setTransportSink(this); } } /** * @return the sub-protocol of the proxied transport. */ public String getSubProtocol() { return wsTransport.getSubProtocol(); } /** * Apply any configure Transport options on the wrapped Transport and its contained * wireFormat instance. */ public void setTransportOptions(Map<String, Object> options) { Map<String, Object> wireFormatOptions = IntrospectionSupport.extractProperties(options, "wireFormat."); IntrospectionSupport.setProperties(transport, options); IntrospectionSupport.setProperties(transport.getWireFormat(), wireFormatOptions); } @Override public void setBrokerService(BrokerService brokerService) { if (transport instanceof BrokerServiceAware) { ((BrokerServiceAware) transport).setBrokerService(brokerService); } } @Override public void oneway(Object command) throws IOException { protocolLock.lock(); try { transport.oneway(command); } catch (Exception e) { onException(IOExceptionSupport.create(e)); } finally { protocolLock.unlock(); } } @Override public X509Certificate[] getPeerCertificates() { return transport.getPeerCertificates(); } @Override public void setPeerCertificates(X509Certificate[] certificates) { transport.setPeerCertificates(certificates); } @Override public String getRemoteAddress() { return remoteAddress; } @Override public WireFormat getWireFormat() { return transport.getWireFormat(); } @Override public int getReceiveCounter() { return transport.getReceiveCounter(); } @Override protected void doStop(ServiceStopper stopper) throws Exception { transport.stop(); if (session != null && session.isOpen()) { session.close(); } } @Override protected void doStart() throws Exception { socketTransportStarted.countDown(); transport.setTransportListener(getTransportListener()); transport.start(); } //----- WebSocket methods being proxied to the WS Transport --------------// @Override public void onWebSocketBinary(byte[] payload, int offset, int length) { if (!transportStartedAtLeastOnce()) { LOG.debug("Waiting for WebSocket to be properly started..."); try { socketTransportStarted.await(); } catch (InterruptedException e) { LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); } } protocolLock.lock(); try { wsTransport.onWebSocketBinary(ByteBuffer.wrap(payload, offset, length)); } catch (Exception e) { onException(IOExceptionSupport.create(e)); } finally { protocolLock.unlock(); } } @Override public void onWebSocketText(String data) { if (!transportStartedAtLeastOnce()) { LOG.debug("Waiting for WebSocket to be properly started..."); try { socketTransportStarted.await(); } catch (InterruptedException e) { LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); } } protocolLock.lock(); try { wsTransport.onWebSocketText(data); } catch (Exception e) { onException(IOExceptionSupport.create(e)); } finally { protocolLock.unlock(); } } @Override public void onWebSocketClose(int statusCode, String reason) { try { if (protocolLock.tryLock() || protocolLock.tryLock(ORDERLY_CLOSE_TIMEOUT, TimeUnit.SECONDS)) { LOG.debug("WebSocket closed: code[{}] message[{}]", statusCode, reason); wsTransport.onWebSocketClosed(); } } catch (Exception e) { LOG.debug("Failed to close WebSocket cleanly", e); } finally { if (protocolLock.isHeldByCurrentThread()) { protocolLock.unlock(); } } } @Override public void onWebSocketConnect(Session session) { this.session = session; if (wsTransport.getMaxFrameSize() > 0) { this.session.getPolicy().setMaxBinaryMessageSize(wsTransport.getMaxFrameSize()); this.session.getPolicy().setMaxTextMessageSize(wsTransport.getMaxFrameSize()); } } @Override public void onWebSocketError(Throwable cause) { onException(IOExceptionSupport.create(cause)); } @Override public void onSocketOutboundText(String data) throws IOException { if (!transportStartedAtLeastOnce()) { LOG.debug("Waiting for WebSocket to be properly started..."); try { socketTransportStarted.await(); } catch (InterruptedException e) { LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); } } LOG.trace("WS Proxy sending string of size {} out", data.length()); try { session.getRemote().sendStringByFuture(data).get(getDefaultSendTimeOut(), TimeUnit.SECONDS); } catch (Exception e) { throw IOExceptionSupport.create(e); } } @Override public void onSocketOutboundBinary(ByteBuffer data) throws IOException { if (!transportStartedAtLeastOnce()) { LOG.debug("Waiting for WebSocket to be properly started..."); try { socketTransportStarted.await(); } catch (InterruptedException e) { LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); } } LOG.trace("WS Proxy sending {} bytes out", data.remaining()); int limit = data.limit(); try { session.getRemote().sendBytesByFuture(data).get(getDefaultSendTimeOut(), TimeUnit.SECONDS); } catch (Exception e) { throw IOExceptionSupport.create(e); } // Reset back to original limit and move position to match limit indicating // that we read everything, the websocket sender clears the passed buffer // which can make it look as if nothing was written. data.limit(limit); data.position(limit); } //----- Internal implementation ------------------------------------------// private boolean transportStartedAtLeastOnce() { return socketTransportStarted.getCount() == 0; } private static int getDefaultSendTimeOut() { return Integer.getInteger("org.apache.activemq.transport.ws.WSTransportProxy.sendTimeout", 30); } }