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}