Developer Documentation
BuddyTH License
System
ระบบตรวจสอบและจัดการ License สำหรับ FiveM Scripts
— ป้องกันการใช้งานที่ไม่ได้รับอนุญาต ผ่านการยืนยัน License Key, Server IP
และ Resource Name แบบ Real-time
FiveM Compatible
ESX / QBCore
Real-time Verify
IP Verification
Simple Prepend
Server Wrapper
ตรวจสอบ License กับ API ตอน resource start แล้วแจ้งผลให้ Client ผ่าน NetEvent ทุก request มี HMAC Secret ยืนยัน
Client Integration
client.lua เชื่อมต่อกับ server ผ่าน NetEvent และรอ Config จาก Server ก่อนเริ่มทำงาน
IP Verification
ระบบดึง Public IP ของ Server อัตโนมัติผ่าน ipify.org และเปรียบเทียบกับ IP ที่ลงทะเบียนไว้ใน Database
Auto Retry
Client Wrapper จะ retry ขอสถานะจาก Server สูงสุด 15 ครั้ง (ทุก 1.5s) ก่อนถือว่าไม่ผ่านและ block script
1
Resource Start
FiveM โหลด resource — Server Wrapper ถูก execute ก่อนโค้ดต้นฉบับ รอ 2 วินาทีก่อนยิง API
2
Get Public IP
ยิง api.ipify.org เพื่อรับ Public IP จริงของ Server (fallback: sv_endpoints)
3
POST to License API
ส่ง license_key, resource, server_ip, timestamp, secret ไปตรวจสอบ
4
Broadcast to Clients
Server แจ้งผลการตรวจสอบให้ Client ผ่าน bdyth:cl:<resource> NetEvent
5
Client Gate Decision
PASS → Client โหลด ESX และขอ Config จาก Server |
FAIL → Server หยุด resource ทันที
6
Stop Resource
ถ้า License ไม่ผ่าน — Server จะ StopResource() ทันที (Hardcoded, ปิดไม่ได้)
หากใช้ระบบ Auto-Download จากหน้า Admin → Licenses ทุกขั้นตอนจะทำอัตโนมัติ ไม่จำเป็นต้องทำด้วยตนเอง
01 สร้าง config.license.lua
ไฟล์นี้เก็บ licenseKey ของ resource — ระบบจะ generate ให้อัตโนมัติเมื่อ Download
ConfigLicense = ConfigLicense or {}
-- License Key สำหรับ resource นี้ (ได้รับจาก BuddyTH Shop)
ConfigLicense.licenseKey = "XXXX-XXXX-XXXX-XXXX"
-- หยุด resource ทันทีเมื่อ License ไม่ผ่าน (แนะนำ: true)
ConfigLicense.stopOnFail = true
02 แก้ไข fxmanifest.lua
เพิ่ม config.license.lua เข้า shared_scripts เพื่อให้ทั้ง server และ client อ่านได้
fx_version 'cerulean'
game 'gta5'
-- เพิ่ม shared_scripts ให้ทั้ง server และ client เข้าถึงได้
shared_scripts {
'config/config.license.lua', -- ← เพิ่มบรรทัดนี้
'config.lua',
}
server_scripts {
'server.lua'
}
client_scripts {
'client.lua'
}
ต้องใช้ shared_scripts เท่านั้น — ไม่ใช่ server_scripts เพียงอย่างเดียว เพราะ Client Wrapper ต้องอ่าน ConfigLicense.licenseKey ด้วย
Server Block วางไว้บนสุดของ server.lua — โค้ดต้นฉบับเดิมต่อจากนี้ได้เลย ไม่ต้องแก้ไขส่วนอื่นใด
Net Events ที่ถูกสร้าง
| Event Name | Direction | คำอธิบาย |
bdyth:sv:<resource> |
Client → Server |
Client ขอสถานะ License จาก Server |
bdyth:cl:<resource> |
Server → Client |
Server ส่งผลการตรวจสอบ (ok, msg, code) กลับ |
-- server.lua
local API_URL = "https://shop.buddyth.com/api/license/verify"
local SECRET = "YOUR_SECRET_HERE"
local licenseOk = false
local licenseMessage = "Checking..."
local licenseData = {}
local __LICENSE_READY = false
-----------------------------------------------------------------
-- ดึง IP จริงของ Server อัตโนมัติ
-----------------------------------------------------------------
local function getServerIp(callback)
PerformHttpRequest("https://api.ipify.org?format=json", function(statusCode, body)
if statusCode == 200 and body then
local ok, data = pcall(json.decode, body)
if ok and data and data.ip then
return callback(data.ip)
end
end
local ip = GetConvar("sv_endpoints", "unknown")
callback(ip)
end, "GET", "", {})
end
-----------------------------------------------------------------
-- ยิง API ตรวจสอบ License
-----------------------------------------------------------------
local function verifyLicense(callback)
getServerIp(function(serverIp)
local payload = {
license_key = (ConfigLicense and ConfigLicense.licenseKey) or "",
resource = GetCurrentResourceName(),
server_ip = serverIp,
timestamp = os.time(),
secret = SECRET
}
print("[LICENSE] Verifying resource: " .. payload.resource .. " | IP: " .. serverIp)
PerformHttpRequest(
API_URL,
function(statusCode, body, headers)
if statusCode ~= 200 or not body then
licenseOk = false
licenseMessage = "API connection failed"
licenseData = {}
return callback(false)
end
local ok, data = pcall(json.decode, body)
if not ok or type(data) ~= "table" then
licenseOk = false
licenseMessage = "Invalid API response"
licenseData = {}
return callback(false)
end
if data.status == true then
licenseOk = true
licenseMessage = data.message or "License valid"
licenseData = data.data or {}
callback(true)
else
licenseOk = false
licenseMessage = data.message or "License invalid"
licenseData = {}
callback(false)
end
end,
"POST",
json.encode(payload),
{ ["Content-Type"] = "application/json" }
)
end)
end
-----------------------------------------------------------------
-- พิมพ์ Banner
-----------------------------------------------------------------
local function printSuccessBanner()
print("^2")
print(" ███████╗██╗ ██╗ ██████╗ ██████╗ ███████╗███████╗███████╗")
print(" ██╔════╝██║ ██║██╔════╝██╔════╝ ██╔════╝██╔════╝██╔════╝")
print(" ███████╗██║ ██║██║ ██║ █████╗ ███████╗███████╗")
print(" ╚════██║██║ ██║██║ ██║ ██╔══╝ ╚════██║╚════██║")
print(" ███████║╚██████╔╝╚██████╗╚██████╗ ███████╗███████║███████║")
print(" ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝")
print("^7")
print("^2 [ LICENSE VERIFIED SUCCESSFULLY ]^7")
print("^3 Resource : " .. GetCurrentResourceName())
print("^3 Message : " .. tostring(licenseMessage))
print("^2 Status : AUTHORIZED^7")
print(" ")
end
local function printFailBanner()
print("^1")
print(" ███████╗ █████╗ ██╗██╗ ")
print(" ██╔════╝██╔══██╗██║██║ ")
print(" █████╗ ███████║██║██║ ")
print(" ██╔══╝ ██╔══██║██║██║ ")
print(" ██║ ██║ ██║██║███████╗")
print(" ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝")
print("^7")
print("^1[LICENSE] FAILED : " .. tostring(licenseMessage) .. "^7")
print("^1╔══════════════════════════════════════╗^7")
print("^1║ shop.buddyth.com ║^7")
print("^1║ LICENSE VERIFICATION ║^7")
print("^1║ STATUS : FAIL ║^7")
print("^1╚══════════════════════════════════════╝^7")
end
-----------------------------------------------------------------
-- ตรวจสอบตอน resource start
-----------------------------------------------------------------
CreateThread(function()
Wait(2000)
verifyLicense(function(ok)
__LICENSE_READY = true
if ok then
printSuccessBanner()
else
printFailBanner()
if ConfigLicense and ConfigLicense.stopOnFail then
StopResource(GetCurrentResourceName())
end
end
end)
end)
-----------------------------------------------------------------
-- รอ ESX โหลด
-----------------------------------------------------------------
Citizen.CreateThread(function()
while ESX == nil do
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
Citizen.Wait(0)
end
end)
-----------------------------------------------------------------
-- ขอ Config จาก Server
-----------------------------------------------------------------
Citizen.CreateThread(function()
while not ESX do
Citizen.Wait(100)
end
TriggerServerEvent('campfire:requestConfig')
end)
-----------------------------------------------------------------
-- รับ Config จาก Server
-----------------------------------------------------------------
RegisterNetEvent('campfire:receiveConfig')
AddEventHandler('campfire:receiveConfig', function(config)
if config then
Config = config
if Config.campfire and Config.campfire.showMarker ~= nil then
showMarkers = Config.campfire.showMarker
end
end
end)
POST
https://shop.buddyth.com/api/license/verify
Request Body
| Field | Type | ตัวอย่าง | Status |
license_key | string | ABCD-EFGH-IJKL-MNOP | required |
resource | string | my_script | required |
server_ip | string | 203.154.1.1 | required |
timestamp | integer | 1718000000 | required — Unix time |
secret | string | e7c57db6... | required — 64-char hex |
License Valid
{
"status": true,
"message": "License valid",
"code": "OK",
"data": {
"expires": "2026-12-31"
}
}
License Invalid
{
"status": false,
"message": "IP not registered",
"code": "IP_MISMATCH",
"data": {}
}
Hardcoded Behaviour: ค่าต่อไปนี้ถูก hardcode ใน wrapper และไม่สามารถปิดหรือเปลี่ยนได้จาก config ใด ๆ
stopOnFail (config)
StopResource() จะถูกเรียกเมื่อ License ไม่ผ่าน หากตั้ง ConfigLicense.stopOnFail = true
Secret Verification
ทุก request ต้องส่ง SECRET 64-char hex ที่ตรงกับ Database จึงจะผ่านการตรวจสอบ
2s Startup Delay
ระบบรอ 2 วินาทีหลัง resource start ก่อนยิง API เพื่อให้ทุก dependency โหลดเสร็จก่อน
IP Auto-Detect
ดึง Public IP จาก api.ipify.org อัตโนมัติ — fallback เป็น sv_endpoints หาก API ล่ม
| Code | ความหมาย | วิธีแก้ไข |
OK |
License ผ่าน |
— |
IP_MISMATCH |
IP ของ Server ไม่ตรงกับที่ลงทะเบียน |
อัปเดต Server IP ในหน้า Dashboard หรือติดต่อ Admin |
KEY_NOT_FOUND |
ไม่พบ License Key ในระบบ |
ตรวจสอบ licenseKey ใน config.license.lua |
EXPIRED |
License หมดอายุแล้ว |
ต่ออายุ License ผ่านหน้า BuddyTH Shop |
INACTIVE |
License ถูก disable |
ติดต่อ Admin เพื่อเปิดใช้งาน |
SECRET_MISMATCH |
Secret ไม่ตรงกับใน Database |
Download สคริปต์ใหม่จากระบบเพื่อรับ Secret ล่าสุด |
NETWORK_ERROR |
เชื่อมต่อ API ไม่ได้ |
ตรวจสอบ sv_endpoints และ firewall ของ Server |
TIMEOUT |
Client ไม่ได้รับสถานะจาก Server |
ตรวจสอบว่า _TAG ทั้ง server/client ตรงกัน |
ต้องเปลี่ยน Secret เมื่อไหร่?
Secret ถูก generate ครั้งเดียวตอนสร้าง License และจะฝังอยู่ใน server wrapper อัตโนมัติเมื่อ Download
ไม่ต้องเปลี่ยนเว้นแต่สงสัยว่ามีการ leak — ในกรณีนั้นติดต่อ Admin เพื่อ Regenerate
ใช้กับ QBCore ได้ไหม?
ได้ — Server Wrapper ใช้ esx:playerLoaded สำหรับ push สถานะให้ผู้เล่นที่ join ทีหลัง
สำหรับ QBCore ให้เปลี่ยนเป็น QBCore:Server:PlayerLoaded แทน
1 License ใช้ได้กี่ Server?
1 License ต่อ 1 Server IP — หากต้องการใช้หลาย Server ต้องซื้อ License แยกต่างหาก
สามารถอัปเดต IP ได้ผ่านหน้า Dashboard
ตรวจสอบ License ทุกครั้งที่ Server Restart?
ใช่ — ทุกครั้งที่ resource start จะมีการ verify กับ API ใหม่ทันที ไม่มี cache
โค้ดต้นฉบับปลอดภัยแค่ไหน?
ระบบ License ตรวจสอบ Key, Server IP และ Secret ทุกครั้งที่ resource start
หาก License ไม่ผ่านและตั้ง stopOnFail = true — resource จะถูกหยุดทันที ป้องกันการใช้งานโดยไม่ได้รับอนุญาต
BuddyTH License System Docs — v2.0