package pmpanel import ( "bufio" "encoding/json" "fmt" "log" "os" "reflect" "regexp" "strconv" "time" "github.com/XrayR-project/XrayR/api" "github.com/go-resty/resty/v2" ) // APIClient create a api client to the panel. type APIClient struct { client *resty.Client APIHost string NodeID int Key string NodeType string EnableVless bool EnableXTLS bool SpeedLimit float64 DeviceLimit int LocalRuleList []api.DetectRule } // New creat a 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.SetHeaders(map[string]string{ "key": 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, 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 make([]api.DetectRule, 0) } 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) 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, %s", c.assembleURL(path), string(body), err) } response := res.Result().(*Response) if response.Ret != 200 { res, _ := json.Marshal(&response) return nil, fmt.Errorf("Ret %s invalid", string(res)) } return response, nil } // GetNodeInfo will pull NodeInfo Config from sspanel func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) { path := fmt.Sprintf("/api/node") var nodeType = "" switch c.NodeType { case "Shadowsocks": nodeType = "ss" case "V2ray": nodeType = "v2ray" case "Trojan": nodeType = "trojan" default: return nil, fmt.Errorf("NodeType Error: %s", c.NodeType) } // body := fmt.Sprintf(`{"type":"%s", "nodeId":%d}`, nodeType, c.NodeID) res, err := c.client.R(). SetQueryParams(map[string]string{ "type": nodeType, "nodeId": strconv.Itoa(c.NodeID), }). SetResult(&Response{}). ForceContentType("application/json"). Get(path) response, err := c.parseResponse(res, path, err) if err != nil { return nil, err } nodeInfoResponse := new(NodeInfoResponse) if err := json.Unmarshal(response.Data, nodeInfoResponse); err != nil { return nil, fmt.Errorf("Unmarshal %s failed: %s", reflect.TypeOf(nodeInfoResponse), err) } switch c.NodeType { case "V2ray": nodeInfo, err = c.ParseV2rayNodeResponse(nodeInfoResponse) case "Trojan": nodeInfo, err = c.ParseTrojanNodeResponse(nodeInfoResponse) case "Shadowsocks": nodeInfo, err = c.ParseSSNodeResponse(nodeInfoResponse) default: return nil, fmt.Errorf("Unsupported Node type: %s", c.NodeType) } if err != nil { res, _ := json.Marshal(nodeInfoResponse) return nil, fmt.Errorf("Parse node info failed: %s, \nError: %s", string(res), err) } return nodeInfo, nil } // GetUserList will pull user form sspanel func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) { path := "/api/users" var nodeType = "" switch c.NodeType { case "Shadowsocks": nodeType = "ss" case "V2ray": nodeType = "v2ray" case "Trojan": nodeType = "trojan" default: return nil, fmt.Errorf("NodeType Error: %s", c.NodeType) } res, err := c.client.R(). SetQueryParams(map[string]string{ "type": nodeType, "nodeId": strconv.Itoa(c.NodeID), "all": "true", }). SetResult(&Response{}). ForceContentType("application/json"). Get(path) response, err := c.parseResponse(res, path, err) if err != nil { return nil, err } var userListResponse *[]UserResponse if err := json.Unmarshal(response.Data, &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 } // ReportNodeStatus reports the node status to the sspanel func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) { return nil } //ReportNodeOnlineUsers reports online user ip func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error { var nodeType = "" switch c.NodeType { case "Shadowsocks": nodeType = "ss" case "V2ray": nodeType = "v2ray" case "Trojan": nodeType = "trojan" default: return fmt.Errorf("NodeType Error: %s", c.NodeType) } data := make([]OnlineUser, len(*onlineUserList)) for i, user := range *onlineUserList { data[i] = OnlineUser{UID: user.UID, IP: user.IP} } postData := &PostData{Type: nodeType, NodeId: c.NodeID, Onlines: data} path := "/api/online" res, err := c.client.R(). SetHeader("Content-Type", "application/json"). SetBody(postData). SetResult(&Response{}). ForceContentType("application/json"). Post(path) _, err = c.parseResponse(res, path, err) if err != nil { return err } return nil } // ReportUserTraffic reports the user traffic func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error { var nodeType = "" switch c.NodeType { case "Shadowsocks": nodeType = "ss" case "V2ray": nodeType = "v2ray" case "Trojan": nodeType = "trojan" default: return fmt.Errorf("NodeType Error: %s", c.NodeType) } data := make([]UserTraffic, len(*userTraffic)) for i, traffic := range *userTraffic { data[i] = UserTraffic{ UID: traffic.UID, Upload: traffic.Upload, Download: traffic.Download, } } postData := &PostData{Type: nodeType, NodeId: c.NodeID, Users: data} path := "/api/traffic" res, err := c.client.R(). SetHeader("Content-Type", "application/json"). SetBody(postData). SetResult(&Response{}). ForceContentType("application/json"). Post(path) _, err = c.parseResponse(res, path, err) if err != nil { return err } return nil } // GetNodeRule will pull the audit rule form pmpanel func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) { ruleList := c.LocalRuleList path := "/api/rules" var nodeType = "" switch c.NodeType { case "Shadowsocks": nodeType = "ss" case "V2ray": nodeType = "v2ray" case "Trojan": nodeType = "trojan" default: return nil, fmt.Errorf("NodeType Error: %s", c.NodeType) } res, err := c.client.R(). SetQueryParams(map[string]string{ "type": nodeType, "nodeId": strconv.Itoa(c.NodeID), }). SetResult(&Response{}). ForceContentType("application/json"). Get(path) response, err := c.parseResponse(res, path, err) if err != nil { return nil, err } ruleListResponse := new([]RuleItem) if err := json.Unmarshal(response.Data, ruleListResponse); err != nil { return nil, fmt.Errorf("Unmarshal %s failed: %s", reflect.TypeOf(ruleListResponse), err) } for _, r := range *ruleListResponse { ruleList = append(ruleList, api.DetectRule{ ID: r.ID, Pattern: regexp.MustCompile(r.Content), }) } return &ruleList, nil } // ReportIllegal reports the user illegal behaviors func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error { return nil } // ParseV2rayNodeResponse parse the response for the given nodeinfor format func (c *APIClient) ParseV2rayNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) { var enableTLS bool var path, host, TLStype, transportProtocol, serviceName string var speedlimit uint64 = 0 port := nodeInfoResponse.Port alterID := nodeInfoResponse.AlterId transportProtocol = nodeInfoResponse.Network switch transportProtocol { case "ws": host = nodeInfoResponse.Host path = nodeInfoResponse.Path case "grpc": serviceName = nodeInfoResponse.Sni case "tcp": // TODO } // Compatible with more node types config switch nodeInfoResponse.Security { case "tls", "xtls": if c.EnableXTLS { TLStype = "xtls" } else { TLStype = "tls" } enableTLS = true default: enableTLS = false } if c.SpeedLimit > 0 { speedlimit = uint64((c.SpeedLimit * 1000000) / 8) } else { speedlimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8) } // Create GeneralNodeInfo nodeinfo := &api.NodeInfo{ NodeType: c.NodeType, NodeID: c.NodeID, Port: port, SpeedLimit: speedlimit, AlterID: alterID, TransportProtocol: transportProtocol, EnableTLS: enableTLS, TLSType: TLStype, Path: path, Host: host, EnableVless: c.EnableVless, ServiceName: serviceName, } return nodeinfo, nil } // ParseSSNodeResponse parse the response for the given nodeinfor format func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) { var port int = 0 var speedlimit uint64 = 0 port = nodeInfoResponse.Port if c.SpeedLimit > 0 { speedlimit = uint64((c.SpeedLimit * 1000000) / 8) } else { speedlimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8) } // Create GeneralNodeInfo nodeinfo := &api.NodeInfo{ NodeType: c.NodeType, NodeID: c.NodeID, Port: port, SpeedLimit: speedlimit, TransportProtocol: "tcp", CypherMethod: nodeInfoResponse.Method, } return nodeinfo, nil } // ParseTrojanNodeResponse parse the response for the given nodeinfor format func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *NodeInfoResponse) (*api.NodeInfo, error) { // 域名或IP;port=连接端口#偏移端口|host=xx // gz.aaa.com;port=443#12345|host=hk.aaa.com var TLSType, host string var transportProtocol = "tcp" var speedlimit uint64 = 0 if c.EnableXTLS { TLSType = "xtls" } else { TLSType = "tls" } host = nodeInfoResponse.Host port := nodeInfoResponse.Port if c.SpeedLimit > 0 { speedlimit = uint64((c.SpeedLimit * 1000000) / 8) } else { speedlimit = uint64((nodeInfoResponse.SpeedLimit * 1000000) / 8) } if nodeInfoResponse.Grpc { transportProtocol = "grpc" } // Create GeneralNodeInfo nodeinfo := &api.NodeInfo{ NodeType: c.NodeType, NodeID: c.NodeID, Port: port, SpeedLimit: speedlimit, TransportProtocol: transportProtocol, EnableTLS: true, TLSType: TLSType, Host: host, ServiceName: nodeInfoResponse.Sni, } return nodeinfo, nil } // ParseUserListResponse parse the response for the given nodeinfo format func (c *APIClient) ParseUserListResponse(userInfoResponse *[]UserResponse) (*[]api.UserInfo, error) { var deviceLimit int = 0 var speedlimit uint64 = 0 userList := make([]api.UserInfo, len(*userInfoResponse)) for i, user := range *userInfoResponse { if c.DeviceLimit > 0 { deviceLimit = c.DeviceLimit } else { deviceLimit = user.DeviceLimit } if c.SpeedLimit > 0 { speedlimit = uint64((c.SpeedLimit * 1000000) / 8) } else { speedlimit = uint64((user.SpeedLimit * 1000000) / 8) } userList[i] = api.UserInfo{ UID: user.ID, Passwd: user.Passwd, UUID: user.Passwd, SpeedLimit: speedlimit, DeviceLimit: deviceLimit, } } return &userList, nil }