From fba310e36e98bb3a7596038f136a4f41f75f3b82 Mon Sep 17 00:00:00 2001 From: LordFokas Date: Fri, 23 Oct 2015 01:43:29 +0100 Subject: Rename NanoWebSocketServer to NanoWSD. --- websocket/src/main/java/fi/iki/elonen/NanoWSD.java | 868 +++++++++++++++++++++ .../java/fi/iki/elonen/NanoWebSocketServer.java | 868 --------------------- .../elonen/samples/echo/DebugWebSocketServer.java | 6 +- .../iki/elonen/samples/echo/EchoSocketSample.java | 4 +- 4 files changed, 873 insertions(+), 873 deletions(-) create mode 100644 websocket/src/main/java/fi/iki/elonen/NanoWSD.java delete mode 100644 websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java (limited to 'websocket/src/main/java/fi/iki/elonen') diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWSD.java b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java new file mode 100644 index 0000000..6ccf21c --- /dev/null +++ b/websocket/src/main/java/fi/iki/elonen/NanoWSD.java @@ -0,0 +1,868 @@ +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 continuousFrames = new LinkedList(); + + 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. Do not Override unless for debug purposes! + * + * @param frame The received WebSocket Frame. + */ + protected void debugFrameReceived(WebSocketFrame frame){} + + /** + * Debug method. Do not Override unless for debug purposes!
+ * 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 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. + *

+ * 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 + *

+ * + * @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 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 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 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; + } +} diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java deleted file mode 100644 index 420bfc2..0000000 --- a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java +++ /dev/null @@ -1,868 +0,0 @@ -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.NanoWebSocketServer.WebSocketFrame.CloseCode; -import fi.iki.elonen.NanoWebSocketServer.WebSocketFrame.CloseFrame; -import fi.iki.elonen.NanoWebSocketServer.WebSocketFrame.OpCode; - -public abstract class NanoWebSocketServer 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 continuousFrames = new LinkedList(); - - 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(NanoWebSocketServer.HEADER_UPGRADE, NanoWebSocketServer.HEADER_UPGRADE_VALUE); - this.handshakeResponse.addHeader(NanoWebSocketServer.HEADER_CONNECTION, NanoWebSocketServer.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. Do not Override unless for debug purposes! - * - * @param frame The received WebSocket Frame. - */ - protected void debugFrameReceived(WebSocketFrame frame){} - - /** - * Debug method. Do not Override unless for debug purposes!
- * 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) { - NanoWebSocketServer.LOG.log(Level.FINE, "close failed", e); - } - } - if (this.out != null) { - try { - this.out.close(); - } catch (IOException e) { - NanoWebSocketServer.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 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(NanoWebSocketServer.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. - *

- * 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 - *

- * - * @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++] = NanoWebSocketServer.ALPHABET[b0 >> 2 & mask]; - ar[a++] = NanoWebSocketServer.ALPHABET[(b0 << 4 | (b1 & 0xFF) >> 4) & mask]; - ar[a++] = NanoWebSocketServer.ALPHABET[(b1 << 2 | (b2 & 0xFF) >> 6) & mask]; - ar[a++] = NanoWebSocketServer.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 + NanoWebSocketServer.WEBSOCKET_KEY_MAGIC; - md.update(text.getBytes(), 0, text.length()); - byte[] sha1hash = md.digest(); - return encodeBase64(sha1hash); - } - - public NanoWebSocketServer(int port) { - super(port); - } - - public NanoWebSocketServer(String hostname, int port) { - super(hostname, port); - } - - private boolean isWebSocketConnectionHeader(Map headers) { - String connection = headers.get(NanoWebSocketServer.HEADER_CONNECTION); - return connection != null && connection.toLowerCase().contains(NanoWebSocketServer.HEADER_CONNECTION_VALUE.toLowerCase()); - } - - protected boolean isWebsocketRequested(IHTTPSession session) { - Map headers = session.getHeaders(); - String upgrade = headers.get(NanoWebSocketServer.HEADER_UPGRADE); - boolean isCorrectConnection = isWebSocketConnectionHeader(headers); - boolean isUpgrade = NanoWebSocketServer.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade); - return isUpgrade && isCorrectConnection; - } - - // --------------------------------Listener-------------------------------- - - protected abstract WebSocket openWebSocket(IHTTPSession handshake); - - @Override - public Response serve(final IHTTPSession session) { - Map headers = session.getHeaders(); - if (isWebsocketRequested(session)) { - if (!NanoWebSocketServer.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_VERSION))) { - return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, - "Invalid Websocket-Version " + headers.get(NanoWebSocketServer.HEADER_WEBSOCKET_VERSION)); - } - - if (!headers.containsKey(NanoWebSocketServer.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(NanoWebSocketServer.HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(NanoWebSocketServer.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(NanoWebSocketServer.HEADER_WEBSOCKET_PROTOCOL)) { - handshakeResponse.addHeader(NanoWebSocketServer.HEADER_WEBSOCKET_PROTOCOL, headers.get(NanoWebSocketServer.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; - } -} diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java index 353d649..44e7068 100644 --- a/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java +++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/DebugWebSocketServer.java @@ -37,13 +37,13 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; -import fi.iki.elonen.NanoWebSocketServer; -import fi.iki.elonen.NanoWebSocketServer.WebSocketFrame.CloseCode; +import fi.iki.elonen.NanoWSD; +import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode; /** * @author Paul S. Hawke (paul.hawke@gmail.com) On: 4/23/14 at 10:31 PM */ -public class DebugWebSocketServer extends NanoWebSocketServer { +public class DebugWebSocketServer extends NanoWSD { /** * logger to log to. diff --git a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java index 3060146..e7d7e3b 100644 --- a/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java +++ b/websocket/src/main/java/fi/iki/elonen/samples/echo/EchoSocketSample.java @@ -35,13 +35,13 @@ package fi.iki.elonen.samples.echo; import java.io.IOException; -import fi.iki.elonen.NanoWebSocketServer; +import fi.iki.elonen.NanoWSD; public class EchoSocketSample { public static void main(String[] args) throws IOException { final boolean debugMode = args.length >= 2 && args[1].toLowerCase().equals("-d"); - NanoWebSocketServer ws = new DebugWebSocketServer(args.length > 0 ? Integer.parseInt(args[0]) : 9090, debugMode); + NanoWSD ws = new DebugWebSocketServer(args.length > 0 ? Integer.parseInt(args[0]) : 9090, debugMode); ws.start(); System.out.println("Server started, hit Enter to stop.\n"); try { -- cgit v1.2.3