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.
428 lines
11 KiB
Go
428 lines
11 KiB
Go
package bunpanel
|
|
|
|
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"
|
|
)
|
|
|
|
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
|
|
LastReportOnline map[int]int
|
|
access sync.Mutex
|
|
eTags map[string]string
|
|
}
|
|
|
|
// ReportIllegal implements api.API.
|
|
func (*APIClient) ReportIllegal(detectResultList *[]api.DetectResult) (err error) {
|
|
return nil
|
|
}
|
|
|
|
// ReportNodeStatus implements api.API.
|
|
func (*APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) {
|
|
return nil
|
|
}
|
|
|
|
// GetNodeRule implements api.API.
|
|
func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
|
ruleList := c.LocalRuleList
|
|
return &ruleList, nil
|
|
}
|
|
|
|
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) {
|
|
if v, ok := err.(*resty.ResponseError); ok {
|
|
// 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.SetQueryParams(map[string]string{
|
|
"serverId": strconv.Itoa(apiConfig.NodeID),
|
|
"nodeType": strings.ToLower(apiConfig.NodeType),
|
|
"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
|
|
}
|
|
defer file.Close()
|
|
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.StatusCode != 200 {
|
|
res, _ := json.Marshal(&response)
|
|
return nil, fmt.Errorf("statusCode %s invalid", string(res))
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
|
path := fmt.Sprintf("/v2/server/%d/get", 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(Server)
|
|
|
|
if err := json.Unmarshal(response.Datas, nodeInfoResponse); err != nil {
|
|
return nil, fmt.Errorf("unmarshal %s failed: %s", reflect.TypeOf(nodeInfoResponse), err)
|
|
}
|
|
|
|
nodeInfo, err = c.ParseNodeInfo(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
|
|
}
|
|
|
|
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
|
path := "/v2/user/get"
|
|
res, err := c.client.R().
|
|
SetQueryParam("serverId", 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([]User)
|
|
|
|
if err := json.Unmarshal(response.Datas, 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
|
|
}
|
|
|
|
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}
|
|
reportOnline[user.UID]++
|
|
}
|
|
c.LastReportOnline = reportOnline // Update LastReportOnline
|
|
|
|
postData := &PostData{Data: data}
|
|
path := "/v2/user/online/create"
|
|
res, err := c.client.R().
|
|
SetQueryParam("serverId", 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
|
|
}
|
|
|
|
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 := "/v2/user/data-usage/create"
|
|
res, err := c.client.R().
|
|
SetQueryParam("serverId", 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
|
|
}
|
|
|
|
func (c *APIClient) ParseUserListResponse(userInfoResponse *[]User) (*[]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,
|
|
SpeedLimit: speedLimit,
|
|
DeviceLimit: deviceLimit,
|
|
Passwd: user.UUID,
|
|
Email: user.UUID + "@bunpanel.user",
|
|
})
|
|
}
|
|
|
|
return &userList, nil
|
|
}
|
|
|
|
func (c *APIClient) ParseNodeInfo(nodeInfoResponse *Server) (*api.NodeInfo, error) {
|
|
var (
|
|
speedLimit uint64 = 0
|
|
enableTLS, enableVless, enableREALITY bool
|
|
alterID uint16 = 0
|
|
tlsType, transportProtocol string
|
|
)
|
|
|
|
nodeConfig := nodeInfoResponse
|
|
port := uint32(nodeConfig.Port)
|
|
|
|
switch c.NodeType {
|
|
case "Shadowsocks":
|
|
transportProtocol = "tcp"
|
|
case "V2ray":
|
|
transportProtocol = nodeConfig.Network
|
|
tlsType = nodeConfig.Security
|
|
|
|
if tlsType == "tls" || tlsType == "xtls" {
|
|
enableTLS = true
|
|
}
|
|
if tlsType == "reality" {
|
|
enableREALITY = true
|
|
enableVless = true
|
|
}
|
|
case "Trojan":
|
|
enableTLS = true
|
|
tlsType = "tls"
|
|
transportProtocol = "tcp"
|
|
}
|
|
|
|
// parse reality config
|
|
realityConfig := new(api.REALITYConfig)
|
|
if nodeConfig.RealitySettings != nil {
|
|
r := new(RealitySettings)
|
|
json.Unmarshal(nodeConfig.RealitySettings, r)
|
|
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,
|
|
}
|
|
}
|
|
wsConfig := new(WsSettings)
|
|
if nodeConfig.WsSettings != nil {
|
|
json.Unmarshal(nodeConfig.WsSettings, wsConfig)
|
|
}
|
|
|
|
grpcConfig := new(GrpcSettigns)
|
|
if nodeConfig.GrpcSettings != nil {
|
|
json.Unmarshal(nodeConfig.GrpcSettings, grpcConfig)
|
|
}
|
|
|
|
tcpConfig := new(TcpSettings)
|
|
if nodeConfig.TcpSettings != nil {
|
|
json.Unmarshal(nodeConfig.TcpSettings, tcpConfig)
|
|
}
|
|
|
|
// Create GeneralNodeInfo
|
|
nodeInfo := &api.NodeInfo{
|
|
NodeType: c.NodeType,
|
|
NodeID: c.NodeID,
|
|
Port: port,
|
|
SpeedLimit: speedLimit,
|
|
AlterID: alterID,
|
|
TransportProtocol: transportProtocol,
|
|
Host: wsConfig.Headers.Host,
|
|
Path: wsConfig.Path,
|
|
EnableTLS: enableTLS,
|
|
EnableVless: enableVless,
|
|
VlessFlow: nodeConfig.Flow,
|
|
CypherMethod: nodeConfig.Method,
|
|
ServiceName: grpcConfig.ServiceName,
|
|
Header: tcpConfig.Header,
|
|
EnableREALITY: enableREALITY,
|
|
REALITYConfig: realityConfig,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|