Skip to content

Commit d8198f5

Browse files
MHSanaeiclaude
andcommitted
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>
1 parent f2bc493 commit d8198f5

2 files changed

Lines changed: 141 additions & 103 deletions

File tree

web/html/modals/warp_modal.html

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@
8888
<a-button @click="addOutbound" :loading="warpModal.confirmLoading"
8989
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
9090
</template>
91-
</a-form-item>
9291
</a-form>
9392
</template>
9493
</template>
@@ -131,34 +130,35 @@
131130
},
132131
methods: {
133132
collectConfig() {
134-
config = warpModal.warpConfig.config;
135-
peer = config.peers[0];
136-
if (config) {
137-
warpModal.warpOutbound = Outbound.fromJson({
138-
tag: 'warp',
139-
protocol: Protocols.Wireguard,
140-
settings: {
141-
mtu: 1420,
142-
secretKey: warpModal.warpData.private_key,
143-
address: this.getAddresses(config.interface.addresses),
144-
reserved: this.getResolved(config.client_id),
145-
domainStrategy: 'ForceIP',
146-
peers: [{
147-
publicKey: peer.public_key,
148-
endpoint: peer.endpoint.host,
149-
}],
150-
noKernelTun: false,
151-
}
152-
});
133+
const config = warpModal.warpConfig && warpModal.warpConfig.config;
134+
if (!config || !config.peers || !config.peers.length) {
135+
return;
153136
}
137+
const peer = config.peers[0];
138+
warpModal.warpOutbound = Outbound.fromJson({
139+
tag: 'warp',
140+
protocol: Protocols.Wireguard,
141+
settings: {
142+
mtu: 1420,
143+
secretKey: warpModal.warpData.private_key,
144+
address: this.getAddresses(config.interface.addresses),
145+
reserved: this.getReserved(config.client_id),
146+
domainStrategy: 'ForceIP',
147+
peers: [{
148+
publicKey: peer.public_key,
149+
endpoint: peer.endpoint.host,
150+
}],
151+
noKernelTun: false,
152+
}
153+
});
154154
},
155155
getAddresses(addrs) {
156156
let addresses = [];
157157
if (addrs.v4) addresses.push(addrs.v4 + "/32");
158158
if (addrs.v6) addresses.push(addrs.v6 + "/128");
159159
return addresses;
160160
},
161-
getResolved(client_id) {
161+
getReserved(client_id) {
162162
let reserved = [];
163163
let decoded = atob(client_id);
164164
let hexString = '';
@@ -218,11 +218,13 @@
218218
}
219219
},
220220
addOutbound() {
221+
if (!warpModal.warpOutbound) return;
221222
app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
222223
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
223224
warpModal.close();
224225
},
225226
resetOutbound() {
227+
if (!warpModal.warpOutbound) return;
226228
app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
227229
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
228230
warpModal.close();

web/service/warp.go

Lines changed: 118 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"os"
910
"time"
1011

11-
"github.com/mhsanaei/3x-ui/v2/logger"
1212
"github.com/mhsanaei/3x-ui/v2/util/common"
1313
)
1414

@@ -18,156 +18,192 @@ type WarpService struct {
1818
SettingService
1919
}
2020

21+
const (
22+
warpAPIBase = "https://api.cloudflareclient.com/v0a4005"
23+
warpClientVer = "a-6.30-3596"
24+
)
25+
26+
var warpHTTPClient = &http.Client{Timeout: 15 * time.Second}
27+
2128
func (s *WarpService) GetWarpData() (string, error) {
22-
warp, err := s.SettingService.GetWarp()
23-
if err != nil {
24-
return "", err
25-
}
26-
return warp, nil
29+
return s.SettingService.GetWarp()
2730
}
2831

2932
func (s *WarpService) DelWarpData() error {
30-
err := s.SettingService.SetWarp("")
31-
if err != nil {
32-
return err
33-
}
34-
return nil
33+
return s.SettingService.SetWarp("")
3534
}
3635

3736
func (s *WarpService) GetWarpConfig() (string, error) {
38-
var warpData map[string]string
39-
warp, err := s.SettingService.GetWarp()
37+
warpData, err := s.loadWarpCreds()
4038
if err != nil {
4139
return "", err
4240
}
43-
err = json.Unmarshal([]byte(warp), &warpData)
44-
if err != nil {
45-
return "", err
46-
}
47-
48-
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"])
4941

50-
req, err := http.NewRequest("GET", url, nil)
42+
url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"])
43+
req, err := http.NewRequest(http.MethodGet, url, nil)
5144
if err != nil {
5245
return "", err
5346
}
5447
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
5548

56-
client := &http.Client{}
57-
resp, err := client.Do(req)
58-
if err != nil {
59-
return "", err
60-
}
61-
defer resp.Body.Close()
62-
buffer := &bytes.Buffer{}
63-
_, err = buffer.ReadFrom(resp.Body)
49+
body, err := doWarpRequest(req)
6450
if err != nil {
6551
return "", err
6652
}
67-
68-
return buffer.String(), nil
53+
return string(body), nil
6954
}
7055

7156
func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
72-
tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
7357
hostName, _ := os.Hostname()
74-
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
75-
76-
url := "https://api.cloudflareclient.com/v0a2158/reg"
77-
78-
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
58+
reqBody, err := json.Marshal(map[string]any{
59+
"key": publicKey,
60+
"tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
61+
"type": "PC",
62+
"model": "x-ui",
63+
"name": hostName,
64+
})
7965
if err != nil {
8066
return "", err
8167
}
8268

83-
req.Header.Add("CF-Client-Version", "a-7.21-0721")
84-
req.Header.Add("Content-Type", "application/json")
85-
86-
client := &http.Client{}
87-
resp, err := client.Do(req)
69+
req, err := http.NewRequest(http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
8870
if err != nil {
8971
return "", err
9072
}
91-
defer resp.Body.Close()
92-
buffer := &bytes.Buffer{}
93-
_, err = buffer.ReadFrom(resp.Body)
73+
req.Header.Set("CF-Client-Version", warpClientVer)
74+
req.Header.Set("Content-Type", "application/json")
75+
76+
body, err := doWarpRequest(req)
9477
if err != nil {
9578
return "", err
9679
}
9780

98-
var rspData map[string]any
99-
err = json.Unmarshal(buffer.Bytes(), &rspData)
100-
if err != nil {
81+
var rsp map[string]any
82+
if err := json.Unmarshal(body, &rsp); err != nil {
10183
return "", err
10284
}
10385

104-
deviceId := rspData["id"].(string)
105-
token := rspData["token"].(string)
106-
license, ok := rspData["account"].(map[string]any)["license"].(string)
86+
deviceID, ok := rsp["id"].(string)
10787
if !ok {
108-
logger.Debug("Error accessing license value.")
109-
return "", err
88+
return "", common.NewError("warp register: missing 'id' in response")
89+
}
90+
token, ok := rsp["token"].(string)
91+
if !ok {
92+
return "", common.NewError("warp register: missing 'token' in response")
93+
}
94+
account, ok := rsp["account"].(map[string]any)
95+
if !ok {
96+
return "", common.NewError("warp register: missing 'account' in response")
97+
}
98+
license, ok := account["license"].(string)
99+
if !ok {
100+
return "", common.NewError("warp register: missing 'account.license' in response")
110101
}
111102

112-
warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId)
113-
warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey)
114-
115-
s.SettingService.SetWarp(warpData)
116-
117-
result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String())
103+
warpData := map[string]string{
104+
"access_token": token,
105+
"device_id": deviceID,
106+
"license_key": license,
107+
"private_key": secretKey,
108+
}
109+
warpJSON, err := json.MarshalIndent(warpData, "", " ")
110+
if err != nil {
111+
return "", err
112+
}
113+
if err := s.SettingService.SetWarp(string(warpJSON)); err != nil {
114+
return "", err
115+
}
118116

119-
return result, nil
117+
result, err := json.MarshalIndent(map[string]any{
118+
"data": warpData,
119+
"config": json.RawMessage(body),
120+
}, "", " ")
121+
if err != nil {
122+
return "", err
123+
}
124+
return string(result), nil
120125
}
121126

