diff --git a/service/xboard/service.go b/service/xboard/service.go index 6f1e767c..9b19d19c 100644 --- a/service/xboard/service.go +++ b/service/xboard/service.go @@ -1003,6 +1003,81 @@ func (s *Service) setupNode() error { return nil } +func normalizePanelNodeType(nodeType string) string { + switch strings.ToLower(strings.TrimSpace(nodeType)) { + case "v2ray": + return "vmess" + case "hysteria2": + return "hysteria" + default: + return strings.ToLower(strings.TrimSpace(nodeType)) + } +} + +func (s *Service) panelRequest(method string, baseURL string, endpoint string, nodeID int, payload []byte, contentType string) (http.Header, []byte, int, error) { + nodeType := normalizePanelNodeType(s.options.NodeType) + nodeTypeCandidates := []string{nodeType} + if nodeType != "" { + nodeTypeCandidates = append(nodeTypeCandidates, "") + } + + var lastHeader http.Header + var lastBody []byte + var lastStatus int + for index, candidate := range nodeTypeCandidates { + requestURL, err := url.Parse(strings.TrimRight(baseURL, "/") + endpoint) + if err != nil { + return nil, nil, 0, err + } + query := requestURL.Query() + query.Set("node_id", strconv.Itoa(nodeID)) + query.Set("token", s.options.Key) + if candidate != "" { + query.Set("node_type", candidate) + } + requestURL.RawQuery = query.Encode() + + var bodyReader io.Reader + if payload != nil { + bodyReader = bytes.NewReader(payload) + } + req, _ := http.NewRequest(method, requestURL.String(), bodyReader) + req.Header.Set("User-Agent", "sing-box/xboard") + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + logNodeType := candidate + if logNodeType == "" { + logNodeType = "" + } + s.logger.Info("Xboard panel request. endpoint=", endpoint, ", node_id=", nodeID, ", node_type=", logNodeType) + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, nil, 0, err + } + responseBody, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + return nil, nil, 0, readErr + } + + lastHeader = resp.Header.Clone() + lastBody = responseBody + lastStatus = resp.StatusCode + + if resp.StatusCode == 400 && candidate != "" && strings.Contains(string(responseBody), "Server does not exist") && index+1 < len(nodeTypeCandidates) { + s.logger.Warn("Xboard panel request failed with node_type=", candidate, ", retrying without node_type") + continue + } + + return lastHeader, lastBody, lastStatus, nil + } + + return lastHeader, lastBody, lastStatus, nil +} + func (s *Service) fetchConfig() (*XNodeConfig, error) { nodeID := s.options.ConfigNodeID if nodeID == 0 { @@ -1012,18 +1087,13 @@ func (s *Service) fetchConfig() (*XNodeConfig, error) { if baseURL == "" { baseURL = s.options.PanelURL } - url := fmt.Sprintf("%s/api/v1/server/UniProxy/config?node_id=%d&node_type=%s&token=%s", baseURL, nodeID, s.options.NodeType, s.options.Key) - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", "sing-box/xboard") - - resp, err := s.httpClient.Do(req) + headers, body, statusCode, err := s.panelRequest("GET", baseURL, "/api/v1/server/UniProxy/config", nodeID, nil, "") if err != nil { return nil, err } - defer resp.Body.Close() // Check time drift - if dateStr := resp.Header.Get("Date"); dateStr != "" { + if dateStr := headers.Get("Date"); dateStr != "" { if panelTime, err := http.ParseTime(dateStr); err == nil { localTime := time.Now() drift := localTime.Sub(panelTime) @@ -1035,13 +1105,10 @@ func (s *Service) fetchConfig() (*XNodeConfig, error) { } } - if resp.StatusCode != 200 { - respBody, _ := io.ReadAll(resp.Body) - return nil, E.New("failed to fetch config, status: ", resp.Status, ", body: ", string(respBody)) + if statusCode != 200 { + return nil, E.New("failed to fetch config, status: ", statusCode, ", body: ", string(body)) } - body, _ := io.ReadAll(resp.Body) - var result struct { Data XNodeConfig `json:"data"` } @@ -1249,22 +1316,15 @@ func (s *Service) pushTraffic(data any) error { if baseURL == "" { baseURL = s.options.PanelURL } - url := fmt.Sprintf("%s/api/v1/server/UniProxy/push?node_id=%d&node_type=%s&token=%s", baseURL, nodeID, s.options.NodeType, s.options.Key) body, _ := json.Marshal(data) - - req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "sing-box/xboard") - - resp, err := s.httpClient.Do(req) + + _, responseBody, statusCode, err := s.panelRequest("POST", baseURL, "/api/v1/server/UniProxy/push", nodeID, body, "application/json") if err != nil { return err } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - respBody, _ := io.ReadAll(resp.Body) - return E.New("failed to push traffic, status: ", resp.Status, ", body: ", string(respBody)) + + if statusCode != 200 { + return E.New("failed to push traffic, status: ", statusCode, ", body: ", string(responseBody)) } return nil } @@ -1278,21 +1338,15 @@ func (s *Service) sendAlive() { if baseURL == "" { baseURL = s.options.PanelURL } - url := fmt.Sprintf("%s/api/v1/server/UniProxy/alive?node_id=%d&node_type=%s&token=%s", baseURL, nodeID, s.options.NodeType, s.options.Key) - - req, _ := http.NewRequest("POST", url, nil) - req.Header.Set("User-Agent", "sing-box/xboard") - - resp, err := s.httpClient.Do(req) + + _, responseBody, statusCode, err := s.panelRequest("POST", baseURL, "/api/v1/server/UniProxy/alive", nodeID, nil, "") if err != nil { s.logger.Error("Xboard heartbeat error: ", err) return } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - respBody, _ := io.ReadAll(resp.Body) - s.logger.Warn("Xboard heartbeat failed, status: ", resp.Status, ", body: ", string(respBody)) + + if statusCode != 200 { + s.logger.Warn("Xboard heartbeat failed, status: ", statusCode, ", body: ", string(responseBody)) } else { s.logger.Trace("Xboard heartbeat sent") } @@ -1369,23 +1423,15 @@ func (s *Service) fetchUsers() ([]XUser, error) { if baseURL == "" { baseURL = s.options.PanelURL } - url := fmt.Sprintf("%s/api/v1/server/UniProxy/user?node_id=%d&node_type=%s&token=%s", baseURL, nodeID, s.options.NodeType, s.options.Key) - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", "sing-box/xboard") - - resp, err := s.httpClient.Do(req) + _, body, statusCode, err := s.panelRequest("GET", baseURL, "/api/v1/server/UniProxy/user", nodeID, nil, "") if err != nil { return nil, err } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - respBody, _ := io.ReadAll(resp.Body) - return nil, E.New("failed to fetch users, status: ", resp.Status, ", body: ", string(respBody)) + + if statusCode != 200 { + return nil, E.New("failed to fetch users, status: ", statusCode, ", body: ", string(body)) } - body, _ := io.ReadAll(resp.Body) - var result struct { Data []XUser `json:"data"` Users []XUser `json:"users"` diff --git a/service/xboard/service_test.go b/service/xboard/service_test.go index 3f51d183..5d805d91 100644 --- a/service/xboard/service_test.go +++ b/service/xboard/service_test.go @@ -116,3 +116,18 @@ func TestBuildInboundMultiplex(t *testing.T) { t.Fatalf("buildInboundMultiplex() brutal = %+v", got.Brutal) } } + +func TestNormalizePanelNodeType(t *testing.T) { + tests := map[string]string{ + "v2ray": "vmess", + "hysteria2": "hysteria", + "vless": "vless", + "": "", + } + + for input, want := range tests { + if got := normalizePanelNodeType(input); got != want { + t.Fatalf("normalizePanelNodeType(%q) = %q, want %q", input, got, want) + } + } +}