增加websocket支持
This commit is contained in:
14
www/api/sha1.lua
Normal file
14
www/api/sha1.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- GET /api/sha1.lua 测试 WebSocket Accept 的 SHA1/Base64 算法
|
||||
-- RFC6455 样例:
|
||||
-- Key = dGhlIHNhbXBsZSBub25jZQ==
|
||||
-- Accept = s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
||||
--
|
||||
-- 可选参数:?key=你的Sec-WebSocket-Key
|
||||
|
||||
local request = require("fastweb.request")
|
||||
local response = require("fastweb.response")
|
||||
local codec = require("fastweb.codec")
|
||||
local base64 = require("base64")
|
||||
local utils = require("utils")
|
||||
local cjson = require("cjson")
|
||||
print("headers:",cjson.encode(request.headers()))
|
||||
77
www/api/ws.lua
Normal file
77
www/api/ws.lua
Normal file
@@ -0,0 +1,77 @@
|
||||
local request = require("fastweb.request")
|
||||
local response = require("fastweb.response")
|
||||
local ws = require("fastweb.websocket")
|
||||
local codec = require("fastweb.codec")
|
||||
local base64 = require("base64")
|
||||
local utils = require("utils")
|
||||
|
||||
|
||||
if not ws.valid() then
|
||||
response.response_done()
|
||||
return
|
||||
end
|
||||
|
||||
if ws.type() == ws.UPGRADE then
|
||||
print("[WS_UPGRADE] CONNID:" .. ws.connid())
|
||||
local accept_key = base64.encode(utils.hex_to_bytes(codec.sha1(ws.sec_websocket_key() .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")))
|
||||
response.header("Sec-WebSocket-Accept", accept_key)
|
||||
response.header("Connection", "Upgrade")
|
||||
response.header("Upgrade", "websocket")
|
||||
response.send_header(101, "Switching Protocols")
|
||||
|
||||
elseif ws.type() == ws.MESSAGE_HEADER then
|
||||
local opcode = ws.opcode()
|
||||
|
||||
local log = "[WS_HEADER] CONNID:" .. ws.connid()
|
||||
if opcode == ws.OPCODE_TEXT then
|
||||
log = log .. " [TEXT]"
|
||||
elseif opcode == ws.OPCODE_BINARY then
|
||||
log = log .. " [BINARY]"
|
||||
elseif opcode == ws.OPCODE_CLOSE then
|
||||
log = log .. " [CLOSE]"
|
||||
elseif opcode == ws.OPCODE_PING then
|
||||
log = log .. " [PING]"
|
||||
elseif opcode == ws.OPCODE_PONG then
|
||||
log = log .. " [PONG]"
|
||||
else
|
||||
log = log .. " [OPCODE:" .. opcode .. "]"
|
||||
end
|
||||
log = log .. " [FINAL:" .. tostring(ws.final()) .. "] [LENGTH:" .. ws.length() .. "]"
|
||||
print(log)
|
||||
|
||||
elseif ws.type() == ws.MESSAGE_BODY then
|
||||
local connid = ws.connid()
|
||||
local opcode = ws.opcode()
|
||||
local body = request.body()
|
||||
local log = "[WS_BODY] CONNID:" .. connid
|
||||
.. " [OPCODE:" .. tostring(opcode) .. "]"
|
||||
.. " [LENGTH:" .. tostring(#body) .. "]"
|
||||
print(log)
|
||||
|
||||
if opcode == ws.OPCODE_CLOSE then
|
||||
-- 客户端关闭:回 Close 帧(可带回原载荷),完成握手
|
||||
print("[WS_BODY] reply CLOSE")
|
||||
response.send_ws(body, ws.OPCODE_CLOSE)
|
||||
elseif opcode == ws.OPCODE_PING then
|
||||
-- Ping 必须回 Pong,载荷原样带回
|
||||
print("[WS_BODY] reply PONG")
|
||||
response.send_ws(body, ws.OPCODE_PONG)
|
||||
elseif opcode == ws.OPCODE_PONG then
|
||||
-- Pong 无需回复
|
||||
print("[WS_BODY] PONG ignored")
|
||||
elseif opcode == ws.OPCODE_TEXT or opcode == ws.OPCODE_BINARY then
|
||||
-- 文本/二进制:原样回显
|
||||
response.send_ws(body, opcode)
|
||||
else
|
||||
print("[WS_BODY] unhandled opcode:" .. tostring(opcode))
|
||||
end
|
||||
|
||||
elseif ws.type() == ws.CLOSE then
|
||||
-- TCP 已断开,只做清理,不要再发帧
|
||||
print("[WS_CLOSE] CONNID:" .. ws.connid())
|
||||
return
|
||||
else
|
||||
print("UNKNOWN")
|
||||
end
|
||||
|
||||
response.response_done()
|
||||
440
www/index.html
440
www/index.html
@@ -1,113 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
.login-container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
width: 350px;
|
||||
}
|
||||
.login-container h1 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.login-container input[type="text"], .login-container input[type="password"] {
|
||||
width: 100%;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.login-container button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background-color: #007BFF;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.error-message {
|
||||
color: red;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
<script src="/js/jquery-3.4.1.min.js"></script>
|
||||
<script>
|
||||
var token = ""
|
||||
function login() {
|
||||
const username = $('#username').val();
|
||||
const password = $('#password').val();
|
||||
if (!username || !password) {
|
||||
alert('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WebSocket 测试</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1419;
|
||||
--surface: #1a2332;
|
||||
--border: #2d3a4d;
|
||||
--text: #e7ecf3;
|
||||
--muted: #8b9bb4;
|
||||
--accent: #3d9cf0;
|
||||
--accent-hover: #5aadf5;
|
||||
--ok: #3ecf8e;
|
||||
--err: #f07178;
|
||||
--warn: #e6c07b;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/user.lua?action=login',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ username: username, password: password }),
|
||||
success: function(response) {
|
||||
if (response.code === 200) {
|
||||
token = response.data.token;
|
||||
fetchUserInfo();
|
||||
} else {
|
||||
$('#errorMessage').text(response.msg || '登录失败,请重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
function fetchUserInfo() {
|
||||
$.ajax({
|
||||
url: '/api/user.lua?action=getinfo',
|
||||
type: 'GET',
|
||||
headers:{
|
||||
"token":token
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.code === 200) {
|
||||
const userInfo = response.data;
|
||||
alert(`用户信息:\n登录时间: ${userInfo.login_time}`);
|
||||
} else {
|
||||
alert('获取用户信息失败,请重试');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('获取用户信息失败,请重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
body {
|
||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%),
|
||||
radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1.25rem 3rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.sub code {
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 0.85em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
|
||||
.dot.connecting { background: var(--warn); }
|
||||
.dot.closed, .dot.error { background: var(--err); }
|
||||
|
||||
button, input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #0a1220;
|
||||
border: none;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) { background: var(--accent-hover); }
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
button.ghost:hover:not(:disabled) {
|
||||
border-color: var(--muted);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.url {
|
||||
width: 100%;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.65rem 0.85rem;
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.url:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.send-row {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.send-row input {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.65rem 0.85rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.send-row input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.log {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
height: min(52vh, 420px);
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.log .line {
|
||||
padding: 0.2rem 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log .line .t { color: var(--muted); margin-right: 0.5rem; }
|
||||
.log .line.sys { color: var(--muted); }
|
||||
.log .line.out { color: var(--accent); }
|
||||
.log .line.in { color: var(--ok); }
|
||||
.log .line.err { color: var(--err); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h1>用户登录</h1>
|
||||
<input type="text" id="username" placeholder="请输入用户名">
|
||||
<input type="password" id="password" placeholder="请输入密码">
|
||||
<button onclick="login()">确认</button>
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
<main>
|
||||
<h1>WebSocket 测试</h1>
|
||||
<p class="sub">连接到当前服务器 <code id="pathHint">/api/ws.lua</code></p>
|
||||
|
||||
<input class="url" id="url" type="text" spellcheck="false" />
|
||||
|
||||
<div class="row">
|
||||
<button id="btnConnect" type="button">连接</button>
|
||||
<button id="btnDisconnect" class="ghost" type="button" disabled>断开</button>
|
||||
<button id="btnClear" class="ghost" type="button">清空日志</button>
|
||||
<span class="status">
|
||||
<span class="dot" id="dot"></span>
|
||||
<span id="statusText">未连接</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="send-row">
|
||||
<input id="msg" type="text" placeholder="输入要发送的消息…" disabled />
|
||||
<button id="btnSend" type="button" disabled>发送</button>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log" aria-live="polite"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var path = "/api/ws.lua";
|
||||
var proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
var defaultUrl = proto + "//" + location.host + path;
|
||||
|
||||
var urlEl = document.getElementById("url");
|
||||
var msgEl = document.getElementById("msg");
|
||||
var logEl = document.getElementById("log");
|
||||
var dotEl = document.getElementById("dot");
|
||||
var statusEl = document.getElementById("statusText");
|
||||
var btnConnect = document.getElementById("btnConnect");
|
||||
var btnDisconnect = document.getElementById("btnDisconnect");
|
||||
var btnSend = document.getElementById("btnSend");
|
||||
var btnClear = document.getElementById("btnClear");
|
||||
|
||||
urlEl.value = defaultUrl;
|
||||
|
||||
var ws = null;
|
||||
|
||||
function now() {
|
||||
var d = new Date();
|
||||
return (
|
||||
String(d.getHours()).padStart(2, "0") + ":" +
|
||||
String(d.getMinutes()).padStart(2, "0") + ":" +
|
||||
String(d.getSeconds()).padStart(2, "0") + "." +
|
||||
String(d.getMilliseconds()).padStart(3, "0")
|
||||
);
|
||||
}
|
||||
|
||||
function log(kind, text) {
|
||||
var line = document.createElement("div");
|
||||
line.className = "line " + kind;
|
||||
var t = document.createElement("span");
|
||||
t.className = "t";
|
||||
t.textContent = now();
|
||||
line.appendChild(t);
|
||||
line.appendChild(document.createTextNode(text));
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setState(state, label) {
|
||||
dotEl.className = "dot " + state;
|
||||
statusEl.textContent = label;
|
||||
var open = state === "open";
|
||||
var connecting = state === "connecting";
|
||||
btnConnect.disabled = open || connecting;
|
||||
btnDisconnect.disabled = !open && !connecting;
|
||||
btnSend.disabled = !open;
|
||||
msgEl.disabled = !open;
|
||||
urlEl.disabled = open || connecting;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var url = urlEl.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
if (ws) {
|
||||
try { ws.close(); } catch (e) {}
|
||||
ws = null;
|
||||
}
|
||||
|
||||
setState("connecting", "连接中…");
|
||||
log("sys", "→ 连接 " + url);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
} catch (e) {
|
||||
setState("error", "错误");
|
||||
log("err", "创建失败: " + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = function () {
|
||||
setState("open", "已连接");
|
||||
log("sys", "✓ 连接已建立 (readyState=" + ws.readyState + ")");
|
||||
msgEl.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = function (ev) {
|
||||
var data = ev.data;
|
||||
if (data instanceof Blob) {
|
||||
data.arrayBuffer().then(function (buf) {
|
||||
log("in", "← binary " + buf.byteLength + " bytes");
|
||||
});
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
log("in", "← binary " + data.byteLength + " bytes");
|
||||
} else {
|
||||
log("in", "← " + data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
log("err", "WebSocket error");
|
||||
setState("error", "错误");
|
||||
};
|
||||
|
||||
ws.onclose = function (ev) {
|
||||
var reason = ev.reason ? " — " + ev.reason : "";
|
||||
log("sys", "× 已关闭 (code=" + ev.code + reason + ", clean=" + ev.wasClean + ")");
|
||||
setState("closed", "已断开");
|
||||
ws = null;
|
||||
};
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (!ws) return;
|
||||
log("sys", "→ 主动关闭");
|
||||
ws.close(1000, "client close");
|
||||
}
|
||||
|
||||
function send() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
var text = msgEl.value;
|
||||
ws.send(text);
|
||||
log("out", "→ " + text);
|
||||
msgEl.value = "";
|
||||
msgEl.focus();
|
||||
}
|
||||
|
||||
btnConnect.addEventListener("click", connect);
|
||||
btnDisconnect.addEventListener("click", disconnect);
|
||||
btnSend.addEventListener("click", send);
|
||||
btnClear.addEventListener("click", function () {
|
||||
logEl.innerHTML = "";
|
||||
});
|
||||
|
||||
msgEl.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") send();
|
||||
});
|
||||
|
||||
urlEl.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") connect();
|
||||
});
|
||||
|
||||
setState("closed", "未连接");
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user