mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,6 @@
|
|||||||
<a-button @click="addOutbound" :loading="warpModal.confirmLoading"
|
<a-button @click="addOutbound" :loading="warpModal.confirmLoading"
|
||||||
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -131,9 +130,11 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
collectConfig() {
|
collectConfig() {
|
||||||
config = warpModal.warpConfig.config;
|
const config = warpModal.warpConfig && warpModal.warpConfig.config;
|
||||||
peer = config.peers[0];
|
if (!config || !config.peers || !config.peers.length) {
|
||||||
if (config) {
|
return;
|
||||||
|
}
|
||||||
|
const peer = config.peers[0];
|
||||||
warpModal.warpOutbound = Outbound.fromJson({
|
warpModal.warpOutbound = Outbound.fromJson({
|
||||||
tag: 'warp',
|
tag: 'warp',
|
||||||
protocol: Protocols.Wireguard,
|
protocol: Protocols.Wireguard,
|
||||||
@@ -141,7 +142,7 @@
|
|||||||
mtu: 1420,
|
mtu: 1420,
|
||||||
secretKey: warpModal.warpData.private_key,
|
secretKey: warpModal.warpData.private_key,
|
||||||
address: this.getAddresses(config.interface.addresses),
|
address: this.getAddresses(config.interface.addresses),
|
||||||
reserved: this.getResolved(config.client_id),
|
reserved: this.getReserved(config.client_id),
|
||||||
domainStrategy: 'ForceIP',
|
domainStrategy: 'ForceIP',
|
||||||
peers: [{
|
peers: [{
|
||||||
publicKey: peer.public_key,
|
publicKey: peer.public_key,
|
||||||
@@ -150,7 +151,6 @@
|
|||||||
noKernelTun: false,
|
noKernelTun: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getAddresses(addrs) {
|
getAddresses(addrs) {
|
||||||
let addresses = [];
|
let addresses = [];
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
if (addrs.v6) addresses.push(addrs.v6 + "/128");
|
if (addrs.v6) addresses.push(addrs.v6 + "/128");
|
||||||
return addresses;
|
return addresses;
|
||||||
},
|
},
|
||||||
getResolved(client_id) {
|
getReserved(client_id) {
|
||||||
let reserved = [];
|
let reserved = [];
|
||||||
let decoded = atob(client_id);
|
let decoded = atob(client_id);
|
||||||
let hexString = '';
|
let hexString = '';
|
||||||
@@ -218,11 +218,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addOutbound() {
|
addOutbound() {
|
||||||
|
if (!warpModal.warpOutbound) return;
|
||||||
app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
|
app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
|
||||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||||
warpModal.close();
|
warpModal.close();
|
||||||
},
|
},
|
||||||
resetOutbound() {
|
resetOutbound() {
|
||||||
|
if (!warpModal.warpOutbound) return;
|
||||||
app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
|
app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
|
||||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||||
warpModal.close();
|
warpModal.close();
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,156 +18,192 @@ type WarpService struct {
|
|||||||
SettingService
|
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) {
|
func (s *WarpService) GetWarpData() (string, error) {
|
||||||
warp, err := s.SettingService.GetWarp()
|
return s.SettingService.GetWarp()
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return warp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WarpService) DelWarpData() error {
|
func (s *WarpService) DelWarpData() error {
|
||||||
err := s.SettingService.SetWarp("")
|
return s.SettingService.SetWarp("")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WarpService) GetWarpConfig() (string, error) {
|
func (s *WarpService) GetWarpConfig() (string, error) {
|
||||||
var warpData map[string]string
|
warpData, err := s.loadWarpCreds()
|
||||||
warp, err := s.SettingService.GetWarp()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal([]byte(warp), &warpData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"])
|
url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"])
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
|
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
|
||||||
|
|
||||||
client := &http.Client{}
|
body, err := doWarpRequest(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
return string(body), nil
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
_, err = buffer.ReadFrom(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.String(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
|
func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
|
||||||
tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
|
|
||||||
hostName, _ := os.Hostname()
|
hostName, _ := os.Hostname()
|
||||||
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
|
reqBody, err := json.Marshal(map[string]any{
|
||||||
|
"key": publicKey,
|
||||||
url := "https://api.cloudflareclient.com/v0a2158/reg"
|
"tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||||
|
"type": "PC",
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
|
"model": "x-ui",
|
||||||
|
"name": hostName,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("CF-Client-Version", "a-7.21-0721")
|
req, err := http.NewRequest(http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
req.Header.Set("CF-Client-Version", warpClientVer)
|
||||||
buffer := &bytes.Buffer{}
|
req.Header.Set("Content-Type", "application/json")
|
||||||
_, err = buffer.ReadFrom(resp.Body)
|
|
||||||
|
body, err := doWarpRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var rspData map[string]any
|
var rsp map[string]any
|
||||||
err = json.Unmarshal(buffer.Bytes(), &rspData)
|
if err := json.Unmarshal(body, &rsp); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceId := rspData["id"].(string)
|
deviceID, ok := rsp["id"].(string)
|
||||||
token := rspData["token"].(string)
|
|
||||||
license, ok := rspData["account"].(map[string]any)["license"].(string)
|
|
||||||
if !ok {
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId)
|
result, err := json.MarshalIndent(map[string]any{
|
||||||
warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey)
|
"data": warpData,
|
||||||
|
"config": json.RawMessage(body),
|
||||||
s.SettingService.SetWarp(warpData)
|
}, "", " ")
|
||||||
|
if err != nil {
|
||||||
result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String())
|
return "", err
|
||||||
|
}
|
||||||
return result, nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WarpService) SetWarpLicense(license string) (string, error) {
|
func (s *WarpService) SetWarpLicense(license string) (string, error) {
|
||||||
var warpData map[string]string
|
warpData, err := s.loadWarpCreds()
|
||||||
warp, err := s.SettingService.GetWarp()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal([]byte(warp), &warpData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
|
url := fmt.Sprintf("%s/reg/%s/account", warpAPIBase, warpData["device_id"])
|
||||||
data := fmt.Sprintf(`{"license": "%s"}`, license)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
|
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
client := &http.Client{}
|
body, err := doWarpRequest(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
_, err = buffer.ReadFrom(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response map[string]any
|
var response map[string]any
|
||||||
err = json.Unmarshal(buffer.Bytes(), &response)
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if response["success"] == false {
|
if success, _ := response["success"].(bool); !success {
|
||||||
errorArr, _ := response["errors"].([]any)
|
if errorArr, ok := response["errors"].([]any); ok && len(errorArr) > 0 {
|
||||||
errorObj := errorArr[0].(map[string]any)
|
if errorObj, ok := errorArr[0].(map[string]any); ok {
|
||||||
return "", common.NewError(errorObj["code"], errorObj["message"])
|
return "", common.NewError(errorObj["code"], errorObj["message"])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return "", common.NewError("warp set license failed: unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
warpData["license_key"] = license
|
warpData["license_key"] = license
|
||||||
newWarpData, err := json.MarshalIndent(warpData, "", " ")
|
newWarpData, err := json.MarshalIndent(warpData, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
s.SettingService.SetWarp(string(newWarpData))
|
if err := s.SettingService.SetWarp(string(newWarpData)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return string(newWarpData), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user