There are perhaps hundreds if not thousands of articles on obtaining your visitor’s IP address. The majority if these entries will refer to a small subset of global $_SERVER variables (HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP, and REMOTE_ADDR). Although both fast and simple solutions utilizing nested ternary operations exist, they are generally prone to a fairly large bug. The HTTP_X_FORWARDED_FOR server directive may contain a comma delimited list of IP addresses based upon several proxy hops prior to the client request packet reaching it’s destination.
After scouring the web I came across two sites demonstrating what appears to be the most accurate IP retrieval method I have come across. I found a number of inefficiencies in the two functions so I’m going to provide you with my optimized version.
A Word of Warning
$_SERVER['REMOTE_ADDR'] still represents the most reliable source of an IP address. The other $_SERVER variables mentioned above can be spoofed by a remote client very easily. The purpose of this solution is to attempt to determine the IP address of a client sitting behind a proxy. For your general purposes, you might consider using this in combination with the IP returned directly from $_SERVER[‘REMOTE_ADDR’] and storing both. For 99.9% of users this solution will suit your needs perfectly. It will not protect you from the 0.1% of malicious users looking to abuse your system by injecting their own request headers. If relying on IP addresses for something mission critical, stick to
$_SERVER['REMOTE_ADDR'] and don’t bother catering to those behind a proxy.
Update: An Optimized Solution
The following solution was the result of a StackOverflow post and makes use of PHP’s filter_var(). It’s much more succinct and easier to understand given the usage of pre-defined constants:
My Original Solution
- I added isset and !empty checks within get_ip_address() to avoid a wasted call to validate_ip() if no IP needed checking
- In the case of the $_SERVER[‘HTTP_X_FORWARDED_FOR’] variable I added a call to strpos to determine if the variable contained a comma separated list of IPs prior to using explode and iterating over a possibly non-existent array.
- In validate_ip(), I did a check against the value “unknown” as it has been noted this header value is set on occasion. The check is done using strtolower to ensure that the cases match.
- I converted the array of private network IP address ranges to their signed integer counterparts using ip2long to speed things up as these values are merely used as a lookup.
- I removed the array and foreach loop and replaced it with a series of if() blocks to test whether the ip falls within invalid ranges. Since the foreach loop no longer existed, I only need one conversion from the IP address to a signed integer.
- As noted in the php.net documentation for ip2long, I convert the IP address to an unsigned integer using sprintf. This alleviates the issue of the ip being converted to a negative signed integer on 32 bit machines.
- Since the ip is now an unsigned integer, the last if() statement can be simplified to only check that the IP is greater than the lower bound since the range carries all the way to 255.255.255.255.
One thing to note is the location I placed the unsigned integer conversion in validate_ip(). It appears after the check to determine if the IP was valid because ip2long returns a -1 in the event that the ip matches 255.255.255.255. If we have converted to an unsigned integer prior to the if statement, the IP would be considered valid. Doing the unsigned int conversion also allows us to simplify the last range of IPs because we know anything equal to 255.255.255.255 did not enter the loop. Other simplifications included a quick check for !empty on $_SERVER variables prior to hitting validate_ip() as we dont need the overhead from making that call if we already know the variable to be invalid.