mirror of
https://github.com/XrayR-project/XrayR.git
synced 2025-06-07 21:09:53 +00:00

The standard "log" package was replaced by the structured logger "github.com/sirupsen/logrus" for better log control in various files. This change will allow to tailor the logging information more precisely and make logs easier to read and analyze. All calls of standard log methods were replaced by their logrus counterparts.
859 lines
23 KiB
Go
859 lines
23 KiB
Go
package sspanel
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
|
|
"github.com/XrayR-project/XrayR/api"
|
|
)
|
|
|
|
var (
|
|
firstPortRe = regexp.MustCompile(`(?m)port=(?P<outport>\d+)#?`) // First Port
|
|
secondPortRe = regexp.MustCompile(`(?m)port=\d+#(\d+)`) // Second Port
|
|
hostRe = regexp.MustCompile(`(?m)host=([\w.]+)\|?`) // Host
|
|
)
|
|
|
|
// APIClient create a 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
|
|
DisableCustomConfig bool
|
|
LocalRuleList []api.DetectRule
|
|
LastReportOnline map[int]int
|
|
access sync.Mutex
|
|
version string
|
|
eTags map[string]string
|
|
}
|
|
|
|
// New create 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)
|
|
}
|
|
})
|
|
|
|
client.SetBaseURL(apiConfig.APIHost)
|
|
// Create Key for each requests
|
|
client.SetQueryParam("key", apiConfig.Key)
|
|
// Add support for muKey
|
|
client.SetQueryParam("muKey", apiConfig.Key)
|
|
// Read local rule list
|
|
localRuleList := readLocalRuleList(apiConfig.RuleListPath)
|
|
|
|
return &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,
|
|
DisableCustomConfig: apiConfig.DisableCustomConfig,
|
|
LastReportOnline: make(map[int]int),
|
|
eTags: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
defer file.Close()
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
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) (*Response, 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, %v", c.assembleURL(path), string(body), err)
|
|
}
|
|
response := res.Result().(*Response)
|
|
|
|
if response.Ret != 1 {
|
|
res, _ := json.Marshal(&response)
|
|
return nil, fmt.Errorf("ret %s invalid", string(res))
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
// GetNodeInfo will pull NodeInfo Config from ssPanel
|
|
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
|
path := fmt.Sprintf("/mod_mu/nodes/%d/info", c.NodeID)
|
|
res, err := c.client.R().
|
|
SetResult(&Response{}).
|
|
SetHeader("If-None-Match", c.eTags["node"]).
|
|
ForceContentType("application/json").
|
|
Get(path)
|
|
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
|
if res.StatusCode() == 304 {
|
|
return nil, errors.New(api.NodeNotModified)
|
|
}
|
|
|
|
if res.Header().Get("ETag") != "" && res.Header().Get("ETag") != c.eTags["node"] {
|
|
c.eTags["node"] = res.Header().Get("ETag")
|
|
}
|
|
|
|
response, err := c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodeInfoResponse := new(NodeInfoResponse)
|
|
|
|
if err := json.Unmarshal(response.Data, nodeInfoResponse); err != nil {
|
|
return nil, fmt.Errorf("unmarshal %s failed: %s", reflect.TypeOf(nodeInfoResponse), err)
|
|
}
|
|
|
|
// determine ssPanel version, if disable custom config or version < 2021.11, then use old api
|
|
c.version = nodeInfoResponse.Version
|
|
var isExpired bool
|
|
if compareVersion(c.version, "2021.11") == -1 {
|
|
isExpired = true
|
|
}
|
|
|
|
if c.DisableCustomConfig || isExpired {
|
|
if isExpired {
|
|
log.Print("The panel version is expired, it is recommended to update immediately")
|
|
}
|
|
|
|
switch c.NodeType {
|
|
case "V2ray":
|
|
nodeInfo, err = c.ParseV2rayNodeResponse(nodeInfoResponse)
|
|
case "Trojan":
|
|
nodeInfo, err = c.ParseTrojanNodeResponse(nodeInfoResponse)
|
|
case "Shadowsocks":
|
|
nodeInfo, err = c.ParseSSNodeResponse(nodeInfoResponse)
|
|
case "Shadowsocks-Plugin":
|
|
nodeInfo, err = c.ParseSSPluginNodeResponse(nodeInfoResponse)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
|
}
|
|
} else {
|
|
nodeInfo, err = c.ParseSSPanelNodeInfo(nodeInfoResponse)
|
|
if err != nil {
|
|
res, _ := json.Marshal(nodeInfoResponse)
|
|
return nil, fmt.Errorf("Parse node info failed: %s, \nError: %s, \nPlease check the doc of custom_config for help: https://xrayr-project.github.io/XrayR-doc/dui-jie-sspanel/sspanel/sspanel_custom_config", string(res), err)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
res, _ := json.Marshal(nodeInfoResponse)
|
|
return nil, fmt.Errorf("Parse node info failed: %s, \nError: %s", string(res), err)
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// GetUserList will pull user form ssPanel
|
|
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
|
path := "/mod_mu/users"
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetHeader("If-None-Match", c.eTags["users"]).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Get(path)
|
|
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
|
if res.StatusCode() == 304 {
|
|
return nil, errors.New(api.UserNotModified)
|
|
}
|
|
|
|
if res.Header().Get("ETag") != "" && res.Header().Get("ETag") != c.eTags["users"] {
|
|
c.eTags["users"] = res.Header().Get("ETag")
|
|
}
|
|
|
|
response, err := c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userListResponse := new([]UserResponse)
|
|
|
|
if err := json.Unmarshal(response.Data, userListResponse); err != nil {
|
|
return nil, fmt.Errorf("unmarshal %s failed: %s", reflect.TypeOf(userListResponse), err)
|
|
}
|
|
userList, err := c.ParseUserListResponse(userListResponse)
|
|
if err != nil {
|
|
res, _ := json.Marshal(userListResponse)
|
|
return nil, fmt.Errorf("parse user list failed: %s", string(res))
|
|
}
|
|
return userList, nil
|
|
}
|
|
|
|
// ReportNodeStatus reports the node status to the ssPanel
|
|
func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) {
|
|
// Determine whether a status report is in need
|
|
if compareVersion(c.version, "2023.2") == -1 {
|
|
path := fmt.Sprintf("/mod_mu/nodes/%d/info", c.NodeID)
|
|
systemLoad := SystemLoad{
|
|
Uptime: strconv.FormatUint(nodeStatus.Uptime, 10),
|
|
Load: fmt.Sprintf("%.2f %.2f %.2f", nodeStatus.CPU/100, nodeStatus.Mem/100, nodeStatus.Disk/100),
|
|
}
|
|
|
|
res, err := c.client.R().
|
|
SetBody(systemLoad).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Post(path)
|
|
|
|
_, err = c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReportNodeOnlineUsers reports online user ip
|
|
func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error {
|
|
c.access.Lock()
|
|
defer c.access.Unlock()
|
|
|
|
reportOnline := make(map[int]int)
|
|
data := make([]OnlineUser, len(*onlineUserList))
|
|
for i, user := range *onlineUserList {
|
|
data[i] = OnlineUser{UID: user.UID, IP: user.IP}
|
|
if _, ok := reportOnline[user.UID]; ok {
|
|
reportOnline[user.UID]++
|
|
} else {
|
|
reportOnline[user.UID] = 1
|
|
}
|
|
}
|
|
c.LastReportOnline = reportOnline // Update LastReportOnline
|
|
|
|
postData := &PostData{Data: data}
|
|
path := fmt.Sprintf("/mod_mu/users/aliveip")
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetBody(postData).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Post(path)
|
|
|
|
_, err = c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return 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}
|
|
}
|
|
postData := &PostData{Data: data}
|
|
path := "/mod_mu/users/traffic"
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetBody(postData).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Post(path)
|
|
_, err = c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetNodeRule will pull the audit rule form ssPanel
|
|
func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
|
ruleList := c.LocalRuleList
|
|
path := "/mod_mu/func/detect_rules"
|
|
res, err := c.client.R().
|
|
SetResult(&Response{}).
|
|
SetHeader("If-None-Match", c.eTags["rules"]).
|
|
ForceContentType("application/json").
|
|
Get(path)
|
|
|
|
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
|
if res.StatusCode() == 304 {
|
|
return nil, errors.New(api.RuleNotModified)
|
|
}
|
|
|
|
if res.Header().Get("ETag") != "" && res.Header().Get("ETag") != c.eTags["rules"] {
|
|
c.eTags["rules"] = res.Header().Get("ETag")
|
|
}
|
|
|
|
response, err := c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ruleListResponse := new([]RuleItem)
|
|
|
|
if err := json.Unmarshal(response.Data, ruleListResponse); err != nil {
|
|
return nil, fmt.Errorf("unmarshal %s failed: %s", reflect.TypeOf(ruleListResponse), err)
|
|
}
|
|
|
|
for _, r := range *ruleListResponse {
|
|
ruleList = append(ruleList, api.DetectRule{
|
|
ID: r.ID,
|
|
Pattern: regexp.MustCompile(r.Content),
|
|
})
|
|
}
|
|
return &ruleList, nil
|
|
}
|
|
|
|
// ReportIllegal reports the user illegal behaviors
|
|
func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error {
|
|
|
|
data := make([]IllegalItem, len(*detectResultList))
|
|
for i, r := range *detectResultList {
|
|
data[i] = IllegalItem{
|
|
ID: r.RuleID,
|
|
UID: r.UID,
|
|
}
|
|
}
|
|
postData := &PostData{Data: data}
|
|
path := "/mod_mu/users/detectlog"
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetBody(postData).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Post(path)
|
|
_, err = c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseV2rayNodeResponse parse the response for the given node info format
|
|
func (c *APIClient) ParseV2rayNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
var enableTLS bool
|
|
var path, host, transportProtocol, serviceName, HeaderType string
|
|
var header json.RawMessage
|
|
var speedLimit uint64 = 0
|
|
if nodeInfoResponse.RawServerString == "" {
|
|
return nil, fmt.Errorf("no server info in response")
|
|
}
|
|
// nodeInfo.RawServerString = strings.ToLower(nodeInfo.RawServerString)
|
|
serverConf := strings.Split(nodeInfoResponse.RawServerString, ";")
|
|
|
|
parsedPort, err := strconv.ParseInt(serverConf[1], 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port := uint32(parsedPort)
|
|
|
|
parsedAlterID, err := strconv.ParseInt(serverConf[2], 10, 16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alterID := uint16(parsedAlterID)
|
|
|
|
// Compatible with more node types config
|
|
for _, value := range serverConf[3:5] {
|
|
switch value {
|
|
case "tls":
|
|
enableTLS = true
|
|
default:
|
|
if value != "" {
|
|
transportProtocol = value
|
|
}
|
|
}
|
|
}
|
|
extraServerConf := strings.Split(serverConf[5], "|")
|
|
serviceName = ""
|
|
for _, item := range extraServerConf {
|
|
conf := strings.Split(item, "=")
|
|
key := conf[0]
|
|
if key == "" {
|
|
continue
|
|
}
|
|
value := conf[1]
|
|
switch key {
|
|
case "path":
|
|
rawPath := strings.Join(conf[1:], "=") // In case of the path strings contains the "="
|
|
path = rawPath
|
|
case "host":
|
|
host = value
|
|
case "servicename":
|
|
serviceName = value
|
|
case "headerType":
|
|
HeaderType = value
|
|
}
|
|
}
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8)
|
|
}
|
|
|
|
if HeaderType != "" {
|
|
headers := map[string]string{"type": HeaderType}
|
|
header, err = json.Marshal(headers)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal Header Type %s into config fialed: %s", header, err)
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
AlterID: alterID,
|
|
TransportProtocol: transportProtocol,
|
|
EnableTLS: enableTLS,
|
|
Path: path,
|
|
Host: host,
|
|
EnableVless: c.EnableVless,
|
|
VlessFlow: c.VlessFlow,
|
|
ServiceName: serviceName,
|
|
Header: header,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseSSNodeResponse parse the response for the given node info format
|
|
func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
var port uint32 = 0
|
|
var speedLimit uint64 = 0
|
|
var method string
|
|
path := "/mod_mu/users"
|
|
res, err := c.client.R().
|
|
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
|
|
SetResult(&Response{}).
|
|
ForceContentType("application/json").
|
|
Get(path)
|
|
|
|
response, err := c.parseResponse(res, path, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userListResponse := new([]UserResponse)
|
|
|
|
if err := json.Unmarshal(response.Data, userListResponse); err != nil {
|
|
return nil, fmt.Errorf("unmarshal %s failed: %s", reflect.TypeOf(userListResponse), err)
|
|
}
|
|
|
|
// init server port
|
|
if len(*userListResponse) != 0 {
|
|
port = (*userListResponse)[0].Port
|
|
}
|
|
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8)
|
|
}
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
TransportProtocol: "tcp",
|
|
CypherMethod: method,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseSSPluginNodeResponse parse the response for the given node info format
|
|
func (c *APIClient) ParseSSPluginNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
var enableTLS bool
|
|
var path, host, transportProtocol string
|
|
var speedLimit uint64 = 0
|
|
|
|
serverConf := strings.Split(nodeInfoResponse.RawServerString, ";")
|
|
parsedPort, err := strconv.ParseInt(serverConf[1], 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port := uint32(parsedPort)
|
|
port = port - 1 // Shadowsocks-Plugin requires two ports, one for ss the other for other stream protocol
|
|
if port <= 0 {
|
|
return nil, fmt.Errorf("Shadowsocks-Plugin listen port must bigger than 1")
|
|
}
|
|
// Compatible with more node types config
|
|
for _, value := range serverConf[3:5] {
|
|
switch value {
|
|
case "tls":
|
|
enableTLS = true
|
|
case "ws":
|
|
transportProtocol = "ws"
|
|
case "obfs":
|
|
transportProtocol = "tcp"
|
|
}
|
|
}
|
|
|
|
extraServerConf := strings.Split(serverConf[5], "|")
|
|
for _, item := range extraServerConf {
|
|
conf := strings.Split(item, "=")
|
|
key := conf[0]
|
|
if key == "" {
|
|
continue
|
|
}
|
|
value := conf[1]
|
|
switch key {
|
|
case "path":
|
|
rawPath := strings.Join(conf[1:], "=") // In case of the path strings contains the "="
|
|
path = rawPath
|
|
case "host":
|
|
host = value
|
|
}
|
|
}
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8)
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
TransportProtocol: transportProtocol,
|
|
EnableTLS: enableTLS,
|
|
Path: path,
|
|
Host: host,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseTrojanNodeResponse parse the response for the given node info format
|
|
func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
// 域名或IP;port=连接端口#偏移端口|host=xx
|
|
// gz.aaa.com;port=443#12345|host=hk.aaa.com
|
|
var p, host, outsidePort, insidePort, transportProtocol, serviceName string
|
|
var speedLimit uint64 = 0
|
|
|
|
if nodeInfoResponse.RawServerString == "" {
|
|
return nil, fmt.Errorf("no server info in response")
|
|
}
|
|
if result := firstPortRe.FindStringSubmatch(nodeInfoResponse.RawServerString); len(result) > 1 {
|
|
outsidePort = result[1]
|
|
}
|
|
if result := secondPortRe.FindStringSubmatch(nodeInfoResponse.RawServerString); len(result) > 1 {
|
|
insidePort = result[1]
|
|
}
|
|
if result := hostRe.FindStringSubmatch(nodeInfoResponse.RawServerString); len(result) > 1 {
|
|
host = result[1]
|
|
}
|
|
|
|
if insidePort != "" {
|
|
p = insidePort
|
|
} else {
|
|
p = outsidePort
|
|
}
|
|
|
|
parsedPort, err := strconv.ParseInt(p, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port := uint32(parsedPort)
|
|
|
|
serverConf := strings.Split(nodeInfoResponse.RawServerString, ";")
|
|
extraServerConf := strings.Split(serverConf[1], "|")
|
|
transportProtocol = "tcp"
|
|
serviceName = ""
|
|
for _, item := range extraServerConf {
|
|
conf := strings.Split(item, "=")
|
|
key := conf[0]
|
|
if key == "" {
|
|
continue
|
|
}
|
|
value := conf[1]
|
|
switch key {
|
|
case "grpc":
|
|
transportProtocol = "grpc"
|
|
case "servicename":
|
|
serviceName = value
|
|
}
|
|
}
|
|
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8)
|
|
}
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
TransportProtocol: transportProtocol,
|
|
EnableTLS: true,
|
|
Host: host,
|
|
ServiceName: serviceName,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// ParseUserListResponse parse the response for the given node info format
|
|
func (c *APIClient) ParseUserListResponse(userInfoResponse *[]UserResponse) (*[]api.UserInfo, error) {
|
|
c.access.Lock()
|
|
// Clear Last report log
|
|
defer func() {
|
|
c.LastReportOnline = make(map[int]int)
|
|
c.access.Unlock()
|
|
}()
|
|
|
|
var deviceLimit, localDeviceLimit = 0, 0
|
|
var speedLimit uint64 = 0
|
|
var userList []api.UserInfo
|
|
for _, user := range *userInfoResponse {
|
|
if c.DeviceLimit > 0 {
|
|
deviceLimit = c.DeviceLimit
|
|
} else {
|
|
deviceLimit = user.DeviceLimit
|
|
}
|
|
|
|
// If there is still device available, add the user
|
|
if deviceLimit > 0 && user.AliveIP > 0 {
|
|
lastOnline := 0
|
|
if v, ok := c.LastReportOnline[user.ID]; ok {
|
|
lastOnline = v
|
|
}
|
|
// If there are any available device.
|
|
if localDeviceLimit = deviceLimit - user.AliveIP + lastOnline; localDeviceLimit > 0 {
|
|
deviceLimit = localDeviceLimit
|
|
// If this backend server has reported any user in the last reporting period.
|
|
} else if lastOnline > 0 {
|
|
deviceLimit = lastOnline
|
|
// Remove this user.
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((user.SpeedLimit * 1000000) / 8)
|
|
}
|
|
userList = append(userList, api.UserInfo{
|
|
UID: user.ID,
|
|
UUID: user.UUID,
|
|
Passwd: user.Passwd,
|
|
SpeedLimit: speedLimit,
|
|
DeviceLimit: deviceLimit,
|
|
Port: user.Port,
|
|
Method: user.Method,
|
|
})
|
|
}
|
|
|
|
return &userList, nil
|
|
}
|
|
|
|
// ParseSSPanelNodeInfo parse the response for the given node info format
|
|
// Only available for SSPanel version >= 2021.11
|
|
func (c *APIClient) ParseSSPanelNodeInfo(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
var (
|
|
speedLimit uint64 = 0
|
|
enableTLS, enableVless bool
|
|
alterID uint16 = 0
|
|
transportProtocol string
|
|
)
|
|
|
|
// Check if custom_config is null
|
|
if len(nodeInfoResponse.CustomConfig) == 0 {
|
|
return nil, errors.New("custom_config is empty, disable custom config")
|
|
}
|
|
|
|
nodeConfig := new(CustomConfig)
|
|
err := json.Unmarshal(nodeInfoResponse.CustomConfig, nodeConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("custom_config format error: %v", err)
|
|
}
|
|
|
|
if c.SpeedLimit > 0 {
|
|
speedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
|
} else {
|
|
speedLimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8)
|
|
}
|
|
|
|
parsedPort, err := strconv.ParseInt(nodeConfig.OffsetPortNode, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
port := uint32(parsedPort)
|
|
|
|
switch c.NodeType {
|
|
case "Shadowsocks":
|
|
transportProtocol = "tcp"
|
|
case "V2ray":
|
|
transportProtocol = nodeConfig.Network
|
|
|
|
tlsType := nodeConfig.Security
|
|
if tlsType == "tls" || tlsType == "xtls" {
|
|
enableTLS = true
|
|
}
|
|
|
|
if nodeConfig.EnableVless == "1" {
|
|
enableVless = true
|
|
}
|
|
case "Trojan":
|
|
enableTLS = true
|
|
transportProtocol = "tcp"
|
|
|
|
// Select transport protocol
|
|
if nodeConfig.Network != "" {
|
|
transportProtocol = nodeConfig.Network // try to read transport protocol from config
|
|
}
|
|
}
|
|
|
|
// parse reality config
|
|
realityConfig := new(api.REALITYConfig)
|
|
if nodeConfig.RealityOpts != nil {
|
|
r := nodeConfig.RealityOpts
|
|
realityConfig = &api.REALITYConfig{
|
|
Dest: r.Dest,
|
|
ProxyProtocolVer: r.ProxyProtocolVer,
|
|
ServerNames: r.ServerNames,
|
|
PrivateKey: r.PrivateKey,
|
|
MinClientVer: r.MinClientVer,
|
|
MaxClientVer: r.MaxClientVer,
|
|
MaxTimeDiff: r.MaxTimeDiff,
|
|
ShortIds: r.ShortIds,
|
|
}
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
AlterID: alterID,
|
|
TransportProtocol: transportProtocol,
|
|
Host: nodeConfig.Host,
|
|
Path: nodeConfig.Path,
|
|
EnableTLS: enableTLS,
|
|
EnableVless: enableVless,
|
|
VlessFlow: nodeConfig.Flow,
|
|
CypherMethod: nodeConfig.Method,
|
|
ServiceName: nodeConfig.Servicename,
|
|
Header: nodeConfig.Header,
|
|
EnableREALITY: nodeConfig.EnableREALITY,
|
|
REALITYConfig: realityConfig,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
// compareVersion, version1 > version2 return 1, version1 < version2 return -1, 0 means equal
|
|
func compareVersion(version1, version2 string) int {
|
|
n, m := len(version1), len(version2)
|
|
i, j := 0, 0
|
|
for i < n || j < m {
|
|
x := 0
|
|
for ; i < n && version1[i] != '.'; i++ {
|
|
x = x*10 + int(version1[i]-'0')
|
|
}
|
|
i++ // jump dot
|
|
y := 0
|
|
for ; j < m && version2[j] != '.'; j++ {
|
|
y = y*10 + int(version2[j]-'0')
|
|
}
|
|
j++ // jump dot
|
|
if x > y {
|
|
return 1
|
|
}
|
|
if x < y {
|
|
return -1
|
|
}
|
|
}
|
|
return 0
|
|
}
|