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 }