Files
2026-07-29 22:52:47 +05:00

207 lines
5.8 KiB
PHP

<?php
/**
* Ядро: перехват исходящих HTTP-запросов и маршрутизация через прокси.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс маршрутизации запросов через прокси.
*
* Реализует ротацию: при ошибке/таймауте текущего прокси WordPress повторно
* выполняет запрос, и хук подставляет следующий включённый прокси.
*
* @since 1.0.0
*/
class AIPMSP_Core {
/**
* Текущий индекс прокси для ротации.
*
* @var int
*/
private $rotation_index = 0;
/**
* Кэш настроек на время запроса.
*
* @var array|null
*/
private $settings = null;
/**
* Конструктор: регистрирует хуки.
*
* @since 1.0.0
*/
public function __construct() {
// Применяем прокси к cURL-хендлу.
add_action( 'http_api_curl', array( $this, 'apply_proxy' ), 10, 3 );
// Ловим ошибки ответа для ротации на следующий прокси.
add_filter( 'http_response', array( $this, 'maybe_rotate' ), 10, 3 );
}
/**
* Лениво загружает настройки.
*
* @since 1.0.0
*
* @return array
*/
private function settings() {
if ( null === $this->settings ) {
$this->settings = AIPMSP_Proxy::get_settings();
}
return $this->settings;
}
/**
* Проверяет, нужно ли проксировать запрос к указанному URL.
*
* @since 1.0.0
*
* @param string $url URL запроса.
* @return bool
*/
private function should_proxy( $url ) {
$settings = $this->settings();
if ( empty( $settings['proxy_enabled'] ) ) {
return false;
}
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( empty( $host ) ) {
return false;
}
$host = strtolower( $host );
foreach ( $settings['domains'] as $domain ) {
$domain = strtolower( trim( $domain ) );
if ( '' === $domain ) {
continue;
}
// Точное совпадение или поддомен разрешённого домена.
if ( $host === $domain || self::str_ends_with( $host, '.' . $domain ) ) {
return true;
}
}
return false;
}
/**
* Хук http_api_curl: подставляет настройки прокси в cURL-хендл.
*
* @since 1.0.0
*
* @param resource $handle cURL-хендл.
* @param array $args Аргументы запроса.
* @param string $url URL запроса.
* @return void
*/
public function apply_proxy( $handle, $args, $url ) {
if ( ! $this->should_proxy( $url ) ) {
return;
}
$enabled = AIPMSP_Proxy::get_enabled_proxies( $this->settings() );
if ( empty( $enabled ) ) {
return;
}
// Ротация: выбираем прокси по текущему индексу (с цикличностью).
$count = count( $enabled );
$index = $this->rotation_index % $count;
$proxy = $enabled[ $index ];
$pass = isset( $proxy['pass'] ) ? AIPMSP_Crypto::decrypt( $proxy['pass'] ) : '';
curl_setopt( $handle, CURLOPT_PROXY, $proxy['ip'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $handle, CURLOPT_PROXYPORT, (int) $proxy['port'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $handle, CURLOPT_PROXYTYPE, AIPMSP_Proxy::curl_proxy_type( $proxy['type'] ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( ! empty( $proxy['user'] ) && '' !== $pass ) {
curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy['user'] . ':' . $pass ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, 10 ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
/**
* Хук http_response: при ошибке соединения пробует следующий прокси.
*
* @since 1.0.0
*
* @param array $response Ответ HTTP API.
* @param array $parsed_args Аргументы запроса.
* @param string $url URL запроса.
* @return array
*/
public function maybe_rotate( $response, $parsed_args, $url ) {
if ( ! $this->should_proxy( $url ) ) {
return $response;
}
$enabled = AIPMSP_Proxy::get_enabled_proxies( $this->settings() );
$count = count( $enabled );
if ( $count < 2 ) {
return $response;
}
// Считаем неудачей ошибку транспорта или 5xx/0 коды.
$is_error = is_wp_error( $response );
$code = $is_error ? 0 : (int) wp_remote_retrieve_response_code( $response );
$failed = $is_error || 0 === $code || $code >= 500;
// Ограничиваем число попыток числом доступных прокси.
$attempts = isset( $parsed_args['_aipmsp_attempts'] ) ? (int) $parsed_args['_aipmsp_attempts'] : 0;
if ( $failed && $attempts < $count - 1 ) {
$this->rotation_index++;
$retry_args = $parsed_args;
$retry_args['_aipmsp_attempts'] = $attempts + 1;
// Индекс ротации уже сдвинут, повторный запрос пойдёт через следующий прокси.
$retry = wp_remote_request( $url, $retry_args );
return $retry;
}
return $response;
}
/**
* Полифилл str_ends_with для PHP < 8.0.
*
* @since 1.0.0
*
* @param string $haystack Строка.
* @param string $needle Окончание.
* @return bool
*/
private static function str_ends_with( $haystack, $needle ) {
if ( function_exists( 'str_ends_with' ) ) {
return str_ends_with( $haystack, $needle );
}
$len = strlen( $needle );
if ( 0 === $len ) {
return true;
}
return substr( $haystack, -$len ) === $needle;
}
}