| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.sdk.internal.websocket; | |
| 6 | |
| 7 import java.io.IOException; | |
| 8 import java.net.InetSocketAddress; | |
| 9 import java.util.Random; | |
| 10 import java.util.logging.Level; | |
| 11 import java.util.logging.Logger; | |
| 12 | |
| 13 import org.chromium.sdk.ConnectionLogger; | |
| 14 import org.chromium.sdk.internal.websocket.ManualLoggingSocketWrapper.LoggableIn
put; | |
| 15 import org.chromium.sdk.internal.websocket.ManualLoggingSocketWrapper.LoggableOu
tput; | |
| 16 import org.chromium.sdk.util.BasicUtil; | |
| 17 | |
| 18 /** | |
| 19 * WebSocket connection. Sends and receives messages. Implements HyBi-17 protoco
l specification. | |
| 20 * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 | |
| 21 */ | |
| 22 public class Hybi17WsConnection extends AbstractWsConnection<LoggableInput, Logg
ableOutput> { | |
| 23 private static final Logger LOGGER = Logger.getLogger(Hybi17WsConnection.class
.getName()); | |
| 24 private static final Random RANDOM = new Random(); | |
| 25 | |
| 26 /** | |
| 27 * Specifies how outgoing frames get masked. While protocol specification requ
ires that every | |
| 28 * outgoing frame must be masked (to disable provocative content that socket c
lient may send), | |
| 29 * this doesn't really make sense when the client is trusted. On the other han
d, transparent mask | |
| 30 * makes debug sniffering easier. | |
| 31 */ | |
| 32 public enum MaskStrategy { | |
| 33 /** | |
| 34 * Directs to use no mask at all. This is explicitly against protocol specif
ication, peer | |
| 35 * is expected to terminate connection in response. | |
| 36 */ | |
| 37 NO_MASK() { | |
| 38 @Override public byte[] generate() { | |
| 39 return null; | |
| 40 } | |
| 41 | |
| 42 @Override | |
| 43 ManualLoggingSocketWrapper.FactoryBase getLogWrapperFactory() { | |
| 44 return ManualLoggingSocketWrapper.PLAIN_ASCII; | |
| 45 } | |
| 46 }, | |
| 47 /** | |
| 48 * Directs to always use transparent mask (i.e. all zeroes). This makes all
frames clear-text. | |
| 49 * Not suitable when untrusted client uses the WebSocket. | |
| 50 */ | |
| 51 TRANSPARENT_MASK() { | |
| 52 private final byte[] bytes = new byte[4]; | |
| 53 | |
| 54 @Override public byte[] generate() { | |
| 55 return bytes; | |
| 56 } | |
| 57 | |
| 58 @Override | |
| 59 ManualLoggingSocketWrapper.FactoryBase getLogWrapperFactory() { | |
| 60 return ManualLoggingSocketWrapper.PLAIN_ASCII; | |
| 61 } | |
| 62 }, | |
| 63 /** | |
| 64 * Directs to use randomly generated masks as specified by specification. As
a by-product makes | |
| 65 * traffic hard to sniff. | |
| 66 */ | |
| 67 NORMAL_MASK() { | |
| 68 @Override | |
| 69 byte[] generate() { | |
| 70 byte[] result = new byte[4]; | |
| 71 RANDOM.nextBytes(result); | |
| 72 return result; | |
| 73 } | |
| 74 | |
| 75 @Override | |
| 76 ManualLoggingSocketWrapper.FactoryBase getLogWrapperFactory() { | |
| 77 return ManualLoggingSocketWrapper.ANNOTATED; | |
| 78 } | |
| 79 }; | |
| 80 | |
| 81 /** @return 4-byte array or null */ | |
| 82 abstract byte[] generate(); | |
| 83 | |
| 84 abstract ManualLoggingSocketWrapper.FactoryBase getLogWrapperFactory(); | |
| 85 } | |
| 86 | |
| 87 public static Hybi17WsConnection connect(InetSocketAddress endpoint, int timeo
ut, | |
| 88 String resourceId, MaskStrategy maskStrategy, ConnectionLogger connectionL
ogger) | |
| 89 throws IOException { | |
| 90 ManualLoggingSocketWrapper socketWrapper = new ManualLoggingSocketWrapper(en
dpoint, timeout, | |
| 91 connectionLogger, maskStrategy.getLogWrapperFactory()); | |
| 92 | |
| 93 boolean handshakeDone = false; | |
| 94 Exception handshakeException = null; | |
| 95 try { | |
| 96 performHandshakeOrFail(socketWrapper, endpoint, resourceId); | |
| 97 handshakeDone = true; | |
| 98 } catch (RuntimeException e) { | |
| 99 handshakeException = e; | |
| 100 throw e; | |
| 101 } catch (IOException e) { | |
| 102 handshakeException = e; | |
| 103 throw e; | |
| 104 } finally { | |
| 105 if (!handshakeDone) { | |
| 106 socketWrapper.getShutdownRelay().sendSignal(null, handshakeException); | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 return new Hybi17WsConnection(socketWrapper, maskStrategy, connectionLogger)
; | |
| 111 } | |
| 112 | |
| 113 private final MaskStrategy maskStrategy; | |
| 114 | |
| 115 private Hybi17WsConnection(ManualLoggingSocketWrapper socketWrapper, MaskStrat
egy maskStrategy, | |
| 116 ConnectionLogger connectionLogger) { | |
| 117 super(socketWrapper, connectionLogger); | |
| 118 this.maskStrategy = maskStrategy; | |
| 119 } | |
| 120 | |
| 121 @Override | |
| 122 public void sendTextualMessage(final String message) throws IOException { | |
| 123 final byte[] bytes = message.getBytes(UTF_8_CHARSET); | |
| 124 | |
| 125 LoggablePayload payload = new LoggablePayload() { | |
| 126 @Override void send(LoggableOutput output, byte[] maskBytes) throws IOExce
ption { | |
| 127 output.writeToLog(message, "utf-8 demasked"); | |
| 128 if (maskBytes != null) { | |
| 129 for (int i = 0; i < bytes.length; i++) { | |
| 130 bytes[i] = (byte) (bytes[i] ^ maskBytes[i % 4]); | |
| 131 } | |
| 132 } | |
| 133 output.writeBytesNoLogging(bytes); | |
| 134 } | |
| 135 @Override int getLength() { | |
| 136 return bytes.length; | |
| 137 } | |
| 138 }; | |
| 139 | |
| 140 sendMessage(OpCode.TEXT, payload, false); | |
| 141 } | |
| 142 | |
| 143 @Override | |
| 144 protected CloseReason runListenLoop(LoggableInput loggableReader) | |
| 145 throws IOException, InterruptedException { | |
| 146 try { | |
| 147 return runListenLoopImpl(loggableReader); | |
| 148 } catch (IOException e) { | |
| 149 String stackTrace = BasicUtil.getStacktraceString(e); | |
| 150 try { | |
| 151 sendClosingMessage(StatusCode.PROTOCOL_ERROR, stackTrace); | |
| 152 } catch (IOException e2) { | |
| 153 // Connection may be closed by this time. We probably don't want to log
this exception. | |
| 154 } | |
| 155 throw new IOException(e); | |
| 156 } catch (IncomingProtocolException e) { | |
| 157 String stackTrace = BasicUtil.getStacktraceString(e); | |
| 158 sendClosingMessage(e.getStatusCode(), stackTrace); | |
| 159 throw new IOException(e); | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 private CloseReason runListenLoopImpl(LoggableInput loggableReader) | |
| 164 throws IOException, InterruptedException, IncomingProtocolException { | |
| 165 while (true) { | |
| 166 loggableReader.markSeparatorForLog(); | |
| 167 int firstByte; | |
| 168 try { | |
| 169 firstByte = loggableReader.readByteOrEos(); | |
| 170 } catch (IOException e) { | |
| 171 if (isClosingGracefully()) { | |
| 172 return CloseReason.USER_REQUEST; | |
| 173 } else { | |
| 174 throw e; | |
| 175 } | |
| 176 } | |
| 177 if (firstByte == -1) { | |
| 178 if (isClosingGracefully()) { | |
| 179 return CloseReason.USER_REQUEST; | |
| 180 } else { | |
| 181 return CloseReason.REMOTE_SILENTLY_CLOSED; | |
| 182 } | |
| 183 } | |
| 184 | |
| 185 if ((firstByte & FrameBits.FIN_BIT) == 0) { | |
| 186 throw new IncomingProtocolException("Fragments unsupported", | |
| 187 StatusCode.CANNOT_ACCEPT, null); | |
| 188 } | |
| 189 if ((firstByte & FrameBits.RESERVED_MASK) != 0) { | |
| 190 throw new IncomingProtocolException("Unexpected reserved bits", | |
| 191 StatusCode.PROTOCOL_ERROR, null); | |
| 192 } | |
| 193 | |
| 194 int opcode = firstByte & FrameBits.OPCODE_MASK; | |
| 195 | |
| 196 IncomingFrameHandler frameHandler; | |
| 197 | |
| 198 switch (opcode) { | |
| 199 case OpCode.CONTINUATION: | |
| 200 throw new IncomingProtocolException("Continuation is not supported", | |
| 201 StatusCode.CANNOT_ACCEPT, null); | |
| 202 case OpCode.TEXT: | |
| 203 frameHandler = IncomingFrameHandler.TEXT_MESSAGE; | |
| 204 break; | |
| 205 case OpCode.BINARY: | |
| 206 throw new IncomingProtocolException("Binary is not supported", | |
| 207 StatusCode.CANNOT_ACCEPT, null); | |
| 208 case OpCode.CLOSE: | |
| 209 sendClosingMessage(StatusCode.NORMAL, null); | |
| 210 return CloseReason.REMOTE_CLOSE_REQUEST; | |
| 211 case OpCode.PING: | |
| 212 frameHandler = IncomingFrameHandler.PING; | |
| 213 break; | |
| 214 case OpCode.PONG: | |
| 215 frameHandler = IncomingFrameHandler.PONG; | |
| 216 break; | |
| 217 default: | |
| 218 throw new IncomingProtocolException("Unsupported opcode " + opcode, | |
| 219 StatusCode.CANNOT_ACCEPT, null); | |
| 220 } | |
| 221 | |
| 222 int secondByte = readByteOfFail(loggableReader); | |
| 223 | |
| 224 boolean hasMask = (secondByte & FrameBits.MASK_BIT) != 0; | |
| 225 | |
| 226 if (hasMask) { | |
| 227 throw new IncomingProtocolException("Masked server-to-client message is
not supported", | |
| 228 StatusCode.PROTOCOL_ERROR, null); | |
| 229 } | |
| 230 | |
| 231 int payloadLenByte = secondByte & FrameBits.LENGTH_MASK; | |
| 232 int payloadLen; | |
| 233 if (payloadLenByte == FrameBits.LENGTH_2_BYTE_CODE) { | |
| 234 int lengthTemp = readByteOfFail(loggableReader); | |
| 235 lengthTemp <<= 8; | |
| 236 lengthTemp += readByteOfFail(loggableReader); | |
| 237 payloadLen = lengthTemp; | |
| 238 } else if (payloadLenByte == FrameBits.LENGTH_8_BYTE_CODE) { | |
| 239 for (int i = 0; i < 4; i++) { | |
| 240 int b = readByteOfFail(loggableReader); | |
| 241 if (b != 0) { | |
| 242 throw new IncomingProtocolException("Payload length is too large", | |
| 243 StatusCode.CANNOT_ACCEPT, null); | |
| 244 } | |
| 245 } | |
| 246 int lengthTemp = readByteOfFail(loggableReader); | |
| 247 if ((lengthTemp & FrameBits.HIGH_BIT) != 0) { | |
| 248 throw new IncomingProtocolException("Payload length is too large", | |
| 249 StatusCode.CANNOT_ACCEPT, null); | |
| 250 } | |
| 251 for (int i = 0; i < 3; i++) { | |
| 252 lengthTemp <<= 8; | |
| 253 lengthTemp += readByteOfFail(loggableReader); | |
| 254 } | |
| 255 payloadLen = lengthTemp; | |
| 256 } else { | |
| 257 payloadLen = payloadLenByte; | |
| 258 } | |
| 259 | |
| 260 byte [] bytes = loggableReader.readBytes(payloadLen); | |
| 261 frameHandler.process(bytes, this); | |
| 262 } | |
| 263 } | |
| 264 | |
| 265 private static class IncomingProtocolException extends Exception { | |
| 266 private final int statusCode; | |
| 267 | |
| 268 private IncomingProtocolException(String message, int statusCode, Throwable
cause) { | |
| 269 super(message, cause); | |
| 270 this.statusCode = statusCode; | |
| 271 } | |
| 272 | |
| 273 int getStatusCode() { | |
| 274 return statusCode; | |
| 275 } | |
| 276 } | |
| 277 | |
| 278 private static abstract class IncomingFrameHandler { | |
| 279 abstract void process(byte[] bytes, Hybi17WsConnection hybiWsConnection); | |
| 280 | |
| 281 static final IncomingFrameHandler TEXT_MESSAGE = new IncomingFrameHandler()
{ | |
| 282 @Override | |
| 283 void process(byte[] bytes, Hybi17WsConnection hybiWsConnection) { | |
| 284 final String text = new String(bytes, UTF_8_CHARSET); | |
| 285 hybiWsConnection.getDispatchQueue().add(new MessageDispatcher() { | |
| 286 @Override | |
| 287 boolean dispatch(Listener userListener) { | |
| 288 userListener.textMessageRecieved(text); | |
| 289 return false; | |
| 290 } | |
| 291 }); | |
| 292 } | |
| 293 }; | |
| 294 | |
| 295 static final IncomingFrameHandler PING = new IncomingFrameHandler() { | |
| 296 @Override | |
| 297 void process(final byte[] bytes, Hybi17WsConnection hybiWsConnection) { | |
| 298 LoggablePayload payload = new LoggablePayload() { | |
| 299 @Override | |
| 300 void send(LoggableOutput output, byte[] maskBytes) throws IOException
{ | |
| 301 output.writeBytesToLog(bytes); | |
| 302 if (maskBytes != null) { | |
| 303 for (int i = 0; i < bytes.length; i++) { | |
| 304 bytes[i] = (byte) (bytes[i] ^ maskBytes[i % 4]); | |
| 305 } | |
| 306 } | |
| 307 output.writeBytes(bytes); | |
| 308 output.markSeparatorForLog(); | |
| 309 } | |
| 310 @Override int getLength() { | |
| 311 return bytes.length; | |
| 312 } | |
| 313 }; | |
| 314 try { | |
| 315 // Should we do in this thread or relay it to Dispatch thread? | |
| 316 hybiWsConnection.sendMessage(OpCode.PONG, payload, false); | |
| 317 } catch (IOException e) { | |
| 318 LOGGER.log(Level.WARNING, "Failed to send pong", e); | |
| 319 } | |
| 320 } | |
| 321 }; | |
| 322 | |
| 323 static final IncomingFrameHandler PONG = new IncomingFrameHandler() { | |
| 324 @Override | |
| 325 void process(byte[] bytes, Hybi17WsConnection hybiWsConnection) { | |
| 326 // Ignore | |
| 327 } | |
| 328 }; | |
| 329 } | |
| 330 | |
| 331 /** | |
| 332 * Payload that can send and properly log itself. Good logging requires that t
he body | |
| 333 * is not masked. | |
| 334 */ | |
| 335 private static abstract class LoggablePayload { | |
| 336 abstract void send(LoggableOutput output, byte[] maskBytes) throws IOExcepti
on; | |
| 337 abstract int getLength(); | |
| 338 } | |
| 339 | |
| 340 private void sendClosingMessage(final int statusCode, final String message) th
rows IOException { | |
| 341 final byte[] bytes; | |
| 342 if (message == null) { | |
| 343 bytes = new byte[0]; | |
| 344 } else { | |
| 345 bytes = message.getBytes(UTF_8_CHARSET); | |
| 346 } | |
| 347 | |
| 348 LoggablePayload payload = new LoggablePayload() { | |
| 349 @Override | |
| 350 void send(LoggableOutput output, byte[] maskBytes) throws IOException { | |
| 351 byte codeByte1 = (byte) ((statusCode >> 8) & 0xFF); | |
| 352 byte codeByte2 = (byte) (statusCode & 0xFF); | |
| 353 | |
| 354 byte codeByteMasked1 = codeByte1; | |
| 355 byte codeByteMasked2 = codeByte2; | |
| 356 if (maskBytes != null) { | |
| 357 codeByteMasked1 ^= maskBytes[0]; | |
| 358 codeByteMasked2 ^= maskBytes[1]; | |
| 359 | |
| 360 for (int i = 0; i < bytes.length; i++) { | |
| 361 bytes[i] = (byte) (bytes[i] ^ maskBytes[(i + STATUS_CODE_LENTGH) % 4
]); | |
| 362 } | |
| 363 } | |
| 364 output.writeByteNoLogging(codeByteMasked1); | |
| 365 output.writeByteNoLogging(codeByteMasked2); | |
| 366 | |
| 367 output.writeByteToLog(codeByte1); | |
| 368 output.writeByteToLog(codeByte2); | |
| 369 | |
| 370 output.writeBytesNoLogging(bytes); | |
| 371 output.writeToLog(message, "utf-8 demasked"); | |
| 372 } | |
| 373 | |
| 374 @Override int getLength() { | |
| 375 return STATUS_CODE_LENTGH + bytes.length; | |
| 376 } | |
| 377 }; | |
| 378 | |
| 379 sendMessage(OpCode.CLOSE, payload, true); | |
| 380 } | |
| 381 | |
| 382 private void sendMessage(int opCode, LoggablePayload loggablePayload, boolean
isClosingMessage) | |
| 383 throws IOException { | |
| 384 int length = loggablePayload.getLength(); | |
| 385 LoggableOutput output = getSocketWrapper().getLoggableOutput(); | |
| 386 | |
| 387 byte[] maskBytes = maskStrategy.generate(); | |
| 388 | |
| 389 synchronized (this) { | |
| 390 if (isOutputClosed()) { | |
| 391 throw new IOException("WebSocket is already closed for output"); | |
| 392 } | |
| 393 | |
| 394 if (isClosingMessage) { | |
| 395 // Close it before actually sending, because we can fail on it. | |
| 396 setOutputClosed(true); | |
| 397 } | |
| 398 | |
| 399 byte firstByte = (byte) (FrameBits.FIN_BIT | OpCode.TEXT); | |
| 400 | |
| 401 output.writeByte(firstByte); | |
| 402 | |
| 403 int maskFlag = maskBytes == null ? 0 : FrameBits.MASK_BIT; | |
| 404 | |
| 405 if (length <= 125) { | |
| 406 output.writeByte((byte) (length | maskFlag)); | |
| 407 } else if (length <= FrameBits.MAX_TWO_BYTE_INT) { | |
| 408 output.writeByte((byte) (FrameBits.LENGTH_2_BYTE_CODE | maskFlag)); | |
| 409 output.writeByte((byte) ((length >> 8) & 0xFF)); | |
| 410 output.writeByte((byte) (length & 0xFF)); | |
| 411 } else { | |
| 412 output.writeByte((byte) (FrameBits.LENGTH_8_BYTE_CODE | maskFlag)); | |
| 413 output.writeByte((byte) 0); | |
| 414 output.writeByte((byte) 0); | |
| 415 output.writeByte((byte) 0); | |
| 416 output.writeByte((byte) 0); | |
| 417 output.writeByte((byte) (length >>> 24)); | |
| 418 output.writeByte((byte) ((length >> 16) & 0xFF)); | |
| 419 output.writeByte((byte) ((length >> 8) & 0xFF)); | |
| 420 output.writeByte((byte) (length & 0xFF)); | |
| 421 } | |
| 422 | |
| 423 if (maskBytes != null) { | |
| 424 output.writeBytes(maskBytes); | |
| 425 } | |
| 426 loggablePayload.send(output, maskBytes); | |
| 427 } | |
| 428 | |
| 429 output.markSeparatorForLog(); | |
| 430 } | |
| 431 | |
| 432 private static void performHandshakeOrFail(ManualLoggingSocketWrapper socket, | |
| 433 InetSocketAddress endpoint, String resourceId) throws IOException { | |
| 434 Hybi17Handshake.Result result = | |
| 435 Hybi17Handshake.performHandshake(socket, endpoint, resourceId, RANDOM); | |
| 436 result.accept(HANDSHAKE_RESULT_VISITOR).get(); | |
| 437 } | |
| 438 | |
| 439 private static final Hybi17Handshake.Result.Visitor<DataOrException<Void>> | |
| 440 HANDSHAKE_RESULT_VISITOR = | |
| 441 new Hybi17Handshake.Result.Visitor<DataOrException<Void>>() { | |
| 442 @Override | |
| 443 public DataOrException<Void> visitConnected() { | |
| 444 return new DataOrException<Void>() { | |
| 445 @Override Void get() throws IOException { | |
| 446 return null; | |
| 447 } | |
| 448 }; | |
| 449 } | |
| 450 | |
| 451 @Override | |
| 452 public DataOrException<Void> visitUnknownError(final Exception exception
) { | |
| 453 return new DataOrException<Void>() { | |
| 454 @Override Void get() throws IOException { | |
| 455 throw new IOException("Failed to establish WebSocket connection",
exception); | |
| 456 } | |
| 457 }; | |
| 458 } | |
| 459 | |
| 460 @Override | |
| 461 public DataOrException<Void> visitErrorMessage(final int code, | |
| 462 final String errorName, final String text) { | |
| 463 return new DataOrException<Void>() { | |
| 464 @Override Void get() throws IOException { | |
| 465 throw new IOException("Failed to establish WebSocket connection: "
+ code + " " + | |
| 466 errorName + " | " + text); | |
| 467 } | |
| 468 }; | |
| 469 } | |
| 470 }; | |
| 471 | |
| 472 /** | |
| 473 * This class is used solely to put IOException through Visitor. | |
| 474 */ | |
| 475 private static abstract class DataOrException<T> { | |
| 476 abstract T get() throws IOException; | |
| 477 } | |
| 478 | |
| 479 private static int readByteOfFail(LoggableInput loggableReader) throws IOExcep
tion { | |
| 480 int b = loggableReader.readByteOrEos(); | |
| 481 if (b == -1) { | |
| 482 throw new IOException("Unexpected EOS"); | |
| 483 } | |
| 484 return b; | |
| 485 } | |
| 486 | |
| 487 private interface FrameBits { | |
| 488 // First byte bits. | |
| 489 int FIN_BIT = 1 << 7; | |
| 490 int MASK_BIT = 1 << 7; | |
| 491 | |
| 492 // Second byte bits. | |
| 493 int OPCODE_LENGTH = 4; | |
| 494 int OPCODE_MASK = (1 << OPCODE_LENGTH) - 1; | |
| 495 int RESERVED_MASK = ((1 << 3) - 1) << OPCODE_LENGTH ; | |
| 496 | |
| 497 int LENGTH_MASK = (1 << 7) - 1; | |
| 498 int LENGTH_2_BYTE_CODE = 126; | |
| 499 int LENGTH_8_BYTE_CODE = 127; | |
| 500 | |
| 501 // Length bytes. | |
| 502 int HIGH_BIT = 1 << 7; | |
| 503 int MAX_TWO_BYTE_INT = 1 << 16 - 1; | |
| 504 } | |
| 505 | |
| 506 private interface OpCode { | |
| 507 int CONTINUATION = 0x0; | |
| 508 int TEXT = 0x1; | |
| 509 int BINARY = 0x2; | |
| 510 int CLOSE = 0x8; | |
| 511 int PING = 0x9; | |
| 512 int PONG = 0xA; | |
| 513 } | |
| 514 | |
| 515 private interface StatusCode { | |
| 516 int NORMAL = 1000; | |
| 517 int PROTOCOL_ERROR = 1002; | |
| 518 int CANNOT_ACCEPT = 1003; | |
| 519 } | |
| 520 | |
| 521 private static final int STATUS_CODE_LENTGH = 2; | |
| 522 } | |
| OLD | NEW |