This commit is contained in:
CN-JS-HuiBai
2026-04-15 12:22:08 +08:00
parent d36e8f5b39
commit b6a685722a
2 changed files with 108 additions and 47 deletions

View File

@@ -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 = "<empty>"
}
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"`

View File

@@ -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)
}
}
}