This commit is contained in:
@@ -51,11 +51,11 @@ func NewManager(cacheDir string) (*Manager, error) {
|
||||
// registerBuiltins 注册内置 plugins
|
||||
// 注意: tcp, udp, http, https 是内置类型,直接在 tunnel 中处理
|
||||
func (m *Manager) registerBuiltins() error {
|
||||
// 注册 SOCKS5 plugin
|
||||
if err := m.registry.RegisterBuiltin(builtin.NewSOCKS5Plugin()); err != nil {
|
||||
// 使用统一的插件注册入口
|
||||
if err := m.registry.RegisterAll(builtin.GetAll()); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("[Plugin] Builtin plugins registered: socks5")
|
||||
log.Printf("[Plugin] Registered %d builtin plugins", len(builtin.GetAll()))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gotunnel/pkg/plugin"
|
||||
"github.com/gotunnel/pkg/protocol"
|
||||
"github.com/gotunnel/pkg/relay"
|
||||
"github.com/hashicorp/yamux"
|
||||
@@ -22,24 +24,27 @@ const (
|
||||
reconnectDelay = 5 * time.Second
|
||||
disconnectDelay = 3 * time.Second
|
||||
udpBufferSize = 65535
|
||||
idFileName = ".gotunnel_id"
|
||||
)
|
||||
|
||||
// Client 隧道客户端
|
||||
type Client struct {
|
||||
ServerAddr string
|
||||
Token string
|
||||
ID string
|
||||
TLSEnabled bool
|
||||
TLSConfig *tls.Config
|
||||
session *yamux.Session
|
||||
rules []protocol.ProxyRule
|
||||
mu sync.RWMutex
|
||||
ServerAddr string
|
||||
Token string
|
||||
ID string
|
||||
TLSEnabled bool
|
||||
TLSConfig *tls.Config
|
||||
session *yamux.Session
|
||||
rules []protocol.ProxyRule
|
||||
mu sync.RWMutex
|
||||
pluginRegistry *plugin.Registry
|
||||
}
|
||||
|
||||
// NewClient 创建客户端
|
||||
func NewClient(serverAddr, token, id string) *Client {
|
||||
// 如果未指定 ID,尝试从本地文件加载
|
||||
if id == "" {
|
||||
id = uuid.New().String()[:8]
|
||||
id = loadClientID()
|
||||
}
|
||||
return &Client{
|
||||
ServerAddr: serverAddr,
|
||||
@@ -48,6 +53,36 @@ func NewClient(serverAddr, token, id string) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// getIDFilePath 获取 ID 文件路径
|
||||
func getIDFilePath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return idFileName
|
||||
}
|
||||
return filepath.Join(home, idFileName)
|
||||
}
|
||||
|
||||
// loadClientID 从本地文件加载客户端 ID
|
||||
func loadClientID() string {
|
||||
data, err := os.ReadFile(getIDFilePath())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// saveClientID 保存客户端 ID 到本地文件
|
||||
func saveClientID(id string) {
|
||||
if err := os.WriteFile(getIDFilePath(), []byte(id), 0600); err != nil {
|
||||
log.Printf("[Client] Failed to save client ID: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetPluginRegistry 设置插件注册表
|
||||
func (c *Client) SetPluginRegistry(registry *plugin.Registry) {
|
||||
c.pluginRegistry = registry
|
||||
}
|
||||
|
||||
// Run 启动客户端(带断线重连)
|
||||
func (c *Client) Run() error {
|
||||
for {
|
||||
@@ -102,6 +137,13 @@ func (c *Client) connect() error {
|
||||
return fmt.Errorf("auth failed: %s", authResp.Message)
|
||||
}
|
||||
|
||||
// 如果服务端分配了新 ID,则更新并保存
|
||||
if authResp.ClientID != "" && authResp.ClientID != c.ID {
|
||||
c.ID = authResp.ClientID
|
||||
saveClientID(c.ID)
|
||||
log.Printf("[Client] New ID assigned and saved: %s", c.ID)
|
||||
}
|
||||
|
||||
log.Printf("[Client] Authenticated as %s", c.ID)
|
||||
|
||||
session, err := yamux.Client(conn, nil)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/gotunnel/internal/server/config"
|
||||
"github.com/gotunnel/internal/server/db"
|
||||
"github.com/gotunnel/internal/server/router"
|
||||
"github.com/gotunnel/pkg/auth"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
@@ -92,6 +93,35 @@ func (w *WebServer) RunWithAuth(addr, username, password string) error {
|
||||
return http.ListenAndServe(addr, handler)
|
||||
}
|
||||
|
||||
// RunWithJWT 启动带 JWT 认证的 Web 服务
|
||||
func (w *WebServer) RunWithJWT(addr, username, password, jwtSecret string) error {
|
||||
r := router.New()
|
||||
|
||||
// JWT 认证器
|
||||
jwtAuth := auth.NewJWTAuth(jwtSecret, 24) // 24小时过期
|
||||
|
||||
// 注册认证路由(不需要认证)
|
||||
authHandler := router.NewAuthHandler(username, password, jwtAuth)
|
||||
router.RegisterAuthRoutes(r, authHandler)
|
||||
|
||||
// 注册业务路由
|
||||
router.RegisterRoutes(r, w)
|
||||
|
||||
// 静态文件
|
||||
staticFS, err := fs.Sub(staticFiles, "dist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Handle("/", spaHandler{fs: http.FS(staticFS)})
|
||||
|
||||
// JWT 中间件,只对 /api/ 路径进行认证(排除 /api/auth/)
|
||||
skipPaths := []string{"/api/auth/"}
|
||||
handler := router.JWTMiddleware(jwtAuth, skipPaths, r.Handler())
|
||||
|
||||
log.Printf("[Web] Console listening on %s (JWT auth enabled)", addr)
|
||||
return http.ListenAndServe(addr, handler)
|
||||
}
|
||||
|
||||
// GetClientStore 获取客户端存储
|
||||
func (w *WebServer) GetClientStore() db.ClientStore {
|
||||
return w.ClientStore
|
||||
|
||||
1
internal/server/app/dist/assets/ArrowBackOutline-QaNKMlLc.js
vendored
Normal file
1
internal/server/app/dist/assets/ArrowBackOutline-QaNKMlLc.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{k as n,S as r,V as o,U as t}from"./vue-vendor-k28cQfDw.js";const l={xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",viewBox:"0 0 512 512"},a=n({name:"ArrowBackOutline",render:function(i,e){return t(),r("svg",l,e[0]||(e[0]=[o("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"48",d:"M244 400L100 256l144-144"},null,-1),o("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"48",d:"M120 256h292"},null,-1)]))}});export{a as A};
|
||||
1
internal/server/app/dist/assets/ClientView-DMo3F6A5.js
vendored
Normal file
1
internal/server/app/dist/assets/ClientView-DMo3F6A5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{d as D,j as F,r,o as I,c as n,a as e,b as p,k as C,u as S,t as s,n as J,F as V,e as g,l as M,m as O,p as P,f as _,v,i as o,_ as $}from"./index-BvqIwKwu.js";const j={class:"client-view"},z={class:"header"},L={key:0,class:"ping-info"},T={class:"rules-section"},q={class:"section-header"},A={key:0},G={key:0,class:"rules-table"},H={key:1,class:"edit-form"},K=["onUpdate:modelValue"],Q=["onUpdate:modelValue"],W=["onUpdate:modelValue"],X=["onUpdate:modelValue"],Y=["onClick"],Z=D({__name:"ClientView",setup(ee){const w=F(),f=S(),u=w.params.id,m=r(!1),h=r(""),b=r([]),i=r(!1),d=r([]),y=async()=>{try{const{data:l}=await M(u);m.value=l.online,h.value=l.last_ping||"",b.value=l.rules||[]}catch(l){console.error("Failed to load client",l)}};I(y);const U=()=>{d.value=JSON.parse(JSON.stringify(b.value)),i.value=!0},R=()=>{i.value=!1},x=()=>{d.value.push({name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080})},E=l=>{d.value.splice(l,1)},N=async()=>{try{await O(u,{id:u,rules:d.value}),i.value=!1,y()}catch{alert("保存失败")}},B=async()=>{if(confirm("确定删除此客户端?"))try{await P(u),f.push("/")}catch{alert("删除失败")}};return(l,c)=>(o(),n("div",j,[e("div",z,[e("button",{class:"btn",onClick:c[0]||(c[0]=t=>C(f).push("/"))},"← 返回"),e("h2",null,s(C(u)),1),e("span",{class:J(["status-badge",m.value?"online":"offline"])},s(m.value?"在线":"离线"),3)]),h.value?(o(),n("div",L,"最后心跳: "+s(h.value),1)):p("",!0),e("div",T,[e("div",q,[c[1]||(c[1]=e("h3",null,"代理规则",-1)),i.value?p("",!0):(o(),n("div",A,[e("button",{class:"btn primary",onClick:U},"编辑"),e("button",{class:"btn danger",onClick:B},"删除")]))]),i.value?p("",!0):(o(),n("table",G,[c[2]||(c[2]=e("thead",null,[e("tr",null,[e("th",null,"名称"),e("th",null,"本地地址"),e("th",null,"远程端口")])],-1)),e("tbody",null,[(o(!0),n(V,null,g(b.value,t=>(o(),n("tr",{key:t.name},[e("td",null,s(t.name),1),e("td",null,s(t.local_ip)+":"+s(t.local_port),1),e("td",null,s(t.remote_port),1)]))),128))])])),i.value?(o(),n("div",H,[(o(!0),n(V,null,g(d.value,(t,k)=>(o(),n("div",{key:k,class:"rule-row"},[_(e("input",{"onUpdate:modelValue":a=>t.name=a,placeholder:"名称"},null,8,K),[[v,t.name]]),_(e("input",{"onUpdate:modelValue":a=>t.local_ip=a,placeholder:"本地IP"},null,8,Q),[[v,t.local_ip]]),_(e("input",{"onUpdate:modelValue":a=>t.local_port=a,type:"number",placeholder:"本地端口"},null,8,W),[[v,t.local_port,void 0,{number:!0}]]),_(e("input",{"onUpdate:modelValue":a=>t.remote_port=a,type:"number",placeholder:"远程端口"},null,8,X),[[v,t.remote_port,void 0,{number:!0}]]),e("button",{class:"btn-icon",onClick:a=>E(k)},"×",8,Y)]))),128)),e("button",{class:"btn secondary",onClick:x},"+ 添加规则"),e("div",{class:"edit-actions"},[e("button",{class:"btn",onClick:R},"取消"),e("button",{class:"btn primary",onClick:N},"保存")])])):p("",!0)])]))}}),le=$(Z,[["__scopeId","data-v-01b16887"]]);export{le as default};
|
||||
@@ -1 +0,0 @@
|
||||
.header[data-v-01b16887]{display:flex;align-items:center;gap:16px;margin-bottom:20px}.header h2[data-v-01b16887]{margin:0}.status-badge[data-v-01b16887]{padding:4px 12px;border-radius:12px;font-size:12px}.status-badge.online[data-v-01b16887]{background:#d4edda;color:#155724}.status-badge.offline[data-v-01b16887]{background:#f8d7da;color:#721c24}.ping-info[data-v-01b16887]{color:#666;margin-bottom:20px}.rules-section[data-v-01b16887]{background:#fff;border-radius:8px;padding:20px;box-shadow:0 2px 4px #0000001a}.section-header[data-v-01b16887]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.section-header h3[data-v-01b16887]{margin:0}.section-header .btn[data-v-01b16887]{margin-left:8px}.btn[data-v-01b16887]{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px}.btn.primary[data-v-01b16887]{background:#3498db;color:#fff}.btn.secondary[data-v-01b16887]{background:#95a5a6;color:#fff}.btn.danger[data-v-01b16887]{background:#e74c3c;color:#fff}.rules-table[data-v-01b16887]{width:100%;border-collapse:collapse}.rules-table th[data-v-01b16887],.rules-table td[data-v-01b16887]{padding:12px;text-align:left;border-bottom:1px solid #eee}.rules-table th[data-v-01b16887]{font-weight:600}.rule-row[data-v-01b16887]{display:flex;gap:8px;margin-bottom:8px}.rule-row input[data-v-01b16887]{flex:1;padding:8px;border:1px solid #ddd;border-radius:4px}.btn-icon[data-v-01b16887]{background:#e74c3c;color:#fff;border:none;border-radius:4px;width:32px;cursor:pointer}.edit-actions[data-v-01b16887]{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
|
||||
1
internal/server/app/dist/assets/HomeView-CC6tujY_.js
vendored
Normal file
1
internal/server/app/dist/assets/HomeView-CC6tujY_.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{k as B,r as V,c as y,o as z,S as p,V as r,_ as l,X as f,Y as a,N as e,F as S,R as $,Z as j,U as u,a4 as F,$ as i,j as m,a3 as R}from"./vue-vendor-k28cQfDw.js";import{e as g,f as E,g as G,h as d,N as c,i as _,j as k,k as N,B as M}from"./index-BJ4y0MF5.js";const T={class:"home"},D={style:{margin:"0 0 4px 0"}},H={key:0,style:{margin:"0 0 8px 0",color:"#999","font-size":"12px"}},Y=B({__name:"HomeView",setup(L){const h=j(),n=V([]),x=async()=>{try{const{data:t}=await G();n.value=t||[]}catch(t){console.error("Failed to load clients",t)}},C=y(()=>n.value.filter(t=>t.online).length),b=y(()=>n.value.reduce((t,o)=>t+o.rule_count,0));z(x);const v=t=>{h.push(`/client/${t}`)};return(t,o)=>(u(),p("div",T,[o[1]||(o[1]=r("div",{style:{"margin-bottom":"24px"}},[r("h2",{style:{margin:"0 0 8px 0"}},"客户端管理"),r("p",{style:{margin:"0",color:"#666"}},"查看已连接的隧道客户端")],-1)),l(e(g),{cols:3,"x-gap":16,"y-gap":16,style:{"margin-bottom":"24px"}},{default:a(()=>[l(e(d),null,{default:a(()=>[l(e(c),null,{default:a(()=>[l(e(_),{label:"总客户端",value:n.value.length},null,8,["value"])]),_:1})]),_:1}),l(e(d),null,{default:a(()=>[l(e(c),null,{default:a(()=>[l(e(_),{label:"在线客户端",value:C.value},null,8,["value"])]),_:1})]),_:1}),l(e(d),null,{default:a(()=>[l(e(c),null,{default:a(()=>[l(e(_),{label:"总规则数",value:b.value},null,8,["value"])]),_:1})]),_:1})]),_:1}),n.value.length===0?(u(),f(e(E),{key:0,description:"暂无客户端连接"})):(u(),f(e(g),{key:1,cols:3,"x-gap":16,"y-gap":16,responsive:"screen","cols-s":"1","cols-m":"2"},{default:a(()=>[(u(!0),p(S,null,$(n.value,s=>(u(),f(e(d),{key:s.id},{default:a(()=>[l(e(c),{hoverable:"",style:{cursor:"pointer"},onClick:w=>v(s.id)},{default:a(()=>[l(e(k),{justify:"space-between",align:"center"},{default:a(()=>[r("div",null,[r("h3",D,i(s.nickname||s.id),1),s.nickname?(u(),p("p",H,i(s.id),1)):F("",!0),l(e(k),null,{default:a(()=>[l(e(N),{type:s.online?"success":"default",size:"small"},{default:a(()=>[m(i(s.online?"在线":"离线"),1)]),_:2},1032,["type"]),l(e(N),{type:"info",size:"small"},{default:a(()=>[m(i(s.rule_count)+" 条规则",1)]),_:2},1024)]),_:2},1024)]),l(e(M),{size:"small",onClick:R(w=>v(s.id),["stop"])},{default:a(()=>[...o[0]||(o[0]=[m("查看详情",-1)])]),_:1},8,["onClick"])]),_:2},1024)]),_:2},1032,["onClick"])]),_:2},1024))),128))]),_:1}))]))}});export{Y as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{d as M,r as p,o as $,c as n,a as e,b as f,F as h,e as b,w as x,f as u,v as c,g as I,h as R,u as B,i as a,t as C,n as D,_ as F}from"./index-BvqIwKwu.js";const H={class:"home"},N={class:"client-grid"},z=["onClick"],A={class:"card-header"},E={class:"client-id"},L={class:"card-info"},P={key:0,class:"empty"},S={class:"modal"},T={class:"form-group"},j={class:"form-group"},q=["onUpdate:modelValue"],G=["onUpdate:modelValue"],J=["onUpdate:modelValue"],K=["onUpdate:modelValue"],O=["onClick"],Q={class:"modal-actions"},W=M({__name:"HomeView",setup(X){const y=B(),v=p([]),d=p(!1),i=p(""),r=p([]),m=async()=>{try{const{data:t}=await I();v.value=t||[]}catch(t){console.error("Failed to load clients",t)}};$(m);const k=()=>{i.value="",r.value=[{name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080}],d.value=!0},V=()=>{r.value.push({name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080})},w=t=>{r.value.splice(t,1)},U=async()=>{if(i.value)try{await R({id:i.value,rules:r.value}),d.value=!1,m()}catch{alert("添加失败")}},g=t=>{y.push(`/client/${t}`)};return(t,l)=>(a(),n("div",H,[e("div",{class:"toolbar"},[l[3]||(l[3]=e("h2",null,"客户端列表",-1)),e("button",{class:"btn primary",onClick:k},"添加客户端")]),e("div",N,[(a(!0),n(h,null,b(v.value,o=>(a(),n("div",{key:o.id,class:"client-card",onClick:_=>g(o.id)},[e("div",A,[e("span",E,C(o.id),1),e("span",{class:D(["status",o.online?"online":"offline"])},null,2)]),e("div",L,[e("span",null,C(o.rule_count)+" 条规则",1)])],8,z))),128))]),v.value.length===0?(a(),n("div",P,"暂无客户端配置")):f("",!0),d.value?(a(),n("div",{key:1,class:"modal-overlay",onClick:l[2]||(l[2]=x(o=>d.value=!1,["self"]))},[e("div",S,[l[6]||(l[6]=e("h3",null,"添加客户端",-1)),e("div",T,[l[4]||(l[4]=e("label",null,"客户端 ID",-1)),u(e("input",{"onUpdate:modelValue":l[0]||(l[0]=o=>i.value=o),placeholder:"例如: client-a"},null,512),[[c,i.value]])]),e("div",j,[l[5]||(l[5]=e("label",null,"代理规则",-1)),(a(!0),n(h,null,b(r.value,(o,_)=>(a(),n("div",{key:_,class:"rule-row"},[u(e("input",{"onUpdate:modelValue":s=>o.name=s,placeholder:"名称"},null,8,q),[[c,o.name]]),u(e("input",{"onUpdate:modelValue":s=>o.local_ip=s,placeholder:"本地IP"},null,8,G),[[c,o.local_ip]]),u(e("input",{"onUpdate:modelValue":s=>o.local_port=s,type:"number",placeholder:"本地端口"},null,8,J),[[c,o.local_port,void 0,{number:!0}]]),u(e("input",{"onUpdate:modelValue":s=>o.remote_port=s,type:"number",placeholder:"远程端口"},null,8,K),[[c,o.remote_port,void 0,{number:!0}]]),e("button",{class:"btn-icon",onClick:s=>w(_)},"×",8,O)]))),128)),e("button",{class:"btn secondary",onClick:V},"+ 添加规则")]),e("div",Q,[e("button",{class:"btn",onClick:l[1]||(l[1]=o=>d.value=!1)},"取消"),e("button",{class:"btn primary",onClick:U},"保存")])])])):f("",!0)]))}}),Z=F(W,[["__scopeId","data-v-fd6e4f1d"]]);export{Z as default};
|
||||
@@ -1 +0,0 @@
|
||||
.toolbar[data-v-fd6e4f1d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.toolbar h2[data-v-fd6e4f1d]{font-size:18px;color:#2c3e50}.btn[data-v-fd6e4f1d]{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px}.btn.primary[data-v-fd6e4f1d]{background:#3498db;color:#fff}.btn.secondary[data-v-fd6e4f1d]{background:#95a5a6;color:#fff}.client-grid[data-v-fd6e4f1d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.client-card[data-v-fd6e4f1d]{background:#fff;border-radius:8px;padding:16px;cursor:pointer;box-shadow:0 2px 4px #0000001a;transition:transform .2s}.client-card[data-v-fd6e4f1d]:hover{transform:translateY(-2px)}.card-header[data-v-fd6e4f1d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.client-id[data-v-fd6e4f1d]{font-weight:600}.status[data-v-fd6e4f1d]{width:10px;height:10px;border-radius:50%}.status.online[data-v-fd6e4f1d]{background:#27ae60}.status.offline[data-v-fd6e4f1d]{background:#95a5a6}.card-info[data-v-fd6e4f1d]{font-size:14px;color:#666}.empty[data-v-fd6e4f1d]{text-align:center;color:#999;padding:40px}.modal-overlay[data-v-fd6e4f1d]{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center}.modal[data-v-fd6e4f1d]{background:#fff;border-radius:8px;padding:24px;width:500px;max-width:90%}.modal h3[data-v-fd6e4f1d],.form-group[data-v-fd6e4f1d]{margin-bottom:16px}.form-group label[data-v-fd6e4f1d]{display:block;margin-bottom:8px;font-weight:500}.form-group input[data-v-fd6e4f1d]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box}.rule-row[data-v-fd6e4f1d]{display:flex;gap:8px;margin-bottom:8px}.rule-row input[data-v-fd6e4f1d]{flex:1;width:auto}.btn-icon[data-v-fd6e4f1d]{background:#e74c3c;color:#fff;border:none;border-radius:4px;width:32px;cursor:pointer}.modal-actions[data-v-fd6e4f1d]{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
|
||||
1
internal/server/app/dist/assets/LoginView-DM1JApcE.js
vendored
Normal file
1
internal/server/app/dist/assets/LoginView-DM1JApcE.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{k as N,r as d,S as k,_ as s,Y as a,N as t,Z as w,U as f,a3 as V,X as h,a4 as x,j as m,$ as g,V as i}from"./vue-vendor-k28cQfDw.js";import{N as B,a as C,b,c as _,d as T,B as I,l as L,s as S}from"./index-BJ4y0MF5.js";const U={class:"login-page"},F=N({__name:"LoginView",setup(v){const p=w(),l=d(""),r=d(""),o=d(""),n=d(!1),y=async()=>{if(!l.value||!r.value){o.value="请输入用户名和密码";return}n.value=!0,o.value="";try{const{data:u}=await L(l.value,r.value);S(u.token),p.push("/")}catch(u){o.value=u.response?.data?.error||"登录失败"}finally{n.value=!1}};return(u,e)=>(f(),k("div",U,[s(t(B),{class:"login-card",bordered:!1},{header:a(()=>[...e[2]||(e[2]=[i("div",{class:"login-header"},[i("h1",{class:"logo"},"GoTunnel"),i("p",{class:"subtitle"},"安全的内网穿透工具")],-1)])]),footer:a(()=>[...e[3]||(e[3]=[i("div",{class:"login-footer"},"欢迎使用 GoTunnel",-1)])]),default:a(()=>[s(t(C),{onSubmit:V(y,["prevent"])},{default:a(()=>[s(t(b),{label:"用户名"},{default:a(()=>[s(t(_),{value:l.value,"onUpdate:value":e[0]||(e[0]=c=>l.value=c),placeholder:"请输入用户名",disabled:n.value},null,8,["value","disabled"])]),_:1}),s(t(b),{label:"密码"},{default:a(()=>[s(t(_),{value:r.value,"onUpdate:value":e[1]||(e[1]=c=>r.value=c),type:"password",placeholder:"请输入密码",disabled:n.value,"show-password-on":"click"},null,8,["value","disabled"])]),_:1}),o.value?(f(),h(t(T),{key:0,type:"error","show-icon":!0,style:{"margin-bottom":"16px"}},{default:a(()=>[m(g(o.value),1)]),_:1})):x("",!0),s(t(I),{type:"primary",block:"",loading:n.value,"attr-type":"submit"},{default:a(()=>[m(g(n.value?"登录中...":"登录"),1)]),_:1},8,["loading"])]),_:1})]),_:1})]))}}),G=(v,p)=>{const l=v.__vccOpts||v;for(const[r,o]of p)l[r]=o;return l},D=G(F,[["__scopeId","data-v-0e29b44b"]]);export{D as default};
|
||||
1
internal/server/app/dist/assets/LoginView-kzRncluE.css
vendored
Normal file
1
internal/server/app/dist/assets/LoginView-kzRncluE.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.login-page[data-v-0e29b44b]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#e8f5e9,#c8e6c9);padding:16px}.login-card[data-v-0e29b44b]{width:100%;max-width:400px;box-shadow:0 8px 24px #0000001a}.login-header[data-v-0e29b44b]{text-align:center}.logo[data-v-0e29b44b]{font-size:28px;font-weight:700;color:#18a058;margin:0 0 8px}.subtitle[data-v-0e29b44b]{color:#666;margin:0;font-size:14px}.login-footer[data-v-0e29b44b]{text-align:center;color:#999;font-size:14px}
|
||||
1
internal/server/app/dist/assets/PluginsView-CPE2IHXI.js
vendored
Normal file
1
internal/server/app/dist/assets/PluginsView-CPE2IHXI.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{k as $,r as b,c as x,o as E,S as N,_ as a,Y as l,N as e,V as u,Z as F,j as c,X as m,F as T,R as j,U as r,$ as i}from"./vue-vendor-k28cQfDw.js";import{u as A,j as d,D as G,y as M,B as U,p as h,e as w,h as p,N as f,i as _,f as D,k as g,E as L,F as O,G as R,H as q}from"./index-BJ4y0MF5.js";import{A as H}from"./ArrowBackOutline-QaNKMlLc.js";const I={class:"plugins-view"},W={style:{margin:"0",color:"#666"}},Q=$({__name:"PluginsView",setup(X){const k=F(),v=A(),o=b([]),y=b(!0),P=async()=>{try{const{data:t}=await M();o.value=t||[]}catch(t){console.error("Failed to load plugins",t)}finally{y.value=!1}},z=x(()=>o.value.filter(t=>t.type==="proxy")),B=x(()=>o.value.filter(t=>t.type==="app")),S=async t=>{try{t.enabled?(await R(t.name),v.success(`已禁用 ${t.name}`)):(await q(t.name),v.success(`已启用 ${t.name}`)),t.enabled=!t.enabled}catch{v.error("操作失败")}},C=t=>({proxy:"协议",app:"应用",service:"服务",tool:"工具"})[t]||t,V=t=>({proxy:"info",app:"success",service:"warning",tool:"default"})[t]||"default";return E(P),(t,n)=>(r(),N("div",I,[a(e(d),{justify:"space-between",align:"center",style:{"margin-bottom":"24px"}},{default:l(()=>[n[2]||(n[2]=u("div",null,[u("h2",{style:{margin:"0 0 8px 0"}},"插件管理"),u("p",{style:{margin:"0",color:"#666"}},"查看和管理已注册的插件")],-1)),a(e(U),{quaternary:"",onClick:n[0]||(n[0]=s=>e(k).push("/"))},{icon:l(()=>[a(e(h),null,{default:l(()=>[a(e(H))]),_:1})]),default:l(()=>[n[1]||(n[1]=c(" 返回首页 ",-1))]),_:1})]),_:1}),a(e(G),{show:y.value},{default:l(()=>[a(e(w),{cols:3,"x-gap":16,"y-gap":16,style:{"margin-bottom":"24px"}},{default:l(()=>[a(e(p),null,{default:l(()=>[a(e(f),null,{default:l(()=>[a(e(_),{label:"总插件数",value:o.value.length},null,8,["value"])]),_:1})]),_:1}),a(e(p),null,{default:l(()=>[a(e(f),null,{default:l(()=>[a(e(_),{label:"协议插件",value:z.value.length},null,8,["value"])]),_:1})]),_:1}),a(e(p),null,{default:l(()=>[a(e(f),null,{default:l(()=>[a(e(_),{label:"应用插件",value:B.value.length},null,8,["value"])]),_:1})]),_:1})]),_:1}),!y.value&&o.value.length===0?(r(),m(e(D),{key:0,description:"暂无插件"})):(r(),m(e(w),{key:1,cols:3,"x-gap":16,"y-gap":16,responsive:"screen","cols-s":"1","cols-m":"2"},{default:l(()=>[(r(!0),N(T,null,j(o.value,s=>(r(),m(e(p),{key:s.name},{default:l(()=>[a(e(f),{hoverable:""},{header:l(()=>[a(e(d),{align:"center"},{default:l(()=>[a(e(h),{size:"24",color:"#18a058"},{default:l(()=>[a(e(O))]),_:1}),u("span",null,i(s.name),1)]),_:2},1024)]),"header-extra":l(()=>[a(e(L),{value:s.enabled,"onUpdate:value":Y=>S(s)},null,8,["value","onUpdate:value"])]),default:l(()=>[a(e(d),{vertical:"",size:8},{default:l(()=>[a(e(d),null,{default:l(()=>[a(e(g),{size:"small"},{default:l(()=>[c("v"+i(s.version),1)]),_:2},1024),a(e(g),{size:"small",type:V(s.type)},{default:l(()=>[c(i(C(s.type)),1)]),_:2},1032,["type"]),a(e(g),{size:"small",type:s.source==="builtin"?"default":"warning"},{default:l(()=>[c(i(s.source==="builtin"?"内置":"WASM"),1)]),_:2},1032,["type"])]),_:2},1024),u("p",W,i(s.description),1)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}))]),_:1},8,["show"])]))}});export{Q as default};
|
||||
7343
internal/server/app/dist/assets/index-BJ4y0MF5.js
vendored
Normal file
7343
internal/server/app/dist/assets/index-BJ4y0MF5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
internal/server/app/dist/assets/index-cn54chxY.css
vendored
Normal file
1
internal/server/app/dist/assets/index-cn54chxY.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{min-height:100vh}
|
||||
@@ -1 +0,0 @@
|
||||
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.card{padding:2em}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.app[data-v-dc56de06]{min-height:100vh;background:#f5f7fa}.header[data-v-dc56de06]{background:#fff;padding:16px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 4px #0000001a}.header h1[data-v-dc56de06]{font-size:20px;color:#2c3e50}.server-info[data-v-dc56de06]{display:flex;align-items:center;gap:12px;color:#666}.badge[data-v-dc56de06]{background:#3498db;color:#fff;padding:4px 12px;border-radius:12px;font-size:12px}.main[data-v-dc56de06]{padding:24px;max-width:1200px;margin:0 auto}
|
||||
1
internal/server/app/dist/assets/vue-vendor-k28cQfDw.js
vendored
Normal file
1
internal/server/app/dist/assets/vue-vendor-k28cQfDw.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
internal/server/app/dist/index.html
vendored
5
internal/server/app/dist/index.html
vendored
@@ -5,8 +5,9 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webui</title>
|
||||
<script type="module" crossorigin src="/assets/index-BvqIwKwu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-fTDfeMRP.css">
|
||||
<script type="module" crossorigin src="/assets/index-BJ4y0MF5.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-k28cQfDw.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-cn54chxY.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -4,33 +4,50 @@ import "github.com/gotunnel/pkg/protocol"
|
||||
|
||||
// Client 客户端数据
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Rules []protocol.ProxyRule `json:"rules"`
|
||||
ID string `json:"id"`
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
Rules []protocol.ProxyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// PluginData 插件数据
|
||||
type PluginData struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Checksum string `json:"checksum"`
|
||||
Size int64 `json:"size"`
|
||||
Enabled bool `json:"enabled"`
|
||||
WASMData []byte `json:"-"`
|
||||
}
|
||||
|
||||
// ClientStore 客户端存储接口
|
||||
type ClientStore interface {
|
||||
// GetAllClients 获取所有客户端
|
||||
GetAllClients() ([]Client, error)
|
||||
|
||||
// GetClient 获取单个客户端
|
||||
GetClient(id string) (*Client, error)
|
||||
|
||||
// CreateClient 创建客户端
|
||||
CreateClient(c *Client) error
|
||||
|
||||
// UpdateClient 更新客户端
|
||||
UpdateClient(c *Client) error
|
||||
|
||||
// DeleteClient 删除客户端
|
||||
DeleteClient(id string) error
|
||||
|
||||
// ClientExists 检查客户端是否存在
|
||||
ClientExists(id string) (bool, error)
|
||||
|
||||
// GetClientRules 获取客户端规则
|
||||
GetClientRules(id string) ([]protocol.ProxyRule, error)
|
||||
|
||||
// Close 关闭连接
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PluginStore 插件存储接口
|
||||
type PluginStore interface {
|
||||
GetAllPlugins() ([]PluginData, error)
|
||||
GetPlugin(name string) (*PluginData, error)
|
||||
SavePlugin(p *PluginData) error
|
||||
DeletePlugin(name string) error
|
||||
SetPluginEnabled(name string, enabled bool) error
|
||||
GetPluginWASM(name string) ([]byte, error)
|
||||
}
|
||||
|
||||
// Store 统一存储接口
|
||||
type Store interface {
|
||||
ClientStore
|
||||
PluginStore
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -34,11 +34,35 @@ func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
|
||||
|
||||
// init 初始化数据库表
|
||||
func (s *SQLiteStore) init() error {
|
||||
// 创建客户端表
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
nickname TEXT NOT NULL DEFAULT '',
|
||||
rules TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移:添加 nickname 列
|
||||
s.db.Exec(`ALTER TABLE clients ADD COLUMN nickname TEXT NOT NULL DEFAULT ''`)
|
||||
|
||||
// 创建插件表
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
name TEXT PRIMARY KEY,
|
||||
version TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'proxy',
|
||||
source TEXT NOT NULL DEFAULT 'wasm',
|
||||
description TEXT,
|
||||
author TEXT,
|
||||
checksum TEXT,
|
||||
size INTEGER DEFAULT 0,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
wasm_data BLOB
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +77,7 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rows, err := s.db.Query(`SELECT id, rules FROM clients`)
|
||||
rows, err := s.db.Query(`SELECT id, nickname, rules FROM clients`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -63,7 +87,7 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
|
||||
for rows.Next() {
|
||||
var c Client
|
||||
var rulesJSON string
|
||||
if err := rows.Scan(&c.ID, &rulesJSON); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
|
||||
@@ -81,7 +105,7 @@ func (s *SQLiteStore) GetClient(id string) (*Client, error) {
|
||||
|
||||
var c Client
|
||||
var rulesJSON string
|
||||
err := s.db.QueryRow(`SELECT id, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &rulesJSON)
|
||||
err := s.db.QueryRow(`SELECT id, nickname, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -100,7 +124,7 @@ func (s *SQLiteStore) CreateClient(c *Client) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(`INSERT INTO clients (id, rules) VALUES (?, ?)`, c.ID, string(rulesJSON))
|
||||
_, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules) VALUES (?, ?, ?)`, c.ID, c.Nickname, string(rulesJSON))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -113,7 +137,7 @@ func (s *SQLiteStore) UpdateClient(c *Client) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE clients SET rules = ? WHERE id = ?`, string(rulesJSON), c.ID)
|
||||
_, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ? WHERE id = ?`, c.Nickname, string(rulesJSON), c.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -144,3 +168,100 @@ func (s *SQLiteStore) GetClientRules(id string) ([]protocol.ProxyRule, error) {
|
||||
}
|
||||
return c.Rules, nil
|
||||
}
|
||||
|
||||
// ========== 插件存储方法 ==========
|
||||
|
||||
// GetAllPlugins 获取所有插件
|
||||
func (s *SQLiteStore) GetAllPlugins() ([]PluginData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rows, err := s.db.Query(`
|
||||
SELECT name, version, type, source, description, author, checksum, size, enabled
|
||||
FROM plugins
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var plugins []PluginData
|
||||
for rows.Next() {
|
||||
var p PluginData
|
||||
var enabled int
|
||||
err := rows.Scan(&p.Name, &p.Version, &p.Type, &p.Source,
|
||||
&p.Description, &p.Author, &p.Checksum, &p.Size, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Enabled = enabled == 1
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// GetPlugin 获取单个插件
|
||||
func (s *SQLiteStore) GetPlugin(name string) (*PluginData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var p PluginData
|
||||
var enabled int
|
||||
err := s.db.QueryRow(`
|
||||
SELECT name, version, type, source, description, author, checksum, size, enabled
|
||||
FROM plugins WHERE name = ?
|
||||
`, name).Scan(&p.Name, &p.Version, &p.Type, &p.Source,
|
||||
&p.Description, &p.Author, &p.Checksum, &p.Size, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Enabled = enabled == 1
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// SavePlugin 保存插件
|
||||
func (s *SQLiteStore) SavePlugin(p *PluginData) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
enabled := 0
|
||||
if p.Enabled {
|
||||
enabled = 1
|
||||
}
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR REPLACE INTO plugins
|
||||
(name, version, type, source, description, author, checksum, size, enabled, wasm_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, p.Name, p.Version, p.Type, p.Source, p.Description, p.Author,
|
||||
p.Checksum, p.Size, enabled, p.WASMData)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePlugin 删除插件
|
||||
func (s *SQLiteStore) DeletePlugin(name string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, err := s.db.Exec(`DELETE FROM plugins WHERE name = ?`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPluginEnabled 设置插件启用状态
|
||||
func (s *SQLiteStore) SetPluginEnabled(name string, enabled bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
val := 0
|
||||
if enabled {
|
||||
val = 1
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE plugins SET enabled = ? WHERE name = ?`, val, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPluginWASM 获取插件 WASM 数据
|
||||
func (s *SQLiteStore) GetPluginWASM(name string) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var data []byte
|
||||
err := s.db.QueryRow(`SELECT wasm_data FROM plugins WHERE name = ?`, name).Scan(&data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -2,28 +2,26 @@ package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/gotunnel/internal/server/db"
|
||||
"github.com/gotunnel/pkg/plugin"
|
||||
"github.com/gotunnel/pkg/plugin/builtin"
|
||||
"github.com/gotunnel/pkg/plugin/store"
|
||||
"github.com/gotunnel/pkg/plugin/wasm"
|
||||
)
|
||||
|
||||
// Manager 服务端 plugin 管理器
|
||||
type Manager struct {
|
||||
registry *plugin.Registry
|
||||
store store.PluginStore
|
||||
store db.PluginStore
|
||||
runtime *wasm.Runtime
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewManager 创建 plugin 管理器
|
||||
func NewManager(pluginStore store.PluginStore) (*Manager, error) {
|
||||
func NewManager(pluginStore db.PluginStore) (*Manager, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
runtime, err := wasm.NewRuntime(ctx)
|
||||
@@ -51,12 +49,11 @@ func NewManager(pluginStore store.PluginStore) (*Manager, error) {
|
||||
// 注意: tcp, udp, http, https 是内置类型,直接在 tunnel 中处理
|
||||
// 这里只注册需要通过 plugin 系统提供的协议
|
||||
func (m *Manager) registerBuiltins() error {
|
||||
// 注册 SOCKS5 plugin
|
||||
if err := m.registry.RegisterBuiltin(builtin.NewSOCKS5Plugin()); err != nil {
|
||||
return fmt.Errorf("register socks5: %w", err)
|
||||
// 使用统一的插件注册入口
|
||||
if err := m.registry.RegisterAll(builtin.GetAll()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("[Plugin] Builtin plugins registered: socks5")
|
||||
log.Printf("[Plugin] Registered %d builtin plugins", len(builtin.GetAll()))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,15 +68,15 @@ func (m *Manager) LoadStoredPlugins(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, meta := range plugins {
|
||||
data, err := m.store.GetPluginData(meta.Name)
|
||||
for _, p := range plugins {
|
||||
data, err := m.store.GetPluginWASM(p.Name)
|
||||
if err != nil {
|
||||
log.Printf("[Plugin] Failed to load %s: %v", meta.Name, err)
|
||||
log.Printf("[Plugin] Failed to load %s: %v", p.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.loadWASMPlugin(ctx, meta.Name, data); err != nil {
|
||||
log.Printf("[Plugin] Failed to init %s: %v", meta.Name, err)
|
||||
if err := m.loadWASMPlugin(ctx, p.Name, data); err != nil {
|
||||
log.Printf("[Plugin] Failed to init %s: %v", p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,28 +94,19 @@ func (m *Manager) loadWASMPlugin(ctx context.Context, name string, data []byte)
|
||||
}
|
||||
|
||||
// InstallPlugin 安装新的 WASM plugin
|
||||
func (m *Manager) InstallPlugin(ctx context.Context, meta plugin.PluginMetadata, wasmData []byte) error {
|
||||
func (m *Manager) InstallPlugin(ctx context.Context, p *db.PluginData) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// 验证 checksum
|
||||
hash := sha256.Sum256(wasmData)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
if meta.Checksum != "" && meta.Checksum != checksum {
|
||||
return fmt.Errorf("checksum mismatch")
|
||||
}
|
||||
meta.Checksum = checksum
|
||||
meta.Size = int64(len(wasmData))
|
||||
|
||||
// 存储到数据库
|
||||
if m.store != nil {
|
||||
if err := m.store.SavePlugin(meta, wasmData); err != nil {
|
||||
if err := m.store.SavePlugin(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 加载到运行时
|
||||
return m.loadWASMPlugin(ctx, meta.Name, wasmData)
|
||||
return m.loadWASMPlugin(ctx, p.Name, p.WASMData)
|
||||
}
|
||||
|
||||
// GetHandler 返回指定代理类型的 handler
|
||||
|
||||
@@ -21,6 +21,7 @@ func validateClientID(id string) bool {
|
||||
// ClientStatus 客户端状态
|
||||
type ClientStatus struct {
|
||||
ID string `json:"id"`
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
Online bool `json:"online"`
|
||||
LastPing string `json:"last_ping,omitempty"`
|
||||
RuleCount int `json:"rule_count"`
|
||||
@@ -36,6 +37,23 @@ type ServerInterface interface {
|
||||
ReloadConfig() error
|
||||
GetBindAddr() string
|
||||
GetBindPort() int
|
||||
// 客户端控制
|
||||
PushConfigToClient(clientID string) error
|
||||
DisconnectClient(clientID string) error
|
||||
GetPluginList() []PluginInfo
|
||||
EnablePlugin(name string) error
|
||||
DisablePlugin(name string) error
|
||||
InstallPluginsToClient(clientID string, plugins []string) error
|
||||
}
|
||||
|
||||
// PluginInfo 插件信息
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// AppInterface 应用接口
|
||||
@@ -68,6 +86,8 @@ func RegisterRoutes(r *Router, app AppInterface) {
|
||||
api.HandleFunc("/client/", h.handleClient)
|
||||
api.HandleFunc("/config", h.handleConfig)
|
||||
api.HandleFunc("/config/reload", h.handleReload)
|
||||
api.HandleFunc("/plugins", h.handlePlugins)
|
||||
api.HandleFunc("/plugin/", h.handlePlugin)
|
||||
}
|
||||
|
||||
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -108,7 +128,7 @@ func (h *APIHandler) getClients(rw http.ResponseWriter) {
|
||||
statusMap := h.server.GetAllClientStatus()
|
||||
var result []ClientStatus
|
||||
for _, c := range clients {
|
||||
cs := ClientStatus{ID: c.ID, RuleCount: len(c.Rules)}
|
||||
cs := ClientStatus{ID: c.ID, Nickname: c.Nickname, RuleCount: len(c.Rules)}
|
||||
if s, ok := statusMap[c.ID]; ok {
|
||||
cs.Online = s.Online
|
||||
cs.LastPing = s.LastPing
|
||||
@@ -156,6 +176,32 @@ func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "client id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理子路径操作
|
||||
if idx := len(clientID) - 1; idx > 0 {
|
||||
if clientID[idx] == '/' {
|
||||
clientID = clientID[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是特殊操作
|
||||
parts := splitPath(clientID)
|
||||
if len(parts) == 2 {
|
||||
clientID = parts[0]
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "push":
|
||||
h.pushConfigToClient(rw, r, clientID)
|
||||
return
|
||||
case "disconnect":
|
||||
h.disconnectClient(rw, r, clientID)
|
||||
return
|
||||
case "install-plugins":
|
||||
h.installPluginsToClient(rw, r, clientID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.getClient(rw, clientID)
|
||||
@@ -168,6 +214,16 @@ func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// splitPath 分割路径
|
||||
func splitPath(path string) []string {
|
||||
for i, c := range path {
|
||||
if c == '/' {
|
||||
return []string{path[:i], path[i+1:]}
|
||||
}
|
||||
}
|
||||
return []string{path}
|
||||
}
|
||||
|
||||
func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) {
|
||||
client, err := h.clientStore.GetClient(clientID)
|
||||
if err != nil {
|
||||
@@ -176,27 +232,29 @@ func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) {
|
||||
}
|
||||
online, lastPing := h.server.GetClientStatus(clientID)
|
||||
h.jsonResponse(rw, map[string]interface{}{
|
||||
"id": client.ID, "rules": client.Rules,
|
||||
"id": client.ID, "nickname": client.Nickname, "rules": client.Rules,
|
||||
"online": online, "last_ping": lastPing,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clientID string) {
|
||||
var req struct {
|
||||
Rules []protocol.ProxyRule `json:"rules"`
|
||||
Nickname string `json:"nickname"`
|
||||
Rules []protocol.ProxyRule `json:"rules"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
exists, _ := h.clientStore.ClientExists(clientID)
|
||||
if !exists {
|
||||
client, err := h.clientStore.GetClient(clientID)
|
||||
if err != nil {
|
||||
http.Error(rw, "client not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
client := &db.Client{ID: clientID, Rules: req.Rules}
|
||||
client.Nickname = req.Nickname
|
||||
client.Rules = req.Rules
|
||||
if err := h.clientStore.UpdateClient(client); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -333,3 +391,130 @@ func (h *APIHandler) jsonResponse(rw http.ResponseWriter, data interface{}) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(data)
|
||||
}
|
||||
|
||||
// pushConfigToClient 推送配置到客户端
|
||||
func (h *APIHandler) pushConfigToClient(rw http.ResponseWriter, r *http.Request, clientID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
online, _ := h.server.GetClientStatus(clientID)
|
||||
if !online {
|
||||
http.Error(rw, "client not online", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.server.PushConfigToClient(clientID); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// disconnectClient 断开客户端连接
|
||||
func (h *APIHandler) disconnectClient(rw http.ResponseWriter, r *http.Request, clientID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.server.DisconnectClient(clientID); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// handlePlugins 处理插件列表
|
||||
func (h *APIHandler) handlePlugins(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
plugins := h.server.GetPluginList()
|
||||
h.jsonResponse(rw, plugins)
|
||||
}
|
||||
|
||||
// handlePlugin 处理单个插件操作
|
||||
func (h *APIHandler) handlePlugin(rw http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[len("/api/plugin/"):]
|
||||
if path == "" {
|
||||
http.Error(rw, "plugin name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
parts := splitPath(path)
|
||||
pluginName := parts[0]
|
||||
|
||||
if len(parts) == 2 {
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "enable":
|
||||
h.enablePlugin(rw, r, pluginName)
|
||||
return
|
||||
case "disable":
|
||||
h.disablePlugin(rw, r, pluginName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(rw, "invalid action", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (h *APIHandler) enablePlugin(rw http.ResponseWriter, r *http.Request, name string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := h.server.EnablePlugin(name); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *APIHandler) disablePlugin(rw http.ResponseWriter, r *http.Request, name string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := h.server.DisablePlugin(name); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// installPluginsToClient 安装插件到客户端
|
||||
func (h *APIHandler) installPluginsToClient(rw http.ResponseWriter, r *http.Request, clientID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
online, _ := h.server.GetClientStatus(clientID)
|
||||
if !online {
|
||||
http.Error(rw, "client not online", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Plugins []string `json:"plugins"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Plugins) == 0 {
|
||||
http.Error(rw, "no plugins specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.server.InstallPluginsToClient(clientID, req.Plugins); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
80
internal/server/router/auth.go
Normal file
80
internal/server/router/auth.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gotunnel/pkg/auth"
|
||||
)
|
||||
|
||||
// AuthHandler 认证处理器
|
||||
type AuthHandler struct {
|
||||
username string
|
||||
password string
|
||||
jwtAuth *auth.JWTAuth
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建认证处理器
|
||||
func NewAuthHandler(username, password string, jwtAuth *auth.JWTAuth) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
username: username,
|
||||
password: password,
|
||||
jwtAuth: jwtAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAuthRoutes 注册认证路由
|
||||
func RegisterAuthRoutes(r *Router, h *AuthHandler) {
|
||||
r.HandleFunc("/api/auth/login", h.handleLogin)
|
||||
r.HandleFunc("/api/auth/check", h.handleCheck)
|
||||
}
|
||||
|
||||
// handleLogin 处理登录请求
|
||||
func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名密码
|
||||
userMatch := subtle.ConstantTimeCompare([]byte(req.Username), []byte(h.username)) == 1
|
||||
passMatch := subtle.ConstantTimeCompare([]byte(req.Password), []byte(h.password)) == 1
|
||||
|
||||
if !userMatch || !passMatch {
|
||||
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 token
|
||||
token, err := h.jwtAuth.GenerateToken(req.Username)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCheck 检查 token 是否有效
|
||||
func (h *AuthHandler) handleCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"valid": true})
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package router
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gotunnel/pkg/auth"
|
||||
)
|
||||
|
||||
// Router 路由管理器
|
||||
@@ -84,3 +87,37 @@ func BasicAuthMiddleware(auth *AuthConfig, next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// JWTMiddleware JWT 认证中间件
|
||||
func JWTMiddleware(jwtAuth *auth.JWTAuth, skipPaths []string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 只对 /api/ 路径进行认证
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否跳过认证
|
||||
for _, path := range skipPaths {
|
||||
if strings.HasPrefix(r.URL.Path, path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 从 Header 获取 token
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if _, err := jwtAuth.ValidateToken(token); err != nil {
|
||||
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gotunnel/internal/server/db"
|
||||
"github.com/gotunnel/internal/server/router"
|
||||
"github.com/gotunnel/pkg/plugin"
|
||||
"github.com/gotunnel/pkg/protocol"
|
||||
"github.com/gotunnel/pkg/proxy"
|
||||
@@ -19,11 +22,18 @@ import (
|
||||
|
||||
// 服务端常量
|
||||
const (
|
||||
authTimeout = 10 * time.Second
|
||||
heartbeatTimeout = 10 * time.Second
|
||||
udpBufferSize = 65535
|
||||
authTimeout = 10 * time.Second
|
||||
heartbeatTimeout = 10 * time.Second
|
||||
udpBufferSize = 65535
|
||||
)
|
||||
|
||||
// generateClientID 生成随机客户端 ID
|
||||
func generateClientID() string {
|
||||
bytes := make([]byte, 8)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// Server 隧道服务端
|
||||
type Server struct {
|
||||
clientStore db.ClientStore
|
||||
@@ -130,24 +140,44 @@ func (s *Server) handleConnection(conn net.Conn) {
|
||||
}
|
||||
|
||||
if authReq.Token != s.token {
|
||||
s.sendAuthResponse(conn, false, "invalid token")
|
||||
s.sendAuthResponse(conn, false, "invalid token", "")
|
||||
return
|
||||
}
|
||||
|
||||
rules, err := s.clientStore.GetClientRules(authReq.ClientID)
|
||||
if err != nil || rules == nil {
|
||||
s.sendAuthResponse(conn, false, "client not configured")
|
||||
// 如果客户端没有提供 ID,则生成一个新的
|
||||
clientID := authReq.ClientID
|
||||
if clientID == "" {
|
||||
clientID = generateClientID()
|
||||
// 创建新客户端记录
|
||||
newClient := &db.Client{ID: clientID, Rules: []protocol.ProxyRule{}}
|
||||
if err := s.clientStore.CreateClient(newClient); err != nil {
|
||||
log.Printf("[Server] Create client error: %v", err)
|
||||
s.sendAuthResponse(conn, false, "failed to create client", "")
|
||||
return
|
||||
}
|
||||
log.Printf("[Server] New client registered: %s", clientID)
|
||||
}
|
||||
|
||||
// 检查客户端是否存在
|
||||
exists, err := s.clientStore.ClientExists(clientID)
|
||||
if err != nil || !exists {
|
||||
s.sendAuthResponse(conn, false, "client not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
rules, _ := s.clientStore.GetClientRules(clientID)
|
||||
if rules == nil {
|
||||
rules = []protocol.ProxyRule{}
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
if err := s.sendAuthResponse(conn, true, "ok"); err != nil {
|
||||
if err := s.sendAuthResponse(conn, true, "ok", clientID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Server] Client %s authenticated", authReq.ClientID)
|
||||
s.setupClientSession(conn, authReq.ClientID, rules)
|
||||
log.Printf("[Server] Client %s authenticated", clientID)
|
||||
s.setupClientSession(conn, clientID, rules)
|
||||
}
|
||||
|
||||
// setupClientSession 建立客户端会话
|
||||
@@ -183,8 +213,8 @@ func (s *Server) setupClientSession(conn net.Conn, clientID string, rules []prot
|
||||
}
|
||||
|
||||
// sendAuthResponse 发送认证响应
|
||||
func (s *Server) sendAuthResponse(conn net.Conn, success bool, message string) error {
|
||||
resp := protocol.AuthResponse{Success: success, Message: message}
|
||||
func (s *Server) sendAuthResponse(conn net.Conn, success bool, message, clientID string) error {
|
||||
resp := protocol.AuthResponse{Success: success, Message: message, ClientID: clientID}
|
||||
msg, err := protocol.NewMessage(protocol.MsgTypeAuthResp, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -458,6 +488,103 @@ func (s *Server) GetBindPort() int {
|
||||
return s.bindPort
|
||||
}
|
||||
|
||||
// PushConfigToClient 推送配置到客户端
|
||||
func (s *Server) PushConfigToClient(clientID string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found", clientID)
|
||||
}
|
||||
|
||||
rules, err := s.clientStore.GetClientRules(clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.sendProxyConfig(cs.Session, rules)
|
||||
}
|
||||
|
||||
// DisconnectClient 断开客户端连接
|
||||
func (s *Server) DisconnectClient(clientID string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found", clientID)
|
||||
}
|
||||
|
||||
return cs.Session.Close()
|
||||
}
|
||||
|
||||
// GetPluginList 获取插件列表
|
||||
func (s *Server) GetPluginList() []router.PluginInfo {
|
||||
var result []router.PluginInfo
|
||||
|
||||
if s.pluginRegistry == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, info := range s.pluginRegistry.List() {
|
||||
result = append(result, router.PluginInfo{
|
||||
Name: info.Metadata.Name,
|
||||
Version: info.Metadata.Version,
|
||||
Type: string(info.Metadata.Type),
|
||||
Description: info.Metadata.Description,
|
||||
Source: string(info.Metadata.Source),
|
||||
Enabled: info.Enabled,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EnablePlugin 启用插件
|
||||
func (s *Server) EnablePlugin(name string) error {
|
||||
if s.pluginRegistry == nil {
|
||||
return fmt.Errorf("plugin registry not initialized")
|
||||
}
|
||||
return s.pluginRegistry.Enable(name)
|
||||
}
|
||||
|
||||
// DisablePlugin 禁用插件
|
||||
func (s *Server) DisablePlugin(name string) error {
|
||||
if s.pluginRegistry == nil {
|
||||
return fmt.Errorf("plugin registry not initialized")
|
||||
}
|
||||
return s.pluginRegistry.Disable(name)
|
||||
}
|
||||
|
||||
// InstallPluginsToClient 安装插件到客户端
|
||||
func (s *Server) InstallPluginsToClient(clientID string, plugins []string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found", clientID)
|
||||
}
|
||||
|
||||
return s.sendInstallPlugins(cs.Session, plugins)
|
||||
}
|
||||
|
||||
// sendInstallPlugins 发送安装插件请求
|
||||
func (s *Server) sendInstallPlugins(session *yamux.Session, plugins []string) error {
|
||||
stream, err := session.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
req := protocol.InstallPluginsRequest{Plugins: plugins}
|
||||
msg, err := protocol.NewMessage(protocol.MsgTypeInstallPlugins, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return protocol.WriteMessage(stream, msg)
|
||||
}
|
||||
|
||||
// startUDPListener 启动 UDP 监听
|
||||
func (s *Server) startUDPListener(cs *ClientSession, rule protocol.ProxyRule) {
|
||||
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user