From b9e3ab8e8213efa260ff3b0030d2c399f3afc653 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Mon, 1 Jun 2026 22:25:56 +0200 Subject: init --- base64.lua | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ client.lua | 119 ++++++++++++++++++++++++++++++ common.lua | 15 ++++ matchsrv.lua | 53 +++++++++++++ server.lua | 110 +++++++++++++++++++++++++++ standalone_server.lua | 20 +++++ test_client.lua | 33 +++++++++ 7 files changed, 551 insertions(+) create mode 100644 base64.lua create mode 100644 client.lua create mode 100644 common.lua create mode 100755 matchsrv.lua create mode 100644 server.lua create mode 100755 standalone_server.lua create mode 100755 test_client.lua diff --git a/base64.lua b/base64.lua new file mode 100644 index 0000000..32de332 --- /dev/null +++ b/base64.lua @@ -0,0 +1,201 @@ +--[[ + + base64 -- v1.5.3 public domain Lua base64 encoder/decoder + no warranty implied; use at your own risk + + Needs bit32.extract function. If not present it's implemented using BitOp + or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua + implementation inspired by Rici Lake's post: + http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html + + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/lbase64 + + COMPATIBILITY + + Lua 5.1+, LuaJIT + + LICENSE + + See end of file for license information. + +--]] + + +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode +if not extract then + if _G.bit then -- LuaJIT + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function( v, from, width ) + return band( shr( v, from ), shl( 1, width ) - 1 ) + end + elseif _G._VERSION == "Lua 5.1" then + extract = function( v, from, width ) + local w = 0 + local flag = 2^from + for i = 0, width-1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2^i + end + flag = flag2 + end + return w + end + else -- Lua 5.3+ + extract = load[[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]]() + end +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder( s62, s63, spad ) + local decoder = {} + for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode( str, encoder, usecaching ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #str + local lastn = n % 3 + local cache = {} + for i = 1, n-lastn, 3 do + local a, b, c = str:byte( i, i+2 ) + local v = a*0x10000 + b*0x100 + c + local s + if usecaching then + s = cache[v] + if not s then + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + cache[v] = s + end + else + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + end + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = str:byte( n-1, n ) + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = str:byte( n )*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +function base64.decode( b64, decoder, usecaching ) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs( decoder ) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) + end + b64 = b64:gsub( pattern, '' ) + local cache = usecaching and {} + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n-4 or n, 4 do + local a, b, c, d = b64:byte( i, i+3 ) + local s + if usecaching then + local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d + s = cache[v0] + if not s then + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + cache[v0] = s + end + else + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + end + t[k] = s + k = k + 1 + end + if padding == 1 then + local a, b, c = b64:byte( n-3, n-1 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + t[k] = char( extract(v,16,8), extract(v,8,8)) + elseif padding == 2 then + local a, b = b64:byte( n-3, n-2 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + t[k] = char( extract(v,16,8)) + end + return concat( t ) +end + +return base64 + +--[[ +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2018 Ilya Kolbin +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +--]] diff --git a/client.lua b/client.lua new file mode 100644 index 0000000..43ddda0 --- /dev/null +++ b/client.lua @@ -0,0 +1,119 @@ +local enet = require("enet") +local json = require("json") +local socket = require("socket") +local base64 = require("base64") +local common = require("common") + +local client = {} + +local function create_client(secret) + local clt = {} + clt.host = enet.host_create() -- enet.host_create("10.75.98.51:58901") + clt.secret = secret + return clt +end + +local function connect(clt, addr) + clt.server = clt.host:connect(addr) + clt.server_req = socket.gettime() + clt.status = "wait_server" +end + +function client.join(invite, match_addr) + local invite_dec = base64.decode(invite) + local game_id = invite_dec:sub(1, common.gameid_len) + local secret = invite_dec:sub(common.gameid_len+1) + + local clt = create_client(secret) + clt.match = clt.host:connect(match_addr or common.default_match_addr) + clt.match:send(json.encode({ type = "match_join", game_id = game_id })) + clt.match_req = socket.gettime() + clt.game_id = game_id + clt.status = "wait_match" + return clt +end + +function client.connect(addr, secret) + local clt = create_client(secret) + connect(clt, addr) + return clt +end + +local function handle_match(clt, pkt) + if pkt.type == "client_join" then + if type(pkt.peer_addr) ~= "string" then + print("[client] client_join: invalid peer_addr") + return + end + connect(clt, pkt.peer_addr) + elseif pkt.type == "client_join_fail" then + clt.status = "fail_match" + end +end + +local function handle_server(clt, pkt) + if pkt.type == "client_hi" then + clt.status = "active" + elseif pkt.type == "client_reject" then + clt.status = "fail_server" + end +end + +function client.update(clt) + local event = clt.host:service(20) + while event do + if event.type == "receive" then + local pkt = json.decode(event.data) + if event.peer == clt.match and clt.status == "wait_match" then + handle_match(clt, pkt) + clt.match:disconnect() + clt.match = nil + elseif event.peer == clt.server then + handle_server(clt, pkt) + end + elseif event.type == "connect" then + if event.peer == clt.match and clt.status == "wait_match" then + clt.match:send(json.encode({ type = "match_join", game_id = clt.game_id })) + elseif event.peer == clt.server and clt.status == "wait_server" then + clt.server:send(json.encode({ type = "server_hi", secret = clt.secret })) + else + event.peer:disconnect_now() + end + print("[client] connect " .. tostring(event.peer)) + elseif event.type == "disconnect" then + print("[client] disconnect " .. tostring(event.peer)) + if event.peer == clt.server and clt.status == "active" then + clt.status = "disco" + end + end + event = clt.host:service() + end +end + +function client.status(clt) + if clt.status == "wait_match" and clt.match_req+3 < socket.gettime() then + clt.status = "timeout_match" + elseif clt.status == "wait_server" and clt.match_req+5 < socket.gettime() then + clt.status = "timeout_server" + end + + return clt.status + + + -- wait_match + -- wait_server + + -- timeout_match + -- fail_match + -- timeout_server + -- fail_server + + -- active + -- disco +end + +function client.close(clt) + clt.host:destroy() +end + +return client diff --git a/common.lua b/common.lua new file mode 100644 index 0000000..adcc178 --- /dev/null +++ b/common.lua @@ -0,0 +1,15 @@ +local table_unpack = table.unpack or unpack +local function rand_string(n) + local b = {} + for i = 1, n do + table.insert(b, math.random(0, 255)) + end + return string.char(table_unpack(b)) +end + +return { + rand_string = rand_string, + default_match_addr = "ivy.vlhl.dev:18252", + gameid_len = 8, + secret_len = 4, +} diff --git a/matchsrv.lua b/matchsrv.lua new file mode 100755 index 0000000..65dd313 --- /dev/null +++ b/matchsrv.lua @@ -0,0 +1,53 @@ +#!/usr/bin/env lua5.1 +local enet = require("enet") +local json = require("json") +local common = require("common") + +local host = enet.host_create("0.0.0.0:18252") + +local game_to_peer = {} +local peer_to_game = {} + +local function remove_game(peer) + local game = game_to_peer[peer] + if game then + peer_to_game[game] = nil + end +end + +while true do + local event = host:service(100) + while event do + if event.type == "receive" then + local pkt = json.decode(event.data) + if pkt.type == "match_register" then + remove_game(event.peer) + local game_id = common.rand_string(common.gameid_len) + peer_to_game[event.peer] = game_id + game_to_peer[game_id] = event.peer + event.peer:send(json.encode({ type = "server_match", game_id = game_id })) + print(event.peer, "registered game") + elseif pkt.type == "match_join" then + if pkt.game_id and type(pkt.game_id) == "string" then + local server = game_to_peer[pkt.game_id] + if server then + server:send(json.encode({ type = "server_join", peer_addr = tostring(event.peer) })) + event.peer:send(json.encode({ type = "client_join", peer_addr = tostring(server) })) + print(event.peer, "joined game", server) + else + event.peer:send(json.encode({ type = "client_join_fail" })) + print(event.peer, "failed to join game") + end + end + else + print("invalid pkt type") + end + elseif event.type == "connect" then + print(event.peer, "connected") + elseif event.type == "disconnect" then + remove_game(event.peer) + print(event.peer, "disconnected") + end + event = host:service() + end +end diff --git a/server.lua b/server.lua new file mode 100644 index 0000000..4ae4c87 --- /dev/null +++ b/server.lua @@ -0,0 +1,110 @@ +local enet = require("enet") +local json = require("json") +local socket = require("socket") +local base64 = require("base64") +local common = require("common") + +local server = {} + +function server.create(match_addr) + local srv = {} + + srv.host = enet.host_create() + srv.secret = common.rand_string(common.secret_len) + srv.clients = {} + + srv.match = srv.host:connect(match_addr or common.default_match_addr) + srv.match_req = socket.gettime() + + return srv +end + +local function handle_match(srv, pkt) + if pkt.type == "server_match" then + if type(pkt.game_id) ~= "string" then + print("[server] server_match: invalid game_id") + return + end + + if srv.game_id then + print("[server] server_match: received while game already running") + return + end + + srv.game_id = pkt.game_id + srv.invite = base64.encode(srv.game_id .. srv.secret) + elseif pkt.type == "server_join" then + if type(pkt.peer_addr) ~= "string" then + print("[server] server_join: invalid peer_addr") + return + end + srv.host:connect(pkt.peer_addr) + end +end + +local function handle_client(srv, peer, pkt) + if pkt.type == "server_hi" then + if type(pkt.secret) ~= "string" then + print("[server] server_hi: invalid secret") + return + end + + if srv.clients[peer] then + print("[server] server_hi: client already connected") + return + end + + if pkt.secret == srv.secret then + print("[server] auth success " .. tostring(peer)) + srv.clients[peer] = { peer = peer } + peer:send(json.encode({ type = "client_hi" })) + else + print("[server] auth failure " .. tostring(peer)) + peer:send(json.encode({ type = "client_reject" })) + peer:disconnect() + end + end +end + +function server.update(srv) + local event = srv.host:service(20) + while event do + if event.type == "receive" then + local pkt = json.decode(event.data) + if event.peer == srv.match then + handle_match(srv, pkt) + else + handle_client(srv, event.peer, pkt) + end + elseif event.type == "connect" then + if event.peer == srv.match then + srv.match:send(json.encode({ type = "match_register" })) + end + print("[server] connect " .. tostring(event.peer)) + elseif event.type == "disconnect" then + print("[server] disconnect " .. tostring(event.peer)) + if event.peer == srv.match then + -- TODO + else + srv.clients[event.peer] = nil + end + end + event = srv.host:service() + end +end + +function server.match_status(srv) + if srv.game_id then + return "active", srv.invite + elseif srv.match_req + 3 >= socket.gettime() then + return "wait" + else + return "fail" + end +end + +function server.close(srv) + srv.host:destroy() +end + +return server diff --git a/standalone_server.lua b/standalone_server.lua new file mode 100755 index 0000000..a1dd571 --- /dev/null +++ b/standalone_server.lua @@ -0,0 +1,20 @@ +#!/usr/bin/env lua5.1 +local server = require("server") + +local srv = server.create() +local started = false + +while true do + server.update(srv) + local status, invite = server.match_status(srv) + + if status == "fail" then + print("failed to register match (match server down?)") + break + elseif status == "active" and not started then + started = true + print("invite: " .. invite) + end +end + +server.close(srv) diff --git a/test_client.lua b/test_client.lua new file mode 100755 index 0000000..aac6fe6 --- /dev/null +++ b/test_client.lua @@ -0,0 +1,33 @@ +#!/usr/bin/env lua5.1 +local client = require("client") + +local invite = assert(arg[1]) +local clt = client.join(invite) +local started + +while true do + client.update(clt) + local status = client.status(clt) + + if status == "timeout_match" then + print("failed to connect to match server") + break + elseif status == "fail_match" then + print("game not found (invalid invite?)") + break + elseif status == "timeout_server" then + print("failed to connect to server") + break + elseif status == "fail_server" then + print("incorrect secret (invalid invite?)") + break + elseif status == "disco" then + print("lost connection to server") + break + elseif status == "active" and not started then + started = true + print("connected to " .. tostring(clt.server)) + end +end + +client.close(clt) -- cgit v1.2.3