feat: remove unused plugin version comparison and types, refactor proxy server to support authentication

- Deleted version comparison logic from `pkg/plugin/sign/version.go`.
- Removed unused types and constants from `pkg/plugin/types.go`.
- Updated `pkg/protocol/message.go` to remove plugin-related message types.
- Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies.
- Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors.
- Added new API endpoint to generate installation commands with a token for clients.
- Created database functions to manage installation tokens in `internal/server/db/install_token.go`.
- Implemented the installation command generation logic in `internal/server/router/handler/install.go`.
- Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`.
This commit is contained in:
2026-03-17 23:16:30 +08:00
parent dcfd2f4466
commit 5a03d9e1f1
42 changed files with 638 additions and 6161 deletions

View File

@@ -1,5 +1,5 @@
import { get, post, put, del, getToken } from '../config/axios'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin, RuleSchemasMap, LogEntry, LogStreamOptions, ConfigField } from '../types'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, LogEntry, LogStreamOptions, InstallCommandResponse } from '../types'
// 重新导出 token 管理方法
export { getToken, setToken, removeToken } from '../config/axios'
@@ -24,80 +24,8 @@ export const reloadConfig = () => post('/config/reload')
export const pushConfigToClient = (id: string) => post(`/client/${id}/push`)
export const disconnectClient = (id: string) => post(`/client/${id}/disconnect`)
export const restartClient = (id: string) => post(`/client/${id}/restart`)
export const installPluginsToClient = (id: string, plugins: string[]) =>
post(`/client/${id}/install-plugins`, { plugins })
// 规则配置模式
export const getRuleSchemas = () => get<RuleSchemasMap>('/rule-schemas')
// 客户端插件控制(使用 pluginID
export const startClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
post(`/client/${clientId}/plugin/${pluginId}/start`, { rule_name: ruleName })
export const stopClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
post(`/client/${clientId}/plugin/${pluginId}/stop`, { rule_name: ruleName })
export const restartClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
post(`/client/${clientId}/plugin/${pluginId}/restart`, { rule_name: ruleName })
export const deleteClientPlugin = (clientId: string, pluginId: string) =>
post(`/client/${clientId}/plugin/${pluginId}/delete`)
export const updateClientPluginConfigWithRestart = (clientId: string, pluginId: string, ruleName: string, config: Record<string, string>, restart: boolean) =>
post(`/client/${clientId}/plugin/${pluginId}/config`, { rule_name: ruleName, config, restart })
// 插件管理
export const getPlugins = () => get<PluginInfo[]>('/plugins')
export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
// 扩展商店
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
export const installStorePlugin = (
pluginName: string,
downloadUrl: string,
signatureUrl: string,
clientId: string,
remotePort?: number,
version?: string,
configSchema?: ConfigField[],
authEnabled?: boolean,
authUsername?: string,
authPassword?: string
) =>
post('/store/install', {
plugin_name: pluginName,
version: version || '',
download_url: downloadUrl,
signature_url: signatureUrl,
client_id: clientId,
remote_port: remotePort || 0,
config_schema: configSchema || [],
auth_enabled: authEnabled || false,
auth_username: authUsername || '',
auth_password: authPassword || ''
})
// 客户端插件配置
export const getClientPluginConfig = (clientId: string, pluginName: string) =>
get<PluginConfigResponse>(`/client-plugin/${clientId}/${pluginName}/config`)
export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record<string, string>) =>
put(`/client-plugin/${clientId}/${pluginName}/config`, { config })
// JS 插件管理
export const getJSPlugins = () => get<JSPlugin[]>('/js-plugins')
export const createJSPlugin = (plugin: JSPlugin) => post('/js-plugins', plugin)
export const getJSPlugin = (name: string) => get<JSPlugin>(`/js-plugin/${name}`)
export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugin/${name}`, plugin)
export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`)
export const pushJSPluginToClient = (pluginName: string, clientId: string, remotePort?: number) =>
post(`/js-plugin/${pluginName}/push/${clientId}`, { remote_port: remotePort || 0 })
export const updateJSPluginConfig = (name: string, config: Record<string, string>) =>
put(`/js-plugin/${name}/config`, { config })
export const setJSPluginEnabled = (name: string, enabled: boolean) =>
post(`/js-plugin/${name}/${enabled ? 'enable' : 'disable'}`)
// 插件 API 代理(通过 pluginID 调用插件自定义 API
export const callPluginAPI = <T = any>(clientId: string, pluginId: string, method: string, route: string, body?: any) => {
const path = `/client/${clientId}/plugin-api/${pluginId}${route.startsWith('/') ? route : '/' + route}`
switch (method.toUpperCase()) {
case 'GET':
// 更新管理
return get<T>(path)
case 'POST':
return post<T>(path, body)
@@ -265,3 +193,7 @@ export interface UpdateServerConfigRequest {
export const getServerConfig = () => get<ServerConfigResponse>('/config')
export const updateServerConfig = (config: UpdateServerConfigRequest) => put('/config', config)
// 安装命令生成
export const generateInstallCommand = (clientId: string) =>
post<InstallCommandResponse>('/install/generate', { client_id: clientId })

View File

@@ -6,43 +6,6 @@ export interface ProxyRule {
remote_port: number
type?: string
enabled?: boolean
plugin_config?: Record<string, string>
plugin_managed?: boolean // 插件管理标记 - 由插件自动创建的规则
}
// 客户端已安装的插件
export interface ClientPlugin {
id: string // 插件实例唯一 ID
name: string
version: string
enabled: boolean
running: boolean
config?: Record<string, string>
remote_port?: number // 远程监听端口
}
// 插件配置字段
export interface ConfigField {
key: string
label: string
type: 'string' | 'number' | 'bool' | 'select' | 'password'
default?: string
required?: boolean
options?: string[]
description?: string
}
// 规则表单模式
export interface RuleSchema {
needs_local_addr: boolean
extra_fields?: ConfigField[]
}
// 插件配置响应
export interface PluginConfigResponse {
plugin_name: string
schema: ConfigField[]
config: Record<string, string>
}
// 客户端配置
@@ -50,7 +13,6 @@ export interface ClientConfig {
id: string
nickname?: string
rules: ProxyRule[]
plugins?: ClientPlugin[]
}
// 客户端状态
@@ -70,7 +32,6 @@ export interface ClientDetail {
id: string
nickname?: string
rules: ProxyRule[]
plugins?: ClientPlugin[]
online: boolean
last_ping?: string
remote_addr?: string
@@ -88,64 +49,12 @@ export interface ServerStatus {
client_count: number
}
// 插件类型
export const PluginType = {
Proxy: 'proxy',
App: 'app',
Service: 'service',
Tool: 'tool'
} as const
export type PluginTypeValue = typeof PluginType[keyof typeof PluginType]
// 插件信息
export interface PluginInfo {
name: string
version: string
type: string
description: string
source: string
icon?: string
enabled: boolean
rule_schema?: RuleSchema
}
// 扩展商店插件信息
export interface StorePluginInfo {
name: string
version: string
type: string
description: string
author: string
icon?: string
download_url?: string
signature_url?: string
config_schema?: ConfigField[]
}
// JS 插件信息
export interface JSPlugin {
name: string
source: string
signature?: string
description: string
author: string
version?: string
auto_push: string[]
config: Record<string, string>
auto_start: boolean
enabled: boolean
}
// 规则配置模式集合
export type RuleSchemasMap = Record<string, RuleSchema>
// 日志条目
export interface LogEntry {
ts: number // Unix 时间戳 (毫秒)
level: string // 日志级别: debug, info, warn, error
msg: string // 日志消息
src: string // 来源: client, plugin:<name>
src: string // 来源: client
}
// 日志流选项
@@ -154,3 +63,15 @@ export interface LogStreamOptions {
follow?: boolean // 是否持续推送
level?: string // 日志级别过滤
}
// 安装命令响应
export interface InstallCommandResponse {
token: string
commands: {
linux: string
macos: string
windows: string
}
expires_at: number
server_addr: string
}

View File

@@ -1,12 +1,16 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { getClients } from '../api'
import type { ClientStatus } from '../types'
import { getClients, generateInstallCommand } from '../api'
import type { ClientStatus, InstallCommandResponse } from '../types'
const router = useRouter()
const clients = ref<ClientStatus[]>([])
const loading = ref(true)
const showInstallModal = ref(false)
const installData = ref<InstallCommandResponse | null>(null)
const installClientId = ref('')
const generatingInstall = ref(false)
const loadClients = async () => {
loading.value = true
@@ -26,6 +30,29 @@ const viewClient = (id: string) => {
router.push(`/client/${id}`)
}
const openInstallModal = async () => {
installClientId.value = `client-${Date.now()}`
generatingInstall.value = true
try {
const { data } = await generateInstallCommand(installClientId.value)
installData.value = data
showInstallModal.value = true
} catch (e) {
console.error('Failed to generate install command', e)
} finally {
generatingInstall.value = false
}
}
const copyCommand = (cmd: string) => {
navigator.clipboard.writeText(cmd)
}
const closeInstallModal = () => {
showInstallModal.value = false
installData.value = null
}
onMounted(loadClients)
</script>
@@ -65,7 +92,12 @@ onMounted(loadClients)
<div class="glass-card">
<div class="card-header">
<h3>客户端列表</h3>
<button class="glass-btn small" @click="loadClients">刷新</button>
<div style="display: flex; gap: 8px;">
<button class="glass-btn small" @click="openInstallModal" :disabled="generatingInstall">
{{ generatingInstall ? '生成中...' : '安装命令' }}
</button>
<button class="glass-btn small" @click="loadClients">刷新</button>
</div>
</div>
<div class="card-body">
<div v-if="loading" class="loading-state">加载中...</div>
@@ -101,6 +133,42 @@ onMounted(loadClients)
</div>
</div>
</div>
<!-- Install Command Modal -->
<div v-if="showInstallModal" class="modal-overlay" @click="closeInstallModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>客户端安装命令</h3>
<button class="close-btn" @click="closeInstallModal">×</button>
</div>
<div class="modal-body" v-if="installData">
<p class="install-hint">选择您的操作系统复制命令并在目标机器上执行</p>
<div class="install-section">
<h4>Linux</h4>
<div class="command-box">
<code>{{ installData.commands.linux }}</code>
<button class="copy-btn" @click="copyCommand(installData.commands.linux)">复制</button>
</div>
</div>
<div class="install-section">
<h4>macOS</h4>
<div class="command-box">
<code>{{ installData.commands.macos }}</code>
<button class="copy-btn" @click="copyCommand(installData.commands.macos)">复制</button>
</div>
</div>
<div class="install-section">
<h4>Windows</h4>
<div class="command-box">
<code>{{ installData.commands.windows }}</code>
<button class="copy-btn" @click="copyCommand(installData.commands.windows)">复制</button>
</div>
</div>
<p class="token-info">客户端ID: <strong>{{ installClientId }}</strong></p>
<p class="token-warning"> 此命令包含一次性token使用后需重新生成</p>
</div>
</div>
</div>
</div>
</template>
@@ -427,4 +495,117 @@ onMounted(loadClients)
transform: scale(1.1);
}
}
/* Install Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--glass-border);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.close-btn {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: var(--color-text-secondary);
line-height: 1;
}
.modal-body {
padding: 24px;
}
.install-hint {
margin-bottom: 20px;
color: var(--color-text-secondary);
}
.install-section {
margin-bottom: 20px;
}
.install-section h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--color-text-primary);
}
.command-box {
display: flex;
gap: 8px;
background: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 8px;
border: 1px solid var(--glass-border);
}
.command-box code {
flex: 1;
font-family: monospace;
font-size: 12px;
word-break: break-all;
color: var(--color-text-primary);
}
.copy-btn {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
color: var(--color-text-primary);
white-space: nowrap;
}
.copy-btn:hover {
background: var(--glass-bg-hover);
}
.token-info {
margin-top: 20px;
padding: 12px;
background: rgba(59, 130, 246, 0.1);
border-radius: 8px;
font-size: 13px;
}
.token-warning {
margin-top: 12px;
padding: 12px;
background: rgba(245, 158, 11, 0.1);
border-radius: 8px;
font-size: 13px;
color: #f59e0b;
}
</style>