1package 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
36import java.io.EOFException;
37import java.io.IOException;
38import java.io.InputStream;
39import java.io.OutputStream;
40import java.nio.charset.CharacterCodingException;
41import java.nio.charset.Charset;
42import java.security.MessageDigest;
43import java.security.NoSuchAlgorithmException;
44import java.util.Arrays;
45import java.util.LinkedList;
46import java.util.List;
47import java.util.Map;
48import java.util.logging.Level;
49import java.util.logging.Logger;
50
51import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
52import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame;
53import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode;
54
55public 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