From 19a803a844487bcc5702117d082ae68386b51326 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 7 Jul 2021 21:19:51 +0300 Subject: [PATCH] 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 --- api-doc.yml | 44 +++++- client/client.go | 92 +++++++++++-- client/client_test.go | 169 ++++++++++++++++------- client/system_info.go | 65 ++++++++- client/system_info_nix.go | 31 +++++ client/system_info_test.go | 47 +++++-- client/system_info_win.go | 23 ++++ client/virt_info_provider.go | 111 +++++++++++++++ client/virt_info_provider_test.go | 219 ++++++++++++++++++++++++++++++ docs/no01-on-demand-tunnels.md | 22 ++- docs/no09-managing-tunnels.md | 20 +++ docs/no11-multi-tenancy.md | 10 ++ docs/no13-vault.md | 13 +- server/api.go | 109 ++++++++++----- server/api_listener.go | 2 +- server/api_test.go | 29 +++- server/client_service.go | 66 ++++++--- server/client_service_test.go | 4 +- server/clients/cleanup_test.go | 2 +- server/clients/client.go | 36 +++-- server/clients/client_builder.go | 38 ++++-- server/clients/cr.go | 116 +++++++++++++++- server/clients/cr_test.go | 165 +++++++++++++++++++++- server/clients/data_test.go | 136 +++++++++++++------ server/clients/sqlite.go | 106 +++++++++------ server/script/manager.go | 2 +- server/server.go | 1 + server/vault/manager.go | 2 +- share/protocol.go | 34 +++-- share/query/http_filter.go | 66 ++++++--- share/query/http_filter_test.go | 2 +- vault.sqlite | Bin 16384 -> 0 bytes 32 files changed, 1487 insertions(+), 295 deletions(-) create mode 100644 client/system_info_nix.go create mode 100644 client/system_info_win.go create mode 100644 client/virt_info_provider.go create mode 100644 client/virt_info_provider_test.go diff --git a/api-doc.yml b/api-doc.yml index 79974efc..adb91556 100644 --- a/api-doc.yml +++ b/api-doc.yml @@ -328,6 +328,14 @@ paths: description: "Sort option `-`(desc) or ``(asc). `` 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[]` or `filter[,] for or conditions`.\n + `` 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" diff --git a/client/client.go b/client/client.go index 74f4a05d..a960220a 100644 --- a/client/client.go +++ b/client/client.go @@ -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)") +} diff --git a/client/client_test.go b/client/client_test.go index b80cedea..2c85fc56 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -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"}, }, }, } diff --git a/client/system_info.go b/client/system_info.go index fdcaca76..f7cc09f5 100644 --- a/client/system_info.go +++ b/client/system_info.go @@ -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 +} diff --git a/client/system_info_nix.go b/client/system_info_nix.go new file mode 100644 index 00000000..3f7c3d36 --- /dev/null +++ b/client/system_info_nix.go @@ -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 +} diff --git a/client/system_info_test.go b/client/system_info_test.go index 571447a5..766a3440 100644 --- a/client/system_info_test.go +++ b/client/system_info_test.go @@ -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 +} diff --git a/client/system_info_win.go b/client/system_info_win.go new file mode 100644 index 00000000..2da26c19 --- /dev/null +++ b/client/system_info_win.go @@ -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 +} diff --git a/client/virt_info_provider.go b/client/virt_info_provider.go new file mode 100644 index 00000000..c09c1174 --- /dev/null +++ b/client/virt_info_provider.go @@ -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 +} diff --git a/client/virt_info_provider_test.go b/client/virt_info_provider_test.go new file mode 100644 index 00000000..cff0c6c9 --- /dev/null +++ b/client/virt_info_provider_test.go @@ -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) + } +} diff --git a/docs/no01-on-demand-tunnels.md b/docs/no01-on-demand-tunnels.md index e2c0d45c..a954d38e 100644 --- a/docs/no01-on-demand-tunnels.md +++ b/docs/no01-on-demand-tunnels.md @@ -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. \ No newline at end of file +You need to run the above command everytime you change the rpotd binary, for example after every update. diff --git a/docs/no09-managing-tunnels.md b/docs/no09-managing-tunnels.md index 123b1d5c..9a5a5797 100644 --- a/docs/no09-managing-tunnels.md +++ b/docs/no09-managing-tunnels.md @@ -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" diff --git a/docs/no11-multi-tenancy.md b/docs/no11-multi-tenancy.md index 4adccbdd..a08782fc 100644 --- a/docs/no11-multi-tenancy.md +++ b/docs/no11-multi-tenancy.md @@ -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" diff --git a/docs/no13-vault.md b/docs/no13-vault.md index 314c5c7b..ea3f9d46 100644 --- a/docs/no13-vault.md +++ b/docs/no13-vault.md @@ -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. diff --git a/server/api.go b/server/api.go index a2b4e03c..d28eeb67 100644 --- a/server/api.go +++ b/server/api.go @@ -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 diff --git a/server/api_listener.go b/server/api_listener.go index 611c99eb..cd70c2bc 100644 --- a/server/api_listener.go +++ b/server/api_listener.go @@ -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 } diff --git a/server/api_test.go b/server/api_test.go index 77f225c3..3687eb5b 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -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, diff --git a/server/client_service.go b/server/client_service.go index e2126e68..7d890071 100644 --- a/server/client_service.go +++ b/server/client_service.go @@ -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) diff --git a/server/client_service_test.go b/server/client_service_test.go index 1aba7a0d..c4374507 100644 --- a/server/client_service_test.go +++ b/server/client_service_test.go @@ -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) diff --git a/server/clients/cleanup_test.go b/server/clients/cleanup_test.go index 7730d221..07cc8a06 100644 --- a/server/clients/cleanup_test.go +++ b/server/clients/cleanup_test.go @@ -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) diff --git a/server/clients/client.go b/server/clients/client.go index 9a71da5e..30cce69c 100644 --- a/server/clients/client.go +++ b/server/clients/client.go @@ -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"` diff --git a/server/clients/client_builder.go b/server/clients/client_builder.go index 7b0dce7f..a945f940 100644 --- a/server/clients/client_builder.go +++ b/server/clients/client_builder.go @@ -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 { diff --git a/server/clients/cr.go b/server/clients/cr.go index eea7e7b2..8b54c93c 100644 --- a/server/clients/cr.go +++ b/server/clients/cr.go @@ -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 +} diff --git a/server/clients/cr_test.go b/server/clients/cr_test.go index f687bba3..39db2294 100644 --- a/server/clients/cr_test.go +++ b/server/clients/cr_test.go @@ -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") +} diff --git a/server/clients/data_test.go b/server/clients/data_test.go index b9cef768..84206220 100644 --- a/server/clients/data_test.go +++ b/server/clients/data_test.go @@ -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, } } diff --git a/server/clients/sqlite.go b/server/clients/sqlite.go index ecf03c54..77f09c1f 100644 --- a/server/clients/sqlite.go +++ b/server/clients/sqlite.go @@ -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 diff --git a/server/script/manager.go b/server/script/manager.go index 9e5148bd..eaf676eb 100644 --- a/server/script/manager.go +++ b/server/script/manager.go @@ -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 { diff --git a/server/server.go b/server/server.go index 12382e94..97d65ce5 100644 --- a/server/server.go +++ b/server/server.go @@ -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 diff --git a/server/vault/manager.go b/server/vault/manager.go index b4d8f72f..d14f9111 100644 --- a/server/vault/manager.go +++ b/server/vault/manager.go @@ -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 { diff --git a/share/protocol.go b/share/protocol.go index 52d62e6b..fc0c0ae5 100644 --- a/share/protocol.go +++ b/share/protocol.go @@ -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) { diff --git a/share/query/http_filter.go b/share/query/http_filter.go index 1f3696ce..d9096cc0 100644 --- a/share/query/http_filter.go +++ b/share/query/http_filter.go @@ -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 { diff --git a/share/query/http_filter_test.go b/share/query/http_filter_test.go index 54543d5d..01c25565 100644 --- a/share/query/http_filter_test.go +++ b/share/query/http_filter_test.go @@ -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) diff --git a/vault.sqlite b/vault.sqlite index c02b3a4bd21a75f2eaa68901dcafbd03d5d96a47..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI&%SyvQ6b9g#)~XFO3NE^+i=iMictP<6tQw@Wh?gvM6=E_MM38k>iJBzNV*HO~5>4bKkt+JS+PbR-3!K{C-9?4(L>ACvu$1 z$dySst(LLM#ZGqPdoPKQic8W