rmmagent/agent/utils.go

321 lines
6.7 KiB
Go

/*
Copyright 2022 AmidaWare LLC.
Licensed under the Tactical RMM License Version 1.0 (the “License”).
You may only use the Licensed Software in accordance with the License.
A copy of the License is available at:
https://license.tacticalrmm.com
*/
package agent
import (
"archive/zip"
"bytes"
"fmt"
"io"
"math"
"math/rand"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"time"
ps "github.com/elastic/go-sysinfo"
"github.com/go-ping/ping"
"github.com/go-resty/resty/v2"
"github.com/shirou/gopsutil/v3/process"
)
type PingResponse struct {
Status string
Output string
}
func DoPing(host string) (PingResponse, error) {
var ret PingResponse
pinger, err := ping.NewPinger(host)
if err != nil {
return ret, err
}
var buf bytes.Buffer
pinger.OnRecv = func(pkt *ping.Packet) {
fmt.Fprintf(&buf, "%d bytes from %s: icmp_seq=%d time=%v\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
pinger.OnFinish = func(stats *ping.Statistics) {
fmt.Fprintf(&buf, "\n--- %s ping statistics ---\n", stats.Addr)
fmt.Fprintf(&buf, "%d packets transmitted, %d packets received, %v%% packet loss\n",
stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
fmt.Fprintf(&buf, "round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
}
pinger.Count = 3
pinger.Size = 24
pinger.Interval = time.Second
pinger.Timeout = 5 * time.Second
pinger.SetPrivileged(true)
err = pinger.Run()
if err != nil {
return ret, err
}
ret.Output = buf.String()
stats := pinger.Statistics()
if stats.PacketsRecv == stats.PacketsSent || stats.PacketLoss == 0 {
ret.Status = "passing"
} else {
ret.Status = "failing"
}
return ret, nil
}
// PublicIP returns the agent's public ip
// Tries 3 times before giving up
func (a *Agent) PublicIP() string {
a.Logger.Debugln("PublicIP start")
client := resty.New()
client.SetTimeout(4 * time.Second)
if len(a.Proxy) > 0 {
client.SetProxy(a.Proxy)
}
urls := []string{"https://icanhazip.tacticalrmm.io/", "https://icanhazip.com", "https://ifconfig.co/ip"}
ip := "error"
for _, url := range urls {
r, err := client.R().Get(url)
if err != nil {
a.Logger.Debugln("PublicIP err", err)
continue
}
ip = StripAll(r.String())
if !IsValidIP(ip) {
a.Logger.Debugln("PublicIP not valid", ip)
continue
}
v4 := net.ParseIP(ip)
if v4.To4() == nil {
r1, err := client.R().Get("https://ifconfig.me/ip")
if err != nil {
return ip
}
ipv4 := StripAll(r1.String())
if !IsValidIP(ipv4) {
continue
}
a.Logger.Debugln("Forcing ipv4:", ipv4)
return ipv4
}
a.Logger.Debugln("PublicIP return: ", ip)
break
}
return ip
}
// GenerateAgentID creates and returns a unique agent id
func GenerateAgentID() string {
rand.Seed(time.Now().UnixNano())
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, 40)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
// ShowVersionInfo prints basic debugging info
func ShowVersionInfo(ver string) {
fmt.Println("Tactical RMM Agent:", ver)
fmt.Println("Arch:", runtime.GOARCH)
fmt.Println("Go version:", runtime.Version())
if runtime.GOOS == "windows" {
fmt.Println("Program Directory:", filepath.Join(os.Getenv("ProgramFiles"), progFilesName))
}
}
// TotalRAM returns total RAM in GB
func (a *Agent) TotalRAM() float64 {
host, err := ps.Host()
if err != nil {
return 8.0
}
mem, err := host.Memory()
if err != nil {
return 8.0
}
return math.Ceil(float64(mem.Total) / 1073741824.0)
}
// BootTime returns system boot time as a unix timestamp
func (a *Agent) BootTime() int64 {
host, err := ps.Host()
if err != nil {
return 1000
}
info := host.Info()
return info.BootTime.Unix()
}
// IsValidIP checks for a valid ipv4 or ipv6
func IsValidIP(ip string) bool {
return net.ParseIP(ip) != nil
}
// StripAll strips all whitespace and newline chars
func StripAll(s string) string {
s = strings.TrimSpace(s)
s = strings.Trim(s, "\n")
s = strings.Trim(s, "\r")
return s
}
// KillProc kills a process and its children
func KillProc(pid int32) error {
p, err := process.NewProcess(pid)
if err != nil {
return err
}
children, err := p.Children()
if err == nil {
for _, child := range children {
if err := child.Kill(); err != nil {
continue
}
}
}
if err := p.Kill(); err != nil {
return err
}
return nil
}
// DjangoStringResp removes double quotes from django rest api resp
func DjangoStringResp(resp string) string {
return strings.Trim(resp, `"`)
}
func TestTCP(addr string) error {
conn, err := net.Dial("tcp4", addr)
if err != nil {
return err
}
defer conn.Close()
return nil
}
// CleanString removes invalid utf-8 byte sequences
func CleanString(s string) string {
r := strings.NewReplacer("\x00", "")
s = r.Replace(s)
return strings.ToValidUTF8(s, "")
}
// https://golangcode.com/unzip-files-in-go/
func Unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
// Store filename/path for returning and using later on
fpath := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", fpath)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(fpath, os.ModePerm)
continue
}
// Make File
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
_, err = io.Copy(outFile, rc)
// Close the file without defer to close before next iteration of loop
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
func ByteCountSI(b uint64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
func randRange(min, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}
func randomCheckDelay() {
time.Sleep(time.Duration(randRange(300, 950)) * time.Millisecond)
}
func removeWinNewLines(s string) string {
return strings.ReplaceAll(s, "\r\n", "\n")
}
func createTmpFile() (*os.File, error) {
var f *os.File
f, err := os.CreateTemp("", "trmm")
if err != nil {
cwd, err := os.Getwd()
if err != nil {
return f, err
}
f, err = os.CreateTemp(cwd, "trmm")
if err != nil {
return f, err
}
}
return f, nil
}