-- 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