commit cb5a0589849c3153af42e8c0f68fdfdcd2d2bcd0 Author: Tooweb Date: Wed Jul 29 22:52:47 2026 +0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dcf897 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..065f18e --- /dev/null +++ b/README.md @@ -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) diff --git a/ai-proxy-manager.php b/ai-proxy-manager.php new file mode 100644 index 0000000..fb0a102 --- /dev/null +++ b/ai-proxy-manager.php @@ -0,0 +1,157 @@ + 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' ); diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..08b72bb --- /dev/null +++ b/assets/css/admin.css @@ -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; +} diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..7cc0546 --- /dev/null +++ b/assets/js/admin.js @@ -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 ); diff --git a/includes/class-aipmsp-admin.php b/includes/class-aipmsp-admin.php new file mode 100644 index 0000000..c497cc9 --- /dev/null +++ b/includes/class-aipmsp-admin.php @@ -0,0 +1,543 @@ +menu_icon(), + 81 + ); + } + + /** + * Добавляет ссылку «Настройки» рядом с кнопкой деактивации плагина. + * + * @since 1.0.5 + * + * @param array $links Существующие ссылки действий. + * @return array + */ + public function action_links( $links ) { + $settings_link = sprintf( + '%s', + 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( + '%s', + 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 '

' + . esc_html__( 'Настройки сохранены.', 'ai-proxy-manager' ) + . '

'; + } + ?> +
+

+ + +
+

+

+
+ + + +
+

+

+
+ + +
+ + + + +

+ + + + + + + + + + + + + + + +

+ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ +

+ + +

+ + + + + + + +

+

+ +

+ + '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 ] ); + ?> + + + + + + + + +
+ +
+ +
+ __( 'Недостаточно прав.', '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'], + ) + ); + } +} diff --git a/includes/class-aipmsp-core.php b/includes/class-aipmsp-core.php new file mode 100644 index 0000000..2cb43c1 --- /dev/null +++ b/includes/class-aipmsp-core.php @@ -0,0 +1,206 @@ +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; + } +} diff --git a/includes/class-aipmsp-crypto.php b/includes/class-aipmsp-crypto.php new file mode 100644 index 0000000..4e30ba6 --- /dev/null +++ b/includes/class-aipmsp-crypto.php @@ -0,0 +1,170 @@ +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 '

' + . esc_html__( 'AI Proxy Manager — проверка подключения к AI-провайдерам:', 'ai-proxy-manager' ) + . '

'; + + foreach ( $providers as $name => $cfg ) { + // Пропускаем провайдера, если его переключатель проверки выключен — без уведомления. + if ( empty( $settings['key_enabled'][ $cfg['slug'] ] ) ) { + continue; + } + + // Пропускаем провайдера без ключа. + if ( '' === $cfg['key'] ) { + echo '
';
+				echo esc_html( $name ) . ":\n";
+				echo esc_html__( '⏭ Ключ не задан — проверка пропущена', 'ai-proxy-manager' );
+				echo '
'; + continue; + } + + $response = wp_remote_get( + $cfg['url'], + array( + 'timeout' => 20, + 'headers' => $cfg['headers'], + ) + ); + + echo '
';
+			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 '
'; + } + } +} diff --git a/includes/class-aipmsp-proxy.php b/includes/class-aipmsp-proxy.php new file mode 100644 index 0000000..6e92499 --- /dev/null +++ b/includes/class-aipmsp-proxy.php @@ -0,0 +1,361 @@ + 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; + } +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..5f02141 --- /dev/null +++ b/readme.txt @@ -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. Рекомендуется чистая установка. diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..0aa16f5 --- /dev/null +++ b/uninstall.php @@ -0,0 +1,30 @@ + 'ids', + 'number' => 0, + ) + ); + + foreach ( (array) $site_ids as $site_id ) { + switch_to_blog( (int) $site_id ); + delete_option( 'aipmsp_settings' ); + restore_current_blog(); + } +}