166 lines
5.8 KiB
Lua
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)
|