commit 6ae2ece6d66bb69c421c34280b2cf1c1da4defc6
Author: yaohunya <1763917516@qq.com>
Date: Fri Jul 18 22:50:07 2025 +0800
1.2.0
diff --git a/lib/BungeeCord.jar b/lib/BungeeCord.jar
new file mode 100644
index 0000000..2ea6995
Binary files /dev/null and b/lib/BungeeCord.jar differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c0afcdf
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ com.yaohun.guaji.AuGuaJi
+ AuMcBot
+ 1.0-SNAPSHOT
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ public
+ https://repo.aurora-pixels.com/repository/public/
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/yaohun/mcbot/McBot.java b/src/main/java/com/yaohun/mcbot/McBot.java
new file mode 100644
index 0000000..4a6e6d8
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/McBot.java
@@ -0,0 +1,66 @@
+package com.yaohun.mcbot;
+
+import com.yaohun.mcbot.client.QQWebSocketClient;
+import com.yaohun.mcbot.commands.BindAdminCmd;
+import com.yaohun.mcbot.commands.BindTencentCmd;
+import com.yaohun.mcbot.config.Config;
+import com.yaohun.mcbot.data.sql.SQLIO;
+import com.yaohun.mcbot.listsener.FriendListener;
+import com.yaohun.mcbot.listsener.GroupListener;
+import com.yaohun.mcbot.manage.CacheManager;
+import com.yaohun.mcbot.manage.WssReconnectManager;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.plugin.Plugin;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class McBot extends Plugin {
+
+ private static McBot instance;
+
+ private static WssReconnectManager wssManager;
+
+ @Override
+ public void onEnable() {
+ instance = this;
+ getLogger().info("[McBot] Start Plugins ...");
+ Config.reloadConfig(this);
+ SQLIO.init(); // 激活MySQL - 数据库的连接
+
+ String serverUri = "ws://127.0.0.1:6050";
+ WssReconnectManager.initialize(this, serverUri);
+
+ QQWebSocketClient client = new QQWebSocketClient(serverUri);
+ WssReconnectManager.setClient(client);
+ client.connect();
+
+ getProxy().getPluginManager().registerCommand(this, new BindTencentCmd());
+ getProxy().getPluginManager().registerCommand(this, new BindAdminCmd());
+
+ getProxy().getPluginManager().registerListener(this, new GroupListener());
+ getProxy().getPluginManager().registerListener(this, new FriendListener());
+
+ ProxyServer.getInstance().getScheduler().schedule(this, CacheManager::checkingCleanUpData, 1L, 5L, TimeUnit.MINUTES);
+ }
+
+ @Override
+ public void onDisable() {
+ SQLIO.closeConnection(); // 关闭MySQL - 数据库的连接
+ getLogger().info("[WebSocket] 插件卸载,准备关闭 WebSocket 通道...");
+ QQWebSocketClient client = WssReconnectManager.getClient();
+ if (client != null && client.isOpen()) {
+ try {
+ client.close(); // 发送关闭帧 + 关闭资源
+ getLogger().info("[WebSocket] WebSocket 通道已成功关闭。");
+ } catch (Exception e) {
+ getLogger().warning("[WebSocket] 关闭通道时出现异常: " + e.getMessage());
+ }
+ }
+ }
+
+
+ public static McBot inst() {
+ return instance;
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/MessageForwarder.java b/src/main/java/com/yaohun/mcbot/MessageForwarder.java
new file mode 100644
index 0000000..a0e7712
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/MessageForwarder.java
@@ -0,0 +1,92 @@
+package com.yaohun.mcbot;
+
+
+import com.yaohun.mcbot.data.BotFriend;
+import com.yaohun.mcbot.data.BotGroup;
+import com.yaohun.mcbot.event.BotFriendMessageEvent;
+import com.yaohun.mcbot.event.BotGroupMessageEvent;
+import net.md_5.bungee.api.ProxyServer;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class MessageForwarder {
+
+ public static void friendsAnalyze(String json){
+ try {
+ JSONObject obj = new JSONObject(json);
+ // 检测类型是否是 “好友消息” 若不是则拦截返回
+ if (!"private".equals(obj.optString("message_type"))) return;
+ // 获取机器人的QQ号
+ long botId = obj.optLong("self_id");
+ // 获取聊天信息内容 若没有消息则返回
+ JSONArray messageArray = obj.optJSONArray("message");
+ if (messageArray == null) return;
+ // 将聊天信息内容 builder
+ StringBuilder finalMessage = new StringBuilder();
+ for (int i = 0; i < messageArray.length(); i++) {
+ JSONObject segment = messageArray.getJSONObject(i);
+ String type = segment.optString("type");
+ if ("text".equals(type)) {
+ String text = segment.getJSONObject("data").optString("text");
+ finalMessage.append(text);
+ }
+ // 可以在这里加对 image、face 等其他类型的支持
+ }
+ String message = finalMessage.toString();
+ if(message.isEmpty()) return;
+ // 解析发送信息的人
+ JSONObject sender = obj.optJSONObject("sender");
+ if (sender == null) return;
+ // 获取信息
+ long senderId = sender.optLong("user_id"); // QQ号
+ String nickname = sender.optString("nickname"); // QQ昵称
+ BotFriendMessageEvent event = new BotFriendMessageEvent(botId,senderId,nickname,message,new BotFriend(senderId));
+ ProxyServer.getInstance().getPluginManager().callEvent(event);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void groupAnalyze(String json) {
+ try {
+ JSONObject obj = new JSONObject(json);
+ // 检测类型是否是 “群消息” 若不是则拦截返回
+ if (!"group".equals(obj.optString("message_type"))) return;
+ // 获取机器人的QQ号
+ long botId = obj.optLong("self_id");
+ long groupId = obj.optLong("group_id");
+ // 获取聊天信息内容 若没有消息则返回
+ JSONArray messageArray = obj.optJSONArray("message");
+ if (messageArray == null) return;
+ // 将聊天信息内容 builder
+ StringBuilder finalMessage = new StringBuilder();
+ for (int i = 0; i < messageArray.length(); i++) {
+ JSONObject segment = messageArray.getJSONObject(i);
+ String type = segment.optString("type");
+ if ("text".equals(type)) {
+ String text = segment.getJSONObject("data").optString("text");
+ finalMessage.append(text);
+ }
+ // 可以在这里加对 image、face 等其他类型的支持
+ }
+ String message = finalMessage.toString();
+ if(message.isEmpty()) return;
+ // 解析发送信息的人
+ JSONObject sender = obj.optJSONObject("sender");
+ if (sender == null) return;
+ // 获取信息
+ long senderId = sender.optLong("user_id"); // QQ号
+ String nickname = sender.optString("nickname"); // QQ昵称
+ String card = sender.optString("card"); // 群名片
+ String role = sender.optString("role"); // 群中职务
+ // 优先显示群名片(如果存在),否则用昵称
+ String displayName = (card != null && !card.isEmpty()) ? card : nickname;
+
+ BotGroupMessageEvent event = new BotGroupMessageEvent(botId,groupId,senderId,displayName,message,new BotGroup(groupId));
+ ProxyServer.getInstance().getPluginManager().callEvent(event);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/api/McBotAPI.java b/src/main/java/com/yaohun/mcbot/api/McBotAPI.java
new file mode 100644
index 0000000..42b75f9
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/api/McBotAPI.java
@@ -0,0 +1,16 @@
+package com.yaohun.mcbot.api;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.data.BotFriend;
+import com.yaohun.mcbot.data.BotGroup;
+
+public class McBotAPI {
+
+ public static BotFriend getFriend(long senderID){
+ return new BotFriend(senderID);
+ }
+
+ public static BotGroup getGroup(long groupID){
+ return new BotGroup(groupID);
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java b/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java
new file mode 100644
index 0000000..3a1a25d
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java
@@ -0,0 +1,53 @@
+package com.yaohun.mcbot.client;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.MessageForwarder;
+import com.yaohun.mcbot.manage.WssReconnectManager;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class QQWebSocketClient extends WebSocketClient {
+
+ public QQWebSocketClient( String serverUri) {
+ super(URI.create(serverUri));
+ }
+
+ @Override
+ public void onOpen(ServerHandshake handshake) {
+ McBot.inst().getLogger().info("[WebSocket-Link] connectionSuccessfully!");
+ }
+
+ @Override
+ public void onMessage(String message) {
+ JSONObject json = new JSONObject(message);
+ if ("group".contains(json.optString("message_type"))){
+ MessageForwarder.groupAnalyze(message);
+ // System.out.println("[WebSocket-Group] "+json.getString("raw_message"));
+ } else if ("private".contains(json.optString("message_type"))){
+ MessageForwarder.friendsAnalyze(message);
+ // System.out.println("[WebSocket-Friends] "+json.getString("raw_message"));
+ } else {
+ System.out.println("[WebSocket-MSG] "+json);
+ }
+ }
+
+
+ @Override
+ public void onClose(int code, String reason, boolean remote) {
+ WssReconnectManager.tryReconnect();
+ }
+
+ @Override
+ public void onError(Exception ex) {
+ WssReconnectManager.tryReconnect();
+ }
+
+}
diff --git a/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java b/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java
new file mode 100644
index 0000000..dc3031b
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java
@@ -0,0 +1,75 @@
+package com.yaohun.mcbot.commands;
+
+import com.yaohun.mcbot.config.Config;
+import com.yaohun.mcbot.data.sql.SQLIO;
+import com.yaohun.mcbot.manage.CacheManager;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.HoverEvent;
+import net.md_5.bungee.api.chat.TextComponent;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
+
+import java.util.List;
+
+public class BindAdminCmd extends Command {
+
+
+ private static String prefix;
+
+ public BindAdminCmd() {
+ super("bindadmin");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args) {
+ if(!sender.hasPermission("bungeecord.*")){return;}
+ if(args.length == 0){
+ sender.sendMessage("§e------- ======= §6妖魂的MC机器人 §e======= -------");
+ sender.sendMessage("§2/bindadmin clear §f- §2清理所有缓存");
+ sender.sendMessage("§2/bindadmin change §e[玩家名] §b<企鹅号> §f- §2修改绑定");
+ sender.sendMessage("§2/bindadmin group §e[群号] §f- §2新增/删除检测群");
+ sender.sendMessage("§e------- ======= §6妖魂的MC机器人 §e======= -------");
+ return;
+ }
+ if(args.length == 1 && args[0].equalsIgnoreCase("clear")){
+ CacheManager.codeDataMap.clear();
+ CacheManager.randomCodeMap.clear();
+ sender.sendMessage("[Bot管理] 所有缓存数据已被清理");
+ return;
+ }
+ if(args[0].equalsIgnoreCase("change")){
+ if(args.length == 1){
+ sender.sendMessage("[Bot管理] 请填入正确的玩家名.");
+ return;
+ }
+ if(args.length == 2){
+ sender.sendMessage("[Bot管理] 请填入正确的企鹅号.");
+ return;
+ }
+ String playerName = args[1];
+ String senderID = args[2];
+ SQLIO.saveGroup(playerName, senderID);
+ sender.sendMessage("[Bot管理] 已更改 "+playerName+" 的绑定为 <"+senderID+">");
+ return;
+ }
+ if(args[0].equalsIgnoreCase("group")){
+ if(args.length == 1){
+ sender.sendMessage("[Bot管理] 请填写群号.");
+ return;
+ }
+ long groupID = Long.parseLong(args[1]);
+ List groupAccountList = Config.BOT_GroupAccounts;
+ if(groupAccountList.contains(groupID)){
+ Config.BOT_GroupAccounts.remove(groupID);
+ sender.sendMessage("[Bot管理] 已移除检测群: "+groupID);
+ } else {
+ Config.BOT_GroupAccounts.add(groupID);
+ sender.sendMessage("[Bot管理] 已添加检测群: "+groupID);
+ }
+ Config.saveBOT_GroupAccounts();
+ return;
+ }
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java b/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java
new file mode 100644
index 0000000..b819800
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java
@@ -0,0 +1,91 @@
+package com.yaohun.mcbot.commands;
+
+import com.yaohun.mcbot.config.Config;
+import com.yaohun.mcbot.data.sql.SQLIO;
+import com.yaohun.mcbot.manage.CacheManager;
+import gnu.trove.stack.TLongStack;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.HoverEvent;
+import net.md_5.bungee.api.chat.TextComponent;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Command;
+
+public class BindTencentCmd extends Command {
+
+ private static String prefix;
+
+ public BindTencentCmd() {
+ super("bind");
+ prefix = Config.getMessage("prefix");
+ }
+
+ @Override
+ public void execute(CommandSender sender, String[] args) {
+ // 首先判断输入此命令的是否是玩家
+ if(sender instanceof ProxiedPlayer) {
+ if (args.length == 2) {
+ String tencentQQ = args[0].replaceAll("\\[|\\]", "");
+ String tencentQQRepeat = args[1].replaceAll("\\[|\\]", "");
+ ProxiedPlayer player = (ProxiedPlayer) sender;
+ if (!tencentQQ.equalsIgnoreCase(tencentQQRepeat)) {
+ String msg = Config.getMessage("differentTwice");
+ player.sendMessage(prefix+msg);
+ return;
+ }
+ String userName = player.getName();
+ String cacheCode = CacheManager.getRandomCodeData(userName);
+ if(cacheCode != null) {
+ sender.sendMessage(" ");
+ sender.sendMessage(" ");
+ if(!Config.Group_Info.isEmpty()) {
+ sender.sendMessage(Config.Group_Info);
+ }
+ SendCopyMessage(player,cacheCode);
+ sender.sendMessage(" ");
+ return;
+ }
+ // 判断玩家是否已绑定QQ号 若已绑定则拦截数据
+ String userID = SQLIO.getUserID(userName);
+ if (userID != null) {
+ String message = Config.getMessage("alreadyBindUserID");
+ player.sendMessage(prefix + message.replace("%qq%", userID));
+ return;
+ }
+ // 判断QQ是否已绑定过账号 若已绑定则连接数据
+ String user = SQLIO.getUserName(tencentQQ);
+ if (user != null) {
+ String message = Config.getMessage("alreadyBindUserName");
+ player.sendMessage(prefix + message.replace("%user%", user));
+ return;
+ }
+ // 获取绑定验证码发送给玩家
+ String randomCode = Config.getRandomString();
+ CacheManager.setRandomCodeData(userName, randomCode); // 将验证码存入缓存
+ CacheManager.setCodeDataMap(randomCode,new CacheManager.CodeData(userName,randomCode,tencentQQ));
+ sender.sendMessage(" ");
+ sender.sendMessage(" ");
+ if(!Config.Group_Info.isEmpty()) {
+ sender.sendMessage(Config.Group_Info);
+ }
+ SendCopyMessage(player, randomCode);
+ sender.sendMessage(" ");
+ } else {
+ String msg = Config.getMessage("correctOperate");
+ sender.sendMessage(prefix+msg);
+ }
+ } else {
+ sender.sendMessage("§c此命令仅限以玩家身份执行.");
+ }
+ }
+
+ public void SendCopyMessage(ProxiedPlayer player,String bind_code) {
+ String message = prefix+Config.getMessage("verificationCode").replace("%code%",bind_code);
+ TextComponent component = new TextComponent(message);
+ System.out.print("玩家: "+player.getName()+" randomCode: "+bind_code);
+ component.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/绑定 "+bind_code));
+ component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§e§l/绑定 "+bind_code).create()));
+ player.sendMessage(component);
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/config/Config.java b/src/main/java/com/yaohun/mcbot/config/Config.java
new file mode 100644
index 0000000..2ede2fd
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/config/Config.java
@@ -0,0 +1,126 @@
+package com.yaohun.mcbot.config;
+
+import com.google.common.io.ByteStreams;
+import com.yaohun.mcbot.McBot;
+import net.md_5.bungee.config.Configuration;
+import net.md_5.bungee.config.ConfigurationProvider;
+import net.md_5.bungee.config.YamlConfiguration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+
+public class Config {
+
+ private static Configuration config;
+ public static String SQL_Host;
+ public static String SQL_Port;
+ public static String SQL_Database;
+ public static String SQL_Users;
+ public static String SQL_Password;
+ public static List BOT_GroupAccounts = new ArrayList<>();
+ public static String Group_Info = "";
+ private static HashMap messageMap = new HashMap<>();
+
+ public static void reloadConfig(McBot plugin) {
+ if(!plugin.getDataFolder().exists()) {
+ plugin.getDataFolder().mkdir();
+ }
+ File file = new File(plugin.getDataFolder(), "config.yml");
+ if(!file.exists()) {
+ try {
+ file.createNewFile();
+ try (InputStream is = plugin.getResourceAsStream("config.yml");
+ OutputStream os = Files.newOutputStream(file.toPath())) {
+ ByteStreams.copy(is, os);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to create configuration file", e);
+ }
+ }
+ try {
+ ConfigurationProvider cfg = ConfigurationProvider.getProvider(YamlConfiguration.class);
+ config = cfg.load(file);
+ loadMySQLData();
+ loadConfigData();
+ loadMessageData();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void saveBOT_GroupAccounts() {
+ List groupAccountList = new ArrayList<>();
+ for (Long l : BOT_GroupAccounts) {
+ groupAccountList.add(l.toString());
+ }
+ File file = new File(McBot.inst().getDataFolder(), "config.yml");
+ // 转换 List 为 List,避免 YAML 不兼容 Long 处理
+ config.set("Group-Numbers", groupAccountList);
+ try {
+ ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, file);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void loadMySQLData(){
+ SQL_Host = config.getString("MYSQL.Host");
+ SQL_Port = config.getString("MYSQL.Port");
+ SQL_Database = config.getString("MYSQL.Database");
+ SQL_Users = config.getString("MYSQL.Users");
+ SQL_Password = config.getString("MYSQL.Password");
+ }
+
+ private static void loadConfigData(){
+ List stringList = config.getStringList("Group-Numbers");
+ System.out.println("[AuBot] 监听QQ群: ");
+ for (String value : stringList) {
+ BOT_GroupAccounts.add(Long.parseLong(value));
+ System.out.println("- "+value);
+ }
+ Group_Info = config.getString("Group_Info","").replace("&","§");
+ }
+
+ private static void loadMessageData(){
+ if(config.getString("Message") == null) return;
+ for (String key : config.getSection("Message").getKeys()){
+ String value = config.getString("Message." + key);
+ messageMap.put(key, value.replace("&","§"));
+ }
+ System.out.println("[AuBot] 语言参数 "+messageMap.size()+"个");
+ }
+
+ public static String getMessage(String key){
+ if(messageMap.containsKey(key)){
+ return messageMap.get(key);
+ }
+ return "缺少节点."+key;
+ }
+
+ public static String getRandomString() {
+ String str = "CEFGHIJKLMNPQTXY34679";
+ Random random = new Random();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 5; i++) {
+ int number = random.nextInt(21);
+ sb.append(str.charAt(number));
+ }
+ return sb.toString();
+ }
+
+ public static String extractBindCode(String message) {
+ return message
+ .replace("/绑定 ", "")
+ .replace(" ", "")
+ .toUpperCase()
+ .replaceAll("[^A-Z0-9]", "");
+ }
+
+}
diff --git a/src/main/java/com/yaohun/mcbot/data/BotFriend.java b/src/main/java/com/yaohun/mcbot/data/BotFriend.java
new file mode 100644
index 0000000..2c472a0
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/data/BotFriend.java
@@ -0,0 +1,27 @@
+package com.yaohun.mcbot.data;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.manage.WssReconnectManager;
+
+public class BotFriend {
+
+ private long userId;
+
+ public BotFriend(long userId) {
+ this.userId = userId;
+ }
+
+ public void sendMessage(String message){
+ String escapedMessage = message
+ .replace("\\", "\\\\") // 转义反斜线
+ .replace("\"", "\\\"") // 转义引号
+ .replace("\n", "\\n") // 转义换行符
+ .replace("\r", "\\r"); // 转义回车符(保险起见)
+
+ String payload = String.format(
+ "{\"action\":\"send_private_msg\",\"params\":{\"user_id\":%d,\"message\":\"%s\"}}",
+ userId, escapedMessage
+ );
+ WssReconnectManager.getClient().send(payload);
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/data/BotGroup.java b/src/main/java/com/yaohun/mcbot/data/BotGroup.java
new file mode 100644
index 0000000..3ae0945
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/data/BotGroup.java
@@ -0,0 +1,27 @@
+package com.yaohun.mcbot.data;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.manage.WssReconnectManager;
+
+public class BotGroup {
+
+ private long groupId;
+
+ public BotGroup(long groupId) {
+ this.groupId = groupId;
+ }
+
+ public void sendMessage(String message){
+ String escapedMessage = message
+ .replace("\\", "\\\\") // 转义反斜线
+ .replace("\"", "\\\"") // 转义引号
+ .replace("\n", "\\n") // 转义换行符
+ .replace("\r", "\\r"); // 转义回车符(保险起见)
+
+ String payload = String.format(
+ "{\"action\":\"send_group_msg\",\"params\":{\"group_id\":%d,\"message\":\"%s\"}}",
+ groupId, escapedMessage
+ );
+ WssReconnectManager.getClient().send(payload);
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java b/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java
new file mode 100644
index 0000000..4a90cb7
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java
@@ -0,0 +1,179 @@
+package com.yaohun.mcbot.data.sql;
+
+import com.yaohun.mcbot.config.Config;
+
+import java.sql.*;
+
+public class SQLIO {
+
+ private static String host;
+ private static int port;
+ private static String database;
+ private static String username;
+ private static String password;
+ private static String tableName;
+
+ private static Connection connection;
+
+ public static void init(){
+ host = Config.SQL_Host;
+ port = Integer.parseInt(Config.SQL_Port);
+ database = Config.SQL_Database;
+ username = Config.SQL_Users;
+ password = Config.SQL_Password;
+ tableName = "binddata";
+ try {
+ Class.forName("com.mysql.jdbc.Driver");
+ getConnection();
+ initTable();
+ } catch (Exception e){
+ e.printStackTrace();
+ }
+ }
+
+ private static void initTable() {
+ try {
+ Statement stat = getConnection().createStatement();
+ stat.execute(
+ "CREATE TABLE IF NOT EXISTS "+tableName+" (" +
+ " username VARCHAR(64) NOT NULL," +
+ " userid VARCHAR(64) NOT NULL," +
+ " PRIMARY KEY (username)" +
+ ")"
+ );
+ stat.close();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static boolean hasUserNameExit(String userName) {
+ try {
+ try (PreparedStatement ps = getConnection().prepareStatement("SELECT `userid` FROM "+tableName+" WHERE `username` = ? LIMIT 1")) {
+ ps.setString(1, userName);
+ try (ResultSet rs = ps.executeQuery()) {
+ return rs.next();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public static boolean hasUserIDExit(String userID) {
+ try {
+ String sql = "SELECT `username` FROM " + tableName + " WHERE `userid` = ? LIMIT 1";
+ try (PreparedStatement ps = getConnection().prepareStatement(sql)) {
+ ps.setString(1, userID);
+ try (ResultSet rs = ps.executeQuery()) {
+ return rs.next(); // 查询到了,说明存在
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public static void saveGroup(String userName, String userID) {
+ if (!SQLIO.hasUserNameExit(userName)) {
+ insertGroup(userName, userID);
+ } else {
+ updateGroup(userName, userID);
+ }
+ }
+
+ private static void insertGroup(String userName, String userID) {
+ try {
+ String sql = "INSERT INTO "+tableName+"(username, userid) VALUES(?,?);";
+ try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) {
+ ps.setString(1, userName);
+ ps.setString(2 , userID);
+ ps.executeUpdate();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void updateGroup(String userName, String userID) {
+ try {
+ String sql = "UPDATE `"+tableName+"` SET `userid` = ? WHERE `username` = ? LIMIT 1";
+ try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) {
+ ps.setString(1, userID);
+ ps.setString(2, userName);
+ ps.executeUpdate();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static String getUserID(String userName) {
+ try {
+ String sql = "SELECT `userid` FROM `"+tableName+"` WHERE `username` = ? LIMIT 1";
+ try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) {
+ ps.setString(1, userName);
+ try (ResultSet resultSet = ps.executeQuery()) {
+ if (resultSet.next()) {
+ return resultSet.getString(1);
+ }
+ }
+ }
+ return null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static String getUserName(String userID) {
+ try {
+ String sql = "SELECT `username` FROM `" + tableName + "` WHERE `userid` = ? LIMIT 1";
+ try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) {
+ ps.setString(1, userID);
+ try (ResultSet resultSet = ps.executeQuery()) {
+ if (resultSet.next()) {
+ return resultSet.getString(1); // 返回 username
+ }
+ }
+ }
+ return null;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Connection getConnection() {
+ try {
+ if (connection == null) {
+ String url = "jdbc:mysql://" + host + ":" + port + "/" + database
+ + "?autoReconnect=true"
+ + "&failOverReadOnly=false"
+ + "&allowMultiQueries=true"
+ + "&useSSL=false"
+ + "&useUnicode=true"
+ + "&characterEncoding=utf8";
+ connection = DriverManager.getConnection(url, username, password);
+ System.out.println("[AuBot] 数据库: "+host+":"+port+"/"+database+" (已正常连接)");
+ return connection;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.out.println("[AuBot] 数据库: "+host+":"+port+"/"+database+" (连接失败)");
+ }
+ return connection;
+ }
+
+ public static void closeConnection() {
+ try {
+ if (connection != null && !connection.isClosed()) {
+ connection.close();
+ }
+ } catch (Exception e){
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java b/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java
new file mode 100644
index 0000000..42d85eb
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java
@@ -0,0 +1,46 @@
+package com.yaohun.mcbot.event;
+
+import com.yaohun.mcbot.data.BotFriend;
+import com.yaohun.mcbot.data.BotGroup;
+import net.md_5.bungee.api.plugin.Event;
+
+public class BotFriendMessageEvent extends Event {
+
+ private long botId;
+
+ private long senderId;
+
+ private String senderName;
+
+ private String message;
+
+ private BotFriend botFriend;
+
+ public BotFriendMessageEvent(long botId, long senderId, String senderName, String message, BotFriend botFriend) {
+ this.botId = botId;
+ this.senderId = senderId;
+ this.senderName = senderName;
+ this.message = message;
+ this.botFriend = botFriend;
+ }
+
+ public long getBotId() {
+ return botId;
+ }
+
+ public long getSenderId() {
+ return senderId;
+ }
+
+ public String getSenderName() {
+ return senderName;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public BotFriend getBotFriend() {
+ return botFriend;
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java b/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java
new file mode 100644
index 0000000..278744f
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java
@@ -0,0 +1,52 @@
+package com.yaohun.mcbot.event;
+
+import com.yaohun.mcbot.data.BotGroup;
+import net.md_5.bungee.api.plugin.Event;
+
+public class BotGroupMessageEvent extends Event {
+
+ private long botId;
+
+ private long groupId;
+
+ private long senderId;
+
+ private String senderName;
+
+ private String message;
+
+ private BotGroup botGroup;
+
+ public BotGroupMessageEvent(long botId, long groupId, long senderId, String senderName, String message, BotGroup botGroup) {
+ this.botId = botId;
+ this.groupId = groupId;
+ this.senderId = senderId;
+ this.senderName = senderName;
+ this.message = message;
+ this.botGroup = botGroup;
+ }
+
+ public long getBotId() {
+ return botId;
+ }
+
+ public long getGroupId() {
+ return groupId;
+ }
+
+ public long getSenderId() {
+ return senderId;
+ }
+
+ public String getSenderName() {
+ return senderName;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public BotGroup getBotGroup() {
+ return botGroup;
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java b/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java
new file mode 100644
index 0000000..1480df9
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java
@@ -0,0 +1,84 @@
+package com.yaohun.mcbot.listsener;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.api.McBotAPI;
+import com.yaohun.mcbot.config.Config;
+import com.yaohun.mcbot.data.sql.SQLIO;
+import com.yaohun.mcbot.event.BotFriendMessageEvent;
+import com.yaohun.mcbot.event.BotGroupMessageEvent;
+import com.yaohun.mcbot.util.CDTimeAPI;
+import net.md_5.bungee.BungeeCord;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Listener;
+import net.md_5.bungee.event.EventHandler;
+
+import java.util.concurrent.TimeUnit;
+
+public class FriendListener implements Listener {
+
+ @EventHandler
+ public void onGroupChat(BotFriendMessageEvent e) {
+ long senderId = e.getSenderId();
+ if(CDTimeAPI.isCD(senderId,"groupBotCd")){
+ return;
+ }
+ CDTimeAPI.addPlayerCD(senderId,"groupBotCd",1000 * 5);
+ String message = e.getMessage();
+ if(message.contains("/功能") || message.contains("/菜单")) {
+ String to_message = "/查询 - 查看绑定信息\n" +
+ "/在线 - 查看当前在线人数\n" +
+ "/延迟 [玩家] - 网络延迟查看\n" +
+ "/强制下线 - 让账号在游戏中强制退出";
+ e.getBotFriend().sendMessage(to_message);
+ }else if(e.getMessage().equalsIgnoreCase("/在线")) {
+ int onlineplayer = BungeeCord.getInstance().getOnlineCount();
+ String msg = Config.getMessage("checkServerOnline").replace("%count%",String.valueOf(onlineplayer));
+ e.getBotFriend().sendMessage(msg);
+ }else if(message.contains("/查询")) {
+ // 获取这个发送者的QQ号
+ String userName = SQLIO.getUserName(String.valueOf(e.getSenderId()));
+ if(userName == null){
+ String msg = Config.getMessage("notBoundYet");
+ e.getBotFriend().sendMessage(msg);
+ return;
+ }
+ String msg = Config.getMessage("querySuccessful");
+ e.getBotFriend().sendMessage(msg.replace("%user%", userName));
+ }else if(e.getMessage().contains("/延迟 ")){
+ // 获取玩家名字
+ String playName = e.getMessage().replace("/延迟 ","").replace(" ","");
+ ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playName);
+ if (player == null) {
+ String msg = Config.getMessage("offlinePlayer").replace("%user%", playName);
+ e.getBotFriend().sendMessage(msg);
+ return;
+ }
+ String msg = Config.getMessage("checkPlayerPing").replace("%user%", playName);
+ msg = msg.replace("%ping%",String.valueOf(player.getPing()));
+ e.getBotFriend().sendMessage(msg);
+ }else if(e.getMessage().contains("/强制下线")){
+ // 获取这个发送者的QQ号
+ String userName = SQLIO.getUserName(String.valueOf(e.getSenderId()));
+ if(userName == null){
+ String msg = Config.getMessage("unboundAccount");
+ e.getBotFriend().sendMessage(msg);
+ return;
+ }
+ ProxiedPlayer player = ProxyServer.getInstance().getPlayer(userName);
+ if (player == null) {
+ String msg = Config.getMessage("offlinePlayerError").replace("%user%", userName);
+ e.getBotFriend().sendMessage(msg);
+ return;
+ }
+ ProxyServer.getInstance().getScheduler().schedule(McBot.inst(),
+ new Runnable() {
+ public void run() {
+ player.disconnect("§c号主已进行强制下线");
+ String msg = Config.getMessage("kickOnlinPlayer").replace("%user%",userName);
+ e.getBotFriend().sendMessage(msg);
+ }
+ }, 1L, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java b/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java
new file mode 100644
index 0000000..62ec9a8
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java
@@ -0,0 +1,105 @@
+package com.yaohun.mcbot.listsener;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.api.McBotAPI;
+import com.yaohun.mcbot.config.Config;
+import com.yaohun.mcbot.data.BotFriend;
+import com.yaohun.mcbot.data.sql.SQLIO;
+import com.yaohun.mcbot.event.BotGroupMessageEvent;
+import com.yaohun.mcbot.manage.CacheManager;
+import com.yaohun.mcbot.util.CDTimeAPI;
+import net.md_5.bungee.BungeeCord;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.api.plugin.Listener;
+import net.md_5.bungee.event.EventHandler;
+
+import java.util.concurrent.TimeUnit;
+
+public class GroupListener implements Listener {
+
+ @EventHandler
+ public void onGroupChat(BotGroupMessageEvent e) {
+ if (Config.BOT_GroupAccounts.contains(e.getGroupId())) {
+ long senderId = e.getSenderId();
+ String message = e.getMessage();
+ if (message.contains("/在线")) {
+ if (CDTimeAPI.isCD(senderId, "groupBotCd")) {
+ return;
+ }
+ CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 10);
+ int onlineplayer = BungeeCord.getInstance().getOnlineCount();
+ String msg = Config.getMessage("checkServerOnline").replace("%count%", String.valueOf(onlineplayer));
+ e.getBotGroup().sendMessage(msg);
+ } else if (message.contains("/查询")) {
+ if (CDTimeAPI.isCD(senderId, "groupBotCd")) {
+ return;
+ }
+ CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 10);
+ // 获取这个发送者的QQ号
+ String userName = SQLIO.getUserName(String.valueOf(e.getSenderId()));
+ if (userName == null) {
+ String msg = Config.getMessage("notBoundYet");
+ e.getBotGroup().sendMessage(msg);
+ return;
+ }
+ String msg = Config.getMessage("querySuccessful");
+ e.getBotGroup().sendMessage(msg.replace("%user%", userName));
+ } else if (message.contains("/绑定 ")) {
+ if (CDTimeAPI.isCD(senderId, "groupBotCd")) {
+ return;
+ }
+ CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 5);
+ String code = Config.extractBindCode(message);
+ // 检测这个验证码是否存在缓存数据
+ CacheManager.CodeData codeData = CacheManager.getCodeData(code);
+ if (codeData == null) {
+ String msg = Config.getMessage("codeError").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]");
+ e.getBotGroup().sendMessage(msg);
+ return;
+ }
+ // 判断缓存数据中的QQ和这个发送验证码的QQ是否为同一个
+ String snederID = String.valueOf(e.getSenderId());
+ if (!codeData.getTencentQQ().equals(snederID)) {
+ String msg = Config.getMessage("codeExpired").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]");
+ e.getBotGroup().sendMessage(msg);
+ return;
+ }
+ // 判断这个QQ号是否已绑定游戏账号
+ String userName = SQLIO.getUserName(snederID);
+ if (userName != null) {
+ String msg = Config.getMessage("alreadyGroupBindUserName").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]");
+ e.getBotGroup().sendMessage(msg.replace("%user%", userName));
+ return;
+ }
+ // 判断这个游戏账号是否已绑定其他QQ号
+ String playerName = codeData.getUserName();
+ String userID = SQLIO.getUserID(playerName);
+ if (userID != null) {
+ String msg = Config.getMessage("alreadyGroupBindUserID").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]");
+ e.getBotGroup().sendMessage(msg.replace("%qq%", userID));
+ return;
+ }
+ // 删除缓存数据
+ CacheManager.cleanUpTheCache(playerName, code);
+ ProxyServer.getInstance().getScheduler().schedule(McBot.inst(),
+ new Runnable() {
+ public void run() {
+ //设置数据库数据
+ SQLIO.saveGroup(playerName, snederID);
+ // 向群里发送绑定成功消息
+ String msg1 = Config.getMessage("botBindingSuccessful").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]");
+ e.getBotGroup().sendMessage(msg1.replace("%user%", playerName));
+ // 向游戏发送绑定成功消息
+ ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerName);
+ if (player != null) {
+ String msg2 = Config.getMessage("playerBindingSuccessful").replace("%qq%", String.valueOf(senderId));
+ player.sendMessage(Config.getMessage("prefix") + msg2);
+ }
+ }
+ }, 1L, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/yaohun/mcbot/manage/CacheManager.java b/src/main/java/com/yaohun/mcbot/manage/CacheManager.java
new file mode 100644
index 0000000..c7d3ad2
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/manage/CacheManager.java
@@ -0,0 +1,82 @@
+package com.yaohun.mcbot.manage;
+
+import com.yaohun.mcbot.McBot;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class CacheManager {
+
+ public static HashMap codeDataMap = new HashMap<>();
+ public static HashMap randomCodeMap = new HashMap<>();
+
+ public static void setRandomCodeData(String userName,String code){
+ randomCodeMap.put(userName,code);
+ }
+
+ public static String getRandomCodeData(String userName){
+ if(randomCodeMap.containsKey(userName)){
+ return randomCodeMap.get(userName);
+ }
+ return null;
+ }
+
+ public static void setCodeDataMap(String randomCode,CacheManager.CodeData codeData){
+ codeDataMap.put(randomCode,codeData);
+ }
+
+ public static CodeData getCodeData(String randomCode){
+ if(codeDataMap.containsKey(randomCode)){
+ return codeDataMap.get(randomCode);
+ }
+ return null;
+ }
+
+ public static void cleanUpTheCache(String playerName,String randomCode){
+ codeDataMap.remove(playerName);
+ randomCodeMap.remove(randomCode);
+ }
+
+ public static void checkingCleanUpData(){
+ if(codeDataMap.isEmpty()){
+ return;
+ }
+ codeDataMap.clear();
+ McBot.inst().getLogger().info("[McBot] 定时清理缓存池...");
+ }
+
+ public static class CodeData {
+ private String randomCode;
+ private String userName;
+ private String tencentQQ;
+ private long recordingTime;
+
+ public CodeData(String userName, String randomCode, String tencentQQ) {
+ this.userName = userName;
+ this.randomCode = randomCode;
+ this.tencentQQ = tencentQQ;
+ this.recordingTime = System.currentTimeMillis();
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public String getRandomCode() {
+ return randomCode;
+ }
+
+ public String getTencentQQ() {
+ return tencentQQ;
+ }
+
+ public boolean isRecordingTime() {
+ long nowTime = System.currentTimeMillis();
+ if(nowTime - recordingTime > (1000 * 120)){
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java b/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java
new file mode 100644
index 0000000..d73af00
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java
@@ -0,0 +1,73 @@
+package com.yaohun.mcbot.manage;
+
+import com.yaohun.mcbot.McBot;
+import com.yaohun.mcbot.client.QQWebSocketClient;
+import net.md_5.bungee.api.ProxyServer;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+public class WssReconnectManager {
+
+ private static final int MAX_RETRY = 5;
+ private static final long RETRY_DELAY_SECONDS = 15;
+
+ private static boolean reconnecting = false;
+
+ private static int retryCount = 0;
+
+ private static QQWebSocketClient currentClient;
+ private static String serverUri;
+ private static McBot plugin;
+
+ public static void initialize(McBot pluginInstance, String uri) {
+ plugin = pluginInstance;
+ serverUri = uri;
+ }
+
+ public static void setClient(QQWebSocketClient client) {
+ currentClient = client;
+ }
+
+ public static QQWebSocketClient getClient() {
+ return currentClient;
+ }
+
+ public static void tryReconnect() {
+ if (reconnecting) return;
+
+ if (currentClient != null && currentClient.isOpen()) return;
+
+ if (retryCount >= MAX_RETRY) {
+ plugin.getLogger().severe("[Bot-WebSocket] 已达到最大重连次数,停止尝试");
+ return;
+ }
+
+ reconnecting = true;
+ retryCount++;
+
+ ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
+ try {
+ // 关闭旧连接
+ if (currentClient != null && currentClient.isOpen()) {
+ plugin.getLogger().info("[Bot-WebSocket] 关闭旧连接...");
+ currentClient.close();
+ }
+
+ plugin.getLogger().info("[Bot-WebSocket] 重新连接尝试 第<" + retryCount + ">次 ...");
+ QQWebSocketClient newClient = new QQWebSocketClient(serverUri);
+ setClient(newClient);
+ newClient.connect();
+ } catch (Exception e) {
+ plugin.getLogger().severe("[Bot-WebSocket] 重连失败: " + e.getMessage());
+ e.printStackTrace();
+ } finally {
+ reconnecting = false;
+ }
+ }, RETRY_DELAY_SECONDS, TimeUnit.SECONDS);
+ }
+
+ public static void resetRetry() {
+ retryCount = 0;
+ }
+}
diff --git a/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java b/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java
new file mode 100644
index 0000000..2739caa
--- /dev/null
+++ b/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java
@@ -0,0 +1,72 @@
+package com.yaohun.mcbot.util;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.UUID;
+
+public class CDTimeAPI {
+
+ private static HashSet cdData = new HashSet<>();
+
+ public static CDData getCDData(long tencentQQ) {
+ for (CDData cDData : cdData) {
+ if (cDData.getTencentQQ() == tencentQQ) {
+ return cDData;
+ }
+ }
+ CDData data = new CDData(tencentQQ);
+ cdData.add(data);
+ return data;
+ }
+
+ public static void setPlayerCD(long tencentQQ, String key, long mills) {
+ CDData data = getCDData(tencentQQ);
+ long now = System.currentTimeMillis();
+ data.setCD(key, now + mills);
+ }
+
+ public static void addPlayerCD(long tencentQQ, String key, long mills) {
+ if(isCD(tencentQQ,key)){
+ setPlayerCD(tencentQQ,key,getCD(tencentQQ,key) + mills);
+ } else {
+ setPlayerCD(tencentQQ,key,mills);
+ }
+ }
+
+ public static long getCD(long tencentQQ, String key) {
+ CDData data = getCDData(tencentQQ);
+ long now = System.currentTimeMillis();
+ return data.getCD(key) - now;
+ }
+
+ public static boolean isCD(long tencentQQ, String key) {
+ return (getCD(tencentQQ, key) >= 0L);
+ }
+
+ public static class CDData{
+ private final long tencentQQ;
+
+ private final HashMap cdTime;
+
+ public CDData(long tencentQQ) {
+ this.tencentQQ = tencentQQ;
+ this.cdTime = new HashMap<>();
+ }
+
+ public long getTencentQQ() {
+ return tencentQQ;
+ }
+
+ public void setCD(String key, long time) {
+ this.cdTime.put(key, time);
+ }
+
+ public long getCD(String key) {
+ return this.cdTime.getOrDefault(key,-1L);
+ }
+
+ public HashMap getCdTime() {
+ return cdTime;
+ }
+ }
+}
diff --git a/src/main/java/org/java_websocket/AbstractWebSocket.java b/src/main/java/org/java_websocket/AbstractWebSocket.java
new file mode 100644
index 0000000..ae9ce18
--- /dev/null
+++ b/src/main/java/org/java_websocket/AbstractWebSocket.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.util.NamedThreadFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Base class for additional implementations for the server as well as the client
+ */
+public abstract class AbstractWebSocket extends WebSocketAdapter {
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(AbstractWebSocket.class);
+
+ /**
+ * Attribute which allows you to deactivate the Nagle's algorithm
+ *
+ * @since 1.3.3
+ */
+ private boolean tcpNoDelay;
+
+ /**
+ * Attribute which allows you to enable/disable the SO_REUSEADDR socket option.
+ *
+ * @since 1.3.5
+ */
+ private boolean reuseAddr;
+
+ /**
+ * Attribute for a service that triggers lost connection checking
+ *
+ * @since 1.4.1
+ */
+ private ScheduledExecutorService connectionLostCheckerService;
+ /**
+ * Attribute for a task that checks for lost connections
+ *
+ * @since 1.4.1
+ */
+ private ScheduledFuture> connectionLostCheckerFuture;
+
+ /**
+ * Attribute for the lost connection check interval in nanoseconds
+ *
+ * @since 1.3.4
+ */
+ private long connectionLostTimeout = TimeUnit.SECONDS.toNanos(60);
+
+ /**
+ * Attribute to keep track if the WebSocket Server/Client is running/connected
+ *
+ * @since 1.3.9
+ */
+ private boolean websocketRunning = false;
+
+ /**
+ * Attribute to start internal threads as daemon
+ *
+ * @since 1.5.6
+ */
+ private boolean daemon = false;
+
+ /**
+ * Attribute to sync on
+ */
+ private final Object syncConnectionLost = new Object();
+
+ /**
+ * TCP receive buffer size that will be used for sockets (zero means use system default)
+ *
+ * @since 1.5.7
+ */
+ private int receiveBufferSize = 0;
+
+ /**
+ * Used for internal buffer allocations when the socket buffer size is not specified.
+ */
+ protected static int DEFAULT_READ_BUFFER_SIZE = 65536;
+
+ /**
+ * Get the interval checking for lost connections Default is 60 seconds
+ *
+ * @return the interval in seconds
+ * @since 1.3.4
+ */
+ public int getConnectionLostTimeout() {
+ synchronized (syncConnectionLost) {
+ return (int) TimeUnit.NANOSECONDS.toSeconds(connectionLostTimeout);
+ }
+ }
+
+ /**
+ * Setter for the interval checking for lost connections A value lower or equal 0 results in the
+ * check to be deactivated
+ *
+ * @param connectionLostTimeout the interval in seconds
+ * @since 1.3.4
+ */
+ public void setConnectionLostTimeout(int connectionLostTimeout) {
+ synchronized (syncConnectionLost) {
+ this.connectionLostTimeout = TimeUnit.SECONDS.toNanos(connectionLostTimeout);
+ if (this.connectionLostTimeout <= 0) {
+ log.trace("Connection lost timer stopped");
+ cancelConnectionLostTimer();
+ return;
+ }
+ if (this.websocketRunning) {
+ log.trace("Connection lost timer restarted");
+ //Reset all the pings
+ try {
+ ArrayList connections = new ArrayList<>(getConnections());
+ WebSocketImpl webSocketImpl;
+ for (WebSocket conn : connections) {
+ if (conn instanceof WebSocketImpl) {
+ webSocketImpl = (WebSocketImpl) conn;
+ webSocketImpl.updateLastPong();
+ }
+ }
+ } catch (Exception e) {
+ log.error("Exception during connection lost restart", e);
+ }
+ restartConnectionLostTimer();
+ }
+ }
+ }
+
+ /**
+ * Stop the connection lost timer
+ *
+ * @since 1.3.4
+ */
+ protected void stopConnectionLostTimer() {
+ synchronized (syncConnectionLost) {
+ if (connectionLostCheckerService != null || connectionLostCheckerFuture != null) {
+ this.websocketRunning = false;
+ log.trace("Connection lost timer stopped");
+ cancelConnectionLostTimer();
+ }
+ }
+ }
+
+ /**
+ * Start the connection lost timer
+ *
+ * @since 1.3.4
+ */
+ protected void startConnectionLostTimer() {
+ synchronized (syncConnectionLost) {
+ if (this.connectionLostTimeout <= 0) {
+ log.trace("Connection lost timer deactivated");
+ return;
+ }
+ log.trace("Connection lost timer started");
+ this.websocketRunning = true;
+ restartConnectionLostTimer();
+ }
+ }
+
+ /**
+ * This methods allows the reset of the connection lost timer in case of a changed parameter
+ *
+ * @since 1.3.4
+ */
+ private void restartConnectionLostTimer() {
+ cancelConnectionLostTimer();
+ connectionLostCheckerService = Executors
+ .newSingleThreadScheduledExecutor(new NamedThreadFactory("WebSocketConnectionLostChecker", daemon));
+ Runnable connectionLostChecker = new Runnable() {
+
+ /**
+ * Keep the connections in a separate list to not cause deadlocks
+ */
+ private ArrayList connections = new ArrayList<>();
+
+ @Override
+ public void run() {
+ connections.clear();
+ try {
+ connections.addAll(getConnections());
+ long minimumPongTime;
+ synchronized (syncConnectionLost) {
+ minimumPongTime = (long) (System.nanoTime() - (connectionLostTimeout * 1.5));
+ }
+ for (WebSocket conn : connections) {
+ executeConnectionLostDetection(conn, minimumPongTime);
+ }
+ } catch (Exception e) {
+ //Ignore this exception
+ }
+ connections.clear();
+ }
+ };
+
+ connectionLostCheckerFuture = connectionLostCheckerService
+ .scheduleAtFixedRate(connectionLostChecker, connectionLostTimeout, connectionLostTimeout,
+ TimeUnit.NANOSECONDS);
+ }
+
+ /**
+ * Send a ping to the endpoint or close the connection since the other endpoint did not respond
+ * with a ping
+ *
+ * @param webSocket the websocket instance
+ * @param minimumPongTime the lowest/oldest allowable last pong time (in nanoTime) before we
+ * consider the connection to be lost
+ */
+ private void executeConnectionLostDetection(WebSocket webSocket, long minimumPongTime) {
+ if (!(webSocket instanceof WebSocketImpl)) {
+ return;
+ }
+ WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket;
+ if (webSocketImpl.getLastPong() < minimumPongTime) {
+ log.trace("Closing connection due to no pong received: {}", webSocketImpl);
+ webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE,
+ "The connection was closed because the other endpoint did not respond with a pong in time. For more information check: https://github.com/TooTallNate/Java-WebSocket/wiki/Lost-connection-detection");
+ } else {
+ if (webSocketImpl.isOpen()) {
+ webSocketImpl.sendPing();
+ } else {
+ log.trace("Trying to ping a non open connection: {}", webSocketImpl);
+ }
+ }
+ }
+
+ /**
+ * Getter to get all the currently available connections
+ *
+ * @return the currently available connections
+ * @since 1.3.4
+ */
+ protected abstract Collection getConnections();
+
+ /**
+ * Cancel any running timer for the connection lost detection
+ *
+ * @since 1.3.4
+ */
+ private void cancelConnectionLostTimer() {
+ if (connectionLostCheckerService != null) {
+ connectionLostCheckerService.shutdownNow();
+ connectionLostCheckerService = null;
+ }
+ if (connectionLostCheckerFuture != null) {
+ connectionLostCheckerFuture.cancel(false);
+ connectionLostCheckerFuture = null;
+ }
+ }
+
+ /**
+ * Tests if TCP_NODELAY is enabled.
+ *
+ * @return a boolean indicating whether or not TCP_NODELAY is enabled for new connections.
+ * @since 1.3.3
+ */
+ public boolean isTcpNoDelay() {
+ return tcpNoDelay;
+ }
+
+ /**
+ * Setter for tcpNoDelay
+ *
+ * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) for new connections
+ *
+ * @param tcpNoDelay true to enable TCP_NODELAY, false to disable.
+ * @since 1.3.3
+ */
+ public void setTcpNoDelay(boolean tcpNoDelay) {
+ this.tcpNoDelay = tcpNoDelay;
+ }
+
+ /**
+ * Tests Tests if SO_REUSEADDR is enabled.
+ *
+ * @return a boolean indicating whether or not SO_REUSEADDR is enabled.
+ * @since 1.3.5
+ */
+ public boolean isReuseAddr() {
+ return reuseAddr;
+ }
+
+ /**
+ * Setter for soReuseAddr
+ *
+ * Enable/disable SO_REUSEADDR for the socket
+ *
+ * @param reuseAddr whether to enable or disable SO_REUSEADDR
+ * @since 1.3.5
+ */
+ public void setReuseAddr(boolean reuseAddr) {
+ this.reuseAddr = reuseAddr;
+ }
+
+
+ /**
+ * Getter for daemon
+ *
+ * @return whether internal threads are spawned in daemon mode
+ * @since 1.5.6
+ */
+ public boolean isDaemon() {
+ return daemon;
+ }
+
+ /**
+ * Setter for daemon
+ *
+ * Controls whether or not internal threads are spawned in daemon mode
+ *
+ * @since 1.5.6
+ */
+ public void setDaemon(boolean daemon) {
+ this.daemon = daemon;
+ }
+
+ /**
+ * Returns the TCP receive buffer size that will be used for sockets (or zero, if not explicitly set).
+ * @see java.net.Socket#setReceiveBufferSize(int)
+ *
+ * @since 1.5.7
+ */
+ public int getReceiveBufferSize() {
+ return receiveBufferSize;
+ }
+
+ /**
+ * Sets the TCP receive buffer size that will be used for sockets.
+ * If this is not explicitly set (or set to zero), the system default is used.
+ * @see java.net.Socket#setReceiveBufferSize(int)
+ *
+ * @since 1.5.7
+ */
+ public void setReceiveBufferSize(int receiveBufferSize) {
+ if (receiveBufferSize < 0) {
+ throw new IllegalArgumentException("buffer size < 0");
+ }
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java
new file mode 100644
index 0000000..33e6ddc
--- /dev/null
+++ b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SocketChannel;
+
+/**
+ * @deprecated
+ */
+@Deprecated
+public class AbstractWrappedByteChannel implements WrappedByteChannel {
+
+ private final ByteChannel channel;
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ public AbstractWrappedByteChannel(ByteChannel towrap) {
+ this.channel = towrap;
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ public AbstractWrappedByteChannel(WrappedByteChannel towrap) {
+ this.channel = towrap;
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException {
+ return channel.read(dst);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ return channel.write(src);
+ }
+
+ @Override
+ public boolean isNeedWrite() {
+ return channel instanceof WrappedByteChannel && ((WrappedByteChannel) channel).isNeedWrite();
+ }
+
+ @Override
+ public void writeMore() throws IOException {
+ if (channel instanceof WrappedByteChannel) {
+ ((WrappedByteChannel) channel).writeMore();
+ }
+
+ }
+
+ @Override
+ public boolean isNeedRead() {
+ return channel instanceof WrappedByteChannel && ((WrappedByteChannel) channel).isNeedRead();
+
+ }
+
+ @Override
+ public int readMore(ByteBuffer dst) throws IOException {
+ return channel instanceof WrappedByteChannel ? ((WrappedByteChannel) channel).readMore(dst) : 0;
+ }
+
+ @Override
+ public boolean isBlocking() {
+ if (channel instanceof SocketChannel) {
+ return ((SocketChannel) channel).isBlocking();
+ } else if (channel instanceof WrappedByteChannel) {
+ return ((WrappedByteChannel) channel).isBlocking();
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/SSLSocketChannel.java b/src/main/java/org/java_websocket/SSLSocketChannel.java
new file mode 100644
index 0000000..b402450
--- /dev/null
+++ b/src/main/java/org/java_websocket/SSLSocketChannel.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ExecutorService;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import org.java_websocket.interfaces.ISSLChannel;
+import org.java_websocket.util.ByteBufferUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * A class that represents an SSL/TLS peer, and can be extended to create a client or a server.
+ *
+ * It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which is
+ * described by Oracle as "an advanced API, not appropriate for casual use", since it requires the
+ * user to implement much of the communication establishment procedure himself. More information
+ * about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine
+ *
+ * {@link SSLSocketChannel} implements the handshake protocol, required to establish a connection
+ * between two peers, which is common for both client and server and provides the abstract {@link
+ * SSLSocketChannel#read(ByteBuffer)} and {@link SSLSocketChannel#write(ByteBuffer)} (String)}
+ * methods, that need to be implemented by the specific SSL/TLS peer that is going to extend this
+ * class.
+ *
+ * @author Alex Karnezis
+ *
+ * Modified by marci4 to allow the usage as a ByteChannel
+ *
+ * Permission for usage received at May 25, 2017 by Alex Karnezis
+ */
+public class SSLSocketChannel implements WrappedByteChannel, ByteChannel, ISSLChannel {
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(SSLSocketChannel.class);
+
+ /**
+ * The underlying socket channel
+ */
+ private final SocketChannel socketChannel;
+
+ /**
+ * The engine which will be used for un-/wrapping of buffers
+ */
+ private final SSLEngine engine;
+
+
+ /**
+ * Will contain this peer's application data in plaintext, that will be later encrypted using
+ * {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can
+ * typically be of any size, as long as it is large enough to contain this peer's outgoing
+ * messages. If this peer tries to send a message bigger than buffer's capacity a {@link
+ * BufferOverflowException} will be thrown.
+ */
+ private ByteBuffer myAppData;
+
+ /**
+ * Will contain this peer's encrypted data, that will be generated after {@link
+ * SSLEngine#wrap(ByteBuffer, ByteBuffer)} is applied on {@link SSLSocketChannel#myAppData}. It
+ * should be initialized using {@link SSLSession#getPacketBufferSize()}, which returns the size up
+ * to which, SSL/TLS packets will be generated from the engine under a session. All SSLEngine
+ * network buffers should be sized at least this large to avoid insufficient space problems when
+ * performing wrap and unwrap calls.
+ */
+ private ByteBuffer myNetData;
+
+ /**
+ * Will contain the other peer's (decrypted) application data. It must be large enough to hold the
+ * application data from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()}
+ * for an estimation of the other peer's application data and should be enlarged if this size is
+ * not enough.
+ */
+ private ByteBuffer peerAppData;
+
+ /**
+ * Will contain the other peer's encrypted data. The SSL/TLS protocols specify that
+ * implementations should produce packets containing at most 16 KB of plaintext, so a buffer sized
+ * to this value should normally cause no capacity problems. However, some implementations violate
+ * the specification and generate large records up to 32 KB. If the {@link
+ * SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes
+ * returned by SSLSession will be updated dynamically, so the this peer should check for overflow
+ * conditions and enlarge the buffer using the session's (updated) buffer size.
+ */
+ private ByteBuffer peerNetData;
+
+ /**
+ * Will be used to execute tasks that may emerge during handshake in parallel with the server's
+ * main thread.
+ */
+ private ExecutorService executor;
+
+
+ public SSLSocketChannel(SocketChannel inputSocketChannel, SSLEngine inputEngine,
+ ExecutorService inputExecutor, SelectionKey key) throws IOException {
+ if (inputSocketChannel == null || inputEngine == null || executor == inputExecutor) {
+ throw new IllegalArgumentException("parameter must not be null");
+ }
+
+ this.socketChannel = inputSocketChannel;
+ this.engine = inputEngine;
+ this.executor = inputExecutor;
+ myNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+ peerNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+ this.engine.beginHandshake();
+ if (doHandshake()) {
+ if (key != null) {
+ key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
+ }
+ } else {
+ try {
+ socketChannel.close();
+ } catch (IOException e) {
+ log.error("Exception during the closing of the channel", e);
+ }
+ }
+ }
+
+ @Override
+ public synchronized int read(ByteBuffer dst) throws IOException {
+ if (!dst.hasRemaining()) {
+ return 0;
+ }
+ if (peerAppData.hasRemaining()) {
+ peerAppData.flip();
+ return ByteBufferUtils.transferByteBuffer(peerAppData, dst);
+ }
+ peerNetData.compact();
+
+ int bytesRead = socketChannel.read(peerNetData);
+ /*
+ * If bytesRead are 0 put we still have some data in peerNetData still to an unwrap (for testcase 1.1.6)
+ */
+ if (bytesRead > 0 || peerNetData.hasRemaining()) {
+ peerNetData.flip();
+ while (peerNetData.hasRemaining()) {
+ peerAppData.compact();
+ SSLEngineResult result;
+ try {
+ result = engine.unwrap(peerNetData, peerAppData);
+ } catch (SSLException e) {
+ log.error("SSLException during unwrap", e);
+ throw e;
+ }
+ switch (result.getStatus()) {
+ case OK:
+ peerAppData.flip();
+ return ByteBufferUtils.transferByteBuffer(peerAppData, dst);
+ case BUFFER_UNDERFLOW:
+ peerAppData.flip();
+ return ByteBufferUtils.transferByteBuffer(peerAppData, dst);
+ case BUFFER_OVERFLOW:
+ peerAppData = enlargeApplicationBuffer(peerAppData);
+ return read(dst);
+ case CLOSED:
+ closeConnection();
+ dst.clear();
+ return -1;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ }
+ } else if (bytesRead < 0) {
+ handleEndOfStream();
+ }
+ ByteBufferUtils.transferByteBuffer(peerAppData, dst);
+ return bytesRead;
+ }
+
+ @Override
+ public synchronized int write(ByteBuffer output) throws IOException {
+ int num = 0;
+ while (output.hasRemaining()) {
+ // The loop has a meaning for (outgoing) messages larger than 16KB.
+ // Every wrap call will remove 16KB from the original message and send it to the remote peer.
+ myNetData.clear();
+ SSLEngineResult result = engine.wrap(output, myNetData);
+ switch (result.getStatus()) {
+ case OK:
+ myNetData.flip();
+ while (myNetData.hasRemaining()) {
+ num += socketChannel.write(myNetData);
+ }
+ break;
+ case BUFFER_OVERFLOW:
+ myNetData = enlargePacketBuffer(myNetData);
+ break;
+ case BUFFER_UNDERFLOW:
+ throw new SSLException(
+ "Buffer underflow occurred after a wrap. I don't think we should ever get here.");
+ case CLOSED:
+ closeConnection();
+ return 0;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ }
+ return num;
+ }
+
+ /**
+ * Implements the handshake protocol between two peers, required for the establishment of the
+ * SSL/TLS connection. During the handshake, encryption configuration information - such as the
+ * list of available cipher suites - will be exchanged and if the handshake is successful will
+ * lead to an established SSL/TLS session.
+ *
+ *
+ * A typical handshake will usually contain the following steps:
+ *
+ *
+ *
1. wrap: ClientHello
+ *
2. unwrap: ServerHello/Cert/ServerHelloDone
+ *
3. wrap: ClientKeyExchange
+ *
4. wrap: ChangeCipherSpec
+ *
5. wrap: Finished
+ *
6. unwrap: ChangeCipherSpec
+ *
7. unwrap: Finished
+ *
+ *
+ * Handshake is also used during the end of the session, in order to properly close the connection between the two peers.
+ * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for
+ * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message
+ * from his peer and then enter the handshake procedure to send his own CLOSE message as well.
+ *
+ * @return True if the connection handshake was successful or false if an error occurred.
+ * @throws IOException - if an error occurs during read/write to the socket channel.
+ */
+ private boolean doHandshake() throws IOException {
+ SSLEngineResult result;
+ HandshakeStatus handshakeStatus;
+
+ // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer
+ // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less
+ // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers
+ // to be used for the handshake, while keeping client's buffers at the same size.
+ int appBufferSize = engine.getSession().getApplicationBufferSize();
+ myAppData = ByteBuffer.allocate(appBufferSize);
+ peerAppData = ByteBuffer.allocate(appBufferSize);
+ myNetData.clear();
+ peerNetData.clear();
+
+ handshakeStatus = engine.getHandshakeStatus();
+ boolean handshakeComplete = false;
+ while (!handshakeComplete) {
+ switch (handshakeStatus) {
+ case FINISHED:
+ handshakeComplete = !this.peerNetData.hasRemaining();
+ if (handshakeComplete) {
+ return true;
+ }
+ socketChannel.write(this.peerNetData);
+ break;
+ case NEED_UNWRAP:
+ if (socketChannel.read(peerNetData) < 0) {
+ if (engine.isInboundDone() && engine.isOutboundDone()) {
+ return false;
+ }
+ try {
+ engine.closeInbound();
+ } catch (SSLException e) {
+ //Ignore, can't do anything against this exception
+ }
+ engine.closeOutbound();
+ // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client.
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ peerNetData.flip();
+ try {
+ result = engine.unwrap(peerNetData, peerAppData);
+ peerNetData.compact();
+ handshakeStatus = result.getHandshakeStatus();
+ } catch (SSLException sslException) {
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ switch (result.getStatus()) {
+ case OK:
+ break;
+ case BUFFER_OVERFLOW:
+ // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap.
+ peerAppData = enlargeApplicationBuffer(peerAppData);
+ break;
+ case BUFFER_UNDERFLOW:
+ // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data.
+ peerNetData = handleBufferUnderflow(peerNetData);
+ break;
+ case CLOSED:
+ if (engine.isOutboundDone()) {
+ return false;
+ } else {
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ break;
+ case NEED_WRAP:
+ myNetData.clear();
+ try {
+ result = engine.wrap(myAppData, myNetData);
+ handshakeStatus = result.getHandshakeStatus();
+ } catch (SSLException sslException) {
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ switch (result.getStatus()) {
+ case OK:
+ myNetData.flip();
+ while (myNetData.hasRemaining()) {
+ socketChannel.write(myNetData);
+ }
+ break;
+ case BUFFER_OVERFLOW:
+ // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap.
+ // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed
+ // to produce messages smaller or equal to that, but a general handling would be the following:
+ myNetData = enlargePacketBuffer(myNetData);
+ break;
+ case BUFFER_UNDERFLOW:
+ throw new SSLException(
+ "Buffer underflow occurred after a wrap. I don't think we should ever get here.");
+ case CLOSED:
+ try {
+ myNetData.flip();
+ while (myNetData.hasRemaining()) {
+ socketChannel.write(myNetData);
+ }
+ // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read.
+ peerNetData.clear();
+ } catch (Exception e) {
+ handshakeStatus = engine.getHandshakeStatus();
+ }
+ break;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ break;
+ case NEED_TASK:
+ Runnable task;
+ while ((task = engine.getDelegatedTask()) != null) {
+ executor.execute(task);
+ }
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+
+ case NOT_HANDSHAKING:
+ break;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + handshakeStatus);
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Enlarging a packet buffer (peerNetData or myNetData)
+ *
+ * @param buffer the buffer to enlarge
+ * @return the enlarged buffer
+ */
+ private ByteBuffer enlargePacketBuffer(ByteBuffer buffer) {
+ return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize());
+ }
+
+ /**
+ * Enlarging a packet buffer (peerAppData or myAppData)
+ *
+ * @param buffer the buffer to enlarge
+ * @return the enlarged buffer
+ */
+ private ByteBuffer enlargeApplicationBuffer(ByteBuffer buffer) {
+ return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize());
+ }
+
+ /**
+ * Compares sessionProposedCapacity with buffer's capacity. If buffer's capacity is
+ * smaller, returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer
+ * with capacity twice the size of the initial one.
+ *
+ * @param buffer - the buffer to be enlarged.
+ * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link
+ * SSLSession}.
+ * @return A new buffer with a larger capacity.
+ */
+ private ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) {
+ if (sessionProposedCapacity > buffer.capacity()) {
+ buffer = ByteBuffer.allocate(sessionProposedCapacity);
+ } else {
+ buffer = ByteBuffer.allocate(buffer.capacity() * 2);
+ }
+ return buffer;
+ }
+
+ /**
+ * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already
+ * filled, and if there is no space problem will return the same buffer, so the client tries to
+ * read again. If the buffer is already filled will try to enlarge the buffer either to session's
+ * proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so
+ * the buffer will always be a peerNetData buffer.
+ *
+ * @param buffer - will always be peerNetData buffer.
+ * @return The same buffer if there is no space problem or a new buffer with the same data but
+ * more space.
+ */
+ private ByteBuffer handleBufferUnderflow(ByteBuffer buffer) {
+ if (engine.getSession().getPacketBufferSize() < buffer.limit()) {
+ return buffer;
+ } else {
+ ByteBuffer replaceBuffer = enlargePacketBuffer(buffer);
+ buffer.flip();
+ replaceBuffer.put(buffer);
+ return replaceBuffer;
+ }
+ }
+
+ /**
+ * This method should be called when this peer wants to explicitly close the connection or when a
+ * close message has arrived from the other peer, in order to provide an orderly shutdown.
+ *
+ * It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close
+ * message and sets {@link SSLEngine} to the NEED_WRAP state. Then, it delegates the
+ * exchange of close messages to the handshake method and finally, it closes socket channel.
+ *
+ * @throws IOException if an I/O error occurs to the socket channel.
+ */
+ private void closeConnection() throws IOException {
+ engine.closeOutbound();
+ try {
+ doHandshake();
+ } catch (IOException e) {
+ //Just ignore this exception since we are closing the connection already
+ }
+ socketChannel.close();
+ }
+
+ /**
+ * In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link
+ * (socket channel) is severed before close messages are exchanged. This may happen by getting an
+ * -1 or {@link IOException} when trying to read from the socket channel, or an {@link
+ * IOException} when trying to write to it. In both cases {@link SSLEngine#closeInbound()} should
+ * be called and then try to follow the standard procedure.
+ *
+ * @throws IOException if an I/O error occurs to the socket channel.
+ */
+ private void handleEndOfStream() throws IOException {
+ try {
+ engine.closeInbound();
+ } catch (Exception e) {
+ log.error(
+ "This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream.");
+ }
+ closeConnection();
+ }
+
+ @Override
+ public boolean isNeedWrite() {
+ return false;
+ }
+
+ @Override
+ public void writeMore() throws IOException {
+ //Nothing to do since we write out all the data in a while loop
+ }
+
+ @Override
+ public boolean isNeedRead() {
+ return peerNetData.hasRemaining() || peerAppData.hasRemaining();
+ }
+
+ @Override
+ public int readMore(ByteBuffer dst) throws IOException {
+ return read(dst);
+ }
+
+ @Override
+ public boolean isBlocking() {
+ return socketChannel.isBlocking();
+ }
+
+
+ @Override
+ public boolean isOpen() {
+ return socketChannel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ closeConnection();
+ }
+
+ @Override
+ public SSLEngine getSSLEngine() {
+ return engine;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/SSLSocketChannel2.java b/src/main/java/org/java_websocket/SSLSocketChannel2.java
new file mode 100644
index 0000000..3b9c778
--- /dev/null
+++ b/src/main/java/org/java_websocket/SSLSocketChannel2.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import org.java_websocket.interfaces.ISSLChannel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements the relevant portions of the SocketChannel interface with the SSLEngine wrapper.
+ */
+public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel, ISSLChannel {
+
+ /**
+ * This object is used to feed the {@link SSLEngine}'s wrap and unwrap methods during the
+ * handshake phase.
+ **/
+ protected static ByteBuffer emptybuffer = ByteBuffer.allocate(0);
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(SSLSocketChannel2.class);
+
+ protected ExecutorService exec;
+
+ protected List> tasks;
+
+ /**
+ * raw payload incoming
+ */
+ protected ByteBuffer inData;
+ /**
+ * encrypted data outgoing
+ */
+ protected ByteBuffer outCrypt;
+ /**
+ * encrypted data incoming
+ */
+ protected ByteBuffer inCrypt;
+
+ /**
+ * the underlying channel
+ */
+ protected SocketChannel socketChannel;
+ /**
+ * used to set interestOP SelectionKey.OP_WRITE for the underlying channel
+ */
+ protected SelectionKey selectionKey;
+
+ protected SSLEngine sslEngine;
+ protected SSLEngineResult readEngineResult;
+ protected SSLEngineResult writeEngineResult;
+
+ /**
+ * Should be used to count the buffer allocations. But because of #190 where
+ * HandshakeStatus.FINISHED is not properly returned by nio wrap/unwrap this variable is used to
+ * check whether {@link #createBuffers(SSLSession)} needs to be called.
+ **/
+ protected int bufferallocations = 0;
+
+ public SSLSocketChannel2(SocketChannel channel, SSLEngine sslEngine, ExecutorService exec,
+ SelectionKey key) throws IOException {
+ if (channel == null || sslEngine == null || exec == null) {
+ throw new IllegalArgumentException("parameter must not be null");
+ }
+
+ this.socketChannel = channel;
+ this.sslEngine = sslEngine;
+ this.exec = exec;
+
+ readEngineResult = writeEngineResult = new SSLEngineResult(Status.BUFFER_UNDERFLOW,
+ sslEngine.getHandshakeStatus(), 0, 0); // init to prevent NPEs
+
+ tasks = new ArrayList>(3);
+ if (key != null) {
+ key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
+ this.selectionKey = key;
+ }
+ createBuffers(sslEngine.getSession());
+ // kick off handshake
+ socketChannel.write(wrap(emptybuffer));// initializes res
+ processHandshake(false);
+ }
+
+ private void consumeFutureUninterruptible(Future> f) {
+ try {
+ while (true) {
+ try {
+ f.get();
+ break;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * This method will do whatever necessary to process the sslEngine handshake. Thats why it's
+ * called both from the {@link #read(ByteBuffer)} and {@link #write(ByteBuffer)}
+ **/
+ private synchronized void processHandshake(boolean isReading) throws IOException {
+ if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
+ return; // since this may be called either from a reading or a writing thread and because this method is synchronized it is necessary to double check if we are still handshaking.
+ }
+ if (!tasks.isEmpty()) {
+ Iterator> it = tasks.iterator();
+ while (it.hasNext()) {
+ Future> f = it.next();
+ if (f.isDone()) {
+ it.remove();
+ } else {
+ if (isBlocking()) {
+ consumeFutureUninterruptible(f);
+ }
+ return;
+ }
+ }
+ }
+
+ if (isReading && sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
+ if (!isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) {
+ inCrypt.compact();
+ int read = socketChannel.read(inCrypt);
+ if (read == -1) {
+ throw new IOException("connection closed unexpectedly by peer");
+ }
+ inCrypt.flip();
+ }
+ inData.compact();
+ unwrap();
+ if (readEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) {
+ createBuffers(sslEngine.getSession());
+ return;
+ }
+ }
+ consumeDelegatedTasks();
+ if (tasks.isEmpty()
+ || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
+ socketChannel.write(wrap(emptybuffer));
+ if (writeEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) {
+ createBuffers(sslEngine.getSession());
+ return;
+ }
+ }
+ assert (sslEngine.getHandshakeStatus()
+ != HandshakeStatus.NOT_HANDSHAKING);// this function could only leave NOT_HANDSHAKING after createBuffers was called unless #190 occurs which means that nio wrap/unwrap never return HandshakeStatus.FINISHED
+
+ bufferallocations = 1; // look at variable declaration why this line exists and #190. Without this line buffers would not be be recreated when #190 AND a rehandshake occur.
+ }
+
+ private synchronized ByteBuffer wrap(ByteBuffer b) throws SSLException {
+ outCrypt.compact();
+ writeEngineResult = sslEngine.wrap(b, outCrypt);
+ outCrypt.flip();
+ return outCrypt;
+ }
+
+ /**
+ * performs the unwrap operation by unwrapping from {@link #inCrypt} to {@link #inData}
+ **/
+ private synchronized ByteBuffer unwrap() throws SSLException {
+ int rem;
+ //There are some ssl test suites, which get around the selector.select() call, which cause an infinite unwrap and 100% cpu usage (see #459 and #458)
+ if (readEngineResult.getStatus() == Status.CLOSED
+ && sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
+ try {
+ close();
+ } catch (IOException e) {
+ //Not really interesting
+ }
+ }
+ do {
+ rem = inData.remaining();
+ readEngineResult = sslEngine.unwrap(inCrypt, inData);
+ } while (readEngineResult.getStatus() == Status.OK && (rem != inData.remaining()
+ || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP));
+ inData.flip();
+ return inData;
+ }
+
+ protected void consumeDelegatedTasks() {
+ Runnable task;
+ while ((task = sslEngine.getDelegatedTask()) != null) {
+ tasks.add(exec.submit(task));
+ // task.run();
+ }
+ }
+
+ protected void createBuffers(SSLSession session) {
+ saveCryptedData(); // save any remaining data in inCrypt
+ int netBufferMax = session.getPacketBufferSize();
+ int appBufferMax = Math.max(session.getApplicationBufferSize(), netBufferMax);
+
+ if (inData == null) {
+ inData = ByteBuffer.allocate(appBufferMax);
+ outCrypt = ByteBuffer.allocate(netBufferMax);
+ inCrypt = ByteBuffer.allocate(netBufferMax);
+ } else {
+ if (inData.capacity() != appBufferMax) {
+ inData = ByteBuffer.allocate(appBufferMax);
+ }
+ if (outCrypt.capacity() != netBufferMax) {
+ outCrypt = ByteBuffer.allocate(netBufferMax);
+ }
+ if (inCrypt.capacity() != netBufferMax) {
+ inCrypt = ByteBuffer.allocate(netBufferMax);
+ }
+ }
+ if (inData.remaining() != 0 && log.isTraceEnabled()) {
+ log.trace(new String(inData.array(), inData.position(), inData.remaining()));
+ }
+ inData.rewind();
+ inData.flip();
+ if (inCrypt.remaining() != 0 && log.isTraceEnabled()) {
+ log.trace(new String(inCrypt.array(), inCrypt.position(), inCrypt.remaining()));
+ }
+ inCrypt.rewind();
+ inCrypt.flip();
+ outCrypt.rewind();
+ outCrypt.flip();
+ bufferallocations++;
+ }
+
+ public int write(ByteBuffer src) throws IOException {
+ if (!isHandShakeComplete()) {
+ processHandshake(false);
+ return 0;
+ }
+ // assert(bufferallocations > 1); // see #190
+ // if(bufferallocations <= 1) {
+ // createBuffers(sslEngine.getSession());
+ // }
+ int num = socketChannel.write(wrap(src));
+ if (writeEngineResult.getStatus() == Status.CLOSED) {
+ throw new EOFException("Connection is closed");
+ }
+ return num;
+
+ }
+
+ /**
+ * Blocks when in blocking mode until at least one byte has been decoded. When not in blocking
+ * mode 0 may be returned.
+ *
+ * @return the number of bytes read.
+ **/
+ public int read(ByteBuffer dst) throws IOException {
+ tryRestoreCryptedData();
+ while (true) {
+ if (!dst.hasRemaining()) {
+ return 0;
+ }
+ if (!isHandShakeComplete()) {
+ if (isBlocking()) {
+ while (!isHandShakeComplete()) {
+ processHandshake(true);
+ }
+ } else {
+ processHandshake(true);
+ if (!isHandShakeComplete()) {
+ return 0;
+ }
+ }
+ }
+ // assert(bufferallocations > 1); // see #190
+ // if (bufferallocations <= 1) {
+ // createBuffers(sslEngine.getSession());
+ // }
+
+ /* 1. When "dst" is smaller than "inData" readRemaining will fill "dst" with data decoded in a previous read call.
+ * 2. When "inCrypt" contains more data than "inData" has remaining space, unwrap has to be called on more time(readRemaining)
+ */
+ int purged = readRemaining(dst);
+ if (purged != 0) {
+ return purged;
+ }
+
+ /* We only continue when we really need more data from the network.
+ * Thats the case if inData is empty or inCrypt holds to less data than necessary for decryption
+ */
+ assert (inData.position() == 0);
+ inData.clear();
+
+ if (!inCrypt.hasRemaining()) {
+ inCrypt.clear();
+ } else {
+ inCrypt.compact();
+ }
+
+ if (isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) {
+ if (socketChannel.read(inCrypt) == -1) {
+ return -1;
+ }
+ }
+ inCrypt.flip();
+ unwrap();
+
+ int transferred = transfereTo(inData, dst);
+ if (transferred == 0 && isBlocking()) {
+ continue;
+ }
+ return transferred;
+ }
+ }
+
+ /**
+ * {@link #read(ByteBuffer)} may not be to leave all buffers(inData, inCrypt)
+ **/
+ private int readRemaining(ByteBuffer dst) throws SSLException {
+ if (inData.hasRemaining()) {
+ return transfereTo(inData, dst);
+ }
+ if (!inData.hasRemaining()) {
+ inData.clear();
+ }
+ tryRestoreCryptedData();
+ // test if some bytes left from last read (e.g. BUFFER_UNDERFLOW)
+ if (inCrypt.hasRemaining()) {
+ unwrap();
+ int amount = transfereTo(inData, dst);
+ if (readEngineResult.getStatus() == Status.CLOSED) {
+ return -1;
+ }
+ if (amount > 0) {
+ return amount;
+ }
+ }
+ return 0;
+ }
+
+ public boolean isConnected() {
+ return socketChannel.isConnected();
+ }
+
+ public void close() throws IOException {
+ sslEngine.closeOutbound();
+ sslEngine.getSession().invalidate();
+ try {
+ if (socketChannel.isOpen()) {
+ socketChannel.write(wrap(emptybuffer));
+ }
+ } finally { // in case socketChannel.write produce exception - channel will never close
+ socketChannel.close();
+ }
+ }
+
+ private boolean isHandShakeComplete() {
+ HandshakeStatus status = sslEngine.getHandshakeStatus();
+ return status == HandshakeStatus.FINISHED
+ || status == HandshakeStatus.NOT_HANDSHAKING;
+ }
+
+ public SelectableChannel configureBlocking(boolean b) throws IOException {
+ return socketChannel.configureBlocking(b);
+ }
+
+ public boolean connect(SocketAddress remote) throws IOException {
+ return socketChannel.connect(remote);
+ }
+
+ public boolean finishConnect() throws IOException {
+ return socketChannel.finishConnect();
+ }
+
+ public Socket socket() {
+ return socketChannel.socket();
+ }
+
+ public boolean isInboundDone() {
+ return sslEngine.isInboundDone();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return socketChannel.isOpen();
+ }
+
+ @Override
+ public boolean isNeedWrite() {
+ return outCrypt.hasRemaining()
+ || !isHandShakeComplete(); // FIXME this condition can cause high cpu load during handshaking when network is slow
+ }
+
+ @Override
+ public void writeMore() throws IOException {
+ write(outCrypt);
+ }
+
+ @Override
+ public boolean isNeedRead() {
+ return saveCryptData != null || inData.hasRemaining() || (inCrypt.hasRemaining()
+ && readEngineResult.getStatus() != Status.BUFFER_UNDERFLOW
+ && readEngineResult.getStatus() != Status.CLOSED);
+ }
+
+ @Override
+ public int readMore(ByteBuffer dst) throws SSLException {
+ return readRemaining(dst);
+ }
+
+ private int transfereTo(ByteBuffer from, ByteBuffer to) {
+ int fremain = from.remaining();
+ int toremain = to.remaining();
+ if (fremain > toremain) {
+ // FIXME there should be a more efficient transfer method
+ int limit = Math.min(fremain, toremain);
+ for (int i = 0; i < limit; i++) {
+ to.put(from.get());
+ }
+ return limit;
+ } else {
+ to.put(from);
+ return fremain;
+ }
+
+ }
+
+ @Override
+ public boolean isBlocking() {
+ return socketChannel.isBlocking();
+ }
+
+ @Override
+ public SSLEngine getSSLEngine() {
+ return sslEngine;
+ }
+
+
+ // to avoid complexities with inCrypt, extra unwrapped data after SSL handshake will be saved off in a byte array
+ // and the inserted back on first read
+ private byte[] saveCryptData = null;
+
+ private void saveCryptedData() {
+ // did we find any extra data?
+ if (inCrypt != null && inCrypt.remaining() > 0) {
+ int saveCryptSize = inCrypt.remaining();
+ saveCryptData = new byte[saveCryptSize];
+ inCrypt.get(saveCryptData);
+ }
+ }
+
+ private void tryRestoreCryptedData() {
+ // was there any extra data, then put into inCrypt and clean up
+ if (saveCryptData != null) {
+ inCrypt.clear();
+ inCrypt.put(saveCryptData);
+ inCrypt.flip();
+ saveCryptData = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/SocketChannelIOHelper.java b/src/main/java/org/java_websocket/SocketChannelIOHelper.java
new file mode 100644
index 0000000..9a6cfbc
--- /dev/null
+++ b/src/main/java/org/java_websocket/SocketChannelIOHelper.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import org.java_websocket.enums.Role;
+
+public class SocketChannelIOHelper {
+
+ private SocketChannelIOHelper() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static boolean read(final ByteBuffer buf, WebSocketImpl ws, ByteChannel channel)
+ throws IOException {
+ buf.clear();
+ int read = channel.read(buf);
+ buf.flip();
+
+ if (read == -1) {
+ ws.eot();
+ return false;
+ }
+ return read != 0;
+ }
+
+ /**
+ * @param buf The ByteBuffer to read from
+ * @param ws The WebSocketImpl associated with the channels
+ * @param channel The channel to read from
+ * @return returns Whether there is more data left which can be obtained via {@link
+ * WrappedByteChannel#readMore(ByteBuffer)}
+ * @throws IOException May be thrown by {@link WrappedByteChannel#readMore(ByteBuffer)}#
+ * @see WrappedByteChannel#readMore(ByteBuffer)
+ **/
+ public static boolean readMore(final ByteBuffer buf, WebSocketImpl ws, WrappedByteChannel channel)
+ throws IOException {
+ buf.clear();
+ int read = channel.readMore(buf);
+ buf.flip();
+
+ if (read == -1) {
+ ws.eot();
+ return false;
+ }
+ return channel.isNeedRead();
+ }
+
+ /**
+ * Returns whether the whole outQueue has been flushed
+ *
+ * @param ws The WebSocketImpl associated with the channels
+ * @param sockchannel The channel to write to
+ * @return returns Whether there is more data to write
+ * @throws IOException May be thrown by {@link WrappedByteChannel#writeMore()}
+ */
+ public static boolean batch(WebSocketImpl ws, ByteChannel sockchannel) throws IOException {
+ if (ws == null) {
+ return false;
+ }
+ ByteBuffer buffer = ws.outQueue.peek();
+ WrappedByteChannel c = null;
+
+ if (buffer == null) {
+ if (sockchannel instanceof WrappedByteChannel) {
+ c = (WrappedByteChannel) sockchannel;
+ if (c.isNeedWrite()) {
+ c.writeMore();
+ }
+ }
+ } else {
+ do {
+ // FIXME writing as much as possible is unfair!!
+ /*int written = */
+ sockchannel.write(buffer);
+ if (buffer.remaining() > 0) {
+ return false;
+ } else {
+ ws.outQueue.poll(); // Buffer finished. Remove it.
+ buffer = ws.outQueue.peek();
+ }
+ } while (buffer != null);
+ }
+
+ if (ws.outQueue.isEmpty() && ws.isFlushAndClose() && ws.getDraft() != null
+ && ws.getDraft().getRole() != null && ws.getDraft().getRole() == Role.SERVER) {
+ ws.closeConnection();
+ }
+ return c == null || !((WrappedByteChannel) sockchannel).isNeedWrite();
+ }
+}
diff --git a/src/main/java/org/java_websocket/WebSocket.java b/src/main/java/org/java_websocket/WebSocket.java
new file mode 100644
index 0000000..d515f63
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocket.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import javax.net.ssl.SSLSession;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.exceptions.WebsocketNotConnectedException;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.protocols.IProtocol;
+
+public interface WebSocket {
+
+ /**
+ * sends the closing handshake. may be send in response to an other handshake.
+ *
+ * @param code the closing code
+ * @param message the closing message
+ */
+ void close(int code, String message);
+
+ /**
+ * sends the closing handshake. may be send in response to an other handshake.
+ *
+ * @param code the closing code
+ */
+ void close(int code);
+
+ /**
+ * Convenience function which behaves like close(CloseFrame.NORMAL)
+ */
+ void close();
+
+ /**
+ * This will close the connection immediately without a proper close handshake. The code and the
+ * message therefore won't be transferred over the wire also they will be forwarded to
+ * onClose/onWebsocketClose.
+ *
+ * @param code the closing code
+ * @param message the closing message
+ **/
+ void closeConnection(int code, String message);
+
+ /**
+ * Send Text data to the other end.
+ *
+ * @param text the text data to send
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ void send(String text);
+
+ /**
+ * Send Binary data (plain bytes) to the other end.
+ *
+ * @param bytes the binary data to send
+ * @throws IllegalArgumentException the data is null
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ void send(ByteBuffer bytes);
+
+ /**
+ * Send Binary data (plain bytes) to the other end.
+ *
+ * @param bytes the byte array to send
+ * @throws IllegalArgumentException the data is null
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ void send(byte[] bytes);
+
+ /**
+ * Send a frame to the other end
+ *
+ * @param framedata the frame to send to the other end
+ */
+ void sendFrame(Framedata framedata);
+
+ /**
+ * Send a collection of frames to the other end
+ *
+ * @param frames the frames to send to the other end
+ */
+ void sendFrame(Collection frames);
+
+ /**
+ * Send a ping to the other end
+ *
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ void sendPing();
+
+ /**
+ * Allows to send continuous/fragmented frames conveniently. For more into on this frame type
+ * see http://tools.ietf.org/html/rfc6455#section-5.4
+ *
+ * If the first frame you send is also the last then it is not a fragmented frame and will
+ * received via onMessage instead of onFragmented even though it was send by this method.
+ *
+ * @param op This is only important for the first frame in the sequence. Opcode.TEXT,
+ * Opcode.BINARY are allowed.
+ * @param buffer The buffer which contains the payload. It may have no bytes remaining.
+ * @param fin true means the current frame is the last in the sequence.
+ **/
+ void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin);
+
+ /**
+ * Checks if the websocket has buffered data
+ *
+ * @return has the websocket buffered data
+ */
+ boolean hasBufferedData();
+
+ /**
+ * Returns the address of the endpoint this socket is connected to, or {@code null} if it is
+ * unconnected.
+ *
+ * @return the remote socket address or null, if this socket is unconnected
+ */
+ InetSocketAddress getRemoteSocketAddress();
+
+ /**
+ * Returns the address of the endpoint this socket is bound to, or {@code null} if it is not
+ * bound.
+ *
+ * @return the local socket address or null, if this socket is not bound
+ */
+ InetSocketAddress getLocalSocketAddress();
+
+ /**
+ * Is the websocket in the state OPEN
+ *
+ * @return state equals ReadyState.OPEN
+ */
+ boolean isOpen();
+
+ /**
+ * Is the websocket in the state CLOSING
+ *
+ * @return state equals ReadyState.CLOSING
+ */
+ boolean isClosing();
+
+ /**
+ * Returns true when no further frames may be submitted This happens before the socket
+ * connection is closed.
+ *
+ * @return true when no further frames may be submitted
+ */
+ boolean isFlushAndClose();
+
+ /**
+ * Is the websocket in the state CLOSED
+ *
+ * @return state equals ReadyState.CLOSED
+ */
+ boolean isClosed();
+
+ /**
+ * Getter for the draft
+ *
+ * @return the used draft
+ */
+ Draft getDraft();
+
+ /**
+ * Retrieve the WebSocket 'ReadyState'. This represents the state of the connection. It returns a
+ * numerical value, as per W3C WebSockets specs.
+ *
+ * @return Returns '0 = CONNECTING', '1 = OPEN', '2 = CLOSING' or '3 = CLOSED'
+ */
+ ReadyState getReadyState();
+
+ /**
+ * Returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2
+ * If the opening handshake has not yet happened it will return null.
+ *
+ * @return Returns the decoded path component of this URI.
+ **/
+ String getResourceDescriptor();
+
+ /**
+ * Setter for an attachment on the socket connection. The attachment may be of any type.
+ *
+ * @param attachment The object to be attached to the user
+ * @param The type of the attachment
+ * @since 1.3.7
+ **/
+ void setAttachment(T attachment);
+
+ /**
+ * Getter for the connection attachment.
+ *
+ * @param The type of the attachment
+ * @return Returns the user attachment
+ * @since 1.3.7
+ **/
+ T getAttachment();
+
+ /**
+ * Does this websocket use an encrypted (wss/ssl) or unencrypted (ws) connection
+ *
+ * @return true, if the websocket does use wss and therefore has a SSLSession
+ * @since 1.4.1
+ */
+ boolean hasSSLSupport();
+
+ /**
+ * Returns the ssl session of websocket, if ssl/wss is used for this instance.
+ *
+ * @return the ssl session of this websocket instance
+ * @throws IllegalArgumentException the underlying channel does not use ssl (use hasSSLSupport()
+ * to check)
+ * @since 1.4.1
+ */
+ SSLSession getSSLSession() throws IllegalArgumentException;
+
+ /**
+ * Returns the used Sec-WebSocket-Protocol for this websocket connection
+ *
+ * @return the Sec-WebSocket-Protocol or null, if no draft available
+ * @throws IllegalArgumentException the underlying draft does not support a Sec-WebSocket-Protocol
+ * @since 1.5.2
+ */
+ IProtocol getProtocol();
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/WebSocketAdapter.java b/src/main/java/org/java_websocket/WebSocketAdapter.java
new file mode 100644
index 0000000..fdaf120
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocketAdapter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.PingFrame;
+import org.java_websocket.framing.PongFrame;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.HandshakeImpl1Server;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.handshake.ServerHandshakeBuilder;
+
+/**
+ * This class default implements all methods of the WebSocketListener that can be overridden
+ * optionally when advances functionalities is needed.
+ **/
+public abstract class WebSocketAdapter implements WebSocketListener {
+
+ private PingFrame pingFrame;
+
+ /**
+ * This default implementation does not do anything. Go ahead and overwrite it.
+ *
+ * @see WebSocketListener#onWebsocketHandshakeReceivedAsServer(WebSocket,
+ * Draft, ClientHandshake)
+ */
+ @Override
+ public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
+ ClientHandshake request) throws InvalidDataException {
+ return new HandshakeImpl1Server();
+ }
+
+ @Override
+ public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request,
+ ServerHandshake response) throws InvalidDataException {
+ //To overwrite
+ }
+
+ /**
+ * This default implementation does not do anything which will cause the connections to always
+ * progress.
+ *
+ * @see WebSocketListener#onWebsocketHandshakeSentAsClient(WebSocket,
+ * ClientHandshake)
+ */
+ @Override
+ public void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request)
+ throws InvalidDataException {
+ //To overwrite
+ }
+
+ /**
+ * This default implementation will send a pong in response to the received ping. The pong frame
+ * will have the same payload as the ping frame.
+ *
+ * @see WebSocketListener#onWebsocketPing(WebSocket, Framedata)
+ */
+ @Override
+ public void onWebsocketPing(WebSocket conn, Framedata f) {
+ conn.sendFrame(new PongFrame((PingFrame) f));
+ }
+
+ /**
+ * This default implementation does not do anything. Go ahead and overwrite it.
+ *
+ * @see WebSocketListener#onWebsocketPong(WebSocket, Framedata)
+ */
+ @Override
+ public void onWebsocketPong(WebSocket conn, Framedata f) {
+ //To overwrite
+ }
+
+ /**
+ * Default implementation for onPreparePing, returns a (cached) PingFrame that has no application
+ * data.
+ *
+ * @param conn The WebSocket connection from which the ping frame will be sent.
+ * @return PingFrame to be sent.
+ * @see WebSocketListener#onPreparePing(WebSocket)
+ */
+ @Override
+ public PingFrame onPreparePing(WebSocket conn) {
+ if (pingFrame == null) {
+ pingFrame = new PingFrame();
+ }
+ return pingFrame;
+ }
+}
diff --git a/src/main/java/org/java_websocket/WebSocketFactory.java b/src/main/java/org/java_websocket/WebSocketFactory.java
new file mode 100644
index 0000000..eed6e5a
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocketFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.util.List;
+import org.java_websocket.drafts.Draft;
+
+public interface WebSocketFactory {
+
+ /**
+ * Create a new Websocket with the provided listener, drafts and socket
+ *
+ * @param a The Listener for the WebsocketImpl
+ * @param d The draft which should be used
+ * @return A WebsocketImpl
+ */
+ WebSocket createWebSocket(WebSocketAdapter a, Draft d);
+
+ /**
+ * Create a new Websocket with the provided listener, drafts and socket
+ *
+ * @param a The Listener for the WebsocketImpl
+ * @param drafts The drafts which should be used
+ * @return A WebsocketImpl
+ */
+ WebSocket createWebSocket(WebSocketAdapter a, List drafts);
+
+}
diff --git a/src/main/java/org/java_websocket/WebSocketImpl.java b/src/main/java/org/java_websocket/WebSocketImpl.java
new file mode 100644
index 0000000..3289aef
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocketImpl.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import javax.net.ssl.SSLSession;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.drafts.Draft_6455;
+import org.java_websocket.enums.CloseHandshakeType;
+import org.java_websocket.enums.HandshakeState;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.enums.Role;
+import org.java_websocket.exceptions.IncompleteHandshakeException;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidHandshakeException;
+import org.java_websocket.exceptions.LimitExceededException;
+import org.java_websocket.exceptions.WebsocketNotConnectedException;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.PingFrame;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.ClientHandshakeBuilder;
+import org.java_websocket.handshake.Handshakedata;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.handshake.ServerHandshakeBuilder;
+import org.java_websocket.interfaces.ISSLChannel;
+import org.java_websocket.protocols.IProtocol;
+import org.java_websocket.server.WebSocketServer.WebSocketWorker;
+import org.java_websocket.util.Charsetfunctions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents one end (client or server) of a single WebSocketImpl connection. Takes care of the
+ * "handshake" phase, then allows for easy sending of text frames, and receiving frames through an
+ * event-based model.
+ */
+public class WebSocketImpl implements WebSocket {
+
+ /**
+ * The default port of WebSockets, as defined in the spec. If the nullary constructor is used,
+ * DEFAULT_PORT will be the port the WebSocketServer is binded to. Note that ports under 1024
+ * usually require root permissions.
+ */
+ public static final int DEFAULT_PORT = 80;
+
+ /**
+ * The default wss port of WebSockets, as defined in the spec. If the nullary constructor is used,
+ * DEFAULT_WSS_PORT will be the port the WebSocketServer is binded to. Note that ports under 1024
+ * usually require root permissions.
+ */
+ public static final int DEFAULT_WSS_PORT = 443;
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(WebSocketImpl.class);
+
+ /**
+ * Queue of buffers that need to be sent to the client.
+ */
+ public final BlockingQueue outQueue;
+ /**
+ * Queue of buffers that need to be processed
+ */
+ public final BlockingQueue inQueue;
+ /**
+ * The listener to notify of WebSocket events.
+ */
+ private final WebSocketListener wsl;
+
+ private SelectionKey key;
+
+ /**
+ * the possibly wrapped channel object whose selection is controlled by {@link #key}
+ */
+ private ByteChannel channel;
+ /**
+ * Helper variable meant to store the thread which ( exclusively ) triggers this objects decode
+ * method.
+ **/
+ private WebSocketWorker workerThread;
+ /**
+ * When true no further frames may be submitted to be sent
+ */
+ private boolean flushandclosestate = false;
+
+ /**
+ * The current state of the connection
+ */
+ private volatile ReadyState readyState = ReadyState.NOT_YET_CONNECTED;
+
+ /**
+ * A list of drafts available for this websocket
+ */
+ private List knownDrafts;
+
+ /**
+ * The draft which is used by this websocket
+ */
+ private Draft draft = null;
+
+ /**
+ * The role which this websocket takes in the connection
+ */
+ private Role role;
+
+ /**
+ * the bytes of an incomplete received handshake
+ */
+ private ByteBuffer tmpHandshakeBytes = ByteBuffer.allocate(0);
+
+ /**
+ * stores the handshake sent by this websocket ( Role.CLIENT only )
+ */
+ private ClientHandshake handshakerequest = null;
+
+ private String closemessage = null;
+ private Integer closecode = null;
+ private Boolean closedremotely = null;
+
+ private String resourceDescriptor = null;
+
+ /**
+ * Attribute, when the last pong was received
+ */
+ private long lastPong = System.nanoTime();
+
+ /**
+ * Attribut to synchronize the write
+ */
+ private final Object synchronizeWriteObject = new Object();
+
+ /**
+ * Attribute to store connection attachment
+ *
+ * @since 1.3.7
+ */
+ private Object attachment;
+
+ /**
+ * Creates a websocket with server role
+ *
+ * @param listener The listener for this instance
+ * @param drafts The drafts which should be used
+ */
+ public WebSocketImpl(WebSocketListener listener, List drafts) {
+ this(listener, (Draft) null);
+ this.role = Role.SERVER;
+ // draft.copyInstance will be called when the draft is first needed
+ if (drafts == null || drafts.isEmpty()) {
+ knownDrafts = new ArrayList<>();
+ knownDrafts.add(new Draft_6455());
+ } else {
+ knownDrafts = drafts;
+ }
+ }
+
+ /**
+ * creates a websocket with client role
+ *
+ * @param listener The listener for this instance
+ * @param draft The draft which should be used
+ */
+ public WebSocketImpl(WebSocketListener listener, Draft draft) {
+ // socket can be null because we want do be able to create the object without already having a bound channel
+ if (listener == null || (draft == null && role == Role.SERVER)) {
+ throw new IllegalArgumentException("parameters must not be null");
+ }
+ this.outQueue = new LinkedBlockingQueue<>();
+ inQueue = new LinkedBlockingQueue<>();
+ this.wsl = listener;
+ this.role = Role.CLIENT;
+ if (draft != null) {
+ this.draft = draft.copyInstance();
+ }
+ }
+
+ /**
+ * Method to decode the provided ByteBuffer
+ *
+ * @param socketBuffer the ByteBuffer to decode
+ */
+ public void decode(ByteBuffer socketBuffer) {
+ assert (socketBuffer.hasRemaining());
+ if (log.isTraceEnabled()) {
+ log.trace("process({}): ({})", socketBuffer.remaining(),
+ (socketBuffer.remaining() > 1000 ? "too big to display"
+ : new String(socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining())));
+ }
+ if (readyState != ReadyState.NOT_YET_CONNECTED) {
+ if (readyState == ReadyState.OPEN) {
+ decodeFrames(socketBuffer);
+ }
+ } else {
+ if (decodeHandshake(socketBuffer) && (!isClosing() && !isClosed())) {
+ assert (tmpHandshakeBytes.hasRemaining() != socketBuffer.hasRemaining() || !socketBuffer
+ .hasRemaining()); // the buffers will never have remaining bytes at the same time
+ if (socketBuffer.hasRemaining()) {
+ decodeFrames(socketBuffer);
+ } else if (tmpHandshakeBytes.hasRemaining()) {
+ decodeFrames(tmpHandshakeBytes);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether the handshake phase has is completed. In case of a broken handshake this will
+ * be never the case.
+ **/
+ private boolean decodeHandshake(ByteBuffer socketBufferNew) {
+ ByteBuffer socketBuffer;
+ if (tmpHandshakeBytes.capacity() == 0) {
+ socketBuffer = socketBufferNew;
+ } else {
+ if (tmpHandshakeBytes.remaining() < socketBufferNew.remaining()) {
+ ByteBuffer buf = ByteBuffer
+ .allocate(tmpHandshakeBytes.capacity() + socketBufferNew.remaining());
+ tmpHandshakeBytes.flip();
+ buf.put(tmpHandshakeBytes);
+ tmpHandshakeBytes = buf;
+ }
+
+ tmpHandshakeBytes.put(socketBufferNew);
+ tmpHandshakeBytes.flip();
+ socketBuffer = tmpHandshakeBytes;
+ }
+ socketBuffer.mark();
+ try {
+ HandshakeState handshakestate;
+ try {
+ if (role == Role.SERVER) {
+ if (draft == null) {
+ for (Draft d : knownDrafts) {
+ d = d.copyInstance();
+ try {
+ d.setParseMode(role);
+ socketBuffer.reset();
+ Handshakedata tmphandshake = d.translateHandshake(socketBuffer);
+ if (!(tmphandshake instanceof ClientHandshake)) {
+ log.trace("Closing due to wrong handshake");
+ closeConnectionDueToWrongHandshake(
+ new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "wrong http function"));
+ return false;
+ }
+ ClientHandshake handshake = (ClientHandshake) tmphandshake;
+ handshakestate = d.acceptHandshakeAsServer(handshake);
+ if (handshakestate == HandshakeState.MATCHED) {
+ resourceDescriptor = handshake.getResourceDescriptor();
+ ServerHandshakeBuilder response;
+ try {
+ response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake);
+ } catch (InvalidDataException e) {
+ log.trace("Closing due to wrong handshake. Possible handshake rejection", e);
+ closeConnectionDueToWrongHandshake(e);
+ return false;
+ } catch (RuntimeException e) {
+ log.error("Closing due to internal server error", e);
+ wsl.onWebsocketError(this, e);
+ closeConnectionDueToInternalServerError(e);
+ return false;
+ }
+ write(d.createHandshake(
+ d.postProcessHandshakeResponseAsServer(handshake, response)));
+ draft = d;
+ open(handshake);
+ return true;
+ }
+ } catch (InvalidHandshakeException e) {
+ // go on with an other draft
+ }
+ }
+ if (draft == null) {
+ log.trace("Closing due to protocol error: no draft matches");
+ closeConnectionDueToWrongHandshake(
+ new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "no draft matches"));
+ }
+ return false;
+ } else {
+ // special case for multiple step handshakes
+ Handshakedata tmphandshake = draft.translateHandshake(socketBuffer);
+ if (!(tmphandshake instanceof ClientHandshake)) {
+ log.trace("Closing due to protocol error: wrong http function");
+ flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false);
+ return false;
+ }
+ ClientHandshake handshake = (ClientHandshake) tmphandshake;
+ handshakestate = draft.acceptHandshakeAsServer(handshake);
+
+ if (handshakestate == HandshakeState.MATCHED) {
+ open(handshake);
+ return true;
+ } else {
+ log.trace("Closing due to protocol error: the handshake did finally not match");
+ close(CloseFrame.PROTOCOL_ERROR, "the handshake did finally not match");
+ }
+ return false;
+ }
+ } else if (role == Role.CLIENT) {
+ draft.setParseMode(role);
+ Handshakedata tmphandshake = draft.translateHandshake(socketBuffer);
+ if (!(tmphandshake instanceof ServerHandshake)) {
+ log.trace("Closing due to protocol error: wrong http function");
+ flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false);
+ return false;
+ }
+ ServerHandshake handshake = (ServerHandshake) tmphandshake;
+ handshakestate = draft.acceptHandshakeAsClient(handshakerequest, handshake);
+ if (handshakestate == HandshakeState.MATCHED) {
+ try {
+ wsl.onWebsocketHandshakeReceivedAsClient(this, handshakerequest, handshake);
+ } catch (InvalidDataException e) {
+ log.trace("Closing due to invalid data exception. Possible handshake rejection", e);
+ flushAndClose(e.getCloseCode(), e.getMessage(), false);
+ return false;
+ } catch (RuntimeException e) {
+ log.error("Closing since client was never connected", e);
+ wsl.onWebsocketError(this, e);
+ flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), false);
+ return false;
+ }
+ open(handshake);
+ return true;
+ } else {
+ log.trace("Closing due to protocol error: draft {} refuses handshake", draft);
+ close(CloseFrame.PROTOCOL_ERROR, "draft " + draft + " refuses handshake");
+ }
+ }
+ } catch (InvalidHandshakeException e) {
+ log.trace("Closing due to invalid handshake", e);
+ close(e);
+ }
+ } catch (IncompleteHandshakeException e) {
+ if (tmpHandshakeBytes.capacity() == 0) {
+ socketBuffer.reset();
+ int newsize = e.getPreferredSize();
+ if (newsize == 0) {
+ newsize = socketBuffer.capacity() + 16;
+ } else {
+ assert (e.getPreferredSize() >= socketBuffer.remaining());
+ }
+ tmpHandshakeBytes = ByteBuffer.allocate(newsize);
+
+ tmpHandshakeBytes.put(socketBufferNew);
+ // tmpHandshakeBytes.flip();
+ } else {
+ tmpHandshakeBytes.position(tmpHandshakeBytes.limit());
+ tmpHandshakeBytes.limit(tmpHandshakeBytes.capacity());
+ }
+ }
+ return false;
+ }
+
+ private void decodeFrames(ByteBuffer socketBuffer) {
+ List frames;
+ try {
+ frames = draft.translateFrame(socketBuffer);
+ for (Framedata f : frames) {
+ log.trace("matched frame: {}", f);
+ draft.processFrame(this, f);
+ }
+ } catch (LimitExceededException e) {
+ if (e.getLimit() == Integer.MAX_VALUE) {
+ log.error("Closing due to invalid size of frame", e);
+ wsl.onWebsocketError(this, e);
+ }
+ close(e);
+ } catch (InvalidDataException e) {
+ log.error("Closing due to invalid data in frame", e);
+ wsl.onWebsocketError(this, e);
+ close(e);
+ } catch (VirtualMachineError | ThreadDeath | LinkageError e) {
+ log.error("Got fatal error during frame processing");
+ throw e;
+ } catch (Error e) {
+ log.error("Closing web socket due to an error during frame processing");
+ Exception exception = new Exception(e);
+ wsl.onWebsocketError(this, exception);
+ String errorMessage = "Got error " + e.getClass().getName();
+ close(CloseFrame.UNEXPECTED_CONDITION, errorMessage);
+ }
+ }
+
+ /**
+ * Close the connection if the received handshake was not correct
+ *
+ * @param exception the InvalidDataException causing this problem
+ */
+ private void closeConnectionDueToWrongHandshake(InvalidDataException exception) {
+ write(generateHttpResponseDueToError(404));
+ flushAndClose(exception.getCloseCode(), exception.getMessage(), false);
+ }
+
+ /**
+ * Close the connection if there was a server error by a RuntimeException
+ *
+ * @param exception the RuntimeException causing this problem
+ */
+ private void closeConnectionDueToInternalServerError(RuntimeException exception) {
+ write(generateHttpResponseDueToError(500));
+ flushAndClose(CloseFrame.NEVER_CONNECTED, exception.getMessage(), false);
+ }
+
+ /**
+ * Generate a simple response for the corresponding endpoint to indicate some error
+ *
+ * @param errorCode the http error code
+ * @return the complete response as ByteBuffer
+ */
+ private ByteBuffer generateHttpResponseDueToError(int errorCode) {
+ String errorCodeDescription;
+ switch (errorCode) {
+ case 404:
+ errorCodeDescription = "404 WebSocket Upgrade Failure";
+ break;
+ case 500:
+ default:
+ errorCodeDescription = "500 Internal Server Error";
+ }
+ return ByteBuffer.wrap(Charsetfunctions.asciiBytes("HTTP/1.1 " + errorCodeDescription
+ + "\r\nContent-Type: text/html\r\nServer: TooTallNate Java-WebSocket\r\nContent-Length: "
+ + (48 + errorCodeDescription.length()) + "\r\n\r\n
"
+ + errorCodeDescription + "
"));
+ }
+
+ public synchronized void close(int code, String message, boolean remote) {
+ if (readyState != ReadyState.CLOSING && readyState != ReadyState.CLOSED) {
+ if (readyState == ReadyState.OPEN) {
+ if (code == CloseFrame.ABNORMAL_CLOSE) {
+ assert (!remote);
+ readyState = ReadyState.CLOSING;
+ flushAndClose(code, message, false);
+ return;
+ }
+ if (draft.getCloseHandshakeType() != CloseHandshakeType.NONE) {
+ try {
+ if (!remote) {
+ try {
+ wsl.onWebsocketCloseInitiated(this, code, message);
+ } catch (RuntimeException e) {
+ wsl.onWebsocketError(this, e);
+ }
+ }
+ if (isOpen()) {
+ CloseFrame closeFrame = new CloseFrame();
+ closeFrame.setReason(message);
+ closeFrame.setCode(code);
+ closeFrame.isValid();
+ sendFrame(closeFrame);
+ }
+ } catch (InvalidDataException e) {
+ log.error("generated frame is invalid", e);
+ wsl.onWebsocketError(this, e);
+ flushAndClose(CloseFrame.ABNORMAL_CLOSE, "generated frame is invalid", false);
+ }
+ }
+ flushAndClose(code, message, remote);
+ } else if (code == CloseFrame.FLASHPOLICY) {
+ assert (remote);
+ flushAndClose(CloseFrame.FLASHPOLICY, message, true);
+ } else if (code == CloseFrame.PROTOCOL_ERROR) { // this endpoint found a PROTOCOL_ERROR
+ flushAndClose(code, message, remote);
+ } else {
+ flushAndClose(CloseFrame.NEVER_CONNECTED, message, false);
+ }
+ readyState = ReadyState.CLOSING;
+ tmpHandshakeBytes = null;
+ return;
+ }
+ }
+
+ @Override
+ public void close(int code, String message) {
+ close(code, message, false);
+ }
+
+ /**
+ * This will close the connection immediately without a proper close handshake. The code and the
+ * message therefore won't be transferred over the wire also they will be forwarded to
+ * onClose/onWebsocketClose.
+ *
+ * @param code the closing code
+ * @param message the closing message
+ * @param remote Indicates who "generated" code.
+ * true means that this endpoint received the code from
+ * the other endpoint. false means this endpoint decided to send the given
+ * code,
+ * remote may also be true if this endpoint started the closing
+ * handshake since the other endpoint may not simply echo the code but
+ * close the connection the same time this endpoint does do but with an other
+ * code.
+ **/
+ public synchronized void closeConnection(int code, String message, boolean remote) {
+ if (readyState == ReadyState.CLOSED) {
+ return;
+ }
+ //Methods like eot() call this method without calling onClose(). Due to that reason we have to adjust the ReadyState manually
+ if (readyState == ReadyState.OPEN) {
+ if (code == CloseFrame.ABNORMAL_CLOSE) {
+ readyState = ReadyState.CLOSING;
+ }
+ }
+ if (key != null) {
+ // key.attach( null ); //see issue #114
+ key.cancel();
+ }
+ if (channel != null) {
+ try {
+ channel.close();
+ } catch (IOException e) {
+ if (e.getMessage() != null && e.getMessage().equals("Broken pipe")) {
+ log.trace("Caught IOException: Broken pipe during closeConnection()", e);
+ } else {
+ log.error("Exception during channel.close()", e);
+ wsl.onWebsocketError(this, e);
+ }
+ }
+ }
+ try {
+ this.wsl.onWebsocketClose(this, code, message, remote);
+ } catch (RuntimeException e) {
+
+ wsl.onWebsocketError(this, e);
+ }
+ if (draft != null) {
+ draft.reset();
+ }
+ handshakerequest = null;
+ readyState = ReadyState.CLOSED;
+ }
+
+ protected void closeConnection(int code, boolean remote) {
+ closeConnection(code, "", remote);
+ }
+
+ public void closeConnection() {
+ if (closedremotely == null) {
+ throw new IllegalStateException("this method must be used in conjunction with flushAndClose");
+ }
+ closeConnection(closecode, closemessage, closedremotely);
+ }
+
+ public void closeConnection(int code, String message) {
+ closeConnection(code, message, false);
+ }
+
+ public synchronized void flushAndClose(int code, String message, boolean remote) {
+ if (flushandclosestate) {
+ return;
+ }
+ closecode = code;
+ closemessage = message;
+ closedremotely = remote;
+
+ flushandclosestate = true;
+
+ wsl.onWriteDemand(
+ this); // ensures that all outgoing frames are flushed before closing the connection
+ try {
+ wsl.onWebsocketClosing(this, code, message, remote);
+ } catch (RuntimeException e) {
+ log.error("Exception in onWebsocketClosing", e);
+ wsl.onWebsocketError(this, e);
+ }
+ if (draft != null) {
+ draft.reset();
+ }
+ handshakerequest = null;
+ }
+
+ public void eot() {
+ if (readyState == ReadyState.NOT_YET_CONNECTED) {
+ closeConnection(CloseFrame.NEVER_CONNECTED, true);
+ } else if (flushandclosestate) {
+ closeConnection(closecode, closemessage, closedremotely);
+ } else if (draft.getCloseHandshakeType() == CloseHandshakeType.NONE) {
+ closeConnection(CloseFrame.NORMAL, true);
+ } else if (draft.getCloseHandshakeType() == CloseHandshakeType.ONEWAY) {
+ if (role == Role.SERVER) {
+ closeConnection(CloseFrame.ABNORMAL_CLOSE, true);
+ } else {
+ closeConnection(CloseFrame.NORMAL, true);
+ }
+ } else {
+ closeConnection(CloseFrame.ABNORMAL_CLOSE, true);
+ }
+ }
+
+ @Override
+ public void close(int code) {
+ close(code, "", false);
+ }
+
+ public void close(InvalidDataException e) {
+ close(e.getCloseCode(), e.getMessage(), false);
+ }
+
+ /**
+ * Send Text data to the other end.
+ *
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ @Override
+ public void send(String text) {
+ if (text == null) {
+ throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl.");
+ }
+ send(draft.createFrames(text, role == Role.CLIENT));
+ }
+
+ /**
+ * Send Binary data (plain bytes) to the other end.
+ *
+ * @throws IllegalArgumentException the data is null
+ * @throws WebsocketNotConnectedException websocket is not yet connected
+ */
+ @Override
+ public void send(ByteBuffer bytes) {
+ if (bytes == null) {
+ throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl.");
+ }
+ send(draft.createFrames(bytes, role == Role.CLIENT));
+ }
+
+ @Override
+ public void send(byte[] bytes) {
+ send(ByteBuffer.wrap(bytes));
+ }
+
+ private void send(Collection frames) {
+ if (!isOpen()) {
+ throw new WebsocketNotConnectedException();
+ }
+ if (frames == null) {
+ throw new IllegalArgumentException();
+ }
+ ArrayList outgoingFrames = new ArrayList<>();
+ for (Framedata f : frames) {
+ log.trace("send frame: {}", f);
+ outgoingFrames.add(draft.createBinaryFrame(f));
+ }
+ write(outgoingFrames);
+ }
+
+ @Override
+ public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) {
+ send(draft.continuousFrame(op, buffer, fin));
+ }
+
+ @Override
+ public void sendFrame(Collection frames) {
+ send(frames);
+ }
+
+ @Override
+ public void sendFrame(Framedata framedata) {
+ send(Collections.singletonList(framedata));
+ }
+
+ public void sendPing() throws NullPointerException {
+ // Gets a PingFrame from WebSocketListener(wsl) and sends it.
+ PingFrame pingFrame = wsl.onPreparePing(this);
+ if (pingFrame == null) {
+ throw new NullPointerException(
+ "onPreparePing(WebSocket) returned null. PingFrame to sent can't be null.");
+ }
+ sendFrame(pingFrame);
+ }
+
+ @Override
+ public boolean hasBufferedData() {
+ return !this.outQueue.isEmpty();
+ }
+
+ public void startHandshake(ClientHandshakeBuilder handshakedata)
+ throws InvalidHandshakeException {
+ // Store the Handshake Request we are about to send
+ this.handshakerequest = draft.postProcessHandshakeRequestAsClient(handshakedata);
+
+ resourceDescriptor = handshakedata.getResourceDescriptor();
+ assert (resourceDescriptor != null);
+
+ // Notify Listener
+ try {
+ wsl.onWebsocketHandshakeSentAsClient(this, this.handshakerequest);
+ } catch (InvalidDataException e) {
+ // Stop if the client code throws an exception
+ throw new InvalidHandshakeException("Handshake data rejected by client.");
+ } catch (RuntimeException e) {
+ log.error("Exception in startHandshake", e);
+ wsl.onWebsocketError(this, e);
+ throw new InvalidHandshakeException("rejected because of " + e);
+ }
+
+ // Send
+ write(draft.createHandshake(this.handshakerequest));
+ }
+
+ private void write(ByteBuffer buf) {
+ log.trace("write({}): {}", buf.remaining(),
+ buf.remaining() > 1000 ? "too big to display" : new String(buf.array()));
+
+ outQueue.add(buf);
+ wsl.onWriteDemand(this);
+ }
+
+ /**
+ * Write a list of bytebuffer (frames in binary form) into the outgoing queue
+ *
+ * @param bufs the list of bytebuffer
+ */
+ private void write(List bufs) {
+ synchronized (synchronizeWriteObject) {
+ for (ByteBuffer b : bufs) {
+ write(b);
+ }
+ }
+ }
+
+ private void open(Handshakedata d) {
+ log.trace("open using draft: {}", draft);
+ readyState = ReadyState.OPEN;
+ updateLastPong();
+ try {
+ wsl.onWebsocketOpen(this, d);
+ } catch (RuntimeException e) {
+ wsl.onWebsocketError(this, e);
+ }
+ }
+
+ @Override
+ public boolean isOpen() {
+ return readyState == ReadyState.OPEN;
+ }
+
+ @Override
+ public boolean isClosing() {
+ return readyState == ReadyState.CLOSING;
+ }
+
+ @Override
+ public boolean isFlushAndClose() {
+ return flushandclosestate;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return readyState == ReadyState.CLOSED;
+ }
+
+ @Override
+ public ReadyState getReadyState() {
+ return readyState;
+ }
+
+ /**
+ * @param key the selection key of this implementation
+ */
+ public void setSelectionKey(SelectionKey key) {
+ this.key = key;
+ }
+
+ /**
+ * @return the selection key of this implementation
+ */
+ public SelectionKey getSelectionKey() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString(); // its nice to be able to set breakpoints here
+ }
+
+ @Override
+ public InetSocketAddress getRemoteSocketAddress() {
+ return wsl.getRemoteSocketAddress(this);
+ }
+
+ @Override
+ public InetSocketAddress getLocalSocketAddress() {
+ return wsl.getLocalSocketAddress(this);
+ }
+
+ @Override
+ public Draft getDraft() {
+ return draft;
+ }
+
+ @Override
+ public void close() {
+ close(CloseFrame.NORMAL);
+ }
+
+ @Override
+ public String getResourceDescriptor() {
+ return resourceDescriptor;
+ }
+
+ /**
+ * Getter for the last pong received
+ *
+ * @return the timestamp for the last received pong
+ */
+ long getLastPong() {
+ return lastPong;
+ }
+
+ /**
+ * Update the timestamp when the last pong was received
+ */
+ public void updateLastPong() {
+ this.lastPong = System.nanoTime();
+ }
+
+ /**
+ * Getter for the websocket listener
+ *
+ * @return the websocket listener associated with this instance
+ */
+ public WebSocketListener getWebSocketListener() {
+ return wsl;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T getAttachment() {
+ return (T) attachment;
+ }
+
+ @Override
+ public boolean hasSSLSupport() {
+ return channel instanceof ISSLChannel;
+ }
+
+ @Override
+ public SSLSession getSSLSession() {
+ if (!hasSSLSupport()) {
+ throw new IllegalArgumentException(
+ "This websocket uses ws instead of wss. No SSLSession available.");
+ }
+ return ((ISSLChannel) channel).getSSLEngine().getSession();
+ }
+
+ @Override
+ public IProtocol getProtocol() {
+ if (draft == null) {
+ return null;
+ }
+ if (!(draft instanceof Draft_6455)) {
+ throw new IllegalArgumentException("This draft does not support Sec-WebSocket-Protocol");
+ }
+ return ((Draft_6455) draft).getProtocol();
+ }
+
+ @Override
+ public void setAttachment(T attachment) {
+ this.attachment = attachment;
+ }
+
+ public ByteChannel getChannel() {
+ return channel;
+ }
+
+ public void setChannel(ByteChannel channel) {
+ this.channel = channel;
+ }
+
+ public WebSocketWorker getWorkerThread() {
+ return workerThread;
+ }
+
+ public void setWorkerThread(WebSocketWorker workerThread) {
+ this.workerThread = workerThread;
+ }
+
+
+}
diff --git a/src/main/java/org/java_websocket/WebSocketListener.java b/src/main/java/org/java_websocket/WebSocketListener.java
new file mode 100644
index 0000000..f0b21d5
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocketListener.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.PingFrame;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.Handshakedata;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.handshake.ServerHandshakeBuilder;
+
+/**
+ * Implemented by WebSocketClient and WebSocketServer. The methods within are
+ * called by WebSocket. Almost every method takes a first parameter conn which represents
+ * the source of the respective event.
+ */
+public interface WebSocketListener {
+
+ /**
+ * Called on the server side when the socket connection is first established, and the WebSocket
+ * handshake has been received. This method allows to deny connections based on the received
+ * handshake. By default this method only requires protocol compliance.
+ *
+ * @param conn The WebSocket related to this event
+ * @param draft The protocol draft the client uses to connect
+ * @param request The opening http message send by the client. Can be used to access additional
+ * fields like cookies.
+ * @return Returns an incomplete handshake containing all optional fields
+ * @throws InvalidDataException Throwing this exception will cause this handshake to be rejected
+ */
+ ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
+ ClientHandshake request) throws InvalidDataException;
+
+ /**
+ * Called on the client side when the socket connection is first established, and the
+ * WebSocketImpl handshake response has been received.
+ *
+ * @param conn The WebSocket related to this event
+ * @param request The handshake initially send out to the server by this websocket.
+ * @param response The handshake the server sent in response to the request.
+ * @throws InvalidDataException Allows the client to reject the connection with the server in
+ * respect of its handshake response.
+ */
+ void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request,
+ ServerHandshake response) throws InvalidDataException;
+
+ /**
+ * Called on the client side when the socket connection is first established, and the
+ * WebSocketImpl handshake has just been sent.
+ *
+ * @param conn The WebSocket related to this event
+ * @param request The handshake sent to the server by this websocket
+ * @throws InvalidDataException Allows the client to stop the connection from progressing
+ */
+ void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request)
+ throws InvalidDataException;
+
+ /**
+ * Called when an entire text frame has been received. Do whatever you want here...
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param message The UTF-8 decoded message that was received.
+ */
+ void onWebsocketMessage(WebSocket conn, String message);
+
+ /**
+ * Called when an entire binary frame has been received. Do whatever you want here...
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param blob The binary message that was received.
+ */
+ void onWebsocketMessage(WebSocket conn, ByteBuffer blob);
+
+ /**
+ * Called after onHandshakeReceived returns true. Indicates that a complete
+ * WebSocket connection has been established, and we are ready to send/receive data.
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param d The handshake of the websocket instance
+ */
+ void onWebsocketOpen(WebSocket conn, Handshakedata d);
+
+ /**
+ * Called after WebSocket#close is explicity called, or when the other end of the
+ * WebSocket connection is closed.
+ *
+ * @param ws The WebSocket instance this event is occurring on.
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ * @param remote Returns whether or not the closing of the connection was initiated by the remote
+ * host.
+ */
+ void onWebsocketClose(WebSocket ws, int code, String reason, boolean remote);
+
+ /**
+ * Called as soon as no further frames are accepted
+ *
+ * @param ws The WebSocket instance this event is occurring on.
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ * @param remote Returns whether or not the closing of the connection was initiated by the remote
+ * host.
+ */
+ void onWebsocketClosing(WebSocket ws, int code, String reason, boolean remote);
+
+ /**
+ * send when this peer sends a close handshake
+ *
+ * @param ws The WebSocket instance this event is occurring on.
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ */
+ void onWebsocketCloseInitiated(WebSocket ws, int code, String reason);
+
+ /**
+ * Called if an exception worth noting occurred. If an error causes the connection to fail onClose
+ * will be called additionally afterwards.
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param ex The exception that occurred. Might be null if the exception is not related to
+ * any specific connection. For example if the server port could not be bound.
+ */
+ void onWebsocketError(WebSocket conn, Exception ex);
+
+ /**
+ * Called a ping frame has been received. This method must send a corresponding pong by itself.
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param f The ping frame. Control frames may contain payload.
+ */
+ void onWebsocketPing(WebSocket conn, Framedata f);
+
+ /**
+ * Called just before a ping frame is sent, in order to allow users to customize their ping frame
+ * data.
+ *
+ * @param conn The WebSocket connection from which the ping frame will be sent.
+ * @return PingFrame to be sent.
+ */
+ PingFrame onPreparePing(WebSocket conn);
+
+ /**
+ * Called when a pong frame is received.
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ * @param f The pong frame. Control frames may contain payload.
+ **/
+ void onWebsocketPong(WebSocket conn, Framedata f);
+
+ /**
+ * This method is used to inform the selector thread that there is data queued to be written to
+ * the socket.
+ *
+ * @param conn The WebSocket instance this event is occurring on.
+ */
+ void onWriteDemand(WebSocket conn);
+
+ /**
+ * @param conn The WebSocket instance this event is occurring on.
+ * @return Returns the address of the endpoint this socket is bound to.
+ * @see WebSocket#getLocalSocketAddress()
+ */
+ InetSocketAddress getLocalSocketAddress(WebSocket conn);
+
+ /**
+ * @param conn The WebSocket instance this event is occurring on.
+ * @return Returns the address of the endpoint this socket is connected to, or{@code null} if it
+ * is unconnected.
+ * @see WebSocket#getRemoteSocketAddress()
+ */
+ InetSocketAddress getRemoteSocketAddress(WebSocket conn);
+}
diff --git a/src/main/java/org/java_websocket/WebSocketServerFactory.java b/src/main/java/org/java_websocket/WebSocketServerFactory.java
new file mode 100644
index 0000000..825aa21
--- /dev/null
+++ b/src/main/java/org/java_websocket/WebSocketServerFactory.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import org.java_websocket.drafts.Draft;
+
+/**
+ * Interface to encapsulate the required methods for a websocket factory
+ */
+public interface WebSocketServerFactory extends WebSocketFactory {
+
+ @Override
+ WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d);
+
+ @Override
+ WebSocketImpl createWebSocket(WebSocketAdapter a, List drafts);
+
+ /**
+ * Allows to wrap the SocketChannel( key.channel() ) to insert a protocol layer( like ssl or proxy
+ * authentication) beyond the ws layer.
+ *
+ * @param channel The SocketChannel to wrap
+ * @param key a SelectionKey of an open SocketChannel.
+ * @return The channel on which the read and write operations will be performed.
+ * @throws IOException may be thrown while writing on the channel
+ */
+ ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException;
+
+ /**
+ * Allows to shutdown the websocket factory for a clean shutdown
+ */
+ void close();
+}
diff --git a/src/main/java/org/java_websocket/WrappedByteChannel.java b/src/main/java/org/java_websocket/WrappedByteChannel.java
new file mode 100644
index 0000000..8dee57d
--- /dev/null
+++ b/src/main/java/org/java_websocket/WrappedByteChannel.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+
+public interface WrappedByteChannel extends ByteChannel {
+
+ /**
+ * returns whether writeMore should be called write additional data.
+ *
+ * @return is a additional write needed
+ */
+ boolean isNeedWrite();
+
+ /**
+ * Gets called when {@link #isNeedWrite()} ()} requires a additional rite
+ *
+ * @throws IOException may be thrown due to an error while writing
+ */
+ void writeMore() throws IOException;
+
+ /**
+ * returns whether readMore should be called to fetch data which has been decoded but not yet been
+ * returned.
+ *
+ * @return is a additional read needed
+ * @see #read(ByteBuffer)
+ * @see #readMore(ByteBuffer)
+ **/
+ boolean isNeedRead();
+
+ /**
+ * This function does not read data from the underlying channel at all. It is just a way to fetch
+ * data which has already be received or decoded but was but was not yet returned to the user.
+ * This could be the case when the decoded data did not fit into the buffer the user passed to
+ * {@link #read(ByteBuffer)}.
+ *
+ * @param dst the destiny of the read
+ * @return the amount of remaining data
+ * @throws IOException when a error occurred during unwrapping
+ **/
+ int readMore(ByteBuffer dst) throws IOException;
+
+ /**
+ * This function returns the blocking state of the channel
+ *
+ * @return is the channel blocking
+ */
+ boolean isBlocking();
+}
diff --git a/src/main/java/org/java_websocket/client/DnsResolver.java b/src/main/java/org/java_websocket/client/DnsResolver.java
new file mode 100644
index 0000000..ec9f17f
--- /dev/null
+++ b/src/main/java/org/java_websocket/client/DnsResolver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.client;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+
+/**
+ * Users may implement this interface to override the default DNS lookup offered by the OS.
+ *
+ * @since 1.4.1
+ */
+public interface DnsResolver {
+
+ /**
+ * Resolves the IP address for the given URI.
+ *
+ * This method should never return null. If it's not able to resolve the IP address then it should
+ * throw an UnknownHostException
+ *
+ * @param uri The URI to be resolved
+ * @return The resolved IP address
+ * @throws UnknownHostException if no IP address for the uri could be found.
+ */
+ InetAddress resolve(URI uri) throws UnknownHostException;
+
+}
diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java
new file mode 100644
index 0000000..0e38326
--- /dev/null
+++ b/src/main/java/org/java_websocket/client/WebSocketClient.java
@@ -0,0 +1,1025 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import org.java_websocket.AbstractWebSocket;
+import org.java_websocket.WebSocket;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.drafts.Draft_6455;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.exceptions.InvalidHandshakeException;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.handshake.HandshakeImpl1Client;
+import org.java_websocket.handshake.Handshakedata;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.protocols.IProtocol;
+
+/**
+ * A subclass must implement at least onOpen, onClose, and
+ * onMessage to be useful. At runtime the user is expected to establish a connection via
+ * {@link #connect()}, then receive events like {@link #onMessage(String)} via the overloaded
+ * methods and to {@link #send(String)} data to the server.
+ */
+public abstract class WebSocketClient extends AbstractWebSocket implements Runnable, WebSocket {
+
+ /**
+ * The URI this channel is supposed to connect to.
+ */
+ protected URI uri = null;
+
+ /**
+ * The underlying engine
+ */
+ private WebSocketImpl engine = null;
+
+ /**
+ * The socket for this WebSocketClient
+ */
+ private Socket socket = null;
+
+ /**
+ * The SocketFactory for this WebSocketClient
+ *
+ * @since 1.4.0
+ */
+ private SocketFactory socketFactory = null;
+
+ /**
+ * The used OutputStream
+ */
+ private OutputStream ostream;
+
+ /**
+ * The used proxy, if any
+ */
+ private Proxy proxy = Proxy.NO_PROXY;
+
+ /**
+ * The thread to write outgoing message
+ */
+ private Thread writeThread;
+
+ /**
+ * The thread to connect and read message
+ */
+ private Thread connectReadThread;
+
+ /**
+ * The draft to use
+ */
+ private Draft draft;
+
+ /**
+ * The additional headers to use
+ */
+ private Map headers;
+
+ /**
+ * The latch for connectBlocking()
+ */
+ private CountDownLatch connectLatch = new CountDownLatch(1);
+
+ /**
+ * The latch for closeBlocking()
+ */
+ private CountDownLatch closeLatch = new CountDownLatch(1);
+
+ /**
+ * The socket timeout value to be used in milliseconds.
+ */
+ private int connectTimeout = 0;
+
+ /**
+ * DNS resolver that translates a URI to an InetAddress
+ *
+ * @see InetAddress
+ * @since 1.4.1
+ */
+ private DnsResolver dnsResolver = null;
+
+ /**
+ * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The
+ * channel does not attampt to connect automatically. The connection will be established once you
+ * call connect.
+ *
+ * @param serverUri the server URI to connect to
+ */
+ public WebSocketClient(URI serverUri) {
+ this(serverUri, new Draft_6455());
+ }
+
+ /**
+ * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The
+ * channel does not attampt to connect automatically. The connection will be established once you
+ * call connect.
+ *
+ * @param serverUri the server URI to connect to
+ * @param protocolDraft The draft which should be used for this connection
+ */
+ public WebSocketClient(URI serverUri, Draft protocolDraft) {
+ this(serverUri, protocolDraft, null, 0);
+ }
+
+ /**
+ * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The
+ * channel does not attampt to connect automatically. The connection will be established once you
+ * call connect.
+ *
+ * @param serverUri the server URI to connect to
+ * @param httpHeaders Additional HTTP-Headers
+ * @since 1.3.8
+ */
+ public WebSocketClient(URI serverUri, Map httpHeaders) {
+ this(serverUri, new Draft_6455(), httpHeaders);
+ }
+
+ /**
+ * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The
+ * channel does not attampt to connect automatically. The connection will be established once you
+ * call connect.
+ *
+ * @param serverUri the server URI to connect to
+ * @param protocolDraft The draft which should be used for this connection
+ * @param httpHeaders Additional HTTP-Headers
+ * @since 1.3.8
+ */
+ public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders) {
+ this(serverUri, protocolDraft, httpHeaders, 0);
+ }
+
+ /**
+ * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The
+ * channel does not attampt to connect automatically. The connection will be established once you
+ * call connect.
+ *
+ * @param serverUri the server URI to connect to
+ * @param protocolDraft The draft which should be used for this connection
+ * @param httpHeaders Additional HTTP-Headers
+ * @param connectTimeout The Timeout for the connection
+ */
+ public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders,
+ int connectTimeout) {
+ if (serverUri == null) {
+ throw new IllegalArgumentException();
+ } else if (protocolDraft == null) {
+ throw new IllegalArgumentException("null as draft is permitted for `WebSocketServer` only!");
+ }
+ this.uri = serverUri;
+ this.draft = protocolDraft;
+ this.dnsResolver = new DnsResolver() {
+ @Override
+ public InetAddress resolve(URI uri) throws UnknownHostException {
+ return InetAddress.getByName(uri.getHost());
+ }
+ };
+ if (httpHeaders != null) {
+ headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ headers.putAll(httpHeaders);
+ }
+ this.connectTimeout = connectTimeout;
+ setTcpNoDelay(false);
+ setReuseAddr(false);
+ this.engine = new WebSocketImpl(this, protocolDraft);
+ }
+
+ /**
+ * Returns the URI that this WebSocketClient is connected to.
+ *
+ * @return the URI connected to
+ */
+ public URI getURI() {
+ return uri;
+ }
+
+ /**
+ * Returns the protocol version this channel uses. For more infos see
+ * https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts
+ *
+ * @return The draft used for this client
+ */
+ public Draft getDraft() {
+ return draft;
+ }
+
+ /**
+ * Returns the socket to allow Hostname Verification
+ *
+ * @return the socket used for this connection
+ */
+ public Socket getSocket() {
+ return socket;
+ }
+
+ /**
+ * @param key Name of the header to add.
+ * @param value Value of the header to add.
+ * @since 1.4.1 Adds an additional header to be sent in the handshake. If the connection is
+ * already made, adding headers has no effect, unless reconnect is called, which then a new
+ * handshake is sent. If a header with the same key already exists, it is overridden.
+ */
+ public void addHeader(String key, String value) {
+ if (headers == null) {
+ headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ headers.put(key, value);
+ }
+
+ /**
+ * @param key Name of the header to remove.
+ * @return the previous value associated with key, or null if there was no mapping for key.
+ * @since 1.4.1 Removes a header from the handshake to be sent, if header key exists.
+ */
+ public String removeHeader(String key) {
+ if (headers == null) {
+ return null;
+ }
+ return headers.remove(key);
+ }
+
+ /**
+ * @since 1.4.1 Clears all previously put headers.
+ */
+ public void clearHeaders() {
+ headers = null;
+ }
+
+ /**
+ * Sets a custom DNS resolver.
+ *
+ * @param dnsResolver The DnsResolver to use.
+ * @since 1.4.1
+ */
+ public void setDnsResolver(DnsResolver dnsResolver) {
+ this.dnsResolver = dnsResolver;
+ }
+
+ /**
+ * Reinitiates the websocket connection. This method does not block.
+ *
+ * @since 1.3.8
+ */
+ public void reconnect() {
+ reset();
+ connect();
+ }
+
+ /**
+ * Same as reconnect but blocks until the websocket reconnected or failed to do
+ * so.
+ *
+ * @return Returns whether it succeeded or not.
+ * @throws InterruptedException Thrown when the threads get interrupted
+ * @since 1.3.8
+ */
+ public boolean reconnectBlocking() throws InterruptedException {
+ reset();
+ return connectBlocking();
+ }
+
+ /**
+ * Same as reconnect but blocks with a timeout until the websocket connected or failed
+ * to do so.
+ *
+ * @param timeout The connect timeout
+ * @param timeUnit The timeout time unit
+ * @return Returns whether it succeeded or not.
+ * @throws InterruptedException Thrown when the threads get interrupted
+ * @since 1.6.1
+ */
+ public boolean reconnectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException {
+ reset();
+ return connectBlocking(timeout, timeUnit);
+ }
+
+ /**
+ * Reset everything relevant to allow a reconnect
+ *
+ * @since 1.3.8
+ */
+ private void reset() {
+ Thread current = Thread.currentThread();
+ if (current == writeThread || current == connectReadThread) {
+ throw new IllegalStateException(
+ "You cannot initialize a reconnect out of the websocket thread. Use reconnect in another thread to ensure a successful cleanup.");
+ }
+ try {
+ // This socket null check ensures we can reconnect a socket that failed to connect. It's an uncommon edge case, but we want to make sure we support it
+ if (engine.getReadyState() == ReadyState.NOT_YET_CONNECTED && socket != null) {
+ // Closing the socket when we have not connected prevents the writeThread from hanging on a write indefinitely during connection teardown
+ socket.close();
+ }
+ closeBlocking();
+
+ if (writeThread != null) {
+ this.writeThread.interrupt();
+ this.writeThread.join();
+ this.writeThread = null;
+ }
+ if (connectReadThread != null) {
+ this.connectReadThread.interrupt();
+ this.connectReadThread.join();
+ this.connectReadThread = null;
+ }
+ this.draft.reset();
+ if (this.socket != null) {
+ this.socket.close();
+ this.socket = null;
+ }
+ } catch (Exception e) {
+ onError(e);
+ engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
+ return;
+ }
+ connectLatch = new CountDownLatch(1);
+ closeLatch = new CountDownLatch(1);
+ this.engine = new WebSocketImpl(this, this.draft);
+ }
+
+ /**
+ * Initiates the websocket connection. This method does not block.
+ */
+ public void connect() {
+ if (connectReadThread != null) {
+ throw new IllegalStateException("WebSocketClient objects are not reuseable");
+ }
+ connectReadThread = new Thread(this);
+ connectReadThread.setDaemon(isDaemon());
+ connectReadThread.setName("WebSocketConnectReadThread-" + connectReadThread.getId());
+ connectReadThread.start();
+ }
+
+ /**
+ * Same as connect but blocks until the websocket connected or failed to do so.
+ *
+ * @return Returns whether it succeeded or not.
+ * @throws InterruptedException Thrown when the threads get interrupted
+ */
+ public boolean connectBlocking() throws InterruptedException {
+ connect();
+ connectLatch.await();
+ return engine.isOpen();
+ }
+
+ /**
+ * Same as connect but blocks with a timeout until the websocket connected or failed
+ * to do so.
+ *
+ * @param timeout The connect timeout
+ * @param timeUnit The timeout time unit
+ * @return Returns whether it succeeded or not.
+ * @throws InterruptedException Thrown when the threads get interrupted
+ */
+ public boolean connectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException {
+ connect();
+
+ boolean connected = connectLatch.await(timeout, timeUnit);
+ if (!connected) {
+ reset();
+ }
+
+ return connected && engine.isOpen();
+ }
+
+ /**
+ * Initiates the websocket close handshake. This method does not block In oder to make sure
+ * the connection is closed use closeBlocking
+ */
+ public void close() {
+ if (writeThread != null) {
+ engine.close(CloseFrame.NORMAL);
+ }
+ }
+
+ /**
+ * Same as close but blocks until the websocket closed or failed to do so.
+ *
+ * @throws InterruptedException Thrown when the threads get interrupted
+ */
+ public void closeBlocking() throws InterruptedException {
+ close();
+ closeLatch.await();
+ }
+
+ /**
+ * Sends text to the connected websocket server.
+ *
+ * @param text The string which will be transmitted.
+ */
+ public void send(String text) {
+ engine.send(text);
+ }
+
+ /**
+ * Sends binary data to the connected webSocket server.
+ *
+ * @param data The byte-Array of data to send to the WebSocket server.
+ */
+ public void send(byte[] data) {
+ engine.send(data);
+ }
+
+ @Override
+ public T getAttachment() {
+ return engine.getAttachment();
+ }
+
+ @Override
+ public void setAttachment(T attachment) {
+ engine.setAttachment(attachment);
+ }
+
+ @Override
+ protected Collection getConnections() {
+ return Collections.singletonList((WebSocket) engine);
+ }
+
+ @Override
+ public void sendPing() {
+ engine.sendPing();
+ }
+
+ public void run() {
+ InputStream istream;
+ try {
+ boolean upgradeSocketToSSLSocket = prepareSocket();
+
+ socket.setTcpNoDelay(isTcpNoDelay());
+ socket.setReuseAddress(isReuseAddr());
+ int receiveBufferSize = getReceiveBufferSize();
+ if (receiveBufferSize > 0) {
+ socket.setReceiveBufferSize(receiveBufferSize);
+ }
+
+ if (!socket.isConnected()) {
+ InetSocketAddress addr = dnsResolver == null ? InetSocketAddress.createUnresolved(uri.getHost(), getPort()) : new InetSocketAddress(dnsResolver.resolve(uri), this.getPort());
+ socket.connect(addr, connectTimeout);
+ }
+
+ // if the socket is set by others we don't apply any TLS wrapper
+ if (upgradeSocketToSSLSocket && "wss".equals(uri.getScheme())) {
+ upgradeSocketToSSL();
+ }
+
+ if (socket instanceof SSLSocket) {
+ SSLSocket sslSocket = (SSLSocket) socket;
+ SSLParameters sslParameters = sslSocket.getSSLParameters();
+ onSetSSLParameters(sslParameters);
+ sslSocket.setSSLParameters(sslParameters);
+ }
+
+ istream = socket.getInputStream();
+ ostream = socket.getOutputStream();
+
+ sendHandshake();
+ } catch (/*IOException | SecurityException | UnresolvedAddressException | InvalidHandshakeException | ClosedByInterruptException | SocketTimeoutException */Exception e) {
+ onWebsocketError(engine, e);
+ engine.closeConnection(CloseFrame.NEVER_CONNECTED, e.getMessage());
+ return;
+ } catch (InternalError e) {
+ // https://bugs.openjdk.java.net/browse/JDK-8173620
+ if (e.getCause() instanceof InvocationTargetException && e.getCause()
+ .getCause() instanceof IOException) {
+ IOException cause = (IOException) e.getCause().getCause();
+ onWebsocketError(engine, cause);
+ engine.closeConnection(CloseFrame.NEVER_CONNECTED, cause.getMessage());
+ return;
+ }
+ throw e;
+ }
+
+ if (writeThread != null) {
+ writeThread.interrupt();
+ try {
+ writeThread.join();
+ } catch (InterruptedException e) {
+ /* ignore */
+ }
+ }
+ writeThread = new Thread(new WebsocketWriteThread(this));
+ writeThread.setDaemon(isDaemon());
+ writeThread.start();
+
+ int receiveBufferSize = getReceiveBufferSize();
+ byte[] rawbuffer = new byte[receiveBufferSize > 0 ? receiveBufferSize : DEFAULT_READ_BUFFER_SIZE];
+ int readBytes;
+
+ try {
+ while (!isClosing() && !isClosed() && (readBytes = istream.read(rawbuffer)) != -1) {
+ engine.decode(ByteBuffer.wrap(rawbuffer, 0, readBytes));
+ }
+ engine.eot();
+ } catch (IOException e) {
+ handleIOException(e);
+ } catch (RuntimeException e) {
+ // this catch case covers internal errors only and indicates a bug in this websocket implementation
+ onError(e);
+ engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
+ }
+ }
+
+ private void upgradeSocketToSSL()
+ throws NoSuchAlgorithmException, KeyManagementException, IOException {
+ SSLSocketFactory factory;
+ // Prioritise the provided socketfactory
+ // Helps when using web debuggers like Fiddler Classic
+ if (socketFactory instanceof SSLSocketFactory) {
+ factory = (SSLSocketFactory) socketFactory;
+ } else {
+ factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+ }
+ socket = factory.createSocket(socket, uri.getHost(), getPort(), true);
+ }
+
+ private boolean prepareSocket() throws IOException {
+ boolean upgradeSocketToSSLSocket = false;
+ // Prioritise a proxy over a socket factory and apply the socketfactory later
+ if (proxy != Proxy.NO_PROXY) {
+ socket = new Socket(proxy);
+ upgradeSocketToSSLSocket = true;
+ } else if (socketFactory != null) {
+ socket = socketFactory.createSocket();
+ } else if (socket == null) {
+ socket = new Socket(proxy);
+ upgradeSocketToSSLSocket = true;
+ } else if (socket.isClosed()) {
+ throw new IOException();
+ }
+ return upgradeSocketToSSLSocket;
+ }
+
+ /**
+ * Apply specific SSLParameters If you override this method make sure to always call
+ * super.onSetSSLParameters() to ensure the hostname validation is active
+ *
+ * @param sslParameters the SSLParameters which will be used for the SSLSocket
+ */
+ protected void onSetSSLParameters(SSLParameters sslParameters) {
+ // If you run into problem on Android (NoSuchMethodException), check out the wiki https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm
+ // Perform hostname validation
+ sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+ }
+
+ /**
+ * Extract the specified port
+ *
+ * @return the specified port or the default port for the specific scheme
+ */
+ private int getPort() {
+ int port = uri.getPort();
+ String scheme = uri.getScheme();
+ if ("wss".equals(scheme)) {
+ return port == -1 ? WebSocketImpl.DEFAULT_WSS_PORT : port;
+ } else if ("ws".equals(scheme)) {
+ return port == -1 ? WebSocketImpl.DEFAULT_PORT : port;
+ } else {
+ throw new IllegalArgumentException("unknown scheme: " + scheme);
+ }
+ }
+
+ /**
+ * Create and send the handshake to the other endpoint
+ *
+ * @throws InvalidHandshakeException a invalid handshake was created
+ */
+ private void sendHandshake() throws InvalidHandshakeException {
+ String path;
+ String part1 = uri.getRawPath();
+ String part2 = uri.getRawQuery();
+ if (part1 == null || part1.length() == 0) {
+ path = "/";
+ } else {
+ path = part1;
+ }
+ if (part2 != null) {
+ path += '?' + part2;
+ }
+ int port = getPort();
+ String host = uri.getHost() + (
+ (port != WebSocketImpl.DEFAULT_PORT && port != WebSocketImpl.DEFAULT_WSS_PORT)
+ ? ":" + port
+ : "");
+
+ HandshakeImpl1Client handshake = new HandshakeImpl1Client();
+ handshake.setResourceDescriptor(path);
+ handshake.put("Host", host);
+ if (headers != null) {
+ for (Map.Entry kv : headers.entrySet()) {
+ handshake.put(kv.getKey(), kv.getValue());
+ }
+ }
+ engine.startHandshake(handshake);
+ }
+
+ /**
+ * This represents the state of the connection.
+ */
+ public ReadyState getReadyState() {
+ return engine.getReadyState();
+ }
+
+ /**
+ * Calls subclass' implementation of onMessage.
+ */
+ @Override
+ public final void onWebsocketMessage(WebSocket conn, String message) {
+ onMessage(message);
+ }
+
+ @Override
+ public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) {
+ onMessage(blob);
+ }
+
+ /**
+ * Calls subclass' implementation of onOpen.
+ */
+ @Override
+ public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) {
+ startConnectionLostTimer();
+ onOpen((ServerHandshake) handshake);
+ connectLatch.countDown();
+ }
+
+ /**
+ * Calls subclass' implementation of onClose.
+ */
+ @Override
+ public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) {
+ stopConnectionLostTimer();
+ if (writeThread != null) {
+ writeThread.interrupt();
+ }
+ onClose(code, reason, remote);
+ connectLatch.countDown();
+ closeLatch.countDown();
+ }
+
+ /**
+ * Calls subclass' implementation of onIOError.
+ */
+ @Override
+ public final void onWebsocketError(WebSocket conn, Exception ex) {
+ onError(ex);
+ }
+
+ @Override
+ public final void onWriteDemand(WebSocket conn) {
+ // nothing to do
+ }
+
+ @Override
+ public void onWebsocketCloseInitiated(WebSocket conn, int code, String reason) {
+ onCloseInitiated(code, reason);
+ }
+
+ @Override
+ public void onWebsocketClosing(WebSocket conn, int code, String reason, boolean remote) {
+ onClosing(code, reason, remote);
+ }
+
+ /**
+ * Send when this peer sends a close handshake
+ *
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ */
+ public void onCloseInitiated(int code, String reason) {
+ //To overwrite
+ }
+
+ /**
+ * Called as soon as no further frames are accepted
+ *
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ * @param remote Returns whether or not the closing of the connection was initiated by the remote
+ * host.
+ */
+ public void onClosing(int code, String reason, boolean remote) {
+ //To overwrite
+ }
+
+ /**
+ * Getter for the engine
+ *
+ * @return the engine
+ */
+ public WebSocket getConnection() {
+ return engine;
+ }
+
+ @Override
+ public InetSocketAddress getLocalSocketAddress(WebSocket conn) {
+ if (socket != null) {
+ return (InetSocketAddress) socket.getLocalSocketAddress();
+ }
+ return null;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteSocketAddress(WebSocket conn) {
+ if (socket != null) {
+ return (InetSocketAddress) socket.getRemoteSocketAddress();
+ }
+ return null;
+ }
+
+ // ABSTRACT METHODS /////////////////////////////////////////////////////////
+
+ /**
+ * Called after an opening handshake has been performed and the given websocket is ready to be
+ * written on.
+ *
+ * @param handshakedata The handshake of the websocket instance
+ */
+ public abstract void onOpen(ServerHandshake handshakedata);
+
+ /**
+ * Callback for string messages received from the remote host
+ *
+ * @param message The UTF-8 decoded message that was received.
+ * @see #onMessage(ByteBuffer)
+ **/
+ public abstract void onMessage(String message);
+
+ /**
+ * Called after the websocket connection has been closed.
+ *
+ * @param code The codes can be looked up here: {@link CloseFrame}
+ * @param reason Additional information string
+ * @param remote Returns whether or not the closing of the connection was initiated by the remote
+ * host.
+ **/
+ public abstract void onClose(int code, String reason, boolean remote);
+
+ /**
+ * Called when errors occurs. If an error causes the websocket connection to fail {@link
+ * #onClose(int, String, boolean)} will be called additionally. This method will be called
+ * primarily because of IO or protocol errors. If the given exception is an RuntimeException
+ * that probably means that you encountered a bug.
+ *
+ * @param ex The exception causing this error
+ **/
+ public abstract void onError(Exception ex);
+
+ /**
+ * Callback for binary messages received from the remote host
+ *
+ * @param bytes The binary message that was received.
+ * @see #onMessage(String)
+ **/
+ public void onMessage(ByteBuffer bytes) {
+ //To overwrite
+ }
+
+
+ private class WebsocketWriteThread implements Runnable {
+
+ private final WebSocketClient webSocketClient;
+
+ WebsocketWriteThread(WebSocketClient webSocketClient) {
+ this.webSocketClient = webSocketClient;
+ }
+
+ @Override
+ public void run() {
+ Thread.currentThread().setName("WebSocketWriteThread-" + Thread.currentThread().getId());
+ try {
+ runWriteData();
+ } catch (IOException e) {
+ handleIOException(e);
+ } finally {
+ closeSocket();
+ }
+ }
+
+ /**
+ * Write the data into the outstream
+ *
+ * @throws IOException if write or flush did not work
+ */
+ private void runWriteData() throws IOException {
+ try {
+ while (!Thread.interrupted()) {
+ ByteBuffer buffer = engine.outQueue.take();
+ ostream.write(buffer.array(), 0, buffer.limit());
+ ostream.flush();
+ }
+ } catch (InterruptedException e) {
+ for (ByteBuffer buffer : engine.outQueue) {
+ ostream.write(buffer.array(), 0, buffer.limit());
+ ostream.flush();
+ }
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Closing the socket
+ */
+ private void closeSocket() {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException ex) {
+ onWebsocketError(webSocketClient, ex);
+ }
+ }
+ }
+
+
+ /**
+ * Method to set a proxy for this connection
+ *
+ * @param proxy the proxy to use for this websocket client
+ */
+ public void setProxy(Proxy proxy) {
+ if (proxy == null) {
+ throw new IllegalArgumentException();
+ }
+ this.proxy = proxy;
+ }
+
+ /**
+ * Accepts bound and unbound sockets. This method must be called before connect.
+ * If the given socket is not yet bound it will be bound to the uri specified in the constructor.
+ *
+ * @param socket The socket which should be used for the connection
+ * @deprecated use setSocketFactory
+ */
+ @Deprecated
+ public void setSocket(Socket socket) {
+ if (this.socket != null) {
+ throw new IllegalStateException("socket has already been set");
+ }
+ this.socket = socket;
+ }
+
+ /**
+ * Accepts a SocketFactory. This method must be called before connect. The socket
+ * will be bound to the uri specified in the constructor.
+ *
+ * @param socketFactory The socket factory which should be used for the connection.
+ */
+ public void setSocketFactory(SocketFactory socketFactory) {
+ this.socketFactory = socketFactory;
+ }
+
+ @Override
+ public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) {
+ engine.sendFragmentedFrame(op, buffer, fin);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return engine.isOpen();
+ }
+
+ @Override
+ public boolean isFlushAndClose() {
+ return engine.isFlushAndClose();
+ }
+
+ @Override
+ public boolean isClosed() {
+ return engine.isClosed();
+ }
+
+ @Override
+ public boolean isClosing() {
+ return engine.isClosing();
+ }
+
+ @Override
+ public boolean hasBufferedData() {
+ return engine.hasBufferedData();
+ }
+
+ @Override
+ public void close(int code) {
+ engine.close(code);
+ }
+
+ @Override
+ public void close(int code, String message) {
+ engine.close(code, message);
+ }
+
+ @Override
+ public void closeConnection(int code, String message) {
+ engine.closeConnection(code, message);
+ }
+
+ @Override
+ public void send(ByteBuffer bytes) {
+ engine.send(bytes);
+ }
+
+ @Override
+ public void sendFrame(Framedata framedata) {
+ engine.sendFrame(framedata);
+ }
+
+ @Override
+ public void sendFrame(Collection frames) {
+ engine.sendFrame(frames);
+ }
+
+ @Override
+ public InetSocketAddress getLocalSocketAddress() {
+ return engine.getLocalSocketAddress();
+ }
+
+ @Override
+ public InetSocketAddress getRemoteSocketAddress() {
+ return engine.getRemoteSocketAddress();
+ }
+
+ @Override
+ public String getResourceDescriptor() {
+ return uri.getPath();
+ }
+
+ @Override
+ public boolean hasSSLSupport() {
+ return socket instanceof SSLSocket;
+ }
+
+ @Override
+ public SSLSession getSSLSession() {
+ if (!hasSSLSupport()) {
+ throw new IllegalArgumentException(
+ "This websocket uses ws instead of wss. No SSLSession available.");
+ }
+ return ((SSLSocket)socket).getSession();
+ }
+
+ @Override
+ public IProtocol getProtocol() {
+ return engine.getProtocol();
+ }
+
+ /**
+ * Method to give some additional info for specific IOExceptions
+ *
+ * @param e the IOException causing a eot.
+ */
+ private void handleIOException(IOException e) {
+ if (e instanceof SSLException) {
+ onError(e);
+ }
+ engine.eot();
+ }
+}
diff --git a/src/main/java/org/java_websocket/client/package-info.java b/src/main/java/org/java_websocket/client/package-info.java
new file mode 100644
index 0000000..e6d799d
--- /dev/null
+++ b/src/main/java/org/java_websocket/client/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all implementations in relation with the WebSocketClient.
+ */
+package org.java_websocket.client;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/drafts/Draft.java b/src/main/java/org/java_websocket/drafts/Draft.java
new file mode 100644
index 0000000..2cda1e5
--- /dev/null
+++ b/src/main/java/org/java_websocket/drafts/Draft.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.drafts;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.enums.CloseHandshakeType;
+import org.java_websocket.enums.HandshakeState;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.Role;
+import org.java_websocket.exceptions.IncompleteHandshakeException;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidHandshakeException;
+import org.java_websocket.framing.BinaryFrame;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.ContinuousFrame;
+import org.java_websocket.framing.DataFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.TextFrame;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.ClientHandshakeBuilder;
+import org.java_websocket.handshake.HandshakeBuilder;
+import org.java_websocket.handshake.HandshakeImpl1Client;
+import org.java_websocket.handshake.HandshakeImpl1Server;
+import org.java_websocket.handshake.Handshakedata;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.handshake.ServerHandshakeBuilder;
+import org.java_websocket.util.Charsetfunctions;
+
+/**
+ * Base class for everything of a websocket specification which is not common such as the way the
+ * handshake is read or frames are transferred.
+ **/
+public abstract class Draft {
+
+ /**
+ * In some cases the handshake will be parsed different depending on whether
+ */
+ protected Role role = null;
+
+ protected Opcode continuousFrameType = null;
+
+ public static ByteBuffer readLine(ByteBuffer buf) {
+ ByteBuffer sbuf = ByteBuffer.allocate(buf.remaining());
+ byte prev;
+ byte cur = '0';
+ while (buf.hasRemaining()) {
+ prev = cur;
+ cur = buf.get();
+ sbuf.put(cur);
+ if (prev == (byte) '\r' && cur == (byte) '\n') {
+ sbuf.limit(sbuf.position() - 2);
+ sbuf.position(0);
+ return sbuf;
+
+ }
+ }
+ // ensure that there wont be any bytes skipped
+ buf.position(buf.position() - sbuf.position());
+ return null;
+ }
+
+ public static String readStringLine(ByteBuffer buf) {
+ ByteBuffer b = readLine(buf);
+ return b == null ? null : Charsetfunctions.stringAscii(b.array(), 0, b.limit());
+ }
+
+ public static HandshakeBuilder translateHandshakeHttp(ByteBuffer buf, Role role)
+ throws InvalidHandshakeException {
+ HandshakeBuilder handshake;
+
+ String line = readStringLine(buf);
+ if (line == null) {
+ throw new IncompleteHandshakeException(buf.capacity() + 128);
+ }
+
+ String[] firstLineTokens = line.split(" ", 3);// eg. HTTP/1.1 101 Switching the Protocols
+ if (firstLineTokens.length != 3) {
+ throw new InvalidHandshakeException();
+ }
+ if (role == Role.CLIENT) {
+ handshake = translateHandshakeHttpClient(firstLineTokens, line);
+ } else {
+ handshake = translateHandshakeHttpServer(firstLineTokens, line);
+ }
+ line = readStringLine(buf);
+ while (line != null && line.length() > 0) {
+ String[] pair = line.split(":", 2);
+ if (pair.length != 2) {
+ throw new InvalidHandshakeException("not an http header");
+ }
+ // If the handshake contains already a specific key, append the new value
+ if (handshake.hasFieldValue(pair[0])) {
+ handshake.put(pair[0],
+ handshake.getFieldValue(pair[0]) + "; " + pair[1].replaceFirst("^ +", ""));
+ } else {
+ handshake.put(pair[0], pair[1].replaceFirst("^ +", ""));
+ }
+ line = readStringLine(buf);
+ }
+ if (line == null) {
+ throw new IncompleteHandshakeException();
+ }
+ return handshake;
+ }
+
+ /**
+ * Checking the handshake for the role as server
+ *
+ * @param firstLineTokens the token of the first line split as as an string array
+ * @param line the whole line
+ * @return a handshake
+ */
+ private static HandshakeBuilder translateHandshakeHttpServer(String[] firstLineTokens,
+ String line) throws InvalidHandshakeException {
+ // translating/parsing the request from the CLIENT
+ if (!"GET".equalsIgnoreCase(firstLineTokens[0])) {
+ throw new InvalidHandshakeException(String
+ .format("Invalid request method received: %s Status line: %s", firstLineTokens[0], line));
+ }
+ if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[2])) {
+ throw new InvalidHandshakeException(String
+ .format("Invalid status line received: %s Status line: %s", firstLineTokens[2], line));
+ }
+ ClientHandshakeBuilder clienthandshake = new HandshakeImpl1Client();
+ clienthandshake.setResourceDescriptor(firstLineTokens[1]);
+ return clienthandshake;
+ }
+
+ /**
+ * Checking the handshake for the role as client
+ *
+ * @param firstLineTokens the token of the first line split as as an string array
+ * @param line the whole line
+ * @return a handshake
+ */
+ private static HandshakeBuilder translateHandshakeHttpClient(String[] firstLineTokens,
+ String line) throws InvalidHandshakeException {
+ // translating/parsing the response from the SERVER
+ if (!"101".equals(firstLineTokens[1])) {
+ throw new InvalidHandshakeException(String
+ .format("Invalid status code received: %s Status line: %s", firstLineTokens[1], line));
+ }
+ if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[0])) {
+ throw new InvalidHandshakeException(String
+ .format("Invalid status line received: %s Status line: %s", firstLineTokens[0], line));
+ }
+ HandshakeBuilder handshake = new HandshakeImpl1Server();
+ ServerHandshakeBuilder serverhandshake = (ServerHandshakeBuilder) handshake;
+ serverhandshake.setHttpStatus(Short.parseShort(firstLineTokens[1]));
+ serverhandshake.setHttpStatusMessage(firstLineTokens[2]);
+ return handshake;
+ }
+
+ public abstract HandshakeState acceptHandshakeAsClient(ClientHandshake request,
+ ServerHandshake response) throws InvalidHandshakeException;
+
+ public abstract HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata)
+ throws InvalidHandshakeException;
+
+ protected boolean basicAccept(Handshakedata handshakedata) {
+ return handshakedata.getFieldValue("Upgrade").equalsIgnoreCase("websocket") && handshakedata
+ .getFieldValue("Connection").toLowerCase(Locale.ENGLISH).contains("upgrade");
+ }
+
+ public abstract ByteBuffer createBinaryFrame(Framedata framedata);
+
+ public abstract List createFrames(ByteBuffer binary, boolean mask);
+
+ public abstract List createFrames(String text, boolean mask);
+
+
+ /**
+ * Handle the frame specific to the draft
+ *
+ * @param webSocketImpl the websocketimpl used for this draft
+ * @param frame the frame which is supposed to be handled
+ * @throws InvalidDataException will be thrown on invalid data
+ */
+ public abstract void processFrame(WebSocketImpl webSocketImpl, Framedata frame)
+ throws InvalidDataException;
+
+ public List continuousFrame(Opcode op, ByteBuffer buffer, boolean fin) {
+ if (op != Opcode.BINARY && op != Opcode.TEXT) {
+ throw new IllegalArgumentException("Only Opcode.BINARY or Opcode.TEXT are allowed");
+ }
+ DataFrame bui = null;
+ if (continuousFrameType != null) {
+ bui = new ContinuousFrame();
+ } else {
+ continuousFrameType = op;
+ if (op == Opcode.BINARY) {
+ bui = new BinaryFrame();
+ } else if (op == Opcode.TEXT) {
+ bui = new TextFrame();
+ }
+ }
+ bui.setPayload(buffer);
+ bui.setFin(fin);
+ try {
+ bui.isValid();
+ } catch (InvalidDataException e) {
+ throw new IllegalArgumentException(
+ e); // can only happen when one builds close frames(Opcode.Close)
+ }
+ if (fin) {
+ continuousFrameType = null;
+ } else {
+ continuousFrameType = op;
+ }
+ return Collections.singletonList((Framedata) bui);
+ }
+
+ public abstract void reset();
+
+ /**
+ * @deprecated use createHandshake without the role
+ */
+ @Deprecated
+ public List createHandshake(Handshakedata handshakedata, Role ownrole) {
+ return createHandshake(handshakedata);
+ }
+
+ public List createHandshake(Handshakedata handshakedata) {
+ return createHandshake(handshakedata, true);
+ }
+
+ /**
+ * @deprecated use createHandshake without the role since it does not have any effect
+ */
+ @Deprecated
+ public List createHandshake(Handshakedata handshakedata, Role ownrole,
+ boolean withcontent) {
+ return createHandshake(handshakedata, withcontent);
+ }
+
+ public List createHandshake(Handshakedata handshakedata, boolean withcontent) {
+ StringBuilder bui = new StringBuilder(100);
+ if (handshakedata instanceof ClientHandshake) {
+ bui.append("GET ").append(((ClientHandshake) handshakedata).getResourceDescriptor())
+ .append(" HTTP/1.1");
+ } else if (handshakedata instanceof ServerHandshake) {
+ bui.append("HTTP/1.1 101 ").append(((ServerHandshake) handshakedata).getHttpStatusMessage());
+ } else {
+ throw new IllegalArgumentException("unknown role");
+ }
+ bui.append("\r\n");
+ Iterator it = handshakedata.iterateHttpFields();
+ while (it.hasNext()) {
+ String fieldname = it.next();
+ String fieldvalue = handshakedata.getFieldValue(fieldname);
+ bui.append(fieldname);
+ bui.append(": ");
+ bui.append(fieldvalue);
+ bui.append("\r\n");
+ }
+ bui.append("\r\n");
+ byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString());
+
+ byte[] content = withcontent ? handshakedata.getContent() : null;
+ ByteBuffer bytebuffer = ByteBuffer
+ .allocate((content == null ? 0 : content.length) + httpheader.length);
+ bytebuffer.put(httpheader);
+ if (content != null) {
+ bytebuffer.put(content);
+ }
+ bytebuffer.flip();
+ return Collections.singletonList(bytebuffer);
+ }
+
+ public abstract ClientHandshakeBuilder postProcessHandshakeRequestAsClient(
+ ClientHandshakeBuilder request) throws InvalidHandshakeException;
+
+ public abstract HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request,
+ ServerHandshakeBuilder response) throws InvalidHandshakeException;
+
+ public abstract List translateFrame(ByteBuffer buffer) throws InvalidDataException;
+
+ public abstract CloseHandshakeType getCloseHandshakeType();
+
+ /**
+ * Drafts must only be by one websocket at all. To prevent drafts to be used more than once the
+ * Websocket implementation should call this method in order to create a new usable version of a
+ * given draft instance. The copy can be safely used in conjunction with a new websocket
+ * connection.
+ *
+ * @return a copy of the draft
+ */
+ public abstract Draft copyInstance();
+
+ public Handshakedata translateHandshake(ByteBuffer buf) throws InvalidHandshakeException {
+ return translateHandshakeHttp(buf, role);
+ }
+
+ public int checkAlloc(int bytecount) throws InvalidDataException {
+ if (bytecount < 0) {
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Negative count");
+ }
+ return bytecount;
+ }
+
+ int readVersion(Handshakedata handshakedata) {
+ String vers = handshakedata.getFieldValue("Sec-WebSocket-Version");
+ if (vers.length() > 0) {
+ int v;
+ try {
+ v = Integer.parseInt(vers.trim());
+ return v;
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+ return -1;
+ }
+
+ public void setParseMode(Role role) {
+ this.role = role;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/drafts/Draft_6455.java b/src/main/java/org/java_websocket/drafts/Draft_6455.java
new file mode 100644
index 0000000..eb48799
--- /dev/null
+++ b/src/main/java/org/java_websocket/drafts/Draft_6455.java
@@ -0,0 +1,1213 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.drafts;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.enums.CloseHandshakeType;
+import org.java_websocket.enums.HandshakeState;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.enums.Role;
+import org.java_websocket.exceptions.IncompleteException;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+import org.java_websocket.exceptions.InvalidHandshakeException;
+import org.java_websocket.exceptions.LimitExceededException;
+import org.java_websocket.exceptions.NotSendableException;
+import org.java_websocket.extensions.DefaultExtension;
+import org.java_websocket.extensions.IExtension;
+import org.java_websocket.framing.BinaryFrame;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.FramedataImpl1;
+import org.java_websocket.framing.TextFrame;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.ClientHandshakeBuilder;
+import org.java_websocket.handshake.HandshakeBuilder;
+import org.java_websocket.handshake.ServerHandshake;
+import org.java_websocket.handshake.ServerHandshakeBuilder;
+import org.java_websocket.protocols.IProtocol;
+import org.java_websocket.protocols.Protocol;
+import org.java_websocket.util.Base64;
+import org.java_websocket.util.Charsetfunctions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation for the RFC 6455 websocket protocol This is the recommended class for your
+ * websocket connection
+ */
+public class Draft_6455 extends Draft {
+
+ /**
+ * Handshake specific field for the key
+ */
+ private static final String SEC_WEB_SOCKET_KEY = "Sec-WebSocket-Key";
+
+ /**
+ * Handshake specific field for the protocol
+ */
+ private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
+
+ /**
+ * Handshake specific field for the extension
+ */
+ private static final String SEC_WEB_SOCKET_EXTENSIONS = "Sec-WebSocket-Extensions";
+
+ /**
+ * Handshake specific field for the accept
+ */
+ private static final String SEC_WEB_SOCKET_ACCEPT = "Sec-WebSocket-Accept";
+
+ /**
+ * Handshake specific field for the upgrade
+ */
+ private static final String UPGRADE = "Upgrade";
+
+ /**
+ * Handshake specific field for the connection
+ */
+ private static final String CONNECTION = "Connection";
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(Draft_6455.class);
+
+ /**
+ * Attribute for the used extension in this draft
+ */
+ private IExtension negotiatedExtension = new DefaultExtension();
+
+ /**
+ * Attribute for the default extension
+ */
+ private IExtension defaultExtension = new DefaultExtension();
+
+ /**
+ * Attribute for all available extension in this draft
+ */
+ private List knownExtensions;
+
+ /**
+ * Current active extension used to decode messages
+ */
+ private IExtension currentDecodingExtension;
+
+ /**
+ * Attribute for the used protocol in this draft
+ */
+ private IProtocol protocol;
+
+ /**
+ * Attribute for all available protocols in this draft
+ */
+ private List knownProtocols;
+
+ /**
+ * Attribute for the current continuous frame
+ */
+ private Framedata currentContinuousFrame;
+
+ /**
+ * Attribute for the payload of the current continuous frame
+ */
+ private final List byteBufferList;
+
+ /**
+ * Attribute for the current incomplete frame
+ */
+ private ByteBuffer incompleteframe;
+
+ /**
+ * Attribute for the reusable random instance
+ */
+ private final SecureRandom reuseableRandom = new SecureRandom();
+
+ /**
+ * Attribute for the maximum allowed size of a frame
+ *
+ * @since 1.4.0
+ */
+ private int maxFrameSize;
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with default extensions
+ *
+ * @since 1.3.5
+ */
+ public Draft_6455() {
+ this(Collections.emptyList());
+ }
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with custom extensions
+ *
+ * @param inputExtension the extension which should be used for this draft
+ * @since 1.3.5
+ */
+ public Draft_6455(IExtension inputExtension) {
+ this(Collections.singletonList(inputExtension));
+ }
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with custom extensions
+ *
+ * @param inputExtensions the extensions which should be used for this draft
+ * @since 1.3.5
+ */
+ public Draft_6455(List inputExtensions) {
+ this(inputExtensions, Collections.singletonList(new Protocol("")));
+ }
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and
+ * protocols
+ *
+ * @param inputExtensions the extensions which should be used for this draft
+ * @param inputProtocols the protocols which should be used for this draft
+ * @since 1.3.7
+ */
+ public Draft_6455(List inputExtensions, List inputProtocols) {
+ this(inputExtensions, inputProtocols, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and
+ * protocols
+ *
+ * @param inputExtensions the extensions which should be used for this draft
+ * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded
+ * frames can be bigger)
+ * @since 1.4.0
+ */
+ public Draft_6455(List inputExtensions, int inputMaxFrameSize) {
+ this(inputExtensions, Collections.singletonList(new Protocol("")),
+ inputMaxFrameSize);
+ }
+
+ /**
+ * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and
+ * protocols
+ *
+ * @param inputExtensions the extensions which should be used for this draft
+ * @param inputProtocols the protocols which should be used for this draft
+ * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded
+ * frames can be bigger)
+ * @since 1.4.0
+ */
+ public Draft_6455(List inputExtensions, List inputProtocols,
+ int inputMaxFrameSize) {
+ if (inputExtensions == null || inputProtocols == null || inputMaxFrameSize < 1) {
+ throw new IllegalArgumentException();
+ }
+ knownExtensions = new ArrayList<>(inputExtensions.size());
+ knownProtocols = new ArrayList<>(inputProtocols.size());
+ boolean hasDefault = false;
+ byteBufferList = new ArrayList<>();
+ for (IExtension inputExtension : inputExtensions) {
+ if (inputExtension.getClass().equals(DefaultExtension.class)) {
+ hasDefault = true;
+ }
+ }
+ knownExtensions.addAll(inputExtensions);
+ //We always add the DefaultExtension to implement the normal RFC 6455 specification
+ if (!hasDefault) {
+ knownExtensions.add(this.knownExtensions.size(), negotiatedExtension);
+ }
+ knownProtocols.addAll(inputProtocols);
+ maxFrameSize = inputMaxFrameSize;
+ currentDecodingExtension = null;
+ }
+
+ @Override
+ public HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata)
+ throws InvalidHandshakeException {
+ int v = readVersion(handshakedata);
+ if (v != 13) {
+ log.trace("acceptHandshakeAsServer - Wrong websocket version.");
+ return HandshakeState.NOT_MATCHED;
+ }
+ HandshakeState extensionState = HandshakeState.NOT_MATCHED;
+ String requestedExtension = handshakedata.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS);
+ for (IExtension knownExtension : knownExtensions) {
+ if (knownExtension.acceptProvidedExtensionAsServer(requestedExtension)) {
+ negotiatedExtension = knownExtension;
+ extensionState = HandshakeState.MATCHED;
+ log.trace("acceptHandshakeAsServer - Matching extension found: {}", negotiatedExtension);
+ break;
+ }
+ }
+ HandshakeState protocolState = containsRequestedProtocol(
+ handshakedata.getFieldValue(SEC_WEB_SOCKET_PROTOCOL));
+ if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) {
+ return HandshakeState.MATCHED;
+ }
+ log.trace("acceptHandshakeAsServer - No matching extension or protocol found.");
+ return HandshakeState.NOT_MATCHED;
+ }
+
+ /**
+ * Check if the requested protocol is part of this draft
+ *
+ * @param requestedProtocol the requested protocol
+ * @return MATCHED if it is matched, otherwise NOT_MATCHED
+ */
+ private HandshakeState containsRequestedProtocol(String requestedProtocol) {
+ for (IProtocol knownProtocol : knownProtocols) {
+ if (knownProtocol.acceptProvidedProtocol(requestedProtocol)) {
+ protocol = knownProtocol;
+ log.trace("acceptHandshake - Matching protocol found: {}", protocol);
+ return HandshakeState.MATCHED;
+ }
+ }
+ return HandshakeState.NOT_MATCHED;
+ }
+
+ @Override
+ public HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response)
+ throws InvalidHandshakeException {
+ if (!basicAccept(response)) {
+ log.trace("acceptHandshakeAsClient - Missing/wrong upgrade or connection in handshake.");
+ return HandshakeState.NOT_MATCHED;
+ }
+ if (!request.hasFieldValue(SEC_WEB_SOCKET_KEY) || !response
+ .hasFieldValue(SEC_WEB_SOCKET_ACCEPT)) {
+ log.trace("acceptHandshakeAsClient - Missing Sec-WebSocket-Key or Sec-WebSocket-Accept");
+ return HandshakeState.NOT_MATCHED;
+ }
+
+ String seckeyAnswer = response.getFieldValue(SEC_WEB_SOCKET_ACCEPT);
+ String seckeyChallenge = request.getFieldValue(SEC_WEB_SOCKET_KEY);
+ seckeyChallenge = generateFinalKey(seckeyChallenge);
+
+ if (!seckeyChallenge.equals(seckeyAnswer)) {
+ log.trace("acceptHandshakeAsClient - Wrong key for Sec-WebSocket-Key.");
+ return HandshakeState.NOT_MATCHED;
+ }
+ HandshakeState extensionState = HandshakeState.NOT_MATCHED;
+ String requestedExtension = response.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS);
+ for (IExtension knownExtension : knownExtensions) {
+ if (knownExtension.acceptProvidedExtensionAsClient(requestedExtension)) {
+ negotiatedExtension = knownExtension;
+ extensionState = HandshakeState.MATCHED;
+ log.trace("acceptHandshakeAsClient - Matching extension found: {}", negotiatedExtension);
+ break;
+ }
+ }
+ HandshakeState protocolState = containsRequestedProtocol(
+ response.getFieldValue(SEC_WEB_SOCKET_PROTOCOL));
+ if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) {
+ return HandshakeState.MATCHED;
+ }
+ log.trace("acceptHandshakeAsClient - No matching extension or protocol found.");
+ return HandshakeState.NOT_MATCHED;
+ }
+
+ /**
+ * Getter for the extension which is used by this draft
+ *
+ * @return the extension which is used or null, if handshake is not yet done
+ */
+ public IExtension getExtension() {
+ return negotiatedExtension;
+ }
+
+ /**
+ * Getter for all available extensions for this draft
+ *
+ * @return the extensions which are enabled for this draft
+ */
+ public List getKnownExtensions() {
+ return knownExtensions;
+ }
+
+ /**
+ * Getter for the protocol which is used by this draft
+ *
+ * @return the protocol which is used or null, if handshake is not yet done or no valid protocols
+ * @since 1.3.7
+ */
+ public IProtocol getProtocol() {
+ return protocol;
+ }
+
+
+ /**
+ * Getter for the maximum allowed payload size which is used by this draft
+ *
+ * @return the size, which is allowed for the payload
+ * @since 1.4.0
+ */
+ public int getMaxFrameSize() {
+ return maxFrameSize;
+ }
+
+ /**
+ * Getter for all available protocols for this draft
+ *
+ * @return the protocols which are enabled for this draft
+ * @since 1.3.7
+ */
+ public List getKnownProtocols() {
+ return knownProtocols;
+ }
+
+ @Override
+ public ClientHandshakeBuilder postProcessHandshakeRequestAsClient(
+ ClientHandshakeBuilder request) {
+ request.put(UPGRADE, "websocket");
+ request.put(CONNECTION, UPGRADE); // to respond to a Connection keep alives
+ byte[] random = new byte[16];
+ reuseableRandom.nextBytes(random);
+ request.put(SEC_WEB_SOCKET_KEY, Base64.encodeBytes(random));
+ request.put("Sec-WebSocket-Version", "13");// overwriting the previous
+ StringBuilder requestedExtensions = new StringBuilder();
+ for (IExtension knownExtension : knownExtensions) {
+ if (knownExtension.getProvidedExtensionAsClient() != null
+ && knownExtension.getProvidedExtensionAsClient().length() != 0) {
+ if (requestedExtensions.length() > 0) {
+ requestedExtensions.append(", ");
+ }
+ requestedExtensions.append(knownExtension.getProvidedExtensionAsClient());
+ }
+ }
+ if (requestedExtensions.length() != 0) {
+ request.put(SEC_WEB_SOCKET_EXTENSIONS, requestedExtensions.toString());
+ }
+ StringBuilder requestedProtocols = new StringBuilder();
+ for (IProtocol knownProtocol : knownProtocols) {
+ if (knownProtocol.getProvidedProtocol().length() != 0) {
+ if (requestedProtocols.length() > 0) {
+ requestedProtocols.append(", ");
+ }
+ requestedProtocols.append(knownProtocol.getProvidedProtocol());
+ }
+ }
+ if (requestedProtocols.length() != 0) {
+ request.put(SEC_WEB_SOCKET_PROTOCOL, requestedProtocols.toString());
+ }
+ return request;
+ }
+
+ @Override
+ public HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request,
+ ServerHandshakeBuilder response) throws InvalidHandshakeException {
+ response.put(UPGRADE, "websocket");
+ response.put(CONNECTION,
+ request.getFieldValue(CONNECTION)); // to respond to a Connection keep alives
+ String seckey = request.getFieldValue(SEC_WEB_SOCKET_KEY);
+ if (seckey == null || "".equals(seckey)) {
+ throw new InvalidHandshakeException("missing Sec-WebSocket-Key");
+ }
+ response.put(SEC_WEB_SOCKET_ACCEPT, generateFinalKey(seckey));
+ if (getExtension().getProvidedExtensionAsServer().length() != 0) {
+ response.put(SEC_WEB_SOCKET_EXTENSIONS, getExtension().getProvidedExtensionAsServer());
+ }
+ if (getProtocol() != null && getProtocol().getProvidedProtocol().length() != 0) {
+ response.put(SEC_WEB_SOCKET_PROTOCOL, getProtocol().getProvidedProtocol());
+ }
+ response.setHttpStatusMessage("Web Socket Protocol Handshake");
+ response.put("Server", "TooTallNate Java-WebSocket");
+ response.put("Date", getServerTime());
+ return response;
+ }
+
+ @Override
+ public Draft copyInstance() {
+ ArrayList newExtensions = new ArrayList<>();
+ for (IExtension knownExtension : getKnownExtensions()) {
+ newExtensions.add(knownExtension.copyInstance());
+ }
+ ArrayList newProtocols = new ArrayList<>();
+ for (IProtocol knownProtocol : getKnownProtocols()) {
+ newProtocols.add(knownProtocol.copyInstance());
+ }
+ return new Draft_6455(newExtensions, newProtocols, maxFrameSize);
+ }
+
+ @Override
+ public ByteBuffer createBinaryFrame(Framedata framedata) {
+ getExtension().encodeFrame(framedata);
+ if (log.isTraceEnabled()) {
+ log.trace("afterEnconding({}): {}", framedata.getPayloadData().remaining(),
+ (framedata.getPayloadData().remaining() > 1000 ? "too big to display"
+ : new String(framedata.getPayloadData().array())));
+ }
+ return createByteBufferFromFramedata(framedata);
+ }
+
+ private ByteBuffer createByteBufferFromFramedata(Framedata framedata) {
+ ByteBuffer mes = framedata.getPayloadData();
+ boolean mask = role == Role.CLIENT;
+ int sizebytes = getSizeBytes(mes);
+ ByteBuffer buf = ByteBuffer.allocate(
+ 1 + (sizebytes > 1 ? sizebytes + 1 : sizebytes) + (mask ? 4 : 0) + mes.remaining());
+ byte optcode = fromOpcode(framedata.getOpcode());
+ byte one = (byte) (framedata.isFin() ? -128 : 0);
+ one |= optcode;
+ if (framedata.isRSV1()) {
+ one |= getRSVByte(1);
+ }
+ if (framedata.isRSV2()) {
+ one |= getRSVByte(2);
+ }
+ if (framedata.isRSV3()) {
+ one |= getRSVByte(3);
+ }
+ buf.put(one);
+ byte[] payloadlengthbytes = toByteArray(mes.remaining(), sizebytes);
+ assert (payloadlengthbytes.length == sizebytes);
+
+ if (sizebytes == 1) {
+ buf.put((byte) (payloadlengthbytes[0] | getMaskByte(mask)));
+ } else if (sizebytes == 2) {
+ buf.put((byte) ((byte) 126 | getMaskByte(mask)));
+ buf.put(payloadlengthbytes);
+ } else if (sizebytes == 8) {
+ buf.put((byte) ((byte) 127 | getMaskByte(mask)));
+ buf.put(payloadlengthbytes);
+ } else {
+ throw new IllegalStateException("Size representation not supported/specified");
+ }
+ if (mask) {
+ ByteBuffer maskkey = ByteBuffer.allocate(4);
+ maskkey.putInt(reuseableRandom.nextInt());
+ buf.put(maskkey.array());
+ for (int i = 0; mes.hasRemaining(); i++) {
+ buf.put((byte) (mes.get() ^ maskkey.get(i % 4)));
+ }
+ } else {
+ buf.put(mes);
+ //Reset the position of the bytebuffer e.g. for additional use
+ mes.flip();
+ }
+ assert (buf.remaining() == 0) : buf.remaining();
+ buf.flip();
+ return buf;
+ }
+
+ private Framedata translateSingleFrame(ByteBuffer buffer)
+ throws IncompleteException, InvalidDataException {
+ if (buffer == null) {
+ throw new IllegalArgumentException();
+ }
+ int maxpacketsize = buffer.remaining();
+ int realpacketsize = 2;
+ translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize);
+ byte b1 = buffer.get(/*0*/);
+ boolean fin = b1 >> 8 != 0;
+ boolean rsv1 = (b1 & 0x40) != 0;
+ boolean rsv2 = (b1 & 0x20) != 0;
+ boolean rsv3 = (b1 & 0x10) != 0;
+ byte b2 = buffer.get(/*1*/);
+ boolean mask = (b2 & -128) != 0;
+ int payloadlength = (byte) (b2 & ~(byte) 128);
+ Opcode optcode = toOpcode((byte) (b1 & 15));
+
+ if (!(payloadlength >= 0 && payloadlength <= 125)) {
+ TranslatedPayloadMetaData payloadData = translateSingleFramePayloadLength(buffer, optcode,
+ payloadlength, maxpacketsize, realpacketsize);
+ payloadlength = payloadData.getPayloadLength();
+ realpacketsize = payloadData.getRealPackageSize();
+ }
+ translateSingleFrameCheckLengthLimit(payloadlength);
+ realpacketsize += (mask ? 4 : 0);
+ realpacketsize += payloadlength;
+ translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize);
+
+ ByteBuffer payload = ByteBuffer.allocate(checkAlloc(payloadlength));
+ if (mask) {
+ byte[] maskskey = new byte[4];
+ buffer.get(maskskey);
+ for (int i = 0; i < payloadlength; i++) {
+ payload.put((byte) (buffer.get(/*payloadstart + i*/) ^ maskskey[i % 4]));
+ }
+ } else {
+ payload.put(buffer.array(), buffer.position(), payload.limit());
+ buffer.position(buffer.position() + payload.limit());
+ }
+
+ FramedataImpl1 frame = FramedataImpl1.get(optcode);
+ frame.setFin(fin);
+ frame.setRSV1(rsv1);
+ frame.setRSV2(rsv2);
+ frame.setRSV3(rsv3);
+ payload.flip();
+ frame.setPayload(payload);
+ if (frame.getOpcode() != Opcode.CONTINUOUS) {
+ // Prioritize the negotiated extension
+ if (frame.isRSV1() || frame.isRSV2() || frame.isRSV3()) {
+ currentDecodingExtension = getExtension();
+ } else {
+ // No encoded message, so we can use the default one
+ currentDecodingExtension = defaultExtension;
+ }
+ }
+ if (currentDecodingExtension == null) {
+ currentDecodingExtension = defaultExtension;
+ }
+ currentDecodingExtension.isFrameValid(frame);
+ currentDecodingExtension.decodeFrame(frame);
+ if (log.isTraceEnabled()) {
+ log.trace("afterDecoding({}): {}", frame.getPayloadData().remaining(),
+ (frame.getPayloadData().remaining() > 1000 ? "too big to display"
+ : new String(frame.getPayloadData().array())));
+ }
+ frame.isValid();
+ return frame;
+ }
+
+ /**
+ * Translate the buffer depending when it has an extended payload length (126 or 127)
+ *
+ * @param buffer the buffer to read from
+ * @param optcode the decoded optcode
+ * @param oldPayloadlength the old payload length
+ * @param maxpacketsize the max packet size allowed
+ * @param oldRealpacketsize the real packet size
+ * @return the new payload data containing new payload length and new packet size
+ * @throws InvalidFrameException thrown if a control frame has an invalid length
+ * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize
+ * @throws LimitExceededException if the payload length is to big
+ */
+ private TranslatedPayloadMetaData translateSingleFramePayloadLength(ByteBuffer buffer,
+ Opcode optcode, int oldPayloadlength, int maxpacketsize, int oldRealpacketsize)
+ throws InvalidFrameException, IncompleteException, LimitExceededException {
+ int payloadlength = oldPayloadlength;
+ int realpacketsize = oldRealpacketsize;
+ if (optcode == Opcode.PING || optcode == Opcode.PONG || optcode == Opcode.CLOSING) {
+ log.trace("Invalid frame: more than 125 octets");
+ throw new InvalidFrameException("more than 125 octets");
+ }
+ if (payloadlength == 126) {
+ realpacketsize += 2; // additional length bytes
+ translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize);
+ byte[] sizebytes = new byte[3];
+ sizebytes[1] = buffer.get(/*1 + 1*/);
+ sizebytes[2] = buffer.get(/*1 + 2*/);
+ payloadlength = new BigInteger(sizebytes).intValue();
+ } else {
+ realpacketsize += 8; // additional length bytes
+ translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize);
+ byte[] bytes = new byte[8];
+ for (int i = 0; i < 8; i++) {
+ bytes[i] = buffer.get(/*1 + i*/);
+ }
+ long length = new BigInteger(bytes).longValue();
+ translateSingleFrameCheckLengthLimit(length);
+ payloadlength = (int) length;
+ }
+ return new TranslatedPayloadMetaData(payloadlength, realpacketsize);
+ }
+
+ /**
+ * Check if the frame size exceeds the allowed limit
+ *
+ * @param length the current payload length
+ * @throws LimitExceededException if the payload length is to big
+ */
+ private void translateSingleFrameCheckLengthLimit(long length) throws LimitExceededException {
+ if (length > Integer.MAX_VALUE) {
+ log.trace("Limit exedeed: Payloadsize is to big...");
+ throw new LimitExceededException("Payloadsize is to big...");
+ }
+ if (length > maxFrameSize) {
+ log.trace("Payload limit reached. Allowed: {} Current: {}", maxFrameSize, length);
+ throw new LimitExceededException("Payload limit reached.", maxFrameSize);
+ }
+ if (length < 0) {
+ log.trace("Limit underflow: Payloadsize is to little...");
+ throw new LimitExceededException("Payloadsize is to little...");
+ }
+ }
+
+ /**
+ * Check if the max packet size is smaller than the real packet size
+ *
+ * @param maxpacketsize the max packet size
+ * @param realpacketsize the real packet size
+ * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize
+ */
+ private void translateSingleFrameCheckPacketSize(int maxpacketsize, int realpacketsize)
+ throws IncompleteException {
+ if (maxpacketsize < realpacketsize) {
+ log.trace("Incomplete frame: maxpacketsize < realpacketsize");
+ throw new IncompleteException(realpacketsize);
+ }
+ }
+
+ /**
+ * Get a byte that can set RSV bits when OR(|)'d. 0 1 2 3 4 5 6 7 +-+-+-+-+-------+ |F|R|R|R|
+ * opcode| |I|S|S|S| (4) | |N|V|V|V| | | |1|2|3| |
+ *
+ * @param rsv Can only be {0, 1, 2, 3}
+ * @return byte that represents which RSV bit is set.
+ */
+ private byte getRSVByte(int rsv) {
+ switch (rsv) {
+ case 1 : // 0100 0000
+ return 0x40;
+ case 2 : // 0010 0000
+ return 0x20;
+ case 3 : // 0001 0000
+ return 0x10;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Get the mask byte if existing
+ *
+ * @param mask is mask active or not
+ * @return -128 for true, 0 for false
+ */
+ private byte getMaskByte(boolean mask) {
+ return mask ? (byte) -128 : 0;
+ }
+
+ /**
+ * Get the size bytes for the byte buffer
+ *
+ * @param mes the current buffer
+ * @return the size bytes
+ */
+ private int getSizeBytes(ByteBuffer mes) {
+ if (mes.remaining() <= 125) {
+ return 1;
+ } else if (mes.remaining() <= 65535) {
+ return 2;
+ }
+ return 8;
+ }
+
+ @Override
+ public List translateFrame(ByteBuffer buffer) throws InvalidDataException {
+ while (true) {
+ List frames = new LinkedList<>();
+ Framedata cur;
+ if (incompleteframe != null) {
+ // complete an incomplete frame
+ try {
+ buffer.mark();
+ int availableNextByteCount = buffer.remaining();// The number of bytes received
+ int expectedNextByteCount = incompleteframe
+ .remaining();// The number of bytes to complete the incomplete frame
+
+ if (expectedNextByteCount > availableNextByteCount) {
+ // did not receive enough bytes to complete the frame
+ incompleteframe.put(buffer.array(), buffer.position(), availableNextByteCount);
+ buffer.position(buffer.position() + availableNextByteCount);
+ return Collections.emptyList();
+ }
+ incompleteframe.put(buffer.array(), buffer.position(), expectedNextByteCount);
+ buffer.position(buffer.position() + expectedNextByteCount);
+ cur = translateSingleFrame((ByteBuffer) incompleteframe.duplicate().position(0));
+ frames.add(cur);
+ incompleteframe = null;
+ } catch (IncompleteException e) {
+ // extending as much as suggested
+ ByteBuffer extendedframe = ByteBuffer.allocate(checkAlloc(e.getPreferredSize()));
+ assert (extendedframe.limit() > incompleteframe.limit());
+ incompleteframe.rewind();
+ extendedframe.put(incompleteframe);
+ incompleteframe = extendedframe;
+ continue;
+ }
+ }
+
+ // Read as much as possible full frames
+ while (buffer.hasRemaining()) {
+ buffer.mark();
+ try {
+ cur = translateSingleFrame(buffer);
+ frames.add(cur);
+ } catch (IncompleteException e) {
+ // remember the incomplete data
+ buffer.reset();
+ int pref = e.getPreferredSize();
+ incompleteframe = ByteBuffer.allocate(checkAlloc(pref));
+ incompleteframe.put(buffer);
+ break;
+ }
+ }
+ return frames;
+ }
+ }
+
+ @Override
+ public List createFrames(ByteBuffer binary, boolean mask) {
+ BinaryFrame curframe = new BinaryFrame();
+ curframe.setPayload(binary);
+ curframe.setTransferemasked(mask);
+ try {
+ curframe.isValid();
+ } catch (InvalidDataException e) {
+ throw new NotSendableException(e);
+ }
+ return Collections.singletonList((Framedata) curframe);
+ }
+
+ @Override
+ public List createFrames(String text, boolean mask) {
+ TextFrame curframe = new TextFrame();
+ curframe.setPayload(ByteBuffer.wrap(Charsetfunctions.utf8Bytes(text)));
+ curframe.setTransferemasked(mask);
+ try {
+ curframe.isValid();
+ } catch (InvalidDataException e) {
+ throw new NotSendableException(e);
+ }
+ return Collections.singletonList((Framedata) curframe);
+ }
+
+ @Override
+ public void reset() {
+ incompleteframe = null;
+ if (negotiatedExtension != null) {
+ negotiatedExtension.reset();
+ }
+ negotiatedExtension = new DefaultExtension();
+ protocol = null;
+ }
+
+ /**
+ * Generate a date for for the date-header
+ *
+ * @return the server time
+ */
+ private String getServerTime() {
+ Calendar calendar = Calendar.getInstance();
+ SimpleDateFormat dateFormat = new SimpleDateFormat(
+ "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return dateFormat.format(calendar.getTime());
+ }
+
+ /**
+ * Generate a final key from a input string
+ *
+ * @param in the input string
+ * @return a final key
+ */
+ private String generateFinalKey(String in) {
+ String seckey = in.trim();
+ String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ MessageDigest sh1;
+ try {
+ sh1 = MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+ return Base64.encodeBytes(sh1.digest(acc.getBytes()));
+ }
+
+ private byte[] toByteArray(long val, int bytecount) {
+ byte[] buffer = new byte[bytecount];
+ int highest = 8 * bytecount - 8;
+ for (int i = 0; i < bytecount; i++) {
+ buffer[i] = (byte) (val >>> (highest - 8 * i));
+ }
+ return buffer;
+ }
+
+
+ private byte fromOpcode(Opcode opcode) {
+ if (opcode == Opcode.CONTINUOUS) {
+ return 0;
+ } else if (opcode == Opcode.TEXT) {
+ return 1;
+ } else if (opcode == Opcode.BINARY) {
+ return 2;
+ } else if (opcode == Opcode.CLOSING) {
+ return 8;
+ } else if (opcode == Opcode.PING) {
+ return 9;
+ } else if (opcode == Opcode.PONG) {
+ return 10;
+ }
+ throw new IllegalArgumentException("Don't know how to handle " + opcode.toString());
+ }
+
+ private Opcode toOpcode(byte opcode) throws InvalidFrameException {
+ switch (opcode) {
+ case 0:
+ return Opcode.CONTINUOUS;
+ case 1:
+ return Opcode.TEXT;
+ case 2:
+ return Opcode.BINARY;
+ // 3-7 are not yet defined
+ case 8:
+ return Opcode.CLOSING;
+ case 9:
+ return Opcode.PING;
+ case 10:
+ return Opcode.PONG;
+ // 11-15 are not yet defined
+ default:
+ throw new InvalidFrameException("Unknown opcode " + (short) opcode);
+ }
+ }
+
+ @Override
+ public void processFrame(WebSocketImpl webSocketImpl, Framedata frame)
+ throws InvalidDataException {
+ Opcode curop = frame.getOpcode();
+ if (curop == Opcode.CLOSING) {
+ processFrameClosing(webSocketImpl, frame);
+ } else if (curop == Opcode.PING) {
+ webSocketImpl.getWebSocketListener().onWebsocketPing(webSocketImpl, frame);
+ } else if (curop == Opcode.PONG) {
+ webSocketImpl.updateLastPong();
+ webSocketImpl.getWebSocketListener().onWebsocketPong(webSocketImpl, frame);
+ } else if (!frame.isFin() || curop == Opcode.CONTINUOUS) {
+ processFrameContinuousAndNonFin(webSocketImpl, frame, curop);
+ } else if (currentContinuousFrame != null) {
+ log.error("Protocol error: Continuous frame sequence not completed.");
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+ "Continuous frame sequence not completed.");
+ } else if (curop == Opcode.TEXT) {
+ processFrameText(webSocketImpl, frame);
+ } else if (curop == Opcode.BINARY) {
+ processFrameBinary(webSocketImpl, frame);
+ } else {
+ log.error("non control or continious frame expected");
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+ "non control or continious frame expected");
+ }
+ }
+
+ /**
+ * Process the frame if it is a continuous frame or the fin bit is not set
+ *
+ * @param webSocketImpl the websocket implementation to use
+ * @param frame the current frame
+ * @param curop the current Opcode
+ * @throws InvalidDataException if there is a protocol error
+ */
+ private void processFrameContinuousAndNonFin(WebSocketImpl webSocketImpl, Framedata frame,
+ Opcode curop) throws InvalidDataException {
+ if (curop != Opcode.CONTINUOUS) {
+ processFrameIsNotFin(frame);
+ } else if (frame.isFin()) {
+ processFrameIsFin(webSocketImpl, frame);
+ } else if (currentContinuousFrame == null) {
+ log.error("Protocol error: Continuous frame sequence was not started.");
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+ "Continuous frame sequence was not started.");
+ }
+ //Check if the whole payload is valid utf8, when the opcode indicates a text
+ if (curop == Opcode.TEXT && !Charsetfunctions.isValidUTF8(frame.getPayloadData())) {
+ log.error("Protocol error: Payload is not UTF8");
+ throw new InvalidDataException(CloseFrame.NO_UTF8);
+ }
+ //Checking if the current continuous frame contains a correct payload with the other frames combined
+ if (curop == Opcode.CONTINUOUS && currentContinuousFrame != null) {
+ addToBufferList(frame.getPayloadData());
+ }
+ }
+
+ /**
+ * Process the frame if it is a binary frame
+ *
+ * @param webSocketImpl the websocket impl
+ * @param frame the frame
+ */
+ private void processFrameBinary(WebSocketImpl webSocketImpl, Framedata frame) {
+ try {
+ webSocketImpl.getWebSocketListener()
+ .onWebsocketMessage(webSocketImpl, frame.getPayloadData());
+ } catch (RuntimeException e) {
+ logRuntimeException(webSocketImpl, e);
+ }
+ }
+
+ /**
+ * Log the runtime exception to the specific WebSocketImpl
+ *
+ * @param webSocketImpl the implementation of the websocket
+ * @param e the runtime exception
+ */
+ private void logRuntimeException(WebSocketImpl webSocketImpl, RuntimeException e) {
+ log.error("Runtime exception during onWebsocketMessage", e);
+ webSocketImpl.getWebSocketListener().onWebsocketError(webSocketImpl, e);
+ }
+
+ /**
+ * Process the frame if it is a text frame
+ *
+ * @param webSocketImpl the websocket impl
+ * @param frame the frame
+ */
+ private void processFrameText(WebSocketImpl webSocketImpl, Framedata frame)
+ throws InvalidDataException {
+ try {
+ webSocketImpl.getWebSocketListener()
+ .onWebsocketMessage(webSocketImpl, Charsetfunctions.stringUtf8(frame.getPayloadData()));
+ } catch (RuntimeException e) {
+ logRuntimeException(webSocketImpl, e);
+ }
+ }
+
+ /**
+ * Process the frame if it is the last frame
+ *
+ * @param webSocketImpl the websocket impl
+ * @param frame the frame
+ * @throws InvalidDataException if there is a protocol error
+ */
+ private void processFrameIsFin(WebSocketImpl webSocketImpl, Framedata frame)
+ throws InvalidDataException {
+ if (currentContinuousFrame == null) {
+ log.trace("Protocol error: Previous continuous frame sequence not completed.");
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+ "Continuous frame sequence was not started.");
+ }
+ addToBufferList(frame.getPayloadData());
+ checkBufferLimit();
+ if (currentContinuousFrame.getOpcode() == Opcode.TEXT) {
+ ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList());
+ ((FramedataImpl1) currentContinuousFrame).isValid();
+ try {
+ webSocketImpl.getWebSocketListener().onWebsocketMessage(webSocketImpl,
+ Charsetfunctions.stringUtf8(currentContinuousFrame.getPayloadData()));
+ } catch (RuntimeException e) {
+ logRuntimeException(webSocketImpl, e);
+ }
+ } else if (currentContinuousFrame.getOpcode() == Opcode.BINARY) {
+ ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList());
+ ((FramedataImpl1) currentContinuousFrame).isValid();
+ try {
+ webSocketImpl.getWebSocketListener()
+ .onWebsocketMessage(webSocketImpl, currentContinuousFrame.getPayloadData());
+ } catch (RuntimeException e) {
+ logRuntimeException(webSocketImpl, e);
+ }
+ }
+ currentContinuousFrame = null;
+ clearBufferList();
+ }
+
+ /**
+ * Process the frame if it is not the last frame
+ *
+ * @param frame the frame
+ * @throws InvalidDataException if there is a protocol error
+ */
+ private void processFrameIsNotFin(Framedata frame) throws InvalidDataException {
+ if (currentContinuousFrame != null) {
+ log.trace("Protocol error: Previous continuous frame sequence not completed.");
+ throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+ "Previous continuous frame sequence not completed.");
+ }
+ currentContinuousFrame = frame;
+ addToBufferList(frame.getPayloadData());
+ checkBufferLimit();
+ }
+
+ /**
+ * Process the frame if it is a closing frame
+ *
+ * @param webSocketImpl the websocket impl
+ * @param frame the frame
+ */
+ private void processFrameClosing(WebSocketImpl webSocketImpl, Framedata frame) {
+ int code = CloseFrame.NOCODE;
+ String reason = "";
+ if (frame instanceof CloseFrame) {
+ CloseFrame cf = (CloseFrame) frame;
+ code = cf.getCloseCode();
+ reason = cf.getMessage();
+ }
+ if (webSocketImpl.getReadyState() == ReadyState.CLOSING) {
+ // complete the close handshake by disconnecting
+ webSocketImpl.closeConnection(code, reason, true);
+ } else {
+ // echo close handshake
+ if (getCloseHandshakeType() == CloseHandshakeType.TWOWAY) {
+ webSocketImpl.close(code, reason, true);
+ } else {
+ webSocketImpl.flushAndClose(code, reason, false);
+ }
+ }
+ }
+
+ /**
+ * Clear the current bytebuffer list
+ */
+ private void clearBufferList() {
+ synchronized (byteBufferList) {
+ byteBufferList.clear();
+ }
+ }
+
+ /**
+ * Add a payload to the current bytebuffer list
+ *
+ * @param payloadData the new payload
+ */
+ private void addToBufferList(ByteBuffer payloadData) {
+ synchronized (byteBufferList) {
+ byteBufferList.add(payloadData);
+ }
+ }
+
+ /**
+ * Check the current size of the buffer and throw an exception if the size is bigger than the max
+ * allowed frame size
+ *
+ * @throws LimitExceededException if the current size is bigger than the allowed size
+ */
+ private void checkBufferLimit() throws LimitExceededException {
+ long totalSize = getByteBufferListSize();
+ if (totalSize > maxFrameSize) {
+ clearBufferList();
+ log.trace("Payload limit reached. Allowed: {} Current: {}", maxFrameSize, totalSize);
+ throw new LimitExceededException(maxFrameSize);
+ }
+ }
+
+ @Override
+ public CloseHandshakeType getCloseHandshakeType() {
+ return CloseHandshakeType.TWOWAY;
+ }
+
+ @Override
+ public String toString() {
+ String result = super.toString();
+ if (getExtension() != null) {
+ result += " extension: " + getExtension().toString();
+ }
+ if (getProtocol() != null) {
+ result += " protocol: " + getProtocol().toString();
+ }
+ result += " max frame size: " + this.maxFrameSize;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Draft_6455 that = (Draft_6455) o;
+
+ if (maxFrameSize != that.getMaxFrameSize()) {
+ return false;
+ }
+ if (negotiatedExtension != null ? !negotiatedExtension.equals(that.getExtension()) : that.getExtension() != null) {
+ return false;
+ }
+ return protocol != null ? protocol.equals(that.getProtocol()) : that.getProtocol() == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = negotiatedExtension != null ? negotiatedExtension.hashCode() : 0;
+ result = 31 * result + (protocol != null ? protocol.hashCode() : 0);
+ result = 31 * result + (maxFrameSize ^ (maxFrameSize >>> 32));
+ return result;
+ }
+
+ /**
+ * Method to generate a full bytebuffer out of all the fragmented frame payload
+ *
+ * @return a bytebuffer containing all the data
+ * @throws LimitExceededException will be thrown when the totalSize is bigger then
+ * Integer.MAX_VALUE due to not being able to allocate more
+ */
+ private ByteBuffer getPayloadFromByteBufferList() throws LimitExceededException {
+ long totalSize = 0;
+ ByteBuffer resultingByteBuffer;
+ synchronized (byteBufferList) {
+ for (ByteBuffer buffer : byteBufferList) {
+ totalSize += buffer.limit();
+ }
+ checkBufferLimit();
+ resultingByteBuffer = ByteBuffer.allocate((int) totalSize);
+ for (ByteBuffer buffer : byteBufferList) {
+ resultingByteBuffer.put(buffer);
+ }
+ }
+ resultingByteBuffer.flip();
+ return resultingByteBuffer;
+ }
+
+ /**
+ * Get the current size of the resulting bytebuffer in the bytebuffer list
+ *
+ * @return the size as long (to not get an integer overflow)
+ */
+ private long getByteBufferListSize() {
+ long totalSize = 0;
+ synchronized (byteBufferList) {
+ for (ByteBuffer buffer : byteBufferList) {
+ totalSize += buffer.limit();
+ }
+ }
+ return totalSize;
+ }
+
+ private class TranslatedPayloadMetaData {
+
+ private int payloadLength;
+ private int realPackageSize;
+
+ private int getPayloadLength() {
+ return payloadLength;
+ }
+
+ private int getRealPackageSize() {
+ return realPackageSize;
+ }
+
+ TranslatedPayloadMetaData(int newPayloadLength, int newRealPackageSize) {
+ this.payloadLength = newPayloadLength;
+ this.realPackageSize = newRealPackageSize;
+ }
+ }
+}
diff --git a/src/main/java/org/java_websocket/drafts/package-info.java b/src/main/java/org/java_websocket/drafts/package-info.java
new file mode 100644
index 0000000..1311429
--- /dev/null
+++ b/src/main/java/org/java_websocket/drafts/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all implementations in relation with the WebSocket drafts.
+ */
+package org.java_websocket.drafts;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/CloseHandshakeType.java b/src/main/java/org/java_websocket/enums/CloseHandshakeType.java
new file mode 100644
index 0000000..0bd1f94
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/CloseHandshakeType.java
@@ -0,0 +1,8 @@
+package org.java_websocket.enums;
+
+/**
+ * Enum which represents type of handshake is required for a close
+ */
+public enum CloseHandshakeType {
+ NONE, ONEWAY, TWOWAY
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/HandshakeState.java b/src/main/java/org/java_websocket/enums/HandshakeState.java
new file mode 100644
index 0000000..c877506
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/HandshakeState.java
@@ -0,0 +1,15 @@
+package org.java_websocket.enums;
+
+/**
+ * Enum which represents the states a handshake may be in
+ */
+public enum HandshakeState {
+ /**
+ * Handshake matched this Draft successfully
+ */
+ MATCHED,
+ /**
+ * Handshake is does not match this Draft
+ */
+ NOT_MATCHED
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/Opcode.java b/src/main/java/org/java_websocket/enums/Opcode.java
new file mode 100644
index 0000000..cc65e31
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/Opcode.java
@@ -0,0 +1,9 @@
+package org.java_websocket.enums;
+
+/**
+ * Enum which contains the different valid opcodes
+ */
+public enum Opcode {
+ CONTINUOUS, TEXT, BINARY, PING, PONG, CLOSING
+ // more to come
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/ReadyState.java b/src/main/java/org/java_websocket/enums/ReadyState.java
new file mode 100644
index 0000000..15bd5cc
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/ReadyState.java
@@ -0,0 +1,8 @@
+package org.java_websocket.enums;
+
+/**
+ * Enum which represents the state a websocket may be in
+ */
+public enum ReadyState {
+ NOT_YET_CONNECTED, OPEN, CLOSING, CLOSED
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/Role.java b/src/main/java/org/java_websocket/enums/Role.java
new file mode 100644
index 0000000..8a057a3
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/Role.java
@@ -0,0 +1,8 @@
+package org.java_websocket.enums;
+
+/**
+ * Enum which represents the states a websocket may be in
+ */
+public enum Role {
+ CLIENT, SERVER
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/enums/package-info.java b/src/main/java/org/java_websocket/enums/package-info.java
new file mode 100644
index 0000000..a5c997e
--- /dev/null
+++ b/src/main/java/org/java_websocket/enums/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all enums.
+ */
+package org.java_websocket.enums;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/exceptions/IncompleteException.java b/src/main/java/org/java_websocket/exceptions/IncompleteException.java
new file mode 100644
index 0000000..d3e8383
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/IncompleteException.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+/**
+ * Exception which indicates that the frame is not yet complete
+ */
+public class IncompleteException extends Exception {
+
+ /**
+ * It's Serializable.
+ */
+ private static final long serialVersionUID = 7330519489840500997L;
+
+ /**
+ * The preferred size
+ */
+ private final int preferredSize;
+
+ /**
+ * Constructor for the preferred size of a frame
+ *
+ * @param preferredSize the preferred size of a frame
+ */
+ public IncompleteException(int preferredSize) {
+ this.preferredSize = preferredSize;
+ }
+
+ /**
+ * Getter for the preferredSize
+ *
+ * @return the value of the preferred size
+ */
+ public int getPreferredSize() {
+ return preferredSize;
+ }
+}
diff --git a/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java b/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java
new file mode 100644
index 0000000..17307c3
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+/**
+ * exception which indicates that a incomplete handshake was received
+ */
+public class IncompleteHandshakeException extends RuntimeException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = 7906596804233893092L;
+
+ /**
+ * attribute which size of handshake would have been preferred
+ */
+ private final int preferredSize;
+
+ /**
+ * constructor for a IncompleteHandshakeException
+ *
+ *
+ * @param preferredSize the preferred size
+ */
+ public IncompleteHandshakeException(int preferredSize) {
+ this.preferredSize = preferredSize;
+ }
+
+ /**
+ * constructor for a IncompleteHandshakeException
+ *
+ * preferredSize will be 0
+ */
+ public IncompleteHandshakeException() {
+ this.preferredSize = 0;
+ }
+
+ /**
+ * Getter preferredSize
+ *
+ * @return the preferredSize
+ */
+ public int getPreferredSize() {
+ return preferredSize;
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/exceptions/InvalidDataException.java b/src/main/java/org/java_websocket/exceptions/InvalidDataException.java
new file mode 100644
index 0000000..c34c8c9
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/InvalidDataException.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+/**
+ * exception which indicates that a invalid data was received
+ */
+public class InvalidDataException extends Exception {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = 3731842424390998726L;
+
+ /**
+ * attribute which closecode will be returned
+ */
+ private final int closecode;
+
+ /**
+ * constructor for a InvalidDataException
+ *
+ * @param closecode the closecode which will be returned
+ */
+ public InvalidDataException(int closecode) {
+ this.closecode = closecode;
+ }
+
+ /**
+ * constructor for a InvalidDataException.
+ *
+ * @param closecode the closecode which will be returned.
+ * @param s the detail message.
+ */
+ public InvalidDataException(int closecode, String s) {
+ super(s);
+ this.closecode = closecode;
+ }
+
+ /**
+ * constructor for a InvalidDataException.
+ *
+ * @param closecode the closecode which will be returned.
+ * @param t the throwable causing this exception.
+ */
+ public InvalidDataException(int closecode, Throwable t) {
+ super(t);
+ this.closecode = closecode;
+ }
+
+ /**
+ * constructor for a InvalidDataException.
+ *
+ * @param closecode the closecode which will be returned.
+ * @param s the detail message.
+ * @param t the throwable causing this exception.
+ */
+ public InvalidDataException(int closecode, String s, Throwable t) {
+ super(s, t);
+ this.closecode = closecode;
+ }
+
+ /**
+ * Getter closecode
+ *
+ * @return the closecode
+ */
+ public int getCloseCode() {
+ return closecode;
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java b/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java
new file mode 100644
index 0000000..8fdbd1a
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java
@@ -0,0 +1,37 @@
+package org.java_websocket.exceptions;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * The Character Encoding is not supported.
+ *
+ * @since 1.4.0
+ */
+public class InvalidEncodingException extends RuntimeException {
+
+ /**
+ * attribute for the encoding exception
+ */
+ private final UnsupportedEncodingException encodingException;
+
+ /**
+ * constructor for InvalidEncodingException
+ *
+ * @param encodingException the cause for this exception
+ */
+ public InvalidEncodingException(UnsupportedEncodingException encodingException) {
+ if (encodingException == null) {
+ throw new IllegalArgumentException();
+ }
+ this.encodingException = encodingException;
+ }
+
+ /**
+ * Get the exception which includes more information on the unsupported encoding
+ *
+ * @return an UnsupportedEncodingException
+ */
+ public UnsupportedEncodingException getEncodingException() {
+ return encodingException;
+ }
+}
diff --git a/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java b/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java
new file mode 100644
index 0000000..7de2034
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+import org.java_websocket.framing.CloseFrame;
+
+/**
+ * exception which indicates that a invalid frame was received (CloseFrame.PROTOCOL_ERROR)
+ */
+public class InvalidFrameException extends InvalidDataException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = -9016496369828887591L;
+
+ /**
+ * constructor for a InvalidFrameException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ */
+ public InvalidFrameException() {
+ super(CloseFrame.PROTOCOL_ERROR);
+ }
+
+ /**
+ * constructor for a InvalidFrameException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param s the detail message.
+ */
+ public InvalidFrameException(String s) {
+ super(CloseFrame.PROTOCOL_ERROR, s);
+ }
+
+ /**
+ * constructor for a InvalidFrameException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param t the throwable causing this exception.
+ */
+ public InvalidFrameException(Throwable t) {
+ super(CloseFrame.PROTOCOL_ERROR, t);
+ }
+
+ /**
+ * constructor for a InvalidFrameException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param s the detail message.
+ * @param t the throwable causing this exception.
+ */
+ public InvalidFrameException(String s, Throwable t) {
+ super(CloseFrame.PROTOCOL_ERROR, s, t);
+ }
+}
diff --git a/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java b/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java
new file mode 100644
index 0000000..af1fd21
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+import org.java_websocket.framing.CloseFrame;
+
+/**
+ * exception which indicates that a invalid handshake was received (CloseFrame.PROTOCOL_ERROR)
+ */
+public class InvalidHandshakeException extends InvalidDataException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = -1426533877490484964L;
+
+ /**
+ * constructor for a InvalidHandshakeException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ */
+ public InvalidHandshakeException() {
+ super(CloseFrame.PROTOCOL_ERROR);
+ }
+
+ /**
+ * constructor for a InvalidHandshakeException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param s the detail message.
+ * @param t the throwable causing this exception.
+ */
+ public InvalidHandshakeException(String s, Throwable t) {
+ super(CloseFrame.PROTOCOL_ERROR, s, t);
+ }
+
+ /**
+ * constructor for a InvalidHandshakeException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param s the detail message.
+ */
+ public InvalidHandshakeException(String s) {
+ super(CloseFrame.PROTOCOL_ERROR, s);
+ }
+
+ /**
+ * constructor for a InvalidHandshakeException
+ *
+ * calling InvalidDataException with closecode PROTOCOL_ERROR
+ *
+ * @param t the throwable causing this exception.
+ */
+ public InvalidHandshakeException(Throwable t) {
+ super(CloseFrame.PROTOCOL_ERROR, t);
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/exceptions/LimitExceededException.java b/src/main/java/org/java_websocket/exceptions/LimitExceededException.java
new file mode 100644
index 0000000..0d4a818
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/LimitExceededException.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+import org.java_websocket.framing.CloseFrame;
+
+/**
+ * exception which indicates that the message limited was exceeded (CloseFrame.TOOBIG)
+ */
+public class LimitExceededException extends InvalidDataException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = 6908339749836826785L;
+
+ /**
+ * A closer indication about the limit
+ */
+ private final int limit;
+
+ /**
+ * constructor for a LimitExceededException
+ *
+ * calling LimitExceededException with closecode TOOBIG
+ */
+ public LimitExceededException() {
+ this(Integer.MAX_VALUE);
+ }
+
+ /**
+ * constructor for a LimitExceededException
+ *
+ * calling InvalidDataException with closecode TOOBIG
+ * @param limit the allowed size which was not enough
+ */
+ public LimitExceededException(int limit) {
+ super(CloseFrame.TOOBIG);
+ this.limit = limit;
+ }
+
+ /**
+ * constructor for a LimitExceededException
+ *
+ * calling InvalidDataException with closecode TOOBIG
+ * @param s the detail message.
+ * @param limit the allowed size which was not enough
+ */
+ public LimitExceededException(String s, int limit) {
+ super(CloseFrame.TOOBIG, s);
+ this.limit = limit;
+ }
+
+ /**
+ * constructor for a LimitExceededException
+ *
+ * calling InvalidDataException with closecode TOOBIG
+ *
+ * @param s the detail message.
+ */
+ public LimitExceededException(String s) {
+ this(s, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Get the limit which was hit so this exception was caused
+ *
+ * @return the limit as int
+ */
+ public int getLimit() {
+ return limit;
+ }
+}
diff --git a/src/main/java/org/java_websocket/exceptions/NotSendableException.java b/src/main/java/org/java_websocket/exceptions/NotSendableException.java
new file mode 100644
index 0000000..fbacca9
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/NotSendableException.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+/**
+ * exception which indicates the frame payload is not sendable
+ */
+public class NotSendableException extends RuntimeException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = -6468967874576651628L;
+
+ /**
+ * constructor for a NotSendableException
+ *
+ * @param s the detail message.
+ */
+ public NotSendableException(String s) {
+ super(s);
+ }
+
+ /**
+ * constructor for a NotSendableException
+ *
+ * @param t the throwable causing this exception.
+ */
+ public NotSendableException(Throwable t) {
+ super(t);
+ }
+
+ /**
+ * constructor for a NotSendableException
+ *
+ * @param s the detail message.
+ * @param t the throwable causing this exception.
+ */
+ public NotSendableException(String s, Throwable t) {
+ super(s, t);
+ }
+
+}
diff --git a/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java b/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java
new file mode 100644
index 0000000..082f0bd
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.exceptions;
+
+/**
+ * exception which indicates the websocket is not yet connected (ReadyState.OPEN)
+ */
+public class WebsocketNotConnectedException extends RuntimeException {
+
+ /**
+ * Serializable
+ */
+ private static final long serialVersionUID = -785314021592982715L;
+}
diff --git a/src/main/java/org/java_websocket/exceptions/WrappedIOException.java b/src/main/java/org/java_websocket/exceptions/WrappedIOException.java
new file mode 100644
index 0000000..3ec3177
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/WrappedIOException.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+package org.java_websocket.exceptions;
+
+import java.io.IOException;
+import org.java_websocket.WebSocket;
+
+/**
+ * Exception to wrap an IOException and include information about the websocket which had the
+ * exception
+ *
+ * @since 1.4.1
+ */
+public class WrappedIOException extends Exception {
+
+ /**
+ * The websocket where the IOException happened
+ */
+ private final transient WebSocket connection;
+
+ /**
+ * The IOException
+ */
+ private final IOException ioException;
+
+ /**
+ * Wrapp an IOException and include the websocket
+ *
+ * @param connection the websocket where the IOException happened
+ * @param ioException the IOException
+ */
+ public WrappedIOException(WebSocket connection, IOException ioException) {
+ this.connection = connection;
+ this.ioException = ioException;
+ }
+
+ /**
+ * The websocket where the IOException happened
+ *
+ * @return the websocket for the wrapped IOException
+ */
+ public WebSocket getConnection() {
+ return connection;
+ }
+
+ /**
+ * The wrapped IOException
+ *
+ * @return IOException which is wrapped
+ */
+ public IOException getIOException() {
+ return ioException;
+ }
+}
diff --git a/src/main/java/org/java_websocket/exceptions/package-info.java b/src/main/java/org/java_websocket/exceptions/package-info.java
new file mode 100644
index 0000000..2972d3c
--- /dev/null
+++ b/src/main/java/org/java_websocket/exceptions/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all implementations in relation with the exceptions thrown in this
+ * lib.
+ */
+package org.java_websocket.exceptions;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/extensions/CompressionExtension.java b/src/main/java/org/java_websocket/extensions/CompressionExtension.java
new file mode 100644
index 0000000..408a158
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/CompressionExtension.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.extensions;
+
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+import org.java_websocket.framing.ControlFrame;
+import org.java_websocket.framing.DataFrame;
+import org.java_websocket.framing.Framedata;
+
+/**
+ * Implementation for a compression extension specified by https://tools.ietf.org/html/rfc7692
+ *
+ * @since 1.3.5
+ */
+public abstract class CompressionExtension extends DefaultExtension {
+
+ @Override
+ public void isFrameValid(Framedata inputFrame) throws InvalidDataException {
+ if ((inputFrame instanceof DataFrame) && (inputFrame.isRSV2() || inputFrame.isRSV3())) {
+ throw new InvalidFrameException(
+ "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: "
+ + inputFrame.isRSV3());
+ }
+ if ((inputFrame instanceof ControlFrame) && (inputFrame.isRSV1() || inputFrame.isRSV2()
+ || inputFrame.isRSV3())) {
+ throw new InvalidFrameException(
+ "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: "
+ + inputFrame.isRSV3());
+ }
+ }
+}
diff --git a/src/main/java/org/java_websocket/extensions/DefaultExtension.java b/src/main/java/org/java_websocket/extensions/DefaultExtension.java
new file mode 100644
index 0000000..3892990
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/DefaultExtension.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.extensions;
+
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+import org.java_websocket.framing.Framedata;
+
+/**
+ * Class which represents the normal websocket implementation specified by rfc6455.
+ *
+ * This is a fallback and will always be available for a Draft_6455
+ *
+ * @since 1.3.5
+ */
+public class DefaultExtension implements IExtension {
+
+ @Override
+ public void decodeFrame(Framedata inputFrame) throws InvalidDataException {
+ //Nothing to do here
+ }
+
+ @Override
+ public void encodeFrame(Framedata inputFrame) {
+ //Nothing to do here
+ }
+
+ @Override
+ public boolean acceptProvidedExtensionAsServer(String inputExtension) {
+ return true;
+ }
+
+ @Override
+ public boolean acceptProvidedExtensionAsClient(String inputExtension) {
+ return true;
+ }
+
+ @Override
+ public void isFrameValid(Framedata inputFrame) throws InvalidDataException {
+ if (inputFrame.isRSV1() || inputFrame.isRSV2() || inputFrame.isRSV3()) {
+ throw new InvalidFrameException(
+ "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: "
+ + inputFrame.isRSV3());
+ }
+ }
+
+ @Override
+ public String getProvidedExtensionAsClient() {
+ return "";
+ }
+
+ @Override
+ public String getProvidedExtensionAsServer() {
+ return "";
+ }
+
+ @Override
+ public IExtension copyInstance() {
+ return new DefaultExtension();
+ }
+
+ public void reset() {
+ //Nothing to do here. No internal stats.
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || o != null && getClass() == o.getClass();
+ }
+}
diff --git a/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java b/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java
new file mode 100644
index 0000000..37bc9de
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java
@@ -0,0 +1,53 @@
+package org.java_websocket.extensions;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ExtensionRequestData {
+
+ public static final String EMPTY_VALUE = "";
+
+ private Map extensionParameters;
+ private String extensionName;
+
+ private ExtensionRequestData() {
+ extensionParameters = new LinkedHashMap<>();
+ }
+
+ public static ExtensionRequestData parseExtensionRequest(String extensionRequest) {
+ ExtensionRequestData extensionData = new ExtensionRequestData();
+ String[] parts = extensionRequest.split(";");
+ extensionData.extensionName = parts[0].trim();
+
+ for (int i = 1; i < parts.length; i++) {
+ String[] keyValue = parts[i].split("=");
+ String value = EMPTY_VALUE;
+
+ // Some parameters don't take a value. For those that do, parse the value.
+ if (keyValue.length > 1) {
+ String tempValue = keyValue[1].trim();
+
+ // If the value is wrapped in quotes, just get the data between them.
+ if ((tempValue.startsWith("\"") && tempValue.endsWith("\""))
+ || (tempValue.startsWith("'") && tempValue.endsWith("'"))
+ && tempValue.length() > 2) {
+ tempValue = tempValue.substring(1, tempValue.length() - 1);
+ }
+
+ value = tempValue;
+ }
+
+ extensionData.extensionParameters.put(keyValue[0].trim(), value);
+ }
+
+ return extensionData;
+ }
+
+ public String getExtensionName() {
+ return extensionName;
+ }
+
+ public Map getExtensionParameters() {
+ return extensionParameters;
+ }
+}
diff --git a/src/main/java/org/java_websocket/extensions/IExtension.java b/src/main/java/org/java_websocket/extensions/IExtension.java
new file mode 100644
index 0000000..02bf581
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/IExtension.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.extensions;
+
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.framing.Framedata;
+
+/**
+ * Interface which specifies all required methods to develop a websocket extension.
+ *
+ * @since 1.3.5
+ */
+public interface IExtension {
+
+ /**
+ * Decode a frame with a extension specific algorithm. The algorithm is subject to be implemented
+ * by the specific extension. The resulting frame will be used in the application
+ *
+ * @param inputFrame the frame, which has do be decoded to be used in the application
+ * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly
+ * implemented by the other endpoint or there are other protocol
+ * errors/decoding errors
+ * @since 1.3.5
+ */
+ void decodeFrame(Framedata inputFrame) throws InvalidDataException;
+
+ /**
+ * Encode a frame with a extension specific algorithm. The algorithm is subject to be implemented
+ * by the specific extension. The resulting frame will be send to the other endpoint.
+ *
+ * @param inputFrame the frame, which has do be encoded to be used on the other endpoint
+ * @since 1.3.5
+ */
+ void encodeFrame(Framedata inputFrame);
+
+ /**
+ * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific
+ * extension if the endpoint is in the role of a server
+ *
+ * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the
+ * other endpoint
+ * @return true, if the offer does fit to this specific extension
+ * @since 1.3.5
+ */
+ boolean acceptProvidedExtensionAsServer(String inputExtensionHeader);
+
+ /**
+ * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific
+ * extension if the endpoint is in the role of a client
+ *
+ * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the
+ * other endpoint
+ * @return true, if the offer does fit to this specific extension
+ * @since 1.3.5
+ */
+ boolean acceptProvidedExtensionAsClient(String inputExtensionHeader);
+
+ /**
+ * Check if the received frame is correctly implemented by the other endpoint and there are no
+ * specification errors (like wrongly set RSV)
+ *
+ * @param inputFrame the received frame
+ * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly
+ * implementing the specification for the specific extension
+ * @since 1.3.5
+ */
+ void isFrameValid(Framedata inputFrame) throws InvalidDataException;
+
+ /**
+ * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is
+ * in the role of a client. If the extension returns an empty string (""), the offer will not be
+ * included in the handshake.
+ *
+ * @return the specific Sec-WebSocket-Extensions header for this extension
+ * @since 1.3.5
+ */
+ String getProvidedExtensionAsClient();
+
+ /**
+ * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is
+ * in the role of a server. If the extension returns an empty string (""), the offer will not be
+ * included in the handshake.
+ *
+ * @return the specific Sec-WebSocket-Extensions header for this extension
+ * @since 1.3.5
+ */
+ String getProvidedExtensionAsServer();
+
+ /**
+ * Extensions must only be by one websocket at all. To prevent extensions to be used more than
+ * once the Websocket implementation should call this method in order to create a new usable
+ * version of a given extension instance. The copy can be safely used in conjunction with a
+ * new websocket connection.
+ *
+ * @return a copy of the extension
+ * @since 1.3.5
+ */
+ IExtension copyInstance();
+
+ /**
+ * Cleaning up internal stats when the draft gets reset.
+ *
+ * @since 1.3.5
+ */
+ void reset();
+
+ /**
+ * Return a string which should contain the class name as well as additional information about the
+ * current configurations for this extension (DEBUG purposes)
+ *
+ * @return a string containing the class name as well as additional information
+ * @since 1.3.5
+ */
+ String toString();
+}
diff --git a/src/main/java/org/java_websocket/extensions/package-info.java b/src/main/java/org/java_websocket/extensions/package-info.java
new file mode 100644
index 0000000..251cbdf
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all interfaces and implementations in relation with the WebSocket
+ * Sec-WebSocket-Extensions.
+ */
+package org.java_websocket.extensions;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java b/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java
new file mode 100644
index 0000000..4027abf
--- /dev/null
+++ b/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java
@@ -0,0 +1,372 @@
+package org.java_websocket.extensions.permessage_deflate;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.zip.DataFormatException;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+import org.java_websocket.extensions.CompressionExtension;
+import org.java_websocket.extensions.ExtensionRequestData;
+import org.java_websocket.extensions.IExtension;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.ContinuousFrame;
+import org.java_websocket.framing.DataFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.framing.FramedataImpl1;
+
+/**
+ * PerMessage Deflate Extension (7. The
+ * "permessage-deflate" Extension in
+ * RFC 7692).
+ *
+ * @see 7. The "permessage-deflate"
+ * Extension in RFC 7692
+ */
+public class PerMessageDeflateExtension extends CompressionExtension {
+
+ // Name of the extension as registered by IETF https://tools.ietf.org/html/rfc7692#section-9.
+ private static final String EXTENSION_REGISTERED_NAME = "permessage-deflate";
+ // Below values are defined for convenience. They are not used in the compression/decompression phase.
+ // They may be needed during the extension-negotiation offer in the future.
+ private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
+ private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
+ private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits";
+ private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits";
+ private static final int serverMaxWindowBits = 1 << 15;
+ private static final int clientMaxWindowBits = 1 << 15;
+ private static final byte[] TAIL_BYTES = {(byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF};
+ private static final int BUFFER_SIZE = 1 << 10;
+
+ private int threshold = 1024;
+
+ private boolean serverNoContextTakeover = true;
+ private boolean clientNoContextTakeover = false;
+
+ // For WebSocketServers, this variable holds the extension parameters that the peer client has requested.
+ // For WebSocketClients, this variable holds the extension parameters that client himself has requested.
+ private Map requestedParameters = new LinkedHashMap<>();
+
+ private final int compressionLevel;
+
+ private final Inflater inflater;
+ private final Deflater deflater;
+
+ /**
+ * Constructor for the PerMessage Deflate Extension (7. Thepermessage-deflate" Extension)
+ *
+ * Uses {@link Deflater#DEFAULT_COMPRESSION} as the compression level for the {@link Deflater#Deflater(int)}
+ */
+ public PerMessageDeflateExtension() {
+ this(Deflater.DEFAULT_COMPRESSION);
+ }
+
+ /**
+ * Constructor for the PerMessage Deflate Extension (7. Thepermessage-deflate" Extension)
+ *
+ * @param compressionLevel The compression level passed to the {@link Deflater#Deflater(int)}
+ */
+ public PerMessageDeflateExtension(int compressionLevel) {
+ this.compressionLevel = compressionLevel;
+ this.deflater = new Deflater(this.compressionLevel, true);
+ this.inflater = new Inflater(true);
+ }
+
+ /**
+ * Get the compression level used for the compressor.
+ * @return the compression level
+ */
+ public int getCompressionLevel() {
+ return this.compressionLevel;
+ }
+
+ /**
+ * Get the size threshold for doing the compression
+ * @return Size (in bytes) below which messages will not be compressed
+ * @since 1.5.3
+ */
+ public int getThreshold() {
+ return threshold;
+ }
+
+ /**
+ * Set the size when payloads smaller than this will not be compressed.
+ * @param threshold the size in bytes
+ * @since 1.5.3
+ */
+ public void setThreshold(int threshold) {
+ this.threshold = threshold;
+ }
+
+ /**
+ * Access the "server_no_context_takeover" extension parameter
+ *
+ * @see The "server_no_context_takeover" Extension Parameter
+ * @return serverNoContextTakeover is the server no context parameter active
+ */
+ public boolean isServerNoContextTakeover() {
+ return serverNoContextTakeover;
+ }
+
+ /**
+ * Setter for the "server_no_context_takeover" extension parameter
+ * @see The "server_no_context_takeover" Extension Parameter
+ * @param serverNoContextTakeover set the server no context parameter
+ */
+ public void setServerNoContextTakeover(boolean serverNoContextTakeover) {
+ this.serverNoContextTakeover = serverNoContextTakeover;
+ }
+
+ /**
+ * Access the "client_no_context_takeover" extension parameter
+ *
+ * @see The "client_no_context_takeover" Extension Parameter
+ * @return clientNoContextTakeover is the client no context parameter active
+ */
+ public boolean isClientNoContextTakeover() {
+ return clientNoContextTakeover;
+ }
+
+ /**
+ * Setter for the "client_no_context_takeover" extension parameter
+ * @see The "client_no_context_takeover" Extension Parameter
+ * @param clientNoContextTakeover set the client no context parameter
+ */
+ public void setClientNoContextTakeover(boolean clientNoContextTakeover) {
+ this.clientNoContextTakeover = clientNoContextTakeover;
+ }
+
+ /*
+ An endpoint uses the following algorithm to decompress a message.
+ 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
+ payload of the message.
+ 2. Decompress the resulting data using DEFLATE.
+ See, https://tools.ietf.org/html/rfc7692#section-7.2.2
+ */
+ @Override
+ public void decodeFrame(Framedata inputFrame) throws InvalidDataException {
+ // Only DataFrames can be decompressed.
+ if (!(inputFrame instanceof DataFrame)) {
+ return;
+ }
+
+ if (!inputFrame.isRSV1() && inputFrame.getOpcode() != Opcode.CONTINUOUS) {
+ return;
+ }
+
+ // RSV1 bit must be set only for the first frame.
+ if (inputFrame.getOpcode() == Opcode.CONTINUOUS && inputFrame.isRSV1()) {
+ throw new InvalidDataException(CloseFrame.POLICY_VALIDATION,
+ "RSV1 bit can only be set for the first frame.");
+ }
+
+ // Decompressed output buffer.
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try {
+ decompress(inputFrame.getPayloadData().array(), output);
+
+ /*
+ If a message is "first fragmented and then compressed", as this project does, then the inflater
+ can not inflate fragments except the first one.
+ This behavior occurs most likely because those fragments end with "final deflate blocks".
+ We can check the getRemaining() method to see whether the data we supplied has been decompressed or not.
+ And if not, we just reset the inflater and decompress again.
+ Note that this behavior doesn't occur if the message is "first compressed and then fragmented".
+ */
+ if (inflater.getRemaining() > 0) {
+ inflater.reset();
+ decompress(inputFrame.getPayloadData().array(), output);
+ }
+
+ if (inputFrame.isFin()) {
+ decompress(TAIL_BYTES, output);
+ // If context takeover is disabled, inflater can be reset.
+ if (clientNoContextTakeover) {
+ inflater.reset();
+ }
+ }
+ } catch (DataFormatException e) {
+ throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, e.getMessage());
+ }
+
+ // Set frames payload to the new decompressed data.
+ ((FramedataImpl1) inputFrame)
+ .setPayload(ByteBuffer.wrap(output.toByteArray(), 0, output.size()));
+ }
+
+ /**
+ * @param data the bytes of data
+ * @param outputBuffer the output stream
+ * @throws DataFormatException
+ */
+ private void decompress(byte[] data, ByteArrayOutputStream outputBuffer)
+ throws DataFormatException {
+ inflater.setInput(data);
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ int bytesInflated;
+ while ((bytesInflated = inflater.inflate(buffer)) > 0) {
+ outputBuffer.write(buffer, 0, bytesInflated);
+ }
+ }
+
+ @Override
+ public void encodeFrame(Framedata inputFrame) {
+ // Only DataFrames can be decompressed.
+ if (!(inputFrame instanceof DataFrame)) {
+ return;
+ }
+
+ byte[] payloadData = inputFrame.getPayloadData().array();
+ if (payloadData.length < threshold) {
+ return;
+ }
+ // Only the first frame's RSV1 must be set.
+ if (!(inputFrame instanceof ContinuousFrame)) {
+ ((DataFrame) inputFrame).setRSV1(true);
+ }
+
+ deflater.setInput(payloadData);
+ // Compressed output buffer.
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ // Temporary buffer to hold compressed output.
+ byte[] buffer = new byte[1024];
+ int bytesCompressed;
+ while ((bytesCompressed = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH))
+ > 0) {
+ output.write(buffer, 0, bytesCompressed);
+ }
+
+ byte[] outputBytes = output.toByteArray();
+ int outputLength = outputBytes.length;
+
+ /*
+ 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 (inputFrame.isFin()) {
+ if (endsWithTail(outputBytes)) {
+ outputLength -= TAIL_BYTES.length;
+ }
+
+ if (serverNoContextTakeover) {
+ deflater.reset();
+ }
+ }
+
+ // Set frames payload to the new compressed data.
+ ((FramedataImpl1) inputFrame).setPayload(ByteBuffer.wrap(outputBytes, 0, outputLength));
+ }
+
+ /**
+ * @param data the bytes of data
+ * @return true if the data is OK
+ */
+ private static boolean endsWithTail(byte[] data) {
+ if (data.length < 4) {
+ return false;
+ }
+
+ int length = data.length;
+ for (int i = 0; i < TAIL_BYTES.length; i++) {
+ if (TAIL_BYTES[i] != data[length - TAIL_BYTES.length + i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean acceptProvidedExtensionAsServer(String inputExtension) {
+ String[] requestedExtensions = inputExtension.split(",");
+ for (String extension : requestedExtensions) {
+ ExtensionRequestData extensionData = ExtensionRequestData.parseExtensionRequest(extension);
+ if (!EXTENSION_REGISTERED_NAME.equalsIgnoreCase(extensionData.getExtensionName())) {
+ continue;
+ }
+
+ // Holds parameters that peer client has sent.
+ Map headers = extensionData.getExtensionParameters();
+ requestedParameters.putAll(headers);
+ if (requestedParameters.containsKey(CLIENT_NO_CONTEXT_TAKEOVER)) {
+ clientNoContextTakeover = true;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean acceptProvidedExtensionAsClient(String inputExtension) {
+ String[] requestedExtensions = inputExtension.split(",");
+ for (String extension : requestedExtensions) {
+ ExtensionRequestData extensionData = ExtensionRequestData.parseExtensionRequest(extension);
+ if (!EXTENSION_REGISTERED_NAME.equalsIgnoreCase(extensionData.getExtensionName())) {
+ continue;
+ }
+
+ // Holds parameters that are sent by the server, as a response to our initial extension request.
+ Map headers = extensionData.getExtensionParameters();
+ // After this point, parameters that the server sent back can be configured, but we don't use them for now.
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getProvidedExtensionAsClient() {
+ requestedParameters.put(CLIENT_NO_CONTEXT_TAKEOVER, ExtensionRequestData.EMPTY_VALUE);
+ requestedParameters.put(SERVER_NO_CONTEXT_TAKEOVER, ExtensionRequestData.EMPTY_VALUE);
+
+ return EXTENSION_REGISTERED_NAME + "; " + SERVER_NO_CONTEXT_TAKEOVER + "; "
+ + CLIENT_NO_CONTEXT_TAKEOVER;
+ }
+
+ @Override
+ public String getProvidedExtensionAsServer() {
+ return EXTENSION_REGISTERED_NAME
+ + "; " + SERVER_NO_CONTEXT_TAKEOVER
+ + (clientNoContextTakeover ? "; " + CLIENT_NO_CONTEXT_TAKEOVER : "");
+ }
+
+ @Override
+ public IExtension copyInstance() {
+ PerMessageDeflateExtension clone = new PerMessageDeflateExtension(this.getCompressionLevel());
+ clone.setThreshold(this.getThreshold());
+ clone.setClientNoContextTakeover(this.isClientNoContextTakeover());
+ clone.setServerNoContextTakeover(this.isServerNoContextTakeover());
+ return clone;
+ }
+
+ /**
+ * This extension requires the RSV1 bit to be set only for the first frame. If the frame is type
+ * is CONTINUOUS, RSV1 bit must be unset.
+ */
+ @Override
+ public void isFrameValid(Framedata inputFrame) throws InvalidDataException {
+ if ((inputFrame instanceof ContinuousFrame) && (inputFrame.isRSV1() || inputFrame.isRSV2()
+ || inputFrame.isRSV3())) {
+ throw new InvalidFrameException(
+ "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: "
+ + inputFrame.isRSV3());
+ }
+ super.isFrameValid(inputFrame);
+ }
+
+ @Override
+ public String toString() {
+ return "PerMessageDeflateExtension";
+ }
+
+
+}
diff --git a/src/main/java/org/java_websocket/framing/BinaryFrame.java b/src/main/java/org/java_websocket/framing/BinaryFrame.java
new file mode 100644
index 0000000..dc05449
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/BinaryFrame.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+
+/**
+ * Class to represent a binary frame
+ */
+public class BinaryFrame extends DataFrame {
+
+ /**
+ * constructor which sets the opcode of this frame to binary
+ */
+ public BinaryFrame() {
+ super(Opcode.BINARY);
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/CloseFrame.java b/src/main/java/org/java_websocket/framing/CloseFrame.java
new file mode 100644
index 0000000..5a63d8a
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/CloseFrame.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import java.nio.ByteBuffer;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+import org.java_websocket.util.ByteBufferUtils;
+import org.java_websocket.util.Charsetfunctions;
+
+/**
+ * Class to represent a close frame
+ */
+public class CloseFrame extends ControlFrame {
+
+ /**
+ * indicates a normal closure, meaning whatever purpose the connection was established for has
+ * been fulfilled.
+ */
+ public static final int NORMAL = 1000;
+ /**
+ * 1001 indicates that an endpoint is "going away", such as a server going down, or a browser
+ * having navigated away from a page.
+ */
+ public static final int GOING_AWAY = 1001;
+ /**
+ * 1002 indicates that an endpoint is terminating the connection due to a protocol error.
+ */
+ public static final int PROTOCOL_ERROR = 1002;
+ /**
+ * 1003 indicates that an endpoint is terminating the connection because it has received a type of
+ * data it cannot accept (e.g. an endpoint that understands only text data MAY send this if it
+ * receives a binary message).
+ */
+ public static final int REFUSE = 1003;
+ /*1004: Reserved. The specific meaning might be defined in the future.*/
+ /**
+ * 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
+ * endpoint. It is designated for use in applications expecting a status code to indicate that no
+ * status code was actually present.
+ */
+ public static final int NOCODE = 1005;
+ /**
+ * 1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
+ * endpoint. It is designated for use in applications expecting a status code to indicate that the
+ * connection was closed abnormally, e.g. without sending or receiving a Close control frame.
+ */
+ public static final int ABNORMAL_CLOSE = 1006;
+ /**
+ * 1007 indicates that an endpoint is terminating the connection because it has received data
+ * within a message that was not consistent with the type of the message (e.g., non-UTF-8
+ * [RFC3629] data within a text message).
+ */
+ public static final int NO_UTF8 = 1007;
+ /**
+ * 1008 indicates that an endpoint is terminating the connection because it has received a message
+ * that violates its policy. This is a generic status code that can be returned when there is no
+ * other more suitable status code (e.g. 1003 or 1009), or if there is a need to hide specific
+ * details about the policy.
+ */
+ public static final int POLICY_VALIDATION = 1008;
+ /**
+ * 1009 indicates that an endpoint is terminating the connection because it has received a message
+ * which is too big for it to process.
+ */
+ public static final int TOOBIG = 1009;
+ /**
+ * 1010 indicates that an endpoint (client) is terminating the connection because it has expected
+ * the server to negotiate one or more extension, but the server didn't return them in the
+ * response message of the WebSocket handshake. The list of extensions which are needed SHOULD
+ * appear in the /reason/ part of the Close frame. Note that this status code is not used by the
+ * server, because it can fail the WebSocket handshake instead.
+ */
+ public static final int EXTENSION = 1010;
+ /**
+ * 1011 indicates that a server is terminating the connection because it encountered an unexpected
+ * condition that prevented it from fulfilling the request.
+ **/
+ public static final int UNEXPECTED_CONDITION = 1011;
+ /**
+ * 1012 indicates that the service is restarted. A client may reconnect, and if it choses to do,
+ * should reconnect using a randomized delay of 5 - 30s. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html
+ * for more information.
+ *
+ * @since 1.3.8
+ **/
+ public static final int SERVICE_RESTART = 1012;
+ /**
+ * 1013 indicates that the service is experiencing overload. A client should only connect to a
+ * different IP (when there are multiple for the target) or reconnect to the same IP upon user
+ * action. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html for more
+ * information.
+ *
+ * @since 1.3.8
+ **/
+ public static final int TRY_AGAIN_LATER = 1013;
+ /**
+ * 1014 indicates that the server was acting as a gateway or proxy and received an invalid
+ * response from the upstream server. This is similar to 502 HTTP Status Code See
+ * https://www.ietf.org/mail-archive/web/hybi/current/msg10748.html fore more information.
+ *
+ * @since 1.3.8
+ **/
+ public static final int BAD_GATEWAY = 1014;
+ /**
+ * 1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
+ * endpoint. It is designated for use in applications expecting a status code to indicate that the
+ * connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate
+ * can't be verified).
+ **/
+ public static final int TLS_ERROR = 1015;
+
+ /**
+ * The connection had never been established
+ */
+ public static final int NEVER_CONNECTED = -1;
+
+ /**
+ * The connection had a buggy close (this should not happen)
+ */
+ public static final int BUGGYCLOSE = -2;
+
+ /**
+ * The connection was flushed and closed
+ */
+ public static final int FLASHPOLICY = -3;
+
+
+ /**
+ * The close code used in this close frame
+ */
+ private int code;
+
+ /**
+ * The close message used in this close frame
+ */
+ private String reason;
+
+ /**
+ * Constructor for a close frame
+ *
+ * Using opcode closing and fin = true
+ */
+ public CloseFrame() {
+ super(Opcode.CLOSING);
+ setReason("");
+ setCode(CloseFrame.NORMAL);
+ }
+
+ /**
+ * Set the close code for this close frame
+ *
+ * @param code the close code
+ */
+ public void setCode(int code) {
+ this.code = code;
+ // CloseFrame.TLS_ERROR is not allowed to be transferred over the wire
+ if (code == CloseFrame.TLS_ERROR) {
+ this.code = CloseFrame.NOCODE;
+ this.reason = "";
+ }
+ updatePayload();
+ }
+
+ /**
+ * Set the close reason for this close frame
+ *
+ * @param reason the reason code
+ */
+ public void setReason(String reason) {
+ if (reason == null) {
+ reason = "";
+ }
+ this.reason = reason;
+ updatePayload();
+ }
+
+ /**
+ * Get the used close code
+ *
+ * @return the used close code
+ */
+ public int getCloseCode() {
+ return code;
+ }
+
+ /**
+ * Get the message that closeframe is containing
+ *
+ * @return the message in this frame
+ */
+ public String getMessage() {
+ return reason;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "code: " + code;
+ }
+
+ @Override
+ public void isValid() throws InvalidDataException {
+ super.isValid();
+ if (code == CloseFrame.NO_UTF8 && reason.isEmpty()) {
+ throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!");
+ }
+ if (code == CloseFrame.NOCODE && 0 < reason.length()) {
+ throw new InvalidDataException(PROTOCOL_ERROR,
+ "A close frame must have a closecode if it has a reason");
+ }
+ //Intentional check for code != CloseFrame.TLS_ERROR just to make sure even if the code earlier changes
+ if ((code > CloseFrame.TLS_ERROR && code < 3000)) {
+ throw new InvalidDataException(PROTOCOL_ERROR, "Trying to send an illegal close code!");
+ }
+ if (code == CloseFrame.ABNORMAL_CLOSE || code == CloseFrame.TLS_ERROR
+ || code == CloseFrame.NOCODE || code > 4999 || code < 1000 || code == 1004) {
+ throw new InvalidFrameException("closecode must not be sent over the wire: " + code);
+ }
+ }
+
+ @Override
+ public void setPayload(ByteBuffer payload) {
+ code = CloseFrame.NOCODE;
+ reason = "";
+ payload.mark();
+ if (payload.remaining() == 0) {
+ code = CloseFrame.NORMAL;
+ } else if (payload.remaining() == 1) {
+ code = CloseFrame.PROTOCOL_ERROR;
+ } else {
+ if (payload.remaining() >= 2) {
+ ByteBuffer bb = ByteBuffer.allocate(4);
+ bb.position(2);
+ bb.putShort(payload.getShort());
+ bb.position(0);
+ code = bb.getInt();
+ }
+ payload.reset();
+ try {
+ int mark = payload.position();// because stringUtf8 also creates a mark
+ validateUtf8(payload, mark);
+ } catch (InvalidDataException e) {
+ code = CloseFrame.NO_UTF8;
+ reason = null;
+ }
+ }
+ }
+
+ /**
+ * Validate the payload to valid utf8
+ *
+ * @param mark the current mark
+ * @param payload the current payload
+ * @throws InvalidDataException the current payload is not a valid utf8
+ */
+ private void validateUtf8(ByteBuffer payload, int mark) throws InvalidDataException {
+ try {
+ payload.position(payload.position() + 2);
+ reason = Charsetfunctions.stringUtf8(payload);
+ } catch (IllegalArgumentException e) {
+ throw new InvalidDataException(CloseFrame.NO_UTF8);
+ } finally {
+ payload.position(mark);
+ }
+ }
+
+ /**
+ * Update the payload to represent the close code and the reason
+ */
+ private void updatePayload() {
+ byte[] by = Charsetfunctions.utf8Bytes(reason);
+ ByteBuffer buf = ByteBuffer.allocate(4);
+ buf.putInt(code);
+ buf.position(2);
+ ByteBuffer pay = ByteBuffer.allocate(2 + by.length);
+ pay.put(buf);
+ pay.put(by);
+ pay.rewind();
+ super.setPayload(pay);
+ }
+
+ @Override
+ public ByteBuffer getPayloadData() {
+ if (code == NOCODE) {
+ return ByteBufferUtils.getEmptyByteBuffer();
+ }
+ return super.getPayloadData();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ CloseFrame that = (CloseFrame) o;
+
+ if (code != that.code) {
+ return false;
+ }
+ return reason != null ? reason.equals(that.reason) : that.reason == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + code;
+ result = 31 * result + (reason != null ? reason.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/ContinuousFrame.java b/src/main/java/org/java_websocket/framing/ContinuousFrame.java
new file mode 100644
index 0000000..c518cc3
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/ContinuousFrame.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+
+/**
+ * Class to represent a continuous frame
+ */
+public class ContinuousFrame extends DataFrame {
+
+ /**
+ * constructor which sets the opcode of this frame to continuous
+ */
+ public ContinuousFrame() {
+ super(Opcode.CONTINUOUS);
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/ControlFrame.java b/src/main/java/org/java_websocket/framing/ControlFrame.java
new file mode 100644
index 0000000..1469dc5
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/ControlFrame.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.exceptions.InvalidFrameException;
+
+/**
+ * Abstract class to represent control frames
+ */
+public abstract class ControlFrame extends FramedataImpl1 {
+
+ /**
+ * Class to represent a control frame
+ *
+ * @param opcode the opcode to use
+ */
+ public ControlFrame(Opcode opcode) {
+ super(opcode);
+ }
+
+ @Override
+ public void isValid() throws InvalidDataException {
+ if (!isFin()) {
+ throw new InvalidFrameException("Control frame can't have fin==false set");
+ }
+ if (isRSV1()) {
+ throw new InvalidFrameException("Control frame can't have rsv1==true set");
+ }
+ if (isRSV2()) {
+ throw new InvalidFrameException("Control frame can't have rsv2==true set");
+ }
+ if (isRSV3()) {
+ throw new InvalidFrameException("Control frame can't have rsv3==true set");
+ }
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/DataFrame.java b/src/main/java/org/java_websocket/framing/DataFrame.java
new file mode 100644
index 0000000..c845c2c
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/DataFrame.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+
+/**
+ * Abstract class to represent data frames
+ */
+public abstract class DataFrame extends FramedataImpl1 {
+
+ /**
+ * Class to represent a data frame
+ *
+ * @param opcode the opcode to use
+ */
+ public DataFrame(Opcode opcode) {
+ super(opcode);
+ }
+
+ @Override
+ public void isValid() throws InvalidDataException {
+ //Nothing specific to check
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/Framedata.java b/src/main/java/org/java_websocket/framing/Framedata.java
new file mode 100644
index 0000000..d3e8f3c
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/Framedata.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import java.nio.ByteBuffer;
+import org.java_websocket.enums.Opcode;
+
+/**
+ * The interface for the frame
+ */
+public interface Framedata {
+
+ /**
+ * Indicates that this is the final fragment in a message. The first fragment MAY also be the
+ * final fragment.
+ *
+ * @return true, if this frame is the final fragment
+ */
+ boolean isFin();
+
+ /**
+ * Indicates that this frame has the rsv1 bit set.
+ *
+ * @return true, if this frame has the rsv1 bit set
+ */
+ boolean isRSV1();
+
+ /**
+ * Indicates that this frame has the rsv2 bit set.
+ *
+ * @return true, if this frame has the rsv2 bit set
+ */
+ boolean isRSV2();
+
+ /**
+ * Indicates that this frame has the rsv3 bit set.
+ *
+ * @return true, if this frame has the rsv3 bit set
+ */
+ boolean isRSV3();
+
+ /**
+ * Defines whether the "Payload data" is masked.
+ *
+ * @return true, "Payload data" is masked
+ */
+ boolean getTransfereMasked();
+
+ /**
+ * Defines the interpretation of the "Payload data".
+ *
+ * @return the interpretation as a Opcode
+ */
+ Opcode getOpcode();
+
+ /**
+ * The "Payload data" which was sent in this frame
+ *
+ * @return the "Payload data" as ByteBuffer
+ */
+ ByteBuffer getPayloadData();// TODO the separation of the application data and the extension data is yet to be done
+
+ /**
+ * Appends an additional frame to the current frame
+ *
+ * This methods does not override the opcode, but does override the fin
+ *
+ * @param nextframe the additional frame
+ */
+ void append(Framedata nextframe);
+}
diff --git a/src/main/java/org/java_websocket/framing/FramedataImpl1.java b/src/main/java/org/java_websocket/framing/FramedataImpl1.java
new file mode 100644
index 0000000..fc74f7a
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/FramedataImpl1.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import java.nio.ByteBuffer;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.util.ByteBufferUtils;
+
+/**
+ * Abstract implementation of a frame
+ */
+public abstract class FramedataImpl1 implements Framedata {
+
+ /**
+ * Indicates that this is the final fragment in a message.
+ */
+ private boolean fin;
+ /**
+ * Defines the interpretation of the "Payload data".
+ */
+ private Opcode optcode;
+
+ /**
+ * The unmasked "Payload data" which was sent in this frame
+ */
+ private ByteBuffer unmaskedpayload;
+
+ /**
+ * Defines whether the "Payload data" is masked.
+ */
+ private boolean transferemasked;
+
+ /**
+ * Indicates that the rsv1 bit is set or not
+ */
+ private boolean rsv1;
+
+ /**
+ * Indicates that the rsv2 bit is set or not
+ */
+ private boolean rsv2;
+
+ /**
+ * Indicates that the rsv3 bit is set or not
+ */
+ private boolean rsv3;
+
+ /**
+ * Check if the frame is valid due to specification
+ *
+ * @throws InvalidDataException thrown if the frame is not a valid frame
+ */
+ public abstract void isValid() throws InvalidDataException;
+
+ /**
+ * Constructor for a FramedataImpl without any attributes set apart from the opcode
+ *
+ * @param op the opcode to use
+ */
+ public FramedataImpl1(Opcode op) {
+ optcode = op;
+ unmaskedpayload = ByteBufferUtils.getEmptyByteBuffer();
+ fin = true;
+ transferemasked = false;
+ rsv1 = false;
+ rsv2 = false;
+ rsv3 = false;
+ }
+
+ @Override
+ public boolean isRSV1() {
+ return rsv1;
+ }
+
+ @Override
+ public boolean isRSV2() {
+ return rsv2;
+ }
+
+ @Override
+ public boolean isRSV3() {
+ return rsv3;
+ }
+
+ @Override
+ public boolean isFin() {
+ return fin;
+ }
+
+ @Override
+ public Opcode getOpcode() {
+ return optcode;
+ }
+
+ @Override
+ public boolean getTransfereMasked() {
+ return transferemasked;
+ }
+
+ @Override
+ public ByteBuffer getPayloadData() {
+ return unmaskedpayload;
+ }
+
+ @Override
+ public void append(Framedata nextframe) {
+ ByteBuffer b = nextframe.getPayloadData();
+ if (unmaskedpayload == null) {
+ unmaskedpayload = ByteBuffer.allocate(b.remaining());
+ b.mark();
+ unmaskedpayload.put(b);
+ b.reset();
+ } else {
+ b.mark();
+ unmaskedpayload.position(unmaskedpayload.limit());
+ unmaskedpayload.limit(unmaskedpayload.capacity());
+
+ if (b.remaining() > unmaskedpayload.remaining()) {
+ ByteBuffer tmp = ByteBuffer.allocate(b.remaining() + unmaskedpayload.capacity());
+ unmaskedpayload.flip();
+ tmp.put(unmaskedpayload);
+ tmp.put(b);
+ unmaskedpayload = tmp;
+
+ } else {
+ unmaskedpayload.put(b);
+ }
+ unmaskedpayload.rewind();
+ b.reset();
+ }
+ fin = nextframe.isFin();
+
+ }
+
+ @Override
+ public String toString() {
+ return "Framedata{ opcode:" + getOpcode() + ", fin:" + isFin() + ", rsv1:" + isRSV1()
+ + ", rsv2:" + isRSV2() + ", rsv3:" + isRSV3() + ", payload length:[pos:" + unmaskedpayload
+ .position() + ", len:" + unmaskedpayload.remaining() + "], payload:" + (
+ unmaskedpayload.remaining() > 1000 ? "(too big to display)"
+ : new String(unmaskedpayload.array())) + '}';
+ }
+
+ /**
+ * Set the payload of this frame to the provided payload
+ *
+ * @param payload the payload which is to set
+ */
+ public void setPayload(ByteBuffer payload) {
+ this.unmaskedpayload = payload;
+ }
+
+ /**
+ * Set the fin of this frame to the provided boolean
+ *
+ * @param fin true if fin has to be set
+ */
+ public void setFin(boolean fin) {
+ this.fin = fin;
+ }
+
+ /**
+ * Set the rsv1 of this frame to the provided boolean
+ *
+ * @param rsv1 true if rsv1 has to be set
+ */
+ public void setRSV1(boolean rsv1) {
+ this.rsv1 = rsv1;
+ }
+
+ /**
+ * Set the rsv2 of this frame to the provided boolean
+ *
+ * @param rsv2 true if rsv2 has to be set
+ */
+ public void setRSV2(boolean rsv2) {
+ this.rsv2 = rsv2;
+ }
+
+ /**
+ * Set the rsv3 of this frame to the provided boolean
+ *
+ * @param rsv3 true if rsv3 has to be set
+ */
+ public void setRSV3(boolean rsv3) {
+ this.rsv3 = rsv3;
+ }
+
+ /**
+ * Set the tranferemask of this frame to the provided boolean
+ *
+ * @param transferemasked true if transferemasked has to be set
+ */
+ public void setTransferemasked(boolean transferemasked) {
+ this.transferemasked = transferemasked;
+ }
+
+ /**
+ * Get a frame with a specific opcode
+ *
+ * @param opcode the opcode representing the frame
+ * @return the frame with a specific opcode
+ */
+ public static FramedataImpl1 get(Opcode opcode) {
+ if (opcode == null) {
+ throw new IllegalArgumentException("Supplied opcode cannot be null");
+ }
+ switch (opcode) {
+ case PING:
+ return new PingFrame();
+ case PONG:
+ return new PongFrame();
+ case TEXT:
+ return new TextFrame();
+ case BINARY:
+ return new BinaryFrame();
+ case CLOSING:
+ return new CloseFrame();
+ case CONTINUOUS:
+ return new ContinuousFrame();
+ default:
+ throw new IllegalArgumentException("Supplied opcode is invalid");
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ FramedataImpl1 that = (FramedataImpl1) o;
+
+ if (fin != that.fin) {
+ return false;
+ }
+ if (transferemasked != that.transferemasked) {
+ return false;
+ }
+ if (rsv1 != that.rsv1) {
+ return false;
+ }
+ if (rsv2 != that.rsv2) {
+ return false;
+ }
+ if (rsv3 != that.rsv3) {
+ return false;
+ }
+ if (optcode != that.optcode) {
+ return false;
+ }
+ return unmaskedpayload != null ? unmaskedpayload.equals(that.unmaskedpayload)
+ : that.unmaskedpayload == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (fin ? 1 : 0);
+ result = 31 * result + optcode.hashCode();
+ result = 31 * result + (unmaskedpayload != null ? unmaskedpayload.hashCode() : 0);
+ result = 31 * result + (transferemasked ? 1 : 0);
+ result = 31 * result + (rsv1 ? 1 : 0);
+ result = 31 * result + (rsv2 ? 1 : 0);
+ result = 31 * result + (rsv3 ? 1 : 0);
+ return result;
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/PingFrame.java b/src/main/java/org/java_websocket/framing/PingFrame.java
new file mode 100644
index 0000000..ae2b291
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/PingFrame.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+
+/**
+ * Class to represent a ping frame
+ */
+public class PingFrame extends ControlFrame {
+
+ /**
+ * constructor which sets the opcode of this frame to ping
+ */
+ public PingFrame() {
+ super(Opcode.PING);
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/PongFrame.java b/src/main/java/org/java_websocket/framing/PongFrame.java
new file mode 100644
index 0000000..4b58139
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/PongFrame.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+
+/**
+ * Class to represent a pong frame
+ */
+public class PongFrame extends ControlFrame {
+
+ /**
+ * constructor which sets the opcode of this frame to pong
+ */
+ public PongFrame() {
+ super(Opcode.PONG);
+ }
+
+ /**
+ * constructor which sets the opcode of this frame to ping copying over the payload of the ping
+ *
+ * @param pingFrame the PingFrame which payload is to copy
+ */
+ public PongFrame(PingFrame pingFrame) {
+ super(Opcode.PONG);
+ setPayload(pingFrame.getPayloadData());
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/TextFrame.java b/src/main/java/org/java_websocket/framing/TextFrame.java
new file mode 100644
index 0000000..52154b4
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/TextFrame.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.framing;
+
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.exceptions.InvalidDataException;
+import org.java_websocket.util.Charsetfunctions;
+
+/**
+ * Class to represent a text frames
+ */
+public class TextFrame extends DataFrame {
+
+ /**
+ * constructor which sets the opcode of this frame to text
+ */
+ public TextFrame() {
+ super(Opcode.TEXT);
+ }
+
+ @Override
+ public void isValid() throws InvalidDataException {
+ super.isValid();
+ if (!Charsetfunctions.isValidUTF8(getPayloadData())) {
+ throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!");
+ }
+ }
+}
diff --git a/src/main/java/org/java_websocket/framing/package-info.java b/src/main/java/org/java_websocket/framing/package-info.java
new file mode 100644
index 0000000..12e1510
--- /dev/null
+++ b/src/main/java/org/java_websocket/framing/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all interfaces and implementations in relation with the WebSocket
+ * frames.
+ */
+package org.java_websocket.framing;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshake.java b/src/main/java/org/java_websocket/handshake/ClientHandshake.java
new file mode 100644
index 0000000..f0cbc3a
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/ClientHandshake.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * The interface for a client handshake
+ */
+public interface ClientHandshake extends Handshakedata {
+
+ /**
+ * returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2
+ *
+ * @return the HTTP Request-URI
+ */
+ String getResourceDescriptor();
+}
diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java
new file mode 100644
index 0000000..8187515
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * The interface for building a handshake for the client
+ */
+public interface ClientHandshakeBuilder extends HandshakeBuilder, ClientHandshake {
+
+ /**
+ * Set a specific resource descriptor
+ *
+ * @param resourceDescriptor the resource descriptior to set
+ */
+ void setResourceDescriptor(String resourceDescriptor);
+}
diff --git a/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java
new file mode 100644
index 0000000..1f4de20
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * The interface for building a handshake
+ */
+public interface HandshakeBuilder extends Handshakedata {
+
+ /**
+ * Setter for the content of the handshake
+ *
+ * @param content the content to set
+ */
+ void setContent(byte[] content);
+
+ /**
+ * Adding a specific field with a specific value
+ *
+ * @param name the http field
+ * @param value the value for this field
+ */
+ void put(String name, String value);
+}
diff --git a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java
new file mode 100644
index 0000000..11ffa43
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * Implementation for a client handshake
+ */
+public class HandshakeImpl1Client extends HandshakedataImpl1 implements ClientHandshakeBuilder {
+
+ /**
+ * Attribute for the resource descriptor
+ */
+ private String resourceDescriptor = "*";
+
+ @Override
+ public void setResourceDescriptor(String resourceDescriptor) {
+ if (resourceDescriptor == null) {
+ throw new IllegalArgumentException("http resource descriptor must not be null");
+ }
+ this.resourceDescriptor = resourceDescriptor;
+ }
+
+ @Override
+ public String getResourceDescriptor() {
+ return resourceDescriptor;
+ }
+}
diff --git a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java
new file mode 100644
index 0000000..8754014
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * Implementation for a server handshake
+ */
+public class HandshakeImpl1Server extends HandshakedataImpl1 implements ServerHandshakeBuilder {
+
+ /**
+ * Attribute for the http status
+ */
+ private short httpstatus;
+
+ /**
+ * Attribute for the http status message
+ */
+ private String httpstatusmessage;
+
+ @Override
+ public String getHttpStatusMessage() {
+ return httpstatusmessage;
+ }
+
+ @Override
+ public short getHttpStatus() {
+ return httpstatus;
+ }
+
+ @Override
+ public void setHttpStatusMessage(String message) {
+ this.httpstatusmessage = message;
+ }
+
+ @Override
+ public void setHttpStatus(short status) {
+ httpstatus = status;
+ }
+}
diff --git a/src/main/java/org/java_websocket/handshake/Handshakedata.java b/src/main/java/org/java_websocket/handshake/Handshakedata.java
new file mode 100644
index 0000000..fd270ec
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/Handshakedata.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+import java.util.Iterator;
+
+/**
+ * The interface for the data of a handshake
+ */
+public interface Handshakedata {
+
+ /**
+ * Iterator for the http fields
+ *
+ * @return the http fields
+ */
+ Iterator iterateHttpFields();
+
+ /**
+ * Gets the value of the field
+ *
+ * @param name The name of the field
+ * @return the value of the field or an empty String if not in the handshake
+ */
+ String getFieldValue(String name);
+
+ /**
+ * Checks if this handshake contains a specific field
+ *
+ * @param name The name of the field
+ * @return true, if it contains the field
+ */
+ boolean hasFieldValue(String name);
+
+ /**
+ * Get the content of the handshake
+ *
+ * @return the content as byte-array
+ */
+ byte[] getContent();
+}
diff --git a/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java
new file mode 100644
index 0000000..62824cd
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.TreeMap;
+
+/**
+ * Implementation of a handshake builder
+ */
+public class HandshakedataImpl1 implements HandshakeBuilder {
+
+ /**
+ * Attribute for the content of the handshake
+ */
+ private byte[] content;
+
+ /**
+ * Attribute for the http fields and values
+ */
+ private TreeMap map;
+
+ /**
+ * Constructor for handshake implementation
+ */
+ public HandshakedataImpl1() {
+ map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ @Override
+ public Iterator iterateHttpFields() {
+ return Collections.unmodifiableSet(map.keySet()).iterator();// Safety first
+ }
+
+ @Override
+ public String getFieldValue(String name) {
+ String s = map.get(name);
+ if (s == null) {
+ return "";
+ }
+ return s;
+ }
+
+ @Override
+ public byte[] getContent() {
+ return content;
+ }
+
+ @Override
+ public void setContent(byte[] content) {
+ this.content = content;
+ }
+
+ @Override
+ public void put(String name, String value) {
+ map.put(name, value);
+ }
+
+ @Override
+ public boolean hasFieldValue(String name) {
+ return map.containsKey(name);
+ }
+}
diff --git a/src/main/java/org/java_websocket/handshake/ServerHandshake.java b/src/main/java/org/java_websocket/handshake/ServerHandshake.java
new file mode 100644
index 0000000..1b5a5a9
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/ServerHandshake.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * Interface for the server handshake
+ */
+public interface ServerHandshake extends Handshakedata {
+
+ /**
+ * Get the http status code
+ *
+ * @return the http status code
+ */
+ short getHttpStatus();
+
+ /**
+ * Get the http status message
+ *
+ * @return the http status message
+ */
+ String getHttpStatusMessage();
+}
diff --git a/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java
new file mode 100644
index 0000000..51212f0
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.handshake;
+
+/**
+ * The interface for building a handshake for the server
+ */
+public interface ServerHandshakeBuilder extends HandshakeBuilder, ServerHandshake {
+
+ /**
+ * Setter for the http status code
+ *
+ * @param status the http status code
+ */
+ void setHttpStatus(short status);
+
+ /**
+ * Setter for the http status message
+ *
+ * @param message the http status message
+ */
+ void setHttpStatusMessage(String message);
+}
diff --git a/src/main/java/org/java_websocket/handshake/package-info.java b/src/main/java/org/java_websocket/handshake/package-info.java
new file mode 100644
index 0000000..7af26d4
--- /dev/null
+++ b/src/main/java/org/java_websocket/handshake/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all interfaces and implementations in relation with the WebSocket
+ * handshake.
+ */
+package org.java_websocket.handshake;
+
diff --git a/src/main/java/org/java_websocket/interfaces/ISSLChannel.java b/src/main/java/org/java_websocket/interfaces/ISSLChannel.java
new file mode 100644
index 0000000..c783c58
--- /dev/null
+++ b/src/main/java/org/java_websocket/interfaces/ISSLChannel.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+package org.java_websocket.interfaces;
+
+import javax.net.ssl.SSLEngine;
+
+/**
+ * Interface which specifies all required methods a SSLSocketChannel has to make public.
+ *
+ * @since 1.4.1
+ */
+public interface ISSLChannel {
+
+ /**
+ * Get the ssl engine used for the de- and encryption of the communication.
+ *
+ * @return the ssl engine of this channel
+ */
+ SSLEngine getSSLEngine();
+}
diff --git a/src/main/java/org/java_websocket/interfaces/package-info.java b/src/main/java/org/java_websocket/interfaces/package-info.java
new file mode 100644
index 0000000..b70dbc1
--- /dev/null
+++ b/src/main/java/org/java_websocket/interfaces/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+/**
+ * This package encapsulates all new interfaces.
+ */
+package org.java_websocket.interfaces;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/protocols/IProtocol.java b/src/main/java/org/java_websocket/protocols/IProtocol.java
new file mode 100644
index 0000000..5300aed
--- /dev/null
+++ b/src/main/java/org/java_websocket/protocols/IProtocol.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.protocols;
+
+/**
+ * Interface which specifies all required methods for a Sec-WebSocket-Protocol
+ *
+ * @since 1.3.7
+ */
+public interface IProtocol {
+
+ /**
+ * Check if the received Sec-WebSocket-Protocol header field contains a offer for the specific
+ * protocol
+ *
+ * @param inputProtocolHeader the received Sec-WebSocket-Protocol header field offered by the
+ * other endpoint
+ * @return true, if the offer does fit to this specific protocol
+ * @since 1.3.7
+ */
+ boolean acceptProvidedProtocol(String inputProtocolHeader);
+
+ /**
+ * Return the specific Sec-WebSocket-protocol header offer for this protocol if the endpoint. If
+ * the extension returns an empty string (""), the offer will not be included in the handshake.
+ *
+ * @return the specific Sec-WebSocket-Protocol header for this protocol
+ * @since 1.3.7
+ */
+ String getProvidedProtocol();
+
+ /**
+ * To prevent protocols to be used more than once the Websocket implementation should call this
+ * method in order to create a new usable version of a given protocol instance.
+ *
+ * @return a copy of the protocol
+ * @since 1.3.7
+ */
+ IProtocol copyInstance();
+
+ /**
+ * Return a string which should contain the protocol name as well as additional information about
+ * the current configurations for this protocol (DEBUG purposes)
+ *
+ * @return a string containing the protocol name as well as additional information
+ * @since 1.3.7
+ */
+ String toString();
+}
diff --git a/src/main/java/org/java_websocket/protocols/Protocol.java b/src/main/java/org/java_websocket/protocols/Protocol.java
new file mode 100644
index 0000000..f7fbfb5
--- /dev/null
+++ b/src/main/java/org/java_websocket/protocols/Protocol.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.protocols;
+
+import java.util.regex.Pattern;
+
+/**
+ * Class which represents the protocol used as Sec-WebSocket-Protocol
+ *
+ * @since 1.3.7
+ */
+public class Protocol implements IProtocol {
+
+ private static final Pattern patternSpace = Pattern.compile(" ");
+ private static final Pattern patternComma = Pattern.compile(",");
+
+ /**
+ * Attribute for the provided protocol
+ */
+ private final String providedProtocol;
+
+ /**
+ * Constructor for a Sec-Websocket-Protocol
+ *
+ * @param providedProtocol the protocol string
+ */
+ public Protocol(String providedProtocol) {
+ if (providedProtocol == null) {
+ throw new IllegalArgumentException();
+ }
+ this.providedProtocol = providedProtocol;
+ }
+
+ @Override
+ public boolean acceptProvidedProtocol(String inputProtocolHeader) {
+ if ("".equals(providedProtocol)) {
+ return true;
+ }
+ String protocolHeader = patternSpace.matcher(inputProtocolHeader).replaceAll("");
+ String[] headers = patternComma.split(protocolHeader);
+ for (String header : headers) {
+ if (providedProtocol.equals(header)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getProvidedProtocol() {
+ return this.providedProtocol;
+ }
+
+ @Override
+ public IProtocol copyInstance() {
+ return new Protocol(getProvidedProtocol());
+ }
+
+ @Override
+ public String toString() {
+ return getProvidedProtocol();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Protocol protocol = (Protocol) o;
+
+ return providedProtocol.equals(protocol.providedProtocol);
+ }
+
+ @Override
+ public int hashCode() {
+ return providedProtocol.hashCode();
+ }
+}
diff --git a/src/main/java/org/java_websocket/protocols/package-info.java b/src/main/java/org/java_websocket/protocols/package-info.java
new file mode 100644
index 0000000..6f8132b
--- /dev/null
+++ b/src/main/java/org/java_websocket/protocols/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package encapsulates all interfaces and implementations in relation with the WebSocket
+ * Sec-WebSocket-Protocol.
+ */
+package org.java_websocket.protocols;
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java
new file mode 100644
index 0000000..1246b22
--- /dev/null
+++ b/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.server;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import org.java_websocket.SSLSocketChannel2;
+
+/**
+ * WebSocketFactory that can be configured to only support specific protocols and cipher suites.
+ */
+public class CustomSSLWebSocketServerFactory extends DefaultSSLWebSocketServerFactory {
+
+ /**
+ * The enabled protocols saved as a String array
+ */
+ private final String[] enabledProtocols;
+
+ /**
+ * The enabled ciphersuites saved as a String array
+ */
+ private final String[] enabledCiphersuites;
+
+ /**
+ * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher
+ * suites.
+ *
+ * @param sslContext - can not be null
+ * @param enabledProtocols - only these protocols are enabled, when null default
+ * settings will be used.
+ * @param enabledCiphersuites - only these cipher suites are enabled, when null
+ * default settings will be used.
+ */
+ public CustomSSLWebSocketServerFactory(SSLContext sslContext, String[] enabledProtocols,
+ String[] enabledCiphersuites) {
+ this(sslContext, Executors.newSingleThreadScheduledExecutor(), enabledProtocols,
+ enabledCiphersuites);
+ }
+
+ /**
+ * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher
+ * suites.
+ *
+ * @param sslContext - can not be null
+ * @param executerService - can not be null
+ * @param enabledProtocols - only these protocols are enabled, when null default
+ * settings will be used.
+ * @param enabledCiphersuites - only these cipher suites are enabled, when null
+ * default settings will be used.
+ */
+ public CustomSSLWebSocketServerFactory(SSLContext sslContext, ExecutorService executerService,
+ String[] enabledProtocols, String[] enabledCiphersuites) {
+ super(sslContext, executerService);
+ this.enabledProtocols = enabledProtocols;
+ this.enabledCiphersuites = enabledCiphersuites;
+ }
+
+ @Override
+ public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException {
+ SSLEngine e = sslcontext.createSSLEngine();
+ if (enabledProtocols != null) {
+ e.setEnabledProtocols(enabledProtocols);
+ }
+ if (enabledCiphersuites != null) {
+ e.setEnabledCipherSuites(enabledCiphersuites);
+ }
+ e.setUseClientMode(false);
+ return new SSLSocketChannel2(channel, e, exec, key);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java
new file mode 100644
index 0000000..f273203
--- /dev/null
+++ b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.server;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import org.java_websocket.SSLSocketChannel2;
+import org.java_websocket.WebSocketAdapter;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.WebSocketServerFactory;
+import org.java_websocket.drafts.Draft;
+
+public class DefaultSSLWebSocketServerFactory implements WebSocketServerFactory {
+
+ protected SSLContext sslcontext;
+ protected ExecutorService exec;
+
+ public DefaultSSLWebSocketServerFactory(SSLContext sslContext) {
+ this(sslContext, Executors.newSingleThreadScheduledExecutor());
+ }
+
+ public DefaultSSLWebSocketServerFactory(SSLContext sslContext, ExecutorService exec) {
+ if (sslContext == null || exec == null) {
+ throw new IllegalArgumentException();
+ }
+ this.sslcontext = sslContext;
+ this.exec = exec;
+ }
+
+ @Override
+ public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException {
+ SSLEngine e = sslcontext.createSSLEngine();
+ /*
+ * See https://github.com/TooTallNate/Java-WebSocket/issues/466
+ *
+ * We remove TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 from the enabled ciphers since it is just available when you patch your java installation directly.
+ * E.g. firefox requests this cipher and this causes some dcs/instable connections
+ */
+ List ciphers = new ArrayList<>(Arrays.asList(e.getEnabledCipherSuites()));
+ ciphers.remove("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
+ e.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]));
+ e.setUseClientMode(false);
+ return new SSLSocketChannel2(channel, e, exec, key);
+ }
+
+ @Override
+ public WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d) {
+ return new WebSocketImpl(a, d);
+ }
+
+ @Override
+ public WebSocketImpl createWebSocket(WebSocketAdapter a, List d) {
+ return new WebSocketImpl(a, d);
+ }
+
+ @Override
+ public void close() {
+ exec.shutdown();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java
new file mode 100644
index 0000000..80527e8
--- /dev/null
+++ b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.server;
+
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import org.java_websocket.WebSocketAdapter;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.WebSocketServerFactory;
+import org.java_websocket.drafts.Draft;
+
+public class DefaultWebSocketServerFactory implements WebSocketServerFactory {
+
+ @Override
+ public WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d) {
+ return new WebSocketImpl(a, d);
+ }
+
+ @Override
+ public WebSocketImpl createWebSocket(WebSocketAdapter a, List d) {
+ return new WebSocketImpl(a, d);
+ }
+
+ @Override
+ public SocketChannel wrapChannel(SocketChannel channel, SelectionKey key) {
+ return channel;
+ }
+
+ @Override
+ public void close() {
+ //Nothing to do for a normal ws factory
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java
new file mode 100644
index 0000000..d7685bf
--- /dev/null
+++ b/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.server;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import org.java_websocket.SSLSocketChannel2;
+
+/**
+ * WebSocketFactory that can be configured to only support specific protocols and cipher suites.
+ */
+public class SSLParametersWebSocketServerFactory extends DefaultSSLWebSocketServerFactory {
+
+ private final SSLParameters sslParameters;
+
+ /**
+ * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher
+ * suites.
+ *
+ * @param sslContext - can not be null
+ * @param sslParameters - can not be null
+ */
+ public SSLParametersWebSocketServerFactory(SSLContext sslContext, SSLParameters sslParameters) {
+ this(sslContext, Executors.newSingleThreadScheduledExecutor(), sslParameters);
+ }
+
+ /**
+ * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher
+ * suites.
+ *
+ * @param sslContext - can not be null
+ * @param executerService - can not be null
+ * @param sslParameters - can not be null
+ */
+ public SSLParametersWebSocketServerFactory(SSLContext sslContext, ExecutorService executerService,
+ SSLParameters sslParameters) {
+ super(sslContext, executerService);
+ if (sslParameters == null) {
+ throw new IllegalArgumentException();
+ }
+ this.sslParameters = sslParameters;
+ }
+
+ @Override
+ public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException {
+ SSLEngine e = sslcontext.createSSLEngine();
+ e.setUseClientMode(false);
+ e.setSSLParameters(sslParameters);
+ return new SSLSocketChannel2(channel, e, exec, key);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/java_websocket/server/WebSocketServer.java b/src/main/java/org/java_websocket/server/WebSocketServer.java
new file mode 100644
index 0000000..d5be3a4
--- /dev/null
+++ b/src/main/java/org/java_websocket/server/WebSocketServer.java
@@ -0,0 +1,1171 @@
+/*
+ * Copyright (c) 2010-2020 Nathan Rajlich
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.java_websocket.server;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.ClosedByInterruptException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.java_websocket.AbstractWebSocket;
+import org.java_websocket.SocketChannelIOHelper;
+import org.java_websocket.WebSocket;
+import org.java_websocket.WebSocketFactory;
+import org.java_websocket.WebSocketImpl;
+import org.java_websocket.WebSocketServerFactory;
+import org.java_websocket.WrappedByteChannel;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.exceptions.WebsocketNotConnectedException;
+import org.java_websocket.exceptions.WrappedIOException;
+import org.java_websocket.framing.CloseFrame;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.handshake.Handshakedata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WebSocketServer is an abstract class that only takes care of the
+ * HTTP handshake portion of WebSockets. It's up to a subclass to add functionality/purpose to the
+ * server.
+ */
+public abstract class WebSocketServer extends AbstractWebSocket implements Runnable {
+
+ private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
+
+ /**
+ * Logger instance
+ *
+ * @since 1.4.0
+ */
+ private final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
+
+ /**
+ * Holds the list of active WebSocket connections. "Active" means WebSocket handshake is complete
+ * and socket can be written to, or read from.
+ */
+ private final Collection connections;
+ /**
+ * The port number that this WebSocket server should listen on. Default is
+ * WebSocketImpl.DEFAULT_PORT.
+ */
+ private final InetSocketAddress address;
+ /**
+ * The socket channel for this WebSocket server.
+ */
+ private ServerSocketChannel server;
+ /**
+ * The 'Selector' used to get event keys from the underlying socket.
+ */
+ private Selector selector;
+ /**
+ * The Draft of the WebSocket protocol the Server is adhering to.
+ */
+ private List drafts;
+
+ private Thread selectorthread;
+
+ private final AtomicBoolean isclosed = new AtomicBoolean(false);
+
+ protected List decoders;
+
+ private List iqueue;
+ private BlockingQueue buffers;
+ private int queueinvokes = 0;
+ private final AtomicInteger queuesize = new AtomicInteger(0);
+
+ private WebSocketServerFactory wsf = new DefaultWebSocketServerFactory();
+
+ /**
+ * Attribute which allows you to configure the socket "backlog" parameter which determines how
+ * many client connections can be queued.
+ *
+ * @since 1.5.0
+ */
+ private int maxPendingConnections = -1;
+
+ /**
+ * Creates a WebSocketServer that will attempt to listen on port WebSocketImpl.DEFAULT_PORT.
+ *
+ * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
+ */
+ public WebSocketServer() {
+ this(new InetSocketAddress(WebSocketImpl.DEFAULT_PORT), AVAILABLE_PROCESSORS, null);
+ }
+
+ /**
+ * Creates a WebSocketServer that will attempt to bind/listen on the given address.
+ *
+ * @param address The address to listen to
+ * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
+ */
+ public WebSocketServer(InetSocketAddress address) {
+ this(address, AVAILABLE_PROCESSORS, null);
+ }
+
+ /**
+ * @param address The address (host:port) this server should listen on.
+ * incoming network data. By default this will be Runtime.getRuntime().availableProcessors()
+ * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
+ */
+ public WebSocketServer(InetSocketAddress address, int decodercount) {
+ this(address, decodercount, null);
+ }
+
+ /**
+ * @param address The address (host:port) this server should listen on.
+ * @param drafts The versions of the WebSocket protocol that this server instance should comply
+ * to. Clients that use an other protocol version will be rejected.
+ * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
+ */
+ public WebSocketServer(InetSocketAddress address, List drafts) {
+ this(address, AVAILABLE_PROCESSORS, drafts);
+ }
+
+ /**
+ * @param address The address (host:port) this server should listen on.
+ * incoming network data. By default this will be Runtime.getRuntime().availableProcessors()
+ * @param drafts The versions of the WebSocket protocol that this server instance should
+ * comply to. Clients that use an other protocol version will be rejected.
+ * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
+ */
+ public WebSocketServer(InetSocketAddress address, int decodercount, List drafts) {
+ this(address, decodercount, drafts, new HashSet());
+ }
+
+ // Small internal helper function to get around limitations of Java constructors.
+ private static InetSocketAddress checkAddressOfExistingChannel(ServerSocketChannel existingChannel) {
+ assert existingChannel.isOpen();
+ SocketAddress addr;
+ try {
+ addr = existingChannel.getLocalAddress();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound", e);
+ }
+ if (addr == null) {
+ throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound");
+ }
+ return (InetSocketAddress)addr;
+ }
+
+ /**
+ * @param existingChannel An already open and bound server socket channel, which this server will use.
+ * For example, it can be System.inheritedChannel() to implement socket activation.
+ */
+ public WebSocketServer(ServerSocketChannel existingChannel) {
+ this(checkAddressOfExistingChannel(existingChannel));
+ this.server = existingChannel;
+ }
+
+ /**
+ * Creates a WebSocketServer that will attempt to bind/listen on the given address, and
+ * comply with Draft version draft.
+ *
+ * @param address The address (host:port) this server should listen on.
+ * @param decodercount The number of {@link WebSocketWorker}s that will be used to process
+ * the incoming network data. By default this will be
+ * Runtime.getRuntime().availableProcessors()
+ * @param drafts The versions of the WebSocket protocol that this server instance
+ * should comply to. Clients that use an other protocol version will
+ * be rejected.
+ * @param connectionscontainer Allows to specify a collection that will be used to store the
+ * websockets in. If you plan to often iterate through the
+ * currently connected websockets you may want to use a collection
+ * that does not require synchronization like a {@link
+ * CopyOnWriteArraySet}. In that case make sure that you overload
+ * {@link #removeConnection(WebSocket)} and {@link
+ * #addConnection(WebSocket)}. By default a {@link HashSet} will
+ * be used.
+ * @see #removeConnection(WebSocket) for more control over syncronized operation
+ * @see more about
+ * drafts
+ */
+ public WebSocketServer(InetSocketAddress address, int decodercount, List drafts,
+ Collection connectionscontainer) {
+ if (address == null || decodercount < 1 || connectionscontainer == null) {
+ throw new IllegalArgumentException(
+ "address and connectionscontainer must not be null and you need at least 1 decoder");
+ }
+
+ if (drafts == null) {
+ this.drafts = Collections.emptyList();
+ } else {
+ this.drafts = drafts;
+ }
+
+ this.address = address;
+ this.connections = connectionscontainer;
+ setTcpNoDelay(false);
+ setReuseAddr(false);
+ iqueue = new LinkedList<>();
+
+ decoders = new ArrayList<>(decodercount);
+ buffers = new LinkedBlockingQueue<>();
+ for (int i = 0; i < decodercount; i++) {
+ WebSocketWorker ex = new WebSocketWorker();
+ decoders.add(ex);
+ }
+ }
+
+
+ /**
+ * Starts the server selectorthread that binds to the currently set port number and listeners for
+ * WebSocket connection requests. Creates a fixed thread pool with the size {@link
+ * WebSocketServer#AVAILABLE_PROCESSORS} May only be called once.
+ *
+ * Alternatively you can call {@link WebSocketServer#run()} directly.
+ *
+ * @throws IllegalStateException Starting an instance again
+ */
+ public void start() {
+ if (selectorthread != null) {
+ throw new IllegalStateException(getClass().getName() + " can only be started once.");
+ }
+ Thread t = new Thread(this);
+ t.setDaemon(isDaemon());
+ t.start();
+ }
+
+ public void stop(int timeout) throws InterruptedException {
+ stop(timeout, "");
+ }
+
+ /**
+ * Closes all connected clients sockets, then closes the underlying ServerSocketChannel,
+ * effectively killing the server socket selectorthread, freeing the port the server was bound to
+ * and stops all internal workerthreads.
+ *
+ * If this method is called before the server is started it will never start.
+ *
+ * @param timeout Specifies how many milliseconds the overall close handshaking may take
+ * altogether before the connections are closed without proper close
+ * handshaking.
+ * @param closeMessage Specifies message for remote client
+ * @throws InterruptedException Interrupt
+ */
+ public void stop(int timeout, String closeMessage) throws InterruptedException {
+ if (!isclosed.compareAndSet(false,
+ true)) { // this also makes sure that no further connections will be added to this.connections
+ return;
+ }
+
+ List socketsToClose;
+
+ // copy the connections in a list (prevent callback deadlocks)
+ synchronized (connections) {
+ socketsToClose = new ArrayList<>(connections);
+ }
+
+ for (WebSocket ws : socketsToClose) {
+ ws.close(CloseFrame.GOING_AWAY, closeMessage);
+ }
+
+ wsf.close();
+
+ synchronized (this) {
+ if (selectorthread != null && selector != null) {
+ selector.wakeup();
+ selectorthread.join(timeout);
+ }
+ }
+ }
+
+ public void stop() throws InterruptedException {
+ stop(0);
+ }
+
+ /**
+ * Returns all currently connected clients. This collection does not allow any modification e.g.
+ * removing a client.
+ *
+ * @return A unmodifiable collection of all currently connected clients
+ * @since 1.3.8
+ */
+ public Collection getConnections() {
+ synchronized (connections) {
+ return Collections.unmodifiableCollection(new ArrayList<>(connections));
+ }
+ }
+
+ public InetSocketAddress getAddress() {
+ return this.address;
+ }
+
+ /**
+ * Gets the port number that this server listens on.
+ *
+ * @return The port number.
+ */
+ public int getPort() {
+ int port = getAddress().getPort();
+ if (port == 0 && server != null) {
+ port = server.socket().getLocalPort();
+ }
+ return port;
+ }
+
+ @Override
+ public void setDaemon(boolean daemon) {
+ // pass it to the AbstractWebSocket too, to use it on the connectionLostChecker thread factory
+ super.setDaemon(daemon);
+ // we need to apply this to the decoders as well since they were created during the constructor
+ for (WebSocketWorker w : decoders) {
+ if (w.isAlive()) {
+ throw new IllegalStateException("Cannot call setDaemon after server is already started!");
+ } else {
+ w.setDaemon(daemon);
+ }
+ }
+ }
+
+ /**
+ * Get the list of active drafts
+ *
+ * @return the available drafts for this server
+ */
+ public List getDraft() {
+ return Collections.unmodifiableList(drafts);
+ }
+
+ /**
+ * Set the requested maximum number of pending connections on the socket. The exact semantics are
+ * implementation specific. The value provided should be greater than 0. If it is less than or
+ * equal to 0, then an implementation specific default will be used. This option will be passed as
+ * "backlog" parameter to {@link ServerSocket#bind(SocketAddress, int)}
+ *
+ * @since 1.5.0
+ * @param numberOfConnections the new number of allowed pending connections
+ */
+ public void setMaxPendingConnections(int numberOfConnections) {
+ maxPendingConnections = numberOfConnections;
+ }
+
+ /**
+ * Returns the currently configured maximum number of pending connections.
+ *
+ * @see #setMaxPendingConnections(int)
+ * @since 1.5.0
+ * @return the maximum number of pending connections
+ */
+ public int getMaxPendingConnections() {
+ return maxPendingConnections;
+ }
+
+ // Runnable IMPLEMENTATION /////////////////////////////////////////////////
+ public void run() {
+ if (!doEnsureSingleThread()) {
+ return;
+ }
+ if (!doSetupSelectorAndServerThread()) {
+ return;
+ }
+ try {
+ int shutdownCount = 5;
+ int selectTimeout = 0;
+ while (!selectorthread.isInterrupted() && shutdownCount != 0) {
+ SelectionKey key = null;
+ try {
+ if (isclosed.get()) {
+ selectTimeout = 5;
+ }
+ int keyCount = selector.select(selectTimeout);
+ if (keyCount == 0 && isclosed.get()) {
+ shutdownCount--;
+ }
+ Set keys = selector.selectedKeys();
+ Iterator i = keys.iterator();
+
+ while (i.hasNext()) {
+ key = i.next();
+
+ if (!key.isValid()) {
+ continue;
+ }
+
+ if (key.isAcceptable()) {
+ doAccept(key, i);
+ continue;
+ }
+
+ if (key.isReadable() && !doRead(key, i)) {
+ continue;
+ }
+
+ if (key.isWritable()) {
+ doWrite(key);
+ }
+ }
+ doAdditionalRead();
+ } catch (CancelledKeyException e) {
+ // an other thread may cancel the key
+ } catch (ClosedByInterruptException e) {
+ return; // do the same stuff as when InterruptedException is thrown
+ } catch (WrappedIOException ex) {
+ handleIOException(key, ex.getConnection(), ex.getIOException());
+ } catch (IOException ex) {
+ handleIOException(key, null, ex);
+ } catch (InterruptedException e) {
+ // FIXME controlled shutdown (e.g. take care of buffermanagement)
+ Thread.currentThread().interrupt();
+ }
+ }
+ } catch (RuntimeException e) {
+ // should hopefully never occur
+ handleFatal(null, e);
+ } finally {
+ doServerShutdown();
+ }
+ }
+
+ /**
+ * Do an additional read
+ *
+ * @throws InterruptedException thrown by taking a buffer
+ * @throws IOException if an error happened during read
+ */
+ private void doAdditionalRead() throws InterruptedException, IOException {
+ WebSocketImpl conn;
+ while (!iqueue.isEmpty()) {
+ conn = iqueue.remove(0);
+ WrappedByteChannel c = ((WrappedByteChannel) conn.getChannel());
+ ByteBuffer buf = takeBuffer();
+ try {
+ if (SocketChannelIOHelper.readMore(buf, conn, c)) {
+ iqueue.add(conn);
+ }
+ if (buf.hasRemaining()) {
+ conn.inQueue.put(buf);
+ queue(conn);
+ } else {
+ pushBuffer(buf);
+ }
+ } catch (IOException e) {
+ pushBuffer(buf);
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Execute a accept operation
+ *
+ * @param key the selectionkey to read off
+ * @param i the iterator for the selection keys
+ * @throws InterruptedException thrown by taking a buffer
+ * @throws IOException if an error happened during accept
+ */
+ private void doAccept(SelectionKey key, Iterator i)
+ throws IOException, InterruptedException {
+ if (!onConnect(key)) {
+ key.cancel();
+ return;
+ }
+
+ SocketChannel channel = server.accept();
+ if (channel == null) {
+ return;
+ }
+ channel.configureBlocking(false);
+ Socket socket = channel.socket();
+ socket.setTcpNoDelay(isTcpNoDelay());
+ socket.setKeepAlive(true);
+ WebSocketImpl w = wsf.createWebSocket(this, drafts);
+ w.setSelectionKey(channel.register(selector, SelectionKey.OP_READ, w));
+ try {
+ w.setChannel(wsf.wrapChannel(channel, w.getSelectionKey()));
+ i.remove();
+ allocateBuffers(w);
+ } catch (IOException ex) {
+ if (w.getSelectionKey() != null) {
+ w.getSelectionKey().cancel();
+ }
+
+ handleIOException(w.getSelectionKey(), null, ex);
+ }
+ }
+
+ /**
+ * Execute a read operation
+ *
+ * @param key the selectionkey to read off
+ * @param i the iterator for the selection keys
+ * @return true, if the read was successful, or false if there was an error
+ * @throws InterruptedException thrown by taking a buffer
+ * @throws IOException if an error happened during read
+ */
+ private boolean doRead(SelectionKey key, Iterator i)
+ throws InterruptedException, WrappedIOException {
+ WebSocketImpl conn = (WebSocketImpl) key.attachment();
+ ByteBuffer buf = takeBuffer();
+ if (conn.getChannel() == null) {
+ key.cancel();
+
+ handleIOException(key, conn, new IOException());
+ return false;
+ }
+ try {
+ if (SocketChannelIOHelper.read(buf, conn, conn.getChannel())) {
+ if (buf.hasRemaining()) {
+ conn.inQueue.put(buf);
+ queue(conn);
+ i.remove();
+ if (conn.getChannel() instanceof WrappedByteChannel && ((WrappedByteChannel) conn
+ .getChannel()).isNeedRead()) {
+ iqueue.add(conn);
+ }
+ } else {
+ pushBuffer(buf);
+ }
+ } else {
+ pushBuffer(buf);
+ }
+ } catch (IOException e) {
+ pushBuffer(buf);
+ throw new WrappedIOException(conn, e);
+ }
+ return true;
+ }
+
+ /**
+ * Execute a write operation
+ *
+ * @param key the selectionkey to write on
+ * @throws IOException if an error happened during batch
+ */
+ private void doWrite(SelectionKey key) throws WrappedIOException {
+ WebSocketImpl conn = (WebSocketImpl) key.attachment();
+ try {
+ if (SocketChannelIOHelper.batch(conn, conn.getChannel()) && key.isValid()) {
+ key.interestOps(SelectionKey.OP_READ);
+ }
+ } catch (IOException e) {
+ throw new WrappedIOException(conn, e);
+ }
+ }
+
+ /**
+ * Setup the selector thread as well as basic server settings
+ *
+ * @return true, if everything was successful, false if some error happened
+ */
+ private boolean doSetupSelectorAndServerThread() {
+ selectorthread.setName("WebSocketSelector-" + selectorthread.getId());
+ try {
+ if (server == null) {
+ server = ServerSocketChannel.open();
+ // If 'server' is not null, that means WebSocketServer was created from existing channel.
+ }
+ server.configureBlocking(false);
+ ServerSocket socket = server.socket();
+ int receiveBufferSize = getReceiveBufferSize();
+ if (receiveBufferSize > 0) {
+ socket.setReceiveBufferSize(receiveBufferSize);
+ }
+ socket.setReuseAddress(isReuseAddr());
+ // Socket may be already bound, if an existing channel was passed to constructor.
+ // In this case we cannot modify backlog size from pure Java code, so leave it as is.
+ if (!socket.isBound()) {
+ socket.bind(address, getMaxPendingConnections());
+ }
+ selector = Selector.open();
+ server.register(selector, server.validOps());
+ startConnectionLostTimer();
+ for (WebSocketWorker ex : decoders) {
+ ex.start();
+ }
+ onStart();
+ } catch (IOException ex) {
+ handleFatal(null, ex);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * The websocket server can only be started once
+ *
+ * @return true, if the server can be started, false if already a thread is running
+ */
+ private boolean doEnsureSingleThread() {
+ synchronized (this) {
+ if (selectorthread != null) {
+ throw new IllegalStateException(getClass().getName() + " can only be started once.");
+ }
+ selectorthread = Thread.currentThread();
+ if (isclosed.get()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Clean up everything after a shutdown
+ */
+ private void doServerShutdown() {
+ stopConnectionLostTimer();
+ if (decoders != null) {
+ for (WebSocketWorker w : decoders) {
+ w.interrupt();
+ }
+ }
+ if (selector != null) {
+ try {
+ selector.close();
+ } catch (IOException e) {
+ log.error("IOException during selector.close", e);
+ onError(null, e);
+ }
+ }
+ if (server != null) {
+ try {
+ server.close();
+ } catch (IOException e) {
+ log.error("IOException during server.close", e);
+ onError(null, e);
+ }
+ }
+ }
+
+ protected void allocateBuffers(WebSocket c) throws InterruptedException {
+ if (queuesize.get() >= 2 * decoders.size() + 1) {
+ return;
+ }
+ queuesize.incrementAndGet();
+ buffers.put(createBuffer());
+ }
+
+ protected void releaseBuffers(WebSocket c) throws InterruptedException {
+ // queuesize.decrementAndGet();
+ // takeBuffer();
+ }
+
+ public ByteBuffer createBuffer() {
+ int receiveBufferSize = getReceiveBufferSize();
+ return ByteBuffer.allocate(receiveBufferSize > 0 ? receiveBufferSize : DEFAULT_READ_BUFFER_SIZE);
+ }
+
+ protected void queue(WebSocketImpl ws) throws InterruptedException {
+ if (ws.getWorkerThread() == null) {
+ ws.setWorkerThread(decoders.get(queueinvokes % decoders.size()));
+ queueinvokes++;
+ }
+ ws.getWorkerThread().put(ws);
+ }
+
+ private ByteBuffer takeBuffer() throws InterruptedException {
+ return buffers.take();
+ }
+
+ private void pushBuffer(ByteBuffer buf) throws InterruptedException {
+ if (buffers.size() > queuesize.intValue()) {
+ return;
+ }
+ buffers.put(buf);
+ }
+
+ private void handleIOException(SelectionKey key, WebSocket conn, IOException ex) {
+ // onWebsocketError( conn, ex );// conn may be null here
+ if (key != null) {
+ key.cancel();
+ }
+ if (conn != null) {
+ conn.closeConnection(CloseFrame.ABNORMAL_CLOSE, ex.getMessage());
+ } else if (key != null) {
+ SelectableChannel channel = key.channel();
+ if (channel != null && channel
+ .isOpen()) { // this could be the case if the IOException ex is a SSLException
+ try {
+ channel.close();
+ } catch (IOException e) {
+ // there is nothing that must be done here
+ }
+ log.trace("Connection closed because of exception", ex);
+ }
+ }
+ }
+
+ private void handleFatal(WebSocket conn, Exception e) {
+ log.error("Shutdown due to fatal error", e);
+ onError(conn, e);
+
+ String causeMessage = e.getCause() != null ? " caused by " + e.getCause().getClass().getName() : "";
+ String errorMessage = "Got error on server side: " + e.getClass().getName() + causeMessage;
+ try {
+ stop(0, errorMessage);
+ } catch (InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ log.error("Interrupt during stop", e);
+ onError(null, e1);
+ }
+
+ //Shutting down WebSocketWorkers, see #222
+ if (decoders != null) {
+ for (WebSocketWorker w : decoders) {
+ w.interrupt();
+ }
+ }
+ if (selectorthread != null) {
+ selectorthread.interrupt();
+ }
+ }
+
+ @Override
+ public final void onWebsocketMessage(WebSocket conn, String message) {
+ onMessage(conn, message);
+ }
+
+
+ @Override
+ public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) {
+ onMessage(conn, blob);
+ }
+
+ @Override
+ public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) {
+ if (addConnection(conn)) {
+ onOpen(conn, (ClientHandshake) handshake);
+ }
+ }
+
+ @Override
+ public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) {
+ selector.wakeup();
+ try {
+ if (removeConnection(conn)) {
+ onClose(conn, code, reason, remote);
+ }
+ } finally {
+ try {
+ releaseBuffers(conn);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ }
+
+ /**
+ * This method performs remove operations on the connection and therefore also gives control over
+ * whether the operation shall be synchronized
+ *