Initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user