* Add rate limiting to cart endpoints based on session

* Handle nonce and rate checks in permission_callback

* Rate limit checkout only

* Debug

* Unused AbstractRoute

* Code standards

* Modify core rate limit table

* Add rate limit at rest api level, not route level

* Rate limit helper

* Remove rate limit from routes

* Usused dep

* Remove custom error logic no longer needed

* Remove dependency

* Remove custom permission_callback

* Hash IP and handle null

* Remove error response handler

* revert error_to_response changes

* Remove add_response_headers

* Remove IDENTIFIER

* Remove white space

* Increase limit

* Missing class comment

* Move rate limiting code within store api codebase

* white space

* Fix return type

* Check rate limit expiry greater than now

* Remove x- prefix

* reorder functions

* remove table

* pass request to add_nonce_headers

* return early and avoid elseif on AbstractCartRoute:get_response()

* Refactor get_ip_address() before implementing options for functionality

* Change rate limit to 5 requests

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>

* Change rate limit window to 60 seconds

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>

* Disable rate limiting by default

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>

* Updated limits comment

* Example for Forwarded header

* Updated "woocommerce_store_api_enable_rate_limit_check" filter doc

* Added filter for the Store API rate limit check proxy support

* Add an action here that carries over the IP address being blocked.

* Added logic around setting the action_id, and returns an error when ip cannot be determined for users not logged in.

* Renamed action for limit exceeded.

* Common rate limiting header naming prefix, and fixed comment typos.

* Doc for Rate Limiting (wip)

* Example for Rate Limiting docs

* Remove private IP range block for rate limiting

* Refactored get_response() to add nonce headers to response instead of request

* Disable batching for Checkout calls to prevent bypassing Rate Limiting.

* Removed redundant arg.

* package-lock.json update

* Removed repeated func calls.

* Fix failing tests.

* Tests wip.

* Request limit and timeframe are now constants for RateLimits utility class.

* Tests for Rate Limit headers.

* Reverted PHPUnit config to enable all tests again.

* Update src/StoreApi/Authentication.php comment wording

Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>

* Removed possibly unnecessary get_ip_address() call.

* Changed wording on comment for get_ip_address() method.

* Simplified validate_ip() method.

* Fixed wrong header entry for "Forwarded" check.

* Unit testing for Authentication::get_ip_address()

* Comment explaining the reason to use ReflectionClass for testing get_ip_address().

* Support for error output outside batch request.

* MD linting.

* Refactor to implement options through a single filter.

* fixed md lint error and config file

* reverted accidental default func arg value removal

* re-enabled batch support for checkout

* action for limit exceed now also triggered in case we can't resolve the IP.

* Doc tweak.

* Return unresolved IP address when REMOTE_ADDR isn't set with proxy support disabled.

* Group unresolved ips for rate limiting

* Fixed bug where current limit wasn't properly initialized.

Co-authored-by: Nadir Seghir <nadir.seghir@gmail.com>
Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com>
Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
This commit is contained in:
Mike Jolley 2022-11-04 15:53:00 +00:00 committed by GitHub
parent 1884040a6a
commit 07a612f575
7 changed files with 621 additions and 9 deletions

View File

@ -188,7 +188,7 @@ export const controls = {
if ( errorResponse.body ) {
reject( errorResponse.body );
} else {
reject();
reject( errorResponse );
}
} );
}

