blob: 0e209dffe85d9f6d90631c0a7bcc6649fbb5cce2 [file] [log] [blame]
package fi.iki.elonen;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Arrays;
import java.util.List;
public class WebSocketFrame {
private OpCode opCode;
private boolean fin;
private byte[] maskingKey;
private byte[] payload;
private transient int _payloadLength;
private transient String _payloadString;
private WebSocketFrame(OpCode opCode, boolean fin) {
setOpCode(opCode);
setFin(fin);
}
public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
this(opCode, fin);
setMaskingKey(maskingKey);
setBinaryPayload(payload);
}
public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
this(opCode, fin, payload, null);
}
public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException {
this(opCode, fin);
setMaskingKey(maskingKey);
setTextPayload(payload);
}
public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
this(opCode, fin, payload, null);
}
public WebSocketFrame(WebSocketFrame clone) {
setOpCode(clone.getOpCode());
setFin(clone.isFin());
setBinaryPayload(clone.getBinaryPayload());
setMaskingKey(clone.getMaskingKey());
}
public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException {
setOpCode(opCode);
setFin(true);
long _payloadLength = 0;
for (WebSocketFrame inter : fragments) {
_payloadLength += inter.getBinaryPayload().length;
}
if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
}
this._payloadLength = (int) _payloadLength;
byte[] payload = new byte[this._payloadLength];
int offset = 0;
for (WebSocketFrame inter : fragments) {
System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length);
offset += inter.getBinaryPayload().length;
}
setBinaryPayload(payload);
}
// --------------------------------GETTERS---------------------------------
public OpCode getOpCode() {
return opCode;
}
public void setOpCode(OpCode opcode) {
this.opCode = opcode;
}
public boolean isFin() {
return fin;
}
public void setFin(boolean fin) {
this.fin = fin;
}
public boolean isMasked() {
return maskingKey != null && maskingKey.length == 4;
}
public byte[] getMaskingKey() {
return maskingKey;
}
public void setMaskingKey(byte[] maskingKey) {
if (maskingKey != null && maskingKey.length != 4) {
throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
}
this.maskingKey = maskingKey;
}
public void setUnmasked() {
setMaskingKey(null);
}
public byte[] getBinaryPayload() {
return payload;
}
public void setBinaryPayload(byte[] payload) {
this.payload = payload;
this._payloadLength = payload.length;
this._payloadString = null;
}
public String getTextPayload() {
if (_payloadString == null) {
try {
_payloadString = binary2Text(getBinaryPayload());
} catch (CharacterCodingException e) {
throw new RuntimeException("Undetected CharacterCodingException", e);
}
}
return _payloadString;
}
public void setTextPayload(String payload) throws CharacterCodingException {
this.payload = text2Binary(payload);
this._payloadLength = payload.length();
this._payloadString = payload;
}
// --------------------------------SERIALIZATION---------------------------
public static WebSocketFrame read(InputStream in) throws IOException {
byte head = (byte) checkedRead(in.read());
boolean fin = ((head & 0x80) != 0);
OpCode opCode = OpCode.find((byte) (head & 0x0F));
if ((head & 0x70) != 0) {
throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
}
if (opCode == null) {
throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
} else if (opCode.isControlFrame() && !fin) {
throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
}
WebSocketFrame frame = new WebSocketFrame(opCode, fin);
frame.readPayloadInfo(in);
frame.readPayload(in);
if (frame.getOpCode() == OpCode.Close) {
return new CloseFrame(frame);
} else {
return frame;
}
}
private static int checkedRead(int read) throws IOException {
if (read < 0) {
throw new EOFException();
}
//System.out.println(Integer.toBinaryString(read) + "/" + read + "/" + Integer.toHexString(read));
return read;
}
private void readPayloadInfo(InputStream in) throws IOException {
byte b = (byte) checkedRead(in.read());
boolean masked = ((b & 0x80) != 0);
_payloadLength = (byte) (0x7F & b);
if (_payloadLength == 126) {
// checkedRead must return int for this to work
_payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
if (_payloadLength < 126) {
throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
}
} else if (_payloadLength == 127) {
long _payloadLength = ((long) checkedRead(in.read())) << 56 |
((long) checkedRead(in.read())) << 48 |
((long) checkedRead(in.read())) << 40 |
((long) checkedRead(in.read())) << 32 |
checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
if (_payloadLength < 65536) {
throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
}
if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
}
this._payloadLength = (int) _payloadLength;
}
if (opCode.isControlFrame()) {
if (_payloadLength > 125) {
throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
}
if (opCode == OpCode.Close && _payloadLength == 1) {
throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
}
}
if (masked) {
maskingKey = new byte[4];
int read = 0;
while (read < maskingKey.length) {
read += checkedRead(in.read(maskingKey, read, maskingKey.length - read));
}
}
}
private void readPayload(InputStream in) throws IOException {
payload = new byte[_payloadLength];
int read = 0;
while (read < _payloadLength) {
read += checkedRead(in.read(payload, read, _payloadLength - read));
}
if (isMasked()) {
for (int i = 0; i < payload.length; i++) {
payload[i] ^= maskingKey[i % 4];
}
}
//Test for Unicode errors
if (getOpCode() == OpCode.Text) {
_payloadString = binary2Text(getBinaryPayload());
}
}
public void write(OutputStream out) throws IOException {
byte header = 0;
if (fin) {
header |= 0x80;
}
header |= opCode.getValue() & 0x0F;
out.write(header);
_payloadLength = getBinaryPayload().length;
if (_payloadLength <= 125) {
out.write(isMasked() ? 0x80 | (byte) _payloadLength : (byte) _payloadLength);
} else if (_payloadLength <= 0xFFFF) {
out.write(isMasked() ? 0xFE : 126);
out.write(_payloadLength >>> 8);
out.write(_payloadLength);
} else {
out.write(isMasked() ? 0xFF : 127);
out.write(_payloadLength >>> 56 & 0); //integer only contains 31 bit
out.write(_payloadLength >>> 48 & 0);
out.write(_payloadLength >>> 40 & 0);
out.write(_payloadLength >>> 32 & 0);
out.write(_payloadLength >>> 24);
out.write(_payloadLength >>> 16);
out.write(_payloadLength >>> 8);
out.write(_payloadLength);
}
if (isMasked()) {
out.write(maskingKey);
for (int i = 0; i < _payloadLength; i++) {
out.write(getBinaryPayload()[i] ^ maskingKey[i % 4]);
}
} else {
out.write(getBinaryPayload());
}
out.flush();
}
// --------------------------------ENCODING--------------------------------
public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");
public static final CharsetDecoder TEXT_DECODER = TEXT_CHARSET.newDecoder();
public static final CharsetEncoder TEXT_ENCODER = TEXT_CHARSET.newEncoder();
public static String binary2Text(byte[] payload) throws CharacterCodingException {
return TEXT_DECODER.decode(ByteBuffer.wrap(payload)).toString();
}
public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
return TEXT_DECODER.decode(ByteBuffer.wrap(payload, offset, length)).toString();
}
public static byte[] text2Binary(String payload) throws CharacterCodingException {
return TEXT_ENCODER.encode(CharBuffer.wrap(payload)).array();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("WS[");
sb.append(getOpCode());
sb.append(", ").append(isFin() ? "fin" : "inter");
sb.append(", ").append(isMasked() ? "masked" : "unmasked");
sb.append(", ").append(payloadToString());
sb.append(']');
return sb.toString();
}
protected String payloadToString() {
if (payload == null) return "null";
else {
final StringBuilder sb = new StringBuilder();
sb.append('[').append(payload.length).append("b] ");
if (getOpCode() == OpCode.Text) {
String text = getTextPayload();
if (text.length() > 100)
sb.append(text.substring(0, 100)).append("...");
else
sb.append(text);
} else {
sb.append("0x");
for (int i = 0; i < Math.min(payload.length, 50); ++i)
sb.append(Integer.toHexString((int) payload[i] & 0xFF));
if (payload.length > 50)
sb.append("...");
}
return sb.toString();
}
}
// --------------------------------CONSTANTS-------------------------------
public static enum OpCode {
Continuation(0), Text(1), Binary(2), Close(8), Ping(9), Pong(10);
private final byte code;
private OpCode(int code) {
this.code = (byte) code;
}
public byte getValue() {
return code;
}
public boolean isControlFrame() {
return this == Close || this == Ping || this == Pong;
}
public static OpCode find(byte value) {
for (OpCode opcode : values()) {
if (opcode.getValue() == value) {
return opcode;
}
}
return null;
}
}
public static enum CloseCode {
NormalClosure(1000), GoingAway(1001), ProtocolError(1002), UnsupportedData(1003), NoStatusRcvd(1005),
AbnormalClosure(1006), InvalidFramePayloadData(1007), PolicyViolation(1008), MessageTooBig(1009),
MandatoryExt(1010), InternalServerError(1011), TLSHandshake(1015);
private final int code;
private CloseCode(int code) {
this.code = code;
}
public int getValue() {
return code;
}
public static CloseCode find(int value) {
for (CloseCode code : values()) {
if (code.getValue() == value) {
return code;
}
}
return null;
}
}
// ------------------------------------------------------------------------
public static class CloseFrame extends WebSocketFrame {
private CloseCode _closeCode;
private String _closeReason;
private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
super(wrap);
assert wrap.getOpCode() == OpCode.Close;
if (wrap.getBinaryPayload().length >= 2) {
_closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 |
(wrap.getBinaryPayload()[1] & 0xFF));
_closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
}
}
public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
super(OpCode.Close, true, generatePayload(code, closeReason));
}
private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
if (code != null) {
byte[] reasonBytes = text2Binary(closeReason);
byte[] payload = new byte[reasonBytes.length + 2];
payload[0] = (byte) ((code.getValue() >> 8) & 0xFF);
payload[1] = (byte) ((code.getValue()) & 0xFF);
System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
return payload;
} else {
return new byte[0];
}
}
protected String payloadToString() {
return (_closeCode != null ? _closeCode : "UnknownCloseCode[" + _closeCode + "]") + (_closeReason != null && !_closeReason.isEmpty() ? ": " + _closeReason : "");
}
public CloseCode getCloseCode() {
return _closeCode;
}
public String getCloseReason() {
return _closeReason;
}
}
}