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
+22
View File
@@ -0,0 +1,22 @@
# OS
.DS_Store
Thumbs.db
# Editors / IDE
.idea/
.vscode/
*.sublime-*
*.swp
# Dependencies / build
/node_modules/
/vendor/
*.log
# Архивы и временные файлы
*.zip
*.tar.gz
*.bak
# Исходное ТЗ — не публиковать
prompt-plagin-proxy.md
+81
View File
@@ -0,0 +1,81 @@
# AI Proxy Manager
Маршрутизация исходящих запросов WordPress к API искусственного интеллекта (OpenAI, Anthropic, Google Gemini) через пользовательские прокси-серверы, с режимом отладки подключения.
[![License: GPL v2+](https://img.shields.io/badge/License-GPLv2%2B-blue.svg)](https://www.gnu.org/licenses/gpl-2.0.html)
![WordPress](https://img.shields.io/badge/WordPress-6.0%2B-21759b.svg)
![PHP](https://img.shields.io/badge/PHP-7.4%2B-777bb4.svg)
## Возможности
- Список прокси в формате `ЛОГИН:ПАРОЛЬ@IP:ПОРТ` (поддерживается и `IP:ПОРТ` без авторизации).
- Типы прокси: HTTP, HTTPS, SOCKS5.
- Главный переключатель и индивидуальные переключатели для каждого прокси.
- Маршрутизация только указанных доменов — прокси применяется лишь к запросам к AI-доменам, остальной трафик идёт напрямую.
- Ротация и резервирование: при ошибке одного прокси автоматически пробуется следующий.
- Health-check каждого прокси (AJAX): доступность и время ответа.
- Режим отладки: проверка подключения к каждому AI-провайдеру с расшифровкой HTTP-кодов.
- Индивидуальные переключатели «использовать для проверки» для каждого провайдера.
- Шифрование секретов: пароли прокси и API-ключи хранятся в зашифрованном виде (AES-256-GCM, ключ выводится из солей `wp-config.php`).
## Требования
- WordPress 6.0+
- PHP 7.4+
- Расширение cURL (для прокси с авторизацией)
- Расширение OpenSSL (для шифрования секретов)
## Установка
1. Скопируйте папку `ai-proxy-manager` в `wp-content/plugins/` (или загрузите ZIP через «Плагины → Добавить новый → Загрузить плагин»).
2. Активируйте плагин в админке WordPress.
3. Откройте пункт меню «AI Proxy Manager».
4. Добавьте прокси, выберите тип, включите главный переключатель и сохраните.
5. При необходимости введите API-ключи провайдеров и включите режим отладки для проверки подключения.
## Использование
Список доменов, для которых применяется прокси, по умолчанию:
```
api.openai.com
api.anthropic.com
generativelanguage.googleapis.com
```
Запросы ко всем остальным доменам идут напрямую и не затрагиваются.
В целях безопасности сохранённые пароли прокси не отображаются в форме (строки показаны как `ЛОГИН@IP:ПОРТ`). Чтобы изменить пароль — укажите его явно: `ЛОГИН:НОВЫЙПАРОЛЬ@IP:ПОРТ`. Если оставить строку без пароля, прежний пароль сохранится.
## Безопасность
- Проверка прав (`manage_options`), nonce/CSRF на всех формах и AJAX.
- Санитизация ввода и экранирование вывода во всех точках.
- Секреты шифруются аутентифицированным AES-256-GCM, расшифровываются только в памяти в момент использования и не выводятся в интерфейс.
- Плагин не передаёт данные третьим лицам и не содержит телеметрии; исходящие запросы идут только к настроенным AI-провайдерам через указанные вами прокси.
## Структура
```
ai-proxy-manager.php Главный файл: заголовок, инициализация, активация
uninstall.php Очистка данных при удалении
includes/
class-aipmsp-core.php Перехват и маршрутизация запросов через прокси
class-aipmsp-admin.php Меню, страница настроек, сохранение
class-aipmsp-proxy.php Парсинг, хранение, ротация, health-check
class-aipmsp-crypto.php Шифрование/расшифровка секретов (AES-256-GCM)
class-aipmsp-debug.php Проверка подключения к AI-провайдерам
class-aipmsp-ajax.php AJAX-обработчик проверки прокси
assets/
css/admin.css
js/admin.js
readme.txt Описание в формате WordPress.org
```
## Лицензия
[GPL-2.0-or-later](LICENSE).
## Автор
[быстросайты.рф](https://быстросайты.рф/) · Поддержка: [Telegram](https://t.me/speedsite)
+157
View File
@@ -0,0 +1,157 @@
<?php
/**
* Plugin Name: AI Proxy Manager
* Description: Маршрутизация запросов к AI API (OpenAI, Anthropic, Google) через пользовательские прокси-серверы, с режимом отладки подключения.
* Version: 1.0.8
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: быстросайты.рф
* Author URI: https://быстросайты.рф/
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: ai-proxy-manager
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Базовые константы плагина.
*/
define( 'AIPMSP_VERSION', '1.0.8' );
define( 'AIPMSP_PLUGIN_FILE', __FILE__ );
define( 'AIPMSP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'AIPMSP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'AIPMSP_OPTION_KEY', 'aipmsp_settings' );
/**
* Подключение файлов классов плагина.
*/
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-crypto.php';
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-proxy.php';
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-core.php';
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-admin.php';
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-debug.php';
require_once AIPMSP_PLUGIN_DIR . 'includes/class-aipmsp-ajax.php';
/**
* Главный класс-загрузчик плагина (singleton).
*
* @since 1.0.0
*/
final class AIPMSP_Plugin {
/**
* Единственный экземпляр класса.
*
* @var AIPMSP_Plugin|null
*/
private static $instance = null;
/**
* Возвращает единственный экземпляр плагина.
*
* @since 1.0.0
*
* @return AIPMSP_Plugin
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Конструктор: инициализирует компоненты плагина.
*
* @since 1.0.0
*/
private function __construct() {
add_action( 'init', array( $this, 'load_textdomain' ) );
// Ядро маршрутизации прокси работает и в админке, и на фронтенде.
new AIPMSP_Core();
if ( is_admin() ) {
new AIPMSP_Admin();
new AIPMSP_Debug();
new AIPMSP_Ajax();
}
}
/**
* Загружает переводы плагина.
*
* @since 1.0.0
*
* @return void
*/
public function load_textdomain() {
load_plugin_textdomain(
'ai-proxy-manager',
false,
dirname( plugin_basename( AIPMSP_PLUGIN_FILE ) ) . '/languages'
);
}
/**
* Возвращает настройки плагина со значениями по умолчанию.
*
* @since 1.0.0
*
* @return array
*/
public static function get_default_settings() {
return array(
'proxy_enabled' => 0,
'debug_enabled' => 0,
'proxy_type' => 'http',
'proxies' => array(),
'domains' => array(
'api.openai.com',
'api.anthropic.com',
'generativelanguage.googleapis.com',
),
'keys' => array(
'openai' => '',
'anthropic' => '',
'google' => '',
),
'key_enabled' => array(
'openai' => 1,
'anthropic' => 1,
'google' => 1,
),
);
}
/**
* Хук активации: задаёт настройки по умолчанию, если их ещё нет.
*
* @since 1.0.0
*
* @return void
*/
public static function activate() {
if ( false === get_option( AIPMSP_OPTION_KEY, false ) ) {
add_option( AIPMSP_OPTION_KEY, self::get_default_settings() );
}
}
}
register_activation_hook( __FILE__, array( 'AIPMSP_Plugin', 'activate' ) );
/**
* Запуск плагина после загрузки всех плагинов.
*
* @since 1.0.0
*
* @return void
*/
function aipmsp_bootstrap() {
AIPMSP_Plugin::instance();
}
add_action( 'plugins_loaded', 'aipmsp_bootstrap' );
+124
View File
@@ -0,0 +1,124 @@
/**
* AI Proxy Manager — стили админки.
*/
.aipmsp-wrap .title {
margin-top: 28px;
padding-top: 14px;
border-top: 1px solid #dcdcde;
}
.aipmsp-proxy-table {
max-width: 980px;
margin-bottom: 8px;
}
.aipmsp-proxy-table code {
background: #f0f0f1;
padding: 1px 5px;
border-radius: 3px;
}
.aipmsp-recommend {
margin-top: 6px;
}
/* Toggle-переключатель */
.aipmsp-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
vertical-align: middle;
}
.aipmsp-switch.aipmsp-switch-sm {
width: 36px;
height: 20px;
}
.aipmsp-switch input {
opacity: 0;
width: 0;
height: 0;
}
.aipmsp-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #c3c4c7;
transition: 0.3s;
border-radius: 24px;
}
.aipmsp-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: #fff;
transition: 0.3s;
border-radius: 50%;
}
.aipmsp-switch-sm .aipmsp-slider:before {
height: 14px;
width: 14px;
}
.aipmsp-switch input:checked + .aipmsp-slider {
background-color: #2271b1;
}
.aipmsp-switch input:checked + .aipmsp-slider:before {
transform: translateX(20px);
}
.aipmsp-switch-sm input:checked + .aipmsp-slider:before {
transform: translateX(16px);
}
.aipmsp-switch input:focus + .aipmsp-slider {
box-shadow: 0 0 0 2px #2271b1;
}
.aipmsp-switch-label {
margin-left: 10px;
vertical-align: middle;
color: #50575e;
}
.aipmsp-clear-key {
margin-left: 12px;
color: #b32d2e;
}
/* Результат health-check */
.aipmsp-check-result {
margin-left: 8px;
font-weight: 600;
}
.aipmsp-check-result.aipmsp-ok {
color: #008a20;
}
.aipmsp-check-result.aipmsp-fail {
color: #b32d2e;
}
.aipmsp-check-result.aipmsp-pending {
color: #646970;
font-weight: 400;
}
/* Футер страницы настроек */
.aipmsp-footer {
color: #646970;
}
+55
View File
@@ -0,0 +1,55 @@
/**
* AI Proxy Manager — скрипты админки.
* Health-check прокси через AJAX.
*/
( function ( $ ) {
'use strict';
$( function () {
$( '.aipmsp-check-btn' ).on( 'click', function () {
var $btn = $( this );
var $result = $btn.siblings( '.aipmsp-check-result' );
var data = {
action: 'aipmsp_check_proxy',
nonce: aipmspData.nonce,
ip: $btn.data( 'ip' ),
port: $btn.data( 'port' ),
type: $btn.data( 'type' )
};
$btn.prop( 'disabled', true );
$result
.removeClass( 'aipmsp-ok aipmsp-fail' )
.addClass( 'aipmsp-pending' )
.text( aipmspData.checking );
$.post( aipmspData.ajaxUrl, data )
.done( function ( response ) {
if ( response && response.success ) {
$result
.removeClass( 'aipmsp-pending aipmsp-fail' )
.addClass( 'aipmsp-ok' )
.text( response.data.message );
} else {
var msg = response && response.data && response.data.message
? response.data.message
: 'Ошибка';
$result
.removeClass( 'aipmsp-pending aipmsp-ok' )
.addClass( 'aipmsp-fail' )
.text( msg );
}
} )
.fail( function () {
$result
.removeClass( 'aipmsp-pending aipmsp-ok' )
.addClass( 'aipmsp-fail' )
.text( 'Ошибка запроса' );
} )
.always( function () {
$btn.prop( 'disabled', false );
} );
} );
} );
} )( jQuery );
+543
View File
@@ -0,0 +1,543 @@
<?php
/**
* Админка: меню, рендер страницы настроек, сохранение.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс администрирования плагина.
*
* @since 1.0.0
*/
class AIPMSP_Admin {
/**
* Слаг страницы настроек.
*
* @var string
*/
const PAGE_SLUG = 'aipmsp-settings';
/**
* URL страницы плагина на сайте автора.
*
* @var string
*/
const PLUGIN_PAGE_URL = 'https://быстросайты.рф/modules/ai-proxy-manager/';
/**
* URL поддержки.
*
* @var string
*/
const SUPPORT_URL = 'https://t.me/speedsite';
/**
* Партнёрская ссылка на рекомендованные прокси.
*
* @var string
*/
const PROXY_AFFILIATE_URL = 'https://proxys.io/?refid=2512';
/**
* Конструктор: регистрирует хуки админки.
*
* @since 1.0.0
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_post_aipmsp_save_settings', array( $this, 'handle_save' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
$basename = plugin_basename( AIPMSP_PLUGIN_FILE );
add_filter( 'plugin_action_links_' . $basename, array( $this, 'action_links' ) );
add_filter( 'plugin_row_meta', array( $this, 'row_meta' ), 10, 2 );
}
/**
* Возвращает data-URI монохромной иконки-молнии для меню админки.
*
* @since 1.0.5
*
* @return string
*/
private function menu_icon() {
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iI2E3YWFhZCIgZD0iTTEyIDEgTDQgMTEgSDguNSBMNyAxOSBMMTYgOCBIMTEuNSBMMTMgMSBaIi8+PC9zdmc+';
}
/**
* Регистрирует пункт меню в админке.
*
* @since 1.0.0
*
* @return void
*/
public function register_menu() {
add_menu_page(
__( 'AI Proxy Manager', 'ai-proxy-manager' ),
__( 'AI Proxy Manager', 'ai-proxy-manager' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' ),
$this->menu_icon(),
81
);
}
/**
* Добавляет ссылку «Настройки» рядом с кнопкой деактивации плагина.
*
* @since 1.0.5
*
* @param array $links Существующие ссылки действий.
* @return array
*/
public function action_links( $links ) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ),
esc_html__( 'Настройки', 'ai-proxy-manager' )
);
array_unshift( $links, $settings_link );
return $links;
}
/**
* Добавляет ссылку «Детали» в строку плагина в списке плагинов.
*
* @since 1.0.5
*
* @param array $links Метаданные строки плагина.
* @param string $file Путь к файлу плагина.
* @return array
*/
public function row_meta( $links, $file ) {
if ( plugin_basename( AIPMSP_PLUGIN_FILE ) === $file ) {
$links[] = sprintf(
'<a href="%s" target="_blank" rel="noopener">%s</a>',
esc_url( self::PLUGIN_PAGE_URL ),
esc_html__( 'Детали', 'ai-proxy-manager' )
);
}
return $links;
}
/**
* Подключает CSS/JS только на странице плагина.
*
* @since 1.0.0
*
* @param string $hook Текущий хук админ-страницы.
* @return void
*/
public function enqueue_assets( $hook ) {
if ( 'toplevel_page_' . self::PAGE_SLUG !== $hook ) {
return;
}
wp_enqueue_style(
'aipmsp-admin',
AIPMSP_PLUGIN_URL . 'assets/css/admin.css',
array(),
AIPMSP_VERSION
);
wp_enqueue_script(
'aipmsp-admin',
AIPMSP_PLUGIN_URL . 'assets/js/admin.js',
array( 'jquery' ),
AIPMSP_VERSION,
true
);
wp_localize_script(
'aipmsp-admin',
'aipmspData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aipmsp_ajax_nonce' ),
'checking' => __( 'Проверка…', 'ai-proxy-manager' ),
'checkLabel' => __( 'Проверить', 'ai-proxy-manager' ),
)
);
}
/**
* Обрабатывает сохранение формы настроек.
*
* @since 1.0.0
*
* @return void
*/
public function handle_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Недостаточно прав.', 'ai-proxy-manager' ) );
}
check_admin_referer( 'aipmsp_save_settings', 'aipmsp_nonce' );
$settings = AIPMSP_Proxy::get_settings();
// Тип прокси.
$type = isset( $_POST['aipmsp_proxy_type'] )
? AIPMSP_Proxy::sanitize_type( wp_unslash( $_POST['aipmsp_proxy_type'] ) )
: 'http';
// Прокси (с сохранением состояния enabled из прежних настроек).
$raw_proxies = isset( $_POST['aipmsp_proxies'] )
? sanitize_textarea_field( wp_unslash( $_POST['aipmsp_proxies'] ) )
: '';
$new_proxies = AIPMSP_Proxy::parse_proxies( $raw_proxies, $type, $settings['proxies'] );
// Применяем индивидуальные переключатели enabled из чекбоксов (по индексу ip:port).
$enabled_keys = array();
if ( isset( $_POST['aipmsp_proxy_enabled_keys'] ) && is_array( $_POST['aipmsp_proxy_enabled_keys'] ) ) {
foreach ( wp_unslash( $_POST['aipmsp_proxy_enabled_keys'] ) as $k ) {
$enabled_keys[ sanitize_text_field( $k ) ] = true;
}
}
// Только если форма прислала набор ключей-переключателей — применяем их.
if ( isset( $_POST['aipmsp_toggles_present'] ) ) {
foreach ( $new_proxies as &$proxy ) {
$key = $proxy['ip'] . ':' . $proxy['port'];
$proxy['enabled'] = isset( $enabled_keys[ $key ] ) ? 1 : 0;
}
unset( $proxy );
}
$settings['proxies'] = $new_proxies;
$settings['proxy_type'] = $type;
$settings['proxy_enabled'] = isset( $_POST['aipmsp_proxy_master'] ) ? 1 : 0;
$settings['debug_enabled'] = isset( $_POST['aipmsp_debug'] ) ? 1 : 0;
// Домены.
$raw_domains = isset( $_POST['aipmsp_domains'] )
? sanitize_textarea_field( wp_unslash( $_POST['aipmsp_domains'] ) )
: '';
$settings['domains'] = AIPMSP_Proxy::parse_domains( $raw_domains );
if ( empty( $settings['domains'] ) ) {
$defaults = AIPMSP_Plugin::get_default_settings();
$settings['domains'] = $defaults['domains'];
}
// Список провайдеров, отмеченных переключателем «Использовать для проверки».
$use_keys = array();
if ( isset( $_POST['aipmsp_key_use'] ) && is_array( $_POST['aipmsp_key_use'] ) ) {
foreach ( wp_unslash( $_POST['aipmsp_key_use'] ) as $slug ) {
$use_keys[ sanitize_text_field( $slug ) ] = true;
}
}
// API-ключи: шифруем только если поле непустое, иначе сохраняем прежний.
$providers = array( 'openai', 'anthropic', 'google' );
foreach ( $providers as $provider ) {
$field = 'aipmsp_key_' . $provider;
if ( isset( $_POST[ $field ] ) ) {
$raw_key = trim( sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) );
if ( '' !== $raw_key ) {
$settings['keys'][ $provider ] = AIPMSP_Crypto::encrypt( $raw_key );
}
}
// Очистка ключа, если установлен флаг.
if ( isset( $_POST[ 'aipmsp_clear_' . $provider ] ) ) {
$settings['keys'][ $provider ] = '';
}
// Состояние переключателя «использовать для проверки».
$settings['key_enabled'][ $provider ] = isset( $use_keys[ $provider ] ) ? 1 : 0;
}
AIPMSP_Proxy::save_settings( $settings );
$redirect = add_query_arg(
array(
'page' => self::PAGE_SLUG,
'aipmsp_saved' => '1',
),
admin_url( 'admin.php' )
);
wp_safe_redirect( $redirect );
exit;
}
/**
* Рендерит страницу настроек.
*
* @since 1.0.0
*
* @return void
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = AIPMSP_Proxy::get_settings();
// Восстанавливаем textarea прокси БЕЗ пароля (пароль не выводится в HTML).
// Формат строки: ЛОГИН@IP:ПОРТ либо IP:ПОРТ. При сохранении пустого пароля
// ранее сохранённый пароль остаётся без изменений.
$proxy_lines = array();
foreach ( $settings['proxies'] as $proxy ) {
if ( ! empty( $proxy['user'] ) ) {
$line = $proxy['user'] . '@' . $proxy['ip'] . ':' . $proxy['port'];
} else {
$line = $proxy['ip'] . ':' . $proxy['port'];
}
$proxy_lines[] = $line;
}
$proxies_text = implode( "\n", $proxy_lines );
$domains_text = implode( "\n", $settings['domains'] );
$curl_available = function_exists( 'curl_init' );
$ssl_available = function_exists( 'openssl_encrypt' );
// Сообщение об успешном сохранении.
if ( isset( $_GET['aipmsp_saved'] ) && '1' === $_GET['aipmsp_saved'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
echo '<div class="notice notice-success is-dismissible"><p>'
. esc_html__( 'Настройки сохранены.', 'ai-proxy-manager' )
. '</p></div>';
}
?>
<div class="wrap aipmsp-wrap">
<h1><?php esc_html_e( 'AI Proxy Manager', 'ai-proxy-manager' ); ?></h1>
<?php if ( ! $curl_available ) : ?>
<div class="notice notice-error">
<p><strong><?php esc_html_e( 'Внимание:', 'ai-proxy-manager' ); ?></strong>
<?php esc_html_e( 'Расширение cURL недоступно на сервере. Прокси с авторизацией работают только через cURL — обратитесь к хостингу.', 'ai-proxy-manager' ); ?></p>
</div>
<?php endif; ?>
<?php if ( ! $ssl_available ) : ?>
<div class="notice notice-warning">
<p><strong><?php esc_html_e( 'Внимание:', 'ai-proxy-manager' ); ?></strong>
<?php esc_html_e( 'Расширение OpenSSL недоступно — шифрование секретов невозможно. Пароли прокси и API-ключи не будут сохранены.', 'ai-proxy-manager' ); ?></p>
</div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aipmsp-form">
<input type="hidden" name="action" value="aipmsp_save_settings" />
<input type="hidden" name="aipmsp_toggles_present" value="1" />
<?php wp_nonce_field( 'aipmsp_save_settings', 'aipmsp_nonce' ); ?>
<h2 class="title"><?php esc_html_e( 'Основные настройки', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Использовать прокси на сайте', 'ai-proxy-manager' ); ?></th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_proxy_master" value="1" <?php checked( ! empty( $settings['proxy_enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Главный переключатель прокси', 'ai-proxy-manager' ); ?></span>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Режим отладки (Debug)', 'ai-proxy-manager' ); ?></th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_debug" value="1" <?php checked( ! empty( $settings['debug_enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Показывать результат проверки подключения к AI-провайдерам', 'ai-proxy-manager' ); ?></span>
</td>
</tr>
<tr>
<th scope="row">
<label for="aipmsp_proxy_type"><?php esc_html_e( 'Тип прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<select name="aipmsp_proxy_type" id="aipmsp_proxy_type">
<?php
$types = array(
'http' => 'HTTP',
'https' => 'HTTPS',
'socks5' => 'SOCKS5',
);
foreach ( $types as $value => $label ) {
printf(
'<option value="%1$s" %2$s>%3$s</option>',
esc_attr( $value ),
selected( $settings['proxy_type'], $value, false ),
esc_html( $label )
);
}
?>
</select>
<p class="description"><?php esc_html_e( 'Применяется ко всем добавленным прокси.', 'ai-proxy-manager' ); ?></p>
</td>
</tr>
</table>
<h2 class="title"><?php esc_html_e( 'Прокси-серверы', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="aipmsp_proxies"><?php esc_html_e( 'Список прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<textarea name="aipmsp_proxies" id="aipmsp_proxies" rows="6" class="large-text code" placeholder="логин:пароль@ip:порт"><?php echo esc_textarea( $proxies_text ); ?></textarea>
<p class="description">
<?php esc_html_e( 'По одному прокси на строку в формате ЛОГИН:ПАРОЛЬ@IP:ПОРТ. Поддерживается формат без авторизации: IP:ПОРТ.', 'ai-proxy-manager' ); ?>
<br />
<?php esc_html_e( 'В целях безопасности сохранённые пароли здесь не отображаются — строки показаны как ЛОГИН@IP:ПОРТ. Если оставить строку без пароля, прежний пароль сохранится. Чтобы изменить пароль — укажите его: ЛОГИН:НОВЫЙПАРОЛЬ@IP:ПОРТ.', 'ai-proxy-manager' ); ?>
</p>
<p class="description aipmsp-recommend">
<?php
printf(
wp_kses(
/* translators: %s: ссылка на сервис прокси */
__( '💡 Нет своих прокси? Рекомендуем проверенный сервис: <a href="%s" target="_blank" rel="noopener nofollow sponsored">proxys.io</a>.', 'ai-proxy-manager' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
),
esc_url( self::PROXY_AFFILIATE_URL )
);
?>
</p>
</td>
</tr>
</table>
<?php if ( ! empty( $settings['proxies'] ) ) : ?>
<h3><?php esc_html_e( 'Добавленные прокси', 'ai-proxy-manager' ); ?></h3>
<table class="widefat striped aipmsp-proxy-table">
<thead>
<tr>
<th><?php esc_html_e( 'Вкл', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'IP', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Порт', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Тип', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Логин', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Пароль', 'ai-proxy-manager' ); ?></th>
<th><?php esc_html_e( 'Проверка', 'ai-proxy-manager' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $settings['proxies'] as $proxy ) : ?>
<?php
$key = $proxy['ip'] . ':' . $proxy['port'];
$pass_masked = empty( $proxy['pass'] ) ? '' : '••••••••';
?>
<tr>
<td>
<label class="aipmsp-switch aipmsp-switch-sm">
<input type="checkbox" name="aipmsp_proxy_enabled_keys[]" value="<?php echo esc_attr( $key ); ?>" <?php checked( ! empty( $proxy['enabled'] ) ); ?> />
<span class="aipmsp-slider"></span>
</label>
</td>
<td><?php echo esc_html( $proxy['ip'] ); ?></td>
<td><?php echo esc_html( $proxy['port'] ); ?></td>
<td><?php echo esc_html( strtoupper( $proxy['type'] ) ); ?></td>
<td><?php echo esc_html( $proxy['user'] ); ?></td>
<td><code><?php echo esc_html( $pass_masked ); ?></code></td>
<td>
<button type="button"
class="button aipmsp-check-btn"
data-ip="<?php echo esc_attr( $proxy['ip'] ); ?>"
data-port="<?php echo esc_attr( $proxy['port'] ); ?>"
data-type="<?php echo esc_attr( $proxy['type'] ); ?>"
data-user="<?php echo esc_attr( $proxy['user'] ); ?>">
<?php esc_html_e( 'Проверить', 'ai-proxy-manager' ); ?>
</button>
<span class="aipmsp-check-result"></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php esc_html_e( 'Чтобы удалить прокси — уберите его строку из поля «Список прокси» выше и сохраните.', 'ai-proxy-manager' ); ?>
</p>
<?php endif; ?>
<h2 class="title"><?php esc_html_e( 'Маршрутизация доменов', 'ai-proxy-manager' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="aipmsp_domains"><?php esc_html_e( 'Домены через прокси', 'ai-proxy-manager' ); ?></label>
</th>
<td>
<textarea name="aipmsp_domains" id="aipmsp_domains" rows="4" class="large-text code"><?php echo esc_textarea( $domains_text ); ?></textarea>
<p class="description">
<?php esc_html_e( 'По одному домену на строку. Прокси применяется только к запросам на эти домены и их поддомены. Остальные запросы идут напрямую.', 'ai-proxy-manager' ); ?>
</p>
</td>
</tr>
</table>
<h2 class="title"><?php esc_html_e( 'API-ключи провайдеров', 'ai-proxy-manager' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Ключи хранятся в зашифрованном виде (AES-256-GCM) и используются только для проверки подключения в режиме отладки. Оставьте поле пустым, чтобы сохранить уже введённый ключ.', 'ai-proxy-manager' ); ?>
</p>
<table class="form-table" role="presentation">
<?php
$providers = array(
'openai' => 'OpenAI API Key',
'anthropic' => 'Anthropic API Key',
'google' => 'Google (Gemini) API Key',
);
foreach ( $providers as $slug => $label ) :
$has_key = ! empty( $settings['keys'][ $slug ] );
$use_key = ! empty( $settings['key_enabled'][ $slug ] );
?>
<tr>
<th scope="row">
<label for="aipmsp_key_<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $label ); ?></label>
</th>
<td>
<label class="aipmsp-switch">
<input type="checkbox" name="aipmsp_key_use[]" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $use_key ); ?> />
<span class="aipmsp-slider"></span>
</label>
<span class="aipmsp-switch-label"><?php esc_html_e( 'Использовать для проверки', 'ai-proxy-manager' ); ?></span>
<br /><br />
<input type="password"
name="aipmsp_key_<?php echo esc_attr( $slug ); ?>"
id="aipmsp_key_<?php echo esc_attr( $slug ); ?>"
class="regular-text"
autocomplete="new-password"
placeholder="<?php echo $has_key ? esc_attr__( '•••••••• (ключ сохранён)', 'ai-proxy-manager' ) : ''; ?>" />
<?php if ( $has_key ) : ?>
<label class="aipmsp-clear-key">
<input type="checkbox" name="aipmsp_clear_<?php echo esc_attr( $slug ); ?>" value="1" />
<?php esc_html_e( 'Удалить ключ', 'ai-proxy-manager' ); ?>
</label>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php submit_button( __( 'Сохранить параметры', 'ai-proxy-manager' ) ); ?>
</form>
<hr />
<p class="aipmsp-footer">
<a href="<?php echo esc_url( self::PLUGIN_PAGE_URL ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Страница плагина', 'ai-proxy-manager' ); ?></a>
&nbsp;·&nbsp;
<a href="<?php echo esc_url( self::SUPPORT_URL ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Получить поддержку', 'ai-proxy-manager' ); ?></a>
</p>
</div>
<?php
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
/**
* AJAX-обработчики: health-check прокси.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс AJAX-обработчиков плагина.
*
* @since 1.0.0
*/
class AIPMSP_Ajax {
/**
* Конструктор: регистрирует AJAX-экшены.
*
* @since 1.0.0
*/
public function __construct() {
add_action( 'wp_ajax_aipmsp_check_proxy', array( $this, 'check_proxy' ) );
}
/**
* Проверяет один прокси по запросу из админки.
*
* @since 1.0.0
*
* @return void
*/
public function check_proxy() {
// Проверка nonce и прав.
check_ajax_referer( 'aipmsp_ajax_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'Недостаточно прав.', 'ai-proxy-manager' ) ),
403
);
}
$ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
$port = isset( $_POST['port'] ) ? absint( wp_unslash( $_POST['port'] ) ) : 0;
$type = isset( $_POST['type'] ) ? AIPMSP_Proxy::sanitize_type( wp_unslash( $_POST['type'] ) ) : 'http';
$valid_host = filter_var( $ip, FILTER_VALIDATE_IP ) || AIPMSP_Proxy::is_valid_host( $ip );
if ( ! $valid_host || $port < 1 || $port > 65535 ) {
wp_send_json_error(
array( 'message' => __( 'Некорректный адрес или порт прокси.', 'ai-proxy-manager' ) )
);
}
// Находим прокси в сохранённых настройках, чтобы взять зашифрованный пароль/логин.
$settings = AIPMSP_Proxy::get_settings();
$proxy = null;
foreach ( $settings['proxies'] as $candidate ) {
if ( $candidate['ip'] === $ip && (int) $candidate['port'] === $port ) {
$proxy = $candidate;
break;
}
}
if ( null === $proxy ) {
// Прокси ещё не сохранён — проверяем без авторизации.
$proxy = array(
'ip' => $ip,
'port' => $port,
'type' => $type,
'user' => '',
'pass' => '',
);
}
$result = AIPMSP_Proxy::health_check( $proxy );
if ( $result['success'] ) {
wp_send_json_success(
array(
'message' => $result['message'],
'time' => $result['time'],
)
);
}
wp_send_json_error(
array(
'message' => $result['message'],
'time' => $result['time'],
)
);
}
}
+206
View File
@@ -0,0 +1,206 @@
<?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;
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/**
* Шифрование и расшифровка секретов (AES-256-GCM, аутентифицированное).
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс шифрования секретов плагина.
*
* Использует AES-256-GCM (authenticated encryption): шифротекст защищён
* тегом аутентификации, поэтому подмена или повреждение значения в БД
* будет обнаружена при расшифровке. Ключ выводится из солей wp-config.php,
* поэтому секреты нельзя расшифровать без доступа к конфигурации сайта.
*
* @since 1.0.0
*/
class AIPMSP_Crypto {
/**
* Используемый шифр.
*
* @var string
*/
const CIPHER = 'aes-256-gcm';
/**
* Префикс-маркер зашифрованного значения.
*
* @var string
*/
const PREFIX = 'aipmsp_enc::';
/**
* Длина тега аутентификации GCM в байтах.
*
* @var int
*/
const TAG_LENGTH = 16;
/**
* Возвращает 256-битный ключ шифрования, производный от солей WordPress.
*
* @since 1.0.0
*
* @return string Бинарный ключ длиной 32 байта.
*/
private static function get_key() {
$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'aipmsp-fallback-auth-key';
$auth_salt = defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : 'aipmsp-fallback-salt';
// hash() с raw_output=true даёт ровно 32 байта для AES-256.
return hash( 'sha256', $auth_key . $auth_salt, true );
}
/**
* Шифрует строку.
*
* @since 1.0.0
*
* @param string $plaintext Открытый текст.
* @return string Зашифрованное значение (PREFIX + base64(iv|tag|ciphertext)), либо пустая строка.
*/
public static function encrypt( $plaintext ) {
$plaintext = (string) $plaintext;
if ( '' === $plaintext ) {
return '';
}
if ( ! function_exists( 'openssl_encrypt' ) ) {
// Без OpenSSL шифровать нечем — не сохраняем секрет в открытом виде.
return '';
}
$iv_length = openssl_cipher_iv_length( self::CIPHER );
if ( false === $iv_length || $iv_length <= 0 ) {
return '';
}
$iv = random_bytes( $iv_length );
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
self::get_key(),
OPENSSL_RAW_DATA,
$iv,
$tag,
'',
self::TAG_LENGTH
);
if ( false === $ciphertext ) {
return '';
}
// Храним IV и тег вместе с шифротекстом: PREFIX . base64( iv . tag . ciphertext ).
return self::PREFIX . base64_encode( $iv . $tag . $ciphertext );
}
/**
* Расшифровывает строку, зашифрованную методом encrypt().
*
* @since 1.0.0
*
* @param string $stored Сохранённое значение.
* @return string Открытый текст, либо пустая строка при ошибке/подмене.
*/
public static function decrypt( $stored ) {
$stored = (string) $stored;
if ( '' === $stored ) {
return '';
}
if ( ! self::is_encrypted( $stored ) ) {
// Значение не зашифровано (например, осталось от ручной правки) — возвращаем как есть.
return $stored;
}
if ( ! function_exists( 'openssl_decrypt' ) ) {
return '';
}
$payload = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true );
if ( false === $payload ) {
return '';
}
$iv_length = openssl_cipher_iv_length( self::CIPHER );
if ( false === $iv_length || strlen( $payload ) <= ( $iv_length + self::TAG_LENGTH ) ) {
return '';
}
$iv = substr( $payload, 0, $iv_length );
$tag = substr( $payload, $iv_length, self::TAG_LENGTH );
$ciphertext = substr( $payload, $iv_length + self::TAG_LENGTH );
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
self::get_key(),
OPENSSL_RAW_DATA,
$iv,
$tag
);
// false означает либо ошибку, либо непрошедшую проверку тега аутентификации.
return ( false === $plaintext ) ? '' : $plaintext;
}
/**
* Проверяет, является ли значение зашифрованным этим классом.
*
* @since 1.0.0
*
* @param string $value Проверяемое значение.
* @return bool
*/
public static function is_encrypted( $value ) {
return is_string( $value ) && 0 === strpos( $value, self::PREFIX );
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
/**
* Режим отладки: проверка подключения к AI-провайдерам.
*
* @package AI_Proxy_Manager
*/
defined( 'ABSPATH' ) || exit;
/**
* Класс отладочной проверки подключения.
*
* @since 1.0.0
*/
class AIPMSP_Debug {
/**
* Конструктор: регистрирует вывод admin_notices.
*
* @since 1.0.0
*/
public function __construct() {
add_action( 'admin_notices', array( $this, 'render_notices' ) );
}
/**
* Выводит результат проверки подключения к каждому провайдеру.
*
* @since 1.0.0
*
* @return void
*/
public function render_notices() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = AIPMSP_Proxy::get_settings();
if ( empty( $settings['debug_enabled'] ) ) {
return;
}
// Показываем отладку только на странице плагина, чтобы не засорять всю админку.
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( $screen && false === strpos( (string) $screen->id, 'aipmsp-settings' ) ) {
return;
}
// Расшифровываем ключи только в память.
$openai_key = AIPMSP_Crypto::decrypt( $settings['keys']['openai'] );
$anthropic_key = AIPMSP_Crypto::decrypt( $settings['keys']['anthropic'] );
$google_key = AIPMSP_Crypto::decrypt( $settings['keys']['google'] );
$providers = array(
'OpenAI' => array(
'slug' => 'openai',
'key' => $openai_key,
'url' => 'https://api.openai.com/v1/models',
'headers' => array( 'Authorization' => 'Bearer ' . $openai_key ),
),
'Anthropic' => array(
'slug' => 'anthropic',
'key' => $anthropic_key,
'url' => 'https://api.anthropic.com/v1/models',
'headers' => array(
'x-api-key' => $anthropic_key,
'anthropic-version' => '2023-06-01',
),
),
'Google' => array(
'slug' => 'google',
'key' => $google_key,
'url' => 'https://generativelanguage.googleapis.com/v1beta/models',
'headers' => array( 'x-goog-api-key' => $google_key ),
),
);
echo '<div class="notice notice-info"><p><strong>'
. esc_html__( 'AI Proxy Manager — проверка подключения к AI-провайдерам:', 'ai-proxy-manager' )
. '</strong></p></div>';
foreach ( $providers as $name => $cfg ) {
// Пропускаем провайдера, если его переключатель проверки выключен — без уведомления.
if ( empty( $settings['key_enabled'][ $cfg['slug'] ] ) ) {
continue;
}
// Пропускаем провайдера без ключа.
if ( '' === $cfg['key'] ) {
echo '<div class="notice notice-warning"><pre>';
echo esc_html( $name ) . ":\n";
echo esc_html__( '⏭ Ключ не задан — проверка пропущена', 'ai-proxy-manager' );
echo '</pre></div>';
continue;
}
$response = wp_remote_get(
$cfg['url'],
array(
'timeout' => 20,
'headers' => $cfg['headers'],
)
);
echo '<div class="notice notice-info"><pre>';
echo esc_html( $name ) . ":\n";
if ( is_wp_error( $response ) ) {
echo esc_html__( '❌ ОШИБКА СОЕДИНЕНИЯ (вероятно прокси или сеть):', 'ai-proxy-manager' ) . "\n";
echo esc_html( $response->get_error_message() );
} else {
$code = (int) wp_remote_retrieve_response_code( $response );
echo esc_html__( 'HTTP CODE:', 'ai-proxy-manager' ) . ' ' . esc_html( $code ) . "\n";
if ( 200 === $code ) {
echo esc_html__( '✅ Соединение успешно установлено', 'ai-proxy-manager' );
} elseif ( 401 === $code ) {
echo esc_html__( '❌ Неверный API-ключ', 'ai-proxy-manager' );
} elseif ( 403 === $code ) {
echo esc_html__( '❌ Доступ запрещён (регион или IP заблокирован — проверьте прокси)', 'ai-proxy-manager' );
} elseif ( 429 === $code ) {
echo esc_html__( '⚠️ Превышен лимит запросов (rate limit)', 'ai-proxy-manager' );
} else {
echo esc_html__( '⚠️ Неожиданный код ответа', 'ai-proxy-manager' );
}
}
echo '</pre></div>';
}
}
}
+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;
}
}
+87
View File
@@ -0,0 +1,87 @@
=== AI Proxy Manager ===
Contributors: tooweb
Author: Александр Головин
Author URI: https://быстросайты.рф/
Tags: proxy, openai, anthropic, gemini, api
Requires at least: 7.0
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.0.8
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Маршрутизация исходящих запросов к AI API (OpenAI, Anthropic, Google) через пользовательские прокси-серверы, с режимом отладки подключения.
== Description ==
AI Proxy Manager позволяет администратору WordPress направлять исходящие запросы сайта к API искусственного интеллекта через собственные прокси-серверы. Это помогает обходить региональные блокировки и обеспечивать стабильное подключение к OpenAI, Anthropic (Claude) и Google Gemini.
Возможности:
* Добавление списка прокси в формате ЛОГИН:ПАРОЛЬ@IP:ПОРТ (поддерживается и формат без авторизации IP:ПОРТ).
* Типы прокси: HTTP, HTTPS, SOCKS5.
* Главный переключатель «Использовать прокси на сайте» и индивидуальные переключатели для каждого прокси.
* Маршрутизация только нужных доменов — прокси применяется лишь к запросам к указанным AI-доменам, остальной трафик идёт напрямую.
* Ротация и резервирование: при ошибке одного прокси автоматически пробуется следующий включённый.
* Health-check каждого прокси по кнопке через AJAX (доступность и время ответа).
* Режим отладки: проверка подключения к каждому AI-провайдеру с расшифровкой HTTP-кодов.
* Шифрование секретов: пароли прокси и API-ключи хранятся в зашифрованном виде (AES-256-GCM, ключ выводится из солей wp-config.php).
== Installation ==
1. Загрузите папку `ai-proxy-manager` в каталог `/wp-content/plugins/`.
2. Активируйте плагин в меню «Плагины» админки WordPress.
3. Откройте пункт меню «AI Proxy Manager» в боковой панели админки.
4. Вставьте прокси, выберите тип, включите главный переключатель и сохраните параметры.
5. При необходимости введите API-ключи провайдеров и включите режим отладки для проверки подключения.
== Frequently Asked Questions ==
= Прокси с логином и паролем не работают =
Прокси с авторизацией работают только на транспорте cURL. Убедитесь, что расширение cURL включено на сервере. Плагин выводит предупреждение в админке, если cURL недоступен.
= Где хранятся API-ключи? =
Ключи шифруются алгоритмом AES-256-GCM и хранятся в таблице опций WordPress. Ключ шифрования выводится из солей `AUTH_KEY` и `SECURE_AUTH_SALT` из wp-config.php. Расшифровка происходит только в момент проверки подключения, в память.
= Прокси применяется ко всем запросам сайта? =
Нет. Прокси применяется только к запросам на домены из списка маршрутизации (по умолчанию — домены OpenAI, Anthropic и Google). Все прочие запросы идут напрямую.
== Changelog ==
= 1.0.8 =
* Подготовка к публикации: обновлено поле «Tested up to», добавлены файлы для репозитория, мелкие правки совместимости.
= 1.0.7 =
* Шифрование секретов переведено на аутентифицированный режим AES-256-GCM.
* Пароли прокси больше не выводятся на странице настроек (ни в поле ввода, ни в таблице); при сохранении пустого пароля прежний сохраняется.
= 1.0.6 =
* Все внутренние идентификаторы приведены к единому префиксу aipmsp (включая маркер шифрования), убрана логика переноса со старого префикса.
= 1.0.5 =
* Изменена иконка в меню админки.
* Префикс плагина переименован на aipmsp, чтобы исключить конфликты с другими плагинами (с автоматическим переносом сохранённых настроек).
* В списке плагинов добавлены ссылки «Настройки» и «Детали».
* Под полем ввода прокси добавлена рекомендация проверенного сервиса прокси.
* На странице настроек добавлены ссылки на страницу плагина и поддержку.
= 1.0.4 =
* Подсказка в поле прокси приведена к актуальному формату: логин:пароль@ip:порт.
= 1.0.3 =
* Добавлены переключатели «Использовать для проверки» для каждого AI-провайдера; выключенные провайдеры не проверяются и не выводят уведомлений.
* Обновлён сайт автора.
= 1.0.1 =
* Формат ввода прокси изменён на логин:пароль@ip:порт (с поддержкой пароля, содержащего «:» и «@»).
= 1.0.0 =
* Первый релиз.
== Upgrade Notice ==
= 1.0.8 =
Релиз для каталога WordPress.org. Рекомендуется чистая установка.
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* Удаление данных плагина при деинсталляции.
*
* @package AI_Proxy_Manager
*/
// Запускается только в контексте удаления плагина WordPress.
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
/**
* Удаляем опции плагина из базы.
*/
delete_option( 'aipmsp_settings' );
// На случай мультисайта — чистим опции по всем сайтам.
if ( is_multisite() ) {
$site_ids = get_sites(
array(
'fields' => 'ids',
'number' => 0,
)
);
foreach ( (array) $site_ids as $site_id ) {
switch_to_blog( (int) $site_id );
delete_option( 'aipmsp_settings' );
restore_current_blog();
}
}