Initial commit

This commit is contained in:
2026-07-29 22:52:47 +05:00
commit cb5a058984
13 changed files with 2064 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
<?php
/**
* Работа с прокси: парсинг, хранение, ротация, health-check.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс управления прокси-серверами.
*
* @since 1.0.0
*/
class AIPMSP_Proxy {
/**
* Возвращает все настройки плагина с применёнными значениями по умолчанию.
*
* @since 1.0.0
*
* @return array
*/
public static function get_settings() {
$defaults = AIPMSP_Plugin::get_default_settings();
$settings = get_option( AIPMSP_OPTION_KEY, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
$settings = wp_parse_args( $settings, $defaults );
// Гарантируем структуру вложенных массивов.
if ( ! is_array( $settings['proxies'] ) ) {
$settings['proxies'] = array();
}
if ( ! is_array( $settings['domains'] ) ) {
$settings['domains'] = $defaults['domains'];
}
$settings['keys'] = wp_parse_args(
is_array( $settings['keys'] ) ? $settings['keys'] : array(),
$defaults['keys']
);
$settings['key_enabled'] = wp_parse_args(
isset( $settings['key_enabled'] ) && is_array( $settings['key_enabled'] ) ? $settings['key_enabled'] : array(),
$defaults['key_enabled']
);
return $settings;
}
/**
* Сохраняет настройки плагина.
*
* @since 1.0.0
*
* @param array $settings Настройки.
* @return void
*/
public static function save_settings( array $settings ) {
update_option( AIPMSP_OPTION_KEY, $settings );
}
/**
* Парсит многострочный список прокси в массив структур.
*
* Формат строки: ЛОГИН:ПАРОЛЬ@IP:ПОРТ либо IP:ПОРТ (без авторизации).
*
* @since 1.0.0
*
* @param string $raw Сырой текст из textarea.
* @param string $type Тип прокси (http|https|socks5).
* @param array $existing Ранее сохранённые прокси (для сохранения состояния enabled и паролей).
* @return array Список нормализованных прокси.
*/
public static function parse_proxies( $raw, $type, $existing = array() ) {
$type = self::sanitize_type( $type );
$result = array();
$lines = preg_split( '/\r\n|\r|\n/', (string) $raw );
// Индекс по "ip:port" для переноса состояния enabled из прежних настроек.
$existing_map = array();
foreach ( (array) $existing as $ex ) {
if ( isset( $ex['ip'], $ex['port'] ) ) {
$existing_map[ $ex['ip'] . ':' . $ex['port'] ] = $ex;
}
}
foreach ( $lines as $line ) {
$line = trim( $line );
if ( '' === $line ) {
continue;
}
$user = '';
$pass = '';
$hostport = $line;
// Если есть авторизация — отделяем её по последнему '@'
// (пароль может содержать '@', а host:port после него — нет).
if ( false !== strpos( $line, '@' ) ) {
$at_pos = strrpos( $line, '@' );
$auth = substr( $line, 0, $at_pos );
$hostport = substr( $line, $at_pos + 1 );
// Лимит 2: пароль может содержать двоеточия.
$auth_parts = explode( ':', $auth, 2 );
$user = isset( $auth_parts[0] ) ? trim( $auth_parts[0] ) : '';
$pass = isset( $auth_parts[1] ) ? $auth_parts[1] : '';
}
// Разбираем host:port.
$hp = explode( ':', $hostport, 2 );
$ip = isset( $hp[0] ) ? trim( $hp[0] ) : '';
$port = isset( $hp[1] ) ? trim( $hp[1] ) : '';
// Валидация IP/хоста и порта.
$valid_host = filter_var( $ip, FILTER_VALIDATE_IP ) || self::is_valid_host( $ip );
$port_int = absint( $port );
if ( ! $valid_host || $port_int < 1 || $port_int > 65535 ) {
continue;
}
$key = $ip . ':' . $port_int;
$enabled = 1;
if ( isset( $existing_map[ $key ]['enabled'] ) ) {
$enabled = (int) $existing_map[ $key ]['enabled'];
}
$sanitized_user = sanitize_text_field( $user );
// Пароль хранится зашифрованным и НЕ показывается в форме.
// Если в строке пароль не введён — сохраняем ранее сохранённый
// (для того же ip:port и логина), иначе шифруем новый.
if ( '' !== $pass ) {
$enc_pass = AIPMSP_Crypto::encrypt( $pass );
} elseif (
isset( $existing_map[ $key ]['pass'], $existing_map[ $key ]['user'] )
&& '' !== (string) $existing_map[ $key ]['pass']
&& $existing_map[ $key ]['user'] === $sanitized_user
) {
$enc_pass = $existing_map[ $key ]['pass'];
} else {
$enc_pass = '';
}
$result[] = array(
'ip' => $ip,
'port' => $port_int,
'user' => $sanitized_user,
'pass' => $enc_pass,
'type' => $type,
'enabled' => $enabled ? 1 : 0,
);
}
return $result;
}
/**
* Возвращает список включённых прокси.
*
* @since 1.0.0
*
* @param array|null $settings Настройки (если не переданы — загружаются).
* @return array
*/
public static function get_enabled_proxies( $settings = null ) {
if ( null === $settings ) {
$settings = self::get_settings();
}
$enabled = array();
foreach ( $settings['proxies'] as $proxy ) {
if ( ! empty( $proxy['enabled'] ) ) {
$enabled[] = $proxy;
}
}
return $enabled;
}
/**
* Проверяет один прокси через тестовый cURL-запрос.
*
* @since 1.0.0
*
* @param array $proxy Структура прокси.
* @param string $test_url URL для проверки.
* @return array Результат: success(bool), message(string), time(float ms), http_code(int).
*/
public static function health_check( array $proxy, $test_url = 'https://api.openai.com/v1/models' ) {
if ( ! function_exists( 'curl_init' ) ) {
return array(
'success' => false,
'message' => __( 'cURL недоступен на сервере', 'ai-proxy-manager' ),
'time' => 0,
'http_code' => 0,
);
}
$ch = curl_init(); // phpcs:ignore WordPress.WP.AlternativeFunctions
$proxy_type = self::curl_proxy_type( $proxy['type'] );
$pass = isset( $proxy['pass'] ) ? AIPMSP_Crypto::decrypt( $proxy['pass'] ) : '';
curl_setopt( $ch, CURLOPT_URL, $test_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_NOBODY, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_PROXY, $proxy['ip'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_PROXYPORT, (int) $proxy['port'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_PROXYTYPE, $proxy_type ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_setopt( $ch, CURLOPT_TIMEOUT, 15 ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( ! empty( $proxy['user'] ) && '' !== $pass ) {
curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy['user'] . ':' . $pass ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
$start = microtime( true );
$exec = curl_exec( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions
$elapsed = round( ( microtime( true ) - $start ) * 1000 );
$http_code = (int) curl_getinfo( $ch, CURLINFO_RESPONSE_CODE ); // phpcs:ignore WordPress.WP.AlternativeFunctions
$err_no = curl_errno( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions
$err_msg = curl_error( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions
curl_close( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( 0 !== $err_no || false === $exec ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: текст ошибки cURL */
__( 'Недоступен: %s', 'ai-proxy-manager' ),
$err_msg
),
'time' => $elapsed,
'http_code' => $http_code,
);
}
// Любой HTTP-ответ означает, что соединение через прокси установлено.
return array(
'success' => true,
'message' => sprintf(
/* translators: 1: HTTP-код, 2: время ответа в мс */
__( 'Доступен (HTTP %1$d, %2$d мс)', 'ai-proxy-manager' ),
$http_code,
$elapsed
),
'time' => $elapsed,
'http_code' => $http_code,
);
}
/**
* Преобразует строковый тип прокси в константу CURLPROXY_*.
*
* @since 1.0.0
*
* @param string $type Тип (http|https|socks5).
* @return int
*/
public static function curl_proxy_type( $type ) {
switch ( self::sanitize_type( $type ) ) {
case 'socks5':
return defined( 'CURLPROXY_SOCKS5' ) ? CURLPROXY_SOCKS5 : 5;
case 'https':
// HTTPS-прокси использует тот же тип-хэндлер, что и HTTP, но через TLS-соединение.
return defined( 'CURLPROXY_HTTP' ) ? CURLPROXY_HTTP : 0;
case 'http':
default:
return defined( 'CURLPROXY_HTTP' ) ? CURLPROXY_HTTP : 0;
}
}
/**
* Нормализует строковый тип прокси.
*
* @since 1.0.0
*
* @param string $type Тип.
* @return string Один из: http|https|socks5.
*/
public static function sanitize_type( $type ) {
$type = strtolower( sanitize_text_field( (string) $type ) );
$allowed = array( 'http', 'https', 'socks5' );
return in_array( $type, $allowed, true ) ? $type : 'http';
}
/**
* Возвращает нейтральную маску для отображения наличия пароля.
*
* Символы пароля не раскрываются (включая длину) — только факт его наличия.
*
* @since 1.0.0
*
* @param string $pass Открытый пароль.
* @return string Фиксированная маска при наличии пароля, иначе пустая строка.
*/
public static function mask_password( $pass ) {
return ( '' === (string) $pass ) ? '' : '••••••••';
}
/**
* Валидирует хост (домен) по символам.
*
* @since 1.0.0
*
* @param string $host Хост.
* @return bool
*/
public static function is_valid_host( $host ) {
$host = (string) $host;
if ( '' === $host || strlen( $host ) > 253 ) {
return false;
}
return (bool) preg_match( '/^(?=.{1,253}$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/', $host );
}
/**
* Санитизирует список доменов из textarea.
*
* @since 1.0.0
*
* @param string $raw Сырой текст.
* @return array
*/
public static function parse_domains( $raw ) {
$lines = preg_split( '/\r\n|\r|\n/', (string) $raw );
$result = array();
foreach ( $lines as $line ) {
$line = strtolower( trim( $line ) );
if ( '' === $line ) {
continue;
}
// Если вставили URL — извлекаем хост.
if ( false !== strpos( $line, '/' ) ) {
$parsed = wp_parse_url( $line );
if ( ! empty( $parsed['host'] ) ) {
$line = $parsed['host'];
}
}
if ( self::is_valid_host( $line ) && ! in_array( $line, $result, true ) ) {
$result[] = $line;
}
}
return $result;
}
}