diff --git a/api/api.go b/api/api.go index 88eec06..ecc8902 100644 --- a/api/api.go +++ b/api/api.go @@ -3,12 +3,92 @@ package api +import ( + "github.com/go-resty/resty/v2" + "log" + "strconv" + "sync" + "time" +) + // API is the interface for different panel's api. -type API interface { - GetNodeInfo() (nodeInfo *NodeInfo, err error) - GetUserList() (userList *[]UserInfo, err error) - ReportUserTraffic(userTraffic *[]UserTraffic) (err error) - Describe() ClientInfo - GetNodeRule() (ruleList *[]DetectRule, err error) - Debug() + +type Config struct { + APIHost string `mapstructure:"ApiHost"` + NodeID int `mapstructure:"NodeID"` + Key string `mapstructure:"ApiKey"` + NodeType string `mapstructure:"NodeType"` + EnableVless bool `mapstructure:"EnableVless"` + EnableXTLS bool `mapstructure:"EnableXTLS"` + EnableSS2022 bool `mapstructure:"EnableSS2022"` + Timeout int `mapstructure:"Timeout"` + SpeedLimit float64 `mapstructure:"SpeedLimit"` + DeviceLimit int `mapstructure:"DeviceLimit"` + RuleListPath string `mapstructure:"RuleListPath"` + DisableCustomConfig bool `mapstructure:"DisableCustomConfig"` +} + +type ClientInfo struct { + APIHost string + NodeID int + Key string + NodeType string +} + +type Client struct { + client *resty.Client + APIHost string + NodeID int + Key string + NodeType string + EnableSS2022 bool + EnableVless bool + EnableXTLS bool + SpeedLimit float64 + DeviceLimit int + LocalRuleList []DetectRule + RemoteRuleCache *Rule + access sync.Mutex + NodeInfoRspMd5 [16]byte + UserListCheckNum int +} + +func New(apiConfig *Config) API { + + 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{ + "node_id": strconv.Itoa(apiConfig.NodeID), + "token": apiConfig.Key, + }) + // Read local rule list + localRuleList := readLocalRuleList(apiConfig.RuleListPath) + apiClient := &Client{ + client: client, + NodeID: apiConfig.NodeID, + Key: apiConfig.Key, + APIHost: apiConfig.APIHost, + NodeType: apiConfig.NodeType, + EnableSS2022: apiConfig.EnableSS2022, + EnableVless: apiConfig.EnableVless, + EnableXTLS: apiConfig.EnableXTLS, + SpeedLimit: apiConfig.SpeedLimit, + DeviceLimit: apiConfig.DeviceLimit, + LocalRuleList: localRuleList, + } + return apiClient } diff --git a/api/apimodel.go b/api/apimodel.go deleted file mode 100644 index 44c1d18..0000000 --- a/api/apimodel.go +++ /dev/null @@ -1,125 +0,0 @@ -package api - -import ( - "github.com/xtls/xray-core/infra/conf" - "regexp" -) - -// API config -type Config struct { - APIHost string `mapstructure:"ApiHost"` - NodeID int `mapstructure:"NodeID"` - Key string `mapstructure:"ApiKey"` - NodeType string `mapstructure:"NodeType"` - EnableVless bool `mapstructure:"EnableVless"` - EnableXTLS bool `mapstructure:"EnableXTLS"` - EnableSS2022 bool `mapstructure:"EnableSS2022"` - Timeout int `mapstructure:"Timeout"` - SpeedLimit float64 `mapstructure:"SpeedLimit"` - DeviceLimit int `mapstructure:"DeviceLimit"` - RuleListPath string `mapstructure:"RuleListPath"` - DisableCustomConfig bool `mapstructure:"DisableCustomConfig"` -} - -type OnlineUser struct { - UID int - IP string -} - -type UserTraffic struct { - UID int - Email string - Upload int64 - Download int64 -} - -type ClientInfo struct { - APIHost string - NodeID int - Key string - NodeType string -} - -type DetectRule struct { - ID int - Pattern *regexp.Regexp -} - -type DetectResult struct { - UID int - RuleID int -} - -type V2RayUserInfo struct { - Uuid string `json:"uuid"` - Email string `json:"email"` - AlterId int `json:"alter_id"` -} -type TrojanUserInfo struct { - Password string `json:"password"` -} -type UserInfo struct { - DeviceLimit int `json:"device_limit"` - SpeedLimit uint64 `json:"speed_limit"` - UID int `json:"id"` - Port int `json:"port"` - Cipher string `json:"cipher"` - Secret string `json:"secret"` - V2rayUser *V2RayUserInfo `json:"v2ray_user"` - TrojanUser *TrojanUserInfo `json:"trojan_user"` -} -type UserListBody struct { - //Msg string `json:"msg"` - Data []UserInfo `json:"data"` -} - -func (p *UserInfo) GetUserEmail() string { - if p.V2rayUser != nil { - return p.V2rayUser.Email - } else if p.TrojanUser != nil { - return p.TrojanUser.Password - } - return p.Cipher -} - -type NodeInfo struct { - RspMd5 string - NodeType string - NodeId int - TLSType string - EnableVless bool - EnableTls bool - EnableSS2022 bool - V2ray *V2rayConfig - Trojan *TrojanConfig - SS *SSConfig -} - -type SSConfig struct { - Port int `json:"port"` - TransportProtocol string `json:"transportProtocol"` - CypherMethod string `json:"cypher"` -} -type V2rayConfig struct { - Inbounds []conf.InboundDetourConfig `json:"inbounds"` - Routing *struct { - Rules []Rule `json:"rules"` - } `json:"routing"` -} - -type Rule struct { - Type string `json:"type"` - InboundTag string `json:"inboundTag,omitempty"` - OutboundTag string `json:"outboundTag"` - Domain []string `json:"domain,omitempty"` - Protocol []string `json:"protocol,omitempty"` -} - -type TrojanConfig struct { - LocalPort int `json:"local_port"` - Password []interface{} `json:"password"` - TransportProtocol string - Ssl struct { - Sni string `json:"sni"` - } `json:"ssl"` -} diff --git a/api/interface.go b/api/interface.go new file mode 100644 index 0000000..40e92ab --- /dev/null +++ b/api/interface.go @@ -0,0 +1,10 @@ +package api + +type API interface { + GetNodeInfo() (nodeInfo *NodeInfo, err error) + GetUserList() (userList *[]UserInfo, err error) + ReportUserTraffic(userTraffic *[]UserTraffic) (err error) + Describe() ClientInfo + GetNodeRule() (ruleList *[]DetectRule, err error) + Debug() +} diff --git a/api/node.go b/api/node.go new file mode 100644 index 0000000..a30b47e --- /dev/null +++ b/api/node.go @@ -0,0 +1,234 @@ +package api + +import ( + "bufio" + md52 "crypto/md5" + "fmt" + "github.com/go-resty/resty/v2" + "github.com/goccy/go-json" + "github.com/xtls/xray-core/infra/conf" + "log" + "os" + "regexp" +) + +type DetectRule struct { + ID int + Pattern *regexp.Regexp +} +type DetectResult struct { + UID int + RuleID int +} + +// readLocalRuleList reads the local rule list file +func readLocalRuleList(path string) (LocalRuleList []DetectRule) { + LocalRuleList = make([]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, 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 []DetectRule{} + } + file.Close() + } + + return LocalRuleList +} + +type NodeInfo struct { + DeviceLimit int + SpeedLimit uint64 + NodeType string + NodeId int + TLSType string + EnableVless bool + EnableTls bool + EnableSS2022 bool + V2ray *V2rayConfig + Trojan *TrojanConfig + SS *SSConfig +} + +type SSConfig struct { + Port int `json:"port"` + TransportProtocol string `json:"transportProtocol"` + CypherMethod string `json:"cypher"` +} +type V2rayConfig struct { + Inbounds []conf.InboundDetourConfig `json:"inbounds"` + Routing *struct { + Rules []Rule `json:"rules"` + } `json:"routing"` +} + +type Rule struct { + Type string `json:"type"` + InboundTag string `json:"inboundTag,omitempty"` + OutboundTag string `json:"outboundTag"` + Domain []string `json:"domain,omitempty"` + Protocol []string `json:"protocol,omitempty"` +} + +type TrojanConfig struct { + LocalPort int `json:"local_port"` + Password []interface{} `json:"password"` + TransportProtocol string + Ssl struct { + Sni string `json:"sni"` + } `json:"ssl"` +} + +// GetNodeInfo will pull NodeInfo Config from sspanel +func (c *Client) GetNodeInfo() (nodeInfo *NodeInfo, err error) { + var path string + var res *resty.Response + switch c.NodeType { + case "V2ray": + path = "/api/v1/server/Deepbwork/config" + res, err = c.client.R(). + SetQueryParam("local_port", "1"). + ForceContentType("application/json"). + Get(path) + case "Trojan": + path = "/api/v1/server/TrojanTidalab/config" + case "Shadowsocks": + if nodeInfo, err = c.ParseSSNodeResponse(); err == nil { + return nodeInfo, nil + } else { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) + } + md := md52.Sum(res.Body()) + if c.NodeInfoRspMd5 != [16]byte{} { + if c.NodeInfoRspMd5 == md { + return nil, nil + } + } + c.NodeInfoRspMd5 = md + res, err = c.client.R(). + SetQueryParam("local_port", "1"). + ForceContentType("application/json"). + Get(path) + err = c.checkResponse(res, path, err) + if err != nil { + return nil, err + } + c.access.Lock() + defer c.access.Unlock() + switch c.NodeType { + case "V2ray": + nodeInfo, err = c.ParseV2rayNodeResponse(res.Body()) + case "Trojan": + nodeInfo, err = c.ParseTrojanNodeResponse(res.Body()) + } + return nodeInfo, nil +} + +func (c *Client) GetNodeRule() (*[]DetectRule, error) { + ruleList := c.LocalRuleList + if c.NodeType != "V2ray" { + return &ruleList, nil + } + + // V2board only support the rule for v2ray + // fix: reuse config response + c.access.Lock() + defer c.access.Unlock() + for i, rule := range c.RemoteRuleCache.Domain { + ruleListItem := DetectRule{ + ID: i, + Pattern: regexp.MustCompile(rule), + } + ruleList = append(ruleList, ruleListItem) + } + return &ruleList, nil +} + +// ParseTrojanNodeResponse parse the response for the given nodeinfor format +func (c *Client) ParseTrojanNodeResponse(body []byte) (*NodeInfo, error) { + node := &NodeInfo{Trojan: &TrojanConfig{}} + var err = json.Unmarshal(body, node.Trojan) + if err != nil { + return nil, fmt.Errorf("unmarshal nodeinfo error: %s", err) + } + node.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) + node.DeviceLimit = c.DeviceLimit + node.NodeId = c.NodeID + node.NodeType = c.NodeType + return node, nil +} + +// ParseSSNodeResponse parse the response for the given nodeinfor format +func (c *Client) ParseSSNodeResponse() (*NodeInfo, error) { + var port int + var method string + userInfo, err := c.GetUserList() + if err != nil { + return nil, err + } + if len(*userInfo) > 0 { + port = (*userInfo)[0].Port + method = (*userInfo)[0].Cipher + } + + if err != nil { + return nil, err + } + node := &NodeInfo{ + SpeedLimit: uint64(c.SpeedLimit * 1000000 / 8), + DeviceLimit: c.DeviceLimit, + EnableSS2022: c.EnableSS2022, + NodeType: c.NodeType, + NodeId: c.NodeID, + SS: &SSConfig{ + Port: port, + TransportProtocol: "tcp", + CypherMethod: method, + }, + } + return node, nil +} + +// ParseV2rayNodeResponse parse the response for the given nodeinfor format +func (c *Client) ParseV2rayNodeResponse(body []byte) (*NodeInfo, error) { + node := &NodeInfo{V2ray: &V2rayConfig{}} + err := json.Unmarshal(body, node.V2ray) + if err != nil { + return nil, fmt.Errorf("unmarshal nodeinfo error: %s", err) + } + node.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) + node.DeviceLimit = c.DeviceLimit + node.NodeType = c.NodeType + node.NodeId = c.NodeID + c.RemoteRuleCache = &node.V2ray.Routing.Rules[0] + node.V2ray.Routing = nil + if c.EnableXTLS { + node.TLSType = "xtls" + } else { + node.TLSType = "tls" + } + node.EnableVless = c.EnableVless + node.EnableTls = node.V2ray.Inbounds[0].StreamSetting.Security == "tls" + return node, nil +} diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..a3ee031 --- /dev/null +++ b/api/user.go @@ -0,0 +1,120 @@ +package api + +import ( + "fmt" + "github.com/goccy/go-json" + "strconv" +) + +type OnlineUser struct { + UID int + IP string +} + +type V2RayUserInfo struct { + Uuid string `json:"uuid"` + Email string `json:"email"` + AlterId int `json:"alter_id"` +} +type TrojanUserInfo struct { + Password string `json:"password"` +} +type UserInfo struct { + DeviceLimit int `json:"device_limit"` + SpeedLimit uint64 `json:"speed_limit"` + UID int `json:"id"` + Port int `json:"port"` + Cipher string `json:"cipher"` + Secret string `json:"secret"` + V2rayUser *V2RayUserInfo `json:"v2ray_user"` + TrojanUser *TrojanUserInfo `json:"trojan_user"` +} + +func (p *UserInfo) GetUserEmail() string { + if p.V2rayUser != nil { + return p.V2rayUser.Email + } else if p.TrojanUser != nil { + return p.TrojanUser.Password + } + return p.Cipher +} + +type UserListBody struct { + //Msg string `json:"msg"` + Data []UserInfo `json:"data"` +} + +// GetUserList will pull user form sspanel +func (c *Client) GetUserList() (UserList *[]UserInfo, err error) { + var path string + switch c.NodeType { + case "V2ray": + path = "/api/v1/server/Deepbwork/user" + case "Trojan": + path = "/api/v1/server/TrojanTidalab/user" + case "Shadowsocks": + path = "/api/v1/server/ShadowsocksTidalab/user" + default: + return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) + } + res, err := c.client.R(). + ForceContentType("application/json"). + Get(path) + err = c.checkResponse(res, path, err) + if err != nil { + return nil, err + } + var userList *UserListBody + err = json.Unmarshal(res.Body(), &userList) + if err != nil { + return nil, fmt.Errorf("unmarshal userlist error: %s", err) + } + checkNum := userList.Data[len(userList.Data)-1].UID + + userList.Data[len(userList.Data)/2-1].UID + + userList.Data[0].UID + if c.UserListCheckNum != 0 { + if c.UserListCheckNum == checkNum { + return nil, nil + } + } + c.UserListCheckNum = userList.Data[len(userList.Data)-1].UID + return &userList.Data, nil +} + +type UserTraffic struct { + UID int `json:"user_id"` + Upload int64 `json:"u"` + Download int64 `json:"d"` +} + +// ReportUserTraffic reports the user traffic +func (c *Client) ReportUserTraffic(userTraffic *[]UserTraffic) error { + var path string + switch c.NodeType { + case "V2ray": + path = "/api/v1/server/Deepbwork/submit" + case "Trojan": + path = "/api/v1/server/TrojanTidalab/submit" + case "Shadowsocks": + path = "/api/v1/server/ShadowsocksTidalab/submit" + } + + 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)). + SetBody(data). + ForceContentType("application/json"). + Post(path) + err = c.checkResponse(res, path, err) + if err != nil { + return err + } + return nil +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..497d1bd --- /dev/null +++ b/api/utils.go @@ -0,0 +1,32 @@ +package api + +import ( + "fmt" + "github.com/go-resty/resty/v2" +) + +// Describe return a description of the client +func (c *Client) Describe() ClientInfo { + return ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType} +} + +// Debug set the client debug for client +func (c *Client) Debug() { + c.client.SetDebug(true) +} + +func (c *Client) assembleURL(path string) string { + return c.APIHost + path +} + +func (c *Client) checkResponse(res *resty.Response, path string, err error) error { + if err != nil { + return fmt.Errorf("request %s failed: %s", c.assembleURL(path), err) + } + + if res.StatusCode() > 400 { + body := res.Body() + return fmt.Errorf("request %s failed: %s, %s", c.assembleURL(path), string(body), err) + } + return nil +} diff --git a/api/v2board/model.go b/api/v2board/model.go deleted file mode 100644 index 606995a..0000000 --- a/api/v2board/model.go +++ /dev/null @@ -1,7 +0,0 @@ -package v2board - -type UserTraffic struct { - UID int `json:"user_id"` - Upload int64 `json:"u"` - Download int64 `json:"d"` -} diff --git a/api/v2board/v2board.go b/api/v2board/v2board.go deleted file mode 100644 index 52375bf..0000000 --- a/api/v2board/v2board.go +++ /dev/null @@ -1,337 +0,0 @@ -package v2board - -import ( - "bufio" - md52 "crypto/md5" - "fmt" - "github.com/Yuzuki616/V2bX/api" - "github.com/go-resty/resty/v2" - "github.com/goccy/go-json" - "log" - "os" - "regexp" - "strconv" - "sync" - "time" -) - -// APIClient create an api client to the panel. -type APIClient struct { - client *resty.Client - APIHost string - NodeID int - Key string - NodeType string - EnableSS2022 bool - EnableVless bool - EnableXTLS bool - SpeedLimit float64 - DeviceLimit int - LocalRuleList []api.DetectRule - RemoteRuleCache *api.Rule - access sync.Mutex - NodeInfoRspMd5 [16]byte - UserListCheckNum int -} - -// 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) { - 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{ - "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, - EnableSS2022: apiConfig.EnableSS2022, - EnableVless: apiConfig.EnableVless, - EnableXTLS: apiConfig.EnableXTLS, - SpeedLimit: apiConfig.SpeedLimit, - DeviceLimit: apiConfig.DeviceLimit, - LocalRuleList: localRuleList, - } - 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 []api.DetectRule{} - } - - 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) checkResponse(res *resty.Response, path string, err error) error { - if err != nil { - return fmt.Errorf("request %s failed: %s", c.assembleURL(path), err) - } - - if res.StatusCode() > 400 { - body := res.Body() - return fmt.Errorf("request %s failed: %s, %s", c.assembleURL(path), string(body), err) - } - return nil -} - -// GetNodeInfo will pull NodeInfo Config from sspanel -func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) { - var path string - var res *resty.Response - switch c.NodeType { - case "V2ray": - path = "/api/v1/server/Deepbwork/config" - res, err = c.client.R(). - SetQueryParam("local_port", "1"). - ForceContentType("application/json"). - Get(path) - case "Trojan": - path = "/api/v1/server/TrojanTidalab/config" - case "Shadowsocks": - if nodeInfo, err = c.ParseSSNodeResponse(); err == nil { - return nodeInfo, nil - } else { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) - } - md := md52.Sum(res.Body()) - if c.NodeInfoRspMd5 != [16]byte{} { - if c.NodeInfoRspMd5 == md { - return nil, nil - } - } - c.NodeInfoRspMd5 = md - res, err = c.client.R(). - SetQueryParam("local_port", "1"). - ForceContentType("application/json"). - Get(path) - err = c.checkResponse(res, path, err) - if err != nil { - return nil, err - } - c.access.Lock() - defer c.access.Unlock() - switch c.NodeType { - case "V2ray": - nodeInfo, err = c.ParseV2rayNodeResponse(res.Body()) - case "Trojan": - nodeInfo, err = c.ParseTrojanNodeResponse(res.Body()) - } - return nodeInfo, nil -} - -// GetUserList will pull user form sspanel -func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) { - var path string - switch c.NodeType { - case "V2ray": - path = "/api/v1/server/Deepbwork/user" - case "Trojan": - path = "/api/v1/server/TrojanTidalab/user" - case "Shadowsocks": - path = "/api/v1/server/ShadowsocksTidalab/user" - default: - return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) - } - res, err := c.client.R(). - ForceContentType("application/json"). - Get(path) - err = c.checkResponse(res, path, err) - if err != nil { - return nil, err - } - var userList *api.UserListBody - err = json.Unmarshal(res.Body(), &userList) - if err != nil { - return nil, fmt.Errorf("unmarshal userlist error: %s", err) - } - checkNum := userList.Data[len(userList.Data)-1].UID + - userList.Data[len(userList.Data)/2-1].UID + - userList.Data[0].UID - if c.UserListCheckNum != 0 { - if c.UserListCheckNum == checkNum { - return nil, nil - } - } - c.UserListCheckNum = userList.Data[len(userList.Data)-1].UID - return &userList.Data, nil -} - -// ReportUserTraffic reports the user traffic -func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error { - var path string - switch c.NodeType { - case "V2ray": - path = "/api/v1/server/Deepbwork/submit" - case "Trojan": - path = "/api/v1/server/TrojanTidalab/submit" - case "Shadowsocks": - path = "/api/v1/server/ShadowsocksTidalab/submit" - } - - 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)). - SetBody(data). - ForceContentType("application/json"). - Post(path) - err = c.checkResponse(res, path, 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 - } - - // V2board only support the rule for v2ray - // fix: reuse config response - c.access.Lock() - defer c.access.Unlock() - for i, rule := range c.RemoteRuleCache.Domain { - ruleListItem := api.DetectRule{ - ID: i, - Pattern: regexp.MustCompile(rule), - } - ruleList = append(ruleList, ruleListItem) - } - return &ruleList, nil -} - -// ParseTrojanNodeResponse parse the response for the given nodeinfor format -func (c *APIClient) ParseTrojanNodeResponse(body []byte) (*api.NodeInfo, error) { - node := &api.NodeInfo{Trojan: &api.TrojanConfig{}} - err := json.Unmarshal(body, node.Trojan) - if err != nil { - return nil, fmt.Errorf("unmarshal nodeinfo error: %s", err) - } - node.NodeId = c.NodeID - node.NodeType = c.NodeType - return node, nil -} - -// ParseSSNodeResponse parse the response for the given nodeinfor format -func (c *APIClient) ParseSSNodeResponse() (*api.NodeInfo, error) { - var port int - var method string - userInfo, err := c.GetUserList() - if err != nil { - return nil, err - } - if len(*userInfo) > 0 { - port = (*userInfo)[0].Port - method = (*userInfo)[0].Cipher - } - - if err != nil { - return nil, err - } - node := &api.NodeInfo{ - EnableSS2022: c.EnableSS2022, - NodeType: c.NodeType, - NodeId: c.NodeID, - SS: &api.SSConfig{ - Port: port, - TransportProtocol: "tcp", - CypherMethod: method, - }, - } - return node, nil -} - -// ParseV2rayNodeResponse parse the response for the given nodeinfor format -func (c *APIClient) ParseV2rayNodeResponse(body []byte) (*api.NodeInfo, error) { - node := &api.NodeInfo{V2ray: &api.V2rayConfig{}} - err := json.Unmarshal(body, node.V2ray) - if err != nil { - return nil, fmt.Errorf("unmarshal nodeinfo error: %s", err) - } - node.NodeType = c.NodeType - node.NodeId = c.NodeID - c.RemoteRuleCache = &node.V2ray.Routing.Rules[0] - node.V2ray.Routing = nil - if c.EnableXTLS { - node.TLSType = "xtls" - } else { - node.TLSType = "tls" - } - node.EnableVless = c.EnableVless - node.EnableTls = node.V2ray.Inbounds[0].StreamSetting.Security == "tls" - return node, nil -} diff --git a/api/v2board/v2board_test.go b/api/v2board/v2board_test.go deleted file mode 100644 index 114d386..0000000 --- a/api/v2board/v2board_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package v2board_test - -import ( - "testing" - - "github.com/Yuzuki616/V2bX/api" - "github.com/Yuzuki616/V2bX/api/v2board" -) - -func CreateClient() api.API { - apiConfig := &api.Config{ - APIHost: "http://localhost:9897", - Key: "qwertyuiopasdfghjkl", - NodeID: 1, - NodeType: "V2ray", - } - client := v2board.New(apiConfig) - return client -} - -func TestGetV2rayNodeinfo(t *testing.T) { - client := CreateClient() - nodeInfo, err := client.GetNodeInfo() - if err != nil { - t.Error(err) - } - t.Log(nodeInfo) -} - -func TestGetSSNodeinfo(t *testing.T) { - apiConfig := &api.Config{ - APIHost: "http://127.0.0.1:668", - Key: "qwertyuiopasdfghjkl", - NodeID: 1, - NodeType: "Shadowsocks", - } - client := v2board.New(apiConfig) - nodeInfo, err := client.GetNodeInfo() - if err != nil { - t.Error(err) - } - t.Log(nodeInfo) -} - -func TestGetTrojanNodeinfo(t *testing.T) { - apiConfig := &api.Config{ - APIHost: "http://127.0.0.1:668", - Key: "qwertyuiopasdfghjkl", - NodeID: 1, - NodeType: "Trojan", - } - client := v2board.New(apiConfig) - nodeInfo, err := client.GetNodeInfo() - if err != nil { - t.Error(err) - } - t.Log(nodeInfo) -} - -func TestGetUserList(t *testing.T) { - client := CreateClient() - - userList, err := client.GetUserList() - if err != nil { - t.Error(err) - } - - t.Log(userList) -} - -func TestReportReportUserTraffic(t *testing.T) { - client := CreateClient() - userList, err := client.GetUserList() - if err != nil { - t.Error(err) - } - generalUserTraffic := make([]api.UserTraffic, len(*userList)) - for i, userInfo := range *userList { - generalUserTraffic[i] = api.UserTraffic{ - UID: userInfo.UID, - Upload: 114514, - Download: 114514, - } - } - //client.Debug() - err = client.ReportUserTraffic(&generalUserTraffic) - if err != nil { - t.Error(err) - } -} - -func TestGetNodeRule(t *testing.T) { - client := CreateClient() - client.Debug() - ruleList, err := client.GetNodeRule() - if err != nil { - t.Error(err) - } - - t.Log(ruleList) -} diff --git a/common/limiter/limiter.go b/common/limiter/limiter.go index e0061ef..046669e 100644 --- a/common/limiter/limiter.go +++ b/common/limiter/limiter.go @@ -34,19 +34,25 @@ func New() *Limiter { } } -func (l *Limiter) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo) error { +func (l *Limiter) AddInboundLimiter(tag string, nodeInfo *api.NodeInfo, userList *[]api.UserInfo) error { inboundInfo := &InboundInfo{ Tag: tag, - NodeSpeedLimit: nodeSpeedLimit, + NodeSpeedLimit: nodeInfo.SpeedLimit, BucketHub: new(sync.Map), UserOnlineIP: new(sync.Map), } userMap := new(sync.Map) - for _, u := range *userList { - userMap.Store(fmt.Sprintf("%s|%d|%d", tag, u.Port, u.UID), UserInfo{ - UID: u.UID, - SpeedLimit: u.SpeedLimit, - DeviceLimit: u.DeviceLimit, + for i := range *userList { + if (*userList)[i].SpeedLimit == 0 { + (*userList)[i].SpeedLimit = nodeInfo.SpeedLimit + } + if (*userList)[i].DeviceLimit == 0 { + (*userList)[i].DeviceLimit = nodeInfo.DeviceLimit + } + userMap.Store(fmt.Sprintf("%s|%d|%d", tag, (*userList)[i].Port, (*userList)[i].UID), UserInfo{ + UID: (*userList)[i].UID, + SpeedLimit: (*userList)[i].SpeedLimit, + DeviceLimit: (*userList)[i].DeviceLimit, }) } inboundInfo.UserInfo = userMap @@ -54,12 +60,18 @@ func (l *Limiter) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList return nil } -func (l *Limiter) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo, usersIndex *[]int) error { +func (l *Limiter) UpdateInboundLimiter(tag string, nodeInfo *api.NodeInfo, updatedUserList *[]api.UserInfo, usersIndex *[]int) error { if value, ok := l.InboundInfo.Load(tag); ok { inboundInfo := value.(*InboundInfo) // Update User info for _, u := range *usersIndex { + if (*updatedUserList)[u].SpeedLimit == 0 { + (*updatedUserList)[u].SpeedLimit = nodeInfo.SpeedLimit + } + if (*updatedUserList)[u].DeviceLimit == 0 { + (*updatedUserList)[u].DeviceLimit = nodeInfo.DeviceLimit + } inboundInfo.UserInfo.Store(fmt.Sprintf("%s|%s|%d", tag, (*updatedUserList)[u].GetUserEmail(), (*updatedUserList)[u].UID), UserInfo{ UID: (*updatedUserList)[u].UID, SpeedLimit: (*updatedUserList)[u].SpeedLimit, diff --git a/panel/panel.go b/panel/panel.go index 31a06d9..184bd92 100644 --- a/panel/panel.go +++ b/panel/panel.go @@ -8,7 +8,6 @@ import ( "sync" "github.com/Yuzuki616/V2bX/api" - "github.com/Yuzuki616/V2bX/api/v2board" _ "github.com/Yuzuki616/V2bX/main/distro/all" "github.com/Yuzuki616/V2bX/service" "github.com/Yuzuki616/V2bX/service/controller" @@ -158,7 +157,7 @@ func (p *Panel) Start() { p.Server = server // Load Nodes config for _, nodeConfig := range p.panelConfig.NodesConfig { - var apiClient api.API = v2board.New(nodeConfig.ApiConfig) + var apiClient = api.New(nodeConfig.ApiConfig) var controllerService service.Service // Register controller service controllerConfig := getDefaultControllerConfig() diff --git a/service/controller/control.go b/service/controller/control.go index 45fe3b6..ba855e7 100644 --- a/service/controller/control.go +++ b/service/controller/control.go @@ -129,15 +129,15 @@ func (c *Controller) getTraffic(email string) (up int64, down int64) { } -func (c *Controller) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo) error { +func (c *Controller) AddInboundLimiter(tag string, userList *[]api.UserInfo) error { dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher) - err := dispather.Limiter.AddInboundLimiter(tag, nodeSpeedLimit, userList) + err := dispather.Limiter.AddInboundLimiter(tag, c.nodeInfo, userList) return err } func (c *Controller) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo, usersIndex *[]int) error { dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher) - err := dispather.Limiter.UpdateInboundLimiter(tag, updatedUserList, usersIndex) + err := dispather.Limiter.UpdateInboundLimiter(tag, c.nodeInfo, updatedUserList, usersIndex) return err } diff --git a/service/controller/controller.go b/service/controller/controller.go index f4790ee..ba5be89 100644 --- a/service/controller/controller.go +++ b/service/controller/controller.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "math" + "runtime" "time" "github.com/Yuzuki616/V2bX/api" @@ -64,7 +65,7 @@ func (c *Controller) Start() error { } //sync controller userList c.userList = userInfo - if err := c.AddInboundLimiter(c.Tag, 0, userInfo); err != nil { + if err := c.AddInboundLimiter(c.Tag, userInfo); err != nil { log.Print(err) } // Add Rule Manager @@ -126,12 +127,6 @@ func (c *Controller) nodeInfoMonitor() (err error) { log.Print(err) return nil } - // Update User - newUserInfo, err := c.apiClient.GetUserList() - if err != nil { - log.Print(err) - return nil - } var nodeInfoChanged = false // If nodeInfo changed @@ -171,26 +166,42 @@ func (c *Controller) nodeInfoMonitor() (err error) { } // Check Cert - if c.nodeInfo.V2ray.Inbounds[0].StreamSetting.Security == "tls" && (c.config.CertConfig.CertMode == "dns" || c.config.CertConfig.CertMode == "http") { + if c.nodeInfo.EnableTls && c.config.CertConfig.CertMode != "none" && + (c.config.CertConfig.CertMode == "dns" || c.config.CertConfig.CertMode == "http") { lego, err := legocmd.New() if err != nil { log.Print(err) } // Xray-core supports the OcspStapling certification hot renew - _, _, err = lego.RenewCert(c.config.CertConfig.CertDomain, c.config.CertConfig.Email, c.config.CertConfig.CertMode, c.config.CertConfig.Provider, c.config.CertConfig.DNSEnv) + _, _, err = lego.RenewCert(c.config.CertConfig.CertDomain, c.config.CertConfig.Email, + c.config.CertConfig.CertMode, c.config.CertConfig.Provider, c.config.CertConfig.DNSEnv) if err != nil { log.Print(err) } } + // Update User + newUserInfo, err := c.apiClient.GetUserList() + if err != nil { + log.Print(err) + return nil + } if newUserInfo == nil { return nil } if nodeInfoChanged { + if newUserInfo != nil { + c.userList = newUserInfo + } err = c.addNewUser(newUserInfo, newNodeInfo) if err != nil { log.Print(err) return nil } + // Add Limiter + if err := c.AddInboundLimiter(c.Tag, newUserInfo); err != nil { + log.Print(err) + return nil + } } else { deleted, added := compareUserList(c.userList, newUserInfo) if len(deleted) > 0 { @@ -210,11 +221,6 @@ func (c *Controller) nodeInfoMonitor() (err error) { if err != nil { log.Print(err) } - // Add Limiter - if err := c.AddInboundLimiter(c.Tag, 0, newUserInfo); err != nil { - log.Print(err) - return nil - } // Update Limiter if err := c.UpdateInboundLimiter(c.Tag, newUserInfo, &added); err != nil { log.Print(err) @@ -222,8 +228,9 @@ func (c *Controller) nodeInfoMonitor() (err error) { } log.Printf("[%s: %d] %d user deleted, %d user added", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(deleted), len(added)) + c.userList = newUserInfo } - c.userList = newUserInfo + runtime.GC() return nil } @@ -325,15 +332,15 @@ func (c *Controller) addNewUserFromIndex(userInfo *[]api.UserInfo, userIndex *[] } func compareUserList(old, new *[]api.UserInfo) (deleted, added []int) { - tmp := map[int]int{} - tmp2 := map[int]int{} + tmp := map[int]struct{}{} + tmp2 := map[int]struct{}{} for i := range *old { - tmp[(*old)[i].UID] = i + tmp[(*old)[i].UID] = struct{}{} } l := len(tmp) for i := range *new { - tmp[(*new)[i].UID] = i - tmp2[(*new)[i].UID] = i + tmp[(*new)[i].UID] = struct{}{} + tmp2[(*new)[i].UID] = struct{}{} if l != len(tmp) { added = append(added, i) l++ @@ -342,7 +349,7 @@ func compareUserList(old, new *[]api.UserInfo) (deleted, added []int) { tmp = nil l = len(tmp2) for i := range *old { - tmp2[(*old)[i].UID] = i + tmp2[(*old)[i].UID] = struct{}{} if l != len(tmp2) { deleted = append(deleted, i) l++ @@ -359,7 +366,6 @@ func (c *Controller) userInfoMonitor() (err error) { if up > 0 || down > 0 { userTraffic = append(userTraffic, api.UserTraffic{ UID: user.UID, - Email: user.V2rayUser.Email, Upload: up, Download: down}) } diff --git a/service/controller/inboundbuilder.go b/service/controller/inboundbuilder.go index cdd0e03..b031b9f 100644 --- a/service/controller/inboundbuilder.go +++ b/service/controller/inboundbuilder.go @@ -16,6 +16,7 @@ import ( func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.InboundHandlerConfig, error) { var proxySetting interface{} if nodeInfo.NodeType == "V2ray" { + defer func() { nodeInfo.V2ray = nil }() if nodeInfo.EnableVless { nodeInfo.V2ray.Inbounds[0].Protocol = "vless" // Enable fallback @@ -39,6 +40,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I proxySetting = &conf.VMessInboundConfig{} } } else if nodeInfo.NodeType == "Trojan" { + defer func() { nodeInfo.V2ray = nil; nodeInfo.Trojan = nil }() nodeInfo.V2ray = &api.V2rayConfig{} nodeInfo.V2ray.Inbounds = make([]conf.InboundDetourConfig, 1) nodeInfo.V2ray.Inbounds[0].Protocol = "trojan" @@ -61,6 +63,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I t := conf.TransportProtocol(nodeInfo.SS.TransportProtocol) nodeInfo.V2ray.Inbounds[0].StreamSetting = &conf.StreamConfig{Network: &t} } else if nodeInfo.NodeType == "Shadowsocks" { + defer func() { nodeInfo.V2ray = nil; nodeInfo.SS = nil }() nodeInfo.V2ray = &api.V2rayConfig{} nodeInfo.V2ray.Inbounds = []conf.InboundDetourConfig{{Protocol: "shadowsocks"}} proxySetting = &conf.ShadowsocksServerConfig{}