122127
func (s *WarpService) SetWarpLicense(license string) (string, error) {
123-
var warpData map[string]string
124-
warp, err := s.SettingService.GetWarp()
128+
warpData, err := s.loadWarpCreds()
125129
if err != nil {
126130
return "", err
127131
}
128-
err = json.Unmarshal([]byte(warp), &warpData)
132+
133+
url := fmt.Sprintf("%s/reg/%s/account", warpAPIBase, warpData["device_id"])
134+
reqBody, err := json.Marshal(map[string]string{"license": license})
129135
if err != nil {
130136
return "", err
131137
}
132138

133-
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
134-
data := fmt.Sprintf(`{"license": "%s"}`, license)
135-
136-
req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data)))
139+
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(reqBody))
137140
if err != nil {
138141
return "", err
139142
}
140143
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
144+
req.Header.Set("Content-Type", "application/json")
141145

142-
client := &http.Client{}
143-
resp, err := client.Do(req)
144-
if err != nil {
145-
return "", err
146-
}
147-
defer resp.Body.Close()
148-
buffer := &bytes.Buffer{}
149-
_, err = buffer.ReadFrom(resp.Body)
146+
body, err := doWarpRequest(req)
150147
if err != nil {
151148
return "", err
152149
}
153150

154151
var response map[string]any
155-
err = json.Unmarshal(buffer.Bytes(), &response)
156-
if err != nil {
152+
if err := json.Unmarshal(body, &response); err != nil {
157153
return "", err
158154
}
159-
if response["success"] == false {
160-
errorArr, _ := response["errors"].([]any)
161-
errorObj := errorArr[0].(map[string]any)
162-
return "", common.NewError(errorObj["code"], errorObj["message"])
155+
if success, _ := response["success"].(bool); !success {
156+
if errorArr, ok := response["errors"].([]any); ok && len(errorArr) > 0 {
157+
if errorObj, ok := errorArr[0].(map[string]any); ok {
158+
return "", common.NewError(errorObj["code"], errorObj["message"])
159+
}
160+
}
161+
return "", common.NewError("warp set license failed: unknown error")
163162
}
164163

165164
warpData["license_key"] = license
166165
newWarpData, err := json.MarshalIndent(warpData, "", " ")
167166
if err != nil {
168167
return "", err
169168
}
170-
s.SettingService.SetWarp(string(newWarpData))
171-
169+
if err := s.SettingService.SetWarp(string(newWarpData)); err != nil {
170+
return "", err
171+
}
172172
return string(newWarpData), nil
173173
}
174+
175+
// loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set.
176+
func (s *WarpService) loadWarpCreds() (map[string]string, error) {
177+
warp, err := s.SettingService.GetWarp()
178+
if err != nil {
179+
return nil, err
180+
}
181+
var data map[string]string
182+
if err := json.Unmarshal([]byte(warp), &data); err != nil {
183+
return nil, err
184+
}
185+
if data["access_token"] == "" || data["device_id"] == "" {
186+
return nil, common.NewError("warp not registered: missing access_token or device_id")
187+
}
188+
return data, nil
189+
}
190+
191+
// doWarpRequest sends the request and returns the response body on 2xx.
192+
// Non-2xx responses are returned as errors including the status code and body.
193+
func doWarpRequest(req *http.Request) ([]byte, error) {
194+
resp, err := warpHTTPClient.Do(req)
195+
if err != nil {
196+
return nil, err
197+
}
198+
defer resp.Body.Close()
199+
200+
body, err := io.ReadAll(resp.Body)
201+
if err != nil {
202+
return nil, err
203+
}
204+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
205+
return nil, common.NewErrorf("warp api %s %s returned status %d: %s",
206+
req.Method, req.URL.Path, resp.StatusCode, string(body))
207+
}
208+
return body, nil
209+
}

0 commit comments

Comments
 (0)