Initial commit
This commit is contained in:
+22
@@ -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
|
||||
@@ -0,0 +1,81 @@
|
||||
# AI Proxy Manager
|
||||
|
||||
Маршрутизация исходящих запросов WordPress к API искусственного интеллекта (OpenAI, Anthropic, Google Gemini) через пользовательские прокси-серверы, с режимом отладки подключения.
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-2.0.html)
|
||||

|
||||

|
||||
|
||||
## Возможности
|
||||
|
||||
- Список прокси в формате `ЛОГИН:ПАРОЛЬ@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)
|
||||
@@ -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' );
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 );
|
||||
@@ -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>
|
||||
·
|
||||
<a href="<?php echo esc_url( self::SUPPORT_URL ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Получить поддержку', 'ai-proxy-manager' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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. Рекомендуется чистая установка.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user