| 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; |
| } |
| } |