Pārlūkot izejas kodu

fix(warp): harden API client and frontend, bump to v0a4005

Backend:
- check HTTP status on every Cloudflare API call so error bodies don't
  get parsed as success
- replace unchecked type assertions with comma-ok form (no more panics
  when Cloudflare returns an error response)
- return real errors when license/id/token fields are missing instead
  of swallowing the failure
- guard SetWarpLicense against an empty errors array
- 15s timeout on the shared http.Client
- build all request bodies and persisted state with json.Marshal
- bump API path to v0a4005 and CF-Client-Version to a-6.30-3596 to
  match the current Cloudflare WARP client

Frontend (warp_modal.html):
- remove stray </a-form-item> closing tag
- declare config/peer with const and null-check before dereferencing
- guard addOutbound/resetOutbound against missing warpOutbound
- rename getResolved -> getReserved (the array it builds is "reserved")

Co-Authored-By: Claude Opus 4.7 <[email protected]>
MHSanaei 1 dienu atpakaļ
vecāks
revīzija
d8198f543b
2 mainītis faili ar 141 papildinājumiem un 103 dzēšanām
  1. 23 21
      web/html/modals/warp_modal.html
  2. 118 82
      web/service/warp.go

+ 23 - 21
web/html/modals/warp_modal.html

@@ -88,7 +88,6 @@
                     <a-button @click="addOutbound" :loading="warpModal.confirmLoading"
                         type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
                 </template>
-                </a-form-item>
             </a-form>
         </template>
     </template>
