diff --git a/backend/database/model/endpoints.go b/backend/database/model/endpoints.go index 43de727..960c963 100644 --- a/backend/database/model/endpoints.go +++ b/backend/database/model/endpoints.go @@ -9,6 +9,7 @@ type Endpoint struct { Type string `json:"type" form:"type"` Tag string `json:"tag" form:"tag" gorm:"unique"` Options json.RawMessage `json:"-" form:"-"` + Ext json.RawMessage `json:"ext" form:"ext"` } func (o *Endpoint) UnmarshalJSON(data []byte) error { @@ -27,9 +28,11 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error { delete(raw, "type") o.Tag = raw["tag"].(string) delete(raw, "tag") + o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ") + delete(raw, "ext") // Remaining fields - o.Options, err = json.Marshal(raw) + o.Options, err = json.MarshalIndent(raw, "", " ") return err } @@ -37,7 +40,12 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error { func (o Endpoint) MarshalJSON() ([]byte, error) { // Combine fixed fields and dynamic fields into one map combined := make(map[string]interface{}) - combined["type"] = o.Type + switch o.Type { + case "warp": + combined["type"] = "wireguard" + default: + combined["type"] = o.Type + } combined["tag"] = o.Tag if o.Options != nil { diff --git a/backend/service/endpoints.go b/backend/service/endpoints.go index 8ca910e..92df592 100644 --- a/backend/service/endpoints.go +++ b/backend/service/endpoints.go @@ -10,7 +10,9 @@ import ( "gorm.io/gorm" ) -type EndpointService struct{} +type EndpointService struct { + WarpService +} func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) { db := database.GetDB() @@ -25,6 +27,7 @@ func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) { "id": endpoint.Id, "type": endpoint.Type, "tag": endpoint.Tag, + "ext": endpoint.Ext, } if endpoint.Options != nil { var restFields map[string]json.RawMessage @@ -68,6 +71,25 @@ func (s *EndpointService) Save(tx *gorm.DB, act string, data json.RawMessage) er return err } + if endpoint.Type == "warp" { + if act == "new" { + err = s.WarpService.RegisterWarp(&endpoint) + if err != nil { + return err + } + } else { + var old_license string + err = tx.Model(model.Endpoint{}).Select("json_extract(ext, '$.license_key')").Where("id = ?", endpoint.Id).Find(&old_license).Error + if err != nil { + return err + } + err = s.WarpService.SetWarpLicense(old_license, &endpoint) + if err != nil { + return err + } + } + } + if corePtr.IsRunning() { configData, err := endpoint.MarshalJSON() if err != nil { diff --git a/backend/service/warp.go b/backend/service/warp.go new file mode 100644 index 0000000..7733dd6 --- /dev/null +++ b/backend/service/warp.go @@ -0,0 +1,229 @@ +package service + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "s-ui/database/model" + "s-ui/logger" + "s-ui/util/common" + "strconv" + "time" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WarpService struct{} + +func (s *WarpService) getWarpInfo(ep *model.Endpoint) ([]byte, error) { + var warpData map[string]string + err := json.Unmarshal(ep.Ext, &warpData) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"]) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (s *WarpService) RegisterWarp(ep *model.Endpoint) error { + tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") + privateKey, _ := wgtypes.GenerateKey() + publicKey := privateKey.PublicKey().String() + hostName, _ := os.Hostname() + + data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "s-ui", "name": "%s"}`, publicKey, tos, hostName) + url := "https://api.cloudflareclient.com/v0a2158/reg" + + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) + 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) + if err != nil { + return err + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return err + } + + var rspData map[string]interface{} + err = json.Unmarshal(buffer.Bytes(), &rspData) + if err != nil { + return err + } + + deviceId := rspData["id"].(string) + token := rspData["token"].(string) + license, ok := rspData["account"].(map[string]interface{})["license"].(string) + if !ok { + logger.Debug("Error accessing license value.") + return err + } + + warpData := map[string]string{ + "access_token": token, + "device_id": deviceId, + "license_key": license, + } + + ep.Ext, err = json.MarshalIndent(warpData, "", " ") + if err != nil { + return err + } + + warpInfo, err := s.getWarpInfo(ep) + if err != nil { + return err + } + + var warpDetails map[string]interface{} + err = json.Unmarshal(warpInfo, &warpDetails) + if err != nil { + return err + } + + warpConfig, _ := warpDetails["config"].(map[string]interface{}) + clientId, _ := warpConfig["client_id"].(string) + reserved := s.getReserved(clientId) + interfaceConfig, _ := warpConfig["interface"].(map[string]interface{}) + addresses, _ := interfaceConfig["addresses"].(map[string]interface{}) + v4, _ := addresses["v4"].(string) + v6, _ := addresses["v6"].(string) + peer, _ := warpConfig["peers"].([]interface{})[0].(map[string]interface{}) + peerEndpoint, _ := peer["endpoint"].(map[string]interface{})["host"].(string) + peerEpAddress, peerEpPort, err := net.SplitHostPort(peerEndpoint) + if err != nil { + return err + } + peerPublicKey, _ := peer["public_key"].(string) + peerPort, _ := strconv.Atoi(peerEpPort) + + peers := []map[string]interface{}{ + { + "address": peerEpAddress, + "port": peerPort, + "public_key": peerPublicKey, + "allowed_ips": []string{"0.0.0.0/0", "::/0"}, + "reserved": reserved, + }, + } + + var epOptions map[string]interface{} + err = json.Unmarshal(ep.Options, &epOptions) + if err != nil { + return err + } + epOptions["private_key"] = privateKey.String() + epOptions["address"] = []string{fmt.Sprintf("%s/32", v4), fmt.Sprintf("%s/128", v6)} + epOptions["listen_port"] = 0 + epOptions["peers"] = peers + + ep.Options, err = json.MarshalIndent(epOptions, "", " ") + return err +} + +func (s *WarpService) getReserved(clientID string) []int { + var reserved []int + decoded, err := base64.StdEncoding.DecodeString(clientID) + if err != nil { + return nil + } + + hexString := "" + for _, char := range decoded { + hex := fmt.Sprintf("%02x", char) + hexString += hex + } + + for i := 0; i < len(hexString); i += 2 { + hexByte := hexString[i : i+2] + decValue, err := strconv.ParseInt(hexByte, 16, 32) + if err != nil { + return nil + } + reserved = append(reserved, int(decValue)) + } + + return reserved +} + +func (s *WarpService) SetWarpLicense(old_license string, ep *model.Endpoint) error { + var warpData map[string]string + err := json.Unmarshal(ep.Ext, &warpData) + if err != nil { + return err + } + + if warpData["license_key"] == old_license { + return nil + } + + url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"]) + data := fmt.Sprintf(`{"license": "%s"}`, warpData["license_key"]) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data))) + 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.NewBuffer(make([]byte, 8192)) + buffer.Reset() + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return err + } + var response map[string]interface{} + err = json.Unmarshal(buffer.Bytes(), &response) + if err != nil { + return err + } + + if success, ok := response["success"].(bool); ok && success == false { + errorArr, _ := response["errors"].([]interface{}) + errorObj := errorArr[0].(map[string]interface{}) + return common.NewError(errorObj["code"], errorObj["message"]) + } + + return nil +} diff --git a/frontend/src/components/protocols/Warp.vue b/frontend/src/components/protocols/Warp.vue new file mode 100644 index 0000000..38df7ad --- /dev/null +++ b/frontend/src/components/protocols/Warp.vue @@ -0,0 +1,152 @@ + + + \ No newline at end of file diff --git a/frontend/src/layouts/modals/Endpoint.vue b/frontend/src/layouts/modals/Endpoint.vue index 82707d7..36af78d 100644 --- a/frontend/src/layouts/modals/Endpoint.vue +++ b/frontend/src/layouts/modals/Endpoint.vue @@ -21,6 +21,7 @@ + @@ -50,6 +51,7 @@ import { EpTypes, createEndpoint } from '@/types/endpoints' import RandomUtil from '@/plugins/randomUtil' import Dial from '@/components/Dial.vue' import Wireguard from '@/components/protocols/Wireguard.vue' +import Warp from '@/components/protocols/Warp.vue' import HttpUtils from '@/plugins/httputil' import { push } from 'notivue' import { i18n } from '@/locales' @@ -151,6 +153,6 @@ export default { } }, }, - components: { Dial, Wireguard } + components: { Dial, Wireguard, Warp } } \ No newline at end of file diff --git a/frontend/src/types/endpoints.ts b/frontend/src/types/endpoints.ts index e26e7ff..b0c4d6c 100644 --- a/frontend/src/types/endpoints.ts +++ b/frontend/src/types/endpoints.ts @@ -2,6 +2,7 @@ import { Dial } from "./outbounds" export const EpTypes = { Wireguard: 'wireguard', + Warp: 'warp', } type EpType = typeof EpTypes[keyof typeof EpTypes] @@ -34,6 +35,10 @@ export interface WireGuard extends EndpointBasics, Dial { workers?: number } +export interface Warp extends WireGuard { + ext: any +} + // Create interfaces dynamically based on EpTypes keys type InterfaceMap = { [Key in keyof typeof EpTypes]: { @@ -48,6 +53,7 @@ export type Endpoint = InterfaceMap[keyof InterfaceMap] // Create defaultValues object dynamically const defaultValues: Record = { wireguard: { type: EpTypes.Wireguard, address: ['10.0.0.2/32','fe80::2/128'], private_key: '', listen_port: 0, peers: [{ address: '', port: 0, public_key: ''}] }, + warp: { type: EpTypes.Warp, address: [], private_key: '', listen_port: 0, mtu: 1420, peers: [{ address: '', port: 0, public_key: ''}] }, } export function createEndpoint(type: string,json?: Partial): Endpoint { diff --git a/frontend/src/views/Endpoints.vue b/frontend/src/views/Endpoints.vue index 6e2c40c..65d920d 100644 --- a/frontend/src/views/Endpoints.vue +++ b/frontend/src/views/Endpoints.vue @@ -38,7 +38,7 @@ {{ $t('in.port') }} - {{ item.listen_port?? '-' }} + {{ item.listen_port>0 ? item.listen_port : '-' }} diff --git a/frontend/src/views/Rules.vue b/frontend/src/views/Rules.vue index 41781ce..5ebe103 100644 --- a/frontend/src/views/Rules.vue +++ b/frontend/src/views/Rules.vue @@ -223,7 +223,7 @@ const outboundTags = computed((): string[] => { }) const inboundTags = computed((): string[] => { - return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)] + return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.filter((e:any) => e.listen_port > 0).map((e:any) => e.tag)] }) let delRuleOverlay = ref(new Array)