mirror of
https://github.com/XrayR-project/XrayR.git
synced 2025-06-26 14:19:50 +00:00
Merge branch 'master' of https://github.com/XrayR-project/XrayR
This commit is contained in:
commit
ac460c2f71
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config API config
|
// Config API config
|
||||||
@ -45,6 +47,7 @@ type NodeInfo struct {
|
|||||||
ServerKey string
|
ServerKey string
|
||||||
ServiceName string
|
ServiceName string
|
||||||
Header json.RawMessage
|
Header json.RawMessage
|
||||||
|
NameServerConfig []*conf.NameServerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
|
|
||||||
"github.com/bitly/go-simplejson"
|
"github.com/bitly/go-simplejson"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
|
|
||||||
"github.com/XrayR-project/XrayR/api"
|
"github.com/XrayR-project/XrayR/api"
|
||||||
)
|
)
|
||||||
@ -261,11 +263,12 @@ func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
|||||||
for i, rule := range nodeInfoResponse.Get("routes").MustArray() {
|
for i, rule := range nodeInfoResponse.Get("routes").MustArray() {
|
||||||
r := rule.(map[string]any)
|
r := rule.(map[string]any)
|
||||||
if r["action"] == "block" {
|
if r["action"] == "block" {
|
||||||
ruleListItem := api.DetectRule{
|
for ii := range r["match"].([]any) {
|
||||||
|
ruleList = append(ruleList, api.DetectRule{
|
||||||
ID: i,
|
ID: i,
|
||||||
Pattern: regexp.MustCompile(strings.TrimPrefix(r["match"].(string), "regexp:")),
|
Pattern: regexp.MustCompile(r["match"].([]any)[ii].(string)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
ruleList = append(ruleList, ruleListItem)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,12 +307,26 @@ func (c *APIClient) parseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (
|
|||||||
TLSType: TLSType,
|
TLSType: TLSType,
|
||||||
Host: nodeInfoResponse.Get("host").MustString(),
|
Host: nodeInfoResponse.Get("host").MustString(),
|
||||||
ServiceName: nodeInfoResponse.Get("server_name").MustString(),
|
ServiceName: nodeInfoResponse.Get("server_name").MustString(),
|
||||||
|
NameServerConfig: parseDNSConfig(nodeInfoResponse),
|
||||||
}
|
}
|
||||||
return nodeInfo, nil
|
return nodeInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSSNodeResponse parse the response for the given nodeInfo format
|
// parseSSNodeResponse parse the response for the given nodeInfo format
|
||||||
func (c *APIClient) parseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
func (c *APIClient) parseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||||
|
var header json.RawMessage
|
||||||
|
|
||||||
|
if nodeInfoResponse.Get("obfs").MustString() == "http" {
|
||||||
|
path := "/"
|
||||||
|
if p := nodeInfoResponse.Get("obfs_settings").Get("path").MustString(); p != "" {
|
||||||
|
path = p
|
||||||
|
}
|
||||||
|
header, _ = json.Marshal(map[string]any{
|
||||||
|
"type": "http",
|
||||||
|
"request": map[string]any{
|
||||||
|
"path": path,
|
||||||
|
}})
|
||||||
|
}
|
||||||
// Create GeneralNodeInfo
|
// Create GeneralNodeInfo
|
||||||
return &api.NodeInfo{
|
return &api.NodeInfo{
|
||||||
NodeType: c.NodeType,
|
NodeType: c.NodeType,
|
||||||
@ -318,6 +335,8 @@ func (c *APIClient) parseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api
|
|||||||
TransportProtocol: "tcp",
|
TransportProtocol: "tcp",
|
||||||
CypherMethod: nodeInfoResponse.Get("cipher").MustString(),
|
CypherMethod: nodeInfoResponse.Get("cipher").MustString(),
|
||||||
ServerKey: nodeInfoResponse.Get("server_key").MustString(), // shadowsocks2022 share key
|
ServerKey: nodeInfoResponse.Get("server_key").MustString(), // shadowsocks2022 share key
|
||||||
|
NameServerConfig: parseDNSConfig(nodeInfoResponse),
|
||||||
|
Header: header,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,5 +392,20 @@ func (c *APIClient) parseV2rayNodeResponse(nodeInfoResponse *simplejson.Json) (*
|
|||||||
EnableVless: c.EnableVless,
|
EnableVless: c.EnableVless,
|
||||||
ServiceName: serviceName,
|
ServiceName: serviceName,
|
||||||
Header: header,
|
Header: header,
|
||||||
|
NameServerConfig: parseDNSConfig(nodeInfoResponse),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseDNSConfig(nodeInfoResponse *simplejson.Json) (nameServerList []*conf.NameServerConfig) {
|
||||||
|
for _, rule := range nodeInfoResponse.Get("routes").MustArray() {
|
||||||
|
r := rule.(map[string]any)
|
||||||
|
if r["action"] == "dns" {
|
||||||
|
nameServerList = append(nameServerList, &conf.NameServerConfig{
|
||||||
|
Address: &conf.Address{Address: net.ParseAddress(r["action_value"].(string))},
|
||||||
|
Domains: strings.Split(r["match"].(string), ","),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -140,7 +140,7 @@ func (c *APIClient) parseResponse(res *resty.Response, path string, err error) (
|
|||||||
return rtn, nil
|
return rtn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNodeInfo will pull NodeInfo Config from sspanel
|
// GetNodeInfo will pull NodeInfo Config from panel
|
||||||
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
||||||
var nodeType string
|
var nodeType string
|
||||||
switch c.NodeType {
|
switch c.NodeType {
|
||||||
@ -184,7 +184,7 @@ func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
|||||||
return nodeInfo, nil
|
return nodeInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserList will pull user form sspanel
|
// GetUserList will pull user form panel
|
||||||
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
||||||
var nodeType string
|
var nodeType string
|
||||||
switch c.NodeType {
|
switch c.NodeType {
|
||||||
@ -215,16 +215,16 @@ func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
|||||||
user.Email = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
user.Email = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
||||||
user.Passwd = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
user.Passwd = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("secret").MustString()
|
||||||
user.Method = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("cipher").MustString()
|
user.Method = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("cipher").MustString()
|
||||||
user.SpeedLimit = uint64(response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("speed_limit").MustUint64() * 1000000 / 8)
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("shadowsocks_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
||||||
case "Trojan":
|
case "Trojan":
|
||||||
user.UUID = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
user.UUID = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
||||||
user.Email = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
user.Email = response.Get("data").GetIndex(i).Get("trojan_user").Get("password").MustString()
|
||||||
user.SpeedLimit = uint64(response.Get("data").GetIndex(i).Get("trojan_user").Get("speed_limit").MustUint64() * 1000000 / 8)
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("trojan_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
||||||
case "V2ray":
|
case "V2ray":
|
||||||
user.UUID = response.Get("data").GetIndex(i).Get("v2ray_user").Get("uuid").MustString()
|
user.UUID = response.Get("data").GetIndex(i).Get("v2ray_user").Get("uuid").MustString()
|
||||||
user.Email = response.Get("data").GetIndex(i).Get("v2ray_user").Get("email").MustString()
|
user.Email = response.Get("data").GetIndex(i).Get("v2ray_user").Get("email").MustString()
|
||||||
user.AlterID = uint16(response.Get("data").GetIndex(i).Get("v2ray_user").Get("alter_id").MustUint64())
|
user.AlterID = uint16(response.Get("data").GetIndex(i).Get("v2ray_user").Get("alter_id").MustUint64())
|
||||||
user.SpeedLimit = uint64(response.Get("data").GetIndex(i).Get("v2ray_user").Get("speed_limit").MustUint64() * 1000000 / 8)
|
user.SpeedLimit = response.Get("data").GetIndex(i).Get("v2ray_user").Get("speed_limit").MustUint64() * 1000000 / 8
|
||||||
}
|
}
|
||||||
if c.SpeedLimit > 0 {
|
if c.SpeedLimit > 0 {
|
||||||
user.SpeedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
user.SpeedLimit = uint64((c.SpeedLimit * 1000000) / 8)
|
||||||
@ -269,7 +269,7 @@ func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
|||||||
return &ruleList, nil
|
return &ruleList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2board only support the rule for v2ray
|
// Only support the rule for v2ray
|
||||||
// fix: reuse config response
|
// fix: reuse config response
|
||||||
c.access.Lock()
|
c.access.Lock()
|
||||||
defer c.access.Unlock()
|
defer c.access.Unlock()
|
||||||
@ -300,7 +300,7 @@ func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTrojanNodeResponse parse the response for the given nodeinfor format
|
// ParseTrojanNodeResponse parse the response for the given nodeInfo format
|
||||||
func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||||
var TLSType = "tls"
|
var TLSType = "tls"
|
||||||
if c.EnableXTLS {
|
if c.EnableXTLS {
|
||||||
@ -315,7 +315,7 @@ func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (
|
|||||||
host := inboundInfo.Get("streamSettings").Get("tlsSettings").Get("serverName").MustString()
|
host := inboundInfo.Get("streamSettings").Get("tlsSettings").Get("serverName").MustString()
|
||||||
|
|
||||||
// Create GeneralNodeInfo
|
// Create GeneralNodeInfo
|
||||||
nodeinfo := &api.NodeInfo{
|
nodeInfo := &api.NodeInfo{
|
||||||
NodeType: c.NodeType,
|
NodeType: c.NodeType,
|
||||||
NodeID: c.NodeID,
|
NodeID: c.NodeID,
|
||||||
Port: port,
|
Port: port,
|
||||||
@ -324,10 +324,10 @@ func (c *APIClient) ParseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (
|
|||||||
TLSType: TLSType,
|
TLSType: TLSType,
|
||||||
Host: host,
|
Host: host,
|
||||||
}
|
}
|
||||||
return nodeinfo, nil
|
return nodeInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSSNodeResponse parse the response for the given nodeinfo format
|
// ParseSSNodeResponse parse the response for the given nodeInfo format
|
||||||
func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||||
var method, serverPsk string
|
var method, serverPsk string
|
||||||
tmpInboundInfo := nodeInfoResponse.Get("inbounds").MustArray()
|
tmpInboundInfo := nodeInfoResponse.Get("inbounds").MustArray()
|
||||||
@ -350,7 +350,7 @@ func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create GeneralNodeInfo
|
// Create GeneralNodeInfo
|
||||||
nodeinfo := &api.NodeInfo{
|
nodeInfo := &api.NodeInfo{
|
||||||
NodeType: c.NodeType,
|
NodeType: c.NodeType,
|
||||||
NodeID: c.NodeID,
|
NodeID: c.NodeID,
|
||||||
Port: port,
|
Port: port,
|
||||||
@ -359,12 +359,12 @@ func (c *APIClient) ParseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api
|
|||||||
ServerKey: serverPsk,
|
ServerKey: serverPsk,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodeinfo, nil
|
return nodeInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseV2rayNodeResponse parse the response for the given nodeinfor format
|
// ParseV2rayNodeResponse parse the response for the given nodeInfo format
|
||||||
func (c *APIClient) ParseV2rayNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
func (c *APIClient) ParseV2rayNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||||
var TLSType string = "tls"
|
var TLSType = "tls"
|
||||||
var path, host, serviceName string
|
var path, host, serviceName string
|
||||||
var header json.RawMessage
|
var header json.RawMessage
|
||||||
var enableTLS bool
|
var enableTLS bool
|
||||||
|
@ -66,10 +66,12 @@ func (p *Panel) loadCore(panelConfig *Config) *core.Instance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dnsConfig, err := coreDnsConfig.Build()
|
|
||||||
if err != nil {
|
// init controller's DNS config
|
||||||
log.Panicf("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help: %s", err)
|
for _, config := range p.panelConfig.NodesConfig {
|
||||||
|
config.ControllerConfig.DNSConfig = coreDnsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routing config
|
// Routing config
|
||||||
coreRouterConfig := &conf.RouterConfig{}
|
coreRouterConfig := &conf.RouterConfig{}
|
||||||
if panelConfig.RouteConfigPath != "" {
|
if panelConfig.RouteConfigPath != "" {
|
||||||
@ -137,7 +139,6 @@ func (p *Panel) loadCore(panelConfig *Config) *core.Instance {
|
|||||||
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
||||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||||
serial.ToTypedMessage(policyConfig),
|
serial.ToTypedMessage(policyConfig),
|
||||||
serial.ToTypedMessage(dnsConfig),
|
|
||||||
serial.ToTypedMessage(routeConfig),
|
serial.ToTypedMessage(routeConfig),
|
||||||
},
|
},
|
||||||
Inbound: inBoundConfig,
|
Inbound: inBoundConfig,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
|
|
||||||
"github.com/XrayR-project/XrayR/common/limiter"
|
"github.com/XrayR-project/XrayR/common/limiter"
|
||||||
"github.com/XrayR-project/XrayR/common/mylego"
|
"github.com/XrayR-project/XrayR/common/mylego"
|
||||||
)
|
)
|
||||||
@ -21,6 +23,7 @@ type Config struct {
|
|||||||
AutoSpeedLimitConfig *AutoSpeedLimitConfig `mapstructure:"AutoSpeedLimitConfig"`
|
AutoSpeedLimitConfig *AutoSpeedLimitConfig `mapstructure:"AutoSpeedLimitConfig"`
|
||||||
GlobalDeviceLimitConfig *limiter.GlobalDeviceLimitConfig `mapstructure:"GlobalDeviceLimitConfig"`
|
GlobalDeviceLimitConfig *limiter.GlobalDeviceLimitConfig `mapstructure:"GlobalDeviceLimitConfig"`
|
||||||
FallBackConfigs []*FallBackConfig `mapstructure:"FallBackConfigs"`
|
FallBackConfigs []*FallBackConfig `mapstructure:"FallBackConfigs"`
|
||||||
|
DNSConfig *conf.DNSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutoSpeedLimitConfig struct {
|
type AutoSpeedLimitConfig struct {
|
||||||
|
@ -7,12 +7,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
|
"github.com/xtls/xray-core/common/serial"
|
||||||
"github.com/xtls/xray-core/common/task"
|
"github.com/xtls/xray-core/common/task"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
|
"github.com/xtls/xray-core/features"
|
||||||
"github.com/xtls/xray-core/features/inbound"
|
"github.com/xtls/xray-core/features/inbound"
|
||||||
"github.com/xtls/xray-core/features/outbound"
|
"github.com/xtls/xray-core/features/outbound"
|
||||||
"github.com/xtls/xray-core/features/routing"
|
"github.com/xtls/xray-core/features/routing"
|
||||||
"github.com/xtls/xray-core/features/stats"
|
"github.com/xtls/xray-core/features/stats"
|
||||||
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
|
|
||||||
"github.com/XrayR-project/XrayR/api"
|
"github.com/XrayR-project/XrayR/api"
|
||||||
"github.com/XrayR-project/XrayR/app/mydispatcher"
|
"github.com/XrayR-project/XrayR/app/mydispatcher"
|
||||||
@ -77,6 +80,13 @@ func (c *Controller) Start() error {
|
|||||||
}
|
}
|
||||||
c.nodeInfo = newNodeInfo
|
c.nodeInfo = newNodeInfo
|
||||||
c.Tag = c.buildNodeTag()
|
c.Tag = c.buildNodeTag()
|
||||||
|
|
||||||
|
// append remote DNS config and init dns service
|
||||||
|
err = c.addNewDNS(newNodeInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Add new tag
|
// Add new tag
|
||||||
err = c.addNewTag(newNodeInfo)
|
err = c.addNewTag(newNodeInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -252,6 +262,13 @@ func (c *Controller) nodeInfoMonitor() (err error) {
|
|||||||
log.Print(err)
|
log.Print(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add DNS
|
||||||
|
log.Printf("%s Reload DNS service", c.logPrefix())
|
||||||
|
if err := c.addNewDNS(newNodeInfo); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var deleted, added []api.UserInfo
|
var deleted, added []api.UserInfo
|
||||||
if usersChanged {
|
if usersChanged {
|
||||||
@ -613,3 +630,40 @@ func (c *Controller) certMonitor() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// append remote dns
|
||||||
|
func (c *Controller) addNewDNS(newNodeInfo *api.NodeInfo) error {
|
||||||
|
// reserve local DNS
|
||||||
|
servers := c.config.DNSConfig.Servers
|
||||||
|
servers = append(servers, newNodeInfo.NameServerConfig...)
|
||||||
|
dns := conf.DNSConfig{
|
||||||
|
Servers: servers,
|
||||||
|
Hosts: c.config.DNSConfig.Hosts,
|
||||||
|
ClientIP: c.config.DNSConfig.ClientIP,
|
||||||
|
Tag: c.config.DNSConfig.Tag,
|
||||||
|
QueryStrategy: c.config.DNSConfig.QueryStrategy,
|
||||||
|
DisableCache: c.config.DNSConfig.DisableCache,
|
||||||
|
DisableFallback: c.config.DNSConfig.DisableFallback,
|
||||||
|
DisableFallbackIfMatch: c.config.DNSConfig.DisableFallbackIfMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsConfig, err := dns.Build()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help: %s", err)
|
||||||
|
}
|
||||||
|
dnsInstance, err := serial.ToTypedMessage(dnsConfig).GetInstance()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj, err := core.CreateObject(c.server, dnsInstance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if feature, ok := obj.(features.Feature); ok {
|
||||||
|
if err := c.server.AddFeature(feature); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user