package jdk.incubator.http.internal.websocket;
import jdk.incubator.http.internal.websocket.Frame.Opcode;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.security.SecureRandom;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static jdk.incubator.http.internal.common.Utils.EMPTY_BYTEBUFFER;
import static jdk.incubator.http.internal.websocket.Frame.MAX_HEADER_SIZE_BYTES;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.BINARY;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.CLOSE;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.CONTINUATION;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.PING;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.PONG;
import static jdk.incubator.http.internal.websocket.Frame.Opcode.TEXT;
abstract class OutgoingMessage {
private static final SecureRandom maskingKeys = new SecureRandom();
protected ByteBuffer[] frame;
protected int offset;
protected boolean contextualize(Context context) {
if (context.isCloseSent()) {
throw new IllegalStateException("Close sent");
}
return true;
}
protected boolean sendTo(RawChannel channel) throws IOException {
while ((offset = nextUnwrittenIndex()) != -1) {
long n = channel.write(frame, offset, frame.length - offset);
if (n == 0) {
return false;
}
}
return true;
}
private int nextUnwrittenIndex() {
for (int i = offset; i < frame.length; i++) {
if (frame[i].hasRemaining()) {
return i;
}
}
return -1;
}
static final class Text extends OutgoingMessage {
private final ByteBuffer payload;
private final boolean isLast;
Text(CharSequence characters, boolean isLast) {
CharsetEncoder encoder = UTF_8.newEncoder();
try {
payload = encoder.encode(CharBuffer.wrap(characters));
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(
"Malformed UTF-8 text message");
}
this.isLast = isLast;
}
@Override
protected boolean contextualize(Context context) {
super.contextualize(context);
if (context.isPreviousBinary() && !context.isPreviousLast()) {
throw new IllegalStateException("Unexpected text message");
}
frame = getDataMessageBuffers(
TEXT, context.isPreviousLast(), isLast, payload, payload);
context.setPreviousBinary(false);
context.setPreviousText(true);
context.setPreviousLast(isLast);
return true;
}
}
static final class Binary extends OutgoingMessage {
private final ByteBuffer payload;
private final boolean isLast;
Binary(ByteBuffer payload, boolean isLast) {
this.payload = requireNonNull(payload);
this.isLast = isLast;
}
@Override
protected boolean contextualize(Context context) {
super.contextualize(context);
if (context.isPreviousText() && !context.isPreviousLast()) {
throw new IllegalStateException("Unexpected binary message");
}
ByteBuffer newBuffer = ByteBuffer.allocate(payload.remaining());
frame = getDataMessageBuffers(
BINARY, context.isPreviousLast(), isLast, payload, newBuffer);
context.setPreviousText(false);
context.setPreviousBinary(true);
context.setPreviousLast(isLast);
return true;
}
}
static final class Ping extends OutgoingMessage {
Ping(ByteBuffer payload) {
frame = getControlMessageBuffers(PING, payload);
}
}
static final class Pong extends OutgoingMessage {
Pong(ByteBuffer payload) {
frame = getControlMessageBuffers(PONG, payload);
}
}
static final class Close extends OutgoingMessage {
Close() {
frame = getControlMessageBuffers(CLOSE, EMPTY_BYTEBUFFER);
}
Close(int statusCode, CharSequence reason) {
ByteBuffer payload = ByteBuffer.allocate(125)
.putChar((char) statusCode);
CoderResult result = UTF_8.newEncoder()
.encode(CharBuffer.wrap(reason),
payload,
true);
if (result.isOverflow()) {
throw new IllegalArgumentException("Long reason");
} else if (result.isError()) {
try {
result.throwException();
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(
"Malformed UTF-8 reason", e);
}
}
payload.flip();
frame = getControlMessageBuffers(CLOSE, payload);
}
@Override
protected boolean contextualize(Context context) {
if (context.isCloseSent()) {
return false;
} else {
context.setCloseSent();
return true;
}
}
}
private static ByteBuffer[] getControlMessageBuffers(Opcode opcode,
ByteBuffer payload) {
assert opcode.isControl() : opcode;
int remaining = payload.remaining();
if (remaining > 125) {
throw new IllegalArgumentException
("Long message: " + remaining);
}
ByteBuffer frame = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES + remaining);
int mask = maskingKeys.nextInt();
new Frame.HeaderWriter()
.fin(true)
.opcode(opcode)
.payloadLen(remaining)
.mask(mask)
.write(frame);
Frame.Masker.transferMasking(payload, frame, mask);
frame.flip();
return new ByteBuffer[]{frame};
}
private static ByteBuffer[] getDataMessageBuffers(Opcode type,
boolean isPreviousLast,
boolean isLast,
ByteBuffer payloadSrc,
ByteBuffer payloadDst) {
assert !type.isControl() && type != CONTINUATION : type;
ByteBuffer header = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES);
int mask = maskingKeys.nextInt();
new Frame.HeaderWriter()
.fin(isLast)
.opcode(isPreviousLast ? type : CONTINUATION)
.payloadLen(payloadDst.remaining())
.mask(mask)
.write(header);
header.flip();
Frame.Masker.transferMasking(payloadSrc, payloadDst, mask);
payloadDst.flip();
return new ByteBuffer[]{header, payloadDst};
}
public static class Context {
boolean previousLast = true;
boolean previousBinary;
boolean previousText;
boolean closeSent;
private boolean isPreviousText() {
return this.previousText;
}
private void setPreviousText(boolean value) {
this.previousText = value;
}
private boolean isPreviousBinary() {
return this.previousBinary;
}
private void setPreviousBinary(boolean value) {
this.previousBinary = value;
}
private boolean isPreviousLast() {
return this.previousLast;
}
private void setPreviousLast(boolean value) {
this.previousLast = value;
}
private boolean isCloseSent() {
return closeSent;
}
private void setCloseSent() {
closeSent = true;
}
}
}