require 'ipaddr'

# Based on ActionDispatch::RemoteIp. All security-related precautions from that
# middleware have been removed, because the Event IP just needs to be accurate,
# and spoofing an IP here only makes data inaccurate, not insecure. Don't re-use
# this module if you have to *trust* the IP address.
module Raven
  module Utils
    class RealIp
      LOCAL_ADDRESSES = [
        "127.0.0.1",      # localhost IPv4
        "::1",            # localhost IPv6
        "fc00::/7",       # private IPv6 range fc00::/7
        "10.0.0.0/8",     # private IPv4 range 10.x.x.x
        "172.16.0.0/12",  # private IPv4 range 172.16.0.0 .. 172.31.255.255
        "192.168.0.0/16", # private IPv4 range 192.168.x.x
      ].map { |proxy| IPAddr.new(proxy) }

      attr_accessor :ip, :ip_addresses

      def initialize(ip_addresses)
        self.ip_addresses = ip_addresses
      end

      def calculate_ip
        # CGI environment variable set by Rack
        remote_addr = ips_from(ip_addresses[:remote_addr]).last

        # Could be a CSV list and/or repeated headers that were concatenated.
        client_ips    = ips_from(ip_addresses[:client_ip])
        real_ips      = ips_from(ip_addresses[:real_ip])
        forwarded_ips = ips_from(ip_addresses[:forwarded_for])

        ips = [client_ips, real_ips, forwarded_ips, remote_addr].flatten.compact

        # If every single IP option is in the trusted list, just return REMOTE_ADDR
        self.ip = filter_local_addresses(ips).first || remote_addr
      end

      protected

      def ips_from(header)
        # Split the comma-separated list into an array of strings
        ips = header ? header.strip.split(/[,\s]+/) : []
        ips.select do |ip|
          begin
            # Only return IPs that are valid according to the IPAddr#new method
            range = IPAddr.new(ip).to_range
            # we want to make sure nobody is sneaking a netmask in
            range.begin == range.end
          rescue ArgumentError
            nil
          end
        end
      end

      def filter_local_addresses(ips)
        ips.reject { |ip| LOCAL_ADDRESSES.any? { |proxy| proxy === ip } }
      end
    end
  end
end
