1 /*
   2  * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.incubator.http.internal.websocket;
  27 
  28 import jdk.incubator.http.internal.websocket.Frame.Opcode;
  29 
  30 import java.io.IOException;
  31 import java.nio.ByteBuffer;
  32 import java.nio.CharBuffer;
  33 import java.nio.charset.CharacterCodingException;
  34 import java.nio.charset.CharsetEncoder;
  35 import java.nio.charset.CoderResult;
  36 import java.security.SecureRandom;
  37 
  38 import static java.nio.charset.StandardCharsets.UTF_8;
  39 import static java.util.Objects.requireNonNull;
  40 import static jdk.incubator.http.internal.common.Utils.EMPTY_BYTEBUFFER;
  41 import static jdk.incubator.http.internal.websocket.Frame.MAX_HEADER_SIZE_BYTES;
  42 import static jdk.incubator.http.internal.websocket.Frame.Opcode.BINARY;
  43 import static jdk.incubator.http.internal.websocket.Frame.Opcode.CLOSE;
  44 import static jdk.incubator.http.internal.websocket.Frame.Opcode.CONTINUATION;
  45 import static jdk.incubator.http.internal.websocket.Frame.Opcode.PING;
  46 import static jdk.incubator.http.internal.websocket.Frame.Opcode.PONG;
  47 import static jdk.incubator.http.internal.websocket.Frame.Opcode.TEXT;
  48 
  49 /*
  50  * A stateful object that represents a WebSocket message being sent to the
  51  * channel.
  52  *
  53  * Data provided to the constructors is copied. Otherwise we would have to deal
  54  * with mutability, security, masking/unmasking, readonly status, etc. So
  55  * copying greatly simplifies the implementation.
  56  *
  57  * In the case of memory-sensitive environments an alternative implementation
  58  * could use an internal pool of buffers though at the cost of extra complexity
  59  * and possible performance degradation.
  60  */
  61 abstract class OutgoingMessage {
  62 
  63     // Share per WebSocket?
  64     private static final SecureRandom maskingKeys = new SecureRandom();
  65 
  66     protected ByteBuffer[] frame;
  67     protected int offset;
  68 
  69     /*
  70      * Performs contextualization. This method is not a part of the constructor
  71      * so it would be possible to defer the work it does until the most
  72      * convenient moment (up to the point where sentTo is invoked).
  73      */
  74     protected void contextualize(Context context) {
  75         // masking and charset decoding should be performed here rather than in
  76         // the constructor (as of today)
  77         if (context.isCloseSent()) {
  78             throw new IllegalStateException("Close sent");
  79         }
  80     }
  81 
  82     protected boolean sendTo(RawChannel channel) throws IOException {
  83         while ((offset = nextUnwrittenIndex()) != -1) {
  84             long n = channel.write(frame, offset, frame.length - offset);
  85             if (n == 0) {
  86                 return false;
  87             }
  88         }
  89         return true;
  90     }
  91 
  92     private int nextUnwrittenIndex() {
  93         for (int i = offset; i < frame.length; i++) {
  94             if (frame[i].hasRemaining()) {
  95                 return i;
  96             }
  97         }
  98         return -1;
  99     }
 100 
 101     static final class Text extends OutgoingMessage {
 102 
 103         private final ByteBuffer payload;
 104         private final boolean isLast;
 105 
 106         Text(CharSequence characters, boolean isLast) {
 107             CharsetEncoder encoder = UTF_8.newEncoder(); // Share per WebSocket?
 108             try {
 109                 payload = encoder.encode(CharBuffer.wrap(characters));
 110             } catch (CharacterCodingException e) {
 111                 throw new IllegalArgumentException(
 112                         "Malformed UTF-8 text message");
 113             }
 114             this.isLast = isLast;
 115         }
 116 
 117         @Override
 118         protected void contextualize(Context context) {
 119             super.contextualize(context);
 120             if (context.isPreviousBinary() && !context.isPreviousLast()) {
 121                 throw new IllegalStateException("Unexpected text message");
 122             }
 123             frame = getDataMessageBuffers(
 124                     TEXT, context.isPreviousLast(), isLast, payload, payload);
 125             context.setPreviousBinary(false);
 126             context.setPreviousText(true);
 127             context.setPreviousLast(isLast);
 128         }
 129     }
 130 
 131     static final class Binary extends OutgoingMessage {
 132 
 133         private final ByteBuffer payload;
 134         private final boolean isLast;
 135 
 136         Binary(ByteBuffer payload, boolean isLast) {
 137             this.payload = requireNonNull(payload);
 138             this.isLast = isLast;
 139         }
 140 
 141         @Override
 142         protected void contextualize(Context context) {
 143             super.contextualize(context);
 144             if (context.isPreviousText() && !context.isPreviousLast()) {
 145                 throw new IllegalStateException("Unexpected binary message");
 146             }
 147             ByteBuffer newBuffer = ByteBuffer.allocate(payload.remaining());
 148             frame = getDataMessageBuffers(
 149                     BINARY, context.isPreviousLast(), isLast, payload, newBuffer);
 150             context.setPreviousText(false);
 151             context.setPreviousBinary(true);
 152             context.setPreviousLast(isLast);
 153         }
 154     }
 155 
 156     static final class Ping extends OutgoingMessage {
 157 
 158         Ping(ByteBuffer payload) {
 159             frame = getControlMessageBuffers(PING, payload);
 160         }
 161     }
 162 
 163     static final class Pong extends OutgoingMessage {
 164 
 165         Pong(ByteBuffer payload) {
 166             frame = getControlMessageBuffers(PONG, payload);
 167         }
 168     }
 169 
 170     static final class Close extends OutgoingMessage {
 171 
 172         Close() {
 173             frame = getControlMessageBuffers(CLOSE, EMPTY_BYTEBUFFER);
 174         }
 175 
 176         Close(int statusCode, CharSequence reason) {
 177             ByteBuffer payload = ByteBuffer.allocate(125)
 178                     .putChar((char) statusCode);
 179             CoderResult result = UTF_8.newEncoder()
 180                     .encode(CharBuffer.wrap(reason),
 181                             payload,
 182                             true);
 183             if (result.isOverflow()) {
 184                 throw new IllegalArgumentException("Long reason");
 185             } else if (result.isError()) {
 186                 try {
 187                     result.throwException();
 188                 } catch (CharacterCodingException e) {
 189                     throw new IllegalArgumentException(
 190                             "Malformed UTF-8 reason", e);
 191                 }
 192             }
 193             payload.flip();
 194             frame = getControlMessageBuffers(CLOSE, payload);
 195         }
 196 
 197         @Override
 198         protected void contextualize(Context context) {
 199             super.contextualize(context);
 200             context.setCloseSent();
 201         }
 202     }
 203 
 204     private static ByteBuffer[] getControlMessageBuffers(Opcode opcode,
 205                                                          ByteBuffer payload) {
 206         assert opcode.isControl() : opcode;
 207         int remaining = payload.remaining();
 208         if (remaining > 125) {
 209             throw new IllegalArgumentException
 210                     ("Long message: " + remaining);
 211         }
 212         ByteBuffer frame = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES + remaining);
 213         int mask = maskingKeys.nextInt();
 214         new Frame.HeaderWriter()
 215                 .fin(true)
 216                 .opcode(opcode)
 217                 .payloadLen(remaining)
 218                 .mask(mask)
 219                 .write(frame);
 220         Frame.Masker.transferMasking(payload, frame, mask);
 221         frame.flip();
 222         return new ByteBuffer[]{frame};
 223     }
 224 
 225     private static ByteBuffer[] getDataMessageBuffers(Opcode type,
 226                                                       boolean isPreviousLast,
 227                                                       boolean isLast,
 228                                                       ByteBuffer payloadSrc,
 229                                                       ByteBuffer payloadDst) {
 230         assert !type.isControl() && type != CONTINUATION : type;
 231         ByteBuffer header = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES);
 232         int mask = maskingKeys.nextInt();
 233         new Frame.HeaderWriter()
 234                 .fin(isLast)
 235                 .opcode(isPreviousLast ? type : CONTINUATION)
 236                 .payloadLen(payloadDst.remaining())
 237                 .mask(mask)
 238                 .write(header);
 239         header.flip();
 240         Frame.Masker.transferMasking(payloadSrc, payloadDst, mask);
 241         payloadDst.flip();
 242         return new ByteBuffer[]{header, payloadDst};
 243     }
 244 
 245     /*
 246      * An instance of this class is passed sequentially between messages, so
 247      * every message in a sequence can check the context it is in and update it
 248      * if necessary.
 249      */
 250     public static class Context {
 251 
 252         boolean previousLast = true;
 253         boolean previousBinary;
 254         boolean previousText;
 255         boolean closeSent;
 256 
 257         private boolean isPreviousText() {
 258             return this.previousText;
 259         }
 260 
 261         private void setPreviousText(boolean value) {
 262             this.previousText = value;
 263         }
 264 
 265         private boolean isPreviousBinary() {
 266             return this.previousBinary;
 267         }
 268 
 269         private void setPreviousBinary(boolean value) {
 270             this.previousBinary = value;
 271         }
 272 
 273         private boolean isPreviousLast() {
 274             return this.previousLast;
 275         }
 276 
 277         private void setPreviousLast(boolean value) {
 278             this.previousLast = value;
 279         }
 280 
 281         private boolean isCloseSent() {
 282             return closeSent;
 283         }
 284 
 285         private void setCloseSent() {
 286             closeSent = true;
 287         }
 288     }
 289 }