Created
August 14, 2017 16:57
-
-
Save dionrhys/b6c1fa4488ba408b84e242c276757e5e to your computer and use it in GitHub Desktop.
Simple Quake 3 "Update" (MOTD) Server Implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-------------------------------------------------- | |
-- Simple Quake 3 Update Server Implementation | |
-- Author: Dion Williams | |
-- Copyright: 2013 Dion Williams | |
-- Licence: MIT/X11; Please see README.md | |
-- Requires: LuaSocket, LuaLogging | |
-------------------------------------------------- | |
local _VERSION = "LuaQ3Update r2" | |
-- CONFIGURATION | |
local hostname = "*" -- "*" means any interface | |
local port = 29061 | |
-- Load library namespaces | |
local logging = require("logging") | |
local socket = require("socket") | |
local logger = require("logging.console")("%date [%level] %message\n") | |
logger:setLevel(logging.DEBUG) | |
-- Setup the local UDP server socket | |
local function setupSocket() | |
if mysock ~= nil then | |
-- Release the socket and binding immediately instead of waiting for GC | |
mysock:close() | |
mysock = nil | |
end | |
mysock = assert( socket.udp() ) | |
assert( mysock:setsockname(hostname, port) ) | |
assert( mysock:settimeout(1) ) | |
logger:info("Listening socket established on " .. hostname .. ":" .. port) | |
end | |
-- Initialize the psuedo-random number generator | |
local function setupRNG() | |
math.randomseed(os.time()) | |
end | |
-- Return a table of tokens for the given Q3 command string | |
local function cmdTokenize(text) | |
local i = 1 | |
local t = {} | |
while true do | |
-- Skip white-space | |
i = text:find("%S", i) | |
-- If end is reached, return the table | |
if i == nil then | |
return t | |
end | |
-- Check if this is a quoted argument | |
local thisarg = text:match("^\"[^\"]*\"?", i) | |
if thisarg ~= nil then | |
i = i + thisarg:len() | |
-- Strip the quotes from the argument | |
table.insert(t, (thisarg:gsub("[\"]", ""))) | |
else | |
-- Else try to parse an unquoted argument | |
thisarg = text:match("[^\"%s]+", i) | |
if thisarg ~= nil then | |
i = i + thisarg:len() | |
table.insert(t, thisarg) | |
end | |
end | |
end | |
end | |
-- Command Tokenizer tests | |
local function testCmdTokenize() | |
local cmds = cmdTokenize("foo bar \"abc\" \"test test\" \"herp\"derp \"one\"\"two\" \"broccoli") | |
assert(cmds[1] == "foo", "TEST #1: Unquoted argument") | |
assert(cmds[2] == "bar", "TEST #2: Unquoted argument") | |
assert(cmds[3] == "abc", "TEST #3: Quoted non-spaced argument") | |
assert(cmds[4] == "test test", "TEST #4: Quoted spaced argument") | |
assert(cmds[5] == "herp", "TEST #5: Quoted argument joined to unquoted argument") | |
assert(cmds[6] == "derp", "TEST #6: Unquoted argument joined to quoted argument") | |
assert(cmds[7] == "one", "TEST #7: Quoted argument joined to quoted argument") | |
assert(cmds[8] == "two", "TEST #8: Quoted argument joined to quoted argument") | |
assert(cmds[9] == "broccoli", "TEST #9: Semi-quoted argument cut-off at the end") | |
print("Command Tokenizer tests completed") | |
end | |
-- Return the key/value pairs of an Info String as a table | |
local function infoStringToTable(text) | |
local i = 1 | |
local t = {} | |
while true do | |
-- Seek to the first backslash | |
i = text:find("\\", i) | |
-- If end is reached, return the table | |
if i == nil then | |
return t | |
end | |
-- Read in this key | |
local key = text:match("^\\[^\\]*", i) | |
if key ~= nil then | |
i = i + key:len() | |
-- Strip the preceding backslash | |
key = key:sub(2) | |
-- Now read the value for this key | |
local value = text:match("^\\[^\\]*", i) | |
if value ~= nil then | |
i = i + value:len() | |
-- Strip the preceding backslash | |
value = value:sub(2) | |
-- Add it into the table | |
t[key] = value | |
else | |
-- Malformed sequence, return nil | |
return nil | |
end | |
else | |
-- Malformed sequence, return nil | |
return nil | |
end | |
end | |
end | |
-- Info Table tests | |
local function testInfoStringToTable() | |
local t = infoStringToTable("\\a_key\\a_value") | |
assert(t["a_key"] ~= nil, "TEST #1: a_key doesn't exist") | |
assert(t["a_key"] == "a_value", "TEST #2: a_key's value isn't \"a_value\"") | |
t = infoStringToTable("\\\\test") | |
assert(t[""] ~= nil, "TEST #3: Empty key doesn't exist") | |
assert(t[""] == "test", "TEST #4: Empty key's value isn't \"test\"") | |
t = infoStringToTable("\\foo\\") | |
assert(t["foo"] ~= nil, "TEST #3: foo doesn't exist") | |
assert(t["foo"] == "", "TEST #4: foo's value isn't \"\"") | |
t = infoStringToTable("\\k1\\v1\\k2") | |
assert(t == nil, "TEST #7: Orphaned key didn't abort parsing") | |
t = infoStringToTable("") | |
assert(t ~= nil, "TEST #8: Empty info string didn't return empty table") | |
print("Info Table tests completed") | |
end | |
-- Print keys/values of a table with fixed columns | |
local function printTable(t) | |
local biglen = 0 | |
if t ~= nil then | |
for k,v in pairs(t) do | |
if biglen < string.len(k) then | |
biglen = string.len(k) | |
end | |
end | |
biglen = biglen + 4 | |
for k,v in pairs(t) do | |
local len = string.len(k) | |
local line = k .. string.rep(" ", biglen-len) .. v | |
print(line) | |
end | |
else | |
print("nil") | |
end | |
end | |
-- Read a line of packet text | |
local function parseMsgLine(msg, i) | |
i = i or 1 | |
local line = msg:match("^[^%z\255\n]*", i) | |
if line ~= nil then | |
-- Replace all '%' characters with '.' (only because JKA does this) | |
line = line:gsub("[%%]", ".") | |
return line | |
else | |
return nil | |
end | |
end | |
-- Handle incoming getmotd request packets from clients | |
local function incomingGetMotd(cmd, fromaddr, fromport) | |
if #cmd < 2 then | |
return | |
end | |
local infotable = infoStringToTable(cmd[2]) | |
if infotable == nil then | |
return | |
end | |
if infotable["challenge"] == nil then | |
logger:debug(fromaddr .. ":" .. fromport .. " GETMOTD, no challenge given") | |
else | |
logger:debug(fromaddr .. ":" .. fromport .. " GETMOTD, challenge = '" .. infotable["challenge"] .. "'") | |
mysock:sendto("\255\255\255\255motd \"\\challenge\\" .. infotable["challenge"] .. "\\motd\\Source FAQ & Interesting Article - JKHub.org\"", fromaddr, fromport) | |
end | |
end | |
local function main() | |
logger:info(_VERSION .. " starting up...") | |
setupRNG() | |
setupSocket() | |
while true do | |
local msg,fromaddr,fromport = mysock:receivefrom() | |
-- Ignore any errors/timeouts | |
if msg ~= nil then | |
-- Ensure packet begins with \255\255\255\255 and at least one more letter, | |
-- and doesn't contain a NUL character | |
if msg:find("^\255\255\255\255%a") and not msg:find("%z") then | |
--print("Valid packet from " .. fromaddr .. ":" .. fromport .. ", Length: " .. msg:len()) | |
msg = msg:sub(5) | |
-- Parse the first line as a command string | |
local line = parseMsgLine(msg) | |
if line ~= nil then | |
local cmd = cmdTokenize(line) | |
if #cmd >= 1 then | |
if cmd[1]:lower() == "getmotd" then | |
incomingGetMotd(cmd, fromaddr, fromport) | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
-- Run! | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment