Current File : //etc/mass_hosting.lua
-- vim: set ts=2 expandtab:

require "apache2"

-- NOTE: apache2.DECLINED simply means that we're returning without modifying the request at all

-- Converts an IP string into an integer for trivial comparison
function ip_to_int(ip)
  oct1, oct2, oct3, oct4 = string.match(ip, "(%d+)%.(%d+)%.(%d+)%.(%d+)")
  return ((oct1 * 256 + oct2)*256+oct3)*256+oct4
end

network_list = {}

network_list_file = '/etc/mass_hosting.list'

-- Doing this here rather than in mass_hosting() provides a speed improvement under certain configurations
test_file = io.open(network_list_file)
if test_file then
  for network in test_file:lines() do
    table.insert(network_list, network)
  end
  io.close(test_file)
end

function mass_hosting(r)
  test_ip = r.useragent_ip
  --[[
  Handle Ezoic customers.
  This header is akin to X-Forwarded-For.
  This has the potential to be abused, but Ezoic uses EC2 so we're short on viable
  alternatives at this time.
  ]]--
  if r.headers_in['X-Middleton-Ip'] then
    test_ip = r.headers_in['X-Middleton-Ip']
  end

  -- We only care about POST requests right now
  if r.method ~= 'POST' then
    return apache2.DECLINED
  end

  -- Check for IPv4 IPs
  if not string.match(test_ip, "%d+%.%d+%.%d+%.%d+") then
    r:warn("useragent_ip is not an ipv4 IP; declining")
    return apache2.DECLINED
  end

  ip_int_rep = ip_to_int(test_ip)

  for idx, network in ipairs(network_list) do
    -- Ignore comments
    if string.find(network, '%#') == 1 then
      goto continue
    end
    net_addr, maskbits = string.match(network, "(%d+%.%d+%.%d+%.%d+)/(%d+)")

    -- Figure out how many host bits we're working with
    hostbits = 32 - maskbits

    --integer representation of the starting and ending IPs of the network space
    base_int_rep = ip_to_int(net_addr)
    max_int_rep = base_int_rep + (2^hostbits)

    if (ip_int_rep >= base_int_rep and ip_int_rep <= max_int_rep) then
      -- Found the connecting IP in a mass hosting network so return HTTP_FORBIDDEN
      r:debug(string.format("Found %s inside %s", test_ip, network))
      r.status = 403
      return apache2.DONE
    end
    ::continue::
  end
  -- If we got this far, we didn't find the IP listed so do nothing
  r:debug(string.format("Unable to find %s inside any known networks", test_ip))
  return apache2.DECLINED
end