mirror of
https://github.com/XrayR-project/XrayR.git
synced 2025-06-09 05:49:55 +00:00

Fixed typos, changed variable naming to follow camel case convention, simplified the error handling, and refactored the function names in sspanel.go for improved readability and consistency. Accordingly, updated the test function names in sspanel_test.go to match the changes made in the main file. Additionally, the if condition in ParseSSPanelNodeInfo was streamlined to improve the function's readability and error handling. The changes will help in better understanding the code while debugging or adding new features.
861 lines
23 KiB
Go
861 lines
23 KiB
Go
package sspanel
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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)
|
|
|
|
// 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) (*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, %s", 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)
|
|
}
|
|
|
|
// New ssPanel API
|
|
c.version = nodeInfoResponse.Version
|
|
if !c.DisableCustomConfig {
|
|
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)
|
|
}
|
|
} else {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
// Find the multi-user
|
|
for _, u := range *userListResponse {
|
|
if u.MultiUser > 0 {
|
|
port = u.Port
|
|
method = u.Method
|
|
break
|
|
}
|
|
}
|
|
if port == 0 || method == "" {
|
|
return nil, fmt.Errorf("cant find the single port multi user")
|
|
}
|
|
|
|
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 int = 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,
|
|
Email: user.Email,
|
|
UUID: user.UUID,
|
|
Passwd: user.Passwd,
|
|
SpeedLimit: speedLimit,
|
|
DeviceLimit: deviceLimit,
|
|
Port: user.Port,
|
|
Method: user.Method,
|
|
Protocol: user.Protocol,
|
|
ProtocolParam: user.ProtocolParam,
|
|
Obfs: user.Obfs,
|
|
ObfsParam: user.ObfsParam,
|
|
})
|
|
}
|
|
|
|
return &userList, nil
|
|
}
|
|
|
|
// ParseSSPanelNodeInfo parse the response for the given node info format
|
|
// Only used for SSPanel version >= 2021.11
|
|
func (c *APIClient) ParseSSPanelNodeInfo(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) {
|
|
var speedLimit uint64 = 0
|
|
var EnableTLS, EnableVless bool
|
|
var AlterID uint16 = 0
|
|
var TLSType, transportProtocol string
|
|
|
|
if nodeInfoResponse.Version == "" {
|
|
return nil, errors.New("panel version must be 2021.11 or above")
|
|
}
|
|
|
|
// Check if custom_config is valid
|
|
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 is 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)
|
|
|
|
if c.NodeType == "Shadowsocks" {
|
|
transportProtocol = "tcp"
|
|
}
|
|
|
|
if c.NodeType == "V2ray" {
|
|
transportProtocol = nodeConfig.Network
|
|
TLSType = nodeConfig.Security
|
|
if parsedAlterID, err := strconv.ParseInt(nodeConfig.AlterID, 10, 16); err != nil {
|
|
return nil, err
|
|
} else {
|
|
AlterID = uint16(parsedAlterID)
|
|
}
|
|
|
|
if TLSType == "tls" || TLSType == "xtls" {
|
|
EnableTLS = true
|
|
}
|
|
if nodeConfig.EnableVless == "1" {
|
|
EnableVless = true
|
|
}
|
|
}
|
|
|
|
if c.NodeType == "Trojan" {
|
|
EnableTLS = true
|
|
TLSType = "tls"
|
|
transportProtocol = "tcp"
|
|
|
|
// Select security type
|
|
if nodeConfig.EnableXtls == "1" {
|
|
TLSType = "xtls"
|
|
} else if nodeConfig.Security != "" {
|
|
TLSType = nodeConfig.Security // try to read security from config
|
|
}
|
|
|
|
// Select transport protocol
|
|
if nodeConfig.Grpc == "1" {
|
|
transportProtocol = "grpc"
|
|
} else if nodeConfig.Network != "" {
|
|
transportProtocol = nodeConfig.Network // try to read transport protocol from config
|
|
}
|
|
}
|
|
|
|
// 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.MuEncryption,
|
|
ServiceName: nodeConfig.Servicename,
|
|
Header: nodeConfig.Header,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
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
|
|
}
|