package handler import ( "encoding/json" "fmt" "net/http" "strings" "time" "go-backend/internal/http/client" "go-backend/internal/http/response" "go-backend/internal/store/sqlite" ) type federationTunnelRequest struct { Protocol string `json:"protocol"` RemotePort int `json:"remotePort"` Target string `json:"target"` } type createPeerShareRequest struct { Name string `json:"name"` NodeID int64 `json:"nodeId"` MaxBandwidth int64 `json:"maxBandwidth"` ExpiryTime int64 `json:"expiryTime"` PortRangeStart int `json:"portRangeStart"` PortRangeEnd int `json:"portRangeEnd"` AllowedDomains string `json:"allowedDomains"` } type deletePeerShareRequest struct { ID int64 `json:"id"` } type nodeImportRequest struct { RemoteURL string `json:"remoteUrl"` Token string `json:"token"` } func (h *Handler) federationShareList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } shares, err := h.repo.ListPeerShares() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(shares)) } func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } var req createPeerShareRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("Invalid JSON")) return } if req.Name == "" || req.NodeID == 0 { response.WriteJSON(w, response.ErrDefault("Name and NodeID are required")) return } if req.MaxBandwidth < 0 { response.WriteJSON(w, response.ErrDefault("Max bandwidth cannot be negative")) return } if req.ExpiryTime < 0 { response.WriteJSON(w, response.ErrDefault("Expiry time cannot be negative")) return } if req.PortRangeStart < 0 || req.PortRangeStart > 65535 || req.PortRangeEnd < 0 || req.PortRangeEnd > 65535 { response.WriteJSON(w, response.ErrDefault("Invalid port range")) return } if req.PortRangeStart > req.PortRangeEnd { response.WriteJSON(w, response.ErrDefault("Port range start cannot be greater than end")) return } node, err := h.repo.GetNodeByID(req.NodeID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if node == nil { response.WriteJSON(w, response.ErrDefault("Node not found")) return } now := time.Now().UnixMilli() token := randomToken(32) share := &sqlite.PeerShare{ Name: req.Name, NodeID: req.NodeID, Token: token, MaxBandwidth: req.MaxBandwidth, ExpiryTime: req.ExpiryTime, PortRangeStart: req.PortRangeStart, PortRangeEnd: req.PortRangeEnd, IsActive: 1, CreatedTime: now, UpdatedTime: now, AllowedDomains: req.AllowedDomains, } if err := h.repo.CreatePeerShare(share); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) federationShareDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } var req deletePeerShareRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("Invalid JSON")) return } if err := h.repo.DeletePeerShare(req.ID); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) nodeImport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } var req nodeImportRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("Invalid JSON")) return } if req.RemoteURL == "" || req.Token == "" { response.WriteJSON(w, response.ErrDefault("Remote URL and Token are required")) return } domainCfg, _ := h.repo.GetConfigByName("panel_domain") localDomain := "" if domainCfg != nil { localDomain = domainCfg.Value } fc := client.NewFederationClient() info, err := fc.Connect(req.RemoteURL, req.Token, localDomain) if err != nil { response.WriteJSON(w, response.Err(-2, "Failed to connect: "+err.Error())) return } // Prepare config json for local storage (metadata about limits) configData := map[string]interface{}{ "shareId": info.ShareID, "maxBandwidth": info.MaxBandwidth, "expiryTime": info.ExpiryTime, "portRangeStart": info.PortRangeStart, "portRangeEnd": info.PortRangeEnd, } configBytes, _ := json.Marshal(configData) db := h.repo.DB() inx := nextIndex(db, "node") now := time.Now().UnixMilli() _, err = db.Exec(` INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config) VALUES(?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?) `, fmt.Sprintf("%s (Remote)", info.NodeName), randomToken(16), // Dummy secret info.ServerIP, "", "", // v4/v6 unknown, use server_ip "0", // port range not applicable for remote "", "", now, now, info.Status, "[::]", "[::]", inx, req.RemoteURL, req.Token, string(configBytes), ) if err != nil { response.WriteJSON(w, response.Err(-2, "Database error: "+err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) authPeer(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { response.WriteJSON(w, response.Err(401, "Missing Authorization header")) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { response.WriteJSON(w, response.Err(401, "Invalid Authorization format")) return } token := parts[1] share, err := h.repo.GetPeerShareByToken(token) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if share == nil { response.WriteJSON(w, response.Err(401, "Invalid token")) return } if share.IsActive == 0 { response.WriteJSON(w, response.Err(403, "Share is disabled")) return } if share.ExpiryTime > 0 && share.ExpiryTime < time.Now().UnixMilli() { response.WriteJSON(w, response.Err(403, "Share expired")) return } if share.AllowedDomains != "" { clientDomain := r.Header.Get("X-Panel-Domain") if clientDomain == "" { response.WriteJSON(w, response.Err(403, "Domain verification required")) return } allowed := false domains := strings.Split(share.AllowedDomains, ",") for _, d := range domains { if strings.TrimSpace(d) == clientDomain { allowed = true break } } if !allowed { response.WriteJSON(w, response.Err(403, "Domain not allowed")) return } } next(w, r) } } func (h *Handler) federationConnect(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } token := extractBearerToken(r) share, err := h.repo.GetPeerShareByToken(token) if err != nil || share == nil { response.WriteJSON(w, response.Err(401, "Unauthorized")) return } var nodeName string var serverIP string var status int err = h.repo.DB().QueryRow("SELECT name, server_ip, status FROM node WHERE id = ?", share.NodeID).Scan(&nodeName, &serverIP, &status) if err != nil { response.WriteJSON(w, response.Err(-2, "Node not found")) return } response.WriteJSON(w, response.OK(map[string]interface{}{ "shareId": share.ID, "shareName": share.Name, "nodeId": share.NodeID, "nodeName": nodeName, "serverIp": serverIP, "status": status, "maxBandwidth": share.MaxBandwidth, "expiryTime": share.ExpiryTime, "portRangeStart": share.PortRangeStart, "portRangeEnd": share.PortRangeEnd, })) } func (h *Handler) federationTunnelCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) return } token := extractBearerToken(r) share, err := h.repo.GetPeerShareByToken(token) if err != nil || share == nil { response.WriteJSON(w, response.Err(401, "Unauthorized")) return } var req federationTunnelRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("Invalid JSON")) return } if req.RemotePort < share.PortRangeStart || req.RemotePort > share.PortRangeEnd { response.WriteJSON(w, response.Err(403, "Port out of range")) return } tunnelType := 1 if strings.ToLower(req.Protocol) == "udp" { tunnelType = 2 } tx, err := h.repo.DB().Begin() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } defer tx.Rollback() now := time.Now().UnixMilli() res, err := tx.Exec(`INSERT INTO tunnel (name, type, protocol, flow, created_time, updated_time, status, in_ip) VALUES (?, ?, ?, 0, ?, ?, 1, ?)`, fmt.Sprintf("Share-%d-Port-%d", share.ID, req.RemotePort), tunnelType, req.Protocol, now, now, "", ) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } tunnelID, _ := res.LastInsertId() _, err = tx.Exec(`INSERT INTO chain_tunnel (tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES (?, 1, ?, ?, 'fifo', 0, ?)`, tunnelID, share.NodeID, req.RemotePort, req.Protocol, ) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if err := tx.Commit(); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } h.wsServer.SendCommand(share.NodeID, "reload", nil, time.Second*5) response.WriteJSON(w, response.OK(map[string]interface{}{ "tunnelId": tunnelID, })) } func extractBearerToken(r *http.Request) string { authHeader := r.Header.Get("Authorization") parts := strings.Split(authHeader, " ") if len(parts) == 2 && parts[0] == "Bearer" { return parts[1] } return "" }