XrayR/api/bunpanel/bunpanel.go
Senis 115d7bad6f
Replace standard log package with logrus
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.
2023-12-28 13:40:31 +08:00

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
}