diff --git a/README.md b/README.md index f7b6bccb..9c2724e7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ A Xray backend framework that can easily support many panels. | [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ | | [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ | | [WHMCS (V2RaySocks)](https://v2raysocks.doxtex.com/) | √ | √ | √ | +| [GoV2Panel](https://github.com/pingProMax/gov2panel) | √ | √ | √ | ## 软件安装 diff --git a/api/gov2panel/gov2panel.go b/api/gov2panel/gov2panel.go new file mode 100644 index 00000000..58c61990 --- /dev/null +++ b/api/gov2panel/gov2panel.go @@ -0,0 +1,402 @@ +package gov2panel + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "regexp" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/bitly/go-simplejson" + "github.com/go-resty/resty/v2" + "github.com/gogf/gf/v2/util/gconv" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/infra/conf" + + "github.com/XrayR-project/XrayR/api" +) + +// APIClient create an 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 + LocalRuleList []api.DetectRule + resp atomic.Value + eTags map[string]string +} + +// 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), + "node_type": 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) + 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) (*simplejson.Json, error) { + if err != nil { + return nil, fmt.Errorf("request %s failed: %v", c.assembleURL(path), err) + } + + if res.StatusCode() > 399 { + return nil, fmt.Errorf("request %s failed: %s, %v", c.assembleURL(path), res.String(), err) + } + + rtn, err := simplejson.NewJson(res.Body()) + if err != nil { + return nil, fmt.Errorf("ret %s invalid", res.String()) + } + + return rtn, nil +} + +// GetNodeInfo will pull NodeInfo Config from panel +func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) { + server := new(serverConfig) + path := "/api/server/config" + + res, err := c.client.R(). + 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) + } + // update etag + if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["node"] { + c.eTags["node"] = res.Header().Get("Etag") + } + + nodeInfoResp, err := c.parseResponse(res, path, err) + if err != nil { + return nil, err + } + b, _ := nodeInfoResp.Encode() + json.Unmarshal(b, server) + + if gconv.Uint32(server.Port) == 0 { + return nil, errors.New("server port must > 0") + } + + c.resp.Store(server) + + switch c.NodeType { + case "V2ray": + nodeInfo, err = c.parseV2rayNodeResponse(server) + case "Trojan": + nodeInfo, err = c.parseTrojanNodeResponse(server) + case "Shadowsocks": + nodeInfo, err = c.parseSSNodeResponse(server) + default: + return nil, fmt.Errorf("unsupported node type: %s", c.NodeType) + } + + if err != nil { + return nil, fmt.Errorf("parse node info failed: %s, \nError: %v", res.String(), err) + } + + return nodeInfo, nil +} + +// GetUserList will pull user form panel +func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) { + var users []*user + path := "/api/server/user" + + switch c.NodeType { + case "V2ray", "Trojan", "Shadowsocks": + break + default: + return nil, fmt.Errorf("unsupported node type: %s", c.NodeType) + } + + res, err := c.client.R(). + SetHeader("If-None-Match", c.eTags["users"]). + 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) + } + // update etag + if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["users"] { + c.eTags["users"] = res.Header().Get("Etag") + } + + usersResp, err := c.parseResponse(res, path, err) + if err != nil { + return nil, err + } + b, _ := usersResp.Get("users").Encode() + json.Unmarshal(b, &users) + if len(users) == 0 { + return nil, errors.New("users is null") + } + + userList := make([]api.UserInfo, len(users)) + for i := 0; i < len(users); i++ { + u := api.UserInfo{ + UID: users[i].Id, + UUID: users[i].Uuid, + } + + // Support 1.7.1 speed limit + if c.SpeedLimit > 0 { + u.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) + } else { + u.SpeedLimit = uint64(users[i].SpeedLimit * 1000000 / 8) + } + + u.DeviceLimit = c.DeviceLimit // todo waiting v2board send configuration + u.Email = u.UUID + "@gov2panel.user" + if c.NodeType == "Shadowsocks" { + u.Passwd = u.UUID + } + userList[i] = u + } + + return &userList, nil +} + +// ReportUserTraffic reports the user traffic +func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error { + path := "/api/server/push" + + res, err := c.client.R().SetBody(userTraffic).ForceContentType("application/json").Post(path) + _, err = c.parseResponse(res, path, err) + if err != nil { + return err + } + + return nil +} + +// GetNodeRule implements the API interface +func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) { + routes := c.resp.Load().(*serverConfig).Routes + + ruleList := c.LocalRuleList + + for i := range routes { + if routes[i].Action == "block" { + + ruleList = append(ruleList, api.DetectRule{ + ID: i, + Pattern: regexp.MustCompile(strings.Join(routes[i].Match, "|")), + }) + } + } + + return &ruleList, nil +} + +// ReportNodeStatus implements the API interface +func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) { + return nil +} + +// ReportNodeOnlineUsers implements the API interface +func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error { + return nil +} + +// ReportIllegal implements the API interface +func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error { + return nil +} + +// parseTrojanNodeResponse parse the response for the given nodeInfo format +func (c *APIClient) parseTrojanNodeResponse(s *serverConfig) (*api.NodeInfo, error) { + // Create GeneralNodeInfo + nodeInfo := &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: gconv.Uint32(s.Port), + TransportProtocol: "tcp", + EnableTLS: true, + Host: s.Host, + ServiceName: s.Sni, + NameServerConfig: s.parseDNSConfig(), + } + return nodeInfo, nil +} + +// parseSSNodeResponse parse the response for the given nodeInfo format +func (c *APIClient) parseSSNodeResponse(s *serverConfig) (*api.NodeInfo, error) { + var header json.RawMessage + + if s.Obfs == "http" { + path := "/" + if p := s.ObfsSettings.Path; p != "" { + if strings.HasPrefix(p, "/") { + path = p + } else { + path += p + } + } + h := simplejson.New() + h.Set("type", "http") + h.SetPath([]string{"request", "path"}, path) + header, _ = h.Encode() + } + // Create GeneralNodeInfo + return &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: gconv.Uint32(s.Port), + TransportProtocol: "tcp", + CypherMethod: s.Encryption, + ServerKey: s.ServerKey, // shadowsocks2022 share key + NameServerConfig: s.parseDNSConfig(), + Header: header, + }, nil +} + +// parseV2rayNodeResponse parse the response for the given nodeInfo format +func (c *APIClient) parseV2rayNodeResponse(s *serverConfig) (*api.NodeInfo, error) { + var ( + header json.RawMessage + enableTLS bool + ) + + switch s.Net { + case "tcp": + if s.Header != nil { + if httpHeader, err := s.Header.MarshalJSON(); err != nil { + return nil, err + } else { + header = httpHeader + } + } + } + + if s.TLS == "tls" { + enableTLS = true + } + + // Create GeneralNodeInfo + return &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: gconv.Uint32(s.Port), + AlterID: 0, + TransportProtocol: s.Net, + EnableTLS: enableTLS, + Path: s.Path, + Host: s.Host, + EnableVless: c.EnableVless, + VlessFlow: c.VlessFlow, + ServiceName: s.Sni, + Header: header, + NameServerConfig: s.parseDNSConfig(), + }, nil +} + +func (s *serverConfig) parseDNSConfig() (nameServerList []*conf.NameServerConfig) { + for i := range s.Routes { + if s.Routes[i].Action == "dns" { + nameServerList = append(nameServerList, &conf.NameServerConfig{ + Address: &conf.Address{Address: net.ParseAddress(s.Routes[i].ActionValue)}, + Domains: s.Routes[i].Match, + }) + } + } + + return +} diff --git a/api/gov2panel/gov2panel_test.go b/api/gov2panel/gov2panel_test.go new file mode 100644 index 00000000..9508e530 --- /dev/null +++ b/api/gov2panel/gov2panel_test.go @@ -0,0 +1,101 @@ +package gov2panel_test + +import ( + "testing" + + "github.com/XrayR-project/XrayR/api" + "github.com/XrayR-project/XrayR/api/gov2panel" +) + +func CreateClient() api.API { + apiConfig := &api.Config{ + APIHost: "http://localhost:8080", + Key: "123456", + NodeID: 1, + NodeType: "V2ray", + } + client := gov2panel.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 := gov2panel.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 := gov2panel.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: 1111, + Download: 2222, + } + } + // 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/api/gov2panel/model.go b/api/gov2panel/model.go new file mode 100644 index 00000000..a20b33c6 --- /dev/null +++ b/api/gov2panel/model.go @@ -0,0 +1,46 @@ +package gov2panel + +import "encoding/json" + +type serverConfig struct { + v2ray + shadowsocks + //--- + Routes []route `json:"routes"` + Header *json.RawMessage `json:"header"` +} + +type v2ray struct { + Port string `json:"port"` + Scy string `json:"scy"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + Path string `json:"path"` + TLS string `json:"tls"` + Sni string `json:"sni"` + Alpn string `json:"alpn"` +} + +type shadowsocks struct { + Encryption string `json:"encryption"` + Obfs string `json:"obfs"` + ObfsSettings struct { + Path string `json:"path"` + Host string `json:"host"` + } `json:"obfs_settings"` + ServerKey string `json:"server_key"` +} + +type route struct { + Id int `json:"id"` + Match []string `json:"match"` + Action string `json:"action"` + ActionValue string `json:"action_value"` +} + +type user struct { + Id int `json:"id"` + Uuid string `json:"uuid"` + SpeedLimit int `json:"speed_limit"` +} diff --git a/go.mod b/go.mod index 4584a35f..93e5d70e 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/go-acme/lego/v4 v4.14.2 github.com/go-resty/resty/v2 v2.9.1 + github.com/gogf/gf/v2 v2.5.4 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/r3labs/diff/v2 v2.15.1 github.com/redis/go-redis/v9 v9.2.1 @@ -202,6 +203,8 @@ require ( github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect diff --git a/go.sum b/go.sum index 5aeea025..8baf6b5f 100644 --- a/go.sum +++ b/go.sum @@ -298,6 +298,8 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVr github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogf/gf/v2 v2.5.4 h1:UBCSw8mInkHmEqL0E1LYc6QhSpaNFY/wHcFrTI/rzTk= +github.com/gogf/gf/v2 v2.5.4/go.mod h1:7yf5qp0BznfsYx7Sw49m3mQvBsHpwAjJk3Q9ZnKoUEc= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= @@ -911,6 +913,10 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/main/config.yml.example b/main/config.yml.example index c82848a1..b16de6ce 100644 --- a/main/config.yml.example +++ b/main/config.yml.example @@ -13,7 +13,7 @@ ConnectionConfig: DownlinkOnly: 4 # Time limit when the connection is closed after the uplink is closed, Second BufferSize: 64 # The internal cache size of each connection, kB Nodes: - - PanelType: "SSpanel" # Panel type: SSpanel, NewV2board, PMpanel, Proxypanel, V2RaySocks + - PanelType: "SSpanel" # Panel type: SSpanel, NewV2board, PMpanel, Proxypanel, V2RaySocks, GoV2Panel ApiConfig: ApiHost: "http://127.0.0.1:667" ApiKey: "123" @@ -78,7 +78,7 @@ Nodes: ALICLOUD_ACCESS_KEY: aaa ALICLOUD_SECRET_KEY: bbb -# - PanelType: "SSpanel" # Panel type: SSpanel, V2board, NewV2board, PMpanel, Proxypanel, V2RaySocks +# - PanelType: "SSpanel" # Panel type: SSpanel, V2board, NewV2board, PMpanel, Proxypanel, V2RaySocks, GoV2Panel # ApiConfig: # ApiHost: "http://127.0.0.1:668" # ApiKey: "123" diff --git a/panel/panel.go b/panel/panel.go index 8ccf3021..d4a0ef72 100644 --- a/panel/panel.go +++ b/panel/panel.go @@ -6,6 +6,7 @@ import ( "os" "sync" + "github.com/XrayR-project/XrayR/api/gov2panel" "github.com/XrayR-project/XrayR/api/newV2board" "github.com/XrayR-project/XrayR/app/mydispatcher" @@ -184,6 +185,8 @@ func (p *Panel) Start() { apiClient = proxypanel.New(nodeConfig.ApiConfig) case "V2RaySocks": apiClient = v2raysocks.New(nodeConfig.ApiConfig) + case "GoV2Panel": + apiClient = gov2panel.New(nodeConfig.ApiConfig) default: log.Panicf("Unsupport panel type: %s", nodeConfig.PanelType) }