Files
Wormhole/internal/api/import_export.go
T
rose_cat707 0d84b07f68
CI / docker (push) Successful in 1m58s
feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。
Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 17:52:43 +08:00

349 lines
9.0 KiB
Go

package api
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/wormhole/wormhole/internal/auth"
"github.com/wormhole/wormhole/internal/store"
)
type importResult struct {
Created int `json:"created"`
Updated int `json:"updated"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
Errors []string `json:"errors"`
}
type tunnelImportRequest struct {
Mode string `json:"mode"` // create, upsert, skip
Items []store.TunnelExportItem `json:"items"`
}
type hostImportRequest struct {
Mode string `json:"mode"`
Items []store.HostExportItem `json:"items"`
}
func (s *Server) exportTunnels(c *gin.Context) {
claims := getClaims(c)
clientID, _ := strconv.ParseUint(c.Query("client_id"), 10, 64)
tunnels, err := s.store.ListTunnels(uint(clientID), claims.UserID, auth.IsAdmin(claims.Role))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
clientNames := tunnelClientNames(s.store, tunnels)
items := make([]store.TunnelExportItem, 0, len(tunnels))
for _, t := range tunnels {
items = append(items, store.TunnelToExportItem(t, clientNames[t.ClientID]))
}
doc := store.TunnelExportDoc{
Version: store.ExportVersion,
ExportedAt: time.Now().UTC(),
Items: items,
}
c.Header("Content-Disposition", `attachment; filename="wormhole-tunnels.json"`)
c.JSON(http.StatusOK, doc)
}
func (s *Server) importTunnels(c *gin.Context) {
claims := getClaims(c)
var req tunnelImportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if len(req.Items) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "items is empty"})
return
}
mode := req.Mode
if mode == "" {
mode = "upsert"
}
result := importResult{}
for i, item := range req.Items {
clientID, err := store.ResolveImportClientID(s.store, item.ClientID, item.ClientName, claims.UserID, auth.IsAdmin(claims.Role))
if err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
t := tunnelFromImportItem(item, clientID)
if err := normalizeTunnel(&t); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
existing, err := s.store.FindTunnelByPort(t.ListenPort)
switch mode {
case "create":
if err == nil {
result.Skipped++
continue
}
if err := s.store.CreateTunnel(&t); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
s.cache.UpsertTunnel(&t)
if t.Status {
_ = s.proxy.StartTunnel(t.ID)
}
result.Created++
case "skip":
if err == nil {
result.Skipped++
continue
}
if err := s.store.CreateTunnel(&t); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
s.cache.UpsertTunnel(&t)
if t.Status {
_ = s.proxy.StartTunnel(t.ID)
}
result.Created++
default: // upsert
if err != nil {
if err := s.store.CreateTunnel(&t); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
s.cache.UpsertTunnel(&t)
if t.Status {
_ = s.proxy.StartTunnel(t.ID)
}
result.Created++
continue
}
wasRunning := existing.RunStatus
if wasRunning {
_ = s.proxy.StopTunnel(existing.ID)
}
t.ID = existing.ID
t.ClientID = existing.ClientID
t.RunStatus = false
if err := s.store.UpdateTunnel(&t); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("tunnel", i+1, err))
continue
}
s.cache.UpsertTunnel(&t)
if t.Status {
_ = s.proxy.StartTunnel(t.ID)
}
result.Updated++
}
}
c.JSON(http.StatusOK, result)
}
func (s *Server) exportHosts(c *gin.Context) {
claims := getClaims(c)
clientID, _ := strconv.ParseUint(c.Query("client_id"), 10, 64)
hosts, err := s.store.ListHosts(uint(clientID), claims.UserID, auth.IsAdmin(claims.Role))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
clientNames := hostClientNames(s.store, hosts)
items := make([]store.HostExportItem, 0, len(hosts))
for _, h := range hosts {
items = append(items, store.HostToExportItem(h, clientNames[h.ClientID]))
}
doc := store.HostExportDoc{
Version: store.ExportVersion,
ExportedAt: time.Now().UTC(),
Items: items,
}
c.Header("Content-Disposition", `attachment; filename="wormhole-hosts.json"`)
c.JSON(http.StatusOK, doc)
}
func (s *Server) importHosts(c *gin.Context) {
claims := getClaims(c)
var req hostImportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if len(req.Items) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "items is empty"})
return
}
mode := req.Mode
if mode == "" {
mode = "upsert"
}
result := importResult{}
for i, item := range req.Items {
clientID, err := store.ResolveImportClientID(s.store, item.ClientID, item.ClientName, claims.UserID, auth.IsAdmin(claims.Role))
if err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
h := hostFromImportItem(item, clientID)
if err := normalizeHost(&h); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
existing, err := s.store.FindHostByRouteGlobal(h.Host, h.Location)
switch mode {
case "create":
if err == nil {
result.Skipped++
continue
}
if err := s.store.CreateHost(&h); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
s.cache.UpsertHost(&h)
result.Created++
case "skip":
if err == nil {
result.Skipped++
continue
}
if err := s.store.CreateHost(&h); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
s.cache.UpsertHost(&h)
result.Created++
default:
if err != nil {
if err := s.store.CreateHost(&h); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
s.cache.UpsertHost(&h)
result.Created++
continue
}
h.ID = existing.ID
h.ClientID = existing.ClientID
if err := s.store.UpdateHost(&h); err != nil {
result.Failed++
result.Errors = append(result.Errors, formatImportError("host", i+1, err))
continue
}
s.cache.UpsertHost(&h)
result.Updated++
}
}
c.JSON(http.StatusOK, result)
}
func tunnelFromImportItem(item store.TunnelExportItem, clientID uint) store.Tunnel {
return store.Tunnel{
ClientID: clientID,
Name: item.Name,
Mode: item.Mode,
ListenIP: item.ListenIP,
ListenPort: item.ListenPort,
TargetAddr: item.TargetAddr,
IPForwardMode: item.IPForwardMode,
CustomHeaders: item.CustomHeaders,
Status: item.Status,
}
}
func hostFromImportItem(item store.HostExportItem, clientID uint) store.Host {
return store.Host{
ClientID: clientID,
Host: item.Host,
Location: item.Location,
Scheme: item.Scheme,
TargetAddr: item.TargetAddr,
AutoHTTPS: item.AutoHTTPS,
IPForwardMode: item.IPForwardMode,
CustomHeaders: item.CustomHeaders,
Status: item.Status,
}
}
func normalizeTunnel(t *store.Tunnel) error {
if t.ListenIP == "" {
t.ListenIP = "0.0.0.0"
}
if t.Mode == "" {
t.Mode = "tcp"
}
if t.ListenPort <= 0 || t.TargetAddr == "" {
return errImportField("listen_port and target_addr required")
}
if t.IPForwardMode == "" {
t.IPForwardMode = "replace"
}
return nil
}
func normalizeHost(h *store.Host) error {
if h.Host == "" || h.TargetAddr == "" {
return errImportField("host and target_addr required")
}
if h.Location == "" {
h.Location = "/"
}
if h.Scheme == "" {
h.Scheme = "all"
}
if h.IPForwardMode == "" {
h.IPForwardMode = "replace"
}
return nil
}
type importFieldError string
func errImportField(msg string) error { return importFieldError(msg) }
func (e importFieldError) Error() string { return string(e) }
func formatImportError(kind string, index int, err error) string {
return kind + " #" + strconv.Itoa(index) + ": " + err.Error()
}
func tunnelClientNames(st *store.Store, tunnels []store.Tunnel) map[uint]string {
ids := make(map[uint]struct{})
for _, t := range tunnels {
ids[t.ClientID] = struct{}{}
}
names := make(map[uint]string, len(ids))
for id := range ids {
if c, err := st.GetClientByID(id); err == nil {
names[id] = c.Name
}
}
return names
}
func hostClientNames(st *store.Store, hosts []store.Host) map[uint]string {
ids := make(map[uint]struct{})
for _, h := range hosts {
ids[h.ClientID] = struct{}{}
}
names := make(map[uint]string, len(ids))
for id := range ids {
if c, err := st.GetClientByID(id); err == nil {
names[id] = c.Name
}
}
return names
}