Home | History | Annotate | Download | only in elonen
      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 
     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 
    100         public boolean isOpen() {
    101             return state == State.OPEN;
    102         }
    103 
    104         protected abstract void onOpen();
    105 
    106         protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);
    107 
    108         protected abstract void onMessage(WebSocketFrame message);
    109 
    110         protected abstract void onPong(WebSocketFrame pong);
    111 
    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          */
    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          */
    130         protected void debugFrameSent(WebSocketFrame frame) {
    131         }
    132 
    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 
    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 
    167         public NanoHTTPD.IHTTPSession getHandshakeRequest() {
    168             return this.handshakeRequest;
    169         }
    170 
    171         public NanoHTTPD.Response getHandshakeResponse() {
    172             return this.handshakeResponse;
    173         }
    174 
    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 
    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 
    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 
    237         public void ping(byte[] payload) throws IOException {
    238             sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
    239         }
    240 
    241         // --------------------------------Public
    242         // Facade---------------------------
    243 
    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 
    262         public void send(byte[] payload) throws IOException {
    263             sendFrame(new WebSocketFrame(OpCode.Binary, true, payload));
    264         }
    265 
    266         public void send(String payload) throws IOException {
    267             sendFrame(new WebSocketFrame(OpCode.Text, true, payload));
    268         }
    269 
    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 
    284         public WebSocketException(CloseCode code, String reason) {
    285             this(code, reason, null);
    286         }
    287 
    288         public WebSocketException(CloseCode code, String reason, Exception cause) {
    289             super(code + ": " + reason, cause);
    290             this.code = code;
    291             this.reason = reason;
    292         }
    293 
    294         public WebSocketException(Exception cause) {
    295             this(CloseCode.InternalServerError, cause.toString(), cause);
    296         }
    297 
    298         public CloseCode getCode() {
    299             return this.code;
    300         }
    301 
    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 
    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 
    334             private CloseCode(int code) {
    335                 this.code = code;
    336             }
    337 
    338             public int getValue() {
    339                 return this.code;
    340             }
    341         }
    342 
    343         public static class CloseFrame extends WebSocketFrame {
    344 
    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 
    362             public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
    363                 super(OpCode.Close, true, generatePayload(code, closeReason));
    364             }
    365 
    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 
    375             public CloseCode getCloseCode() {
    376                 return this._closeCode;
    377             }
    378 
    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 
    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 
    403             private OpCode(int code) {
    404                 this.code = (byte) code;
    405             }
    406 
    407             public byte getValue() {
    408                 return this.code;
    409             }
    410 
    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 
    418         public static String binary2Text(byte[] payload) throws CharacterCodingException {
    419             return new String(payload, WebSocketFrame.TEXT_CHARSET);
    420         }
    421 
    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 
    426         private static int checkedRead(int read) throws IOException {
    427             if (read < 0) {
    428                 throw new EOFException();
    429             }
    430             return read;
    431         }
    432 
    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 
    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 
    474         private WebSocketFrame(OpCode opCode, boolean fin) {
    475             setOpCode(opCode);
    476             setFin(fin);
    477         }
    478 
    479         public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
    480             this(opCode, fin, payload, null);
    481         }
    482 
    483         public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
    484             this(opCode, fin);
    485             setMaskingKey(maskingKey);
    486             setBinaryPayload(payload);
    487         }
    488 
    489         public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
    490             this(opCode, fin, payload, null);
    491         }
    492 
    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 
    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 
    520         public WebSocketFrame(WebSocketFrame clone) {
    521             setOpCode(clone.getOpCode());
    522             setFin(clone.isFin());
    523             setBinaryPayload(clone.getBinaryPayload());
    524             setMaskingKey(clone.getMaskingKey());
    525         }
    526 
    527         public byte[] getBinaryPayload() {
    528             return this.payload;
    529         }
    530 
    531         public byte[] getMaskingKey() {
    532             return this.maskingKey;
    533         }
    534 
    535         public OpCode getOpCode() {
    536             return this.opCode;
    537         }
    538 
    539         // --------------------------------SERIALIZATION---------------------------
    540 
    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 
    552         public boolean isFin() {
    553             return this.fin;
    554         }
    555 
    556         public boolean isMasked() {
    557             return this.maskingKey != null && this.maskingKey.length == 4;
    558         }
    559 
    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 
    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 
    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 
    649         public void setBinaryPayload(byte[] payload) {
    650             this.payload = payload;
    651             this._payloadLength = payload.length;
    652             this._payloadString = null;
    653         }
    654 
    655         public void setFin(boolean fin) {
    656             this.fin = fin;
    657         }
    658 
    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 
    666         public void setOpCode(OpCode opcode) {
    667             this.opCode = opcode;
    668         }
    669 
    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 
    678         public void setUnmasked() {
    679             setMaskingKey(null);
    680         }
    681 
    682         @Override
    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 
    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      */
    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 
    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 
    808     public NanoWSD(int port) {
    809         super(port);
    810     }
    811 
    812     public NanoWSD(String hostname, int port) {
    813         super(hostname, port);
    814     }
    815 
    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 
    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 
    831     protected abstract WebSocket openWebSocket(IHTTPSession handshake);
    832 
    833     @Override
    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 
    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
    873     protected boolean useGzipWhenAccepted(Response r) {
    874         return false;
    875     }
    876 }
    877