| /* |
| * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| 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; |
| |
| /* |
| * A stateful object that represents a WebSocket message being sent to the |
| * channel. |
| * |
| * Data provided to the constructors is copied. Otherwise we would have to deal |
| * with mutability, security, masking/unmasking, readonly status, etc. So |
| * copying greatly simplifies the implementation. |
| * |
| * In the case of memory-sensitive environments an alternative implementation |
| * could use an internal pool of buffers though at the cost of extra complexity |
| * and possible performance degradation. |
| */ |
| abstract class OutgoingMessage { |
| |
| private static final SecureRandom maskingKeys = new SecureRandom(); |
| |
| protected ByteBuffer[] frame; |
| protected int offset; |
| |
| /* |
| * Performs contextualization. This method is not a part of the constructor |
| * so it would be possible to defer the work it does until the most |
| * convenient moment (up to the point where sentTo is invoked). |
| */ |
| protected void contextualize(Context context) { |
| if (context.isCloseSent()) { |
| throw new IllegalStateException("Close sent"); |
| } |
| } |
| |
| 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 void 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); |
| } |
| } |
| |
| 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 void 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); |
| } |
| } |
| |
| 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 void contextualize(Context context) { |
| super.contextualize(context); |
| context.setCloseSent(); |
| } |
| } |
| |
| 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}; |
| } |
| |
| /* |
| * An instance of this class is passed sequentially between messages, so |
| * every message in a sequence can check the context it is in and update it |
| * if necessary. |
| */ |
| 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; |
| } |
| } |
| } |