feat(http): client IP address behind trusted proxies - #2244
Conversation
Benchmark ResultsComparison of Open to see the benchmark results
Generated by phpbench against commit b8031ab |
|
Perhaps we could make this a bit more exhaustive if we're adding it. |
|
@xHeaven You mean |
| use Tempest\Http\Ip\TrustedProxiesConfig; | ||
|
|
||
| return new TrustedProxiesConfig( | ||
| proxies: ['10.0.0.0/8'], |
There was a problem hiding this comment.
Ok so, one thought I have it whether we could introduce an IpAddress value object that's used for the request's property and (optionally) in this config as well.
I believe comparing IPs is more involved than a simple === operation on strings, so maybe it could have an $request->ip->equals($ip) method?
| * matches('10.0.1.24', '::/0'); // false | ||
| * ``` | ||
| */ | ||
| function matches(string $ip, string $range): bool |
There was a problem hiding this comment.
With this newly added value object, this function's implementation would be moved to the value object.
The function can be kept, but should be called ip_matches. Same for the other functions
| * is_private('203.0.113.9'); // false | ||
| * ``` | ||
| */ | ||
| function is_private(string $ip): bool |
| * | ||
| * @internal | ||
| */ | ||
| function to_bytes(string $ip): ?string |
| * | ||
| * @param string[] $ranges | ||
| */ | ||
| function matches_any(string $ip, array $ranges): bool |
Right now, getting the IP address a request came from means digging into
$_SERVERyourself. The PSR request has it inREMOTE_ADDR, but the mapper toGenericRequestthrows the server params away.This PR adds
Request::$ip:It's
nullwhen the server doesn't report an address. In tests,fromIpsets it:Trusted proxies
Behind a reverse proxy you get the proxy's address, and the client's real one sits in a header like
X-Forwarded-For. Anyone can set that header, so it's only read if the request came through a proxy you've declared. Nothing is trusted by default:Addresses and CIDR ranges both work, IPv4 and IPv6. In a chain of hops, the closest one that isn't a trusted proxy is the client. Header names are configurable too, for the likes of Cloudflare. Docs included, under routing.
Notes
ipis now a reserved request parameter name, since it's a property on theRequestinterface. If a payload has a field calledipand gets mapped onto a custom request object, it'll throwRequestParametersIncludedReservedNames, same as it already does forpath,queryand so on. Probably worth a line in the release notes.iprather thanclientIpto keep it short, like Laravel's$request->ip().Tempest\Support\Iprather than the HTTP package, since it seemed generally useful:matches(),matches_any(),is_private().Included changes
Request::$ipon the interface and inIsRequestTrustedProxiesConfigandClientIpResolverinTempest\Http\Ip,Tempest\Support\Ipfunctions andPRIVATE_RANGESPsrRequestToGenericRequestMapperresolves the address,RequestToPsrRequestMapperwrites it back for the tester's dispatch, andRequestToObjectMappercarries it onto custom request objectsHttpRouterTester::fromIp(), across all verb methods andmakePsrRequest