diff --git a/src/one/nio/http/Response.java b/src/one/nio/http/Response.java
index b148023..99a1624 100755
--- a/src/one/nio/http/Response.java
+++ b/src/one/nio/http/Response.java
@@ -59,6 +59,7 @@ public class Response {
public static final String UNSUPPORTED_MEDIA_TYPE = "415 Unsupported Media Type";
public static final String REQUESTED_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable";
public static final String EXPECTATION_FAILED = "417 Expectation Failed";
+ public static final String UPGRADE_REQUIRED = "426 Upgrade Required";
public static final String INTERNAL_ERROR = "500 Internal Server Error";
public static final String NOT_IMPLEMENTED = "501 Not Implemented";
public static final String BAD_GATEWAY = "502 Bad Gateway";
diff --git a/src/one/nio/ws/WebSocketHeaders.java b/src/one/nio/ws/WebSocketHeaders.java
new file mode 100644
index 0000000..4c84ac4
--- /dev/null
+++ b/src/one/nio/ws/WebSocketHeaders.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import one.nio.http.Request;
+import one.nio.util.Base64;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class WebSocketHeaders {
+ public final static String CONNECTION = "Connection: ";
+ public final static String UPGRADE = "Upgrade: ";
+ public final static String KEY = "Sec-WebSocket-Key: ";
+ public final static String VERSION = "Sec-WebSocket-Version: ";
+ public final static String ACCEPT = "Sec-WebSocket-Accept: ";
+ public final static String EXTENSIONS = "Sec-WebSocket-Extensions: ";
+ public final static String PROTOCOL = "Sec-WebSocket-Protocol: ";
+
+ private static final String ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ private static final ThreadLocal SHA1 = ThreadLocal.withInitial(() -> {
+ try {
+ return MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new InternalError("SHA-1 not supported on this platform");
+ }
+ });
+
+ public static boolean isUpgradableRequest(Request request) {
+ final String upgradeHeader = request.getHeader(WebSocketHeaders.UPGRADE);
+ final String connectionHeader = request.getHeader(WebSocketHeaders.CONNECTION);
+ return upgradeHeader != null && upgradeHeader.toLowerCase().contains("websocket") &&
+ connectionHeader != null && connectionHeader.toLowerCase().contains("upgrade");
+ }
+
+ public static String createVersionHeader(String version) {
+ return VERSION + version;
+ }
+
+ public static String createAcceptHeader(Request request) {
+ return ACCEPT + generateHash(request);
+ }
+
+ private static String generateHash(Request request) {
+ String key = request.getHeader(WebSocketHeaders.KEY);
+ String acceptSeed = key + ACCEPT_GUID;
+ byte[] sha1 = sha1(acceptSeed.getBytes(StandardCharsets.ISO_8859_1));
+ return new String(Base64.encode(sha1));
+ }
+
+ private static byte[] sha1(byte[] data) {
+ MessageDigest digest = SHA1.get();
+ digest.reset();
+ return digest.digest(data);
+ }
+}
diff --git a/src/one/nio/ws/WebSocketServer.java b/src/one/nio/ws/WebSocketServer.java
new file mode 100644
index 0000000..334753a
--- /dev/null
+++ b/src/one/nio/ws/WebSocketServer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws;
+
+import java.io.IOException;
+
+import one.nio.http.HttpServer;
+import one.nio.http.HttpSession;
+import one.nio.http.Request;
+import one.nio.net.Socket;
+import one.nio.ws.message.BinaryMessage;
+import one.nio.ws.message.CloseMessage;
+import one.nio.ws.message.PingMessage;
+import one.nio.ws.message.PongMessage;
+import one.nio.ws.message.TextMessage;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class WebSocketServer extends HttpServer {
+ private final WebSocketServerConfig config;
+
+ public WebSocketServer(WebSocketServerConfig config, Object... routers) throws IOException {
+ super(config, routers);
+ this.config = config;
+ }
+
+ @Override
+ public WebSocketSession createSession(Socket socket) {
+ return new WebSocketSession(socket, this, config);
+ }
+
+ @Override
+ public void handleRequest(Request request, HttpSession session) throws IOException {
+ if (config.isWebSocketURI(request.getURI())) {
+ ((WebSocketSession) session).handshake(request);
+ return;
+ }
+
+ super.handleRequest(request, session);
+ }
+
+ public void handleMessage(WebSocketSession session, PingMessage message) throws IOException {
+ session.sendMessage(PongMessage.EMPTY);
+ }
+
+ public void handleMessage(WebSocketSession session, PongMessage message) throws IOException {
+ // nothing by default
+ }
+
+ public void handleMessage(WebSocketSession session, TextMessage message) throws IOException {
+ // nothing by default
+ }
+
+ public void handleMessage(WebSocketSession session, BinaryMessage message) throws IOException {
+ // nothing by default
+ }
+
+ public void handleMessage(WebSocketSession session, CloseMessage message) throws IOException {
+ session.close(CloseMessage.NORMAL);
+ }
+}
diff --git a/src/one/nio/ws/WebSocketServerConfig.java b/src/one/nio/ws/WebSocketServerConfig.java
new file mode 100644
index 0000000..4a27739
--- /dev/null
+++ b/src/one/nio/ws/WebSocketServerConfig.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+import one.nio.http.HttpServerConfig;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class WebSocketServerConfig extends HttpServerConfig {
+ public String websocketBaseUri = "/";
+ public Set supportedProtocols = Collections.emptySet();
+
+ public boolean isWebSocketURI(String uri) {
+ return Objects.equals(websocketBaseUri, uri);
+ }
+
+ public boolean isSupportedProtocol(String protocol) {
+ return supportedProtocols.contains(protocol);
+ }
+}
diff --git a/src/one/nio/ws/WebSocketSession.java b/src/one/nio/ws/WebSocketSession.java
new file mode 100644
index 0000000..f907eec
--- /dev/null
+++ b/src/one/nio/ws/WebSocketSession.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import one.nio.http.HttpSession;
+import one.nio.http.Request;
+import one.nio.http.Response;
+import one.nio.net.Socket;
+import one.nio.ws.exception.HandshakeException;
+import one.nio.ws.exception.VersionException;
+import one.nio.ws.exception.WebSocketException;
+import one.nio.ws.extension.Extension;
+import one.nio.ws.extension.ExtensionRequest;
+import one.nio.ws.extension.ExtensionRequestParser;
+import one.nio.ws.extension.PerMessageDeflate;
+import one.nio.ws.message.BinaryMessage;
+import one.nio.ws.message.CloseMessage;
+import one.nio.ws.message.Message;
+import one.nio.ws.message.MessageReader;
+import one.nio.ws.message.MessageWriter;
+import one.nio.ws.message.PingMessage;
+import one.nio.ws.message.PongMessage;
+import one.nio.ws.message.TextMessage;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class WebSocketSession extends HttpSession {
+ public final static String VERSION_13 = "13";
+
+ private final WebSocketServer server;
+ private final WebSocketServerConfig config;
+ private final List extensions;
+
+ private MessageReader reader;
+ private MessageWriter writer;
+
+ public WebSocketSession(Socket socket, WebSocketServer server, WebSocketServerConfig config) {
+ super(socket, server);
+ this.server = server;
+ this.config = config;
+ this.extensions = new ArrayList<>();
+ }
+
+ @Override
+ public int checkStatus(long currentTime, long keepAlive) {
+ if (currentTime - lastAccessTime < keepAlive) {
+ return ACTIVE;
+ }
+
+ try {
+ if (wasSelected) {
+ sendMessage(PingMessage.EMPTY);
+ }
+
+ return ACTIVE;
+ } catch (IOException e) {
+ return STALE;
+ }
+ }
+
+ @Override
+ protected void processRead(byte[] buffer) throws IOException {
+ if (reader == null) {
+ super.processRead(buffer);
+ } else {
+ final Message> message = reader.read();
+ if (message != null) {
+ handleMessage(this, message);
+ }
+ }
+ }
+
+ public void handshake(Request request) throws IOException {
+ try {
+ validateRequest(request);
+ Response response = createResponse(request);
+ reader = new MessageReader(this, extensions);
+ writer = new MessageWriter(this, extensions);
+ sendResponse(response);
+ } catch (VersionException e) {
+ log.debug("Unsupported version", e);
+ Response response = new Response(Response.UPGRADE_REQUIRED, Response.EMPTY);
+ response.addHeader(WebSocketHeaders.createVersionHeader(VERSION_13));
+ sendResponse(response);
+ } catch (HandshakeException e) {
+ log.debug("Handshake error", e);
+ sendError(Response.BAD_REQUEST, e.getMessage());
+ }
+ }
+
+ public void sendMessage(Message> message) throws IOException {
+ if (writer == null) {
+ throw new IllegalStateException("websocket message was sent before handshake");
+ }
+ writer.write(message);
+ }
+
+ protected void handleMessage(WebSocketSession session, Message> message) throws IOException {
+ switch (message.opcode()) {
+ case PING:
+ server.handleMessage(session, (PingMessage) message);
+ break;
+ case PONG:
+ server.handleMessage(session, (PongMessage) message);
+ break;
+ case TEXT:
+ server.handleMessage(session, (TextMessage) message);
+ break;
+ case BINARY:
+ server.handleMessage(session, (BinaryMessage) message);
+ break;
+ case CLOSE:
+ server.handleMessage(session, (CloseMessage) message);
+ break;
+ default:
+ throw new IllegalArgumentException("unexpected message with opcode: " + message.opcode());
+ }
+ }
+
+ @Override
+ public void handleException(Throwable e) {
+ if (e instanceof WebSocketException) {
+ log.error("Cannot process session from {}", getRemoteHost(), e);
+ close(((WebSocketException) e).code());
+ return;
+ }
+ super.handleException(e);
+ }
+
+ public void close(short code) {
+ try {
+ sendMessage(new CloseMessage(code));
+ } catch (Exception e) {
+ log.warn("error while sending closing frame", e);
+ } finally {
+ close();
+ }
+ }
+
+ @Override
+ public void close() {
+ closeExtensions();
+ super.close();
+ }
+
+ protected void validateRequest(Request request) {
+ final String version = request.getHeader(WebSocketHeaders.VERSION);
+ if (!VERSION_13.equals(version)) {
+ throw new VersionException(version);
+ }
+ if (request.getMethod() != Request.METHOD_GET) {
+ throw new HandshakeException("only GET method supported");
+ }
+ if (request.getHeader(WebSocketHeaders.KEY) == null) {
+ throw new HandshakeException("missing websocket key");
+ }
+ if (!WebSocketHeaders.isUpgradableRequest(request)) {
+ throw new HandshakeException("missing upgrade header");
+ }
+ }
+
+ protected Response createResponse(Request request) {
+ Response response = new Response(Response.SWITCHING_PROTOCOLS, Response.EMPTY);
+ response.addHeader("Upgrade: websocket");
+ response.addHeader("Connection: Upgrade");
+ response.addHeader(WebSocketHeaders.createAcceptHeader(request));
+ processExtensions(request, response);
+ processProtocol(request, response);
+ return response;
+ }
+
+ protected void processProtocol(Request request, Response response) {
+ final String protocols = request.getHeader(WebSocketHeaders.PROTOCOL);
+ if (protocols != null) {
+ for (String protocol : protocols.split(",")) {
+ if (config.isSupportedProtocol(protocol)) {
+ response.addHeader(WebSocketHeaders.PROTOCOL + protocol);
+ break;
+ }
+ }
+ }
+ }
+
+ protected void processExtensions(Request request, Response response) {
+ final String extensionsHeader = request.getHeader(WebSocketHeaders.EXTENSIONS);
+ if (extensionsHeader == null || extensionsHeader.isEmpty()) {
+ return;
+ }
+ final List extensionRequests = ExtensionRequestParser.parse(extensionsHeader);
+ if (extensionRequests.isEmpty()) {
+ return;
+ }
+ final StringBuilder responseHeaderBuilder = new StringBuilder(WebSocketHeaders.EXTENSIONS);
+ for (ExtensionRequest extensionRequest : extensionRequests) {
+ Extension extension = createExtension(extensionRequest);
+ if (extension != null) {
+ extensions.add(extension);
+ if (extensions.size() > 1) {
+ responseHeaderBuilder.append(',');
+ }
+ extension.appendResponseHeaderValue(responseHeaderBuilder);
+ }
+ }
+ if (!extensions.isEmpty()) {
+ response.addHeader(responseHeaderBuilder.toString());
+ }
+ }
+
+ protected Extension createExtension(ExtensionRequest request) {
+ if (PerMessageDeflate.NAME.equals(request.getName())) {
+ return PerMessageDeflate.negotiate(request.getParameters());
+ }
+ return null;
+ }
+
+ private void closeExtensions() {
+ for (Extension extension : extensions) {
+ try {
+ extension.close();
+ } catch (Exception e) {
+ log.warn("error while closing extension", e);
+ }
+ }
+ }
+}
diff --git a/src/one/nio/ws/exception/CannotAcceptException.java b/src/one/nio/ws/exception/CannotAcceptException.java
new file mode 100644
index 0000000..7ac7abc
--- /dev/null
+++ b/src/one/nio/ws/exception/CannotAcceptException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+import one.nio.ws.message.CloseMessage;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class CannotAcceptException extends WebSocketException {
+
+ public CannotAcceptException(String message) {
+ super(CloseMessage.CANNOT_ACCEPT, message);
+ }
+}
diff --git a/src/one/nio/ws/exception/HandshakeException.java b/src/one/nio/ws/exception/HandshakeException.java
new file mode 100644
index 0000000..ea59b32
--- /dev/null
+++ b/src/one/nio/ws/exception/HandshakeException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class HandshakeException extends IllegalArgumentException {
+
+ public HandshakeException(String s) {
+ super(s);
+ }
+}
diff --git a/src/one/nio/ws/exception/ProtocolException.java b/src/one/nio/ws/exception/ProtocolException.java
new file mode 100644
index 0000000..e0a392b
--- /dev/null
+++ b/src/one/nio/ws/exception/ProtocolException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+import one.nio.ws.message.CloseMessage;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class ProtocolException extends WebSocketException {
+
+ public ProtocolException(String message) {
+ super(CloseMessage.PROTOCOL_ERROR, message);
+ }
+}
diff --git a/src/one/nio/ws/exception/TooBigException.java b/src/one/nio/ws/exception/TooBigException.java
new file mode 100644
index 0000000..7da198e
--- /dev/null
+++ b/src/one/nio/ws/exception/TooBigException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+import one.nio.ws.message.CloseMessage;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class TooBigException extends WebSocketException {
+
+ public TooBigException(String message) {
+ super(CloseMessage.TOO_BIG, message);
+ }
+}
diff --git a/src/one/nio/ws/exception/VersionException.java b/src/one/nio/ws/exception/VersionException.java
new file mode 100644
index 0000000..6f37f39
--- /dev/null
+++ b/src/one/nio/ws/exception/VersionException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class VersionException extends HandshakeException {
+
+ public VersionException(String version) {
+ super("Unsupported websocket version " + version);
+ }
+}
diff --git a/src/one/nio/ws/exception/WebSocketException.java b/src/one/nio/ws/exception/WebSocketException.java
new file mode 100644
index 0000000..e8e9007
--- /dev/null
+++ b/src/one/nio/ws/exception/WebSocketException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.exception;
+
+import java.io.IOException;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class WebSocketException extends IOException {
+ private final short code;
+
+ public WebSocketException(short code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ public short code() {
+ return code;
+ }
+}
diff --git a/src/one/nio/ws/extension/Extension.java b/src/one/nio/ws/extension/Extension.java
new file mode 100644
index 0000000..9ddf15d
--- /dev/null
+++ b/src/one/nio/ws/extension/Extension.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.extension;
+
+import java.io.IOException;
+
+import one.nio.ws.frame.Frame;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public interface Extension {
+
+ void appendResponseHeaderValue(StringBuilder builder);
+
+ void transformInput(Frame frame) throws IOException;
+
+ void transformOutput(Frame frame) throws IOException;
+
+ void close();
+}
diff --git a/src/one/nio/ws/extension/ExtensionRequest.java b/src/one/nio/ws/extension/ExtensionRequest.java
new file mode 100644
index 0000000..99672ac
--- /dev/null
+++ b/src/one/nio/ws/extension/ExtensionRequest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.extension;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class ExtensionRequest {
+ private final String name;
+ private final Map parameters;
+
+ public ExtensionRequest(String name) {
+ this.name = name;
+ this.parameters = new HashMap<>();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void addParameter(String name, String value) {
+ this.parameters.put(name, value);
+ }
+
+ public Map getParameters() {
+ return Collections.unmodifiableMap(parameters);
+ }
+}
diff --git a/src/one/nio/ws/extension/ExtensionRequestParser.java b/src/one/nio/ws/extension/ExtensionRequestParser.java
new file mode 100644
index 0000000..3aa95aa
--- /dev/null
+++ b/src/one/nio/ws/extension/ExtensionRequestParser.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.extension;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The relevant ABNF for the Sec-WebSocket-Extensions is as follows:
+ * extension-list = 1#extension
+ * extension = extension-token *( ";" extension-param )
+ * extension-token = registered-token
+ * registered-token = token
+ * extension-param = token [ "=" (token | quoted-string) ]
+ * ; When using the quoted-string syntax variant, the value
+ * ; after quoted-string unescaping MUST conform to the
+ * ; 'token' ABNF.
+ * The limiting of parameter values to tokens or "quoted tokens" makes
+ * the parsing of the header significantly simpler and allows a number
+ * of short-cuts to be taken.
+ *
+ * @author Vadim Yelisseyev
+ */
+public class ExtensionRequestParser {
+
+ public static List parse(String header) {
+ final List result = new ArrayList<>();
+
+ // split the header into array of extensions using ',' as a separator
+ for (String unparsedExtension : header.split(",")) {
+ // split the extension into the registered name and parameter/value pairs
+ final String[] unparsedParameters = unparsedExtension.split(";");
+ final ExtensionRequest request = new ExtensionRequest(unparsedParameters[0].trim());
+
+ for (int i = 1; i < unparsedParameters.length; i++) {
+ int equalsPos = unparsedParameters[i].indexOf('=');
+ String name;
+ String value;
+ if (equalsPos == -1) {
+ name = unparsedParameters[i].trim();
+ value = null;
+ } else {
+ name = unparsedParameters[i].substring(0, equalsPos).trim();
+ value = unparsedParameters[i].substring(equalsPos + 1).trim();
+ int len = value.length();
+ if (len > 1) {
+ if (value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
+ value = value.substring(1, value.length() - 1);
+ }
+ }
+ }
+
+ // Make sure value doesn't contain any of the delimiters since that would indicate something went wrong
+ if (containsDelims(name) || containsDelims(value)) {
+ throw new IllegalArgumentException("An illegal extension parameter was specified with name [" + name + "] and value [" + value + "]");
+ }
+
+ request.addParameter(name, value);
+ }
+
+ result.add(request);
+ }
+
+ return result;
+ }
+
+ private static boolean containsDelims(String input) {
+ if (input == null || input.isEmpty()) {
+ return false;
+ }
+ for (int i = 0; i < input.length(); i++) {
+ switch (input.charAt(i)) {
+ case ',':
+ case ';':
+ case '\"':
+ case '=':
+ return true;
+ default:
+ // NO_OP
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/one/nio/ws/extension/PerMessageDeflate.java b/src/one/nio/ws/extension/PerMessageDeflate.java
new file mode 100644
index 0000000..db3087c
--- /dev/null
+++ b/src/one/nio/ws/extension/PerMessageDeflate.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.extension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.zip.DataFormatException;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+
+import one.nio.ws.exception.HandshakeException;
+import one.nio.ws.frame.Frame;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class PerMessageDeflate implements Extension {
+ private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
+ private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
+
+ // according to rfc7692 4 octets of 0x00 0x00 0xff 0xff must be at the tail end of the payload of the message
+ // https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.2
+ private static final byte[] EOM_BYTES = new byte[] {0, 0, -1, -1};
+ // the deflate extension requires the RSV1 bit, so RSV1 is 4 (0b100)
+ private static final int RSV_BITMASK = 0b100;
+
+ private static final int INPUT_BUFFER_SIZE = Integer.getInteger("one.nio.ws.permessage-deflate.INPUT_BUFFER_SIZE", 2048);
+ private static final int OUTPUT_BUFFER_SIZE = Integer.getInteger("one.nio.ws.permessage-deflate.OUTPUT_BUFFER_SIZE", 2048);
+
+ public static final String NAME = "permessage-deflate";
+
+ private final boolean clientContextTakeover;
+ private final boolean serverContextTakeover;
+
+ private final Inflater inflater = new Inflater(true);
+ private final byte[] inputBuffer = new byte[INPUT_BUFFER_SIZE];
+ private boolean skipDecompression = false;
+
+ private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ private final byte[] outputBuffer = new byte[OUTPUT_BUFFER_SIZE];
+
+ public static PerMessageDeflate negotiate(Map parameters) {
+ boolean clientContextTakeover = true;
+ boolean serverContextTakeover = true;
+
+ for (Map.Entry parameter : parameters.entrySet()) {
+ final String name = parameter.getKey();
+
+ if (SERVER_NO_CONTEXT_TAKEOVER.equals(name)) {
+ if (serverContextTakeover) {
+ serverContextTakeover = false;
+ } else {
+ throw new HandshakeException("Duplicate definition of the server_no_context_takeover extension parameter");
+ }
+ }
+ if (CLIENT_NO_CONTEXT_TAKEOVER.equals(name)) {
+ if (clientContextTakeover) {
+ clientContextTakeover = false;
+ } else {
+ throw new HandshakeException("Duplicate definition of the client_no_context_takeover extension parameter");
+ }
+ }
+ }
+
+ return new PerMessageDeflate(clientContextTakeover, serverContextTakeover);
+ }
+
+ private PerMessageDeflate(boolean clientContextTakeover, boolean serverContextTakeover) {
+ this.clientContextTakeover = clientContextTakeover;
+ this.serverContextTakeover = serverContextTakeover;
+ }
+
+ @Override
+ public void appendResponseHeaderValue(StringBuilder builder) {
+ builder.append(NAME);
+
+ if (!clientContextTakeover) {
+ builder.append("; ").append(CLIENT_NO_CONTEXT_TAKEOVER);
+ }
+
+ if (!serverContextTakeover) {
+ builder.append("; ").append(SERVER_NO_CONTEXT_TAKEOVER);
+ }
+ }
+
+ @Override
+ public void transformInput(Frame frame) throws IOException {
+ if (frame.isControl()) {
+ // Control frames are never compressed and may appear in the middle of fragmented frames.
+ // Pass them straight through.
+ return;
+ }
+
+ if (!frame.getOpcode().isContinuation()) {
+ // First frame in new message
+ skipDecompression = (frame.getRsv() & RSV_BITMASK) == 0;
+ }
+
+ if (skipDecompression) {
+ // Pass uncompressed frames straight through.
+ return;
+ }
+
+ frame.setPayload(decompress(frame.isFin(), frame.getPayload()));
+ frame.setRsv(frame.getRsv() & ~RSV_BITMASK);
+ }
+
+ @Override
+ public void transformOutput(Frame frame) throws IOException {
+ if (frame.isControl()) {
+ // Control frames are never compressed and may appear in the middle of fragmented frames
+ // Pass them straight through
+ return;
+ }
+
+ if (frame.getPayloadLength() == 0) {
+ // Zero length messages can't be compressed so pass them straight through
+ return;
+ }
+
+ frame.setPayload(compress(frame.getPayload()));
+ frame.setRsv(frame.getRsv() + RSV_BITMASK);
+ }
+
+ @Override
+ public void close() {
+ inflater.end();
+ deflater.end();
+ }
+
+ private byte[] decompress(boolean fin, byte[] payload) throws IOException {
+ boolean usedEomBytes = false;
+
+ if (payload == null || payload.length == 0) {
+ return payload;
+ }
+
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ inflater.setInput(payload);
+ while (true) {
+ int uncompressedBytes;
+
+ try {
+ uncompressedBytes = inflater.inflate(inputBuffer);
+ } catch (DataFormatException e) {
+ throw new IOException("Failed to decompress WebSocket frame", e);
+ }
+
+ if (uncompressedBytes > 0) {
+ out.write(inputBuffer, 0, uncompressedBytes);
+ } else {
+ if (inflater.needsInput() && !usedEomBytes) {
+ if (fin) {
+ inflater.setInput(EOM_BYTES);
+ usedEomBytes = true;
+ } else {
+ break;
+ }
+ } else {
+ if (fin && !clientContextTakeover) {
+ inflater.reset();
+ }
+ break;
+ }
+ }
+ }
+
+ return out.toByteArray();
+ }
+ }
+
+ private byte[] compress(byte[] payload) throws IOException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ deflater.setInput(payload, 0, payload.length);
+
+ int compressedLength;
+ do {
+ compressedLength = deflater.deflate(outputBuffer, 0, outputBuffer.length, Deflater.SYNC_FLUSH);
+ out.write(outputBuffer, 0, compressedLength);
+ } while (compressedLength > 0);
+
+ byte[] result = out.toByteArray();
+
+ // https://tools.ietf.org/html/rfc7692#section-7.2.1 states that if the final fragment's compressed
+ // payload ends with 0x00 0x00 0xff 0xff, they should be removed.
+ // To simulate removal, we just pass 4 bytes less to the new payload
+ // if the frame is final and outputBytes ends with 0x00 0x00 0xff 0xff.
+ if (endsWithTail(result)) {
+ result = Arrays.copyOf(result, result.length - EOM_BYTES.length);
+ }
+
+ if (!serverContextTakeover) {
+ deflater.reset();
+ }
+
+ return result;
+ }
+ }
+
+ private boolean endsWithTail(byte[] payload){
+ if(payload.length < 4) {
+ return false;
+ }
+
+ int length = payload.length;
+
+ for (int i = 0; i < EOM_BYTES.length; i++) {
+ if (EOM_BYTES[i] != payload[length - EOM_BYTES.length + i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/one/nio/ws/frame/Frame.java b/src/one/nio/ws/frame/Frame.java
new file mode 100644
index 0000000..d6eefe4
--- /dev/null
+++ b/src/one/nio/ws/frame/Frame.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.frame;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class Frame {
+ private final boolean fin;
+ private final Opcode opcode;
+ private int rsv;
+ private int payloadLength;
+ private byte[] mask;
+ private byte[] payload;
+
+ public Frame(boolean fin, Opcode opcode, int rsv, int payloadLength) {
+ this.fin = fin;
+ this.opcode = opcode;
+ this.rsv = rsv;
+ this.payloadLength = payloadLength;
+ }
+
+ public Frame(Opcode opcode, byte[] payload) {
+ this.fin = true;
+ this.rsv = 0;
+ this.opcode = opcode;
+ this.payload = payload;
+ this.payloadLength = payload.length;
+ }
+
+ public boolean isFin() {
+ return fin;
+ }
+
+ public int getRsv() {
+ return rsv;
+ }
+
+ public void setRsv(int rsv) {
+ this.rsv = rsv;
+ }
+
+ public Opcode getOpcode() {
+ return opcode;
+ }
+
+ public boolean isControl() {
+ return opcode.isControl();
+ }
+
+ public int getPayloadLength() {
+ return payloadLength;
+ }
+
+ public byte[] getMask() {
+ return mask;
+ }
+
+ public void setMask(byte[] mask) {
+ this.mask = mask;
+ }
+
+ public byte[] getPayload() {
+ return payload;
+ }
+
+ public void setPayload(byte[] payload) {
+ this.payload = payload;
+ this.payloadLength = payload.length;
+ }
+
+ public void unmask() {
+ if (mask == null) {
+ return;
+ }
+ final ByteBuffer buffer = ByteBuffer.wrap(payload);
+ final int intMask = ByteBuffer.wrap(mask).getInt();
+ while (buffer.remaining() >= 4) {
+ int pos = buffer.position();
+ buffer.putInt(pos, buffer.getInt() ^ intMask);
+ }
+ while (buffer.hasRemaining()) {
+ int pos = buffer.position();
+ buffer.put(pos, (byte) (buffer.get() ^ mask[pos % 4]));
+ }
+ setPayload(buffer.array());
+ this.mask = null;
+ }
+}
diff --git a/src/one/nio/ws/frame/FrameReader.java b/src/one/nio/ws/frame/FrameReader.java
new file mode 100644
index 0000000..cf1d01d
--- /dev/null
+++ b/src/one/nio/ws/frame/FrameReader.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.frame;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import one.nio.net.Session;
+import one.nio.ws.exception.CannotAcceptException;
+import one.nio.ws.exception.ProtocolException;
+import one.nio.ws.exception.TooBigException;
+import one.nio.ws.exception.WebSocketException;
+
+/**
+ * Websocket frame reader
+ * https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+ *
+ * @author Vadim Yelisseyev
+ */
+public class FrameReader {
+ // The smallest valid full WebSocket message is 2 bytes,
+ // such as this close message sent from the server with no payload: 138, 0.
+ // Yet the longest possible header is 14 bytes
+ // which would represent a message sent from the client to the server with a payload greater then 64KB.
+ private static final int HEADER_LENGTH = 14;
+ private static final int FIRST_HEADER_LENGTH = 2;
+ private static final int MASK_LENGTH = 4;
+
+ private final Session session;
+ private final byte[] header;
+ private final int maxFramePayloadLength = Integer.getInteger("one.nio.ws.MAX_FRAME_PAYLOAD_LENGTH", 128 * 1024);
+
+ private Frame frame;
+ private int ptr;
+
+ public FrameReader(Session session) {
+ this.session = session;
+ this.header = new byte[HEADER_LENGTH];
+ }
+
+ public Frame read() throws IOException {
+ Frame frame = this.frame;
+ int ptr = this.ptr;
+
+ if (frame == null) {
+ ptr += session.read(header, ptr, FIRST_HEADER_LENGTH - ptr);
+
+ if (ptr < FIRST_HEADER_LENGTH) {
+ this.ptr = ptr;
+ return null;
+ }
+
+ frame = createFrame(header);
+ ptr = 0;
+ this.frame = frame;
+ }
+
+ if (frame.getPayload() == null) {
+ int payloadLength = frame.getPayloadLength();
+ int len = payloadLength == 126 ? 2 : payloadLength == 127 ? 8 : 0;
+ if (len > 0) {
+ ptr += session.read(header, ptr, len - ptr);
+ if (ptr < len) {
+ this.ptr = ptr;
+ return null;
+ }
+ payloadLength = byteArrayToInt(header, len);
+ }
+ if (payloadLength < 0) {
+ throw new ProtocolException("negative payload length");
+ }
+ if (payloadLength > maxFramePayloadLength) {
+ throw new TooBigException("payload can not be more than " + maxFramePayloadLength);
+ }
+ frame.setPayload(new byte[payloadLength]);
+ ptr = 0;
+ }
+
+ if (frame.getMask() == null) {
+ ptr += session.read(header, ptr, MASK_LENGTH - ptr);
+
+ if (ptr < MASK_LENGTH) {
+ this.ptr = ptr;
+ return null;
+ }
+
+ frame.setMask(Arrays.copyOf(header, MASK_LENGTH));
+ ptr = 0;
+ }
+
+ if (ptr < frame.getPayloadLength()) {
+ ptr += session.read(frame.getPayload(), ptr, frame.getPayloadLength() - ptr);
+
+ if (ptr < frame.getPayloadLength()) {
+ this.ptr = ptr;
+ return null;
+ }
+ }
+
+ this.frame = null;
+ this.ptr = 0;
+
+ return frame;
+ }
+
+ private Frame createFrame(byte[] header) throws WebSocketException {
+ byte b0 = header[0];
+ byte b1 = header[1];
+
+ boolean fin = (b0 & 0x80) > 0;
+ int rsv = (b0 & 0x70) >>> 4;
+ Opcode opcode = Opcode.valueOf(b0 & 0x0F);
+ int payloadLength = b1 & 0x7F;
+
+ if ((b1 & 0x80) == 0) {
+ throw new ProtocolException("not masked");
+ }
+
+ if (opcode == null) {
+ throw new CannotAcceptException("invalid opcode (" + (b0 & 0x0F) + ')');
+ } else if (opcode.isControl()) {
+ if (payloadLength > 125) {
+ throw new ProtocolException("control payload too big");
+ }
+
+ if (!fin) {
+ throw new ProtocolException("control payload can not be fragmented");
+ }
+ }
+
+ return new Frame(fin, opcode, rsv, payloadLength);
+ }
+
+ private int byteArrayToInt(byte[] b, int len) {
+ int result = 0;
+ int shift = 0;
+ for (int i = len - 1; i >= 0; i--) {
+ result |= ((b[i] & 0xFF) << shift);
+ shift += 8;
+ }
+ return result;
+ }
+}
diff --git a/src/one/nio/ws/frame/FrameWriter.java b/src/one/nio/ws/frame/FrameWriter.java
new file mode 100644
index 0000000..7f39aa6
--- /dev/null
+++ b/src/one/nio/ws/frame/FrameWriter.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.frame;
+
+import java.io.IOException;
+
+import one.nio.net.Session;
+import one.nio.net.Socket;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class FrameWriter {
+ private final Session session;
+
+ public FrameWriter(Session session) {
+ this.session = session;
+ }
+
+ public void write(Frame frame) throws IOException {
+ final byte[] payload = frame.getPayload();
+ final byte[] header = serializeHeader(frame.getRsv(), frame.getOpcode(), payload);
+ session.write(header, 0, header.length, Socket.MSG_MORE);
+ session.write(payload, 0, payload.length);
+ }
+
+ private byte[] serializeHeader(int rsv, Opcode opcode, byte[] payload) {
+ int len = payload.length < 126 ? 2 : payload.length < 65536 ? 4 : 10;
+ byte[] header = new byte[len];
+ header[0] = (byte) (0x80 | (rsv << 4) | opcode.value);
+ // Next write the mask && length
+ if (payload.length < 126) {
+ header[1] = (byte) (payload.length);
+ } else if (payload.length < 65536) {
+ header[1] = (byte) 126;
+ header[2] = (byte) (payload.length >>> 8);
+ header[3] = (byte) (payload.length & 0xFF);
+ } else {
+ // Will never be more than 2^31-1
+ header[1] = (byte) 127;
+ header[6] = (byte) (payload.length >>> 24);
+ header[7] = (byte) (payload.length >>> 16);
+ header[8] = (byte) (payload.length >>> 8);
+ header[9] = (byte) payload.length;
+ }
+ return header;
+ }
+}
diff --git a/src/one/nio/ws/frame/Opcode.java b/src/one/nio/ws/frame/Opcode.java
new file mode 100644
index 0000000..77b750b
--- /dev/null
+++ b/src/one/nio/ws/frame/Opcode.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.frame;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public enum Opcode {
+ CONTINUATION(0x00),
+ TEXT(0x01),
+ BINARY(0x02),
+ CLOSE(0x08),
+ PING(0x09),
+ PONG(0x0A);
+
+ private static final Opcode[] VALUES;
+ static {
+ VALUES = new Opcode[11];
+ for (Opcode opcode : Opcode.values()) {
+ if (VALUES[opcode.value] != null) {
+ throw new IllegalArgumentException("Opcode " + opcode.value + " already used.");
+ }
+ VALUES[opcode.value] = opcode;
+ }
+ }
+
+ public final byte value;
+
+ Opcode(int value) {
+ this.value = (byte) value;
+ }
+
+ public boolean isControl() {
+ return (value & 0x08) > 0;
+ }
+
+ public boolean isContinuation() {
+ return this == CONTINUATION;
+ }
+
+ public static Opcode valueOf(int value) {
+ return VALUES[value];
+ }
+}
diff --git a/src/one/nio/ws/message/BinaryMessage.java b/src/one/nio/ws/message/BinaryMessage.java
new file mode 100644
index 0000000..2bfdd98
--- /dev/null
+++ b/src/one/nio/ws/message/BinaryMessage.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import one.nio.util.Hex;
+import one.nio.util.SimpleName;
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class BinaryMessage extends Message {
+
+ public BinaryMessage(byte[] payload) {
+ this(Opcode.BINARY, payload);
+ }
+
+ protected BinaryMessage(Opcode opcode, byte[] payload) {
+ super(opcode, payload);
+ }
+
+ @Override
+ public byte[] payload() {
+ return payload;
+ }
+
+ @Override
+ public String toString() {
+ return SimpleName.of(getClass()) + "<" + Hex.toHex(payload) + ">";
+ }
+}
diff --git a/src/one/nio/ws/message/CloseMessage.java b/src/one/nio/ws/message/CloseMessage.java
new file mode 100644
index 0000000..b490762
--- /dev/null
+++ b/src/one/nio/ws/message/CloseMessage.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import one.nio.ws.frame.Opcode;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class CloseMessage extends Message {
+ public static final short NORMAL = 1000;
+ public static final short GOING_AWAY = 1001;
+ public static final short PROTOCOL_ERROR = 1002;
+ public static final short CANNOT_ACCEPT = 1003;
+ public static final short RESERVED = 1004;
+ public static final short NO_STATUS_CODE = 1005;
+ public static final short CLOSED_ABNORMALLY = 1006;
+ public static final short NOT_CONSISTENT = 1007;
+ public static final short VIOLATED_POLICY = 1008;
+ public static final short TOO_BIG = 1009;
+ public static final short NO_EXTENSION = 1010;
+ public static final short UNEXPECTED_CONDITION = 1011;
+ public static final short SERVICE_RESTART = 1012;
+ public static final short TRY_AGAIN_LATER = 1013;
+ public static final short TLS_HANDSHAKE_FAILURE = 1015;
+
+ public CloseMessage(byte[] payload) {
+ this(payload.length == 0 ? null : ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN).getShort());
+ }
+
+ public CloseMessage(Short code) {
+ super(Opcode.CLOSE, code);
+ }
+
+ @Override
+ public byte[] payload() {
+ return new byte[] {
+ (byte) (payload & 0xff),
+ (byte) ((payload >> 8) & 0xff)
+ };
+ }
+
+ @Override
+ public String toString() {
+ return "CloseMessage<" + payload + ">";
+ }
+}
+
diff --git a/src/one/nio/ws/message/Message.java b/src/one/nio/ws/message/Message.java
new file mode 100644
index 0000000..f32f952
--- /dev/null
+++ b/src/one/nio/ws/message/Message.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public abstract class Message {
+ protected final Opcode opcode;
+ protected final T payload;
+
+ protected Message(Opcode opcode, T payload) {
+ this.opcode = opcode;
+ this.payload = payload;
+ }
+
+ public Opcode opcode() {
+ return opcode;
+ }
+
+ public abstract byte[] payload();
+}
diff --git a/src/one/nio/ws/message/MessageReader.java b/src/one/nio/ws/message/MessageReader.java
new file mode 100644
index 0000000..97bf89d
--- /dev/null
+++ b/src/one/nio/ws/message/MessageReader.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import one.nio.net.Session;
+import one.nio.ws.exception.TooBigException;
+import one.nio.ws.exception.WebSocketException;
+import one.nio.ws.extension.Extension;
+import one.nio.ws.frame.Frame;
+import one.nio.ws.frame.FrameReader;
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class MessageReader {
+ private final FrameReader reader;
+ private final List extensions;
+ private final int maxMessagePayloadLength = Integer.getInteger("one.nio.ws.MAX_MESSAGE_PAYLOAD_LENGTH", 16 * 1024 * 1024);
+
+ private PayloadBuffer buffer;
+
+ public MessageReader(Session session, List extensions) {
+ this.reader = new FrameReader(session);
+ this.extensions = extensions;
+ }
+
+ public Message> read() throws IOException {
+ final Frame frame = reader.read();
+ if (frame == null) {
+ // not all frame data was read from socket
+ return null;
+ }
+ if (frame.isControl()) {
+ // control messages can not be fragmented
+ // and it can be between 2 fragments of another message
+ // so handle it separately
+ return createMessage(frame.getOpcode(), getPayload(frame));
+ }
+ if (!frame.isFin()) {
+ // not finished fragmented frame
+ // append it to buffer and wait for next frames
+ appendFrame(frame);
+ return null;
+ }
+ if (buffer != null) {
+ // buffer is not null, and this is the frame with fin=true
+ // so collect all data from buffer to the resulting message
+ appendFrame(frame);
+ Message> message = createMessage(buffer.getOpcode(), buffer.getPayload());
+ buffer = null;
+ return message;
+ }
+ // just a simple message consisting of one fragment
+ return createMessage(frame.getOpcode(), getPayload(frame));
+ }
+
+ private void appendFrame(Frame frame) throws IOException {
+ if (buffer == null) {
+ buffer = new PayloadBuffer(frame.getOpcode(), maxMessagePayloadLength);
+ }
+ buffer.append(getPayload(frame));
+ }
+
+ private Message> createMessage(Opcode opcode, byte[] payload) {
+ switch (opcode) {
+ case CLOSE:
+ return new CloseMessage(payload);
+ case PING:
+ return new PingMessage(payload);
+ case PONG:
+ return new PongMessage(payload);
+ case BINARY:
+ return new BinaryMessage(payload);
+ case TEXT:
+ return new TextMessage(new String(payload, StandardCharsets.UTF_8));
+ }
+ throw new IllegalArgumentException("Unsupported opcode: " + opcode);
+ }
+
+ private byte[] getPayload(Frame frame) throws IOException {
+ frame.unmask();
+ for (Extension extension : extensions) {
+ extension.transformInput(frame);
+ }
+ return frame.getPayload();
+ }
+
+ public static class PayloadBuffer {
+ private final Opcode opcode;
+ private final List chunks;
+ private final int maxMessagePayloadLength;
+ private int payloadLength;
+
+ public PayloadBuffer(Opcode opcode, int maxMessagePayloadLength) {
+ this.opcode = opcode;
+ this.chunks = new ArrayList<>();
+ this.maxMessagePayloadLength = maxMessagePayloadLength;
+ }
+
+ public Opcode getOpcode() {
+ return opcode;
+ }
+
+ public byte[] getPayload() {
+ final byte[] result = new byte[payloadLength];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ int length = chunk.length;
+ System.arraycopy(chunk,0, result, pos, length);
+ pos += length;
+ }
+ return result;
+ }
+
+ public void append(byte[] payload) throws WebSocketException {
+ payloadLength += payload.length;
+ if (payloadLength > this.maxMessagePayloadLength) {
+ throw new TooBigException("payload can not be more than " + maxMessagePayloadLength);
+ }
+ chunks.add(payload);
+ }
+ }
+}
diff --git a/src/one/nio/ws/message/MessageWriter.java b/src/one/nio/ws/message/MessageWriter.java
new file mode 100644
index 0000000..69e3ce6
--- /dev/null
+++ b/src/one/nio/ws/message/MessageWriter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import java.io.IOException;
+import java.util.List;
+
+import one.nio.net.Session;
+import one.nio.ws.extension.Extension;
+import one.nio.ws.frame.Frame;
+import one.nio.ws.frame.FrameWriter;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class MessageWriter {
+ private final FrameWriter writer;
+ private final List extensions;
+
+ public MessageWriter(Session session, List extensions) {
+ this.writer = new FrameWriter(session);
+ this.extensions = extensions;
+ }
+
+ public void write(Message> message) throws IOException {
+ Frame frame = new Frame(message.opcode(), message.payload());
+ for (Extension extension : extensions) {
+ extension.transformOutput(frame);
+ }
+ writer.write(frame);
+ }
+}
diff --git a/src/one/nio/ws/message/PingMessage.java b/src/one/nio/ws/message/PingMessage.java
new file mode 100644
index 0000000..350d46d
--- /dev/null
+++ b/src/one/nio/ws/message/PingMessage.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import one.nio.http.Response;
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class PingMessage extends BinaryMessage {
+ public static final PingMessage EMPTY = new PingMessage(Response.EMPTY);
+
+ public PingMessage(byte[] payload) {
+ super(Opcode.PING, payload);
+ }
+}
diff --git a/src/one/nio/ws/message/PongMessage.java b/src/one/nio/ws/message/PongMessage.java
new file mode 100644
index 0000000..0c7a535
--- /dev/null
+++ b/src/one/nio/ws/message/PongMessage.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import one.nio.http.Response;
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class PongMessage extends BinaryMessage {
+ public static final PongMessage EMPTY = new PongMessage(Response.EMPTY);
+
+ public PongMessage(byte[] payload) {
+ super(Opcode.PONG, payload);
+ }
+}
+
diff --git a/src/one/nio/ws/message/TextMessage.java b/src/one/nio/ws/message/TextMessage.java
new file mode 100644
index 0000000..e208d66
--- /dev/null
+++ b/src/one/nio/ws/message/TextMessage.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 Odnoklassniki Ltd, Mail.Ru Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package one.nio.ws.message;
+
+import java.nio.charset.StandardCharsets;
+
+import one.nio.ws.frame.Opcode;
+
+/**
+ * @author Vadim Yelisseyev
+ */
+public class TextMessage extends Message {
+
+ public TextMessage(String payload) {
+ super(Opcode.TEXT, payload);
+ }
+
+ @Override
+ public byte[] payload() {
+ return payload.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public String toString() {
+ return "TextMessage<" + payload + ">";
+ }
+}
diff --git a/test/one/nio/ws/EchoServerTest.java b/test/one/nio/ws/EchoServerTest.java
new file mode 100644
index 0000000..8eb1f7f
--- /dev/null
+++ b/test/one/nio/ws/EchoServerTest.java
@@ -0,0 +1,51 @@
+package one.nio.ws;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+
+import one.nio.rpc.echo.EchoServer;
+import one.nio.server.AcceptorConfig;
+import one.nio.ws.message.TextMessage;
+
+public class EchoServerTest {
+
+ public static void main(String[] args) throws IOException {
+ EchoServer server = new EchoServer(config());
+ server.registerShutdownHook();
+ server.start();
+ }
+
+ private static WebSocketServerConfig config() {
+ WebSocketServerConfig config = new WebSocketServerConfig();
+ config.supportedProtocols = Collections.singleton("echo1");
+ config.websocketBaseUri = "/echo";
+ config.keepAlive = 30000;
+ config.maxWorkers = 1000;
+ config.queueTime = 50;
+ config.acceptors = acceptors();
+ return config;
+ }
+
+ private static AcceptorConfig[] acceptors() {
+ AcceptorConfig config = new AcceptorConfig();
+ config.port = 8002;
+ config.backlog = 10000;
+ config.deferAccept = true;
+ return new AcceptorConfig[] {
+ config
+ };
+ }
+
+ public static class EchoServer extends WebSocketServer {
+
+ public EchoServer(WebSocketServerConfig config) throws IOException {
+ super(config);
+ }
+
+ @Override
+ public void handleMessage(WebSocketSession session, TextMessage message) throws IOException {
+ session.sendMessage(new TextMessage(new String(message.payload(), StandardCharsets.UTF_8)));
+ }
+ }
+}
diff --git a/test/one/nio/ws/ExtensionRequestTest.java b/test/one/nio/ws/ExtensionRequestTest.java
new file mode 100644
index 0000000..3a694e8
--- /dev/null
+++ b/test/one/nio/ws/ExtensionRequestTest.java
@@ -0,0 +1,44 @@
+package one.nio.ws;
+
+import one.nio.ws.extension.ExtensionRequest;
+import one.nio.ws.extension.ExtensionRequestParser;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+public class ExtensionRequestTest {
+
+ @Test
+ public void testSimpleValue() {
+ String header = "permessage-deflate; client_max_window_bits";
+ List parsed = ExtensionRequestParser.parse(header);
+ assertEquals(1, parsed.size());
+ assertEquals("permessage-deflate", parsed.get(0).getName());
+ assertEquals("permessage-deflate", parsed.get(0).getName());
+ }
+
+ @Test
+ public void testMultiValue() {
+ String header = "mux;max-channels=a;flow-control,deflate-stream";
+ List parsed = ExtensionRequestParser.parse(header);
+ assertEquals(2, parsed.size());
+ assertEquals("mux", parsed.get(0).getName());
+ assertEquals(2, parsed.get(0).getParameters().size());
+ assertEquals("deflate-stream", parsed.get(1).getName());
+ assertEquals(0, parsed.get(1).getParameters().size());
+ }
+
+ @Test
+ public void testMultiValueWithQuotasAndSpaces() {
+ String header = "mux; max-channels=\"a\"; flow-control=\"\"";
+ List parsed = ExtensionRequestParser.parse(header);
+ assertEquals(1, parsed.size());
+ assertEquals("mux", parsed.get(0).getName());
+ assertEquals(2, parsed.get(0).getParameters().size());
+ assertEquals("a", parsed.get(0).getParameters().get("max-channels"));
+ assertEquals("", parsed.get(0).getParameters().get("flow-control"));
+ }
+
+}
diff --git a/test/one/nio/ws/echo.html b/test/one/nio/ws/echo.html
new file mode 100644
index 0000000..b107dc1
--- /dev/null
+++ b/test/one/nio/ws/echo.html
@@ -0,0 +1,87 @@
+
+
+
+
+ Echo server test
+
+
+
+