woocommerce/plugins/woocommerce-blocks/tests/php/StoreApi/RateLimitsTests.php

142 lines
5.8 KiB
PHP
Raw Normal View History

Experiment: Add Rate Limits to Store API (https://github.com/woocommerce/woocommerce-blocks/pull/5962) * 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>
2022-11-04 15:53:00 +00:00
<?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 ) ) );
}
}