mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
- Update go.mod module path from mhsanaei/3x-ui/v3 to saeederamy/3x-ui/v3 - Update all 73 Go files' import paths accordingly - Fix README.fa_IR.md install command to point to fork's main branch The fork was referencing the original repo's module path in go.mod and all Go source imports, making it dependent on MHSanaei's namespace at build time. https://claude.ai/code/session_01M6d5atbWjuLTj6UwRHoK5m
209 lines
5.2 KiB
Go
209 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/saeederamy/3x-ui/v3/util/common"
|
|
)
|
|
|
|
// WarpService provides business logic for Cloudflare WARP integration.
|
|
// It manages WARP configuration and connectivity settings.
|
|
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) {
|
|
return s.SettingService.GetWarp()
|
|
}
|
|
|
|
func (s *WarpService) DelWarpData() error {
|
|
return s.SettingService.SetWarp("")
|
|
}
|
|
|
|
func (s *WarpService) GetWarpConfig() (string, error) {
|
|
warpData, err := s.loadWarpCreds()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
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"])
|
|
|
|
body, err := doWarpRequest(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(body), nil
|
|
}
|
|
|
|
func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
|
|
hostName, _ := os.Hostname()
|
|
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, err := http.NewRequest(http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("CF-Client-Version", warpClientVer)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
body, err := doWarpRequest(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var rsp map[string]any
|
|
if err := json.Unmarshal(body, &rsp); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
deviceID, ok := rsp["id"].(string)
|
|
if !ok {
|
|
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
|
|
}
|
|
|
|
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) {
|
|
warpData, err := s.loadWarpCreds()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
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(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")
|
|
|
|
body, err := doWarpRequest(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var response map[string]any
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|
return "", err
|
|
}
|
|
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
|
|
newWarpData, err := json.MarshalIndent(warpData, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
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
|
|
}
|