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 }