Last active
August 4, 2025 02:29
-
-
Save eprosync/b5ff370b5ee6d12bc9f01e38ed06468b to your computer and use it in GitHub Desktop.
Interserve - Initial Concept Version
This file contains hidden or 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
--[[ | |
Interserve - Abusing HTTP to get around net.* limits. | |
Initial Concept Version, do not use on production servers. | |
Contact: https://github.com/eprosync | |
Prerequisite: https://github.com/eprosync/interstellar_gmod | |
]] | |
local interserve = {} | |
_G.interserve = interserve | |
if SERVER then | |
if not iot then require("interstellar") end | |
util.AddNetworkString("Interserve:Initialize") | |
util.AddNetworkString("Interserve:Post") | |
util.AddNetworkString("Interserve:Get") | |
interserve.trusted = true -- Allows X-Forwarded-For headers (assuming you are using CF or Nginx-Reverse-Proxy) | |
interserve.port = INTERSERVE_IPORT or 10000 -- Port for serve to start on. | |
interserve.timeout = 30 -- How long till a request is invalidated in seconds. | |
interserve.registry = {} | |
interserve.sessions = {} | |
local ip = game.GetIPAddress() | |
if INTERSERVE_IP then ip = INTERSERVE_IP end | |
ip = string.Explode(":", ip)[1] .. ":" .. (INTERSERVE_PORT or interserve.port) | |
SetGlobalString("interserve", (INTERSERVE_SSL and "https://" or "http://") .. ip) | |
-- ^ Its recommended you have a proxy like Cloudflare. | |
-- Clients will communicate through this. | |
-- Make sure its only tailored to IPv4. | |
-- Change it via globals at startup "INTERSERVE_SSL", "INTERSERVE_IP" and "INTERSERVE_PORT" | |
function interserve:add(id, verify) | |
local self = interserve | |
local r = self.registry | |
if r[id] then return end | |
r[id] = verify or true | |
r[#r+1] = id | |
local players = player.GetHumans() | |
for i=1, #players do | |
self.open(players[i], id) | |
end | |
end | |
function interserve:remove(id) | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
r[id] = nil | |
for i=1, #r do | |
if r[i] == id then | |
table.remove(r, i) | |
break | |
end | |
end | |
local players = player.GetHumans() | |
for i=1, #players do | |
self.close(players[i], id) | |
end | |
end | |
function interserve:exists(id) | |
local self = interserve | |
local r = self.registry | |
return r[id] ~= nil | |
end | |
interserve.receivers = {} | |
function interserve:receive(id, callback) | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
local receivers = self.receivers | |
receivers[id] = callback | |
end | |
interserve.sending = {} | |
interserve.receiving = {} | |
timer.Create("Interserve:Validation", 1, 0, function() | |
local self = interserve | |
local r = self.registry | |
local t = os.time() | |
for k, v in player.Iterator() do | |
local senders = self.sending[v] | |
if senders then | |
for id, entry in pairs(senders) do | |
for uid, queue in pairs(entry) do | |
if queue.time + self.timeout < t then | |
entry[uid] = nil | |
end | |
end | |
end | |
end | |
local receivers = self.receiving[v] | |
if receivers then | |
for id, entry in pairs(receivers) do | |
for uid, queue in pairs(entry) do | |
if queue.time + self.timeout < t then | |
entry[uid] = nil | |
end | |
end | |
end | |
end | |
end | |
end) | |
function interserve:send(id, invoker, data) | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
local sending = self.sending | |
if not sending[invoker] then return end | |
local entry = sending[invoker] | |
if not entry[id] then entry[id] = {} end | |
local uid = util.SHA256(tostring(SysTime())) | |
entry[id][uid] = { | |
id = id, | |
uid = uid, | |
time = os.time(), | |
data = data | |
} | |
net.Start("Interserve:Get") | |
net.WriteString(id) | |
net.WriteString(uid) | |
net.Send(invoker) | |
end | |
function interserve:broadcast(id, data) | |
local humans = player.GetHumans() | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
local sending = self.sending | |
local uid = util.SHA256(tostring(SysTime())) | |
for i=1, #humans do | |
local invoker = humans[i] | |
if not sending[invoker] then continue end | |
local entry = sending[invoker] | |
if not entry[id] then entry[id] = {} end | |
entry[id][uid] = { | |
id = id, | |
uid = uid, | |
time = os.time(), | |
data = data | |
} | |
end | |
net.Start("Interserve:Get") | |
net.WriteString(id) | |
net.WriteString(uid) | |
net.Broadcast() | |
end | |
function interserve:omit(id, ignore, data) | |
local humans = player.GetHumans() | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
local sending = self.sending | |
local uid = util.SHA256(tostring(SysTime())) | |
local is_array = istable(ignore) | |
for i=1, #humans do | |
local invoker = humans[i] | |
if is_array then | |
local ignoring = false | |
for k=1, #ignore do | |
if ignore[k] == invoker then | |
ignoring = true | |
break | |
end | |
end | |
if ignoring then continue end | |
else | |
if invoker == ignore then continue end | |
end | |
if not sending[invoker] then continue end | |
local entry = sending[invoker] | |
if not entry[id] then entry[id] = {} end | |
entry[id][uid] = { | |
id = id, | |
uid = uid, | |
time = os.time(), | |
data = data | |
} | |
end | |
net.Start("Interserve:Get") | |
net.WriteString(id) | |
net.WriteString(uid) | |
net.SendOmit(ignore) | |
end | |
net.Receive("Interserve:Post", function(_, invoker) | |
local id = net.ReadString() | |
local self = interserve | |
local r = self.registry | |
if not r[id] then return end | |
local receiving = self.receiving | |
if not receiving[invoker] then return end | |
local entry = receiving[invoker] | |
if not entry[id] then entry[id] = {} end | |
local uid = util.SHA256(tostring(SysTime())) | |
entry[id][uid] = { | |
id = id, | |
uid = uid, | |
time = os.time() | |
} | |
net.Start("Interserve:Post") | |
net.WriteString(id) | |
net.WriteString(uid) | |
net.Send(invoker) | |
end) | |
function interserve.open(invoker, id) | |
local self = interserve | |
local engine = self.engine | |
local sid = self.sessions[invoker] | |
assert(sid ~= nil, "[Interserve] Cannot construct " .. invoker:SteamID64() .. " without session ID") | |
engine:post("/interserve/" .. sid .. "/" .. id, function(req, res) | |
if self.trusted then | |
if req.headers["X-Forwarded-For"] then | |
req.ip = req.headers["X-Forwarded-For"]:match("([^,]+)") or req.ip | |
end | |
end | |
local r = self.registry | |
local uid = req.parameters.uid | |
local session_id, id = string.match(req.path, "^/interserve/(%w+)/([%w%._%-~]+)") | |
if not session_id or not id or not uid or not r[id] then return res:status(400) end | |
local invoker = self.sessions[session_id] | |
if not IsValid(invoker) or not invoker:IsPlayer() then return res:status(401) end | |
local ip = invoker:IPAddress() | |
ip = string.Explode(":", ip)[1] | |
if req.ip ~= ip then return res:status(401) end | |
if isfunction(r[id]) then | |
local ran, err = xpcall(r[id], debug.traceback, invoker, req) | |
if not ran then | |
ErrorNoHalt(err) | |
return res:status(500) | |
end | |
if err == false then return res:status(403) end | |
end | |
local receiving = self.receiving | |
if not receiving[invoker] then return res:status(403) end | |
local entry = receiving[invoker] | |
if not entry[id] or not entry[id][uid] then return res:status(403) end | |
entry[id][uid] = nil | |
local receivers = self.receivers | |
if not receivers[id] then return res:status(403) end | |
local ran, err = xpcall(receivers[id], debug.traceback, invoker, req.body, req) | |
if not ran then ErrorNoHalt(err) end | |
return res:status(200) | |
end) | |
engine:get("/interserve/" .. sid .. "/" .. id, function(req, res) | |
if self.trusted then | |
if req.headers["X-Forwarded-For"] then | |
req.ip = req.headers["X-Forwarded-For"]:match("([^,]+)") or req.ip | |
end | |
end | |
local r = self.registry | |
local uid = req.parameters.uid | |
local session_id, id = string.match(req.path, "^/interserve/(%w+)/([%w%._%-~]+)") | |
if not session_id or not id or not uid or not r[id] then return res:status(400) end | |
local invoker = self.sessions[session_id] | |
if not IsValid(invoker) or not invoker:IsPlayer() then return res:status(401) end | |
local ip = invoker:IPAddress() | |
ip = string.Explode(":", ip)[1] | |
if req.ip ~= ip then return res:status(401) end | |
if isfunction(r[id]) then | |
local ran, err = xpcall(r[id], debug.traceback, invoker, req) | |
if not ran then | |
ErrorNoHalt(err) | |
return res:status(500) | |
end | |
if err == false then return res:status(403) end | |
end | |
local sending = self.sending | |
if not sending[invoker] then return res:status(401) end | |
local entry = sending[invoker] | |
if not entry[id] or not entry[id][uid] then return res:status(401) end | |
local queue = entry[id][uid] | |
entry[id][uid] = nil | |
res:body(queue.data or "") | |
res:status(200) | |
end) | |
end | |
function interserve.close(invoker, id) | |
local self = interserve | |
local engine = self.engine | |
local sid = self.sessions[invoker] | |
self.sending[invoker][id] = nil | |
self.receiving[invoker][id] = nil | |
if not sid then return end | |
engine:post("/interserve/" .. sid .. "/" .. id, nil) | |
engine:get("/interserve/" .. sid .. "/" .. id, nil) | |
end | |
function interserve.construct(invoker) | |
local self = interserve | |
invoker.INTERSERVE_SID = invoker.INTERSERVE_SID or util.MD5(invoker:SteamID64() .. "-" .. SysTime()) | |
if not self.sessions[invoker] then | |
self.sending[invoker] = self.sending[invoker] or {} | |
self.receiving[invoker] = self.receiving[invoker] or {} | |
self.sessions[invoker] = invoker.INTERSERVE_SID | |
self.sessions[invoker.INTERSERVE_SID] = invoker | |
local r = self.registry | |
for i=1, #r do | |
self.open(invoker, r[i]) | |
end | |
end | |
net.Start("Interserve:Initialize") | |
net.WriteString(invoker.INTERSERVE_SID) | |
net.Send(invoker) | |
end | |
function interserve.destruct(invoker) | |
local self = interserve | |
local r = self.registry | |
for i=1, #r do | |
local id = r[i] | |
self.close(invoker, r[i]) | |
end | |
local sid = self.sessions[invoker] | |
self.sessions[invoker] = nil | |
self.sessions[sid or ""] = nil | |
self.sending[invoker] = nil | |
self.receiving[invoker] = nil | |
end | |
function interserve.deploy() | |
local self = interserve | |
self.engine = iot.serve(self.port) | |
local engine = self.engine | |
if engine:active() or engine:start() then | |
print("[Interserve] Engine Ready.") | |
else | |
error("[Interserve] Unable to start serve engine, port is probably in-use [" .. self.port .. "]") | |
end | |
end | |
hook.Add("PlayerInitialSpawn", "Interserve", function(invoker) | |
local self = interserve | |
self.sending[invoker] = {} | |
self.receiving[invoker] = {} | |
end) | |
net.Receive("Interserve:Initialize", function(_, invoker) interserve.construct(invoker) end) | |
hook.Add("PlayerDisconnected", "Interserve", interserve.destruct) | |
do | |
local players = player.GetHumans() | |
for i=1, #players do | |
local invoker = players[i] | |
interserve.construct(players[i]) | |
end | |
end | |
interserve.deploy() | |
else | |
interserve.sending = {} | |
function interserve:send(id, data) | |
net.Start("Interserve:Post") | |
net.WriteString(id) | |
net.SendToServer() | |
if not self.sending[id] then self.sending[id] = {} end | |
local queue = self.sending[id] | |
queue[#queue+1] = data | |
end | |
interserve.receivers = {} | |
function interserve:receive(id, callback) | |
local self = interserve | |
local receivers = self.receivers | |
receivers[id] = callback | |
end | |
interserve.failures = {} | |
function interserve:failure(id, callback) | |
local self = interserve | |
local failures = self.failures | |
failures[id] = callback | |
end | |
function interserve.post(uid, id, body) | |
local self = interserve | |
local sid = self.sid | |
local failures = self.failures | |
HTTP({ | |
failed = function( reason ) | |
print("[Interserve] [post] [" .. id .. "] Failure: ", reason) | |
if failures[id] then | |
local ran, err = xpcall(failures[id], debug.traceback, 504, reason) | |
if not ran then ErrorNoHalt(err) end | |
end | |
end, | |
success = function( code, body, headers ) | |
if code ~= 200 then | |
print("[Interserve] [post] [" .. id .. "] Failure: ", code) | |
if failures[id] then | |
local ran, err = xpcall(failures[id], debug.traceback, code, body, headers) | |
if not ran then ErrorNoHalt(err) end | |
end | |
return | |
end | |
end, | |
method = "POST", | |
body = body, | |
url = GetGlobalString("interserve") .. "/interserve/" .. sid .. "/" .. id .. "?uid=" .. uid | |
}) | |
end | |
function interserve.get(uid, id) | |
local self = interserve | |
local sid = self.sid | |
HTTP({ | |
failed = function( reason ) | |
print("[Interserve] [get] [" .. id .. "] Failure: ", reason) | |
if failures[id] then | |
local ran, err = xpcall(failures[id], debug.traceback, 504, reason) | |
if not ran then ErrorNoHalt(err) end | |
end | |
end, | |
success = function( code, body, headers ) | |
if code ~= 200 then | |
print("[Interserve] [get] [" .. id .. "] Failure: ", code) | |
if failures[id] then | |
local ran, err = xpcall(failures[id], debug.traceback, code, body, headers) | |
if not ran then ErrorNoHalt(err) end | |
end | |
return | |
end | |
local receivers = self.receivers | |
if not receivers[id] then return end | |
local ran, err = xpcall(receivers[id], debug.traceback, body) | |
if not ran then ErrorNoHalt(err) end | |
end, | |
method = "GET", | |
url = GetGlobalString("interserve") .. "/interserve/" .. sid .. "/" .. id .. "?uid=" .. uid | |
}) | |
end | |
interserve.unaccounted = {} | |
net.Receive("Interserve:Initialize", function() | |
local self = interserve | |
local sid = net.ReadString() | |
self.sid = sid | |
local unaccounted = self.unaccounted | |
self.unaccounted = {} | |
for i=1, #unaccounted do | |
local entry = unaccounted[i] | |
local id = entry[2] | |
local uid = entry[3] | |
if entry[1] then | |
local body = entry[4] | |
interserve.post(uid, id, body) | |
else | |
interserve.get(uid, id) | |
end | |
end | |
end) | |
timer.Simple(0, function() | |
net.Start("Interserve:Initialize") | |
net.SendToServer() | |
end) | |
net.Receive("Interserve:Get", function() | |
local self = interserve | |
local id = net.ReadString() | |
local uid = net.ReadString() | |
local sid = self.sid | |
if not sid then | |
local unaccounted = interserve.unaccounted | |
unaccounted[#unaccounted+1] = {false, id, uid} | |
return | |
end | |
interserve.get(uid, id) | |
end) | |
net.Receive("Interserve:Post", function() | |
local self = interserve | |
local id = net.ReadString() | |
local uid = net.ReadString() | |
local sid = self.sid | |
local body = "" | |
if self.sending[id] then | |
body = table.remove(self.sending[id], 1) or "" | |
end | |
if not sid then | |
local unaccounted = interserve.unaccounted | |
unaccounted[#unaccounted+1] = {true, id, uid, body} | |
return | |
end | |
interserve.post(uid, id, body) | |
end) | |
end | |
if true then return end | |
if SERVER then | |
-- interserve:add(id: string, verify?: function(invoker: Player, request: iot.serve.request): boolean) | |
-- interserve:remove(id: string) | |
-- interserve:receive(id: string, callback: function(invoker: Player, data: string, request: iot.serve.request)) | |
-- interserve:send(id: string, target: Player, data: string) | |
interserve:add("early_test") | |
interserve:receive("early_test", function(invoker, data) | |
print("[early_test] Received From " .. invoker:Nick() .. ": ", invoker, data) | |
interserve:send("early_test", invoker, data) | |
end) | |
interserve:add("regular_test") | |
interserve:receive("regular_test", function(invoker, data) | |
print("[regular_test] Received From " .. invoker:Nick() .. ": ", invoker, data) | |
interserve:send("regular_test", invoker, data) | |
end) | |
else | |
-- interserve:receive(id: string, callback: function(data: string)) | |
-- interserve:send(id: string, data: string) | |
print("Uploading...") | |
interserve:send("early_test", "hello world") | |
interserve:receive("early_test", function(data) | |
print("[early_test] Received From Server: ", data) | |
end) | |
timer.Simple(2, function() | |
interserve:send("regular_test", "hello world") | |
interserve:receive("regular_test", function(data) | |
print("[regular_test] Received From Server: ", data) | |
end) | |
end) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment