mirror of
https://github.com/XrayR-project/XrayR.git
synced 2025-06-08 05:19:54 +00:00

The updated code eliminates a redundant user retrieval operation in the v2raysocks module. This operation was not needed because the desired information (method) can be inferred elsewhere in the application. This change simplifies the code, improves efficiency, and may reduce potential errors related to user data retrieval.
490 lines
15 KiB
Go
490 lines
15 KiB
Go
package v2raysocks
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/bitly/go-simplejson"
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
|
C "github.com/sagernet/sing/common"
|
|
|
|
"github.com/XrayR-project/XrayR/api"
|
|
)
|
|
|
|
// APIClient create an api client to the panel.
|
|
type APIClient struct {
|
|
client *resty.Client
|
|
APIHost string
|
|
NodeID int
|
|
Key string
|
|
NodeType string
|
|
EnableVless bool
|
|
VlessFlow string
|
|
SpeedLimit float64
|
|
DeviceLimit int
|
|
LocalRuleList []api.DetectRule
|
|
ConfigResp *simplejson.Json
|
|
access sync.Mutex
|
|
eTags map[string]string
|
|
}
|
|
|
|
// New create an api instance
|
|
func New(apiConfig *api.Config) *APIClient {
|
|
|
|
client := resty.New()
|
|
client.SetRetryCount(3)
|
|
if apiConfig.Timeout > 0 {
|
|
client.SetTimeout(time.Duration(apiConfig.Timeout) * time.Second)
|
|
} else {
|
|
client.SetTimeout(5 * time.Second)
|
|
}
|
|
|
|
client.OnError(func(req *resty.Request, err error) {
|
|
var v *resty.ResponseError
|
|
if errors.As(err, &v) {
|
|
// v.Response contains the last response from the server
|
|
// v.Err contains the original error
|
|
log.Print(v.Err)
|
|
}
|
|
})
|
|
|
|
// Create Key for each requests
|
|
client.SetQueryParams(map[string]string{
|
|
"node_id": strconv.Itoa(apiConfig.NodeID),
|
|
"token": apiConfig.Key,
|
|
})
|
|
// Read local rule list
|
|
localRuleList := readLocalRuleList(apiConfig.RuleListPath)
|
|
apiClient := &APIClient{
|
|
client: client,
|
|
NodeID: apiConfig.NodeID,
|
|
Key: apiConfig.Key,
|
|
APIHost: apiConfig.APIHost,
|
|
NodeType: apiConfig.NodeType,
|
|
EnableVless: apiConfig.EnableVless,
|
|
VlessFlow: apiConfig.VlessFlow,
|
|
SpeedLimit: apiConfig.SpeedLimit,
|
|
DeviceLimit: apiConfig.DeviceLimit,
|
|
LocalRuleList: localRuleList,
|
|
eTags: make(map[string]string),
|
|
}
|
|
return apiClient
|
|
}
|
|
|
|
// readLocalRuleList reads the local rule list file
|
|
func readLocalRuleList(path string) (LocalRuleList []api.DetectRule) {
|
|
|
|
LocalRuleList = make([]api.DetectRule, 0)
|
|
if path != "" {
|
|
// open the file
|
|
file, err := os.Open(path)
|
|
|
|
// handle errors while opening
|
|
if err != nil {
|
|
log.Printf("Error when opening file: %s", err)
|
|
return LocalRuleList
|
|
}
|
|
|
|
fileScanner := bufio.NewScanner(file)
|
|
|
|
// read line by line
|
|
for fileScanner.Scan() {
|
|
LocalRuleList = append(LocalRuleList, api.DetectRule{
|
|
ID: -1,
|
|
Pattern: regexp.MustCompile(fileScanner.Text()),
|
|
})
|
|
}
|
|
// handle first encountered error while reading
|
|
if err := fileScanner.Err(); err != nil {
|
|
log.Fatalf("Error while reading file: %s", err)
|
|
return
|
|
}
|
|
|
|
file.Close()
|
|
}
|
|
|
|
return LocalRuleList
|
|
}
|
|
|
|
// Describe return a description of the client
|
|
func (c *APIClient) Describe() api.ClientInfo {
|
|
return api.ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType}
|
|
}
|
|
|
|
// Debug set the client debug for client
|
|
func (c *APIClient) Debug() {
|
|
c.client.SetDebug(true)
|
|
}
|
|
|
|
func (c *APIClient) assembleURL(path string) string {
|
|
return c.APIHost + path
|
|
}
|
|
|
|
func (c *APIClient) parseResponse(res *resty.Response, path string, err error) (*simplejson.Json, error) {
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request %s failed: %s", c.assembleURL(path), err)
|
|
}
|
|
|
|
if res.StatusCode() > 400 {
|
|
body := res.Body()
|
|
return nil, fmt.Errorf("request %s failed: %s, %s", c.assembleURL(path), string(body), err)
|
|
}
|
|
rtn, err := simplejson.NewJson(res.Body())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ret %s invalid", res.String())
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
// GetNodeInfo will pull NodeInfo Config from panel
|
|
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
|
var nodeType string
|
|
switch c.NodeType {
|
|
case "V2ray", "Trojan", "Shadowsocks":
|
|
nodeType = strings.ToLower(c.NodeType)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
|
}
|
|
res, err := c.client.R().
|
|
SetHeader("If-None-Match", c.eTags["config"]).
|
|
SetQueryParams(map[string]string{
|
|
"act": "config",
|
|
"nodetype": nodeType,
|
|
}).
|
|
ForceContentType("application/json").
|
|
Get(c.APIHost)
|
|
|
|
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
|
if res.StatusCode() == 304 {
|
|
return nil, errors.New(api.NodeNotModified)
|
|
}
|
|
// update etag
|
|
if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["config"] {
|
|
c.eTags["config"] = res.Header().Get("Etag")
|
|
}
|
|
|
|
response, err := c.parseResponse(res, "", err)
|
|
c.access.Lock()
|
|
defer c.access.Unlock()
|
|
c.ConfigResp = response
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch c.NodeType {
|
|
case "V2ray":
|
|
nodeInfo, err = c.ParseV2rayNodeResponse(response)
|
|
case "Trojan":
|
|
nodeInfo, err = c.ParseTrojanNodeResponse(response)
|
|
case "Shadowsocks":
|
|
nodeInfo, err = c.ParseSSNodeResponse(response)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
|
}
|
|
|
|
if err != nil {
|
|
res, _ := response.MarshalJSON()
|
|
return nil, fmt.Errorf("Parse node info failed: %s, \nError: %s", string(res), err)
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// GetUserList will pull user form panel
|
|
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
|
var nodeType string
|
|
switch c.NodeType {
|
|
case "V2ray", "Trojan", "Shadowsocks":
|
|
nodeType = strings.ToLower(c.NodeType)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
|
}
|
|
res, err := c.client.R().
|
|
SetHeader("If-None-Match", c.eTags["user"]).
|
|
SetQueryParams(map[string]string{
|
|
"act": "user",
|
|
"nodetype": nodeType,
|
|
}).
|
|
ForceContentType("application/json").
|
|
Get(c.APIHost)
|
|
|
|
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
|
if res.StatusCode() == 304 {
|
|
return nil, errors.New(api.UserNotModified)
|
|
}
|
|
// update etag
|
|
if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["user"] {
|
|
c.eTags["user"] = res.Header().Get("Etag")
|
|
}
|
|
|
|
response, err := c.parseResponse(res, "", err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
numOfUsers := len(response.Get("data").MustArray())
|
|
userList := make([]api.UserInfo, numOfUsers)
|
|
for i := 0; i < numOfUsers; i++ {
|
|
user := api.UserInfo{}
|
|
user.UID = response.Get("data").GetIndex(i).Get("id").MustInt()
|
|
switch c.NodeType {
|
|
case "Shadowsocks":
|
|
user.Email = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
|
user.Passwd = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
|
user.Method = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("cipher").MustString()
|
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
|
case "Trojan":
|
|
user.UUID = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
|
user.Email = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("trojan_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
|
case "V2ray":
|
|
user.UUID = response.Get("data").GetIndex(i).Get("v2ray_user").Get("uuid").MustString()
|
|
user.Email = response.Get("data").GetIndex(i).Get("v2ray_user").Get("email").MustString()
|
|
user.AlterID = uint16(response.Get("data").GetIndex(i).Get("v2ray_user").Get("alter_id").MustUint64())
|
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("v2ray_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
|
}
|
|
if c.SpeedLimit > 0 {
|
|
user.SpeedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
}
|
|
user.DeviceLimit = c.DeviceLimit
|
|
userList[i] = user
|
|
}
|
|
return &userList, nil
|
|
}
|
|
|
|
// ReportUserTraffic reports the user traffic
|
|
func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error {
|
|
|
|
data := make([]UserTraffic, len(*userTraffic))
|
|
for i, traffic := range *userTraffic {
|
|
data[i] = UserTraffic{
|
|
UID: traffic.UID,
|
|
Upload: traffic.Upload,
|
|
Download: traffic.Download}
|
|
}
|
|
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetQueryParams(map[string]string{
|
|
"act": "submit",
|
|
"nodetype": strings.ToLower(c.NodeType),
|
|
}).
|
|
SetBody(data).
|
|
ForceContentType("application/json").
|
|
Post(c.APIHost)
|
|
_, err = c.parseResponse(res, "", err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetNodeRule implements the API interface
|
|
func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
|
ruleList := c.LocalRuleList
|
|
if c.NodeType != "V2ray" {
|
|
return &ruleList, nil
|
|
}
|
|
|
|
// Only support the rule for v2ray
|
|
// fix: reuse config response
|
|
c.access.Lock()
|
|
defer c.access.Unlock()
|
|
ruleListResponse := c.ConfigResp.Get("routing").Get("rules").GetIndex(1).Get("domain").MustStringArray()
|
|
for i, rule := range ruleListResponse {
|
|
rule = strings.TrimPrefix(rule, "regexp:")
|
|
ruleListItem := api.DetectRule{
|
|
ID: i,
|
|
Pattern: regexp.MustCompile(rule),
|
|
}
|
|
ruleList = append(ruleList, ruleListItem)
|
|
}
|
|
return &ruleList, nil
|
|
}
|
|
|
|
// ReportNodeStatus implements the API interface
|
|
func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) {
|
|
systemload := NodeStatus{
|
|
Uptime: int(nodeStatus.Uptime),
|
|
CPU: fmt.Sprintf("%d%%", int(nodeStatus.CPU)),
|
|
Mem: fmt.Sprintf("%d%%", int(nodeStatus.Mem)),
|
|
Disk: fmt.Sprintf("%d%%", int(nodeStatus.Disk)),
|
|
}
|
|
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetQueryParams(map[string]string{
|
|
"act": "nodestatus",
|
|
"nodetype": strings.ToLower(c.NodeType),
|
|
}).
|
|
SetBody(systemload).
|
|
ForceContentType("application/json").
|
|
Post(c.APIHost)
|
|
_, err = c.parseResponse(res, "", err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReportNodeOnlineUsers implements the API interface
|
|
func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error {
|
|
data := make([]NodeOnline, len(*onlineUserList))
|
|
for i, user := range *onlineUserList {
|
|
data[i] = NodeOnline{UID: user.UID, IP: user.IP}
|
|
}
|
|
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetQueryParams(map[string]string{
|
|
"act": "onlineusers",
|
|
"nodetype": strings.ToLower(c.NodeType),
|
|
}).
|
|
SetBody(data).
|
|
ForceContentType("application/json").
|
|
Post(c.APIHost)
|
|
_, err = c.parseResponse(res, "", err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReportIllegal implements the API interface
|
|
func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error {
|
|
return nil
|
|
}
|
|
|
|
// ParseTrojanNodeResponse parse the response for the given nodeInfo format
|
|
func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
|
tmpInboundInfo := nodeInfoResponse.Get("inbounds").MustArray()
|
|
marshalByte, _ := json.Marshal(tmpInboundInfo[0].(map[string]interface{}))
|
|
inboundInfo, _ := simplejson.NewJson(marshalByte)
|
|
|
|
port := uint32(inboundInfo.Get("port").MustUint64())
|
|
host := inboundInfo.Get("streamSettings").Get("tlsSettings").Get("serverName").MustString()
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
TransportProtocol: "tcp",
|
|
EnableTLS: true,
|
|
Host: host,
|
|
}
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseSSNodeResponse parse the response for the given nodeInfo format
|
|
func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
|
var method, serverPsk string
|
|
tmpInboundInfo := nodeInfoResponse.Get("inbounds").MustArray()
|
|
marshalByte, _ := json.Marshal(tmpInboundInfo[0].(map[string]interface{}))
|
|
inboundInfo, _ := simplejson.NewJson(marshalByte)
|
|
|
|
port := uint32(inboundInfo.Get("port").MustUint64())
|
|
method = inboundInfo.Get("settings").Get("method").MustString()
|
|
// Shadowsocks 2022
|
|
if C.Contains(shadowaead_2022.List, method) {
|
|
serverPsk = inboundInfo.Get("settings").Get("password").MustString()
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
TransportProtocol: "tcp",
|
|
CypherMethod: method,
|
|
ServerKey: serverPsk,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseV2rayNodeResponse parse the response for the given nodeInfo format
|
|
func (c *APIClient) ParseV2rayNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
|
var path, host, serviceName string
|
|
var header json.RawMessage
|
|
var enableTLS bool
|
|
var enableVless bool
|
|
var enableReality bool
|
|
var alterID uint16 = 0
|
|
|
|
tmpInboundInfo := nodeInfoResponse.Get("inbounds").MustArray()
|
|
marshalByte, _ := json.Marshal(tmpInboundInfo[0].(map[string]interface{}))
|
|
inboundInfo, _ := simplejson.NewJson(marshalByte)
|
|
|
|
port := uint32(inboundInfo.Get("port").MustUint64())
|
|
transportProtocol := inboundInfo.Get("streamSettings").Get("network").MustString()
|
|
|
|
switch transportProtocol {
|
|
case "ws":
|
|
path = inboundInfo.Get("streamSettings").Get("wsSettings").Get("path").MustString()
|
|
host = inboundInfo.Get("streamSettings").Get("wsSettings").Get("headers").Get("Host").MustString()
|
|
case "grpc":
|
|
if data, ok := inboundInfo.Get("streamSettings").Get("grpcSettings").CheckGet("serviceName"); ok {
|
|
serviceName = data.MustString()
|
|
}
|
|
case "tcp":
|
|
if data, ok := inboundInfo.Get("streamSettings").Get("tcpSettings").CheckGet("header"); ok {
|
|
if httpHeader, err := data.MarshalJSON(); err != nil {
|
|
return nil, err
|
|
} else {
|
|
header = httpHeader
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
enableTLS = inboundInfo.Get("streamSettings").Get("security").MustString() == "tls"
|
|
enableVless = inboundInfo.Get("streamSettings").Get("security").MustString() == "reality"
|
|
enableReality = enableVless
|
|
|
|
realityConfig := new(api.REALITYConfig)
|
|
if enableVless {
|
|
// parse reality config
|
|
realityConfig = &api.REALITYConfig{
|
|
Dest: inboundInfo.Get("streamSettings").Get("realitySettings").Get("dest").MustString(),
|
|
ProxyProtocolVer: inboundInfo.Get("streamSettings").Get("realitySettings").Get("xver").MustUint64(),
|
|
ServerNames: inboundInfo.Get("streamSettings").Get("realitySettings").Get("serverNames").MustStringArray(),
|
|
PrivateKey: inboundInfo.Get("streamSettings").Get("realitySettings").Get("privateKey").MustString(),
|
|
MinClientVer: inboundInfo.Get("streamSettings").Get("realitySettings").Get("minClientVer").MustString(),
|
|
MaxClientVer: inboundInfo.Get("streamSettings").Get("realitySettings").Get("maxClientVer").MustString(),
|
|
MaxTimeDiff: inboundInfo.Get("streamSettings").Get("realitySettings").Get("maxTimeDiff").MustUint64(),
|
|
ShortIds: inboundInfo.Get("streamSettings").Get("realitySettings").Get("shortIds").MustStringArray(),
|
|
}
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
// AlterID will be updated after next sync
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
AlterID: alterID,
|
|
TransportProtocol: transportProtocol,
|
|
EnableTLS: enableTLS,
|
|
Path: path,
|
|
Host: host,
|
|
EnableVless: enableVless,
|
|
VlessFlow: c.VlessFlow,
|
|
ServiceName: serviceName,
|
|
Header: header,
|
|
EnableREALITY: enableReality,
|
|
REALITYConfig: realityConfig,
|
|
}
|
|
return nodeInfo, nil
|
|
}
|