jeopardy/server_test/server.lua
2026-05-06 19:39:08 -07:00

424 lines
13 KiB
Lua

-- server.lua
-- Simple HTTP server in Lua using LuaSocket
-- Install dependency: luarocks install luasocket
local socket = require("socket")
-- ─── Configuration ────────────────────────────────────────────────────────────
local HOST = "127.0.0.1"
local PORT = 8080
-- ─── In-memory data store ─────────────────────────────────────────────────────
local messages = {} -- stores messages sent from the client
-- ─── Helper: parse the request body from raw HTTP request ─────────────────────
local function parse_body(raw)
-- Body follows the blank line after headers
local body = raw:match("\r\n\r\n(.*)")
return body or ""
end
-- ─── Helper: URL-decode a string ──────────────────────────────────────────────
local function url_decode(str)
str = str:gsub("+", " ")
str = str:gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end)
return str
end
-- ─── Helper: parse application/x-www-form-urlencoded body ────────────────────
local function parse_form(body)
local params = {}
for key, value in body:gmatch("([^&=]+)=([^&=]*)") do
params[url_decode(key)] = url_decode(value)
end
return params
end
-- ─── Helper: encode a Lua table as a simple JSON object ───────────────────────
local function to_json(tbl)
local parts = {}
for k, v in pairs(tbl) do
local val
if type(v) == "string" then
val = string.format('"%s"', v:gsub('"', '\\"'))
elseif type(v) == "number" then
val = tostring(v)
elseif type(v) == "boolean" then
val = tostring(v)
else
val = '"[unsupported]"'
end
table.insert(parts, string.format('"%s": %s', k, val))
end
return "{ " .. table.concat(parts, ", ") .. " }"
end
-- ─── Helper: encode a list of messages as a JSON array ────────────────────────
local function messages_to_json()
local items = {}
for _, msg in ipairs(messages) do
table.insert(items, to_json(msg))
end
return "[" .. table.concat(items, ", ") .. "]"
end
-- ─── HTTP response builders ────────────────────────────────────────────────────
local function http_response(status, content_type, body)
return string.format(
"HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n%s",
status, content_type, #body, body
)
end
local function ok_json(body)
return http_response("200 OK", "application/json", body)
end
local function ok_html(body)
return http_response("200 OK", "text/html", body)
end
local function not_found()
return http_response("404 Not Found", "text/plain", "404 Not Found")
end
-- ─── HTML page served to the browser ─────────────────────────────────────────
local HTML = [[<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Lua Server Demo</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--accent: #58a6ff;
--green: #3fb950;
--text: #e6edf3;
--muted: #8b949e;
--radius: 10px;
}
body {
font-family: 'Courier New', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
}
header {
text-align: center;
margin-bottom: 36px;
}
header h1 {
font-size: 2rem;
color: var(--accent);
letter-spacing: 2px;
}
header p {
color: var(--muted);
margin-top: 6px;
font-size: 0.85rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
width: 100%;
max-width: 640px;
margin-bottom: 24px;
}
.card h2 {
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--muted);
margin-bottom: 16px;
}
input[type="text"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
padding: 10px 14px;
font-family: inherit;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
margin-bottom: 10px;
}
input[type="text"]:focus { border-color: var(--accent); }
button {
background: var(--accent);
color: #0d1117;
border: none;
border-radius: var(--radius);
padding: 10px 20px;
font-family: inherit;
font-size: 0.9rem;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
}
button:hover { opacity: 0.85; }
#status {
margin-top: 10px;
font-size: 0.8rem;
color: var(--green);
min-height: 18px;
}
#message-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
#message-list li {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 14px;
font-size: 0.88rem;
}
#message-list li span.sender {
color: var(--accent);
font-weight: bold;
margin-right: 8px;
}
#message-list li span.time {
float: right;
color: var(--muted);
font-size: 0.78rem;
}
#server-info {
font-size: 0.82rem;
color: var(--muted);
line-height: 1.8;
}
#server-info b { color: var(--text); }
</style>
</head>
<body>
<header>
<h1>&#x25B6; Lua HTTP Server</h1>
<p>Bidirectional client &harr; server communication demo</p>
</header>
<!-- Send a message -->
<div class="card">
<h2>Send a Message</h2>
<input type="text" id="nameInput" placeholder="Your name" value="Alice" />
<input type="text" id="messageInput" placeholder="Your message" value="Hello from the browser!" />
<button onclick="sendMessage()">Send to Server</button>
<div id="status"></div>
</div>
<!-- Live message feed -->
<div class="card">
<h2>Server Message Log</h2>
<ul id="message-list"><li style="color:var(--muted)">Loading…</li></ul>
</div>
<!-- Server info -->
<div class="card">
<h2>Server Info</h2>
<div id="server-info">Fetching…</div>
</div>
<script>
const BASE = ""; // same origin
// ── Send a message to the server ─────────────────────────────────────────
async function sendMessage() {
const name = document.getElementById("nameInput").value.trim();
const message = document.getElementById("messageInput").value.trim();
if (!name || !message) return;
const body = new URLSearchParams({ name, message });
const res = await fetch(BASE + "/send", { method: "POST", body });
const data = await res.json();
document.getElementById("status").textContent =
data.ok ? "✓ Message received by server!" : "✗ Error: " + data.error;
document.getElementById("messageInput").value = "";
fetchMessages();
}
// ── Fetch all messages from the server ───────────────────────────────────
async function fetchMessages() {
const res = await fetch(BASE + "/messages");
const list = await res.json();
const ul = document.getElementById("message-list");
if (list.length === 0) {
ul.innerHTML = '<li style="color:var(--muted)">No messages yet.</li>';
return;
}
ul.innerHTML = list.reverse().map(m => `
<li>
<span class="sender">${esc(m.name)}</span>${esc(m.text)}
<span class="time">${esc(m.time)}</span>
</li>
`).join("");
}
// ── Fetch server metadata ────────────────────────────────────────────────
async function fetchInfo() {
const res = await fetch(BASE + "/info");
const data = await res.json();
document.getElementById("server-info").innerHTML =
`<b>Host:</b> ${esc(data.host)} &nbsp;|&nbsp; <b>Port:</b> ${esc(data.port)}<br>` +
`<b>Uptime:</b> ${esc(data.uptime)}s &nbsp;|&nbsp; <b>Requests served:</b> ${esc(data.requests)}<br>` +
`<b>Lua version:</b> ${esc(data.lua_version)}`;
}
// ── Simple HTML escape ───────────────────────────────────────────────────
function esc(s) {
return String(s)
.replace(/&/g,"&amp;").replace(/</g,"&lt;")
.replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
// ── Poll the server every 3 seconds ─────────────────────────────────────
fetchMessages();
fetchInfo();
setInterval(fetchMessages, 3000);
setInterval(fetchInfo, 5000);
</script>
</body>
</html>
]]
-- ─── Route handler ────────────────────────────────────────────────────────────
local start_time = os.time()
local request_count = 0
local function handle(raw_request)
request_count = request_count + 1
-- Parse method and path from the request line
local method, path = raw_request:match("^(%u+) (/[^ ]*) HTTP")
method = method or "GET"
path = path or "/"
-- GET / → serve the HTML page
if method == "GET" and path == "/" then
return ok_html(HTML)
-- GET /messages → return all stored messages as JSON
elseif method == "GET" and path == "/messages" then
return ok_json(messages_to_json())
-- GET /info → return server metadata as JSON
elseif method == "GET" and path == "/info" then
local info = {
host = HOST,
port = tostring(PORT),
uptime = tostring(os.time() - start_time),
requests = tostring(request_count),
lua_version = _VERSION,
}
return ok_json(to_json(info))
-- POST /send → accept a message from the client
elseif method == "POST" and path == "/send" then
local body = parse_body(raw_request)
local params = parse_form(body)
local name = params["name"] or "Anonymous"
local text = params["message"] or ""
if text == "" then
return ok_json('{ "ok": false, "error": "empty message" }')
end
-- Store the message
table.insert(messages, {
name = name,
text = text,
time = os.date("%H:%M:%S"),
})
print(string.format("[POST /send] %s: %s", name, text))
return ok_json('{ "ok": true }')
else
return not_found()
end
end
-- ─── Main server loop ─────────────────────────────────────────────────────────
local server = assert(socket.bind(HOST, PORT))
server:settimeout(0) -- non-blocking accept
print(string.format("Lua HTTP server running at http://%s:%d/", HOST, PORT))
print("Press Ctrl-C to stop.\n")
while true do
local client = server:accept()
if client then
client:settimeout(5)
-- Read the full request (stop at blank line for GET, read body for POST)
local raw = {}
local content_length = 0
-- Read headers
while true do
local line, err = client:receive("*l")
if not line or line == "" then break end
table.insert(raw, line)
-- Capture Content-Length so we can read the body
local cl = line:match("^[Cc]ontent%-[Ll]ength:%s*(%d+)")
if cl then content_length = tonumber(cl) end
end
local header_str = table.concat(raw, "\r\n") .. "\r\n\r\n"
-- Read body if present
local body_str = ""
if content_length > 0 then
body_str = client:receive(content_length) or ""
end
local full_request = header_str .. body_str
local response = handle(full_request)
client:send(response)
client:close()
end
socket.sleep(0.01) -- prevent busy-loop
end