monitor/resource/menu/server/sv_webpipe.lua
2025-04-16 22:30:27 +07:00

166 lines
5.8 KiB
Lua

-- Prevent running in monitor mode
if not TX_SERVER_MODE then return end
-- Prevent running if menu is disabled
if not TX_MENU_ENABLED then return end
if TX_LUACOMHOST == "invalid" or TX_LUACOMTOKEN == "invalid" then
txPrint('^1API Host or Pipe Token ConVars not found. Do not start this resource if not using txAdmin.')
return
end
if TX_LUACOMTOKEN == "removed" then
txPrint('^1Please do not restart the monitor resource.')
return
end
-- =============================================
-- This file is responsible for all the webpipe
-- handling and caching.
-- =============================================
local _pipeLastReject
local _pipeFastCache = {}
-- Check if we should use latent events
local useLatentEvents = GetConvarBool('sv_enableNetEventReassembly', true);
if not useLatentEvents then
txPrint('^3WARNING: Latent events are disabled. If you have issues using the txAdmin in-game menu, please enable them by setting sv_enableNetEventReassembly to true in your server.cfg.')
end
-- So it's easy to enable-disable debug messages for webpipe
function debugWebPipe(...)
-- debugPrint(...)
end
---Sends a response to the client
---@param src string
---@param callbackId number
---@param statusCode number
---@param path string
---@param body string
---@param headers table
---@param cached boolean|nil
local function sendResponse(src, callbackId, statusCode, path, body, headers, cached)
local errorCode = tonumber(statusCode) >= 400
local resultColor = errorCode and '^1' or '^2'
local cachedStr = cached and " ^1(cached)^0" or ""
debugWebPipe(("^3WebPipe[^5%d^0:^1%d^3]^0 %s<< %s ^4%s%s^0"):format(
src, callbackId, resultColor, statusCode, path, cachedStr))
if errorCode then
debugWebPipe(("^3WebPipe[^5%d^0:^1%d^3]^0 %s<< Headers: %s^0"):format(src, callbackId, resultColor, json.encode(headers)))
end
if useLatentEvents then
TriggerLatentClientEvent('txcl:webpipe:resp', src, 125000, callbackId, statusCode, body, headers)
else
TriggerClientEvent('txcl:webpipe:resp', src, callbackId, statusCode, body, headers)
end
end
---Handles incoming webpipe request events
---@param callbackId number
---@param method string
---@param path string
---@param headers table
---@param body string
RegisterNetEvent('txsv:webpipe:req', function(callbackId, method, path, headers, body)
local s = source
local src = tostring(s)
if type(callbackId) ~= 'number' or type(headers) ~= 'table' then
return
end
if type(method) ~= 'string' or type(path) ~= 'string' or type(body) ~= 'string' then
return
end
-- Reject large paths as we use regex
if #path > 500 then
return sendResponse(s, callbackId, 400, path:sub(1, 300), "{}", {})
end
-- Treat path slashes
local url = "http://" .. (TX_LUACOMHOST .. '/' .. path):gsub("//+", "/")
-- Reject requests from un-authed players
if not TX_ADMINS[src] then
if _pipeLastReject ~= nil then
if (GetGameTimer() - _pipeLastReject) < 1250 then
_pipeLastReject = GetGameTimer()
return
end
end
debugWebPipe(string.format(
"^3WebPipe[^5%d^0:^1%d^3]^0 ^1rejected request from ^3%s^1 for ^5%s^0", s, callbackId, s, path))
TriggerClientEvent('txcl:webpipe:resp', s, callbackId, 403, "{}", {})
return
end
-- Return fast cache
if _pipeFastCache[path] ~= nil then
local cachedData = _pipeFastCache[path]
sendResponse(s, callbackId, 200, path, cachedData.data, cachedData.headers, true)
return
end
-- Adding admin identifiers for auth middleware to deal with
headers['X-TxAdmin-Token'] = TX_LUACOMTOKEN
headers['X-TxAdmin-Identifiers'] = table.concat(GetPlayerIdentifiers(s), ',')
debugWebPipe(("^3WebPipe[^5%d^0:^1%d^3]^0 ^4>>^0 ^6%s^0"):format(s, callbackId, url))
debugWebPipe(("^3WebPipe[^5%d^0:^1%d^3]^0 ^4>>^0 ^6Headers: %s^0"):format(s, callbackId, json.encode(headers)))
PerformHttpRequest(url, function(httpCode, data, resultHeaders)
-- fixing body for error pages (eg 404)
-- this is likely because of how json.encode() interprets null and an empty table
data = data or ''
resultHeaders['x-badcast-fix'] = 'https://youtu.be/LDU_Txk06tM' -- fixed in artifact v3996
--https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
resultHeaders['Content-Security-Policy'] = 'frame-ancestors https://monitor/ https://cfx-nui-monitor/ nui://game/'
-- fixing redirects
if resultHeaders.Location then
if resultHeaders.Location:sub(1, 1) == '/' then
resultHeaders.Location = '/WebPipe' .. resultHeaders.Location
end
end
-- fixing cookies
if resultHeaders['Set-Cookie'] then
local cookieHeader = resultHeaders['Set-Cookie']
local cookies = type(cookieHeader) == 'table' and cookieHeader or { cookieHeader }
for k in pairs(cookies) do
cookies[k] = cookies[k] .. '; SameSite=None; Secure'
end
resultHeaders['Set-Cookie'] = cookies
end
-- cache response if it is a static file
local sub = string.sub
if
httpCode == 200 and
(
sub(path, 1, 5) == '/css/' or
sub(path, 1, 4) == '/js/' or
sub(path, 1, 5) == '/img/' or
sub(path, 1, 7) == '/fonts/'
)
then
-- remove query params from path, so people can't consume memory by spamming cache-busters
for safePath in path:gmatch("([^?]+)") do
local slimHeaders = {}
for k, v in pairs(resultHeaders) do
if k ~= 'Set-Cookie' then
slimHeaders[k] = v
end
end
_pipeFastCache[safePath] = { data = data, headers = slimHeaders }
debugWebPipe(("^3WebPipe[^5%d^0:^1%d^3]^0 ^5cached ^4%s^0"):format(s, callbackId, safePath))
break
end
end
sendResponse(s, callbackId, httpCode, path, data, resultHeaders)
end, method, body, headers, {followLocation = false})
end)