Initial commit
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
-- 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>▶ Lua HTTP Server</h1>
|
||||
<p>Bidirectional client ↔ 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)} | <b>Port:</b> ${esc(data.port)}<br>` +
|
||||
`<b>Uptime:</b> ${esc(data.uptime)}s | <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,"&").replace(/</g,"<")
|
||||
.replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
// ── 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
|
||||
Reference in New Issue
Block a user