1 package fi.iki.elonen; 2 3 /* 4 * #%L 5 * NanoHttpd-Websocket 6 * %% 7 * Copyright (C) 2012 - 2015 nanohttpd 8 * %% 9 * Redistribution and use in source and binary forms, with or without modification, 10 * are permitted provided that the following conditions are met: 11 * 12 * 1. Redistributions of source code must retain the above copyright notice, this 13 * list of conditions and the following disclaimer. 14 * 15 * 2. Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * 3. Neither the name of the nanohttpd nor the names of its contributors 20 * may be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 * OF THE POSSIBILITY OF SUCH DAMAGE. 33 * #L% 34 */ 35 36 import java.io.EOFException; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.nio.charset.CharacterCodingException; 41 import java.nio.charset.Charset; 42 import java.security.MessageDigest; 43 import java.security.NoSuchAlgorithmException; 44 import java.util.Arrays; 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.logging.Level; 49 import java.util.logging.Logger; 50 51 import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode; 52 import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame; 53 import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode; 54 55 public abstract class NanoWSD extends NanoHTTPD { 56 57 public static enum State { 58 UNCONNECTED, 59 CONNECTING, 60 OPEN, 61 CLOSING, 62 CLOSED 63 } 64 65 public static abstract class WebSocket { 66 67 private final InputStream in; 68 69 private OutputStream out; 70 71 private WebSocketFrame.OpCode continuousOpCode = null; 72 73 private final List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>(); 74 75 private State state = State.UNCONNECTED; 76 77 private final NanoHTTPD.IHTTPSession handshakeRequest; 78 79 private final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null, 0) { 80 81 @Override 82 protected void send(OutputStream out) { 83 WebSocket.this.out = out; 84 WebSocket.this.state = State.CONNECTING; 85 super.send(out); 86 WebSocket.this.state = State.OPEN; 87 WebSocket.this.onOpen(); 88 readWebsocket(); 89 } 90 }; 91 WebSocket(NanoHTTPD.IHTTPSession handshakeRequest)92 public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) { 93 this.handshakeRequest = handshakeRequest; 94 this.in = handshakeRequest.getInputStream(); 95 96 this.handshakeResponse.addHeader(NanoWSD.HEADER_UPGRADE, NanoWSD.HEADER_UPGRADE_VALUE); 97 this.handshakeResponse.addHeader(NanoWSD.HEADER_CONNECTION, NanoWSD.HEADER_CONNECTION_VALUE); 98 } 99 isOpen()100 public boolean isOpen() { 101 return state == State.OPEN; 102 } 103 onOpen()104 protected abstract void onOpen(); 105 onClose(CloseCode code, String reason, boolean initiatedByRemote)106 protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote); 107 onMessage(WebSocketFrame message)108 protected abstract void onMessage(WebSocketFrame message); 109 onPong(WebSocketFrame pong)110 protected abstract void onPong(WebSocketFrame pong); 111 onException(IOException exception)112 protected abstract void onException(IOException exception); 113 114 /** 115 * Debug method. <b>Do not Override unless for debug purposes!</b> 116 * 117 * @param frame 118 * The received WebSocket Frame. 119 */ debugFrameReceived(WebSocketFrame frame)120 protected void debugFrameReceived(WebSocketFrame frame) { 121 } 122 123 /** 124 * Debug method. <b>Do not Override unless for debug purposes!</b><br> 125 * This method is called before actually sending the frame. 126 * 127 * @param frame 128 * The sent WebSocket Frame. 129 */ debugFrameSent(WebSocketFrame frame)130 protected void debugFrameSent(WebSocketFrame frame) { 131 } 132 close(CloseCode code, String reason, boolean initiatedByRemote)133 public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException { 134 State oldState = this.state; 135 this.state = State.CLOSING; 136 if (oldState == State.OPEN) { 137 sendFrame(new CloseFrame(code, reason)); 138 } else { 139 doClose(code, reason, initiatedByRemote); 140 } 141 } 142 doClose(CloseCode code, String reason, boolean initiatedByRemote)143 private void doClose(CloseCode code, String reason, boolean initiatedByRemote) { 144 if (this.state == State.CLOSED) { 145 return; 146 } 147 if (this.in != null) { 148 try { 149 this.in.close(); 150 } catch (IOException e) { 151 NanoWSD.LOG.log(Level.FINE, "close failed", e); 152 } 153 } 154 if (this.out != null) { 155 try { 156 this.out.close(); 157 } catch (IOException e) { 158 NanoWSD.LOG.log(Level.FINE, "close failed", e); 159 } 160 } 161 this.state = State.CLOSED; 162 onClose(code, reason, initiatedByRemote); 163 } 164 165 // --------------------------------IO-------------------------------------- 166 getHandshakeRequest()167 public NanoHTTPD.IHTTPSession getHandshakeRequest() { 168 return this.handshakeRequest; 169 } 170 getHandshakeResponse()171 public NanoHTTPD.Response getHandshakeResponse() { 172 return this.handshakeResponse; 173 } 174 handleCloseFrame(WebSocketFrame frame)175 private void handleCloseFrame(WebSocketFrame frame) throws IOException { 176 CloseCode code = CloseCode.NormalClosure; 177 String reason = ""; 178 if (frame instanceof CloseFrame) { 179 code = ((CloseFrame) frame).getCloseCode(); 180 reason = ((CloseFrame) frame).getCloseReason(); 181 } 182 if (this.state == State.CLOSING) { 183 // Answer for my requested close 184 doClose(code, reason, false); 185 } else { 186 close(code, reason, true); 187 } 188 } 189 handleFrameFragment(WebSocketFrame frame)190 private void handleFrameFragment(WebSocketFrame frame) throws IOException { 191 if (frame.getOpCode() != OpCode.Continuation) { 192 // First 193 if (this.continuousOpCode != null) { 194 throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed."); 195 } 196 this.continuousOpCode = frame.getOpCode(); 197 this.continuousFrames.clear(); 198 this.continuousFrames.add(frame); 199 } else if (frame.isFin()) { 200 // Last 201 if (this.continuousOpCode == null) { 202 throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started."); 203 } 204 onMessage(new WebSocketFrame(this.continuousOpCode, this.continuousFrames)); 205 this.continuousOpCode = null; 206 this.continuousFrames.clear(); 207 } else if (this.continuousOpCode == null) { 208 // Unexpected 209 throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started."); 210 } else { 211 // Intermediate 212 this.continuousFrames.add(frame); 213 } 214 } 215 handleWebsocketFrame(WebSocketFrame frame)216 private void handleWebsocketFrame(WebSocketFrame frame) throws IOException { 217 debugFrameReceived(frame); 218 if (frame.getOpCode() == OpCode.Close) { 219 handleCloseFrame(frame); 220 } else if (frame.getOpCode() == OpCode.Ping) { 221 sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload())); 222 } else if (frame.getOpCode() == OpCode.Pong) { 223 onPong(frame); 224 } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) { 225 handleFrameFragment(frame); 226 } else if (this.continuousOpCode != null) { 227 throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed."); 228 } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) { 229 onMessage(frame); 230 } else { 231 throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected."); 232 } 233 } 234 235 // --------------------------------Close----------------------------------- 236 ping(byte[] payload)237 public void ping(byte[] payload) throws IOException { 238 sendFrame(new WebSocketFrame(OpCode.Ping, true, payload)); 239 } 240 241 // --------------------------------Public 242 // Facade--------------------------- 243 readWebsocket()244 private void readWebsocket() { 245 try { 246 while (this.state == State.OPEN) { 247 handleWebsocketFrame(WebSocketFrame.read(this.in)); 248 } 249 } catch (CharacterCodingException e) { 250 onException(e); 251 doClose(CloseCode.InvalidFramePayloadData, e.toString(), false); 252 } catch (IOException e) { 253 onException(e); 254 if (e instanceof WebSocketException) { 255 doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false); 256 } 257 } finally { 258 doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false); 259 } 260 } 261 send(byte[] payload)262 public void send(byte[] payload) throws IOException { 263 sendFrame(new WebSocketFrame(OpCode.Binary, true, payload)); 264 } 265 send(String payload)266 public void send(String payload) throws IOException { 267 sendFrame(new WebSocketFrame(OpCode.Text, true, payload)); 268 } 269 sendFrame(WebSocketFrame frame)270 public synchronized void sendFrame(WebSocketFrame frame) throws IOException { 271 debugFrameSent(frame); 272 frame.write(this.out); 273 } 274 } 275 276 public static class WebSocketException extends IOException { 277 278 private static final long serialVersionUID = 1L; 279 280 private final CloseCode code; 281 282 private final String reason; 283 WebSocketException(CloseCode code, String reason)284 public WebSocketException(CloseCode code, String reason) { 285 this(code, reason, null); 286 } 287 WebSocketException(CloseCode code, String reason, Exception cause)288 public WebSocketException(CloseCode code, String reason, Exception cause) { 289 super(code + ": " + reason, cause); 290 this.code = code; 291 this.reason = reason; 292 } 293 WebSocketException(Exception cause)294 public WebSocketException(Exception cause) { 295 this(CloseCode.InternalServerError, cause.toString(), cause); 296 } 297 getCode()298 public CloseCode getCode() { 299 return this.code; 300 } 301 getReason()302 public String getReason() { 303 return this.reason; 304 } 305 } 306 307 public static class WebSocketFrame { 308 309 public static enum CloseCode { 310 NormalClosure(1000), 311 GoingAway(1001), 312 ProtocolError(1002), 313 UnsupportedData(1003), 314 NoStatusRcvd(1005), 315 AbnormalClosure(1006), 316 InvalidFramePayloadData(1007), 317 PolicyViolation(1008), 318 MessageTooBig(1009), 319 MandatoryExt(1010), 320 InternalServerError(1011), 321 TLSHandshake(1015); 322 find(int value)323 public static CloseCode find(int value) { 324 for (CloseCode code : values()) { 325 if (code.getValue() == value) { 326 return code; 327 } 328 } 329 return null; 330 } 331 332 private final int code; 333 CloseCode(int code)334 private CloseCode(int code) { 335 this.code = code; 336 } 337 getValue()338 public int getValue() { 339 return this.code; 340 } 341 } 342 343 public static class CloseFrame extends WebSocketFrame { 344 generatePayload(CloseCode code, String closeReason)345 private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException { 346 if (code != null) { 347 byte[] reasonBytes = text2Binary(closeReason); 348 byte[] payload = new byte[reasonBytes.length + 2]; 349 payload[0] = (byte) (code.getValue() >> 8 & 0xFF); 350 payload[1] = (byte) (code.getValue() & 0xFF); 351 System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length); 352 return payload; 353 } else { 354 return new byte[0]; 355 } 356 } 357 358 private CloseCode _closeCode; 359 360 private String _closeReason; 361 CloseFrame(CloseCode code, String closeReason)362 public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException { 363 super(OpCode.Close, true, generatePayload(code, closeReason)); 364 } 365 CloseFrame(WebSocketFrame wrap)366 private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException { 367 super(wrap); 368 assert wrap.getOpCode() == OpCode.Close; 369 if (wrap.getBinaryPayload().length >= 2) { 370 this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF); 371 this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2); 372 } 373 } 374 getCloseCode()375 public CloseCode getCloseCode() { 376 return this._closeCode; 377 } 378 getCloseReason()379 public String getCloseReason() { 380 return this._closeReason; 381 } 382 } 383 384 public static enum OpCode { 385 Continuation(0), 386 Text(1), 387 Binary(2), 388 Close(8), 389 Ping(9), 390 Pong(10); 391 find(byte value)392 public static OpCode find(byte value) { 393 for (OpCode opcode : values()) { 394 if (opcode.getValue() == value) { 395 return opcode; 396 } 397 } 398 return null; 399 } 400 401 private final byte code; 402 OpCode(int code)403 private OpCode(int code) { 404 this.code = (byte) code; 405 } 406 getValue()407 public byte getValue() { 408 return this.code; 409 } 410 isControlFrame()411 public boolean isControlFrame() { 412 return this == Close || this == Ping || this == Pong; 413 } 414 } 415 416 public static final Charset TEXT_CHARSET = Charset.forName("UTF-8"); 417 binary2Text(byte[] payload)418 public static String binary2Text(byte[] payload) throws CharacterCodingException { 419 return new String(payload, WebSocketFrame.TEXT_CHARSET); 420 } 421 binary2Text(byte[] payload, int offset, int length)422 public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException { 423 return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET); 424 } 425 checkedRead(int read)426 private static int checkedRead(int read) throws IOException { 427 if (read < 0) { 428 throw new EOFException(); 429 } 430 return read; 431 } 432 read(InputStream in)433 public static WebSocketFrame read(InputStream in) throws IOException { 434 byte head = (byte) checkedRead(in.read()); 435 boolean fin = (head & 0x80) != 0; 436 OpCode opCode = OpCode.find((byte) (head & 0x0F)); 437 if ((head & 0x70) != 0) { 438 throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0."); 439 } 440 if (opCode == null) { 441 throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + "."); 442 } else if (opCode.isControlFrame() && !fin) { 443 throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame."); 444 } 445 446 WebSocketFrame frame = new WebSocketFrame(opCode, fin); 447 frame.readPayloadInfo(in); 448 frame.readPayload(in); 449 if (frame.getOpCode() == OpCode.Close) { 450 return new CloseFrame(frame); 451 } else { 452 return frame; 453 } 454 } 455 text2Binary(String payload)456 public static byte[] text2Binary(String payload) throws CharacterCodingException { 457 return payload.getBytes(WebSocketFrame.TEXT_CHARSET); 458 } 459 460 private OpCode opCode; 461 462 private boolean fin; 463 464 private byte[] maskingKey; 465 466 private byte[] payload; 467 468 // --------------------------------GETTERS--------------------------------- 469 470 private transient int _payloadLength; 471 472 private transient String _payloadString; 473 WebSocketFrame(OpCode opCode, boolean fin)474 private WebSocketFrame(OpCode opCode, boolean fin) { 475 setOpCode(opCode); 476 setFin(fin); 477 } 478 WebSocketFrame(OpCode opCode, boolean fin, byte[] payload)479 public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) { 480 this(opCode, fin, payload, null); 481 } 482 WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey)483 public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) { 484 this(opCode, fin); 485 setMaskingKey(maskingKey); 486 setBinaryPayload(payload); 487 } 488 WebSocketFrame(OpCode opCode, boolean fin, String payload)489 public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException { 490 this(opCode, fin, payload, null); 491 } 492 WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey)493 public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException { 494 this(opCode, fin); 495 setMaskingKey(maskingKey); 496 setTextPayload(payload); 497 } 498 WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments)499 public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException { 500 setOpCode(opCode); 501 setFin(true); 502 503 long _payloadLength = 0; 504 for (WebSocketFrame inter : fragments) { 505 _payloadLength += inter.getBinaryPayload().length; 506 } 507 if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) { 508 throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded."); 509 } 510 this._payloadLength = (int) _payloadLength; 511 byte[] payload = new byte[this._payloadLength]; 512 int offset = 0; 513 for (WebSocketFrame inter : fragments) { 514 System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length); 515 offset += inter.getBinaryPayload().length; 516 } 517 setBinaryPayload(payload); 518 } 519 WebSocketFrame(WebSocketFrame clone)520 public WebSocketFrame(WebSocketFrame clone) { 521 setOpCode(clone.getOpCode()); 522 setFin(clone.isFin()); 523 setBinaryPayload(clone.getBinaryPayload()); 524 setMaskingKey(clone.getMaskingKey()); 525 } 526 getBinaryPayload()527 public byte[] getBinaryPayload() { 528 return this.payload; 529 } 530 getMaskingKey()531 public byte[] getMaskingKey() { 532 return this.maskingKey; 533 } 534 getOpCode()535 public OpCode getOpCode() { 536 return this.opCode; 537 } 538 539 // --------------------------------SERIALIZATION--------------------------- 540 getTextPayload()541 public String getTextPayload() { 542 if (this._payloadString == null) { 543 try { 544 this._payloadString = binary2Text(getBinaryPayload()); 545 } catch (CharacterCodingException e) { 546 throw new RuntimeException("Undetected CharacterCodingException", e); 547 } 548 } 549 return this._payloadString; 550 } 551 isFin()552 public boolean isFin() { 553 return this.fin; 554 } 555 isMasked()556 public boolean isMasked() { 557 return this.maskingKey != null && this.maskingKey.length == 4; 558 } 559 payloadToString()560 private String payloadToString() { 561 if (this.payload == null) { 562 return "null"; 563 } else { 564 final StringBuilder sb = new StringBuilder(); 565 sb.append('[').append(this.payload.length).append("b] "); 566 if (getOpCode() == OpCode.Text) { 567 String text = getTextPayload(); 568 if (text.length() > 100) { 569 sb.append(text.substring(0, 100)).append("..."); 570 } else { 571 sb.append(text); 572 } 573 } else { 574 sb.append("0x"); 575 for (int i = 0; i < Math.min(this.payload.length, 50); ++i) { 576 sb.append(Integer.toHexString(this.payload[i] & 0xFF)); 577 } 578 if (this.payload.length > 50) { 579 sb.append("..."); 580 } 581 } 582 return sb.toString(); 583 } 584 } 585 readPayload(InputStream in)586 private void readPayload(InputStream in) throws IOException { 587 this.payload = new byte[this._payloadLength]; 588 int read = 0; 589 while (read < this._payloadLength) { 590 read += checkedRead(in.read(this.payload, read, this._payloadLength - read)); 591 } 592 593 if (isMasked()) { 594 for (int i = 0; i < this.payload.length; i++) { 595 this.payload[i] ^= this.maskingKey[i % 4]; 596 } 597 } 598 599 // Test for Unicode errors 600 if (getOpCode() == OpCode.Text) { 601 this._payloadString = binary2Text(getBinaryPayload()); 602 } 603 } 604 605 // --------------------------------ENCODING-------------------------------- 606 readPayloadInfo(InputStream in)607 private void readPayloadInfo(InputStream in) throws IOException { 608 byte b = (byte) checkedRead(in.read()); 609 boolean masked = (b & 0x80) != 0; 610 611 this._payloadLength = (byte) (0x7F & b); 612 if (this._payloadLength == 126) { 613 // checkedRead must return int for this to work 614 this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF; 615 if (this._payloadLength < 126) { 616 throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)"); 617 } 618 } else if (this._payloadLength == 127) { 619 long _payloadLength = 620 (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32 621 | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read()); 622 if (_payloadLength < 65536) { 623 throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)"); 624 } 625 if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) { 626 throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded."); 627 } 628 this._payloadLength = (int) _payloadLength; 629 } 630 631 if (this.opCode.isControlFrame()) { 632 if (this._payloadLength > 125) { 633 throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes."); 634 } 635 if (this.opCode == OpCode.Close && this._payloadLength == 1) { 636 throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1."); 637 } 638 } 639 640 if (masked) { 641 this.maskingKey = new byte[4]; 642 int read = 0; 643 while (read < this.maskingKey.length) { 644 read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read)); 645 } 646 } 647 } 648 setBinaryPayload(byte[] payload)649 public void setBinaryPayload(byte[] payload) { 650 this.payload = payload; 651 this._payloadLength = payload.length; 652 this._payloadString = null; 653 } 654 setFin(boolean fin)655 public void setFin(boolean fin) { 656 this.fin = fin; 657 } 658 setMaskingKey(byte[] maskingKey)659 public void setMaskingKey(byte[] maskingKey) { 660 if (maskingKey != null && maskingKey.length != 4) { 661 throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4"); 662 } 663 this.maskingKey = maskingKey; 664 } 665 setOpCode(OpCode opcode)666 public void setOpCode(OpCode opcode) { 667 this.opCode = opcode; 668 } 669 setTextPayload(String payload)670 public void setTextPayload(String payload) throws CharacterCodingException { 671 this.payload = text2Binary(payload); 672 this._payloadLength = payload.length(); 673 this._payloadString = payload; 674 } 675 676 // --------------------------------CONSTANTS------------------------------- 677 setUnmasked()678 public void setUnmasked() { 679 setMaskingKey(null); 680 } 681 682 @Override toString()683 public String toString() { 684 final StringBuilder sb = new StringBuilder("WS["); 685 sb.append(getOpCode()); 686 sb.append(", ").append(isFin() ? "fin" : "inter"); 687 sb.append(", ").append(isMasked() ? "masked" : "unmasked"); 688 sb.append(", ").append(payloadToString()); 689 sb.append(']'); 690 return sb.toString(); 691 } 692 693 // ------------------------------------------------------------------------ 694 write(OutputStream out)695 public void write(OutputStream out) throws IOException { 696 byte header = 0; 697 if (this.fin) { 698 header |= 0x80; 699 } 700 header |= this.opCode.getValue() & 0x0F; 701 out.write(header); 702 703 this._payloadLength = getBinaryPayload().length; 704 if (this._payloadLength <= 125) { 705 out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength); 706 } else if (this._payloadLength <= 0xFFFF) { 707 out.write(isMasked() ? 0xFE : 126); 708 out.write(this._payloadLength >>> 8); 709 out.write(this._payloadLength); 710 } else { 711 out.write(isMasked() ? 0xFF : 127); 712 out.write(this._payloadLength >>> 56 & 0); // integer only 713 // contains 714 // 31 bit 715 out.write(this._payloadLength >>> 48 & 0); 716 out.write(this._payloadLength >>> 40 & 0); 717 out.write(this._payloadLength >>> 32 & 0); 718 out.write(this._payloadLength >>> 24); 719 out.write(this._payloadLength >>> 16); 720 out.write(this._payloadLength >>> 8); 721 out.write(this._payloadLength); 722 } 723 724 if (isMasked()) { 725 out.write(this.maskingKey); 726 for (int i = 0; i < this._payloadLength; i++) { 727 out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]); 728 } 729 } else { 730 out.write(getBinaryPayload()); 731 } 732 out.flush(); 733 } 734 } 735 736 /** 737 * logger to log to. 738 */ 739 private static final Logger LOG = Logger.getLogger(NanoWSD.class.getName()); 740 741 public static final String HEADER_UPGRADE = "upgrade"; 742 743 public static final String HEADER_UPGRADE_VALUE = "websocket"; 744 745 public static final String HEADER_CONNECTION = "connection"; 746 747 public static final String HEADER_CONNECTION_VALUE = "Upgrade"; 748 749 public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version"; 750 751 public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13"; 752 753 public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key"; 754 755 public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept"; 756 757 public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol"; 758 759 private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 760 761 private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); 762 763 /** 764 * Translates the specified byte array into Base64 string. 765 * <p> 766 * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8 767 * hast java.util.Base64, I have this from stackoverflow: 768 * http://stackoverflow.com/a/4265472 769 * </p> 770 * 771 * @param buf 772 * the byte array (not null) 773 * @return the translated Base64 string (not null) 774 */ encodeBase64(byte[] buf)775 private static String encodeBase64(byte[] buf) { 776 int size = buf.length; 777 char[] ar = new char[(size + 2) / 3 * 4]; 778 int a = 0; 779 int i = 0; 780 while (i < size) { 781 byte b0 = buf[i++]; 782 byte b1 = i < size ? buf[i++] : 0; 783 byte b2 = i < size ? buf[i++] : 0; 784 785 int mask = 0x3F; 786 ar[a++] = NanoWSD.ALPHABET[b0 >> 2 & mask]; 787 ar[a++] = NanoWSD.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask]; 788 ar[a++] = NanoWSD.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask]; 789 ar[a++] = NanoWSD.ALPHABET[b2 & mask]; 790 } 791 switch (size % 3) { 792 case 1: 793 ar[--a] = '='; 794 case 2: 795 ar[--a] = '='; 796 } 797 return new String(ar); 798 } 799 makeAcceptKey(String key)800 public static String makeAcceptKey(String key) throws NoSuchAlgorithmException { 801 MessageDigest md = MessageDigest.getInstance("SHA-1"); 802 String text = key + NanoWSD.WEBSOCKET_KEY_MAGIC; 803 md.update(text.getBytes(), 0, text.length()); 804 byte[] sha1hash = md.digest(); 805 return encodeBase64(sha1hash); 806 } 807 NanoWSD(int port)808 public NanoWSD(int port) { 809 super(port); 810 } 811 NanoWSD(String hostname, int port)812 public NanoWSD(String hostname, int port) { 813 super(hostname, port); 814 } 815 isWebSocketConnectionHeader(Map<String, String> headers)816 private boolean isWebSocketConnectionHeader(Map<String, String> headers) { 817 String connection = headers.get(NanoWSD.HEADER_CONNECTION); 818 return connection != null && connection.toLowerCase().contains(NanoWSD.HEADER_CONNECTION_VALUE.toLowerCase()); 819 } 820 isWebsocketRequested(IHTTPSession session)821 protected boolean isWebsocketRequested(IHTTPSession session) { 822 Map<String, String> headers = session.getHeaders(); 823 String upgrade = headers.get(NanoWSD.HEADER_UPGRADE); 824 boolean isCorrectConnection = isWebSocketConnectionHeader(headers); 825 boolean isUpgrade = NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade); 826 return isUpgrade && isCorrectConnection; 827 } 828 829 // --------------------------------Listener-------------------------------- 830 openWebSocket(IHTTPSession handshake)831 protected abstract WebSocket openWebSocket(IHTTPSession handshake); 832 833 @Override serve(final IHTTPSession session)834 public Response serve(final IHTTPSession session) { 835 Map<String, String> headers = session.getHeaders(); 836 if (isWebsocketRequested(session)) { 837 if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) { 838 return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, 839 "Invalid Websocket-Version " + headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION)); 840 } 841 842 if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) { 843 return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key"); 844 } 845 846 WebSocket webSocket = openWebSocket(session); 847 Response handshakeResponse = webSocket.getHandshakeResponse(); 848 try { 849 handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWSD.HEADER_WEBSOCKET_KEY))); 850 } catch (NoSuchAlgorithmException e) { 851 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, 852 "The SHA-1 Algorithm required for websockets is not available on the server."); 853 } 854 855 if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) { 856 handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]); 857 } 858 859 return handshakeResponse; 860 } else { 861 return serveHttp(session); 862 } 863 } 864 serveHttp(final IHTTPSession session)865 protected Response serveHttp(final IHTTPSession session) { 866 return super.serve(session); 867 } 868 869 /** 870 * not all websockets implementations accept gzip compression. 871 */ 872 @Override useGzipWhenAccepted(Response r)873 protected boolean useGzipWhenAccepted(Response r) { 874 return false; 875 } 876 } 877