001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.activemq.transport.ws; 018 019import java.io.IOException; 020import java.nio.ByteBuffer; 021import java.security.cert.X509Certificate; 022import java.util.Map; 023import java.util.concurrent.CountDownLatch; 024import java.util.concurrent.TimeUnit; 025import java.util.concurrent.locks.ReentrantLock; 026 027import org.apache.activemq.broker.BrokerService; 028import org.apache.activemq.broker.BrokerServiceAware; 029import org.apache.activemq.transport.Transport; 030import org.apache.activemq.transport.TransportSupport; 031import org.apache.activemq.transport.ws.WSTransport.WSTransportSink; 032import org.apache.activemq.util.IOExceptionSupport; 033import org.apache.activemq.util.IntrospectionSupport; 034import org.apache.activemq.util.ServiceStopper; 035import org.apache.activemq.wireformat.WireFormat; 036import org.eclipse.jetty.websocket.api.Session; 037import org.eclipse.jetty.websocket.api.WebSocketListener; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041/** 042 * A proxy class that manages sending WebSocket events to the wrapped protocol level 043 * WebSocket Transport. 044 */ 045public final class WSTransportProxy extends TransportSupport implements Transport, WebSocketListener, BrokerServiceAware, WSTransportSink { 046 047 private static final Logger LOG = LoggerFactory.getLogger(WSTransportProxy.class); 048 049 private final int ORDERLY_CLOSE_TIMEOUT = 10; 050 051 private final ReentrantLock protocolLock = new ReentrantLock(); 052 private final CountDownLatch socketTransportStarted = new CountDownLatch(1); 053 private final String remoteAddress; 054 055 private final Transport transport; 056 private final WSTransport wsTransport; 057 private Session session; 058 059 /** 060 * Create a WebSocket Transport Proxy instance that will pass 061 * along WebSocket event to the underlying protocol level transport. 062 * 063 * @param remoteAddress 064 * the provided remote address to report being connected to. 065 * @param transport 066 * The protocol level WebSocket Transport 067 */ 068 public WSTransportProxy(String remoteAddress, Transport transport) { 069 this.remoteAddress = remoteAddress; 070 this.transport = transport; 071 this.wsTransport = transport.narrow(WSTransport.class); 072 073 if (wsTransport == null) { 074 throw new IllegalArgumentException("Provided Transport does not contains a WSTransport implementation"); 075 } else { 076 wsTransport.setTransportSink(this); 077 } 078 } 079 080 /** 081 * @return the sub-protocol of the proxied transport. 082 */ 083 public String getSubProtocol() { 084 return wsTransport.getSubProtocol(); 085 } 086 087 /** 088 * Apply any configure Transport options on the wrapped Transport and its contained 089 * wireFormat instance. 090 */ 091 public void setTransportOptions(Map<String, Object> options) { 092 Map<String, Object> wireFormatOptions = IntrospectionSupport.extractProperties(options, "wireFormat."); 093 094 IntrospectionSupport.setProperties(transport, options); 095 IntrospectionSupport.setProperties(transport.getWireFormat(), wireFormatOptions); 096 } 097 098 @Override 099 public void setBrokerService(BrokerService brokerService) { 100 if (transport instanceof BrokerServiceAware) { 101 ((BrokerServiceAware) transport).setBrokerService(brokerService); 102 } 103 } 104 105 @Override 106 public void oneway(Object command) throws IOException { 107 protocolLock.lock(); 108 try { 109 transport.oneway(command); 110 } catch (Exception e) { 111 onException(IOExceptionSupport.create(e)); 112 } finally { 113 protocolLock.unlock(); 114 } 115 } 116 117 @Override 118 public X509Certificate[] getPeerCertificates() { 119 return transport.getPeerCertificates(); 120 } 121 122 @Override 123 public void setPeerCertificates(X509Certificate[] certificates) { 124 transport.setPeerCertificates(certificates); 125 } 126 127 @Override 128 public String getRemoteAddress() { 129 return remoteAddress; 130 } 131 132 @Override 133 public WireFormat getWireFormat() { 134 return transport.getWireFormat(); 135 } 136 137 @Override 138 public int getReceiveCounter() { 139 return transport.getReceiveCounter(); 140 } 141 142 @Override 143 protected void doStop(ServiceStopper stopper) throws Exception { 144 transport.stop(); 145 if (session != null && session.isOpen()) { 146 session.close(); 147 } 148 } 149 150 @Override 151 protected void doStart() throws Exception { 152 transport.setTransportListener(getTransportListener()); 153 socketTransportStarted.countDown(); 154 155 transport.start(); 156 } 157 158 //----- WebSocket methods being proxied to the WS Transport --------------// 159 160 @Override 161 public void onWebSocketBinary(byte[] payload, int offset, int length) { 162 if (!transportStartedAtLeastOnce()) { 163 LOG.debug("Waiting for WebSocket to be properly started..."); 164 try { 165 socketTransportStarted.await(); 166 } catch (InterruptedException e) { 167 LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); 168 } 169 } 170 171 protocolLock.lock(); 172 try { 173 wsTransport.onWebSocketBinary(ByteBuffer.wrap(payload, offset, length)); 174 } catch (Exception e) { 175 onException(IOExceptionSupport.create(e)); 176 } finally { 177 protocolLock.unlock(); 178 } 179 } 180 181 @Override 182 public void onWebSocketText(String data) { 183 if (!transportStartedAtLeastOnce()) { 184 LOG.debug("Waiting for WebSocket to be properly started..."); 185 try { 186 socketTransportStarted.await(); 187 } catch (InterruptedException e) { 188 LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); 189 } 190 } 191 192 protocolLock.lock(); 193 try { 194 wsTransport.onWebSocketText(data); 195 } catch (Exception e) { 196 onException(IOExceptionSupport.create(e)); 197 } finally { 198 protocolLock.unlock(); 199 } 200 } 201 202 @Override 203 public void onWebSocketClose(int statusCode, String reason) { 204 try { 205 if (protocolLock.tryLock() || protocolLock.tryLock(ORDERLY_CLOSE_TIMEOUT, TimeUnit.SECONDS)) { 206 LOG.debug("WebSocket closed: code[{}] message[{}]", statusCode, reason); 207 wsTransport.onWebSocketClosed(); 208 } 209 } catch (Exception e) { 210 LOG.debug("Failed to close WebSocket cleanly", e); 211 } finally { 212 if (protocolLock.isHeldByCurrentThread()) { 213 protocolLock.unlock(); 214 } 215 } 216 } 217 218 @Override 219 public void onWebSocketConnect(Session session) { 220 this.session = session; 221 222 if (wsTransport.getMaxFrameSize() > 0) { 223 this.session.getPolicy().setMaxBinaryMessageSize(wsTransport.getMaxFrameSize()); 224 this.session.getPolicy().setMaxTextMessageSize(wsTransport.getMaxFrameSize()); 225 } 226 } 227 228 @Override 229 public void onWebSocketError(Throwable cause) { 230 onException(IOExceptionSupport.create(cause)); 231 } 232 233 @Override 234 public void onSocketOutboundText(String data) throws IOException { 235 if (!transportStartedAtLeastOnce()) { 236 LOG.debug("Waiting for WebSocket to be properly started..."); 237 try { 238 socketTransportStarted.await(); 239 } catch (InterruptedException e) { 240 LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); 241 } 242 } 243 244 LOG.trace("WS Proxy sending string of size {} out", data.length()); 245 try { 246 session.getRemote().sendStringByFuture(data).get(getDefaultSendTimeOut(), TimeUnit.SECONDS); 247 } catch (Exception e) { 248 throw IOExceptionSupport.create(e); 249 } 250 } 251 252 @Override 253 public void onSocketOutboundBinary(ByteBuffer data) throws IOException { 254 if (!transportStartedAtLeastOnce()) { 255 LOG.debug("Waiting for WebSocket to be properly started..."); 256 try { 257 socketTransportStarted.await(); 258 } catch (InterruptedException e) { 259 LOG.warn("While waiting for WebSocket to be properly started, we got interrupted!! Should be okay, but you could see race conditions..."); 260 } 261 } 262 263 LOG.trace("WS Proxy sending {} bytes out", data.remaining()); 264 int limit = data.limit(); 265 try { 266 session.getRemote().sendBytesByFuture(data).get(getDefaultSendTimeOut(), TimeUnit.SECONDS); 267 } catch (Exception e) { 268 throw IOExceptionSupport.create(e); 269 } 270 271 // Reset back to original limit and move position to match limit indicating 272 // that we read everything, the websocket sender clears the passed buffer 273 // which can make it look as if nothing was written. 274 data.limit(limit); 275 data.position(limit); 276 } 277 278 //----- Internal implementation ------------------------------------------// 279 280 private boolean transportStartedAtLeastOnce() { 281 return socketTransportStarted.getCount() == 0; 282 } 283 284 private static int getDefaultSendTimeOut() { 285 return Integer.getInteger("org.apache.activemq.transport.ws.WSTransportProxy.sendTimeout", 30); 286 } 287}