1
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 2m28s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 2m25s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 3m12s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 3m23s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m1s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m59s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m15s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 2m43s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 2m28s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 2m25s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 3m12s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 3m23s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m1s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m59s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m15s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 2m43s
This commit is contained in:
@@ -3,8 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gotunnel/internal/client/tunnel"
|
"github.com/gotunnel/internal/client/tunnel"
|
||||||
"github.com/gotunnel/pkg/crypto"
|
"github.com/gotunnel/pkg/crypto"
|
||||||
@@ -17,27 +15,19 @@ func main() {
|
|||||||
token := flag.String("t", "", "auth token")
|
token := flag.String("t", "", "auth token")
|
||||||
id := flag.String("id", "", "client id (optional, auto-assigned if empty)")
|
id := flag.String("id", "", "client id (optional, auto-assigned if empty)")
|
||||||
noTLS := flag.Bool("no-tls", false, "disable TLS")
|
noTLS := flag.Bool("no-tls", false, "disable TLS")
|
||||||
skipVerify := flag.Bool("skip-verify", false, "skip TLS certificate verification (insecure)")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *server == "" || *token == "" {
|
if *server == "" || *token == "" {
|
||||||
log.Fatal("Usage: client -s <server:port> -t <token> [-id <client_id>] [-no-tls] [-skip-verify]")
|
log.Fatal("Usage: client -s <server:port> -t <token> [-id <client_id>] [-no-tls]")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := tunnel.NewClient(*server, *token, *id)
|
client := tunnel.NewClient(*server, *token, *id)
|
||||||
|
|
||||||
// TLS 默认启用,使用 TOFU 验证
|
// TLS 默认启用,默认跳过证书验证(类似 frp)
|
||||||
if !*noTLS {
|
if !*noTLS {
|
||||||
client.TLSEnabled = true
|
client.TLSEnabled = true
|
||||||
// 获取数据目录
|
client.TLSConfig = crypto.ClientTLSConfig()
|
||||||
home, _ := os.UserHomeDir()
|
log.Printf("[Client] TLS enabled")
|
||||||
dataDir := filepath.Join(home, ".gotunnel")
|
|
||||||
client.TLSConfig = crypto.ClientTLSConfigWithTOFU(*server, dataDir, *skipVerify)
|
|
||||||
if *skipVerify {
|
|
||||||
log.Printf("[Client] TLS enabled (certificate verification DISABLED - insecure)")
|
|
||||||
} else {
|
|
||||||
log.Printf("[Client] TLS enabled with TOFU certificate verification")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
|
|||||||
@@ -826,11 +826,24 @@ func (h *APIHandler) getClientPluginConfig(rw http.ResponseWriter, clientID, plu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取插件配置模式
|
// 尝试从内置插件获取配置模式
|
||||||
schema, err := h.server.GetPluginConfigSchema(pluginName)
|
schema, err := h.server.GetPluginConfigSchema(pluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
// 如果内置插件中找不到,尝试从 JS 插件获取
|
||||||
return
|
jsPlugin, jsErr := h.jsPluginStore.GetJSPlugin(pluginName)
|
||||||
|
if jsErr != nil {
|
||||||
|
// 两者都找不到,返回空 schema(允许配置但没有预定义的 schema)
|
||||||
|
schema = []ConfigField{}
|
||||||
|
} else {
|
||||||
|
// 使用 JS 插件的 config 作为动态 schema
|
||||||
|
for key := range jsPlugin.Config {
|
||||||
|
schema = append(schema, ConfigField{
|
||||||
|
Key: key,
|
||||||
|
Label: key,
|
||||||
|
Type: "string",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找客户端的插件配置
|
// 查找客户端的插件配置
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NCard, NButton, NSpace, NTag, NTable, NEmpty,
|
NCard, NButton, NSpace, NTag, NTable, NEmpty,
|
||||||
NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox, NSwitch,
|
NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch,
|
||||||
NIcon, useMessage, useDialog, NSpin
|
NIcon, useMessage, useDialog, NSpin
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
ArrowBackOutline, CreateOutline, TrashOutline,
|
ArrowBackOutline, CreateOutline, TrashOutline,
|
||||||
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
||||||
DownloadOutline, SettingsOutline, StorefrontOutline, RefreshOutline, StopOutline
|
SettingsOutline, StorefrontOutline, RefreshOutline, StopOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import {
|
import {
|
||||||
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient,
|
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient,
|
||||||
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig,
|
getClientPluginConfig, updateClientPluginConfig,
|
||||||
getStorePlugins, installStorePlugin, getRuleSchemas, restartClientPlugin, stopClientPlugin
|
getStorePlugins, installStorePlugin, getRuleSchemas, restartClientPlugin, stopClientPlugin
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
|
import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -74,11 +74,6 @@ const getExtraFields = (type: string): ConfigField[] => {
|
|||||||
return schema?.extra_fields || []
|
return schema?.extra_fields || []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件安装相关
|
|
||||||
const showInstallModal = ref(false)
|
|
||||||
const availablePlugins = ref<PluginInfo[]>([])
|
|
||||||
const selectedPlugins = ref<string[]>([])
|
|
||||||
|
|
||||||
// 插件配置相关
|
// 插件配置相关
|
||||||
const showConfigModal = ref(false)
|
const showConfigModal = ref(false)
|
||||||
const configPluginName = ref('')
|
const configPluginName = ref('')
|
||||||
@@ -93,37 +88,6 @@ const storeLoading = ref(false)
|
|||||||
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
|
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
|
||||||
const storeInstalling = ref(false)
|
const storeInstalling = ref(false)
|
||||||
|
|
||||||
const loadPlugins = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await getPlugins()
|
|
||||||
availablePlugins.value = (data || []).filter(p => p.enabled)
|
|
||||||
|
|
||||||
// 更新类型选项:内置类型 + proxy 类型插件
|
|
||||||
const proxyPlugins = availablePlugins.value
|
|
||||||
.filter(p => p.type === 'proxy')
|
|
||||||
.map(p => ({ label: `${p.name.toUpperCase()} (插件)`, value: p.name }))
|
|
||||||
typeOptions.value = [...builtinTypes, ...proxyPlugins]
|
|
||||||
|
|
||||||
// 合并插件的 RuleSchema 到 pluginRuleSchemas
|
|
||||||
for (const p of availablePlugins.value) {
|
|
||||||
if (p.rule_schema) {
|
|
||||||
pluginRuleSchemas.value[p.name] = p.rule_schema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load plugins', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openInstallModal = async () => {
|
|
||||||
await loadPlugins()
|
|
||||||
// 过滤掉已安装的插件
|
|
||||||
const installedNames = clientPlugins.value.map(p => p.name)
|
|
||||||
availablePlugins.value = availablePlugins.value.filter(p => !installedNames.includes(p.name))
|
|
||||||
selectedPlugins.value = []
|
|
||||||
showInstallModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 商店插件相关函数
|
// 商店插件相关函数
|
||||||
const openStoreModal = async () => {
|
const openStoreModal = async () => {
|
||||||
showStoreModal.value = true
|
showStoreModal.value = true
|
||||||
@@ -160,11 +124,6 @@ const handleInstallStorePlugin = async (plugin: StorePluginInfo) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
|
||||||
const labels: Record<string, string> = { proxy: '协议', app: '应用', service: '服务', tool: '工具' }
|
|
||||||
return labels[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadClient = async () => {
|
const loadClient = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await getClient(clientId)
|
const { data } = await getClient(clientId)
|
||||||
@@ -182,7 +141,6 @@ const loadClient = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadRuleSchemas() // 加载内置协议配置模式
|
loadRuleSchemas() // 加载内置协议配置模式
|
||||||
loadClient()
|
loadClient()
|
||||||
loadPlugins()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 打开重命名弹窗
|
// 打开重命名弹窗
|
||||||
@@ -339,21 +297,6 @@ const handleStopPlugin = async (plugin: ClientPlugin) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const installPlugins = async () => {
|
|
||||||
if (selectedPlugins.value.length === 0) {
|
|
||||||
message.warning('请选择要安装的插件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await installPluginsToClient(clientId, selectedPlugins.value)
|
|
||||||
message.success(`已推送 ${selectedPlugins.value.length} 个插件到客户端`)
|
|
||||||
showInstallModal.value = false
|
|
||||||
await loadClient() // 刷新客户端数据
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.response?.data || '安装失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
||||||
const newEnabled = !plugin.enabled
|
const newEnabled = !plugin.enabled
|
||||||
const updatedPlugins = clientPlugins.value.map(p =>
|
const updatedPlugins = clientPlugins.value.map(p =>
|
||||||
@@ -441,10 +384,6 @@ const savePluginConfig = async () => {
|
|||||||
<template #icon><n-icon><PushOutline /></n-icon></template>
|
<template #icon><n-icon><PushOutline /></n-icon></template>
|
||||||
推送配置
|
推送配置
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="success" @click="openInstallModal">
|
|
||||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
|
||||||
安装插件
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="openStoreModal">
|
<n-button @click="openStoreModal">
|
||||||
<template #icon><n-icon><StorefrontOutline /></n-icon></template>
|
<template #icon><n-icon><StorefrontOutline /></n-icon></template>
|
||||||
从商店安装
|
从商店安装
|
||||||
@@ -644,39 +583,6 @@ const savePluginConfig = async () => {
|
|||||||
</n-table>
|
</n-table>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 安装插件模态框 -->
|
|
||||||
<n-modal v-model:show="showInstallModal" preset="card" title="安装插件到客户端" style="width: 500px;">
|
|
||||||
<n-empty v-if="availablePlugins.length === 0" description="暂无可用插件" />
|
|
||||||
<n-space v-else vertical :size="12">
|
|
||||||
<n-card v-for="plugin in availablePlugins" :key="plugin.name" size="small">
|
|
||||||
<n-space justify="space-between" align="center">
|
|
||||||
<n-space vertical :size="4">
|
|
||||||
<n-space align="center">
|
|
||||||
<span style="font-weight: 500;">{{ plugin.name }}</span>
|
|
||||||
<n-tag size="small">{{ getTypeLabel(plugin.type) }}</n-tag>
|
|
||||||
</n-space>
|
|
||||||
<span style="color: #666; font-size: 12px;">{{ plugin.description }}</span>
|
|
||||||
</n-space>
|
|
||||||
<n-checkbox
|
|
||||||
:checked="selectedPlugins.includes(plugin.name)"
|
|
||||||
@update:checked="(v: boolean) => {
|
|
||||||
if (v) selectedPlugins.push(plugin.name)
|
|
||||||
else selectedPlugins = selectedPlugins.filter(n => n !== plugin.name)
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</n-space>
|
|
||||||
<template #footer>
|
|
||||||
<n-space justify="end">
|
|
||||||
<n-button @click="showInstallModal = false">取消</n-button>
|
|
||||||
<n-button type="primary" @click="installPlugins" :disabled="selectedPlugins.length === 0">
|
|
||||||
安装 ({{ selectedPlugins.length }})
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
|
|
||||||
<!-- 插件配置模态框 -->
|
<!-- 插件配置模态框 -->
|
||||||
<n-modal v-model:show="showConfigModal" preset="card" :title="`${configPluginName} 配置`" style="width: 500px;">
|
<n-modal v-model:show="showConfigModal" preset="card" :title="`${configPluginName} 配置`" style="width: 500px;">
|
||||||
<n-empty v-if="configLoading" description="加载中..." />
|
<n-empty v-if="configLoading" description="加载中..." />
|
||||||
|
|||||||
Reference in New Issue
Block a user