diff --git a/api/aikopanel/aikopanel.go b/api/aikopanel/aikopanel.go new file mode 100644 index 000000000..655d2ce20 --- /dev/null +++ b/api/aikopanel/aikopanel.go @@ -0,0 +1,472 @@ +package aikopanel + +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/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 + LastReportOnline map[int]int + resp atomic.Value + eTags map[string]string +} + +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 { + log.Print(v.Err) + } + }) + client.SetBaseURL(apiConfig.APIHost) + nodeType_for_requests := func() string { + if apiConfig.NodeType == "V2ray" && apiConfig.EnableVless { + return "vless" + } else { + return apiConfig.NodeType + } + }() + + client.SetQueryParams(map[string]string{ + "node_id": strconv.Itoa(apiConfig.NodeID), + "node_type": strings.ToLower(nodeType_for_requests), + "token": apiConfig.Key, + }) + 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 +} + +func readLocalRuleList(path string) (LocalRuleList []api.DetectRule) { + LocalRuleList = make([]api.DetectRule, 0) + + if path != "" { + file, err := os.Open(path) + if err != nil { + log.Printf("Error when opening file: %s", err) + return LocalRuleList + } + defer file.Close() + fileScanner := bufio.NewScanner(file) + for fileScanner.Scan() { + LocalRuleList = append(LocalRuleList, api.DetectRule{ + ID: -1, + Pattern: regexp.MustCompile(fileScanner.Text()), + }) + } + if err := fileScanner.Err(); err != nil { + log.Fatalf("Error while reading file: %s", err) + return + } + } + + return LocalRuleList +} + +func (c *APIClient) Describe() api.ClientInfo { + return api.ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType} +} + +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 +} + +func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) { + server := new(serverConfig) + path := "/api/v1/server/Aiko/config" + + res, err := c.client.R(). + SetHeader("If-None-Match", c.eTags["node"]). + ForceContentType("application/json"). + Get(path) + + 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") + } + + nodeInfoResp, err := c.parseResponse(res, path, err) + if err != nil { + return nil, err + } + b, _ := nodeInfoResp.Encode() + json.Unmarshal(b, server) + + if server.ServerPort == 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/v1/server/Aiko/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) + + 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") + } + + 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") + } + + var deviceLimit, localDeviceLimit int = 0, 0 + var userList []api.UserInfo + for _, user := range users { + u := api.UserInfo{ + UID: user.Id, + UUID: user.Uuid, + } + if c.SpeedLimit > 0 { + u.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) + } else { + u.SpeedLimit = uint64(user.SpeedLimit * 1000000 / 8) + } + if c.DeviceLimit > 0 { + deviceLimit = c.DeviceLimit + } else { + deviceLimit = user.DeviceLimit + } + if deviceLimit > 0 && user.AliveIp > 0 { + lastOnline := 0 + if v, ok := c.LastReportOnline[user.Id]; ok { + lastOnline = v + } + if localDeviceLimit = deviceLimit - user.AliveIp + lastOnline; localDeviceLimit > 0 { + deviceLimit = localDeviceLimit + } else if lastOnline > 0 { + deviceLimit = lastOnline + } else { + continue + } + } + u.DeviceLimit = deviceLimit + u.Email = u.UUID + "@aikopanel.user" + if c.NodeType == "Shadowsocks" { + u.Passwd = u.UUID + } + + userList = append(userList, u) + } + + return &userList, nil +} + +func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error { + path := "/api/v1/server/Aiko/push" + data := make(map[int][]int64, len(*userTraffic)) + for _, traffic := range *userTraffic { + data[traffic.UID] = []int64{traffic.Upload, traffic.Download} + } + res, err := c.client.R().SetBody(data).ForceContentType("application/json").Post(path) + _, err = c.parseResponse(res, path, err) + if err != nil { + return err + } + return nil +} + +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 +} + +func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) { + return nil +} + +func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error { + reportOnline := make(map[int]int) + data := make(map[int][]string) + for _, onlineuser := range *onlineUserList { + data[onlineuser.UID] = append(data[onlineuser.UID], onlineuser.IP) + if _, ok := reportOnline[onlineuser.UID]; ok { + reportOnline[onlineuser.UID]++ + } else { + reportOnline[onlineuser.UID] = 1 + } + } + c.LastReportOnline = reportOnline + + path := "/api/v1/server/Aiko/alive" + res, err := c.client.R().SetBody(data).ForceContentType("application/json").Post(path) + _, err = c.parseResponse(res, path, err) + if err != nil { + return nil + } + + return nil +} + +func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error { + return nil +} + +func (c *APIClient) parseTrojanNodeResponse(s *serverConfig) (*api.NodeInfo, error) { + var ( + host string + header json.RawMessage + ) + transportProtocol := func() string { + if s.Network == "" { + return "tcp" + } else { + return s.Network + } + }() + switch transportProtocol { + case "ws": + if s.NetworkSettings.Headers != nil { + if httpHeader, err := s.NetworkSettings.Headers.MarshalJSON(); err != nil { + return nil, err + } else { + b, _ := simplejson.NewJson(httpHeader) + host = b.Get("Host").MustString() + } + } + case "tcp": + if s.NetworkSettings.Header != nil { + if httpHeader, err := s.NetworkSettings.Header.MarshalJSON(); err != nil { + return nil, err + } else { + header = httpHeader + } + } + } + nodeInfo := &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: uint32(s.ServerPort), + TransportProtocol: transportProtocol, + Path: s.NetworkSettings.Path, + EnableTLS: true, + Host: host, + Header: header, + ServiceName: s.NetworkSettings.ServiceName, + SpeedLimit: uint64(s.SpeedLimit * 1000000 / 8), + } + return nodeInfo, nil +} + +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() + } + return &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: uint32(s.ServerPort), + TransportProtocol: "tcp", + CypherMethod: s.Cipher, + ServerKey: s.ServerKey, + Header: header, + SpeedLimit: uint64(s.SpeedLimit * 1000000 / 8), + }, nil +} + +func (c *APIClient) parseV2rayNodeResponse(s *serverConfig) (*api.NodeInfo, error) { + var ( + host string + header json.RawMessage + enableTLS bool + enableREALITY bool + ) + var dest string + if s.TlsSettings.Dest != "" { + dest = s.TlsSettings.Dest + } else { + dest = s.TlsSettings.Sni + } + realityconfig := api.REALITYConfig{ + Dest: dest + ":" + s.TlsSettings.ServerPort, + ProxyProtocolVer: s.TlsSettings.Xver, + ServerNames: []string{s.TlsSettings.Sni}, + PrivateKey: s.TlsSettings.PrivateKey, + ShortIds: []string{s.TlsSettings.ShortId}, + } + switch s.Network { + case "ws": + if s.NetworkSettings.Headers != nil { + if httpHeader, err := s.NetworkSettings.Headers.MarshalJSON(); err != nil { + return nil, err + } else { + b, _ := simplejson.NewJson(httpHeader) + host = b.Get("Host").MustString() + } + } + case "tcp": + if s.NetworkSettings.Header != nil { + if httpHeader, err := s.NetworkSettings.Header.MarshalJSON(); err != nil { + return nil, err + } else { + header = httpHeader + } + } + case "h2": + if s.NetworkSettings.Header != nil { + if httpHeader, err := s.NetworkSettings.Header.MarshalJSON(); err != nil { + return nil, err + } else { + header = httpHeader + } + } + if s.NetworkSettings.Host != "" { + host = s.NetworkSettings.Host + } else { + host = "aikopanel.com" + } + } + + if s.Tls != 0 { + enableTLS = true + if s.Tls == 2 { + enableREALITY = true + } + } + + return &api.NodeInfo{ + NodeType: c.NodeType, + NodeID: c.NodeID, + Port: uint32(s.ServerPort), + AlterID: 0, + TransportProtocol: s.Network, + EnableTLS: enableTLS, + Path: s.NetworkSettings.Path, + Host: host, + EnableVless: c.EnableVless, + VlessFlow: s.VlessFlow, + REALITYConfig: &realityconfig, + EnableREALITY: enableREALITY, + ServiceName: s.NetworkSettings.ServiceName, + Header: header, + SpeedLimit: uint64(s.SpeedLimit * 1000000 / 8), + }, nil +} diff --git a/api/aikopanel/aikopanel_test.go b/api/aikopanel/aikopanel_test.go new file mode 100644 index 000000000..984dbb2a8 --- /dev/null +++ b/api/aikopanel/aikopanel_test.go @@ -0,0 +1,101 @@ +package aikopanel_test + +import ( + "testing" + + "github.com/XrayR-project/XrayR/api" + "github.com/XrayR-project/XrayR/api/aikopanel" +) + +func CreateClient() api.API { + apiConfig := &api.Config{ + APIHost: "http://localhost:9897", + Key: "aikopanelaikopanelaikopanel", + NodeID: 1, + NodeType: "V2ray", + } + client := aikopanel.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: "aikopanelaikopanelaikopanel", + NodeID: 1, + NodeType: "Shadowsocks", + } + client := aikopanel.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: "aikopanelaikopanelaikopanel", + NodeID: 1, + NodeType: "Trojan", + } + client := aikopanel.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/api/aikopanel/model.go b/api/aikopanel/model.go new file mode 100644 index 000000000..aea05c902 --- /dev/null +++ b/api/aikopanel/model.go @@ -0,0 +1,70 @@ +package aikopanel + +import ( + "encoding/json" +) + +type serverConfig struct { + shadowsocks + v2ray + trojan + + ServerPort int `json:"server_port"` + SpeedLimit int `json:"speedlimit"` + BaseConfig struct { + PushInterval int `json:"push_interval"` + PullInterval int `json:"pull_interval"` + } `json:"base_config"` + Routes []route `json:"routes"` +} + +type shadowsocks struct { + Cipher string `json:"cipher"` + Obfs string `json:"obfs"` + ObfsSettings struct { + Path string `json:"path"` + Host string `json:"host"` + } `json:"obfs_settings"` + ServerKey string `json:"server_key"` +} + +type v2ray struct { + Network string `json:"network"` + NetworkSettings struct { + Path string `json:"path"` + Headers *json.RawMessage `json:"headers"` + ServiceName string `json:"serviceName"` + Header *json.RawMessage `json:"header"` + Host string `json:"host"` + } `json:"networkSettings"` + VlessFlow string `json:"flow"` + TlsSettings struct { + ServerPort string `json:"server_port"` + Dest string `json:"dest"` + Xver uint64 `json:"xver,string"` + Sni string `json:"server_name"` + PrivateKey string `json:"private_key"` + ShortId string `json:"short_id"` + } `json:"tls_settings"` + Tls int `json:"tls"` +} + +type trojan struct { + Host string `json:"host"` + ServerName string `json:"server_name"` +} + +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"` + DeviceLimit int `json:"device_limit"` + AliveIp int `json:"alive_ip"` +} diff --git a/panel/panel.go b/panel/panel.go index 69c8fca30..c90cfac44 100644 --- a/panel/panel.go +++ b/panel/panel.go @@ -15,6 +15,7 @@ import ( "github.com/xtls/xray-core/infra/conf" "github.com/XrayR-project/XrayR/api" + "github.com/XrayR-project/XrayR/api/aikopanel" "github.com/XrayR-project/XrayR/api/bunpanel" "github.com/XrayR-project/XrayR/api/gov2panel" "github.com/XrayR-project/XrayR/api/newV2board" @@ -188,6 +189,8 @@ func (p *Panel) Start() { apiClient = gov2panel.New(nodeConfig.ApiConfig) case "BunPanel": apiClient = bunpanel.New(nodeConfig.ApiConfig) + case "AikoPanel": + apiClient = aikopanel.New(nodeConfig.ApiConfig) default: log.Panicf("Unsupport panel type: %s", nodeConfig.PanelType) } diff --git a/release/config/config.yml.example b/release/config/config.yml.example index 4f6e540a0..409152f73 100644 --- a/release/config/config.yml.example +++ b/release/config/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, GoV2Panel, BunPanel + - PanelType: "SSpanel" # Panel type: SSpanel, NewV2board, PMpanel, Proxypanel, V2RaySocks, GoV2Panel, BunPanel, AikoPanel ApiConfig: ApiHost: "http://127.0.0.1:667" ApiKey: "123"