View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "@woocommerce/block-library",
"version": "8.8.0-dev",
"version": "8.9.0-dev",
"hasInstallScript": true,
"license": "GPL-3.0+",
"dependencies": {

View File

@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\StoreApi\Utilities\RateLimits;
/**
* Authentication class.
*/
@ -20,15 +22,51 @@ class Authentication {
* @return \WP_Error|null|bool
*/
public function check_authentication( $result ) {
if ( ! empty( $result ) ) {
if ( ! $this->is_request_to_store_api() ) {
return $result;
}
if ( $this->is_request_to_store_api() ) {
return true;
$rate_limiting_options = RateLimits::get_options();
if ( $rate_limiting_options->enabled ) {
$action_id = 'store_api_request_';
if ( is_user_logged_in() ) {
$action_id .= get_current_user_id();
} else {
$ip_address = self::get_ip_address( $rate_limiting_options->proxy_support );
$action_id .= md5( $ip_address );
}
$retry = RateLimits::is_exceeded_retry_after( $action_id );
$server = rest_get_server();
$server->send_header( 'RateLimit-Limit', $rate_limiting_options->limit );
if ( false !== $retry ) {
$server->send_header( 'RateLimit-Retry-After', $retry );
$server->send_header( 'RateLimit-Remaining', 0 );
$server->send_header( 'RateLimit-Reset', time() + $retry );
$ip_address = $ip_address ?? self::get_ip_address( $rate_limiting_options->proxy_support );
do_action( 'woocommerce_store_api_rate_limit_exceeded', $ip_address );
return new \WP_Error(
'rate_limit_exceeded',
sprintf(
'Too many requests. Please wait %d seconds before trying again.',
$retry
),
array( 'status' => 400 )
);
}
$rate_limit = RateLimits::update_rate_limit( $action_id );
$server->send_header( 'RateLimit-Remaining', $rate_limit->remaining );
$server->send_header( 'RateLimit-Reset', $rate_limit->reset );
}
return $result;
// Pass through errors from other authentication methods used before this one.
return ! empty( $result ) ? $result : true;
}
/**
@ -55,4 +93,86 @@ class Authentication {
}
return 0 === strpos( $GLOBALS['wp']->query_vars['rest_route'], '/wc/store/' );
}
/**
* Get current user IP Address.
*
* X_REAL_IP and CLIENT_IP are custom implementations designed to facilitate obtaining a user's ip through proxies, load balancers etc.
*
* _FORWARDED_FOR (XFF) request header is a de-facto standard header for identifying the originating IP address of a client connecting to a web server through a proxy server.
* Note for X_FORWARDED_FOR, Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2.
* Make sure we always only send through the first IP in the list which should always be the client IP.
* Documentation at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
*
* Forwarded request header contains information that may be added by reverse proxy servers (load balancers, CDNs, and so on).
* Documentation at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
* Full RFC at https://datatracker.ietf.org/doc/html/rfc7239
*
* @param boolean $proxy_support Enables/disables proxy support.
*
* @return string
*/
protected static function get_ip_address( bool $proxy_support = false ) {
if ( ! $proxy_support ) {
return self::validate_ip( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? 'unresolved_ip' ) ) );
}
if ( array_key_exists( 'HTTP_X_REAL_IP', $_SERVER ) ) {
return self::validate_ip( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ) );
}
if ( array_key_exists( 'HTTP_CLIENT_IP', $_SERVER ) ) {
return self::validate_ip( sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ) );
}
if ( array_key_exists( 'HTTP_X_FORWARDED_FOR', $_SERVER ) ) {
$ips = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
if ( is_array( $ips ) && ! empty( $ips ) ) {
return self::validate_ip( trim( $ips[0] ) );
}
}
if ( array_key_exists( 'HTTP_FORWARDED', $_SERVER ) ) {
// Using regex instead of explode() for a smaller code footprint.
// Expected format: Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43,for="[2001:db8:cafe::17]:4711"...
preg_match(
'/(?<=for\=)[^;,]*/i', // We catch everything on the first "for" entry, and validate later.
sanitize_text_field( wp_unslash( $_SERVER['HTTP_FORWARDED'] ) ),
$matches
);
if ( strpos( $matches[0] ?? '', '"[' ) !== false ) { // Detect for ipv6, eg "[ipv6]:port".
preg_match(
'/(?<=\[).*(?=\])/i', // We catch only the ipv6 and overwrite $matches.
$matches[0],
$matches
);
}
if ( ! empty( $matches ) ) {
return self::validate_ip( trim( $matches[0] ) );
}
}
return '0.0.0.0';
}
/**
* Uses filter_var() to validate and return ipv4 and ipv6 addresses
* Will return 0.0.0.0 if the ip is not valid. This is done to group and still rate limit invalid ips.
*
* @param string $ip ipv4 or ipv6 ip string.
*
* @return string
*/
protected static function validate_ip( $ip ) {
$ip = filter_var(
$ip,
FILTER_VALIDATE_IP,
array( FILTER_FLAG_NO_RES_RANGE, FILTER_FLAG_IPV6 )
);
return $ip ?: '0.0.0.0';
}
}