@@ -131,26 +130,27 @@
         },
         methods: {
             collectConfig() {
-                config = warpModal.warpConfig.config;
-                peer = config.peers[0];
-                if (config) {
-                    warpModal.warpOutbound = Outbound.fromJson({
-                        tag: 'warp',
-                        protocol: Protocols.Wireguard,
-                        settings: {
-                            mtu: 1420,
-                            secretKey: warpModal.warpData.private_key,
-                            address: this.getAddresses(config.interface.addresses),
-                            reserved: this.getResolved(config.client_id),
-                            domainStrategy: 'ForceIP',
-                            peers: [{
-                                publicKey: peer.public_key,
-                                endpoint: peer.endpoint.host,
-                            }],
-                            noKernelTun: false,
-                        }
-                    });
+                const config = warpModal.warpConfig && warpModal.warpConfig.config;
+                if (!config || !config.peers || !config.peers.length) {
+                    return;
                 }
+                const peer = config.peers[0];
+                warpModal.warpOutbound = Outbound.fromJson({
+                    tag: 'warp',
+                    protocol: Protocols.Wireguard,
+                    settings: {
+                        mtu: 1420,
+                        secretKey: warpModal.warpData.private_key,
+                        address: this.getAddresses(config.interface.addresses),
+                        reserved: this.getReserved(config.client_id),
+                        domainStrategy: 'ForceIP',
+                        peers: [{
+                            publicKey: peer.public_key,
+                            endpoint: peer.endpoint.host,
+                        }],
+                        noKernelTun: false,
+                    }
+                });
             },
             getAddresses(addrs) {
                 let addresses = [];
@@ -158,7 +158,7 @@
                 if (addrs.v6) addresses.push(addrs.v6 + "/128");
                 return addresses;
             },
-            getResolved(client_id) {
+            getReserved(client_id) {
                 let reserved = [];
                 let decoded = atob(client_id);
                 let hexString = '';
@@ -218,11 +218,13 @@
                 }
             },
             addOutbound() {
+                if (!warpModal.warpOutbound) return;
                 app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
                 app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
                 warpModal.close();
             },
             resetOutbound() {
+                if (!warpModal.warpOutbound) return;
                 app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
                 app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
                 warpModal.close();

+ 118 - 82
web/service/warp.go

@@ -4,11 +4,11 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"os"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v2/logger"
 	"github.com/mhsanaei/3x-ui/v2/util/common"
 )
 
@@ -18,148 +18,147 @@ type WarpService struct {
 	SettingService
 }
 
+const (
+	warpAPIBase   = "https://api.cloudflareclient.com/v0a4005"
+	warpClientVer = "a-6.30-3596"
+)
+
+var warpHTTPClient = &http.Client{Timeout: 15 * time.Second}
+
 func (s *WarpService) GetWarpData() (string, error) {
-	warp, err := s.SettingService.GetWarp()
-	if err != nil {
-		return "", err
-	}
-	return warp, nil
+	return s.SettingService.GetWarp()
 }
 
 func (s *WarpService) DelWarpData() error {
-	err := s.SettingService.SetWarp("")
-	if err != nil {
-		return err
-	}
-	return nil
+	return s.SettingService.SetWarp("")
 }
 
 func (s *WarpService) GetWarpConfig() (string, error) {
-	var warpData map[string]string
-	warp, err := s.SettingService.GetWarp()
+	warpData, err := s.loadWarpCreds()
 	if err != nil {
 		return "", err
 	}
-	err = json.Unmarshal([]byte(warp), &warpData)
-	if err != nil {
-		return "", err
-	}
-
-	url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"])
 
-	req, err := http.NewRequest("GET", url, nil)
+	url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"])
+	req, err := http.NewRequest(http.MethodGet, url, nil)
 	if err != nil {
 		return "", err
 	}
 	req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
 
-	client := &http.Client{}
-	resp, err := client.Do(req)
-	if err != nil {
-		return "", err
-	}
-	defer resp.Body.Close()
-	buffer := &bytes.Buffer{}
-	_, err = buffer.ReadFrom(resp.Body)
+	body, err := doWarpRequest(req)
 	if err != nil {
 		return "", err
 	}
-
-	return buffer.String(), nil
+	return string(body), nil
 }
 
 func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
-	tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
 	hostName, _ := os.Hostname()
-	data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
-
-	url := "https://api.cloudflareclient.com/v0a2158/reg"
-
-	req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
+	reqBody, err := json.Marshal(map[string]any{
+		"key":   publicKey,
+		"tos":   time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
+		"type":  "PC",
+		"model": "x-ui",
+		"name":  hostName,
+	})
 	if err != nil {
 		return "", err
 	}
 
-	req.Header.Add("CF-Client-Version", "a-7.21-0721")
-	req.Header.Add("Content-Type", "application/json")
-
-	client := &http.Client{}
-	resp, err := client.Do(req)
+	req, err := http.NewRequest(http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
 	if err != nil {
 		return "", err
 	}
-	defer resp.Body.Close()
-	buffer := &bytes.Buffer{}
-	_, err = buffer.ReadFrom(resp.Body)
+	req.Header.Set("CF-Client-Version", warpClientVer)
+	req.Header.Set("Content-Type", "application/json")
+
+	body, err := doWarpRequest(req)
 	if err != nil {
 		return "", err
 	}
 
-	var rspData map[string]any
-	err = json.Unmarshal(buffer.Bytes(), &rspData)
-	if err != nil {
+	var rsp map[string]any
+	if err := json.Unmarshal(body, &rsp); err != nil {
 		return "", err
 	}
 
-	deviceId := rspData["id"].(string)
-	token := rspData["token"].(string)
-	license, ok := rspData["account"].(map[string]any)["license"].(string)
+	deviceID, ok := rsp["id"].(string)
 	if !ok {
-		logger.Debug("Error accessing license value.")
-		return "", err
+		return "", common.NewError("warp register: missing 'id' in response")
+	}
+	token, ok := rsp["token"].(string)
+	if !ok {
+		return "", common.NewError("warp register: missing 'token' in response")
+	}
+	account, ok := rsp["account"].(map[string]any)
+	if !ok {
+		return "", common.NewError("warp register: missing 'account' in response")
+	}
+	license, ok := account["license"].(string)
+	if !ok {
+		return "", common.NewError("warp register: missing 'account.license' in response")
 	}
 
-	warpData := fmt.Sprintf("{\n  \"access_token\": \"%s\",\n  \"device_id\": \"%s\",", token, deviceId)
-	warpData += fmt.Sprintf("\n  \"license_key\": \"%s\",\n  \"private_key\": \"%s\"\n}", license, secretKey)
-
-	s.SettingService.SetWarp(warpData)
-
-	result := fmt.Sprintf("{\n  \"data\": %s,\n  \"config\": %s\n}", warpData, buffer.String())
+	warpData := map[string]string{
+		"access_token": token,
+		"device_id":    deviceID,
+		"license_key":  license,
+		"private_key":  secretKey,
+	}
+	warpJSON, err := json.MarshalIndent(warpData, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	if err := s.SettingService.SetWarp(string(warpJSON)); err != nil {
+		return "", err
+	}
 
-	return result, nil
+	result, err := json.MarshalIndent(map[string]any{
+		"data":   warpData,
+		"config": json.RawMessage(body),
+	}, "", "  ")
+	if err != nil {
+		return "", err
+	}
+	return string(result), nil
 }
 
 func (s *WarpService) SetWarpLicense(license string) (string, error) {
-	var warpData map[string]string
-	warp, err := s.SettingService.GetWarp()
+	warpData, err := s.loadWarpCreds()
 	if err != nil {
 		return "", err
 	}
-	err = json.Unmarshal([]byte(warp), &warpData)
+
+	url := fmt.Sprintf("%s/reg/%s/account", warpAPIBase, warpData["device_id"])
+	reqBody, err := json.Marshal(map[string]string{"license": license})
 	if err != nil {
 		return "", err
 	}
 
-	url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
-	data := fmt.Sprintf(`{"license": "%s"}`, license)
-
-	req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data)))
+	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(reqBody))
 	if err != nil {
 		return "", err
 	}
 	req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
+	req.Header.Set("Content-Type", "application/json")
 
-	client := &http.Client{}
-	resp, err := client.Do(req)
-	if err != nil {
-		return "", err
-	}
-	defer resp.Body.Close()
-	buffer := &bytes.Buffer{}
-	_, err = buffer.ReadFrom(resp.Body)
+	body, err := doWarpRequest(req)
 	if err != nil {
 		return "", err
 	}
 
 	var response map[string]any
-	err = json.Unmarshal(buffer.Bytes(), &response)
-	if err != nil {
+	if err := json.Unmarshal(body, &response); err != nil {
 		return "", err
 	}
-	if response["success"] == false {
-		errorArr, _ := response["errors"].([]any)
-		errorObj := errorArr[0].(map[string]any)
-		return "", common.NewError(errorObj["code"], errorObj["message"])
+	if success, _ := response["success"].(bool); !success {
+		if errorArr, ok := response["errors"].([]any); ok && len(errorArr) > 0 {
+			if errorObj, ok := errorArr[0].(map[string]any); ok {
+				return "", common.NewError(errorObj["code"], errorObj["message"])
+			}
+		}
+		return "", common.NewError("warp set license failed: unknown error")
 	}
 
 	warpData["license_key"] = license
@@ -167,7 +166,44 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	s.SettingService.SetWarp(string(newWarpData))
-
+	if err := s.SettingService.SetWarp(string(newWarpData)); err != nil {
+		return "", err
+	}
 	return string(newWarpData), nil
 }
+
+// loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set.
+func (s *WarpService) loadWarpCreds() (map[string]string, error) {
+	warp, err := s.SettingService.GetWarp()
+	if err != nil {
+		return nil, err
+	}
+	var data map[string]string
+	if err := json.Unmarshal([]byte(warp), &data); err != nil {
+		return nil, err
+	}
+	if data["access_token"] == "" || data["device_id"] == "" {
+		return nil, common.NewError("warp not registered: missing access_token or device_id")
+	}
+	return data, nil
+}
+
+// doWarpRequest sends the request and returns the response body on 2xx.
+// Non-2xx responses are returned as errors including the status code and body.
+func doWarpRequest(req *http.Request) ([]byte, error) {
+	resp, err := warpHTTPClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, common.NewErrorf("warp api %s %s returned status %d: %s",
+			req.Method, req.URL.Path, resp.StatusCode, string(body))
+	}
+	return body, nil
+}