/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.websocket;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Binding driver between raw socket I/O and a high-level WebSocket interface. This implementation
* is generally very weak and doesn't offer sensible optimizations such as re-used buffers,
* efficient UTF-8 encoding/decoding, or the full spectrum of features defined in the RFC.
*/
class WebSocketSession implements SimpleSession {
private final ReadHandler mReadHandler;
private final WriteHandler mWriteHandler;
private final SimpleEndpoint mEndpoint;
private AtomicBoolean mIsOpen = new AtomicBoolean(false);
private volatile boolean mSentClose;
public WebSocketSession(
InputStream rawSocketInput,
OutputStream rawSocketOutput,
SimpleEndpoint endpoint) {
mReadHandler = new ReadHandler(rawSocketInput, endpoint);
mWriteHandler = new WriteHandler(rawSocketOutput);
mEndpoint = endpoint;
}
public void handle() throws IOException {
markAndSignalOpen();
// Loop until orderly shutdown or socket exception.
try {
mReadHandler.readLoop(mReadCallback);
} catch (EOFException e) {
// No need to rethrow, this can be considered a graceful shutdown of the socket (though
// not the WebSocket).
markAndSignalClosed(CloseCodes.UNEXPECTED_CONDITION, "EOF while reading");
} catch (IOException e) {
markAndSignalClosed(CloseCodes.CLOSED_ABNORMALLY, null /* reasonPhrase */);
throw e;
}
}
@Override
public void sendText(String payload) {
doWrite(FrameHelper.createTextFrame(payload));
}
@Override
public void sendBinary(byte[] payload) {
doWrite(FrameHelper.createBinaryFrame(payload));
}
@Override
public void close(int closeReason, String reasonPhrase) {
sendClose(closeReason, reasonPhrase);
markAndSignalClosed(closeReason, reasonPhrase);
}
private void sendClose(int closeReason, String reasonPhrase) {
doWrite(FrameHelper.createCloseFrame(closeReason, reasonPhrase));
markSentClose();
}
void markSentClose() {
mSentClose = true;
}
void markAndSignalOpen() {
if (!mIsOpen.getAndSet(true)) {
mEndpoint.onOpen(this /* session */);
}
}
void markAndSignalClosed(int closeReason, String reasonPhrase) {
if (mIsOpen.getAndSet(false)) {
mEndpoint.onClose(this /* session */, closeReason, reasonPhrase);
}
}
@Override
public boolean isOpen() {
return mIsOpen.get();
}
private void doWrite(Frame frame) {
if (signalErrorIfNotOpen()) {
return;
}
mWriteHandler.write(frame, mErrorForwardingWriteCallback);
}
/**
* Signals an error to the {@link SimpleEndpoint} if the session is closed.
*
* @return True if an error was signaled (the session is closed); false otherwise.
*/
private boolean signalErrorIfNotOpen() {
if (!isOpen()) {
signalError(new IOException("Session is closed"));
return true;
}
return false;
}
private void signalError(IOException e) {
mEndpoint.onError(this /* session */, e);
}
private final ReadCallback mReadCallback = new ReadCallback() {
@Override
public void onCompleteFrame(byte opcode, byte[] payload, int payloadLen) {
switch (opcode) {
case Frame.OPCODE_CONNECTION_CLOSE:
handleClose(payload, payloadLen);
break;
case Frame.OPCODE_CONNECTION_PING:
handlePing(payload, payloadLen);
break;
case Frame.OPCODE_CONNECTION_PONG:
handlePong(payload, payloadLen);
break;
case Frame.OPCODE_TEXT_FRAME:
handleTextFrame(payload, payloadLen);
break;
case Frame.OPCODE_BINARY_FRAME:
handleBinaryFrame(payload, payloadLen);
break;
default:
signalError(new IOException("Unsupported frame opcode=" + opcode));
break;
}
}
private void handleClose(byte[] payload, int payloadLen) {
int closeCode;
String closeReasonPhrase;
if (payloadLen >= 2) {
closeCode = ((payload[0] & 0xff) << 8) | (payload[1] & 0xff);
closeReasonPhrase = (payloadLen > 2) ? new String(payload, 2, payloadLen - 2) : null;
} else {
closeCode = CloseCodes.CLOSED_ABNORMALLY;
closeReasonPhrase = "Unparseable close frame";
}
// We must acknowledge the peer's close frame.
if (!mSentClose) {
sendClose(CloseCodes.NORMAL_CLOSURE, "Received close frame");
}
markAndSignalClosed(closeCode, closeReasonPhrase);
}
private void handlePing(byte[] payload, int payloadLen) {
doWrite(FrameHelper.createPongFrame(payload, payloadLen));
}
private void handlePong(byte[] payload, int payloadLen) {
// Great, whatever...
}
private void handleTextFrame(byte[] payload, int payloadLen) {
mEndpoint.onMessage(WebSocketSession.this, new String(payload, 0, payloadLen));
}
private void handleBinaryFrame(byte[] payload, int payloadLen) {
mEndpoint.onMessage(WebSocketSession.this, payload, payloadLen);
}
};
private final WriteCallback mErrorForwardingWriteCallback = new WriteCallback() {
@Override
public void onFailure(IOException e) {
signalError(e);
}
@Override
public void onSuccess() {
// Boring...
}
};
}