Java程序  |  877行  |  32.4 KB

package fi.iki.elonen;

/*
 * #%L
 * NanoHttpd-Websocket
 * %%
 * Copyright (C) 2012 - 2015 nanohttpd
 * %%
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the nanohttpd nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseFrame;
import fi.iki.elonen.NanoWSD.WebSocketFrame.OpCode;

public abstract class NanoWSD extends NanoHTTPD {

    public static enum State {
        UNCONNECTED,
        CONNECTING,
        OPEN,
        CLOSING,
        CLOSED
    }

    public static abstract class WebSocket {

        private final InputStream in;

        private OutputStream out;

        private WebSocketFrame.OpCode continuousOpCode = null;

        private final List<WebSocketFrame> continuousFrames = new LinkedList<WebSocketFrame>();

        private State state = State.UNCONNECTED;

        private final NanoHTTPD.IHTTPSession handshakeRequest;

        private final NanoHTTPD.Response handshakeResponse = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SWITCH_PROTOCOL, null, (InputStream) null, 0) {

            @Override
            protected void send(OutputStream out) {
                WebSocket.this.out = out;
                WebSocket.this.state = State.CONNECTING;
                super.send(out);
                WebSocket.this.state = State.OPEN;
                WebSocket.this.onOpen();
                readWebsocket();
            }
        };

        public WebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {
            this.handshakeRequest = handshakeRequest;
            this.in = handshakeRequest.getInputStream();

            this.handshakeResponse.addHeader(NanoWSD.HEADER_UPGRADE, NanoWSD.HEADER_UPGRADE_VALUE);
            this.handshakeResponse.addHeader(NanoWSD.HEADER_CONNECTION, NanoWSD.HEADER_CONNECTION_VALUE);
        }

        public boolean isOpen() {
            return state == State.OPEN;
        }

        protected abstract void onOpen();

        protected abstract void onClose(CloseCode code, String reason, boolean initiatedByRemote);

        protected abstract void onMessage(WebSocketFrame message);

        protected abstract void onPong(WebSocketFrame pong);

        protected abstract void onException(IOException exception);

        /**
         * Debug method. <b>Do not Override unless for debug purposes!</b>
         * 
         * @param frame
         *            The received WebSocket Frame.
         */
        protected void debugFrameReceived(WebSocketFrame frame) {
        }

        /**
         * Debug method. <b>Do not Override unless for debug purposes!</b><br>
         * This method is called before actually sending the frame.
         * 
         * @param frame
         *            The sent WebSocket Frame.
         */
        protected void debugFrameSent(WebSocketFrame frame) {
        }

        public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException {
            State oldState = this.state;
            this.state = State.CLOSING;
            if (oldState == State.OPEN) {
                sendFrame(new CloseFrame(code, reason));
            } else {
                doClose(code, reason, initiatedByRemote);
            }
        }

        private void doClose(CloseCode code, String reason, boolean initiatedByRemote) {
            if (this.state == State.CLOSED) {
                return;
            }
            if (this.in != null) {
                try {
                    this.in.close();
                } catch (IOException e) {
                    NanoWSD.LOG.log(Level.FINE, "close failed", e);
                }
            }
            if (this.out != null) {
                try {
                    this.out.close();
                } catch (IOException e) {
                    NanoWSD.LOG.log(Level.FINE, "close failed", e);
                }
            }
            this.state = State.CLOSED;
            onClose(code, reason, initiatedByRemote);
        }

        // --------------------------------IO--------------------------------------

        public NanoHTTPD.IHTTPSession getHandshakeRequest() {
            return this.handshakeRequest;
        }

        public NanoHTTPD.Response getHandshakeResponse() {
            return this.handshakeResponse;
        }

        private void handleCloseFrame(WebSocketFrame frame) throws IOException {
            CloseCode code = CloseCode.NormalClosure;
            String reason = "";
            if (frame instanceof CloseFrame) {
                code = ((CloseFrame) frame).getCloseCode();
                reason = ((CloseFrame) frame).getCloseReason();
            }
            if (this.state == State.CLOSING) {
                // Answer for my requested close
                doClose(code, reason, false);
            } else {
                close(code, reason, true);
            }
        }

        private void handleFrameFragment(WebSocketFrame frame) throws IOException {
            if (frame.getOpCode() != OpCode.Continuation) {
                // First
                if (this.continuousOpCode != null) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Previous continuous frame sequence not completed.");
                }
                this.continuousOpCode = frame.getOpCode();
                this.continuousFrames.clear();
                this.continuousFrames.add(frame);
            } else if (frame.isFin()) {
                // Last
                if (this.continuousOpCode == null) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
                }
                onMessage(new WebSocketFrame(this.continuousOpCode, this.continuousFrames));
                this.continuousOpCode = null;
                this.continuousFrames.clear();
            } else if (this.continuousOpCode == null) {
                // Unexpected
                throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence was not started.");
            } else {
                // Intermediate
                this.continuousFrames.add(frame);
            }
        }

        private void handleWebsocketFrame(WebSocketFrame frame) throws IOException {
            debugFrameReceived(frame);
            if (frame.getOpCode() == OpCode.Close) {
                handleCloseFrame(frame);
            } else if (frame.getOpCode() == OpCode.Ping) {
                sendFrame(new WebSocketFrame(OpCode.Pong, true, frame.getBinaryPayload()));
            } else if (frame.getOpCode() == OpCode.Pong) {
                onPong(frame);
            } else if (!frame.isFin() || frame.getOpCode() == OpCode.Continuation) {
                handleFrameFragment(frame);
            } else if (this.continuousOpCode != null) {
                throw new WebSocketException(CloseCode.ProtocolError, "Continuous frame sequence not completed.");
            } else if (frame.getOpCode() == OpCode.Text || frame.getOpCode() == OpCode.Binary) {
                onMessage(frame);
            } else {
                throw new WebSocketException(CloseCode.ProtocolError, "Non control or continuous frame expected.");
            }
        }

        // --------------------------------Close-----------------------------------

        public void ping(byte[] payload) throws IOException {
            sendFrame(new WebSocketFrame(OpCode.Ping, true, payload));
        }

        // --------------------------------Public
        // Facade---------------------------

        private void readWebsocket() {
            try {
                while (this.state == State.OPEN) {
                    handleWebsocketFrame(WebSocketFrame.read(this.in));
                }
            } catch (CharacterCodingException e) {
                onException(e);
                doClose(CloseCode.InvalidFramePayloadData, e.toString(), false);
            } catch (IOException e) {
                onException(e);
                if (e instanceof WebSocketException) {
                    doClose(((WebSocketException) e).getCode(), ((WebSocketException) e).getReason(), false);
                }
            } finally {
                doClose(CloseCode.InternalServerError, "Handler terminated without closing the connection.", false);
            }
        }

        public void send(byte[] payload) throws IOException {
            sendFrame(new WebSocketFrame(OpCode.Binary, true, payload));
        }

        public void send(String payload) throws IOException {
            sendFrame(new WebSocketFrame(OpCode.Text, true, payload));
        }

        public synchronized void sendFrame(WebSocketFrame frame) throws IOException {
            debugFrameSent(frame);
            frame.write(this.out);
        }
    }

    public static class WebSocketException extends IOException {

        private static final long serialVersionUID = 1L;

        private final CloseCode code;

        private final String reason;

        public WebSocketException(CloseCode code, String reason) {
            this(code, reason, null);
        }

        public WebSocketException(CloseCode code, String reason, Exception cause) {
            super(code + ": " + reason, cause);
            this.code = code;
            this.reason = reason;
        }

        public WebSocketException(Exception cause) {
            this(CloseCode.InternalServerError, cause.toString(), cause);
        }

        public CloseCode getCode() {
            return this.code;
        }

        public String getReason() {
            return this.reason;
        }
    }

    public static class WebSocketFrame {

        public static enum CloseCode {
            NormalClosure(1000),
            GoingAway(1001),
            ProtocolError(1002),
            UnsupportedData(1003),
            NoStatusRcvd(1005),
            AbnormalClosure(1006),
            InvalidFramePayloadData(1007),
            PolicyViolation(1008),
            MessageTooBig(1009),
            MandatoryExt(1010),
            InternalServerError(1011),
            TLSHandshake(1015);

            public static CloseCode find(int value) {
                for (CloseCode code : values()) {
                    if (code.getValue() == value) {
                        return code;
                    }
                }
                return null;
            }

            private final int code;

            private CloseCode(int code) {
                this.code = code;
            }

            public int getValue() {
                return this.code;
            }
        }

        public static class CloseFrame extends WebSocketFrame {

            private static byte[] generatePayload(CloseCode code, String closeReason) throws CharacterCodingException {
                if (code != null) {
                    byte[] reasonBytes = text2Binary(closeReason);
                    byte[] payload = new byte[reasonBytes.length + 2];
                    payload[0] = (byte) (code.getValue() >> 8 & 0xFF);
                    payload[1] = (byte) (code.getValue() & 0xFF);
                    System.arraycopy(reasonBytes, 0, payload, 2, reasonBytes.length);
                    return payload;
                } else {
                    return new byte[0];
                }
            }

            private CloseCode _closeCode;

            private String _closeReason;

            public CloseFrame(CloseCode code, String closeReason) throws CharacterCodingException {
                super(OpCode.Close, true, generatePayload(code, closeReason));
            }

            private CloseFrame(WebSocketFrame wrap) throws CharacterCodingException {
                super(wrap);
                assert wrap.getOpCode() == OpCode.Close;
                if (wrap.getBinaryPayload().length >= 2) {
                    this._closeCode = CloseCode.find((wrap.getBinaryPayload()[0] & 0xFF) << 8 | wrap.getBinaryPayload()[1] & 0xFF);
                    this._closeReason = binary2Text(getBinaryPayload(), 2, getBinaryPayload().length - 2);
                }
            }

            public CloseCode getCloseCode() {
                return this._closeCode;
            }

            public String getCloseReason() {
                return this._closeReason;
            }
        }

        public static enum OpCode {
            Continuation(0),
            Text(1),
            Binary(2),
            Close(8),
            Ping(9),
            Pong(10);

            public static OpCode find(byte value) {
                for (OpCode opcode : values()) {
                    if (opcode.getValue() == value) {
                        return opcode;
                    }
                }
                return null;
            }

            private final byte code;

            private OpCode(int code) {
                this.code = (byte) code;
            }

            public byte getValue() {
                return this.code;
            }

            public boolean isControlFrame() {
                return this == Close || this == Ping || this == Pong;
            }
        }

        public static final Charset TEXT_CHARSET = Charset.forName("UTF-8");

        public static String binary2Text(byte[] payload) throws CharacterCodingException {
            return new String(payload, WebSocketFrame.TEXT_CHARSET);
        }

        public static String binary2Text(byte[] payload, int offset, int length) throws CharacterCodingException {
            return new String(payload, offset, length, WebSocketFrame.TEXT_CHARSET);
        }

        private static int checkedRead(int read) throws IOException {
            if (read < 0) {
                throw new EOFException();
            }
            return read;
        }

        public static WebSocketFrame read(InputStream in) throws IOException {
            byte head = (byte) checkedRead(in.read());
            boolean fin = (head & 0x80) != 0;
            OpCode opCode = OpCode.find((byte) (head & 0x0F));
            if ((head & 0x70) != 0) {
                throw new WebSocketException(CloseCode.ProtocolError, "The reserved bits (" + Integer.toBinaryString(head & 0x70) + ") must be 0.");
            }
            if (opCode == null) {
                throw new WebSocketException(CloseCode.ProtocolError, "Received frame with reserved/unknown opcode " + (head & 0x0F) + ".");
            } else if (opCode.isControlFrame() && !fin) {
                throw new WebSocketException(CloseCode.ProtocolError, "Fragmented control frame.");
            }

            WebSocketFrame frame = new WebSocketFrame(opCode, fin);
            frame.readPayloadInfo(in);
            frame.readPayload(in);
            if (frame.getOpCode() == OpCode.Close) {
                return new CloseFrame(frame);
            } else {
                return frame;
            }
        }

        public static byte[] text2Binary(String payload) throws CharacterCodingException {
            return payload.getBytes(WebSocketFrame.TEXT_CHARSET);
        }

        private OpCode opCode;

        private boolean fin;

        private byte[] maskingKey;

        private byte[] payload;

        // --------------------------------GETTERS---------------------------------

        private transient int _payloadLength;

        private transient String _payloadString;

        private WebSocketFrame(OpCode opCode, boolean fin) {
            setOpCode(opCode);
            setFin(fin);
        }

        public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload) {
            this(opCode, fin, payload, null);
        }

        public WebSocketFrame(OpCode opCode, boolean fin, byte[] payload, byte[] maskingKey) {
            this(opCode, fin);
            setMaskingKey(maskingKey);
            setBinaryPayload(payload);
        }

        public WebSocketFrame(OpCode opCode, boolean fin, String payload) throws CharacterCodingException {
            this(opCode, fin, payload, null);
        }

        public WebSocketFrame(OpCode opCode, boolean fin, String payload, byte[] maskingKey) throws CharacterCodingException {
            this(opCode, fin);
            setMaskingKey(maskingKey);
            setTextPayload(payload);
        }

        public WebSocketFrame(OpCode opCode, List<WebSocketFrame> fragments) throws WebSocketException {
            setOpCode(opCode);
            setFin(true);

            long _payloadLength = 0;
            for (WebSocketFrame inter : fragments) {
                _payloadLength += inter.getBinaryPayload().length;
            }
            if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
                throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
            }
            this._payloadLength = (int) _payloadLength;
            byte[] payload = new byte[this._payloadLength];
            int offset = 0;
            for (WebSocketFrame inter : fragments) {
                System.arraycopy(inter.getBinaryPayload(), 0, payload, offset, inter.getBinaryPayload().length);
                offset += inter.getBinaryPayload().length;
            }
            setBinaryPayload(payload);
        }

        public WebSocketFrame(WebSocketFrame clone) {
            setOpCode(clone.getOpCode());
            setFin(clone.isFin());
            setBinaryPayload(clone.getBinaryPayload());
            setMaskingKey(clone.getMaskingKey());
        }

        public byte[] getBinaryPayload() {
            return this.payload;
        }

        public byte[] getMaskingKey() {
            return this.maskingKey;
        }

        public OpCode getOpCode() {
            return this.opCode;
        }

        // --------------------------------SERIALIZATION---------------------------

        public String getTextPayload() {
            if (this._payloadString == null) {
                try {
                    this._payloadString = binary2Text(getBinaryPayload());
                } catch (CharacterCodingException e) {
                    throw new RuntimeException("Undetected CharacterCodingException", e);
                }
            }
            return this._payloadString;
        }

        public boolean isFin() {
            return this.fin;
        }

        public boolean isMasked() {
            return this.maskingKey != null && this.maskingKey.length == 4;
        }

        private String payloadToString() {
            if (this.payload == null) {
                return "null";
            } else {
                final StringBuilder sb = new StringBuilder();
                sb.append('[').append(this.payload.length).append("b] ");
                if (getOpCode() == OpCode.Text) {
                    String text = getTextPayload();
                    if (text.length() > 100) {
                        sb.append(text.substring(0, 100)).append("...");
                    } else {
                        sb.append(text);
                    }
                } else {
                    sb.append("0x");
                    for (int i = 0; i < Math.min(this.payload.length, 50); ++i) {
                        sb.append(Integer.toHexString(this.payload[i] & 0xFF));
                    }
                    if (this.payload.length > 50) {
                        sb.append("...");
                    }
                }
                return sb.toString();
            }
        }

        private void readPayload(InputStream in) throws IOException {
            this.payload = new byte[this._payloadLength];
            int read = 0;
            while (read < this._payloadLength) {
                read += checkedRead(in.read(this.payload, read, this._payloadLength - read));
            }

            if (isMasked()) {
                for (int i = 0; i < this.payload.length; i++) {
                    this.payload[i] ^= this.maskingKey[i % 4];
                }
            }

            // Test for Unicode errors
            if (getOpCode() == OpCode.Text) {
                this._payloadString = binary2Text(getBinaryPayload());
            }
        }

        // --------------------------------ENCODING--------------------------------

        private void readPayloadInfo(InputStream in) throws IOException {
            byte b = (byte) checkedRead(in.read());
            boolean masked = (b & 0x80) != 0;

            this._payloadLength = (byte) (0x7F & b);
            if (this._payloadLength == 126) {
                // checkedRead must return int for this to work
                this._payloadLength = (checkedRead(in.read()) << 8 | checkedRead(in.read())) & 0xFFFF;
                if (this._payloadLength < 126) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 2byte length. (not using minimal length encoding)");
                }
            } else if (this._payloadLength == 127) {
                long _payloadLength =
                        (long) checkedRead(in.read()) << 56 | (long) checkedRead(in.read()) << 48 | (long) checkedRead(in.read()) << 40 | (long) checkedRead(in.read()) << 32
                                | checkedRead(in.read()) << 24 | checkedRead(in.read()) << 16 | checkedRead(in.read()) << 8 | checkedRead(in.read());
                if (_payloadLength < 65536) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Invalid data frame 4byte length. (not using minimal length encoding)");
                }
                if (_payloadLength < 0 || _payloadLength > Integer.MAX_VALUE) {
                    throw new WebSocketException(CloseCode.MessageTooBig, "Max frame length has been exceeded.");
                }
                this._payloadLength = (int) _payloadLength;
            }

            if (this.opCode.isControlFrame()) {
                if (this._payloadLength > 125) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Control frame with payload length > 125 bytes.");
                }
                if (this.opCode == OpCode.Close && this._payloadLength == 1) {
                    throw new WebSocketException(CloseCode.ProtocolError, "Received close frame with payload len 1.");
                }
            }

            if (masked) {
                this.maskingKey = new byte[4];
                int read = 0;
                while (read < this.maskingKey.length) {
                    read += checkedRead(in.read(this.maskingKey, read, this.maskingKey.length - read));
                }
            }
        }

        public void setBinaryPayload(byte[] payload) {
            this.payload = payload;
            this._payloadLength = payload.length;
            this._payloadString = null;
        }

        public void setFin(boolean fin) {
            this.fin = fin;
        }

        public void setMaskingKey(byte[] maskingKey) {
            if (maskingKey != null && maskingKey.length != 4) {
                throw new IllegalArgumentException("MaskingKey " + Arrays.toString(maskingKey) + " hasn't length 4");
            }
            this.maskingKey = maskingKey;
        }

        public void setOpCode(OpCode opcode) {
            this.opCode = opcode;
        }

        public void setTextPayload(String payload) throws CharacterCodingException {
            this.payload = text2Binary(payload);
            this._payloadLength = payload.length();
            this._payloadString = payload;
        }

        // --------------------------------CONSTANTS-------------------------------

        public void setUnmasked() {
            setMaskingKey(null);
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("WS[");
            sb.append(getOpCode());
            sb.append(", ").append(isFin() ? "fin" : "inter");
            sb.append(", ").append(isMasked() ? "masked" : "unmasked");
            sb.append(", ").append(payloadToString());
            sb.append(']');
            return sb.toString();
        }

        // ------------------------------------------------------------------------

        public void write(OutputStream out) throws IOException {
            byte header = 0;
            if (this.fin) {
                header |= 0x80;
            }
            header |= this.opCode.getValue() & 0x0F;
            out.write(header);

            this._payloadLength = getBinaryPayload().length;
            if (this._payloadLength <= 125) {
                out.write(isMasked() ? 0x80 | (byte) this._payloadLength : (byte) this._payloadLength);
            } else if (this._payloadLength <= 0xFFFF) {
                out.write(isMasked() ? 0xFE : 126);
                out.write(this._payloadLength >>> 8);
                out.write(this._payloadLength);
            } else {
                out.write(isMasked() ? 0xFF : 127);
                out.write(this._payloadLength >>> 56 & 0); // integer only
                                                           // contains
                // 31 bit
                out.write(this._payloadLength >>> 48 & 0);
                out.write(this._payloadLength >>> 40 & 0);
                out.write(this._payloadLength >>> 32 & 0);
                out.write(this._payloadLength >>> 24);
                out.write(this._payloadLength >>> 16);
                out.write(this._payloadLength >>> 8);
                out.write(this._payloadLength);
            }

            if (isMasked()) {
                out.write(this.maskingKey);
                for (int i = 0; i < this._payloadLength; i++) {
                    out.write(getBinaryPayload()[i] ^ this.maskingKey[i % 4]);
                }
            } else {
                out.write(getBinaryPayload());
            }
            out.flush();
        }
    }

    /**
     * logger to log to.
     */
    private static final Logger LOG = Logger.getLogger(NanoWSD.class.getName());

    public static final String HEADER_UPGRADE = "upgrade";

    public static final String HEADER_UPGRADE_VALUE = "websocket";

    public static final String HEADER_CONNECTION = "connection";

    public static final String HEADER_CONNECTION_VALUE = "Upgrade";

    public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";

    public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";

    public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";

    public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";

    public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";

    private final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

    /**
     * Translates the specified byte array into Base64 string.
     * <p>
     * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8
     * hast java.util.Base64, I have this from stackoverflow:
     * http://stackoverflow.com/a/4265472
     * </p>
     * 
     * @param buf
     *            the byte array (not null)
     * @return the translated Base64 string (not null)
     */
    private static String encodeBase64(byte[] buf) {
        int size = buf.length;
        char[] ar = new char[(size + 2) / 3 * 4];
        int a = 0;
        int i = 0;
        while (i < size) {
            byte b0 = buf[i++];
            byte b1 = i < size ? buf[i++] : 0;
            byte b2 = i < size ? buf[i++] : 0;

            int mask = 0x3F;
            ar[a++] = NanoWSD.ALPHABET[b0 >> 2 & mask];
            ar[a++] = NanoWSD.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask];
            ar[a++] = NanoWSD.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask];
            ar[a++] = NanoWSD.ALPHABET[b2 & mask];
        }
        switch (size % 3) {
            case 1:
                ar[--a] = '=';
            case 2:
                ar[--a] = '=';
        }
        return new String(ar);
    }

    public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        String text = key + NanoWSD.WEBSOCKET_KEY_MAGIC;
        md.update(text.getBytes(), 0, text.length());
        byte[] sha1hash = md.digest();
        return encodeBase64(sha1hash);
    }

    public NanoWSD(int port) {
        super(port);
    }

    public NanoWSD(String hostname, int port) {
        super(hostname, port);
    }

    private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
        String connection = headers.get(NanoWSD.HEADER_CONNECTION);
        return connection != null && connection.toLowerCase().contains(NanoWSD.HEADER_CONNECTION_VALUE.toLowerCase());
    }

    protected boolean isWebsocketRequested(IHTTPSession session) {
        Map<String, String> headers = session.getHeaders();
        String upgrade = headers.get(NanoWSD.HEADER_UPGRADE);
        boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
        boolean isUpgrade = NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
        return isUpgrade && isCorrectConnection;
    }

    // --------------------------------Listener--------------------------------

    protected abstract WebSocket openWebSocket(IHTTPSession handshake);

    @Override
    public Response serve(final IHTTPSession session) {
        Map<String, String> headers = session.getHeaders();
        if (isWebsocketRequested(session)) {
            if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) {
                return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
                        "Invalid Websocket-Version " + headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION));
            }

            if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) {
                return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Missing Websocket-Key");
            }

            WebSocket webSocket = openWebSocket(session);
            Response handshakeResponse = webSocket.getHandshakeResponse();
            try {
                handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWSD.HEADER_WEBSOCKET_KEY)));
            } catch (NoSuchAlgorithmException e) {
                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
                        "The SHA-1 Algorithm required for websockets is not available on the server.");
            }

            if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) {
                handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
            }

            return handshakeResponse;
        } else {
            return serveHttp(session);
        }
    }

    protected Response serveHttp(final IHTTPSession session) {
        return super.serve(session);
    }

    /**
     * not all websockets implementations accept gzip compression.
     */
    @Override
    protected boolean useGzipWhenAccepted(Response r) {
        return false;
    }
}