Additional os fields (#251)

* Additional os fields for clients

* Returned back vault

* Fixed windows version

* Fixing api failures

* Minor documentation adjustments

* Added client filtering test in client repo

* Added tests for virtual info mapping, small code review fixes

* Started documentation

* Added documentation and refactored extraction of filters from get params

* Unittest fixing

* Fixing unittest 2

* api docs: fix os fields descriptions

Co-authored-by: Mykola Terelia <mykola.terelia@gmail.com>
This commit is contained in:
Andrey 2021-07-07 21:19:51 +03:00 committed by GitHub
parent 7aa480343e
commit 19a803a844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1487 additions and 295 deletions

View File

@ -328,6 +328,14 @@ paths:
description: "Sort option `-<field>`(desc) or `<field>`(asc). `<field>` can be one of `'id', 'name', 'os', 'hostname', 'version'`. For example, `&sort=-name` or `&sort=hostname`, etc"
required: false
type: "string"
- name: "filter"
in: "query"
description: "Filter option `filter[<field>]` or `filter[<field>,<field>] for or conditions`.\n
`<field>` can be one of `'os_full_name', 'os_version', 'os_virtualization_system', 'os_virtualization_role',\n
'cpu_family', 'cpu_model', 'cpu_model_name', 'num_cpus', 'timezone'`. For example, `&filter[os_full_name]=Ubuntu 20.04` or `filter[os_full_name]=Ubuntu 20.04,Ubuntu 18.04`, etc.\n
Multiple filters are possible. You can also use wildcards for partial matches e.g. `filter[os_full_name]=Ubuntu*` will list all clients whose os_full_name starts with 'Ubuntu'."
required: false
type: "string"
summary: "List all active and disconnected client connections. By default sorted by ID in asc order"
description: ""
produces:
@ -1857,7 +1865,13 @@ definitions:
description: "client name"
os:
type: "string"
description: "client OS description"
description: "long description of client OS"
os_full_name:
type: "string"
description: "short description of client OS (e.g. Microsoft Windows Server 2016 Standard)"
os_version:
type: "string"
description: "version info about client's OS e.g. 10.0.14393 Build 14393"
os_arch:
type: "string"
description: "client cpu architecture (ex: 386, amd64)"
@ -1867,9 +1881,33 @@ definitions:
os_kernel:
type: "string"
description: "client OS kernel (ex: linux, windows)"
os_virtualization_system:
type: "string"
description: "info about the VM where client is running e.g. KVM, LXC, HyperV, VMWare, Xen"
os_virtualization_role:
type: "string"
description: "role of the client in the running VM e.g. host or guest"
hostname:
type: "string"
description: "client hostname"
cpu_family:
type: "string"
description: "client's processor family info"
cpu_model:
type: "string"
description: "client's processor model info, e.g. 85"
cpu_model_name:
type: "string"
description: "human readable name of the client's processor model, e.g. Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz"
num_cpus:
type: "integer"
description: "Number of cpu cores in the client's machine"
mem_total:
type: "number"
description: "Total memory in bytes"
timezone:
type: "string"
description: "Client's timezone e.g. PDT (UTC-07:00)"
ipv4:
type: "array"
items:
@ -2353,7 +2391,7 @@ definitions:
script:
type: "string"
description: "text of the script"
sudo:
is_sudo:
type: "boolean"
description: "if true, this script will be executed as a sudo user"
ScriptInput:
@ -2371,6 +2409,6 @@ definitions:
cwd:
type: "string"
description: "current working directory, where the script should be executed"
sudo:
is_sudo:
type: "boolean"
description: "if true, this script will be executed as a sudo user"

View File

@ -23,6 +23,8 @@ import (
"github.com/cloudradar-monitoring/rport/share/comm"
)
const UnknownValue = "unknown"
//Client represents a client instance
type Client struct {
*chshare.Logger
@ -42,13 +44,14 @@ type Client struct {
//NewClient creates a new client instance
func NewClient(config *Config) *Client {
cmdExec := NewCmdExecutor()
client := &Client{
Logger: chshare.NewLogger("client", config.Logging.LogOutput, config.Logging.LogLevel),
config: config,
running: true,
runningc: make(chan error, 1),
cmdExec: NewCmdExecutor(),
systemInfo: NewSystemInfo(),
cmdExec: cmdExec,
systemInfo: NewSystemInfo(cmdExec),
}
client.sshConfig = &ssh.ClientConfig{
@ -368,47 +371,106 @@ func (c *Client) connectionRequest(ctx context.Context) *chshare.ConnectionReque
defer cancel()
connReq := &chshare.ConnectionRequest{
Version: chshare.BuildVersion,
ID: c.config.Client.ID,
Name: c.config.Client.Name,
OSArch: c.systemInfo.GoArch(),
Tags: c.config.Client.Tags,
Remotes: c.config.Client.remotes,
ID: c.config.Client.ID,
Name: c.config.Client.Name,
Tags: c.config.Client.Tags,
Remotes: c.config.Client.remotes,
OS: UnknownValue,
OSArch: c.systemInfo.GoArch(),
OSKernel: UnknownValue,
OSFamily: UnknownValue,
OSVersion: UnknownValue,
OSVirtualizationRole: UnknownValue,
OSVirtualizationSystem: UnknownValue,
Version: chshare.BuildVersion,
Hostname: UnknownValue,
CPUFamily: UnknownValue,
CPUModel: UnknownValue,
CPUModelName: UnknownValue,
}
info, err := c.systemInfo.HostInfo(ctx)
if err != nil {
c.Logger.Errorf("Could not get os information: %v", err)
connReq.OSKernel = "unknown"
connReq.OSFamily = "unknown"
} else {
connReq.OSKernel = info.OS
connReq.OSFamily = info.PlatformFamily
}
connReq.OS, err = c.getOS(ctx, info)
os, err := c.getOS(ctx, info)
if err != nil {
connReq.OS = "unknown"
c.Logger.Errorf("Could not get os name: %v", err)
} else {
connReq.OS = os
}
connReq.OSFullName = c.getOSFullName(info)
if info != nil && info.PlatformVersion != "" {
connReq.OSVersion = info.PlatformVersion
}
oSVirtualizationSystem, oSVirtualizationRole, err := c.systemInfo.VirtualizationInfo(ctx, info)
if err != nil {
c.Logger.Errorf("Could not get OS Virtualization Info: %v", err)
} else {
connReq.OSVirtualizationSystem = oSVirtualizationSystem
connReq.OSVirtualizationRole = oSVirtualizationRole
}
connReq.IPv4, connReq.IPv6, err = c.localIPAddresses()
if err != nil {
c.Logger.Errorf("Could not get local ips: %v", err)
}
connReq.Hostname, err = c.systemInfo.Hostname()
hostname, err := c.systemInfo.Hostname()
if err != nil {
connReq.Hostname = "unknown"
c.Logger.Errorf("Could not get hostname: %v", err)
} else {
connReq.Hostname = hostname
}
cpuInfo, err := c.systemInfo.CPUInfo(ctx)
if err != nil {
c.Logger.Errorf("Could not get cpu information: %v", err)
}
if len(cpuInfo.CPUs) > 0 {
connReq.CPUFamily = cpuInfo.CPUs[0].Family
connReq.CPUModel = cpuInfo.CPUs[0].Model
connReq.CPUModelName = cpuInfo.CPUs[0].ModelName
}
connReq.NumCPUs = cpuInfo.NumCores
memoryInfo, err := c.systemInfo.MemoryStats(ctx)
if err != nil {
c.Logger.Errorf("Could not get memory information: %v", err)
} else if memoryInfo != nil {
connReq.MemoryTotal = memoryInfo.Total
}
connReq.Timezone = c.getTimezone()
return connReq
}
func (c *Client) getOS(ctx context.Context, info *host.InfoStat) (string, error) {
if info == nil {
return "unknown", nil
return UnknownValue, nil
} else if info.OS == "windows" {
return info.Platform + " " + info.PlatformVersion + " " + info.PlatformFamily, nil
}
return c.systemInfo.Uname(ctx)
}
func (c *Client) getOSFullName(infoStat *host.InfoStat) string {
if infoStat == nil {
return UnknownValue
}
return fmt.Sprintf("%s %s", strings.Title(strings.ToLower(infoStat.Platform)), infoStat.PlatformVersion)
}
func (c *Client) getTimezone() string {
return c.systemInfo.SystemTime().Format("MST (UTC-07:00)")
}

View File

@ -10,6 +10,9 @@ import (
"testing"
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/host"
"github.com/stretchr/testify/assert"
@ -96,28 +99,57 @@ func TestConnectionRequest(t *testing.T) {
{
Name: "no errors",
SystemInfo: &mockSystemInfo{
ReturnHostname: "test-hostname",
ReturnUname: "test-uname",
ReturnUname: "test-uname",
ReturnHostname: "test-hostname",
ReturnHostnameError: nil,
ReturnHostInfo: &host.InfoStat{
OS: "test-os",
PlatformFamily: "test-family",
OS: "test-os",
PlatformFamily: "test-family",
Platform: "UBUNTU",
PlatformVersion: "18.04",
VirtualizationSystem: "KVM",
VirtualizationRole: "guest",
},
ReturnInterfaceAddrs: interfaceAddrs,
ReturnGoArch: "test-arch",
ReturnCPUInfo: CPUInfo{
CPUs: []cpu.InfoStat{
{
Family: "fam1",
Model: "mod1",
ModelName: "mod name 123",
},
},
NumCores: 4,
},
ReturnMemoryStat: &mem.VirtualMemoryStat{
Total: 100000,
},
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
},
ExpectedConnectionRequest: &chshare.ConnectionRequest{
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: "test-uname",
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "test-os",
Hostname: "test-hostname",
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
NumCPUs: 4,
MemoryTotal: 100000,
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
OS: "test-uname",
OSFullName: "Ubuntu 18.04",
OSVersion: "18.04",
OSVirtualizationSystem: "KVM",
OSVirtualizationRole: "guest",
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "test-os",
Hostname: "test-hostname",
CPUFamily: "fam1",
CPUModel: "mod1",
CPUModelName: "mod name 123",
Timezone: "UTC (UTC+00:00)",
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
},
}, {
Name: "windows, no errors",
@ -132,20 +164,38 @@ func TestConnectionRequest(t *testing.T) {
},
ReturnInterfaceAddrs: interfaceAddrs,
ReturnGoArch: "test-arch",
ReturnCPUInfo: CPUInfo{
CPUs: []cpu.InfoStat{
{
Family: "cpufam1",
Model: "cpumod1",
ModelName: "cpumod_name1",
},
},
NumCores: 2,
},
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
},
ExpectedConnectionRequest: &chshare.ConnectionRequest{
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: "test-platform 123 test-family",
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "windows",
Hostname: "test-hostname",
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: "test-platform 123 test-family",
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "windows",
Hostname: "test-hostname",
OSFullName: "Test-Platform 123",
OSVersion: "123",
CPUFamily: "cpufam1",
CPUModel: "cpumod1",
CPUModelName: "cpumod_name1",
Timezone: "UTC (UTC+00:00)",
NumCPUs: 2,
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
},
}, {
Name: "all errors",
@ -155,20 +205,28 @@ func TestConnectionRequest(t *testing.T) {
ReturnHostInfoError: errors.New("test error"),
ReturnInterfaceAddrsError: errors.New("test error"),
ReturnGoArch: "test-arch",
ReturnCPUInfoError: errors.New("test error"),
ReturnMemoryError: errors.New("test error"),
},
ExpectedConnectionRequest: &chshare.ConnectionRequest{
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: "unknown",
OSArch: "test-arch",
OSFamily: "unknown",
OSKernel: "unknown",
Hostname: "unknown",
IPv4: nil,
IPv6: nil,
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: UnknownValue,
OSArch: "test-arch",
OSFamily: UnknownValue,
OSKernel: UnknownValue,
Hostname: UnknownValue,
CPUFamily: UnknownValue,
CPUModel: UnknownValue,
CPUModelName: UnknownValue,
OSFullName: UnknownValue,
OSVersion: UnknownValue,
Timezone: "UTC (UTC+00:00)",
IPv4: nil,
IPv6: nil,
},
}, {
Name: "uname error",
@ -183,20 +241,27 @@ func TestConnectionRequest(t *testing.T) {
},
ReturnInterfaceAddrs: interfaceAddrs,
ReturnGoArch: "test-arch",
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
},
ExpectedConnectionRequest: &chshare.ConnectionRequest{
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: "unknown",
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "test-os",
Hostname: "test-hostname",
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
Version: "0.0.0-src",
ID: "test-client-id",
Name: "test-name",
OSVersion: "123",
OSFullName: "Test-Platform 123",
Tags: []string{"tag1", "tag2"},
Remotes: []*chshare.Remote{remote1, remote2},
OS: UnknownValue,
OSArch: "test-arch",
OSFamily: "test-family",
OSKernel: "test-os",
Hostname: "test-hostname",
Timezone: "UTC (UTC+00:00)",
CPUFamily: UnknownValue,
CPUModel: UnknownValue,
CPUModelName: UnknownValue,
IPv4: []string{"192.0.2.1", "192.0.2.2"},
IPv6: []string{"2001:db8::1", "2001:db8::2"},
},
},
}

View File

@ -2,28 +2,45 @@ package chclient
import (
"context"
"errors"
"net"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/host"
)
type CPUInfo struct {
CPUs []cpu.InfoStat
NumCores int
}
type SystemInfo interface {
Hostname() (string, error)
HostInfo(context.Context) (*host.InfoStat, error)
CPUInfo(ctx context.Context) (CPUInfo, error)
MemoryStats(context.Context) (*mem.VirtualMemoryStat, error)
Uname(context.Context) (string, error)
InterfaceAddrs() ([]net.Addr, error)
GoArch() string
SystemTime() time.Time
VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error)
}
type realSystemInfo struct {
cmdExec CmdExecutor
}
func NewSystemInfo() SystemInfo {
return &realSystemInfo{}
func NewSystemInfo(cmdExec CmdExecutor) SystemInfo {
return &realSystemInfo{
cmdExec: cmdExec,
}
}
func (s *realSystemInfo) Hostname() (string, error) {
@ -48,3 +65,47 @@ func (s *realSystemInfo) InterfaceAddrs() ([]net.Addr, error) {
}
func (s *realSystemInfo) GoArch() string { return runtime.GOARCH }
func (s *realSystemInfo) CPUInfo(ctx context.Context) (CPUInfo, error) {
cpuInfo := CPUInfo{
CPUs: []cpu.InfoStat{},
}
errs := make([]string, 0, 2)
cpuInfos, err1 := cpu.InfoWithContext(ctx)
if err1 == nil {
cpuInfo.CPUs = cpuInfos
} else {
errs = append(errs, err1.Error())
}
cpuCount, err2 := cpu.CountsWithContext(ctx, true)
if err2 == nil {
cpuInfo.NumCores = cpuCount
} else {
errs = append(errs, err2.Error())
}
return cpuInfo, errors.New(strings.Join(errs, ", "))
}
func (s *realSystemInfo) MemoryStats(ctx context.Context) (*mem.VirtualMemoryStat, error) {
return mem.VirtualMemoryWithContext(ctx)
}
func (s *realSystemInfo) SystemTime() time.Time {
return time.Now()
}
func (s *realSystemInfo) VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error) {
if infoStat != nil && infoStat.VirtualizationSystem != "" {
return strings.ToUpper(infoStat.VirtualizationSystem), strings.ToLower(infoStat.VirtualizationRole), nil
}
virtSystem, virtRole, err = s.virtualizationInfo(ctx)
if err != nil {
return "", "", err
}
return strings.ToUpper(virtSystem), strings.ToLower(virtRole), nil
}

31
client/system_info_nix.go Normal file
View File

@ -0,0 +1,31 @@
//+build !windows
package chclient
import (
"context"
"io/ioutil"
"os"
)
const devicesInfoPath = "/proc/bus/pci/devices"
func (s *realSystemInfo) virtualizationInfo(ctx context.Context) (virtSystem, virtRole string, err error) {
_, err = os.Stat(devicesInfoPath)
if err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", err
}
fileContent, err := ioutil.ReadFile(devicesInfoPath)
if err != nil {
return "", "", err
}
virtSystem, virtRole = getVirtInfoFromNixDevicesList(string(fileContent))
return virtSystem, virtRole, nil
}

View File

@ -3,20 +3,29 @@ package chclient
import (
"context"
"net"
"time"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/host"
)
type mockSystemInfo struct {
ReturnHostname string
ReturnHostnameError error
ReturnHostInfo *host.InfoStat
ReturnHostInfoError error
ReturnUname string
ReturnUnameError error
ReturnInterfaceAddrs []net.Addr
ReturnInterfaceAddrsError error
ReturnGoArch string
ReturnHostname string
ReturnHostnameError error
ReturnHostInfo *host.InfoStat
ReturnHostInfoError error
ReturnCPUInfo CPUInfo
ReturnCPUInfoError error
ReturnMemoryStat *mem.VirtualMemoryStat
ReturnMemoryError error
ReturnUname string
ReturnUnameError error
ReturnInterfaceAddrs []net.Addr
ReturnInterfaceAddrsError error
ReturnGoArch string
ReturnSystemTime time.Time
ReturnVirtualizationInfoError error
}
func (s *mockSystemInfo) Hostname() (string, error) {
@ -38,3 +47,23 @@ func (s *mockSystemInfo) InterfaceAddrs() ([]net.Addr, error) {
func (s *mockSystemInfo) GoArch() string {
return s.ReturnGoArch
}
func (s *mockSystemInfo) CPUInfo(ctx context.Context) (CPUInfo, error) {
return s.ReturnCPUInfo, s.ReturnCPUInfoError
}
func (s *mockSystemInfo) MemoryStats(ctx context.Context) (*mem.VirtualMemoryStat, error) {
return s.ReturnMemoryStat, s.ReturnMemoryError
}
func (s *mockSystemInfo) SystemTime() time.Time {
return s.ReturnSystemTime
}
func (s *mockSystemInfo) VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error) {
if infoStat == nil {
return "", "", s.ReturnVirtualizationInfoError
}
return infoStat.VirtualizationSystem, infoStat.VirtualizationRole, s.ReturnVirtualizationInfoError
}

23
client/system_info_win.go Normal file
View File

@ -0,0 +1,23 @@
//+build windows
package chclient
import (
"context"
"strings"
)
func (s *realSystemInfo) virtualizationInfo(ctx context.Context) (virtSystem, virtRole string, err error) {
cmd := s.cmdExec.New(ctx, "powerShell", "Get-Service", "", false)
execRes, err := cmd.CombinedOutput()
if err != nil {
return "", "", err
}
sysInfo := strings.TrimSpace(string(execRes))
virtSystem, virtRole = getVirtInfoFromPowershellServicesList(sysInfo)
return virtSystem, virtRole, nil
}

View File

@ -0,0 +1,111 @@
package chclient
import (
"regexp"
"strings"
)
const (
VirtualSystemHyperV = "HyperV"
VirtualSystemVMWare = "VMware"
VirtualSystemKVM = "KVM"
VirtualSystemXen = "Xen"
VirtualSystemRoleGuest = "guest"
VirtualSystemRoleHost = "host"
)
/**
This method interprets result of powerShell Get-Service output as following:
Get-Service | findstr vmcompute detect if a machine is a HyperV host.
This would result in "os_virtualization_system": "HyperV", "os_virtualization_role":"guest"
Get-Service|findstr "Running.*vmicheartbeat" detects if a machine is a Hyper-V guest.
Get-service|findstr "Running.*VMTools" detects if a machine is VMware guest.
Get-service|findstr "Running.*QEMU-GA" detects if a machine is KVM guest.
*/
func getVirtInfoFromPowershellServicesList(rawServicesList string) (virtSystem, virtRole string) {
if strings.Contains(rawServicesList, "vmcompute") {
virtSystem = VirtualSystemHyperV
virtRole = VirtualSystemRoleHost
return virtSystem, virtRole
}
regexToVirtInfoMapping := map[string]struct {
virtSystem string
virtRole string
}{
`Running.*vmicheartbeat`: {
virtSystem: VirtualSystemHyperV,
virtRole: VirtualSystemRoleGuest,
},
`Running.*VMTools`: {
virtSystem: VirtualSystemVMWare,
virtRole: VirtualSystemRoleGuest,
},
`Running.*QEMU-GA`: {
virtSystem: VirtualSystemKVM,
virtRole: VirtualSystemRoleGuest,
},
}
for regexStr, virtInfo := range regexToVirtInfoMapping {
rx := regexp.MustCompile(regexStr)
if rx.MatchString(rawServicesList) {
return virtInfo.virtSystem, virtInfo.virtRole
}
}
return UnknownValue, UnknownValue
}
/**
Parses the output of /proc/bus/pci/devices on nix systems as following:
If grep -c hyperv_fb /proc/bus/pci/devices = 1 the Linux system is a HyperV guest.
If grep -c vmwgfx /proc/bus/pci/devices = 1 the Linux system is a Vmware guest.
If grep -c virtio-pci /proc/bus/pci/devices > 0 the Linux system is a KVM guest.
If grep -c xen-platform-pci /proc/bus/pci/devices > 0 the Linux system is a Xen guest.
*/
func getVirtInfoFromNixDevicesList(rawDevicesList string) (virtSystem, virtRole string) {
devicesInfoExpectations := []struct {
expectedSubstr string
expectedCount int
virtSystem string
virtRole string
}{
{
expectedSubstr: "hyperv_fb",
expectedCount: 1,
virtSystem: VirtualSystemHyperV,
virtRole: VirtualSystemRoleGuest,
},
{
expectedSubstr: "vmwgfx",
expectedCount: 1,
virtSystem: VirtualSystemVMWare,
virtRole: VirtualSystemRoleGuest,
},
{
expectedSubstr: "virtio-pci",
expectedCount: 0,
virtSystem: VirtualSystemKVM,
virtRole: VirtualSystemRoleGuest,
},
{
expectedSubstr: "xen-platform-pci",
expectedCount: 0,
virtSystem: VirtualSystemXen,
virtRole: VirtualSystemRoleGuest,
},
}
for _, exp := range devicesInfoExpectations {
if exp.expectedCount > 0 && strings.Count(rawDevicesList, exp.expectedSubstr) == exp.expectedCount {
return exp.virtSystem, exp.virtRole
}
if exp.expectedCount == 0 && strings.Count(rawDevicesList, exp.expectedSubstr) > 0 {
return exp.virtSystem, exp.virtRole
}
}
return UnknownValue, UnknownValue
}

View File

@ -0,0 +1,219 @@
package chclient
import (
"testing"
"github.com/stretchr/testify/assert"
)
type virtInfoTestCase struct {
rawData string
virtSystemToExpect string
virtRoleToExpect string
}
func TestGetVirtInfoFromPowershellServicesList(t *testing.T) {
testCases := []virtInfoTestCase{
{
rawData: `
Stopped PerfHost Performance Counter DLL Host
Stopped PhoneSvc Phone Service
Stopped PimIndexMainten... Contact Data_1400f45
Stopped pla Performance Logs & Alerts
Running PlugPlay Plug and Play
Running PolicyAgent IPsec Policy Agent
Running Power Power
Stopped PrintNotify Printer Extensions and Notifications
Running ProfSvc User Profile Service
Stopped PhoneSvc Phone Service
Stopped PimIndexMainten... Contact Data_1400f45
Stopped pla Performance Logs & Alerts
Running PlugPlay Plug and Play
Running PolicyAgent IPsec Policy Agent
Running Power Power
Stopped PrintNotify Printer Extensions and Notifications
Running ProfSvc User Profile Service
Running Pulse Pulse by freeping.io
Stopped QEMU Guest Agen... QEMU Guest Agent VSS Provider
Running QEMU-GA QEMU Guest Agent
Stopped WiaRpc Still Image Acquisition Events
`,
virtSystemToExpect: VirtualSystemKVM,
virtRoleToExpect: VirtualSystemRoleGuest,
},
{
rawData: `
Stopped PerfHost Performance Counter DLL Host
Stopped PhoneSvc Phone Service
Stopped PimIndexMainten... Contact Data_1400f45
Stopped pla Performance Logs & Alerts
Running PlugPlay Plug and Play
Running PolicyAgent IPsec Policy Agent
Running Power Power
Stopped PrintNotify Printer Extensions and Notifications
Running ProfSvc User Profile Service
Running VMTools VMWare Service
Stopped PimIndexMainten... Contact Data_1400f45
Stopped pla Performance Logs & Alerts
Running PlugPlay Plug and Play
Running PolicyAgent IPsec Policy Agent
Running Power Power
Stopped PrintNotify Printer Extensions and Notifications
Running ProfSvc User Profile Service
Running Pulse Pulse by freeping.io
Stopped WiaRpc Still Image Acquisition Events
`,
virtRoleToExpect: VirtualSystemRoleGuest,
virtSystemToExpect: VirtualSystemVMWare,
},
{
rawData: `
Stopped AppIDSvc Application Identity
Stopped Appinfo Application Information
Stopped AppMgmt Application Management
Stopped AppReadiness App Readiness
Stopped AppVClient Microsoft App-V Client
Stopped AppXSvc AppX Deployment Service (AppXSVC)
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
Stopped Audiosrv Windows Audio
Stopped AxInstSV ActiveX Installer (AxInstSV)
Running BFE Base Filtering Engine
Stopped BITS Background Intelligent Transfer Ser...
Running BrokerInfrastru... Background Tasks Infrastructure Ser...
Stopped Browser Computer Browser
Stopped bthserv Bluetooth Support Service
Running CDPSvc Connected Devices Platform Service
Running CDPUserSvc_1400f45 CDPUserSvc_1400f45
Running CertPropSvc Certificate Propagation
Stopped ClipSVC Client License Service (ClipSVC)
Running vmicheartbeat Hyper-V Service
`,
virtSystemToExpect: VirtualSystemHyperV,
virtRoleToExpect: VirtualSystemRoleGuest,
},
{
rawData: `
Stopped AppIDSvc Application Identity
Stopped Appinfo Application Information
Stopped AppMgmt Application Management
Stopped AppReadiness App Readiness
Stopped AppVClient Microsoft App-V Client
Stopped AppXSvc AppX Deployment Service (AppXSVC)
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
Stopped Audiosrv Windows Audio
Stopped AxInstSV ActiveX Installer (AxInstSV)
Running BFE Base Filtering Engine
Stopped BITS Background Intelligent Transfer Ser...
Running BrokerInfrastru... Background Tasks Infrastructure Ser...
Stopped Browser Computer Browser
Stopped bthserv Bluetooth Support Service
Running CDPSvc Connected Devices Platform Service
Running CDPUserSvc_1400f45 CDPUserSvc_1400f45
Running vmcompute Hyper-V Service
`,
virtRoleToExpect: VirtualSystemRoleHost,
virtSystemToExpect: VirtualSystemHyperV,
},
{
rawData: `
Stopped AppIDSvc Application Identity
Stopped Appinfo Application Information
Stopped AppMgmt Application Management
Stopped AppReadiness App Readiness
Stopped AppVClient Microsoft App-V Client
Stopped AppXSvc AppX Deployment Service (AppXSVC)
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
`,
virtRoleToExpect: UnknownValue,
virtSystemToExpect: UnknownValue,
},
}
for _, testCase := range testCases {
virtSystemGiven, virtRoleGiven := getVirtInfoFromPowershellServicesList(testCase.rawData)
assert.Equal(t, testCase.virtRoleToExpect, virtRoleGiven)
assert.Equal(t, testCase.virtSystemToExpect, virtSystemGiven)
}
}
func TestGetVirtInfoFromNixDevicesList(t *testing.T) {
cases := []struct {
rawData string
expectedVirtSystem string
expectedVirtRole string
}{
{
rawData: `
0000 80861237 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0008 80867000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0009 80867010 0 1f0 3f6 170 376 e0a1 0 0 8 0 8 0 10 0 0 ata_piix
000a 80867020 b 0 0 0 0 e041 0 0 0 0 0 0 20 0 0 uhci_hcd
000b 80867113 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 piix4_smbus
0010 12341111 0 fd000008 0 fea50000 0 0 0 c0002 1000000 0 1000 0 0 0 20000
0018 1af41002 a e061 0 0 0 fe40000c 0 0 20 0 0 0 4000 0 0 virtio-pci
0028 1af41004 a e001 fea51000 0 0 fe40400c 0 0 40 1000 0 0 4000 0 0 virtio-pci
0090 1af41000 b e081 fea52000 0 0 fe40800c 0 fea00000 20 1000 0 0 4000 0 40000 virtio-pci
00f0 1b360001 a fea53004 0 0 0 0 0 0 100 0 0 0 0 0 0
00f8 1b360001 b fea54004 0 0 0 0 0 0 100 0 0 0 0 0 0
`,
expectedVirtRole: VirtualSystemRoleGuest,
expectedVirtSystem: VirtualSystemKVM,
},
{
rawData: `
b0a8 80869018 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0
b0b0 80962018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b0b4 80862020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b100 9005028f 20 f6100004 0 0 0 c002 0 0 8000 0 0 0 100 0 0 smartpqi
b202 80861572 22 f400000c 0 0 f610000c 0 0 f6080100 1000000 0 0 8000 0 0 80000 i40e
b204 80861572 22 f500000c 0 0 f600801c 0 0 0 1000000 0 0 8000 0 0 0 i40e
`,
expectedVirtRole: UnknownValue,
expectedVirtSystem: UnknownValue,
},
{
rawData: `
b0a8 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b0b0 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b0b4 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 hyperv_fb
2100 60861572 22 f400000c 0 0 f600002c 0 0 f6082000 1000000 0 0 8000 0 0 80000 i40e
a101 20861572 22 f500001c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
`,
expectedVirtRole: VirtualSystemRoleGuest,
expectedVirtSystem: VirtualSystemHyperV,
},
{
rawData: `
a0a9 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
r0b0 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
t0b4 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
w100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 vmwgfx
q200 80861532 22 f400100c 0 0 f600000c 0 0 f87080000 1000000 0 0 8000 0 0 80000 i40e
5201 80861572 22 f500000c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
`,
expectedVirtRole: VirtualSystemRoleGuest,
expectedVirtSystem: VirtualSystemVMWare,
},
{
rawData: `
b1a8 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
t0b0 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
q0b4 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 xen-platform-pci
21200 80861572 22 f400000c 0 0 f600000c 0 0 f6080000 1000000 0 0 8000 0 0 80000 i40e
a201 80861572 22 f500000c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
`,
expectedVirtRole: VirtualSystemRoleGuest,
expectedVirtSystem: VirtualSystemXen,
},
}
for _, tc := range cases {
actualVirtSystem, actualVirtRole := getVirtInfoFromNixDevicesList(tc.rawData)
assert.Equal(t, tc.expectedVirtSystem, actualVirtSystem)
assert.Equal(t, tc.expectedVirtRole, actualVirtRole)
}
}

View File

@ -75,6 +75,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
"os_arch": "amd64",
"os_family": "debian",
"os_kernel": "linux",
"os_full_name": "Debian",
"os_version": "5.4.0-37",
"os_virtualization_system":"KVM",
"os_virtualization_role":"guest",
"cpu_family":"59",
"cpu_model":"6",
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
"num_cpus":16,
"mem_total":67020316672,
"timezone":"UTC (UTC+00:00)",
"hostname": "my-devvm-v3",
"ipv4": [
"192.168.3.148"
@ -97,6 +107,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
"os_arch": "amd64",
"os_family": "alpine",
"os_kernel": "linux",
"os_full_name": "Alpine Linux",
"os_version": "4.19.80-0",
"os_virtualization_system":"",
"os_virtualization_role":"",
"cpu_family":"6",
"cpu_model":"79",
"cpu_model_name":"Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
"num_cpus":4,
"mem_total":8363900928,
"timezone":"CEST (UTC+02:00)",
"hostname": "alpine-3-10-tk-01",
"ipv4": [
"192.168.122.117"
@ -134,4 +154,4 @@ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/rportd
That's all.
Now you can use "0.0.0.0:443" as API address.
You need to run the above command everytime you change the rpotd binary, for example after every update.
You need to run the above command everytime you change the rpotd binary, for example after every update.

View File

@ -22,6 +22,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
"os_arch": "amd64",
"os_family": "debian",
"os_kernel": "linux",
"os_full_name": "Debian",
"os_version": "5.4.0-37",
"os_virtualization_system":"KVM",
"os_virtualization_role":"guest",
"cpu_family":"59",
"cpu_model":"6",
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
"num_cpus":16,
"mem_total":67020316672,
"timezone":"UTC (UTC+00:00)",
"hostname": "my-devvm-v3",
"ipv4": [
"192.168.3.148"
@ -44,6 +54,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
"os_arch": "amd64",
"os_family": "alpine",
"os_kernel": "linux",
"os_full_name": "Debian",
"os_version": "5.4.0-37",
"os_virtualization_system":"KVM",
"os_virtualization_role":"guest",
"cpu_family":"59",
"cpu_model":"6",
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
"num_cpus":16,
"mem_total":67020316672,
"timezone":"UTC (UTC+00:00)",
"hostname": "alpine-3-10-tk-01",
"ipv4": [
"192.168.122.117"

View File

@ -65,6 +65,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
"os_arch": "amd64",
"os_family": "debian",
"os_kernel": "linux",
"os_full_name": "Debian",
"os_version": "5.4.0-37",
"os_virtualization_system":"KVM",
"os_virtualization_role":"guest",
"cpu_family":"59",
"cpu_model":"6",
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
"num_cpus":16,
"mem_total":67020316672,
"timezone":"UTC (UTC+00:00)",
"hostname": "my-devvm-v3",
"ipv4": [
"192.168.3.148"

View File

@ -139,6 +139,15 @@ You can filter entries by `id`, `client_id`, `created_by`, `created_at`, `key` f
`http://localhost:3000/api/v1/vault?filter[key]=one` will list you entries with the key=one.
Note:
If you use curl to test filters, you should switch off URL globbing parser by providing `-g` flag (see curl documentation for the details), e.g.:
```
curl -g -X GET 'http://localhost:3000/api/v1/vault?filter[created_by]=admin' \
-u admin:foobaz \
-H 'Content-Type: application/json'
```
You can combine filters for multiple fields:
`http://localhost:3000/api/v1/vault?filter[client_id]=client123&filter[created_by]=admin` - gives you list of entries for client `client123` and created by `admin`
@ -179,7 +188,7 @@ The response will be
}
```
In the "value" field you will find the decrypted secure value. If `required_group` value of the stored vault entry is not empty, only users of this group can read this value, e.g. if `required_group` = 'admin' and the current user doesn't belong to this group, an error will be returned.
In the "value" field you will find the decrypted secure value. If `required_group` value of the stored vault entry is not empty, only users of this group can read this value, e.g. if `required_group` = 'Administrators' and the current user doesn't belong to this group, an error will be returned.
### Add a new secured value
@ -257,7 +266,7 @@ You can delete a vault entry by calling the following API:
```
curl -X DELETE 'http://localhost:3000/api/v1/vault/1' \
-u admin:foobaz'
-u admin:foobaz
```
If `required_group` value of the entry you want to delete is not empty, only users of this group can change this value, otherwise an error will be returned.

View File

@ -14,6 +14,8 @@ import (
"strings"
"time"
"github.com/cloudradar-monitoring/rport/share/query"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/tomasen/realip"
@ -565,21 +567,34 @@ func (al *APIListener) handleGetStatus(w http.ResponseWriter, req *http.Request)
}
func (al *APIListener) handleGetClients(w http.ResponseWriter, req *http.Request) {
var err error
sortFunc, desc, err := getCorrespondingSortFunc(req.URL.Query().Get(queryParamSort))
if err != nil {
al.jsonErrorResponse(w, http.StatusBadRequest, err)
return
}
clients, err := al.clientService.GetAll()
if err != nil {
al.jsonErrorResponse(w, http.StatusInternalServerError, err)
filterOptions := query.ExtractFilterOptions(req)
filterErr := query.ValidateFilterOptions(filterOptions, clientsSupportedFields)
if filterErr != nil {
al.jsonError(w, filterErr)
return
}
sortFunc(clients, desc)
var cls []*clients.Client
if len(filterOptions) > 0 {
cls, err = al.clientService.GetFiltered(filterOptions)
} else {
cls, err = al.clientService.GetAll()
}
if err != nil {
al.jsonError(w, err)
return
}
clientsPayload := convertToClientsPayload(clients)
sortFunc(cls, desc)
clientsPayload := convertToClientsPayload(cls)
al.writeJSONResponse(w, http.StatusOK, api.NewSuccessPayload(clientsPayload))
}
@ -661,44 +676,64 @@ func (al *APIListener) handleDeleteUser(w http.ResponseWriter, req *http.Request
}
type ClientPayload struct {
ID string `json:"id"`
Name string `json:"name"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
Hostname string `json:"hostname"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Version string `json:"version"`
Address string `json:"address"`
Tunnels []*clients.Tunnel `json:"tunnels"`
DisconnectedAt *time.Time `json:"disconnected_at"`
ConnectionState clients.ConnectionState `json:"connection_state"`
ClientAuthID string `json:"client_auth_id"`
ID string `json:"id"`
Name string `json:"name"`
DisconnectedAt *time.Time `json:"disconnected_at"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
Hostname string `json:"hostname"`
OSFullName string `json:"os_full_name"`
OSVersion string `json:"os_version"`
OSVirtualizationSystem string `json:"os_virtualization_system"`
OSVirtualizationRole string `json:"os_virtualization_role"`
NumCPUs int `json:"num_cpus"`
CPUFamily string `json:"cpu_family"`
CPUModel string `json:"cpu_model"`
CPUModelName string `json:"cpu_model_name"`
MemoryTotal uint64 `json:"mem_total"`
Timezone string `json:"timezone"`
Address string `json:"address"`
ClientAuthID string `json:"client_auth_id"`
Version string `json:"version"`
ConnectionState clients.ConnectionState `json:"connection_state"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Tunnels []*clients.Tunnel `json:"tunnels"`
}
func convertToClientsPayload(clients []*clients.Client) []ClientPayload {
r := make([]ClientPayload, 0, len(clients))
for _, cur := range clients {
r = append(r, ClientPayload{
ID: cur.ID,
Name: cur.Name,
OS: cur.OS,
OSArch: cur.OSArch,
OSFamily: cur.OSFamily,
OSKernel: cur.OSKernel,
Hostname: cur.Hostname,
IPv4: cur.IPv4,
IPv6: cur.IPv6,
Tags: cur.Tags,
Version: cur.Version,
Address: cur.Address,
Tunnels: cur.Tunnels,
DisconnectedAt: cur.DisconnectedAt,
ConnectionState: cur.ConnectionState(),
ClientAuthID: cur.ClientAuthID,
ID: cur.ID,
Name: cur.Name,
OS: cur.OS,
OSArch: cur.OSArch,
OSFamily: cur.OSFamily,
OSKernel: cur.OSKernel,
Hostname: cur.Hostname,
IPv4: cur.IPv4,
IPv6: cur.IPv6,
Tags: cur.Tags,
Version: cur.Version,
Address: cur.Address,
Tunnels: cur.Tunnels,
DisconnectedAt: cur.DisconnectedAt,
ConnectionState: cur.ConnectionState(),
ClientAuthID: cur.ClientAuthID,
OSFullName: cur.OSFullName,
OSVersion: cur.OSVersion,
OSVirtualizationSystem: cur.OSVirtualizationSystem,
OSVirtualizationRole: cur.OSVirtualizationRole,
CPUFamily: cur.CPUFamily,
CPUModel: cur.CPUModel,
CPUModelName: cur.CPUModelName,
Timezone: cur.Timezone,
NumCPUs: cur.NumCPUs,
MemoryTotal: cur.MemoryTotal,
})
}
return r

View File

@ -127,7 +127,7 @@ func NewAPIListener(
}
scriptLogger := chshare.NewLogger("scripts", config.Logging.LogOutput, config.Logging.LogLevel)
scriptDb, err := script.NewSqliteProvider(path.Join(config.Server.DataDir, "scripts.db"), scriptLogger)
scriptDb, err := script.NewSqliteProvider(path.Join(config.Server.DataDir, "library.db"), scriptLogger)
if err != nil {
return nil, err
}

View File

@ -583,7 +583,7 @@ func TestHandleDeleteClient(t *testing.T) {
al := APIListener{
insecureForTests: true,
Server: &Server{
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour)),
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour, testLog)),
config: &Config{
Server: ServerConfig{
AuthWrite: tc.clientAuthWrite,
@ -819,7 +819,7 @@ func TestHandlePostCommand(t *testing.T) {
al := APIListener{
insecureForTests: true,
Server: &Server{
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour)),
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour, testLog)),
config: &Config{
Server: ServerConfig{
RunRemoteCmdTimeoutSec: defaultTimeout,
@ -1054,7 +1054,7 @@ func TestHandleGetClients(t *testing.T) {
al := APIListener{
insecureForTests: true,
Server: &Server{
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2}, &hour)),
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2}, &hour, testLog)),
config: &Config{
Server: ServerConfig{MaxRequestBytes: 1024 * 1024},
},
@ -1070,11 +1070,17 @@ func TestHandleGetClients(t *testing.T) {
"data":[
{
"id":"client-1",
"mem_total":100000,
"name":"Random Rport Client",
"num_cpus":2,
"os":"Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
"os_arch":"amd64",
"os_family":"alpine",
"os_full_name":"Debian 18.0",
"os_kernel":"linux",
"os_version":"18.0",
"os_virtualization_role":"guest",
"os_virtualization_system":"LVM",
"hostname":"alpine-3-10-tk-01",
"ipv4":[
"192.168.122.111"
@ -1088,6 +1094,7 @@ func TestHandleGetClients(t *testing.T) {
],
"version":"0.1.12",
"address":"88.198.189.161:50078",
"timezone":"UTC-0",
"tunnels":[
{
"lhost":"0.0.0.0",
@ -1113,16 +1120,25 @@ func TestHandleGetClients(t *testing.T) {
}
],
"connection_state":"connected",
"cpu_family":"Virtual CPU",
"cpu_model":"Virtual CPU",
"cpu_model_name":"",
"disconnected_at":null,
"client_auth_id":"user1"
},
{
"id":"client-2",
"mem_total":100000,
"name":"Random Rport Client",
"num_cpus":2,
"os":"Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
"os_arch":"amd64",
"os_family":"alpine",
"os_full_name":"Debian 18.0",
"os_kernel":"linux",
"os_version": "18.0",
"os_virtualization_role":"guest",
"os_virtualization_system":"LVM",
"hostname":"alpine-3-10-tk-01",
"ipv4":[
"192.168.122.111"
@ -1136,6 +1152,7 @@ func TestHandleGetClients(t *testing.T) {
],
"version":"0.1.12",
"address":"88.198.189.161:50078",
"timezone":"UTC-0",
"tunnels":[
{
"lhost":"0.0.0.0",
@ -1161,12 +1178,14 @@ func TestHandleGetClients(t *testing.T) {
}
],
"connection_state":"disconnected",
"cpu_family":"Virtual CPU",
"cpu_model":"Virtual CPU",
"cpu_model_name":"",
"disconnected_at":"2020-08-19T13:04:23+03:00",
"client_auth_id":"user1"
}
]
}`
assert.Equal(t, 200, w.Code)
assert.JSONEq(t, expectedJSON, w.Body.String())
}
@ -1286,7 +1305,7 @@ func TestHandlePostMultiClientCommand(t *testing.T) {
al := APIListener{
insecureForTests: true,
Server: &Server{
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2, c3}, &hour)),
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2, c3}, &hour, testLog)),
config: &Config{
Server: ServerConfig{
RunRemoteCmdTimeoutSec: defaultTimeout,

View File

@ -10,6 +10,8 @@ import (
"sync"
"time"
"github.com/cloudradar-monitoring/rport/share/query"
"golang.org/x/crypto/ssh"
"github.com/cloudradar-monitoring/rport/server/api/errors"
@ -26,6 +28,18 @@ type ClientService struct {
mu sync.Mutex
}
var clientsSupportedFields = map[string]bool{
"os_full_name": true,
"os_virtualization_system": true,
"os_virtualization_role": true,
"cpu_model_name": true,
"timezone": true,
"os_version": true,
"cpu_family": true,
"cpu_model": true,
"num_cpus": true,
}
// NewClientService returns a new instance of client service.
func NewClientService(
portDistributor *ports.PortDistributor,
@ -42,8 +56,9 @@ func InitClientService(
portDistributor *ports.PortDistributor,
provider clients.ClientProvider,
keepLostClients *time.Duration,
logger *chshare.Logger,
) (*ClientService, error) {
repo, err := clients.InitClientRepository(ctx, provider, keepLostClients)
repo, err := clients.InitClientRepository(ctx, provider, keepLostClients, logger)
if err != nil {
return nil, fmt.Errorf("failed to init Client Repository: %v", err)
}
@ -111,6 +126,10 @@ func (s *ClientService) GetAll() ([]*clients.Client, error) {
return s.repo.GetAll()
}
func (s *ClientService) GetFiltered(filterOptions []query.FilterOption) ([]*clients.Client, error) {
return s.repo.GetFiltered(filterOptions)
}
func (s *ClientService) StartClient(
ctx context.Context, clientAuthID, clientID string, sshConn ssh.Conn, authMultiuseCreds bool,
req *chshare.ConnectionRequest, clog *chshare.Logger,
@ -148,23 +167,34 @@ func (s *ClientService) StartClient(
}
client := &clients.Client{
ID: clientID,
ClientAuthID: clientAuthID,
Name: req.Name,
Tags: req.Tags,
OS: req.OS,
OSArch: req.OSArch,
OSFamily: req.OSFamily,
OSKernel: req.OSKernel,
Hostname: req.Hostname,
Version: req.Version,
IPv4: req.IPv4,
IPv6: req.IPv6,
Address: clientHost,
Tunnels: make([]*clients.Tunnel, 0),
Connection: sshConn,
Context: ctx,
Logger: clog,
ID: clientID,
Name: req.Name,
OS: req.OS,
OSArch: req.OSArch,
OSFamily: req.OSFamily,
OSKernel: req.OSKernel,
OSFullName: req.OSFullName,
OSVersion: req.OSVersion,
OSVirtualizationSystem: req.OSVirtualizationSystem,
OSVirtualizationRole: req.OSVirtualizationRole,
Hostname: req.Hostname,
CPUFamily: req.OSFamily,
CPUModel: req.CPUModel,
CPUModelName: req.CPUModelName,
NumCPUs: req.NumCPUs,
MemoryTotal: req.MemoryTotal,
Timezone: req.Timezone,
IPv4: req.IPv4,
IPv6: req.IPv6,
Tags: req.Tags,
Version: req.Version,
Address: clientHost,
Tunnels: make([]*clients.Tunnel, 0),
DisconnectedAt: nil,
ClientAuthID: clientAuthID,
Connection: sshConn,
Context: ctx,
Logger: clog,
}
_, err = s.startClientTunnels(client, req.Remotes)

View File

@ -84,7 +84,7 @@ func TestStartClient(t *testing.T) {
repo: clients.NewClientRepository([]*clients.Client{{
ID: "test-client",
ClientAuthID: "test-client-auth",
}}, nil),
}}, nil, testLog),
portDistributor: ports.NewPortDistributor(mapset.NewThreadUnsafeSet()),
}
_, err := cs.StartClient(
@ -140,7 +140,7 @@ func TestDeleteOfflineClient(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// given
clientService := NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1Active, c2Active, c3Offline, c4Offline}, &hour))
clientService := NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1Active, c2Active, c3Offline, c4Offline}, &hour, testLog))
before, err := clientService.Count()
require.NoError(t, err)
require.Equal(t, 4, before)

View File

@ -18,7 +18,7 @@ func TestCleanup(t *testing.T) {
clients := []*Client{c1, c2, c3}
p := newFakeClientProvider(t, hour, c1, c2, c3)
defer p.Close()
repo := newClientRepositoryWithDB(clients, &hour, p)
repo := newClientRepositoryWithDB(clients, &hour, p, testLog)
require.Len(t, repo.clients, 3)
gotObsolete, err := p.get(ctx, c3.ID)
require.NoError(t, err)

View File

@ -26,19 +26,29 @@ const (
// Client represents client connection
type Client struct {
ID string `json:"id"`
Name string `json:"name"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
Hostname string `json:"hostname"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Version string `json:"version"`
Address string `json:"address"`
Tunnels []*Tunnel `json:"tunnels"`
ID string `json:"id"`
Name string `json:"name"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
OSFullName string `json:"os_full_name"`
OSVersion string `json:"os_version"`
OSVirtualizationSystem string `json:"os_virtualization_system"`
OSVirtualizationRole string `json:"os_virtualization_role"`
CPUFamily string `json:"cpu_family"`
CPUModel string `json:"cpu_model"`
CPUModelName string `json:"cpu_model_name"`
NumCPUs int `json:"num_cpus"`
MemoryTotal uint64 `json:"mem_total"`
Timezone string `json:"timezone"`
Hostname string `json:"hostname"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Version string `json:"version"`
Address string `json:"address"`
Tunnels []*Tunnel `json:"tunnels"`
// DisconnectedAt is a time when a client was disconnected. If nil - it's connected.
DisconnectedAt *time.Time `json:"disconnected_at"`
ClientAuthID string `json:"client_auth_id"`

View File

@ -69,18 +69,28 @@ func (b ClientBuilder) Connection(conn ssh.Conn) ClientBuilder {
func (b ClientBuilder) Build() *Client {
return &Client{
ID: b.id,
Name: "Random Rport Client",
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
Hostname: "alpine-3-10-tk-01",
IPv4: []string{"192.168.122.111"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
Tags: []string{"Linux", "Datacenter 1"},
Version: "0.1.12",
Address: "88.198.189.161:50078",
NumCPUs: 2,
MemoryTotal: 100000,
ID: b.id,
Name: "Random Rport Client",
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
OSFullName: "Debian 18.0",
OSVersion: "18.0",
OSVirtualizationSystem: "LVM",
OSVirtualizationRole: "guest",
CPUFamily: "Virtual CPU",
CPUModel: "Virtual CPU",
CPUModelName: "",
Timezone: "UTC-0",
Hostname: "alpine-3-10-tk-01",
IPv4: []string{"192.168.122.111"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
Tags: []string{"Linux", "Datacenter 1"},
Version: "0.1.12",
Address: "88.198.189.161:50078",
Tunnels: []*Tunnel{
{
ID: "1",
@ -103,10 +113,8 @@ func (b ClientBuilder) Build() *Client {
},
DisconnectedAt: b.disconnectedAt,
ClientAuthID: b.clientAuthID,
Connection: b.conn,
Connection: b.conn,
}
}
func generateRandomClientAuthID() string {

View File

@ -2,9 +2,15 @@ package clients
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"sync"
"time"
chshare "github.com/cloudradar-monitoring/rport/share"
"github.com/cloudradar-monitoring/rport/share/query"
)
type ClientRepository struct {
@ -14,16 +20,17 @@ type ClientRepository struct {
KeepLostClients *time.Duration
// storage
provider ClientProvider
logger *chshare.Logger
}
// NewClientRepository returns a new thread-safe in-memory cache to store client connections populated with given clients if any.
// keepLostClients is a duration to keep disconnected clients. If a client was disconnected longer than a given
// duration it will be treated as obsolete.
func NewClientRepository(initClients []*Client, keepLostClients *time.Duration) *ClientRepository {
return newClientRepositoryWithDB(initClients, keepLostClients, nil)
func NewClientRepository(initClients []*Client, keepLostClients *time.Duration, logger *chshare.Logger) *ClientRepository {
return newClientRepositoryWithDB(initClients, keepLostClients, nil, logger)
}
func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Duration, provider ClientProvider) *ClientRepository {
func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Duration, provider ClientProvider, logger *chshare.Logger) *ClientRepository {
clients := make(map[string]*Client)
for i := range initClients {
clients[initClients[i].ID] = initClients[i]
@ -32,16 +39,22 @@ func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Dura
clients: clients,
KeepLostClients: keepLostClients,
provider: provider,
logger: logger,
}
}
func InitClientRepository(ctx context.Context, provider ClientProvider, keepLostClients *time.Duration) (*ClientRepository, error) {
func InitClientRepository(
ctx context.Context,
provider ClientProvider,
keepLostClients *time.Duration,
logger *chshare.Logger,
) (*ClientRepository, error) {
initClients, err := GetInitState(ctx, provider)
if err != nil {
return nil, err
}
return newClientRepositoryWithDB(initClients, keepLostClients, provider), nil
return newClientRepositoryWithDB(initClients, keepLostClients, provider, logger), nil
}
func (s *ClientRepository) Save(client *Client) error {
@ -127,7 +140,7 @@ func (s *ClientRepository) CountDisconnected() (int, error) {
return n, nil
}
// GetActiveByID returns non-obsolete active or disconnected client by a given id.
// GetByID returns non-obsolete active or disconnected client by a given id.
func (s *ClientRepository) GetByID(id string) (*Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@ -168,6 +181,13 @@ func (s *ClientRepository) GetAll() ([]*Client, error) {
return s.getNonObsolete()
}
// GetFiltered returns all non-obsolete active and disconnected clients filtered by os parameters
func (s *ClientRepository) GetFiltered(filterOptions []query.FilterOption) ([]*Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.getNonObsoleteFiltered(filterOptions)
}
func (s *ClientRepository) GetAllActive() []*Client {
s.mu.RLock()
defer s.mu.RUnlock()
@ -189,3 +209,87 @@ func (s *ClientRepository) getNonObsolete() ([]*Client, error) {
}
return result, nil
}
func (s *ClientRepository) getNonObsoleteFiltered(filterOptions []query.FilterOption) ([]*Client, error) {
result := make([]*Client, 0, len(s.clients))
for _, client := range s.clients {
if !client.Obsolete(s.KeepLostClients) {
matches, err := s.clientMatchesFilters(client, filterOptions)
if err != nil {
return result, err
}
if !matches {
continue
}
result = append(result, client)
}
}
return result, nil
}
func (s *ClientRepository) clientMatchesFilters(cl *Client, filterOptions []query.FilterOption) (bool, error) {
for _, f := range filterOptions {
matches, err := s.clientMatchesFilter(cl, f)
if err != nil {
return false, err
}
if !matches {
return false, nil
}
}
return true, nil
}
func (s *ClientRepository) clientMatchesFilter(cl *Client, filter query.FilterOption) (bool, error) {
clientMap, err := s.clientToMap(cl)
if err != nil {
return false, err
}
clientFieldValueToMatch, ok := clientMap[filter.Column]
if !ok {
return false, fmt.Errorf("unsupported filter column: %s", filter.Column)
}
clientFieldValueToMatchStr := fmt.Sprint(clientFieldValueToMatch)
regx := regexp.MustCompile(`[^\\]\*+`)
for _, filterValue := range filter.Values {
hasUnescapedWildCard := regx.MatchString(filterValue)
if !hasUnescapedWildCard {
if filterValue == clientFieldValueToMatchStr {
return true, nil
}
continue
}
filterValueRegex, err := regexp.Compile(strings.ReplaceAll(filterValue, "*", ".*"))
if err != nil {
s.logger.Errorf("failed to generate regex for '%s': %v", filterValue, err)
if filterValue == clientFieldValueToMatchStr {
return true, nil
}
continue
}
if filterValueRegex.MatchString(clientFieldValueToMatchStr) {
return true, nil
}
}
return false, nil
}
func (s *ClientRepository) clientToMap(cl *Client) (map[string]interface{}, error) {
clientBytes, err := json.Marshal(cl)
if err != nil {
return nil, err
}
res := make(map[string]interface{})
err = json.Unmarshal(clientBytes, &res)
return res, err
}

View File

@ -4,6 +4,8 @@ import (
"testing"
"time"
"github.com/cloudradar-monitoring/rport/share/query"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -12,7 +14,7 @@ func TestCRWithExpiration(t *testing.T) {
now = nowMockF
exp := 2 * time.Hour
repo := NewClientRepository([]*Client{c1, c2}, &exp)
repo := NewClientRepository([]*Client{c1, c2}, &exp, testLog)
assert := assert.New(t)
assert.NoError(repo.Save(c3))
@ -61,7 +63,7 @@ func TestCRWithExpiration(t *testing.T) {
func TestCRWithNoExpiration(t *testing.T) {
now = nowMockF
repo := NewClientRepository([]*Client{c1, c2, c3}, nil)
repo := NewClientRepository([]*Client{c1, c2, c3}, nil, testLog)
c4Active := shallowCopy(c4)
c4Active.DisconnectedAt = nil
@ -103,3 +105,162 @@ func TestCRWithNoExpiration(t *testing.T) {
assert.NoError(err)
assert.ElementsMatch([]*Client{c1, c2, c3}, gotClients)
}
func TestCRWithFilter(t *testing.T) {
testCases := []struct {
filters []query.FilterOption
expectedClientIDs []string
}{
{
filters: []query.FilterOption{
{
Column: "os_full_name",
Values: []string{
"Alpine Linux",
},
},
},
expectedClientIDs: []string{
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
},
},
{
filters: []query.FilterOption{
{
Column: "os_full_name",
Values: []string{
"Alpine*",
},
},
},
expectedClientIDs: []string{
"aa1210c7-1899-491e-8e71-564cacaf1df8",
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
},
},
{
filters: []query.FilterOption{
{
Column: "os_full_name",
Values: []string{
"Alpine*",
"Microsoft Windows Server 2016 Standard",
},
},
},
expectedClientIDs: []string{
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
"aa1210c7-1899-491e-8e71-564cacaf1df8",
"daflkdfjqlkerlkejrqlwedalfdfadfa",
},
},
{
filters: []query.FilterOption{
{
Column: "os_virtualization_system",
Values: []string{
"KVM",
"Microsoft Windows Server 2016 Standard",
},
},
{
Column: "os_virtualization_role",
Values: []string{
"guest",
},
},
},
expectedClientIDs: []string{
"aa1210c7-1899-491e-8e71-564cacaf1df8",
},
},
{
filters: []query.FilterOption{
{
Column: "os_full_name",
Values: []string{
"Oracle",
},
},
},
expectedClientIDs: []string{},
},
{
filters: []query.FilterOption{
{
Column: "os_full_name",
Values: []string{
"Microsoft Windows Server 2016 Standard",
},
},
{
Column: "os_version",
Values: []string{
"10.0.14393 Build 14393",
},
},
{
Column: "cpu_family",
Values: []string{
"1",
},
},
{
Column: "cpu_model",
Values: []string{
"4",
},
},
{
Column: "cpu_model_name",
Values: []string{
"Intel*",
},
},
{
Column: "num_cpus",
Values: []string{
"2",
},
},
{
Column: "timezone",
Values: []string{
"UTC*",
},
},
},
expectedClientIDs: []string{
"daflkdfjqlkerlkejrqlwedalfdfadfa",
},
},
}
for _, testcase := range testCases {
repo := NewClientRepository([]*Client{c1, c2, c5}, nil, testLog)
actualClients, err := repo.GetFiltered(testcase.filters)
require.NoError(t, err)
actualClientIDs := make([]string, 0, len(actualClients))
for _, actualClient := range actualClients {
actualClientIDs = append(actualClientIDs, actualClient.ID)
}
assert.ElementsMatch(t, testcase.expectedClientIDs, actualClientIDs)
}
}
func TestCRWithUnsupportedFilter(t *testing.T) {
repo := NewClientRepository([]*Client{c1}, nil, testLog)
_, err := repo.GetFiltered([]query.FilterOption{
{
Column: "unknown_field",
Values: []string{
"1",
},
},
})
require.EqualError(t, err, "unsupported filter column: unknown_field")
}

View File

@ -22,18 +22,28 @@ var (
)
var c1 = &Client{
ID: "aa1210c7-1899-491e-8e71-564cacaf1df8",
Name: "Random Rport Client 1",
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
Hostname: "alpine-3-10-tk-01",
IPv4: []string{"192.168.122.111"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
Tags: []string{"Linux", "Datacenter 1"},
Version: "0.1.12",
Address: "88.198.189.161:50078",
ID: "aa1210c7-1899-491e-8e71-564cacaf1df8",
Name: "Random Rport Client 1",
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
OSFullName: "Alpine",
OSVersion: "3.14.0",
OSVirtualizationRole: "guest",
OSVirtualizationSystem: "KVM",
CPUFamily: "6",
CPUModel: "79",
CPUModelName: "Common KVM processor",
NumCPUs: 2,
MemoryTotal: 1000000,
Timezone: "CEST (UTC+02:00)",
Hostname: "alpine-3-10-tk-01",
IPv4: []string{"192.168.122.111"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
Tags: []string{"Linux", "Datacenter 1"},
Version: "0.1.12",
Address: "88.198.189.161:50078",
Tunnels: []*Tunnel{
{
ID: "1",
@ -59,18 +69,28 @@ var c1 = &Client{
}
var c2 = &Client{
ID: "2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
Name: "Random Rport Client 2",
OS: "Linux alpine-3-10-tk-02 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
Hostname: "alpine-3-10-tk-02",
IPv4: []string{"192.168.122.112"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b2"},
Tags: []string{"Linux", "Datacenter 2"},
Version: "0.1.12",
Address: "88.198.189.162:50078",
ID: "2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
Name: "Random Rport Client 2",
OS: "Linux alpine-3-10-tk-02 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
OSArch: "amd64",
OSFamily: "alpine",
OSKernel: "linux",
OSFullName: "Alpine Linux",
OSVersion: "2.0.0",
OSVirtualizationRole: "",
OSVirtualizationSystem: "",
CPUFamily: "5",
CPUModel: "33",
CPUModelName: "Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
NumCPUs: 4,
MemoryTotal: 1500000,
Timezone: "CEST (UTC+00:00)",
Hostname: "alpine-3-10-tk-02",
IPv4: []string{"192.168.122.112"},
IPv6: []string{"fe80::b84f:aff:fe59:a0b2"},
Tags: []string{"Linux", "Datacenter 2"},
Version: "0.1.12",
Address: "88.198.189.162:50078",
Tunnels: []*Tunnel{
{
ID: "1",
@ -122,6 +142,34 @@ var c4 = &Client{
DisconnectedAt: &c4DisconnectedTime,
}
var c5 = &Client{
ID: "daflkdfjqlkerlkejrqlwedalfdfadfa",
Name: "Windows Client",
OS: "Windows",
OSArch: "x86_64",
OSFamily: "Server",
OSKernel: "10.0.1 4393 Build 14393",
OSFullName: "Microsoft Windows Server 2016 Standard",
OSVersion: "10.0.14393 Build 14393",
OSVirtualizationRole: "",
OSVirtualizationSystem: "",
CPUFamily: "1",
CPUModel: "4",
CPUModelName: "Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
NumCPUs: 2,
MemoryTotal: 4294422528,
Timezone: "PDT (UTC-07:00)",
Hostname: "RPORT-WIN-SRV2016",
IPv4: []string{"192.168.122.124"},
IPv6: []string{"fe80::b84f:aff:fe56:a0b4"},
Tags: []string{"Linux", "Datacenter 4"},
Version: "0.1.12",
Address: "88.198.189.124:50078",
Tunnels: make([]*Tunnel, 0),
ClientAuthID: "client-5",
DisconnectedAt: &c4DisconnectedTime,
}
// shallowCopy is used only in tests.
func shallowCopy(c *Client) *Client {
if c == nil {
@ -129,21 +177,31 @@ func shallowCopy(c *Client) *Client {
}
return &Client{
ID: c.ID,
Name: c.Name,
OS: c.OS,
OSArch: c.OSArch,
OSFamily: c.OSFamily,
OSKernel: c.OSKernel,
Hostname: c.Hostname,
IPv4: append([]string{}, c.IPv4...),
IPv6: append([]string{}, c.IPv6...),
Tags: append([]string{}, c.Tags...),
Version: c.Version,
Address: c.Address,
Tunnels: append([]*Tunnel{}, c.Tunnels...),
DisconnectedAt: c.DisconnectedAt,
ClientAuthID: c.ClientAuthID,
NumCPUs: c.NumCPUs,
MemoryTotal: c.MemoryTotal,
ID: c.ID,
Name: c.Name,
OS: c.OS,
OSArch: c.OSArch,
OSFamily: c.OSFamily,
OSKernel: c.OSKernel,
OSFullName: c.OSFullName,
OSVersion: c.OSVersion,
OSVirtualizationSystem: c.OSVirtualizationSystem,
OSVirtualizationRole: c.OSVirtualizationRole,
CPUFamily: c.CPUFamily,
CPUModel: c.CPUModel,
CPUModelName: c.CPUModelName,
Timezone: c.Timezone,
Hostname: c.Hostname,
IPv4: append([]string{}, c.IPv4...),
IPv6: append([]string{}, c.IPv6...),
Tags: append([]string{}, c.Tags...),
Version: c.Version,
Address: c.Address,
Tunnels: append([]*Tunnel{}, c.Tunnels...),
DisconnectedAt: c.DisconnectedAt,
ClientAuthID: c.ClientAuthID,
}
}

View File

@ -97,18 +97,28 @@ func convertToSqlite(v *Client) *clientSqlite {
ID: v.ID,
ClientAuthID: v.ClientAuthID,
Details: &clientDetails{
Name: v.Name,
OS: v.OS,
OSArch: v.OSArch,
OSFamily: v.OSFamily,
OSKernel: v.OSKernel,
Hostname: v.Hostname,
Version: v.Version,
Address: v.Address,
IPv4: v.IPv4,
IPv6: v.IPv6,
Tags: v.Tags,
Tunnels: v.Tunnels,
Name: v.Name,
OS: v.OS,
OSArch: v.OSArch,
OSFamily: v.OSFamily,
OSKernel: v.OSKernel,
Hostname: v.Hostname,
Version: v.Version,
Address: v.Address,
OSFullName: v.OSFullName,
OSVersion: v.OSVersion,
OSVirtualizationSystem: v.OSVirtualizationSystem,
OSVirtualizationRole: v.OSVirtualizationRole,
CPUFamily: v.CPUFamily,
CPUModel: v.CPUModel,
CPUModelName: v.CPUModelName,
NumCPUs: v.NumCPUs,
MemoryTotal: v.MemoryTotal,
Timezone: v.Timezone,
IPv4: v.IPv4,
IPv6: v.IPv6,
Tags: v.Tags,
Tunnels: v.Tunnels,
},
}
if v.DisconnectedAt != nil {
@ -125,18 +135,28 @@ type clientSqlite struct {
}
type clientDetails struct {
Name string `json:"name"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
Hostname string `json:"hostname"`
Version string `json:"version"`
Address string `json:"address"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Tunnels []*Tunnel `json:"tunnels"`
NumCPUs int `json:"num_cpus"`
MemoryTotal uint64 `json:"mem_total"`
Name string `json:"name"`
OS string `json:"os"`
OSArch string `json:"os_arch"`
OSFamily string `json:"os_family"`
OSKernel string `json:"os_kernel"`
OSFullName string `json:"os_full_name"`
OSVersion string `json:"os_version"`
OSVirtualizationSystem string `json:"os_virtualization_system"`
OSVirtualizationRole string `json:"os_virtualization_role"`
CPUFamily string `json:"cpu_family"`
CPUModel string `json:"cpu_model"`
CPUModelName string `json:"cpu_model_name"`
Timezone string `json:"timezone"`
Hostname string `json:"hostname"`
Version string `json:"version"`
Address string `json:"address"`
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
Tags []string `json:"tags"`
Tunnels []*Tunnel `json:"tunnels"`
}
func (d *clientDetails) Scan(value interface{}) error {
@ -168,20 +188,30 @@ func (d *clientDetails) Value() (driver.Value, error) {
func (s *clientSqlite) convert() *Client {
d := s.Details
res := &Client{
ID: s.ID,
ClientAuthID: s.ClientAuthID,
Name: d.Name,
OS: d.OS,
OSArch: d.OSArch,
OSFamily: d.OSFamily,
OSKernel: d.OSKernel,
Hostname: d.Hostname,
IPv4: d.IPv4,
IPv6: d.IPv6,
Tags: d.Tags,
Version: d.Version,
Address: d.Address,
Tunnels: d.Tunnels,
ID: s.ID,
ClientAuthID: s.ClientAuthID,
Name: d.Name,
OS: d.OS,
OSArch: d.OSArch,
OSFamily: d.OSFamily,
OSKernel: d.OSKernel,
Hostname: d.Hostname,
IPv4: d.IPv4,
IPv6: d.IPv6,
Tags: d.Tags,
Version: d.Version,
Address: d.Address,
Tunnels: d.Tunnels,
OSFullName: d.OSFullName,
OSVersion: d.OSVersion,
OSVirtualizationSystem: d.OSVirtualizationSystem,
OSVirtualizationRole: d.OSVirtualizationRole,
CPUFamily: d.CPUFamily,
CPUModel: d.CPUModel,
CPUModelName: d.CPUModelName,
NumCPUs: d.NumCPUs,
MemoryTotal: d.MemoryTotal,
Timezone: d.Timezone,
}
if s.DisconnectedAt.Valid {
res.DisconnectedAt = &s.DisconnectedAt.Time

View File

@ -44,7 +44,7 @@ func NewManager(db DbProvider, ex *Executor, logger *chshare.Logger) *Manager {
}
func (m *Manager) List(ctx context.Context, re *http.Request) ([]Script, error) {
listOptions := query.ConvertGetParamsToFilterOptions(re)
listOptions := query.GetSortAndFilterOptions(re)
err := query.ValidateListOptions(listOptions, supportedFields)
if err != nil {

View File

@ -108,6 +108,7 @@ func NewServer(config *Config, filesAPI files.FileAPI) (*Server, error) {
ports.NewPortDistributor(config.AllowedPorts()),
s.clientProvider,
keepLostClients,
s.Logger,
)
if err != nil {
return nil, err

View File

@ -253,7 +253,7 @@ func (m *Manager) List(ctx context.Context, re *http.Request) ([]ValueKey, error
return nil, err
}
listOptions := query.ConvertGetParamsToFilterOptions(re)
listOptions := query.GetSortAndFilterOptions(re)
err = query.ValidateListOptions(listOptions, supportedFields)
if err != nil {

View File

@ -7,18 +7,28 @@ import (
// ConnectionRequest represents configuration options when initiating client-server connection
type ConnectionRequest struct {
Version string
ID string
Name string
OS string
OSArch string
OSFamily string
OSKernel string
Hostname string
IPv4 []string
IPv6 []string
Tags []string
Remotes []*Remote
ID string
Name string
OS string
OSFullName string
OSVersion string
OSVirtualizationSystem string
OSVirtualizationRole string
OSArch string
OSFamily string
OSKernel string
Version string
Hostname string
CPUFamily string
CPUModel string
CPUModelName string
NumCPUs int
MemoryTotal uint64
Timezone string
IPv4 []string
IPv6 []string
Tags []string
Remotes []*Remote
}
func DecodeConnectionRequest(b []byte) (*ConnectionRequest, error) {

View File

@ -24,14 +24,14 @@ type ListOptions struct {
Filters []FilterOption
}
func ConvertGetParamsToFilterOptions(req *http.Request) *ListOptions {
func GetSortAndFilterOptions(req *http.Request) *ListOptions {
return &ListOptions{
Sorts: extractSortOptions(req),
Filters: extractFilterOptions(req),
Sorts: ExtractSortOptions(req),
Filters: ExtractFilterOptions(req),
}
}
func extractSortOptions(req *http.Request) []SortOption {
func ExtractSortOptions(req *http.Request) []SortOption {
res := make([]SortOption, 0)
query := req.URL.Query()
@ -61,23 +61,13 @@ func extractSortOptions(req *http.Request) []SortOption {
return res
}
func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error {
func ValidateFilterOptions(fo []FilterOption, supportedFields map[string]bool) errors2.APIErrors {
errs := errors2.APIErrors{}
for i := range lo.Sorts {
ok := supportedFields[lo.Sorts[i].Column]
for i := range fo {
ok := supportedFields[fo[i].Column]
if !ok {
errs = append(errs, errors2.APIError{
Message: fmt.Sprintf("unsupported sort field '%s'", lo.Sorts[i].Column),
Code: http.StatusBadRequest,
})
}
}
for i := range lo.Filters {
ok := supportedFields[lo.Filters[i].Column]
if !ok {
errs = append(errs, errors2.APIError{
Message: fmt.Sprintf("unsupported filter field '%s'", lo.Filters[i].Column),
Message: fmt.Sprintf("unsupported filter field '%s'", fo[i].Column),
Code: http.StatusBadRequest,
})
}
@ -90,7 +80,45 @@ func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error
return nil
}
func extractFilterOptions(req *http.Request) []FilterOption {
func ValidateSortOptions(so []SortOption, supportedFields map[string]bool) errors2.APIErrors {
errs := errors2.APIErrors{}
for i := range so {
ok := supportedFields[so[i].Column]
if !ok {
errs = append(errs, errors2.APIError{
Message: fmt.Sprintf("unsupported sort field '%s'", so[i].Column),
Code: http.StatusBadRequest,
})
}
}
if len(errs) > 0 {
return errs
}
return nil
}
func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error {
errs := errors2.APIErrors{}
sortErrs := ValidateSortOptions(lo.Sorts, supportedFields)
if sortErrs != nil {
errs = append(errs, sortErrs...)
}
filterErrs := ValidateFilterOptions(lo.Filters, supportedFields)
if filterErrs != nil {
errs = append(errs, filterErrs...)
}
if len(errs) > 0 {
return errs
}
return nil
}
func ExtractFilterOptions(req *http.Request) []FilterOption {
res := make([]FilterOption, 0)
for filterKey, filterValues := range req.URL.Query() {
if !strings.HasPrefix(filterKey, "filter") || len(filterValues) == 0 {

View File

@ -67,7 +67,7 @@ func TestConvertGetParamsToFilterOptions(t *testing.T) {
URL: inputURL,
}
actualListOptions := ConvertGetParamsToFilterOptions(req)
actualListOptions := GetSortAndFilterOptions(req)
assert.ElementsMatch(t, testCases[i].expectedListOptions.Sorts, actualListOptions.Sorts)
assert.ElementsMatch(t, testCases[i].expectedListOptions.Filters, actualListOptions.Filters)

Binary file not shown.