View File

@ -69,6 +69,16 @@ abstract class AbstractCartRoute extends AbstractRoute {
$this->order_controller = new OrderController();
}
/**
* Are we updating data or getting data?
*
* @param \WP_REST_Request $request Request object.
* @return boolean
*/
protected function is_update_request( \WP_REST_Request $request ) {
return in_array( $request->get_method(), [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true );
}
/**
* Get the route response based on the type of request.
*
@ -97,8 +107,10 @@ abstract class AbstractCartRoute extends AbstractRoute {
}
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
} elseif ( in_array( $request->get_method(), [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) {
return $this->error_to_response( $response );
}
if ( $this->is_update_request( $request ) ) {
$this->cart_updated( $request );
}
@ -192,7 +204,7 @@ abstract class AbstractCartRoute extends AbstractRoute {
* @return bool
*/
protected function requires_nonce( \WP_REST_Request $request ) {
return 'GET' !== $request->get_method();
return $this->is_update_request( $request );
}
/**

View File

@ -0,0 +1,245 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Utilities;
use WC_Rate_Limiter;
use WC_Cache_Helper;
/**
* RateLimits class.
*/
class RateLimits extends WC_Rate_Limiter {
/**
* Cache group.
*/
const CACHE_GROUP = 'store_api_rate_limit';
/**
* Rate limiting enabled default value.
*
* @var boolean
*/
const ENABLED = false;
/**
* Proxy support enabled default value.
*
* @var boolean
*/
const PROXY_SUPPORT = false;
/**
* Default amount of max requests allowed for the defined timeframe.
*
* @var int
*/
const LIMIT = 25;
/**
* Default time in seconds before rate limits are reset.
*
* @var int
*/
const SECONDS = 10;
/**
* Gets a cache prefix.
*
* @param string $action_id Identifier of the action.
* @return string
*/
protected static function get_cache_key( $action_id ) {
return WC_Cache_Helper::get_cache_prefix( 'store_api_rate_limit' . $action_id );
}
/**
* Get current rate limit row from DB and normalize types. This query is not cached, and returns
* a new rate limit row if none exists.
*
* @param string $action_id Identifier of the action.
* @return object Object containing reset and remaining.
*/
protected static function get_rate_limit_row( $action_id ) {
global $wpdb;
$row = $wpdb->get_row(
$wpdb->prepare(
"
SELECT rate_limit_expiry as reset, rate_limit_remaining as remaining
FROM {$wpdb->prefix}wc_rate_limits
WHERE rate_limit_key = %s
AND rate_limit_expiry > %s
",
$action_id,
time()
),
'OBJECT'
);
if ( empty( $row ) ) {
$options = self::get_options();
return (object) [
'reset' => (int) $options->seconds + time(),
'remaining' => (int) $options->limit,
];
}
return (object) [
'reset' => (int) $row->reset,
'remaining' => (int) $row->remaining,
];
}
/**
* Returns current rate limit values using cache where possible.
*
* @param string $action_id Identifier of the action.
* @return object
*/
public static function get_rate_limit( $action_id ) {
$current_limit = self::get_cached( $action_id );
if ( false === $current_limit ) {
$current_limit = self::get_rate_limit_row( $action_id );
self::set_cache( $action_id, $current_limit );
}
return $current_limit;
}
/**
* If exceeded, seconds until reset.
*
* @param string $action_id Identifier of the action.
*
* @return bool|int
*/
public static function is_exceeded_retry_after( $action_id ) {
$current_limit = self::get_rate_limit( $action_id );
// Before the next run is allowed, retry forbidden.
if ( time() <= $current_limit->reset && 0 === $current_limit->remaining ) {
return (int) $current_limit->reset - time();
}
// After the next run is allowed, retry allowed.
return false;
}
/**
* Sets the rate limit delay in seconds for action with identifier $id.
*
* @param string $action_id Identifier of the action.
* @return object Current rate limits.
*/
public static function update_rate_limit( $action_id ) {
global $wpdb;
$options = self::get_options();
$rate_limit_expiry = time() + $options->seconds;
$wpdb->query(
$wpdb->prepare(
"INSERT INTO {$wpdb->prefix}wc_rate_limits
(`rate_limit_key`, `rate_limit_expiry`, `rate_limit_remaining`)
VALUES
(%s, %d, %d)
ON DUPLICATE KEY UPDATE
`rate_limit_remaining` = IF(`rate_limit_expiry` < %d, VALUES(`rate_limit_remaining`), GREATEST(`rate_limit_remaining` - 1, 0)),
`rate_limit_expiry` = IF(`rate_limit_expiry` < %d, VALUES(`rate_limit_expiry`), `rate_limit_expiry`);
",
$action_id,
$rate_limit_expiry,
$options->limit - 1,
time(),
time()
)
);
$current_limit = self::get_rate_limit_row( $action_id );
self::set_cache( $action_id, $current_limit );
return $current_limit;
}
/**
* Retrieve a cached store api rate limit.
*
* @param string $action_id Identifier of the action.
* @return bool|object
*/
protected static function get_cached( $action_id ) {
return wp_cache_get( self::get_cache_key( $action_id ), self::CACHE_GROUP );
}
/**
* Cache a rate limit.
*
* @param string $action_id Identifier of the action.
* @param object $current_limit Current limit object with expiry and retries remaining.
* @return bool
*/
protected static function set_cache( $action_id, $current_limit ) {
return wp_cache_set( self::get_cache_key( $action_id ), $current_limit, self::CACHE_GROUP );
}
/**
* Return options for Rate Limits, to be returned by the "woocommerce_store_api_rate_limit_options" filter.
*
* @return object Default options.
*/
public static function get_options() {
$default_options = [
/**
* Filters the Store API rate limit check, which is disabled by default.
*
* This can be used also to disable the rate limit check when testing API endpoints via a REST API client.
*/
'enabled' => self::ENABLED,
/**
* Filters whether proxy support is enabled for the Store API rate limit check. This is disabled by default.
*
* If the store is behind a proxy, load balancer, CDN etc. the user can enable this to properly obtain
* the client's IP address through standard transport headers.
*/
'proxy_support' => self::PROXY_SUPPORT,
'limit' => self::LIMIT,
'seconds' => self::SECONDS,
];
return (object) array_merge( // By using array_merge we ensure we get a properly populated options object.
$default_options,
/**
* Filters options for Rate Limits.
*
* @param array $rate_limit_options Array of option values.
* @return array
*/
apply_filters(
'woocommerce_store_api_rate_limit_options',
$default_options
)
);
}
/**
* Gets a single option through provided name.
*
* @param string $option Option name.
*
* @return mixed
*/
public static function get_option( $option ) {
if ( ! is_string( $option ) || ! defined( 'RateLimits::' . strtoupper( $option ) ) ) {
return null;
}
return self::get_options()[ $option ];
}
}

View File

@ -0,0 +1,94 @@
# Rate Limiting for Store API endpoints <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [Limit information](#limit-information)
- [Methods restricted by Rate Limiting](#methods-restricted-by-rate-limiting)
- [Rate Limiting options filter](#rate-limiting-options-filter)
- [Proxy standard support](#proxy-standard-support)
- [Limit usage information observability](#limit-usage-information-observability)
- [Response headers example](#response-headers-example)
- [Tracking limit abuses](#tracking-limit-abuses)
- [Custom tracking usage example](#custom-tracking-usage-example)
[Rate Limiting](https://github.com/woocommerce/woocommerce-blocks/pull/5962) is available for Store API endpoints. This is optional and disabled by default. It can be enabled by following [these instructions](#rate-limiting-options-filter).
The main purpose prevent abuse on endpoints from excessive calls and performance degradation on the machine running the store.
Rate limit tracking is controlled by either `USER ID` (logged in) or `IP ADDRESS` (unauthenticated requests).
It also offers standard support for running behind a proxy, load balancer, etc. This also optional and disabled by default.
## Limit information
A default maximum of 25 requests can be made within a 10-second time frame. These can be changed through an [options filter](#rate-limiting-options-filter).
## Methods restricted by Rate Limiting
`POST`, `PUT`, `PATCH`, and `DELETE`
## Rate Limiting options filter
A filter is available for setting options for rate limiting:
```php
add_filter( 'woocommerce_store_api_rate_limit_options', function() {
return [
'enabled' => RateLimits::ENABLED, // enables/disables Rate Limiting. Default: false
'proxy_support' => RateLimits::PROXY_SUPPORT, // enables/disables Proxy support. Default: false
'limit' => RateLimits::LIMIT, // limit of request per timeframe. Default: 25
'seconds' => RateLimits::SECONDS, // timeframe in seconds. Default: 10
];
} );
```
## Proxy standard support
If the Store is running behind a proxy, load balancer, cache service, CDNs, etc. keying limits by IP is supported through standard IP forwarding headers, namely:
- `X_REAL_IP`|`CLIENT_IP` *Custom popular implementations that simplify obtaining the origin IP for the request*
- `X_FORWARDED_FOR` *De-facto standard header for identifying the originating IP, [Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)*
- `X_FORWARDED` *[Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded), [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)*
This is disabled by default.
## Limit usage information observability
Current limit information can be observed via custom response headers:
- `RateLimit-Limit` *Maximum requests per time frame.*
- `RateLimit-Remaining` *Requests available during current time frame.*
- `RateLimit-Reset` *Unix timestamp of next time frame reset.*
- `RateLimit-Retry-After` *Seconds until requests are unblocked again. Only shown when the limit is reached.*
### Response headers example
```http
RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 1654880642
RateLimit-Retry-After: 28
```
## Tracking limit abuses
This uses a modified wc_rate_limit table with an additional remaining column for tracking the request count in any given request window.
A custom action `woocommerce_store_api_rate_limit_exceeded` was implemented for extendability in tracking such abuses.
### Custom tracking usage example
```php
add_action(
'woocommerce_store_api_rate_limit_exceeded',
function ( $offending_ip ) { /* Custom tracking implementation */ }
);
```
---
[We're hiring!](https://woocommerce.com/careers/) Come work with us!
🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./src/StoreApi/docs/rate-limiting.md)
<!-- /FEEDBACK -->

View File

@ -0,0 +1,141 @@
<?php
/**
* Rate Limits Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\StoreApi;
use Automattic\WooCommerce\StoreApi\Authentication;
use Automattic\WooCommerce\StoreApi\Utilities\RateLimits;
use ReflectionClass;
use ReflectionException;
use Spy_REST_Server;
use WP_REST_Server;
use WP_Test_REST_TestCase;
/**
* ControllerTests
*/
class RateLimitsTests extends WP_Test_REST_TestCase {
/**
* Setup Rest API server.
*/
public function setUp() {
/** @var WP_REST_Server $wp_rest_server */
global $wp_rest_server;
$wp_rest_server = new Spy_REST_Server();
$GLOBALS['wp']->query_vars['rest_route'] = '/wc/store/cart';
$_SERVER['REMOTE_ADDR'] = '76.45.67.179';
do_action( 'rest_api_init', $wp_rest_server );
}
/**
* Tests that Rate limiting headers are sent and set correctly when Rate Limiting
* main functionality is enabled.
*
* @return void
*/
public function test_rate_limits_response_headers() {
add_filter(
'woocommerce_store_api_rate_limit_options',
function () {
return array( 'enabled' => true );
}
);
$rate_limiting_options = RateLimits::get_options();
/** @var Spy_REST_Server $spy_rest_server */
$spy_rest_server = rest_get_server();
$spy_rest_server->serve_request( '/wc/store/cart' );
$this->assertArrayHasKey( 'RateLimit-Limit', $spy_rest_server->sent_headers );
$this->assertArrayHasKey( 'RateLimit-Remaining', $spy_rest_server->sent_headers );
$this->assertArrayHasKey( 'RateLimit-Reset', $spy_rest_server->sent_headers );
$this->assertEquals( $rate_limiting_options->limit, $spy_rest_server->sent_headers['RateLimit-Limit'] );
$this->assertTrue( $spy_rest_server->sent_headers['RateLimit-Remaining'] > 0 );
$this->assertIsInt( $spy_rest_server->sent_headers['RateLimit-Reset'] );
$this->assertGreaterThan( time(), $spy_rest_server->sent_headers['RateLimit-Reset'] );
// Exhaust the limit.
do {
$remaining = $spy_rest_server->sent_headers['RateLimit-Remaining'];
$spy_rest_server->serve_request( '/wc/store/cart' );
$this->assertEquals( $rate_limiting_options->limit, $spy_rest_server->sent_headers['RateLimit-Limit'] );
$this->assertIsInt( $spy_rest_server->sent_headers['RateLimit-Reset'] );
$this->assertGreaterThan( time(), $spy_rest_server->sent_headers['RateLimit-Reset'] );
$this->assertEquals( $remaining - 1, $spy_rest_server->sent_headers['RateLimit-Remaining'] );
} while ( $spy_rest_server->sent_headers['RateLimit-Remaining'] > 0 );
// Attempt a request after rate limit is reached.
$spy_rest_server->serve_request( '/wc/store/cart' );
$body = json_decode( $spy_rest_server->sent_body );
$this->assertEquals( JSON_ERROR_NONE, json_last_error() );
$this->assertEquals( 400, $body->data->status );
$this->assertEquals( $rate_limiting_options->limit, $spy_rest_server->sent_headers['RateLimit-Limit'] );
$this->assertIsInt( $spy_rest_server->sent_headers['RateLimit-Reset'] );
$this->assertGreaterThan( time(), $spy_rest_server->sent_headers['RateLimit-Reset'] );
$this->assertEquals( 0, $spy_rest_server->sent_headers['RateLimit-Remaining'] );
$this->assertArrayHasKey( 'RateLimit-Retry-After', $spy_rest_server->sent_headers );
$this->assertIsInt( $spy_rest_server->sent_headers['RateLimit-Retry-After'] );
$this->assertLessThanOrEqual( $rate_limiting_options->seconds, $spy_rest_server->sent_headers['RateLimit-Retry-After'] );
}
/**
* Tests that get_ip_address() correctly selects the $_SERVER var, parses and return the IP whether
* behind a proxy or not.
*
* @return void
* @throws ReflectionException On failing invoked protected method through reflection class.
*/
public function test_get_ip_address_method() {
$_SERVER = array_merge(
$_SERVER,
array(
'REMOTE_ADDR' => '76.45.67.100',
'HTTP_X_REAL_IP' => '76.45.67.101',
'HTTP_CLIENT_IP' => '76.45.67.102',
'HTTP_X_FORWARDED_FOR' => '76.45.67.103,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178',
'HTTP_FORWARDED' => 'for="[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:4711";proto=http;by=203.0.113.43,for=192.0.2.60;proto=https;by=203.0.113.43',
)
);
$authentication = new ReflectionClass( Authentication::class );
// As the method we're testing is protected, we're using ReflectionClass to set it accessible from the outside.
$get_ip_address = $authentication->getMethod( 'get_ip_address' );
$get_ip_address->setAccessible( true );
$this->assertEquals( '76.45.67.100', $get_ip_address->invokeArgs( $authentication, array() ) );
$this->assertEquals( '76.45.67.101', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
$_SERVER['HTTP_X_REAL_IP'] = 'invalid_ip_address';
$this->assertequals( '0.0.0.0', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
unset( $_SERVER['REMOTE_ADDR'] );
unset( $_SERVER['HTTP_X_REAL_IP'] );
$this->assertEquals( '76.45.67.102', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
$_SERVER['HTTP_CLIENT_IP'] = 'invalid_ip_address';
$this->assertequals( '0.0.0.0', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
unset( $_SERVER['HTTP_CLIENT_IP'] );
$this->assertEquals( '76.45.67.103', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
$_SERVER['HTTP_X_FORWARDED_FOR'] = 'invalid_ip_address,76.45.67.103';
$this->assertequals( '0.0.0.0', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
unset( $_SERVER['HTTP_X_FORWARDED_FOR'] );
$this->assertEquals( '2001:0db8:85a3:0000:0000:8a2e:0370:7334', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
$_SERVER['HTTP_FORWARDED'] = 'for=invalid_ip_address;proto=https;by=203.0.113.43';
$this->assertequals( '0.0.0.0', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
unset( $_SERVER['HTTP_FORWARDED'] );
$this->assertequals( '0.0.0.0', $get_ip_address->invokeArgs( $authentication, array( true ) ) );
}
}