Tutorial for replacing Chatterbox service using solarwebsockets plugin with TLS.

Since Chatterbox does not have TLS service, according to Apple’s 2024 specification, any connection without TLS will be rejected for audit, so I used plugin.solarwebsockets to replace the Chatterbox function.

After three days of research, I finally successfully added TLS service to plugin.solarwebsockets.

First of all, I would like to thank joehinkle11 for making such a great plugin, and I would also like to thank develephant for teaching me the core operation of the server in the past.

The following tutorial is only for adding TLS service, which still depends on plugin.solarwebsockets, so you must refer to the API in the plugin to achieve other functions you want.

Since solarwebsockets itself does not support TLS server, you must add a JS configuration file to the server.

Next, I will introduce it in 3 parts, namely ubuntu, the js configuration file inside, and the logic used by the front-end solar2d.

Since I live in Taiwan, the annotation will be written in the language I am used to, and those who need it will be translate.

3 Likes

★首先是ubuntu的介紹,要先開起 port 443 然後支援Ubuntu 24 此外我已經在Ipad與Android手機和Mac電腦共同連線測試過了,目前我使用的是Lightsail但EC2大概也沒問題。

sudo passwd ubuntu

mkdir ~/solar-server

sudo nano /home/ubuntu/solar-server/yourdomain.key ### 將憑證添加進去
sudo nano /home/ubuntu/solar-server/yourdomain.crt

sudo apt update && sudo apt upgrade -y

sudo apt install curl git build-essential -y

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

sudo apt install -y nodejs

node -v

npm -v

cd ~/solar-server

放入 tls_server.js ### 你可以使用WinSCP將檔案直接丟入或使用sudo nano編寫

npm init -y

npm install ws

sudo systemctl restart ssh

sudo node tls_server.js

1 Like

接著是JS檔案,要使用TLS服務的話,你必須編寫JS檔案

const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');

// 讀取你的 key & cert 並確保剛剛編寫是否有與他們相同的路徑
const serverOptions = {
	key: fs.readFileSync('/home/ubuntu/solar-server/yourdomain.key'),
	cert: fs.readFileSync('/home/ubuntu/solar-server/yourdomain.crt'),
};

// 建立 HTTPS 伺服器
const httpsServer = https.createServer(serverOptions, (req, res) => {
	res.writeHead(200);
	res.end('WebSocket TLS Server is running\n');
});
const wss = new WebSocket.Server({ server: httpsServer });

const clients = new Map();  // 儲存所有連線使用者

function heartbeat() {	// 伺服器端自動心跳清理 防止斷線卡住
  this.isAlive = true;
}

function sendRoomUserList(TheRoomName) {
	const usersInRoom = [];
	wss.clients.forEach((EachClient) => {
		if (EachClient.readyState === WebSocket.OPEN && EachClient.MyRoomName === TheRoomName) {
			usersInRoom.push(EachClient.MyGameID);
		}
	});

	const payload = JSON.stringify(["房間列表",usersInRoom]);

	wss.clients.forEach((EachClient) => {
		if (EachClient.readyState === WebSocket.OPEN && EachClient.MyRoomName === TheRoomName) {
			EachClient.send(payload);
		}
	});
}

