- Remove Go artifacts (cmd/, internal/, pkg/, go.mod) - Move furt-lua/* content to repository root - Restructure as clean src/, config/, scripts/, tests/ layout - Rewrite README.md as practical tool documentation - Remove timeline references and marketing language - Clean .gitignore from Go-era artifacts - Update config/server.lua with example.org defaults - Add .env.production to .gitignore for security Repository now ready for open source distribution with minimal, focused structure and generic configuration templates. close issue DAW/furt#86
117 lines
3.2 KiB
Lua
117 lines
3.2 KiB
Lua
-- furt-lua/src/ip_utils.lua
|
|
-- IP address and CIDR utilities
|
|
-- Dragons@Work Digital Sovereignty Project
|
|
|
|
local IpUtils = {}
|
|
|
|
-- Simple bitwise AND for Lua 5.1 compatibility
|
|
local function bitwise_and(a, b)
|
|
local result = 0
|
|
local bit = 1
|
|
while a > 0 or b > 0 do
|
|
if (a % 2 == 1) and (b % 2 == 1) then
|
|
result = result + bit
|
|
end
|
|
a = math.floor(a / 2)
|
|
b = math.floor(b / 2)
|
|
bit = bit * 2
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Create subnet mask for given CIDR bits
|
|
local function create_mask(mask_bits)
|
|
if mask_bits >= 32 then
|
|
return 0xFFFFFFFF
|
|
elseif mask_bits <= 0 then
|
|
return 0
|
|
else
|
|
-- Create mask: 32-bit with 'mask_bits' ones from left
|
|
local mask = 0
|
|
for i = 0, mask_bits - 1 do
|
|
mask = mask + math.pow(2, 31 - i)
|
|
end
|
|
return mask
|
|
end
|
|
end
|
|
|
|
-- CIDR IP matching function (Lua 5.1 compatible)
|
|
function IpUtils.ip_matches_cidr(ip, cidr)
|
|
if not cidr:find("/") then
|
|
-- No subnet mask, direct comparison
|
|
return ip == cidr
|
|
end
|
|
|
|
local network, mask_bits = cidr:match("([^/]+)/(%d+)")
|
|
if not network or not mask_bits then
|
|
return false
|
|
end
|
|
|
|
mask_bits = tonumber(mask_bits)
|
|
|
|
-- Simple IPv4 CIDR matching
|
|
if ip:find("%.") and network:find("%.") then
|
|
-- Convert IPv4 to number
|
|
local function ip_to_num(ip_str)
|
|
local parts = {}
|
|
for part in ip_str:gmatch("(%d+)") do
|
|
table.insert(parts, tonumber(part))
|
|
end
|
|
if #parts == 4 then
|
|
return (parts[1] * 16777216) + (parts[2] * 65536) + (parts[3] * 256) + parts[4]
|
|
end
|
|
return 0
|
|
end
|
|
|
|
local ip_num = ip_to_num(ip)
|
|
local network_num = ip_to_num(network)
|
|
|
|
-- Create subnet mask
|
|
local mask = create_mask(mask_bits)
|
|
|
|
-- Apply mask to both IPs and compare
|
|
return bitwise_and(ip_num, mask) == bitwise_and(network_num, mask)
|
|
end
|
|
|
|
-- Fallback: if CIDR parsing fails, allow if IP matches network part
|
|
return ip == network or ip:find("^" .. network:gsub("%.", "%%."))
|
|
end
|
|
|
|
-- Check if IP is in allowed list
|
|
function IpUtils.is_ip_allowed(client_ip, allowed_ips)
|
|
if not allowed_ips or #allowed_ips == 0 then
|
|
return true -- No restrictions
|
|
end
|
|
|
|
for _, allowed_cidr in ipairs(allowed_ips) do
|
|
if IpUtils.ip_matches_cidr(client_ip, allowed_cidr) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Extract client IP (considering proxies)
|
|
function IpUtils.get_client_ip(request)
|
|
-- Check for forwarded IP headers first
|
|
local forwarded_for = request.headers["x-forwarded-for"]
|
|
if forwarded_for then
|
|
-- Take first IP from comma-separated list
|
|
local first_ip = forwarded_for:match("([^,]+)")
|
|
if first_ip then
|
|
return first_ip:match("^%s*(.-)%s*$") -- trim whitespace
|
|
end
|
|
end
|
|
|
|
local real_ip = request.headers["x-real-ip"]
|
|
if real_ip then
|
|
return real_ip
|
|
end
|
|
|
|
-- Fallback to connection IP (would need socket info, defaulting to localhost for now)
|
|
return "127.0.0.1"
|
|
end
|
|
|
|
return IpUtils
|
|
|