diff --git a/web/html/modals/warp_modal.html b/web/html/modals/warp_modal.html index 78025e31..667e4ed2 100644 --- a/web/html/modals/warp_modal.html +++ b/web/html/modals/warp_modal.html @@ -88,7 +88,6 @@ {{ i18n "pages.xray.outbound.addOutbound" }} - @@ -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(); diff --git a/web/service/warp.go b/web/service/warp.go index 6b048ea6..cee716fa 100644 --- a/web/service/warp.go +++ b/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() - if err != nil { - return "", err - } - err = json.Unmarshal([]byte(warp), &warpData) + warpData, err := s.loadWarpCreds() 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) + body, err := doWarpRequest(req) if err != nil { return "", err } - defer resp.Body.Close() - buffer := &bytes.Buffer{} - _, err = buffer.ReadFrom(resp.Body) - 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 "", 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 := 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 } - 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()) - - 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() - if err != nil { - return "", err - } - err = json.Unmarshal([]byte(warp), &warpData) + warpData, err := s.loadWarpCreds() 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) + 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 + } - 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 +}