wss.on("connection", (URL_Data, TheReData) => {	// URL_Data URL+自己給的名稱和房間號碼和其他想添加資料 , TheReData單純是習慣回應的寫法
	//////////////////初始登入後的參數使用//////////////////
	const params = new URLSearchParams(TheReData.url.replace(/^.*\?/, ""));
	const MyGameID = params.get("MyGameID");
	const MyRoomName = params.get("MyRoomName");

	URL_Data.MyGameID = MyGameID;
	URL_Data.MyRoomName = MyRoomName;

	if (clients.has(MyGameID)) {
		const oldSocket = clients.get(MyGameID);
		console.log("⚠️ 重複登入,踢掉舊連線:", MyGameID);	// 通知舊的連線
		oldSocket.send(JSON.stringify(["剔除","您已從其他裝置登入"]));
		oldSocket.terminate();	// 關閉舊連線
	}

	clients.set(MyGameID, URL_Data);

	console.log(`🔌 ${MyGameID} joined room: ${MyRoomName}`);  // 當你開起伺服器時 你可以藉由即時的log檔案來看自己有沒有成功連線
	//////////////////初始登入後的參數使用//////////////////
  
	//////////////////閃退檢查機制//////////////////
	URL_Data.isAlive = true
	URL_Data.on("pong", heartbeat);
	//////////////////閃退檢查機制//////////////////
  
	sendRoomUserList(MyRoomName);	// 把房間內有誰的所有資料傳過去
  
	//////////////////關閉檢查//////////////////
	URL_Data.on("close", () => {
		if (clients.get(MyGameID) === URL_Data) {	// 防止踢錯人
			clients.delete(MyGameID);
		}
		sendRoomUserList(MyRoomName); // 有人離開所以重新傳一次房間資料
	});
	
	//////////////////關閉檢查//////////////////
	
	////////////////////////////////////訊息////////////////////////////////////	
	URL_Data.on("message", (message) => {
		try {
			const data = JSON.parse(message);
			if (data.type === "chat" && typeof data.text === "string") {	// 只廣播給同房玩家
				wss.clients.forEach((EachClient) => {
					if (EachClient.readyState === WebSocket.OPEN && EachClient.MyRoomName === MyRoomName) {
						EachClient.send(JSON.stringify(["chat",data.text]));	// 靠字串分解就能讀出發送者是誰 沒必要為了黑名單多傳一筆資料
					}
				});
			}
		} catch (e) {
			console.error("Invalid message format:", message);
		}
	});
	////////////////////////////////////訊息////////////////////////////////////
});

setInterval(() => {
	wss.clients.forEach((EachClient) => {
		if (!EachClient.missedHeartbeats) EachClient.missedHeartbeats = 0;
		if (EachClient.isAlive === false) {
			EachClient.missedHeartbeats++;
			if (EachClient.missedHeartbeats >= 3) {
				console.log("超過 3 次未回應,踢除:", EachClient.MyGameID);
				return EachClient.terminate();
			}
		} else {
			EachClient.missedHeartbeats = 0;
		}
		EachClient.isAlive = false;
		EachClient.ping();
	});
}, 40000);  // 每 40 秒檢查一次

httpsServer.listen(443, () => {
	console.log('TLS WebSocket server started on port 443');
});
1 Like

最後則是Solar2D前端的編寫

local solarwebsockets = require "plugin.solarwebsockets"
local json = require("json")

local function wsListener(event)	-- init 只能跑一次 最好放在main.lua 並且考慮使用全域變數 這樣你就能靠connect切換伺服器了
    if event.name == "join" then
        print("加入後會馬上觸發message的房間列表")
    elseif event.name == "message" then
		local TheData = json.decode(event.message)
		if TheData[1] == "房間列表" then
			for k,v in pairs(TheData[2]) do			
				print("檢查房間所有人資料:", k,v)
			end
		elseif TheData[1] == "chat" then
			native.showAlert( "測試聊天資料", TheData[2],{"Ok"} )
		elseif TheData[1] == "剔除" then
			solarwebsockets.disconnect()
			native.showAlert( "被踢掉了", "被踢掉了",{"Ok"} )
		end
    elseif event.name == "leave" then
         print("離開")
    end
end
solarwebsockets.init(wsListener)

local MyGameID = "Player:"..(math.random(1,99999))
local GoConnect = display.newText( "GoConnect", 320, 540, native.systemFont, 30 )
local function GoConnectFun(event)
	if event.numTaps == 1 then			
		local MyURL = "wss://yourdomain:443"  -- 替換成你伺服器的 wss URL
		local MyRoomName = "World"
		solarwebsockets.connect(MyURL.."?MyGameID=" .. MyGameID .. "&MyRoomName=" .. MyRoomName)
	end
end
GoConnect:addEventListener("tap", GoConnectFun)

local GoDis = display.newText( "GoDis", 900, 540, native.systemFont, 30 )
local function GoDisFun(event)
	if event.numTaps == 1 then
		solarwebsockets.disconnect()
	end
end
GoDis:addEventListener("tap", GoDisFun)

local GoChat = display.newText( "GoChat", 1500, 540, native.systemFont, 30 )
local function GoChatFun(event)
	if event.numTaps == 1 then
		local chatMsg = {
			type = "chat",
			text = "哈囉大家好!★:"..MyGameID
		}
		solarwebsockets.sendServer(json.encode(chatMsg))
	end
end
GoChat:addEventListener("tap", GoChatFun)

若您不確定自己的網域是否有新增TLS的話可以將domain放入SSL Lab進行檢測。
若要添加其他功能請參考原始網站https://github.com/joehinkle11/SolarWebSockets/tree/master

2 Likes