Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d7db23d58c | ||
![]() |
5a62622892 | ||
![]() |
e797a14911 | ||
![]() |
8260e4fa93 | ||
![]() |
e5f7296d2e | ||
![]() |
82f58d4fbf | ||
![]() |
772324247a | ||
![]() |
7b0156775c |
7
TODO
7
TODO
@ -1,8 +1,9 @@
|
||||
- refactor the elevation detection stuff. I'm not terribly happy with it.
|
||||
|
||||
- password generator utility/library
|
||||
-- incorporate with r00t2.io/pwgen
|
||||
-- incorporate with https://github.com/tredoe/osutil ?
|
||||
-- cli flag to dump flat hashes too
|
||||
--- https://github.com/hlandau/passlib
|
||||
-- incoprporated separately; https://git.r00t2.io/r00t2/PWGen (import r00t2.io/pwgen)
|
||||
-- cli flag to dump flat hashes too (https://github.com/hlandau/passlib and others soon in pwgen)
|
||||
|
||||
- auger needs to be build-constrained to linux.
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
package envs
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`errors`
|
||||
`fmt`
|
||||
`io/ioutil`
|
||||
`os`
|
||||
`reflect`
|
||||
`strings`
|
||||
`sync`
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
`r00t2.io/goutils/multierr`
|
||||
`r00t2.io/goutils/structutils`
|
||||
`r00t2.io/sysutils/errs`
|
||||
`r00t2.io/sysutils/internal`
|
||||
`r00t2.io/sysutils/paths`
|
||||
"r00t2.io/goutils/multierr"
|
||||
"r00t2.io/goutils/structutils"
|
||||
"r00t2.io/sysutils/errs"
|
||||
"r00t2.io/sysutils/internal"
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -45,6 +45,54 @@ func DefEnvBlank(key, fallback string) (value string) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an EnvErrNoVal.
|
||||
func GetEnvErr(key string) (value string, err error) {
|
||||
|
||||
var exists bool
|
||||
|
||||
if value, exists = os.LookupEnv(key); !exists {
|
||||
err = &EnvErrNoVal{
|
||||
VarName: key,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetEnvErrNoBlank behaves exactly like GetEnvErr with the
|
||||
additional stipulation that the value must not be empty.
|
||||
|
||||
An error for a value that is non-empty but whitespace only (e.g. VARNM="\t")
|
||||
can be returned if ignoreWhitespace == true.
|
||||
|
||||
(If it is, an EnvErrNoVal will also be returned.)
|
||||
*/
|
||||
func GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) {
|
||||
|
||||
var exists bool
|
||||
var e *EnvErrNoVal = &EnvErrNoVal{
|
||||
VarName: key,
|
||||
WasRequiredNonEmpty: true,
|
||||
IgnoreWhitespace: ignoreWhitespace,
|
||||
}
|
||||
|
||||
if value, exists = os.LookupEnv(key); !exists {
|
||||
err = e
|
||||
return
|
||||
} else {
|
||||
e.WasFound = true
|
||||
e.WasWhitespace = (strings.TrimSpace(value) == "") && (value != "")
|
||||
if ignoreWhitespace && e.WasWhitespace {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetEnvMap returns a map of all environment variables. All values are strings.
|
||||
func GetEnvMap() (envVars map[string]string) {
|
||||
|
||||
|
27
envs/funcs_enverrnoval.go
Normal file
27
envs/funcs_enverrnoval.go
Normal file
@ -0,0 +1,27 @@
|
||||
package envs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error conforms to a stdlib error interface.
|
||||
func (e *EnvErrNoVal) Error() (errStr string) {
|
||||
|
||||
var sb *strings.Builder = new(strings.Builder)
|
||||
|
||||
sb.WriteString("the variable '")
|
||||
sb.WriteString(e.VarName)
|
||||
sb.WriteString("' was ")
|
||||
if e.WasFound {
|
||||
sb.WriteString("found")
|
||||
} else {
|
||||
sb.WriteString("not found")
|
||||
}
|
||||
if e.WasRequiredNonEmpty && e.WasFound {
|
||||
sb.WriteString(" but is empty and was required to be non-empty")
|
||||
}
|
||||
|
||||
errStr = sb.String()
|
||||
|
||||
return
|
||||
}
|
20
envs/types.go
Normal file
20
envs/types.go
Normal file
@ -0,0 +1,20 @@
|
||||
package envs
|
||||
|
||||
type (
|
||||
/*
|
||||
EnvErrNoVal is an error containing the variable that does not exist
|
||||
(and information surrounding the errored state).
|
||||
*/
|
||||
EnvErrNoVal struct {
|
||||
// VarName is the variable name/key name originally specified in the function call.
|
||||
VarName string `json:"var" toml:"VariableName" yaml:"Variable Name/Key" xml:"key,attr"`
|
||||
// WasFound is only used for GetEnvErrNoBlank(). It is true if the variable was found/populated.
|
||||
WasFound bool `json:"found" toml:"Found" yaml:"Found" xml:"found,attr"`
|
||||
// WasRequiredNonEmpty indicates that this error was returned in a context where a variable was required to be non-empty (e.g. via GetEnvErrNoBlank()) but was empty.
|
||||
WasRequiredNonEmpty bool `json:"reqd_non_empty" toml:"RequiredNonEmpty" yaml:"Required Non-Empty" xml:"reqNonEmpty,attr"`
|
||||
// IgnoreWhitespace is true if the value was found but its evaluation was done against a whitestripped version.
|
||||
IgnoreWhitespace bool `json:"ignore_ws" toml:"IgnoreWhitespace" yaml:"Ignore Whitespace" xml:"ignoreWhitespace,attr"`
|
||||
// WasWhitespace is true if the value was whitespace-only.
|
||||
WasWhitespace bool `json:"was_ws" toml:"WasWhitespace" yaml:"Was Whitespace Only" xml:"wasWhitespace,attr"`
|
||||
}
|
||||
)
|
@ -1,10 +1,10 @@
|
||||
package envs
|
||||
|
||||
import (
|
||||
`strconv`
|
||||
`strings`
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
`r00t2.io/sysutils/internal`
|
||||
"r00t2.io/sysutils/internal"
|
||||
)
|
||||
|
||||
// envListToMap splits a []string of env var keypairs to a map.
|
||||
@ -35,7 +35,7 @@ func nativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{})
|
||||
var pathVar string = internal.GetPathEnvName()
|
||||
var err error
|
||||
|
||||
envMap = make(map[string]interface{}, 0)
|
||||
envMap = make(map[string]interface{})
|
||||
|
||||
for k, v := range stringMap {
|
||||
|
||||
|
@ -118,6 +118,7 @@ func unsetAttrs(f *os.File, attrs fsAttr) (err error) {
|
||||
}
|
||||
ab = bitmask.MaskBit(curAttrs)
|
||||
|
||||
// TODO: Should this be IsOneOf instad of HasFlag?
|
||||
if !ab.HasFlag(bitmask.MaskBit(attrs)) {
|
||||
return
|
||||
}
|
||||
|
163
funcs_idstate.go
Normal file
163
funcs_idstate.go
Normal file
@ -0,0 +1,163 @@
|
||||
package sysutils
|
||||
|
||||
// Checked consolidates all the provided checked functions.
|
||||
func (i *IDState) Checked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.uidsChecked &&
|
||||
i.gidsChecked &&
|
||||
i.sudoChecked &&
|
||||
i.ppidUidChecked &&
|
||||
i.ppidGidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsReal consolidates all the elevation/dropped-privs checks into a single method.
|
||||
|
||||
It will only return true if no sudo was detected and *all* UIDs/GIDs match.
|
||||
*/
|
||||
func (i *IDState) IsReal(real bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
real = true
|
||||
|
||||
for _, b := range []bool{
|
||||
i.IsSuid(),
|
||||
i.IsSgid(),
|
||||
i.IsSudoUser(),
|
||||
i.IsSudoGroup(),
|
||||
} {
|
||||
if b {
|
||||
real = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoGroup is true if any of the group sudo env vars are set,
|
||||
or the parent process has a different group (and is not PID 1).
|
||||
|
||||
It will always return false if SudoChecked returns false oor PPIDGIDsChecked returns false.
|
||||
*/
|
||||
func (i *IDState) IsSudoGroup() (sudo bool) {
|
||||
|
||||
if i == nil || !i.sudoChecked || !i.ppidGidChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sudo = i.SudoEnvGroup || !i.PPIDGidMatch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoUser is true if any of the user sudo env vars are set,
|
||||
or the parent process has a different owner (and is not PID 1).
|
||||
|
||||
It will always return false if SudoChecked returns false or PPIDUIDsChecked returns false.
|
||||
*/
|
||||
func (i *IDState) IsSudoUser() (sudo bool) {
|
||||
|
||||
if i == nil || !i.sudoChecked || !i.ppidUidChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sudo = i.SudoEnvUser || !i.PPIDUidMatch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsSuid is true if the RUID does not match EUID or SUID. It will always return false if UIDsChecked returns false.
|
||||
func (i *IDState) IsSuid() (suid bool) {
|
||||
|
||||
if i == nil || !i.uidsChecked {
|
||||
return
|
||||
}
|
||||
|
||||
suid = i.RUID != i.EUID || i.RUID != i.SUID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsSgid is true if the RGID does not match EGID or SGID. It will always return false if GIDsChecked returns false.
|
||||
func (i *IDState) IsSgid() (sgid bool) {
|
||||
|
||||
if i == nil || !i.gidsChecked {
|
||||
return
|
||||
}
|
||||
|
||||
sgid = i.RGID != i.EGID || i.RGID != i.SGID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GIDsChecked is true if the GIDs presented can be trusted.
|
||||
func (i *IDState) GIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.gidsChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PPIDGIDsChecked is true if PPIDGidMatch can be trusted.
|
||||
func (i *IDState) PPIDGIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.ppidGidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PPIDUIDsChecked is true if PPIDUidMatch can be trusted.
|
||||
func (i *IDState) PPIDUIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.ppidUidChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SudoChecked is true if SudoEnvVars can be trusted
|
||||
func (i *IDState) SudoChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.sudoChecked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UIDsChecked is true if the UIDs presented can be trusted.
|
||||
func (i *IDState) UIDsChecked() (checked bool) {
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
checked = i.uidsChecked
|
||||
|
||||
return
|
||||
}
|
50
funcs_linux.go
Normal file
50
funcs_linux.go
Normal file
@ -0,0 +1,50 @@
|
||||
package sysutils
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`os`
|
||||
|
||||
`golang.org/x/sys/unix`
|
||||
`r00t2.io/sysutils/envs`
|
||||
)
|
||||
|
||||
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
|
||||
func GetIDState() (ids IDState) {
|
||||
|
||||
var err error
|
||||
|
||||
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
|
||||
ids.uidsChecked = true
|
||||
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
|
||||
ids.gidsChecked = true
|
||||
|
||||
ids.SudoEnvCmd = envs.HasEnv("SUDO_COMMAND")
|
||||
ids.SudoEnvHome = envs.HasEnv("SUDO_HOME")
|
||||
ids.SudoEnvGroup = envs.HasEnv("SUDO_GID")
|
||||
ids.SudoEnvUser = envs.HasEnv("SUDO_UID") || envs.HasEnv("SUDO_USER")
|
||||
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
|
||||
ids.SudoEnvVars = true
|
||||
}
|
||||
ids.sudoChecked = true
|
||||
|
||||
// PID 1 will *always* be root, so that can return a false positive for sudo.
|
||||
if os.Getppid() != 1 {
|
||||
ids.stat = new(unix.Stat_t)
|
||||
if err = unix.Stat(
|
||||
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
|
||||
ids.stat,
|
||||
); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
|
||||
ids.ppidUidChecked = true
|
||||
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
|
||||
ids.ppidGidChecked = true
|
||||
}
|
||||
} else {
|
||||
ids.ppidUidChecked = true
|
||||
ids.ppidGidChecked = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
19
go.mod
19
go.mod
@ -1,13 +1,24 @@
|
||||
module r00t2.io/sysutils
|
||||
|
||||
go 1.23.2
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
golang.org/x/sync v0.9.0
|
||||
golang.org/x/sys v0.26.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/sys v0.35.0
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
||||
r00t2.io/goutils v1.7.1
|
||||
r00t2.io/goutils v1.9.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
)
|
||||
|
57
go.sum
57
go.sum
@ -1,19 +1,56 @@
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
|
||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
|
||||
r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
|
||||
r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
||||
r00t2.io/goutils v1.9.2 h1:1rcDgJ3MorWVBmZSvLpbAUNC+J+ctRfJQq5Wliucjww=
|
||||
r00t2.io/goutils v1.9.2/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||
r00t2.io/goutils v1.9.3 h1:pR9Ggu5JBpVjfrqNBrZg9bZpKan0TCcwt3MXrSdkhLo=
|
||||
r00t2.io/goutils v1.9.3/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||
r00t2.io/goutils v1.9.4 h1:+Bm72mKhgXs6DRtU3P4sBjqUNwAKAFfdF9lx5bomwQY=
|
||||
r00t2.io/goutils v1.9.4/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||
r00t2.io/goutils v1.9.5 h1:tIBtXKbGPLCkdhHZSESdTZ2QzC1e+8jDToNr/BauWe0=
|
||||
r00t2.io/goutils v1.9.5/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||
r00t2.io/goutils v1.9.6/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||
|
14
ispriv/consts_nix.go
Normal file
14
ispriv/consts_nix.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
const (
|
||||
sudoEnvPfx string = "SUDO_"
|
||||
sudoUidEnv string = sudoEnvPfx + "UID"
|
||||
sudoGidEnv string = sudoEnvPfx + "GID"
|
||||
sudoUnameEnv string = sudoEnvPfx + "USER"
|
||||
)
|
||||
|
||||
const (
|
||||
curLoginUidFile string = "/proc/self/loginuid"
|
||||
)
|
7
ispriv/doc_nix.go
Normal file
7
ispriv/doc_nix.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build unix
|
||||
|
||||
/*
|
||||
ispriv provides functions and a method to determine if a process is being run SUID/SGID, under sudo, etc.
|
||||
*/
|
||||
|
||||
package ispriv
|
7
ispriv/doc_windows.go
Normal file
7
ispriv/doc_windows.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
ispriv provides functions on Windows to determine the currentl privilege status.
|
||||
*/
|
||||
|
||||
package ispriv
|
68
ispriv/funcs_nix.go
Normal file
68
ispriv/funcs_nix.go
Normal file
@ -0,0 +1,68 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`os`
|
||||
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
/*
|
||||
GetProcIDs returns a ProcIDs from a given PID. An error will be raised if the process ID doesn't exist.
|
||||
A negative value indicates "self" (see also GetProcIDsSelf).
|
||||
|
||||
Note that if you are not EUID == 0 (root) or you/the sudo target user does not own the process,
|
||||
the returning ProcIDs is HIGHLY LIKELY to be very inaccurate.
|
||||
*/
|
||||
func GetProcIDs(pid int32) (p *ProcIDs, err error) {
|
||||
|
||||
var proc ProcIDs
|
||||
var ids []uint32
|
||||
|
||||
if pid < 0 {
|
||||
pid = int32(os.Getpid())
|
||||
}
|
||||
|
||||
if proc.proc, err = process.NewProcess(pid); err != nil {
|
||||
return
|
||||
}
|
||||
if ids, err = proc.proc.Gids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.gids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.gids.filesystem = new(uint)
|
||||
*p.gids.filesystem = uint(ids[3])
|
||||
}
|
||||
if ids, err = proc.proc.Uids(); err != nil {
|
||||
return
|
||||
}
|
||||
p.uids = &IdInfo{
|
||||
real: uint(ids[0]),
|
||||
effective: uint(ids[1]),
|
||||
savedSet: uint(ids[2]),
|
||||
filesystem: nil,
|
||||
}
|
||||
if len(ids) == 4 {
|
||||
p.uids.filesystem = new(uint)
|
||||
*p.uids.filesystem = uint(ids[3])
|
||||
}
|
||||
|
||||
p = &proc
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetProcIDsSelf returns a ProcIDs from the current process' PID.
|
||||
func GetProcIDsSelf() (p *ProcIDs, err error) {
|
||||
|
||||
p, err = GetProcIDs(int32(os.Getpid()))
|
||||
|
||||
return
|
||||
}
|
426
ispriv/funcs_procids_nix.go
Normal file
426
ispriv/funcs_procids_nix.go
Normal file
@ -0,0 +1,426 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`os`
|
||||
`os/user`
|
||||
`strconv`
|
||||
`strings`
|
||||
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
`golang.org/x/sys/unix`
|
||||
`r00t2.io/sysutils/envs`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
// GetEffective returns the EUID/EGID.
|
||||
func (p *ProcIDs) GetEffective() (euid, egid uint) {
|
||||
|
||||
euid = p.uids.effective
|
||||
egid = p.gids.effective
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetFS returns the FSUID/FSGID. Not all platforms have this, in which case they'll be nil.
|
||||
func (p *ProcIDs) GetFS() (fsuid, fsgid *uint) {
|
||||
|
||||
if p.uids.filesystem != nil {
|
||||
fsuid = new(uint)
|
||||
*fsuid = *p.uids.filesystem
|
||||
}
|
||||
if p.gids.filesystem != nil {
|
||||
fsgid = new(uint)
|
||||
*fsgid = *p.gids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetGids returms a set of a ProcIDs GIDs.
|
||||
fs will be nil if unsupported on the platform.
|
||||
If invoked with SGID, "savedSet" will be the SGID GID.
|
||||
*/
|
||||
func (p *ProcIDs) GetGids() (real, effective, savedSet uint, fs *uint) {
|
||||
|
||||
real = p.gids.real
|
||||
effective = p.gids.effective
|
||||
savedSet = p.gids.savedSet
|
||||
if p.gids.filesystem != nil {
|
||||
fs = new(uint)
|
||||
*fs = *p.gids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetReal returns the (R)UID/(R)GID.
|
||||
func (p *ProcIDs) GetReal() (ruid, rgid uint) {
|
||||
|
||||
ruid = p.uids.real
|
||||
rgid = p.gids.real
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSaved returns the SUID/SGID.
|
||||
func (p *ProcIDs) GetSaved() (suid, sgid uint) {
|
||||
|
||||
suid = p.uids.savedSet
|
||||
sgid = p.gids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetUids returms a set of a ProcIDs UIDs.
|
||||
fs will be nil if unsupported on the platform.
|
||||
If invoked with SUID, "savedSet" will be the SUID UID.
|
||||
*/
|
||||
func (p *ProcIDs) GetUids() (real, effective, savedSet uint, fs *uint) {
|
||||
|
||||
real = p.uids.real
|
||||
effective = p.uids.effective
|
||||
savedSet = p.uids.savedSet
|
||||
if p.uids.filesystem != nil {
|
||||
fs = new(uint)
|
||||
*fs = *p.uids.filesystem
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSGID returns true if the process is Set GID/SGID.
|
||||
|
||||
Note that it will return false if invoked by a group with the same GID as an SGID that's set.
|
||||
*/
|
||||
func (p *ProcIDs) IsSGID() (isSgid bool) {
|
||||
|
||||
isSgid = p.gids.real != p.gids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSUID returns true if the process is Set UID/SUID.
|
||||
|
||||
Note that it will return false if invoked by a user with the same UID as an SUID that's set.
|
||||
*/
|
||||
func (p *ProcIDs) IsSUID() (isSuid bool) {
|
||||
|
||||
isSuid = p.uids.real != p.uids.savedSet
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudo does a very fast (and potentially inaccurate) evaluation of whether the process is running under sudo.
|
||||
|
||||
DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations!
|
||||
Use IsSudoWithConfidence instead for those cases.
|
||||
IsSudo only does the most basic of checking, which can be easily and completely overridden by a non-privileged user.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudo() (isSudo bool) {
|
||||
|
||||
// This is how every other Joe Blow does this. It's an extremely dumb way to do it. The caller has been warned.
|
||||
for k, _ := range envs.GetEnvMap() {
|
||||
if strings.HasPrefix(k, sudoEnvPfx) {
|
||||
isSudo = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoDetailed returns true for a very fast evaluation of whether the process is running under sudo,
|
||||
and information about that context.
|
||||
(If isSudo is false, originalUid/originalGid will both be -1 and originalUser will be nil.)
|
||||
|
||||
DO NOT use this function for security-sensitive uses, fully accurate results, or critical implementations!
|
||||
Use IsSudoWithConfidenceDetailed instead for those cases.
|
||||
IsSudoDetailed only does the most basic of checking, which can be easily and completely overridden by a non-privileged user.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoDetailed() (isSudo bool, originalUid, originalGid int, originalUser *user.User, err error) {
|
||||
|
||||
if originalUid, originalGid, originalUser, err = p.getSudoInfoEnv(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if originalUid >= 0 || originalGid >= 0 || originalUser != nil {
|
||||
isSudo = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoWithConfidence is like IsSudo, but is *much* more throrough.
|
||||
|
||||
It not only returns isSudo, which is true if *any* indicators pass,
|
||||
but also:
|
||||
|
||||
* a confidence value (which indicates *how many* indicators *passed*)
|
||||
* a maxConfidence value (which indicates how many indicators were *tested*)
|
||||
* a score value (which is a float indicating overall confidence on a fixed and weighted scale; higher is more confident, 1.0 indicates 100% confidence)
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoWithConfidence() (isSudo bool, confidence, maxConfidence uint, score float64, err error) {
|
||||
|
||||
// confidence/maxConfidence are not used directly; they're unweighted counters.
|
||||
var scoreConf uint
|
||||
var scoreMaxConf uint
|
||||
|
||||
score = float64(scoreConf) / float64(scoreMaxConf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsSudoWithConfidenceDetailed is like IsSudoDetailed, but is *much* more throrough.
|
||||
|
||||
It not only returns the same results as IsSudoDetailed, but includes the same scoring values/system as IsSudoWithConfidence.
|
||||
*/
|
||||
func (p *ProcIDs) IsSudoWithConfidenceDetailed() (isSudo bool, confidence, maxConfidence uint, score float64, originalUid, originalGid int, originalUser *user.User, err error) {
|
||||
|
||||
var b []byte
|
||||
var ok bool
|
||||
var permErr bool
|
||||
var envUid int
|
||||
var envGid int
|
||||
var scoreConf uint
|
||||
var scoreMaxConf uint
|
||||
var curUser *user.User
|
||||
var envUser *user.User
|
||||
var curUid uint64
|
||||
var fstat unix.Stat_t
|
||||
var fsUid int
|
||||
var procFiles []process.OpenFilesStat
|
||||
var loginUidFile string = curLoginUidFile
|
||||
|
||||
if curUser, err = user.Current(); err != nil {
|
||||
return
|
||||
}
|
||||
if curUid, err = strconv.ParseUint(curUser.Uid, 10, 32); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if procFiles, err = p.proc.OpenFiles(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Env vars; only score 1x/each.
|
||||
maxConfidence += 3
|
||||
scoreMaxConf += 3
|
||||
if envUid, envGid, envUser, err = p.getSudoInfoEnv(); err != nil {
|
||||
return
|
||||
}
|
||||
originalUid, originalGid, originalUser = envUid, envGid, envUser
|
||||
if envUid >= 0 {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
if envGid >= 0 {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
if envUser != nil {
|
||||
confidence++
|
||||
scoreConf++
|
||||
}
|
||||
|
||||
/*
|
||||
TTY/PTY ownership. We (can) only check this if we're running in an interactive session.
|
||||
|
||||
Typically this is done via (golang.org/x/term).IsTerminal(),
|
||||
That pulls in a bunch of stuff I don't need, though, so I'll just replicate (...).IsTerminal() here;
|
||||
it's just a wrapped single function call.
|
||||
*/
|
||||
// procFiles[0] is always STDIN. Whether it's a pipe, or TTY/PTY, or file, etc.
|
||||
// (likewise, procFiles[1] is always STDOUT, procFiles[2] is always STDERR); however...
|
||||
if _, err = unix.IoctlGetTermios(int(procFiles[0].Fd), unix.TCGETS); err == nil {
|
||||
// Interactive
|
||||
maxConfidence++
|
||||
// This is only worth 2. It's pretty hard to fake unless origin user is root,
|
||||
// but it's ALSO usually set to the target user.
|
||||
scoreMaxConf += 2
|
||||
fstat = unix.Stat_t{}
|
||||
if err = unix.Fstat(int(procFiles[0].Fd), &fstat); err != nil {
|
||||
return
|
||||
}
|
||||
if uint64(fstat.Uid) != curUid {
|
||||
// This is a... *potential* indicator, if a lateral sudo was done (user1 => user2),
|
||||
// or root used sudo to *drop* privs to a regular user.
|
||||
// We mark it as a pass for confidence since it IS a terminal, and it's permission-related.
|
||||
confidence++
|
||||
scoreConf += 2
|
||||
originalUid = int(fstat.Uid)
|
||||
}
|
||||
} else {
|
||||
// err is OK; just means non-interactive. No counter or score/max score increase; basically a NO-OP.
|
||||
err = nil
|
||||
}
|
||||
|
||||
// /proc/self/loginuid
|
||||
// This is a REALLY good indicator. Probably the strongest next to reverse-walking the proc tree. It depends on PAM and auditd support, I think,
|
||||
// BUT if it's present it's *really* really strong.
|
||||
if ok, err = paths.RealPathExists(&loginUidFile); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
maxConfidence++
|
||||
scoreMaxConf += 5
|
||||
if b, err = os.ReadFile(loginUidFile); err != nil {
|
||||
return
|
||||
}
|
||||
if fsUid, err = strconv.Atoi(strings.TrimSpace(string(b))); err != nil {
|
||||
return
|
||||
}
|
||||
if uint64(fsUid) != curUid {
|
||||
confidence++
|
||||
scoreConf += 5
|
||||
originalUid = fsUid
|
||||
}
|
||||
}
|
||||
|
||||
// proc tree reverse walking.
|
||||
// This is, by far, the most reliable method.
|
||||
// There are some valid conditions in which this would fail due to permissions
|
||||
// (e.g. lateral sudo: user1 => user2), but if it's a permission error it's *probably*
|
||||
// a lateral move anyways.
|
||||
if isSudo, permErr, originalUid, originalGid, originalUser, err = p.revProcWalk(); err != nil {
|
||||
return
|
||||
}
|
||||
maxConfidence++
|
||||
scoreMaxConf += 10
|
||||
if permErr {
|
||||
confidence++
|
||||
scoreConf += 5
|
||||
} else if isSudo {
|
||||
confidence++
|
||||
scoreConf += 10
|
||||
}
|
||||
|
||||
score = float64(scoreConf) / float64(scoreMaxConf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
getSudoInfoEnv returns env var driven sudo information.
|
||||
|
||||
These are in no way guaranteed to be accurate as the user can remove or override them.
|
||||
*/
|
||||
func (p *ProcIDs) getSudoInfoEnv() (uid, gid int, u *user.User, err error) {
|
||||
|
||||
var ok bool
|
||||
var val string
|
||||
var envMap map[string]string = envs.GetEnvMap()
|
||||
|
||||
uid = -1
|
||||
gid = -1
|
||||
|
||||
if val, ok = envMap[sudoUnameEnv]; ok {
|
||||
if u, err = user.Lookup(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if val, ok = envMap[sudoUidEnv]; ok {
|
||||
if uid, err = strconv.Atoi(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if val, ok = envMap[sudoGidEnv]; ok {
|
||||
if gid, err = strconv.Atoi(val); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
revProcWalk walks up the process tree ("proctree") until it either:
|
||||
|
||||
* finds a process invoked with sudo (true)
|
||||
* hits PID == 1 (false)
|
||||
* hits a permission error (true-ish)
|
||||
*/
|
||||
func (p *ProcIDs) revProcWalk() (sudoFound, isPermErr bool, origUid, origGid int, origUser *user.User, err error) {
|
||||
|
||||
var cmd []string
|
||||
var parent *ProcIDs
|
||||
var parentPid int32
|
||||
var parentUname string
|
||||
var parentUids []uint32
|
||||
var parentGids []uint32
|
||||
|
||||
origUid = -1
|
||||
origGid = -1
|
||||
|
||||
parent = p
|
||||
for {
|
||||
if parent == nil || parent.proc.Pid == 1 {
|
||||
break
|
||||
}
|
||||
if cmd, err = parent.proc.CmdlineSlice(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if cmd[0] == "sudo" {
|
||||
sudoFound = true
|
||||
if parentUname, err = parent.proc.Username(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parentUids, err = parent.proc.Uids(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parentGids, err = parent.proc.Gids(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if origUser, err = user.Lookup(parentUname); err != nil {
|
||||
return
|
||||
}
|
||||
origUid = int(parentUids[0])
|
||||
origGid = int(parentGids[0])
|
||||
}
|
||||
if sudoFound {
|
||||
break
|
||||
}
|
||||
if parentPid, err = parent.proc.Ppid(); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if parent, err = GetProcIDs(parentPid); err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
isPermErr = true
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
60
ispriv/funcs_windows.go
Normal file
60
ispriv/funcs_windows.go
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build windows
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/windows`
|
||||
)
|
||||
|
||||
// IsAdmin returns true if currently running with Administrator privileges.
|
||||
func IsAdmin() (admin bool, err error) {
|
||||
|
||||
var sid *windows.SID
|
||||
var tok windows.Token
|
||||
|
||||
if err = windows.AllocateAndInitializeSid(
|
||||
&windows.SECURITY_NT_AUTHORITY, // identAuth
|
||||
2, // subAuth
|
||||
windows.SECURITY_BUILTIN_DOMAIN_RID, // subAuth0
|
||||
windows.DOMAIN_ALIAS_RID_ADMINS, // subAuth1
|
||||
0, 0, 0, 0, 0, 0, // subAuth2-10
|
||||
&sid, // sid
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
defer windows.FreeSid(sid)
|
||||
|
||||
tok = windows.Token(0)
|
||||
if admin, err = tok.IsMember(sid); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsElevated returns true if running in an elevated ("Run as Administrator") context.
|
||||
func IsElevated() (elevated bool) {
|
||||
|
||||
var tok windows.Token = windows.Token(0)
|
||||
|
||||
elevated = tok.IsElevated()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPrivileged indicates that the current security context is running both
|
||||
with Administrator priviliges AND is elevated.
|
||||
*/
|
||||
func IsPrivileged() (privileged bool, err error) {
|
||||
|
||||
if privileged, err = IsAdmin(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if privileged {
|
||||
privileged = IsElevated()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
19
ispriv/types_nix.go
Normal file
19
ispriv/types_nix.go
Normal file
@ -0,0 +1,19 @@
|
||||
//go:build unix
|
||||
|
||||
package ispriv
|
||||
|
||||
import (
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
)
|
||||
|
||||
type ProcIDs struct {
|
||||
proc *process.Process
|
||||
uids *IdInfo
|
||||
gids *IdInfo
|
||||
}
|
||||
type IdInfo struct {
|
||||
real uint
|
||||
effective uint
|
||||
savedSet uint
|
||||
filesystem *uint
|
||||
}
|
1
paths/TODO
Normal file
1
paths/TODO
Normal file
@ -0,0 +1 @@
|
||||
- search criteria should *also* support a timestamp range (e.g. so a search can be restricted to both older than AND newer than; e.g. older than 00:00, newer than 01:00)
|
@ -4,6 +4,10 @@ import (
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
GenericSeparator rune = '/'
|
||||
)
|
||||
|
||||
// Mostly just for reference.
|
||||
const (
|
||||
// ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular
|
||||
|
17
paths/consts_unix.go
Normal file
17
paths/consts_unix.go
Normal file
@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package paths
|
||||
|
||||
const (
|
||||
/*
|
||||
MaxSymlinkLevel is hardcoded into the kernel for macOS, BSDs and Linux. It's unlikely to change.
|
||||
Thankfully, it's the same on all of them.
|
||||
|
||||
On all, it's defined as MAXSYMLINKS in the following headers:
|
||||
|
||||
macOS (no, macOS is not a BSD; no, it is not FreeBSD; yes, I *will* fight you on it and win): sys/param.h
|
||||
BSDs: sys/sys/param.h
|
||||
Linux: include/linux/namei.h
|
||||
*/
|
||||
MaxSymlinkLevel uint = 40
|
||||
)
|
15
paths/consts_windows.go
Normal file
15
paths/consts_windows.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build windows
|
||||
|
||||
package paths
|
||||
|
||||
const (
|
||||
/*
|
||||
MaxSymLinkLevel on Windows is weird; Microsoft calls them "reparse points".
|
||||
|
||||
And it changes on the Windows version you're on, but it's been 63 past Windows Server 2003/Windows XP.
|
||||
They're *very* EOL, so I'm completely ignoring them.
|
||||
|
||||
https://learn.microsoft.com/en-us/windows/win32/fileio/symbolic-link-programming-consideration
|
||||
*/
|
||||
MaxSymlinkLevel uint = 63
|
||||
)
|
@ -1,10 +1,12 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
`errors`
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMaxSymlinkLevel = fmt.Errorf("max symlink level met/exceeded")
|
||||
ErrNilErrChan error = errors.New("an initialized error channel is required")
|
||||
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
|
||||
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")
|
||||
|
402
paths/funcs.go
402
paths/funcs.go
@ -19,29 +19,31 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
`context`
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
`sort`
|
||||
"sort"
|
||||
"strings"
|
||||
`sync`
|
||||
`time`
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// "syscall"
|
||||
|
||||
`github.com/djherbis/times`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
"github.com/djherbis/times"
|
||||
"r00t2.io/goutils/bitmask"
|
||||
)
|
||||
|
||||
/*
|
||||
ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place.
|
||||
"Nested" user paths (~someuser/somechroot/~someotheruser) are not supported as home directories are expected to be absolute paths.
|
||||
*/
|
||||
func ExpandHome(path *string) (err error) {
|
||||
func ExpandHome(p *string) (err error) {
|
||||
|
||||
var unameSplit []string
|
||||
var uname string
|
||||
@ -50,10 +52,10 @@ func ExpandHome(path *string) (err error) {
|
||||
|
||||
// Props to this guy.
|
||||
// https://stackoverflow.com/a/43578461/733214
|
||||
if len(*path) == 0 {
|
||||
if len(*p) == 0 {
|
||||
err = errors.New("empty path")
|
||||
return
|
||||
} else if (*path)[0] != '~' {
|
||||
} else if (*p)[0] != '~' {
|
||||
return
|
||||
}
|
||||
|
||||
@ -69,7 +71,7 @@ func ExpandHome(path *string) (err error) {
|
||||
}
|
||||
*/
|
||||
// K but do it smarter.
|
||||
unameSplit = strings.SplitN(*path, string(os.PathSeparator), 2)
|
||||
unameSplit = strings.SplitN(*p, string(os.PathSeparator), 2)
|
||||
if len(unameSplit) != 2 {
|
||||
unameSplit = append(unameSplit, "")
|
||||
}
|
||||
@ -85,7 +87,7 @@ func ExpandHome(path *string) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
*path = filepath.Join(u.HomeDir, unameSplit[1])
|
||||
*p = filepath.Join(u.HomeDir, unameSplit[1])
|
||||
|
||||
return
|
||||
}
|
||||
@ -107,9 +109,9 @@ func ExpandHome(path *string) (err error) {
|
||||
|
||||
This is a thin wrapper around GetFirstWithRef.
|
||||
*/
|
||||
func GetFirst(paths []string) (content []byte, isDir, ok bool) {
|
||||
func GetFirst(p []string) (content []byte, isDir, ok bool) {
|
||||
|
||||
content, isDir, ok, _ = GetFirstWithRef(paths)
|
||||
content, isDir, ok, _ = GetFirstWithRef(p)
|
||||
|
||||
return
|
||||
}
|
||||
@ -118,25 +120,25 @@ func GetFirst(paths []string) (content []byte, isDir, ok bool) {
|
||||
GetFirstWithRef is the file equivalent of envs.GetFirstWithRef.
|
||||
|
||||
It behaves exactly like GetFirst, but with an additional returned value, idx,
|
||||
which specifies the index in paths in which a path was found.
|
||||
which specifies the index in p in which a path was found.
|
||||
|
||||
As always, results are not guaranteed due to permissions, etc.
|
||||
potentially returning an inaccurate result.
|
||||
*/
|
||||
func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) {
|
||||
func GetFirstWithRef(p []string) (content []byte, isDir, ok bool, idx int) {
|
||||
|
||||
var locPaths []string
|
||||
var exists bool
|
||||
var stat os.FileInfo
|
||||
var stat fs.FileInfo
|
||||
var err error
|
||||
|
||||
idx = -1
|
||||
// We have to be a little less cavalier about this.
|
||||
if paths == nil {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
locPaths = make([]string, len(paths))
|
||||
locPaths = paths[:] // Create an explicit copy so we don't modify paths.
|
||||
locPaths = make([]string, len(p))
|
||||
locPaths = p[:] // Create an explicit copy so we don't modify p.
|
||||
for i, p := range locPaths {
|
||||
if exists, stat, err = RealPathExistsStat(&p); err != nil {
|
||||
err = nil
|
||||
@ -159,6 +161,30 @@ func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Len returns the number of path segments in p, as split with the same param signature to Segment.
|
||||
|
||||
See Segment for details on abs and strict.
|
||||
*/
|
||||
func Len(p string, abs, strict bool) (segments int) {
|
||||
|
||||
segments = len(Segment(p, abs, strict))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
LenSys returns the number of path segments in p, as split with the same param signature to SegmentSys.
|
||||
|
||||
See Segment for details on abs and strict.
|
||||
*/
|
||||
func LenSys(p string, abs, strict bool) (segments int) {
|
||||
|
||||
segments = len(SegmentSys(p, abs, strict))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MakeDirIfNotExist will create a directory at a given path if it doesn't exist.
|
||||
|
||||
@ -166,11 +192,11 @@ See also the documentation for RealPath.
|
||||
|
||||
This is a bit more sane option than os.MkdirAll as it will normalize paths a little better.
|
||||
*/
|
||||
func MakeDirIfNotExist(path string) (err error) {
|
||||
func MakeDirIfNotExist(p string) (err error) {
|
||||
|
||||
var stat os.FileInfo
|
||||
var stat fs.FileInfo
|
||||
var exists bool
|
||||
var locPath string = path
|
||||
var locPath string = p
|
||||
|
||||
if exists, stat, err = RealPathExistsStat(&locPath); err != nil {
|
||||
if !exists {
|
||||
@ -207,20 +233,86 @@ It is recommended to check err (if not nil) for an invalid path error. If this i
|
||||
path syntax/string itself is not supported on the runtime OS. This can be done via:
|
||||
|
||||
if errors.Is(err, fs.ErrInvalid) {...}
|
||||
|
||||
RealPath is simply a wrapper around ExpandHome(path) and filepath.Abs(*path).
|
||||
|
||||
Note that RealPath does *not* resolve symlinks. Only RealPathExistsStatTarget does that.
|
||||
*/
|
||||
func RealPath(path *string) (err error) {
|
||||
func RealPath(p *string) (err error) {
|
||||
|
||||
if err = ExpandHome(path); err != nil {
|
||||
if err = ExpandHome(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *path, err = filepath.Abs(*path); err != nil {
|
||||
if *p, err = filepath.Abs(*p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RealPathJoin combines RealPath with (path).Join.
|
||||
|
||||
If dst is nil, then p will be updated with the new value.
|
||||
You probably don't want that.
|
||||
*/
|
||||
func RealPathJoin(p, dst *string, subPaths ...string) (err error) {
|
||||
|
||||
var newPath string
|
||||
var realDst *string
|
||||
|
||||
if err = RealPath(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if dst == nil {
|
||||
realDst = p
|
||||
} else {
|
||||
realDst = dst
|
||||
}
|
||||
|
||||
newPath = path.Join(append([]string{*p}, subPaths...)...)
|
||||
if err = RealPath(&newPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
*realDst = newPath
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RealPathJoinSys combines RealPath with (path/filepath).Join.
|
||||
|
||||
If dst is nil, then path will be updated with the new value.
|
||||
You probably don't want that.
|
||||
*/
|
||||
func RealPathJoinSys(p, dst *string, subPaths ...string) (err error) {
|
||||
|
||||
var newPath string
|
||||
var realDst *string
|
||||
|
||||
if err = RealPath(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if dst == nil {
|
||||
realDst = p
|
||||
} else {
|
||||
realDst = dst
|
||||
}
|
||||
|
||||
newPath = filepath.Join(append([]string{*p}, subPaths...)...)
|
||||
if err = RealPath(&newPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
*realDst = newPath
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RealPathExists is like RealPath, but will also return a boolean as to whether the path
|
||||
actually exists or not.
|
||||
@ -237,13 +329,13 @@ See also the documentation for RealPath.
|
||||
|
||||
In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil.
|
||||
*/
|
||||
func RealPathExists(path *string) (exists bool, err error) {
|
||||
func RealPathExists(p *string) (exists bool, err error) {
|
||||
|
||||
if err = RealPath(path); err != nil {
|
||||
if err = RealPath(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = os.Stat(*path); err != nil {
|
||||
if _, err = os.Stat(*p); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
@ -256,22 +348,68 @@ func RealPathExists(path *string) (exists bool, err error) {
|
||||
}
|
||||
|
||||
/*
|
||||
RealPathExistsStat is like RealPathExists except it will also return the os.FileInfo
|
||||
RealPathExistsStat is like RealPathExists except it will also return the fs.FileInfo
|
||||
for the path (assuming it exists).
|
||||
|
||||
If stat is nil, it is highly recommended to check err via the methods suggested
|
||||
in the documentation for RealPath and RealPathExists.
|
||||
*/
|
||||
func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) {
|
||||
func RealPathExistsStat(p *string) (exists bool, stat fs.FileInfo, err error) {
|
||||
|
||||
if exists, err = RealPathExists(path); err != nil {
|
||||
if exists, err = RealPathExists(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if stat, err = os.Stat(*path); err != nil {
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
if stat, err = os.Stat(*p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
RealPathExistsStatTarget is the only "RealPather" that will resolve p to the (final) *target* of p if p is a symlink.
|
||||
|
||||
If p is not a symlink but does exist, the tgt* will reflect the same as p*.
|
||||
|
||||
See WalkLink for details on relRoot and other assorted rules/logic (RealPathExistsStatTarget wraps WalkLink).
|
||||
*/
|
||||
func RealPathExistsStatTarget(p *string, relRoot string) (pExists, tgtExists, wasLink bool, pStat fs.FileInfo, tgtStat fs.FileInfo, err error) {
|
||||
|
||||
var tgts []string
|
||||
|
||||
if pExists, err = RealPathExists(p); err != nil {
|
||||
return
|
||||
}
|
||||
tgtExists = pExists
|
||||
if !pExists {
|
||||
return
|
||||
}
|
||||
|
||||
// Can't use RealPathExistsStat because it calls os.Stat, not os.Lstat... thus defeating the purpose.
|
||||
if pStat, err = os.Lstat(*p); err != nil {
|
||||
return
|
||||
}
|
||||
tgtStat = pStat
|
||||
|
||||
wasLink = pStat.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink
|
||||
|
||||
if wasLink {
|
||||
if tgts, err = WalkLink(*p, relRoot); err != nil || tgts == nil || len(tgts) == 0 {
|
||||
tgtExists = false
|
||||
tgtStat = nil
|
||||
return
|
||||
}
|
||||
if tgtExists, tgtStat, err = RealPathExistsStat(&tgts[len(tgts)-1]); err != nil {
|
||||
return
|
||||
}
|
||||
*p = tgts[len(tgts)-1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -435,12 +573,204 @@ func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Segment returns path p's segments as a slice of strings, using GenericSeparator as a separator.
|
||||
|
||||
If abs is true, the placeholder leading prefix(es) (if any) of GenericSeparator will be kept in-place;
|
||||
otherwise it/they will be trimmed out.
|
||||
e.g.:
|
||||
|
||||
abs == true: //foo/bar/baz => []string{"", "", "foo", "bar", "baz"}
|
||||
abs == false: /foo/bar/baz => []string{"foo", "bar", "baz"}
|
||||
|
||||
If strict is true, any trailing GenericSeparator will be kept in-place;
|
||||
otherwise they will be trimmed out.
|
||||
e.g. (assuming abs == false):
|
||||
|
||||
strict == true: /foo/bar/baz// => []string{"foo", "bar", "baz", "", ""}
|
||||
strict == false: /foo/bar/baz/ => []string{"foo", "bar", "baz"}
|
||||
|
||||
It is recommended to call RealPath for path's ptr first for normalization.
|
||||
*/
|
||||
func Segment(p string, abs, strict bool) (segments []string) {
|
||||
|
||||
if !abs {
|
||||
p = strings.TrimLeft(p, string(GenericSeparator))
|
||||
}
|
||||
if !strict {
|
||||
p = strings.TrimRight(p, string(GenericSeparator))
|
||||
}
|
||||
|
||||
segments = strings.Split(p, string(GenericSeparator))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SegmentSys is exactly like Segment, except using os.PathSeparator instead of GenericSeparator.
|
||||
func SegmentSys(p string, abs, strict bool) (segments []string) {
|
||||
|
||||
if !abs {
|
||||
p = strings.TrimLeft(p, string(os.PathSeparator))
|
||||
}
|
||||
if !strict {
|
||||
p = strings.TrimRight(p, string(os.PathSeparator))
|
||||
}
|
||||
|
||||
segments = strings.Split(p, string(os.PathSeparator))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Strip is like Segment but trims out the leading n number of segments and reassembles the path using path.Join.
|
||||
|
||||
n may be negative, in which case the *trailing* n number of segments will be trimmed out.
|
||||
(i.e. n == -1, p == `foo/bar/baz/quux` would be `foo/bar/baz`, not `bar/baz/quux`)
|
||||
|
||||
If you require more traditional slicing (e.g. with interval),
|
||||
you may want to use path.Join with a sliced result of Segment instead.
|
||||
e.g.: *only* the *last* n segments: path.Join(Segment(p, ...)[Len(p, ...)-n:]...)
|
||||
|
||||
If n == 0 or int(math.Abs(float64(n))) >= len(Segment(p, ...)), no transformation will be done.
|
||||
|
||||
e.g.
|
||||
|
||||
n == 2: foo/bar/baz/foobar/quux => baz/foobar/quux
|
||||
n == -2: foo/bar/baz/foobar/quux => foo/bar/baz
|
||||
*/
|
||||
func Strip(p string, abs, strict bool, n int) (slicedPath string) {
|
||||
|
||||
var pLen int
|
||||
var absN int
|
||||
var segments []string
|
||||
|
||||
segments = Segment(p, abs, strict)
|
||||
pLen = len(segments)
|
||||
|
||||
absN = int(math.Abs(float64(n)))
|
||||
|
||||
if n == 0 || absN >= pLen {
|
||||
slicedPath = p
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
segments = segments[n:]
|
||||
} else {
|
||||
segments = segments[:pLen-absN]
|
||||
}
|
||||
|
||||
slicedPath = path.Join(segments...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StripSys is exactly like Strip but using (path/filepath).Join and SegmentSys.
|
||||
func StripSys(p string, abs, strict bool, n int) (slicedPath string) {
|
||||
|
||||
var pLen int
|
||||
var absN int
|
||||
var segments []string
|
||||
|
||||
segments = SegmentSys(p, abs, strict)
|
||||
pLen = len(segments)
|
||||
|
||||
absN = int(math.Abs(float64(n)))
|
||||
|
||||
if n == 0 || absN >= pLen {
|
||||
slicedPath = p
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
segments = segments[n:]
|
||||
} else {
|
||||
segments = segments[:pLen-absN]
|
||||
}
|
||||
|
||||
slicedPath = filepath.Join(segments...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
WalkLink walks the recursive target(s) of lnk (unless/until MaxSymlinkLevel is hit, which will trigger ErrMaxSymlinkLevel)
|
||||
until it reaches a real (non-symlink) target.
|
||||
|
||||
lnk will have RealPath called on it first.
|
||||
|
||||
If lnk is not a symlink, then tgts == []string{lnk} and err = nil.
|
||||
|
||||
A broken link will return fs.ErrNotExist, with tgts containing the targets up to and including the path that triggered the error.
|
||||
|
||||
If lnk itself does not exist, tgts will be nil and err will be that of fs.ErrNotExist.
|
||||
|
||||
relRoot is a root directory to resolve relative links to. If empty, relative link target `t` from link `l` will be treated
|
||||
as relative to `(path/filepath).Dir(l)` (that is to say, `t = filepath.Join(filepath.Dir(l), os.Readlink(l))`).
|
||||
*/
|
||||
func WalkLink(lnk, relRoot string) (tgts []string, err error) {
|
||||
|
||||
var exists bool
|
||||
var curDepth uint
|
||||
var stat fs.FileInfo
|
||||
var curTgt string
|
||||
var prevTgt string
|
||||
|
||||
if exists, err = RealPathExists(&lnk); err != nil {
|
||||
return
|
||||
} else if !exists {
|
||||
err = fs.ErrNotExist
|
||||
return
|
||||
}
|
||||
|
||||
if relRoot != "" {
|
||||
if err = RealPath(&relRoot); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tgts = []string{}
|
||||
|
||||
curTgt = lnk
|
||||
for curDepth = 0; curDepth < MaxSymlinkLevel; curDepth++ {
|
||||
if exists, err = RealPathExists(&curTgt); err != nil {
|
||||
return
|
||||
}
|
||||
prevTgt = curTgt
|
||||
tgts = append(tgts, curTgt)
|
||||
if !exists {
|
||||
err = fs.ErrNotExist
|
||||
return
|
||||
}
|
||||
if stat, err = os.Lstat(curTgt); err != nil {
|
||||
return
|
||||
}
|
||||
if stat.Mode().Type()&os.ModeSymlink != os.ModeSymlink {
|
||||
break
|
||||
}
|
||||
if curTgt, err = os.Readlink(curTgt); err != nil {
|
||||
return
|
||||
}
|
||||
if !filepath.IsAbs(curTgt) {
|
||||
if relRoot != "" {
|
||||
curTgt = filepath.Join(relRoot, curTgt)
|
||||
} else {
|
||||
curTgt = filepath.Join(filepath.Dir(prevTgt), curTgt)
|
||||
}
|
||||
}
|
||||
}
|
||||
if curDepth >= MaxSymlinkLevel {
|
||||
err = ErrMaxSymlinkLevel
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
filterTimes checks a times.Timespec of a file using:
|
||||
* an age specified by the caller
|
||||
* an ageType bitmask for types of times to compare
|
||||
* an olderThan bool (if false, the file must be younger than)
|
||||
* an optional "now" timestamp for the age derivation.
|
||||
- an age specified by the caller
|
||||
- an ageType bitmask for types of times to compare
|
||||
- an olderThan bool (if false, the file must be younger than)
|
||||
- an optional "now" timestamp for the age derivation.
|
||||
*/
|
||||
func filterTimes(tspec times.Timespec, age *time.Duration, ageType *pathTimeType, olderThan bool, now *time.Time) (include bool) {
|
||||
|
||||
|
@ -111,7 +111,7 @@ func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (ma
|
||||
miss = &m
|
||||
return
|
||||
} else if typeMode != 0 {
|
||||
if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) {
|
||||
if !typeFilter.IsOneOf(bitmask.MaskBit(typeMode)) {
|
||||
m.MissReason = MissType
|
||||
miss = &m
|
||||
return
|
||||
|
27
pdsh/docs.go
Normal file
27
pdsh/docs.go
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Package pdsh (!! WIP !!) provides PDSH-compatible functionality for parsing group/gender/etc. files/sources.
|
||||
|
||||
Note that this library will *only* generate the host list/etc.,
|
||||
it will not actually connect to anything.
|
||||
It simply provides ways of returning lists of hosts using generation rules/patterns.
|
||||
Said another way, it does not implement any of PDSH's "rcmd" modules, only the "misc" modules.
|
||||
|
||||
(As a hint, you can implement SSH connections via [golang.org/x/crypto/ssh] in goroutine'd functions
|
||||
using this package to generate the target addresses, etc.)
|
||||
|
||||
Currently, the only supported PDSH module is misc/dshgroup (as [r00t2.io/sysutils/pdsh/dshgroup]) but additional/all other
|
||||
host list modules are planned.
|
||||
|
||||
This package deviates slightly from PDSH in some areas; allowing for more loose or more strict behavior occasionally.
|
||||
Whenever a deviation is offered, this package allows for configuring the generator to behave exactly like PDSH instead
|
||||
(if the deviating behavior is enabled by default).
|
||||
|
||||
For details, see the [chaos/pdsh GitHub], the associated [MAN page source], and/or the [rendered MAN page] (via ManKier).
|
||||
You may also want to see the ManKier rendered MAN pages for the [pdsh package].
|
||||
|
||||
[chaos/pdsh GitHub]: https://github.com/chaos/pdsh/
|
||||
[MAN page source]: https://github.com/chaos/pdsh/blob/master/doc/pdsh.1.in
|
||||
[rendered MAN page]: https://www.mankier.com/1/pdsh
|
||||
[pdsh package]: https://www.mankier.com/package/pdsh
|
||||
*/
|
||||
package pdsh
|
18
pdsh/dshgroup/consts.go
Normal file
18
pdsh/dshgroup/consts.go
Normal file
@ -0,0 +1,18 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"r00t2.io/goutils/remap"
|
||||
)
|
||||
|
||||
const (
|
||||
dshGrpPathEnv string = "DSHGROUP_PATH"
|
||||
)
|
||||
|
||||
// DSH Groups
|
||||
var (
|
||||
dshGrpDefGrpDir string = "/etc/dsh/group"
|
||||
dshGrpInclPtrn *remap.ReMap = &remap.ReMap{Regexp: regexp.MustCompile(`^\s*#include\s+(?P<incl>.+)$`)}
|
||||
dshGrpSubTokenPtrn *remap.ReMap = &remap.ReMap{Regexp: regexp.MustCompile(`^(?P<start_pad>0+)?(?P<start>[0-9]+)(-(?P<end_pad>0+)?(?P<end>[0-9]+))?$`)}
|
||||
)
|
30
pdsh/dshgroup/docs.go
Normal file
30
pdsh/dshgroup/docs.go
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Package dshgroup implements so-called "DSH (Dancer's SHell) Group" files.
|
||||
|
||||
It is equivalent to PDSH's [misc/dshgroup] module. ([source])
|
||||
|
||||
Be sure to read the [HOSTLIST EXPRESSIONS] section in the MAN page.
|
||||
|
||||
# Notable Differences
|
||||
|
||||
* This package allows for *never* reading the DSHGROUP_PATH env var (PDSH always reads it) via the "NoEnv" option.
|
||||
|
||||
* This package allows for not adding /etc/dsh/group/<group> files by default via the "NoDefault" option.
|
||||
|
||||
* This package allows for not adding ~/.dsh/group/<group> files by default via the "NoHome" option.
|
||||
|
||||
* This package allows for a "ForceLegacy" mode, disabled by default, that DISABLES the PDSH
|
||||
extension for "#include <path/group>" extension.
|
||||
If ForceLegacy is enabled, "#include ..." lines will be treated as comment lines (ignored) instead.
|
||||
|
||||
* This package allows for whitespace between group patterns. This can be disabled by the "StrictWhitespace" option.
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/dshgroup]: https://www.mankier.com/1/pdsh#dshgroup_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/dshgroup.c
|
||||
[HOSTLIST EXPRESSIONS]: https://www.mankier.com/1/pdsh#Hostlist_Expressions
|
||||
*/
|
||||
package dshgroup
|
11
pdsh/dshgroup/errs.go
Normal file
11
pdsh/dshgroup/errs.go
Normal file
@ -0,0 +1,11 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyDshGroupTok error = errors.New("empty dsh group pattern token")
|
||||
ErrInvalidDshGrpSyntax error = errors.New("invalid dsh group file syntax")
|
||||
ErrInvalidDshGrpPtrn error = errors.New("invalid dsh group pattern syntax")
|
||||
)
|
176
pdsh/dshgroup/funcs_dshgrouplister.go
Normal file
176
pdsh/dshgroup/funcs_dshgrouplister.go
Normal file
@ -0,0 +1,176 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"r00t2.io/sysutils/envs"
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
/*
|
||||
Evaluate returns a list of directories and files that would be searched/read with
|
||||
the given call and DshGroupLister configuration, in order of parsing.
|
||||
|
||||
The behavior is the same as DshGroupLister.GroupedHosts, including searchPaths.
|
||||
If DshGroupLister.ForceLegacy is false, include files will also be parsed in.
|
||||
(This may incur slightly additional processing time.)
|
||||
|
||||
Only existing dirs/files are returned. Symlinks are evaluated to their target.
|
||||
|
||||
If dedupe is true, deduplication is performed. This adds some cycles, but may be desired if you make heavy use of symlinks.
|
||||
*/
|
||||
func (d *DshGroupLister) Evaluate(dedupe bool, searchPaths ...string) (dirs, files []string, err error) {
|
||||
|
||||
var exists bool
|
||||
// var u *user.User
|
||||
var spl []string
|
||||
var dPath string
|
||||
var fPath string
|
||||
var incls []string
|
||||
var de fs.DirEntry
|
||||
var stat fs.FileInfo
|
||||
var entries []fs.DirEntry
|
||||
var tmpF []string
|
||||
var fpathMap map[string]bool = make(map[string]bool)
|
||||
|
||||
// TODO: Does/how does pdsh resolve relative symlinks?
|
||||
|
||||
// Dirs first
|
||||
if searchPaths != nil {
|
||||
for _, dPath = range searchPaths {
|
||||
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
|
||||
return
|
||||
} else if !exists {
|
||||
continue
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, dPath)
|
||||
}
|
||||
}
|
||||
if !d.NoHome && envs.HasEnv("HOME") {
|
||||
// So pdsh actually checks $HOME, it doesn't pull the homedir for the user.
|
||||
/*
|
||||
if u, err = user.Current(); err != nil {
|
||||
return
|
||||
}
|
||||
dPath = filepath.Join(u.HomeDir, ".dsh", "group")
|
||||
*/
|
||||
dPath = filepath.Join(os.Getenv("HOME"), ".dsh", "group")
|
||||
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
|
||||
return
|
||||
} else if exists {
|
||||
if stat.IsDir() {
|
||||
dirs = append(dirs, dPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !d.NoEnv && envs.HasEnv(dshGrpPathEnv) {
|
||||
spl = strings.Split(os.Getenv(dshGrpPathEnv), string(os.PathListSeparator))
|
||||
for _, dPath = range spl {
|
||||
if strings.TrimSpace(dPath) == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
|
||||
return
|
||||
} else if !exists {
|
||||
continue
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, dPath)
|
||||
}
|
||||
}
|
||||
if !d.NoDefault && !envs.HasEnv(dshGrpPathEnv) {
|
||||
dPath = dshGrpDefGrpDir
|
||||
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
|
||||
return
|
||||
} else if exists {
|
||||
if stat.IsDir() {
|
||||
dirs = append(dirs, dPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then files. Do *not* walk the dirs; only first-level is parsed by pdsh so this does the same.
|
||||
for _, dPath = range dirs {
|
||||
if entries, err = os.ReadDir(dPath); err != nil {
|
||||
return
|
||||
}
|
||||
for _, de = range entries {
|
||||
fPath = filepath.Join(dPath, de.Name())
|
||||
// NORMALLY, os.Stat calls stat(2), which follows symlinks. (os.Lstat()/lstat(2) does not.)
|
||||
// But the stat for an fs.DirEntry? Uses lstat.
|
||||
// Whatever, we want to resolve symlinks anyways.
|
||||
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&fPath, "."); err != nil {
|
||||
return
|
||||
} else if exists {
|
||||
if !stat.Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
if dedupe {
|
||||
if _, exists = fpathMap[fPath]; !exists {
|
||||
fpathMap[fPath] = true
|
||||
files = append(files, fPath)
|
||||
}
|
||||
} else {
|
||||
files = append(files, fPath)
|
||||
}
|
||||
if !d.ForceLegacy {
|
||||
if incls, err = getDshGrpIncludes(fPath); err != nil {
|
||||
return
|
||||
}
|
||||
if dedupe {
|
||||
for _, i := range incls {
|
||||
if _, exists = fpathMap[i]; !exists {
|
||||
fpathMap[i] = true
|
||||
files = append(files, i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = append(files, incls...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files = tmpF
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GroupedHosts returns a map of `map[<GROUP>][]string{<HOST>[, <HOST>, ...]}.
|
||||
|
||||
Additional search paths may be specified via searchpaths.
|
||||
|
||||
If there are any conflicting group names, the first found group name is used.
|
||||
For example, assuming the group name `<GROUP>`, the following files will be checked in this order:
|
||||
|
||||
0. IF searchPaths is not nil:
|
||||
a. searchpaths[0]/<GROUP>
|
||||
b. searchpaths[1]/<GROUP>
|
||||
c. searchpaths[2]/<GROUP>
|
||||
d. ( ... )
|
||||
1. IF DshGroupLister.NoHome is false:
|
||||
a. `~/.dsh/group/<GROUP>`
|
||||
2. IF $DSHGROUP_PATH is defined AND DshGroupLister.NoEnv is false:
|
||||
a. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[0]/<GROUP>`
|
||||
b. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[1]/<GROUP>`
|
||||
c. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[2]/<GROUP>`
|
||||
d. ( ... )
|
||||
3. IF $DSHGROUP_PATH is NOT defined AND DshGroupLister.NoDefault is false:
|
||||
a. `/etc/dsh/group/<GROUP>`
|
||||
*/
|
||||
func (d *DshGroupLister) GroupedHosts(dedupe bool, searchPaths ...string) (groupedHosts map[string][]string, err error) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
309
pdsh/dshgroup/funcs_dshgrp.go
Normal file
309
pdsh/dshgroup/funcs_dshgrp.go
Normal file
@ -0,0 +1,309 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
/*
|
||||
ParseDshPtrn parses ptrn using the DSH group pattern ptrn as according to `HOSTLIST EXPRESSSIONS` in pdsh(1).
|
||||
`#include` directives are explicitly skipped; this only parses actual generation pattern strings.
|
||||
|
||||
The returning generator may either be iterated over with `range` or have `Hosts()` called explicitly. // TODO
|
||||
*/
|
||||
func ParseDshPtrn(ptrn string) (generator *DshGrpGenerator, err error) {
|
||||
|
||||
var r rune
|
||||
var pos int
|
||||
var s string
|
||||
var inToken bool
|
||||
var tokStr string
|
||||
var tok dshGrpToken
|
||||
var strBuf *bytes.Buffer = new(bytes.Buffer)
|
||||
var tokBuf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
// TODO: users can be specified per-pattern.
|
||||
|
||||
generator = &DshGrpGenerator{
|
||||
tokens: make([]dshGrpToken, 0),
|
||||
tokenized: make([]string, 0),
|
||||
text: ptrn,
|
||||
}
|
||||
|
||||
s = strings.TrimSpace(ptrn)
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(s, "#") {
|
||||
return
|
||||
}
|
||||
// A quick sanity check. The end-state from the state machine below will catch any weird bracket issues beyond this.
|
||||
if strings.Count(s, "[") != strings.Count(s, "]") {
|
||||
err = ErrInvalidDshGrpSyntax
|
||||
return
|
||||
}
|
||||
|
||||
// Now the hacky bits. We read until we get to a start-token ('['), end-token (']'), or a pattern separator (',') that is *outside* a range token.
|
||||
for pos, r = range s {
|
||||
switch r {
|
||||
case '[':
|
||||
if inToken {
|
||||
// Nested [...[
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: ErrInvalidDshGrpSyntax,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
generator.tokenized = append(generator.tokenized, strBuf.String())
|
||||
strBuf.Reset()
|
||||
inToken = true
|
||||
case ']':
|
||||
if !inToken {
|
||||
// Nested ]...]
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: ErrInvalidDshGrpSyntax,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
tokStr = tokBuf.String()
|
||||
if tok, err = parseDshGrpToken(tokStr); err != nil {
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: err,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
generator.tokens = append(generator.tokens, tok)
|
||||
tokBuf.Reset()
|
||||
// Don't forget the empty element placeholder.
|
||||
generator.tokenized = append(generator.tokenized, "")
|
||||
inToken = false
|
||||
default:
|
||||
if inToken {
|
||||
// If it isn't between '0' and '9', isn't '-', and isn't ','...
|
||||
if !(0x30 <= r && r <= 0x39) && (r != 0x2d) && (r != 0x2c) {
|
||||
// It's not a valid token. (The actual syntax is validated in parseDshGrpToken and parseDshGrpSubtoken)
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: ErrInvalidDshGrpSyntax,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
tokBuf.WriteRune(r)
|
||||
} else {
|
||||
// TODO: confirm if inline comments and/or trailing/leading whitespace are handled by pdsh?
|
||||
if strings.TrimSpace(string(r)) == "" || r == '#' {
|
||||
// Whitespace is "invalid" (treat it as the end of the pattern).
|
||||
// Same for end-of-line octothorpes.
|
||||
if tokBuf.Len() > 0 {
|
||||
// This should never happen.
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: ErrInvalidDshGrpSyntax,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
if strBuf.Len() > 0 {
|
||||
generator.tokenized = append(generator.tokenized, strBuf.String())
|
||||
}
|
||||
break
|
||||
}
|
||||
// Otherwise we just check for valid DNS chars.
|
||||
if !(0x30 <= r && r <= 0x39) && // '0'-'9'
|
||||
(r != 0x2d) && // '-'
|
||||
(r != 0x2e) && // '.'
|
||||
!(0x41 <= r && r <= 0x5a) && // 'A' through 'Z' (inclusive)
|
||||
!(0x61 <= r && r <= 0x7a) { // 'a' through 'z' (inclusive)
|
||||
err = &PtrnParseErr{
|
||||
pos: uint(pos),
|
||||
ptrn: ptrn,
|
||||
r: r,
|
||||
err: ErrInvalidDshGrpPtrn,
|
||||
inToken: inToken,
|
||||
}
|
||||
return
|
||||
}
|
||||
// (Probably) valid(-ish), so add it.
|
||||
strBuf.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the token never closed, it's also invalid.
|
||||
if inToken {
|
||||
err = ErrInvalidDshGrpSyntax
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseDshGrpToken parses a token string into a dshGrpToken.
|
||||
func parseDshGrpToken(tokenStr string) (token dshGrpToken, err error) {
|
||||
|
||||
var s string
|
||||
var st []string
|
||||
var sub dshGrpSubtoken
|
||||
|
||||
s = strings.TrimSpace(tokenStr)
|
||||
if s == "" {
|
||||
err = ErrEmptyDshGroupTok
|
||||
return
|
||||
}
|
||||
st = strings.Split(s, ",")
|
||||
token = dshGrpToken{
|
||||
token: tokenStr,
|
||||
subtokens: make([]dshGrpSubtoken, 0, len(st)),
|
||||
}
|
||||
for _, s = range st {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
if sub, err = parseDshGrpSubtoken(s); err != nil {
|
||||
return
|
||||
}
|
||||
token.subtokens = append(token.subtokens, sub)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseDshGrpSubtoken parses a subtoken string into a dshGrpSubtoken.
|
||||
func parseDshGrpSubtoken(subTokenStr string) (subtoken dshGrpSubtoken, err error) {
|
||||
|
||||
var u64 uint64
|
||||
var vals []string
|
||||
var endPad string
|
||||
var startPad string
|
||||
var st dshGrpSubtoken
|
||||
var matches map[string][]string
|
||||
|
||||
if matches = dshGrpSubTokenPtrn.MapString(subTokenStr, false, false, true); matches == nil || len(matches) == 0 {
|
||||
err = ErrInvalidDshGrpPtrn
|
||||
return
|
||||
}
|
||||
if vals = matches["start_pad"]; vals != nil && len(vals) == 1 {
|
||||
startPad = vals[0]
|
||||
}
|
||||
|
||||
if vals = matches["start"]; vals != nil && len(vals) == 1 {
|
||||
if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil {
|
||||
return
|
||||
}
|
||||
st.start = uint(u64)
|
||||
}
|
||||
|
||||
if vals = matches["end_pad"]; vals != nil && len(vals) == 1 {
|
||||
endPad = vals[0]
|
||||
}
|
||||
if vals = matches["end"]; vals != nil && len(vals) == 1 {
|
||||
if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil {
|
||||
return
|
||||
}
|
||||
st.end = uint(u64)
|
||||
}
|
||||
|
||||
if startPad != "" && endPad != "" {
|
||||
// We set the pad to the largest.
|
||||
if len(startPad) > len(endPad) {
|
||||
st.pad = startPad
|
||||
} else {
|
||||
st.pad = endPad
|
||||
}
|
||||
} else if startPad != "" {
|
||||
st.pad = startPad
|
||||
} else if endPad != "" {
|
||||
st.pad = endPad
|
||||
}
|
||||
|
||||
subtoken = st
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
getDshGrpIncludes parses fpath for `#include ...` directives. It skips any entries in which
|
||||
`len(paths.SegmentSys(p) == []string{p}`, as these are inherently included by the dir read.
|
||||
|
||||
It is assumed that fpath is a cleaned, absolute filepath.
|
||||
*/
|
||||
func getDshGrpIncludes(fpath string) (includes []string, err error) {
|
||||
|
||||
var f *os.File
|
||||
var line string
|
||||
var exists bool
|
||||
var inclpath string
|
||||
var subIncl []string
|
||||
var segs []string
|
||||
var scanner *bufio.Scanner
|
||||
var matches map[string][]string
|
||||
|
||||
if f, err = os.Open(fpath); err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner = bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if !dshGrpInclPtrn.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
matches = dshGrpInclPtrn.MapString(line, false, false, true)
|
||||
if matches == nil {
|
||||
err = ErrInvalidDshGrpSyntax
|
||||
return
|
||||
}
|
||||
if matches["incl"] == nil || len(matches["incl"]) == 0 {
|
||||
err = ErrInvalidDshGrpSyntax
|
||||
return
|
||||
}
|
||||
inclpath = matches["incl"][0]
|
||||
segs = paths.SegmentSys(inclpath, false, false)
|
||||
if segs == nil || len(segs) == 0 || (len(segs) == 1 && segs[0] == inclpath) {
|
||||
continue
|
||||
}
|
||||
|
||||
if exists, err = paths.RealPathExists(&inclpath); err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
includes = append(includes, inclpath)
|
||||
if subIncl, err = getDshGrpIncludes(inclpath); err != nil {
|
||||
return
|
||||
}
|
||||
if subIncl != nil && len(subIncl) > 0 {
|
||||
includes = append(includes, subIncl...)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
59
pdsh/dshgroup/funcs_dshgrp_test.go
Normal file
59
pdsh/dshgroup/funcs_dshgrp_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
`log`
|
||||
`testing`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
)
|
||||
|
||||
func TestParseDshPtrn(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var s string
|
||||
var generator *DshGrpGenerator
|
||||
var hostList []string
|
||||
var tgtList []string = []string{
|
||||
"0foo1bar46004quux", "0foo1bar46005quux", "0foo1bar46006quux", "0foo1bar46007quux", "0foo1bar46008quux", "0foo1bar46009quux",
|
||||
"0foo1bar4615quux", "0foo1bar47004quux", "0foo1bar47005quux", "0foo1bar47006quux", "0foo1bar47007quux", "0foo1bar47008quux",
|
||||
"0foo1bar47009quux", "0foo1bar4715quux", "0foo2bar46004quux", "0foo2bar46005quux", "0foo2bar46006quux", "0foo2bar46007quux",
|
||||
"0foo2bar46008quux", "0foo2bar46009quux", "0foo2bar4615quux", "0foo2bar47004quux", "0foo2bar47005quux", "0foo2bar47006quux",
|
||||
"0foo2bar47007quux", "0foo2bar47008quux", "0foo2bar47009quux", "0foo2bar4715quux", "0foo3bar46004quux", "0foo3bar46005quux",
|
||||
"0foo3bar46006quux", "0foo3bar46007quux", "0foo3bar46008quux", "0foo3bar46009quux", "0foo3bar4615quux", "0foo3bar47004quux",
|
||||
"0foo3bar47005quux", "0foo3bar47006quux", "0foo3bar47007quux", "0foo3bar47008quux", "0foo3bar47009quux", "0foo3bar4715quux",
|
||||
"1foo1bar46004quux", "1foo1bar46005quux", "1foo1bar46006quux", "1foo1bar46007quux", "1foo1bar46008quux", "1foo1bar46009quux",
|
||||
"1foo1bar4615quux", "1foo1bar47004quux", "1foo1bar47005quux", "1foo1bar47006quux", "1foo1bar47007quux", "1foo1bar47008quux",
|
||||
"1foo1bar47009quux", "1foo1bar4715quux", "1foo2bar46004quux", "1foo2bar46005quux", "1foo2bar46006quux", "1foo2bar46007quux",
|
||||
"1foo2bar46008quux", "1foo2bar46009quux", "1foo2bar4615quux", "1foo2bar47004quux", "1foo2bar47005quux", "1foo2bar47006quux",
|
||||
"1foo2bar47007quux", "1foo2bar47008quux", "1foo2bar47009quux", "1foo2bar4715quux", "1foo3bar46004quux", "1foo3bar46005quux",
|
||||
"1foo3bar46006quux", "1foo3bar46007quux", "1foo3bar46008quux", "1foo3bar46009quux", "1foo3bar4615quux", "1foo3bar47004quux",
|
||||
"1foo3bar47005quux", "1foo3bar47006quux", "1foo3bar47007quux", "1foo3bar47008quux", "1foo3bar47009quux", "1foo3bar4715quux",
|
||||
"2foo1bar46004quux", "2foo1bar46005quux", "2foo1bar46006quux", "2foo1bar46007quux", "2foo1bar46008quux", "2foo1bar46009quux",
|
||||
"2foo1bar4615quux", "2foo1bar47004quux", "2foo1bar47005quux", "2foo1bar47006quux", "2foo1bar47007quux", "2foo1bar47008quux",
|
||||
"2foo1bar47009quux", "2foo1bar4715quux", "2foo2bar46004quux", "2foo2bar46005quux", "2foo2bar46006quux", "2foo2bar46007quux",
|
||||
"2foo2bar46008quux", "2foo2bar46009quux", "2foo2bar4615quux", "2foo2bar47004quux", "2foo2bar47005quux", "2foo2bar47006quux",
|
||||
"2foo2bar47007quux", "2foo2bar47008quux", "2foo2bar47009quux", "2foo2bar4715quux", "2foo3bar46004quux", "2foo3bar46005quux",
|
||||
"2foo3bar46006quux", "2foo3bar46007quux", "2foo3bar46008quux", "2foo3bar46009quux", "2foo3bar4615quux", "2foo3bar47004quux",
|
||||
"2foo3bar47005quux", "2foo3bar47006quux", "2foo3bar47007quux", "2foo3bar47008quux", "2foo3bar47009quux", "2foo3bar4715quux",
|
||||
}
|
||||
|
||||
if generator, err = ParseDshPtrn("[0-2]foo[1-3]bar[4][6-7]baz[004-009,15]quux"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_ = spew.Sdump(generator)
|
||||
|
||||
hostList = generator.Hosts()
|
||||
t.Log(hostList)
|
||||
|
||||
if len(hostList) != len(tgtList) {
|
||||
t.Fatalf("Generated list length (%d) does not match target (%d)", len(hostList), len(tgtList))
|
||||
}
|
||||
|
||||
for idx, s = range hostList {
|
||||
if s != tgtList[idx] {
|
||||
log.Fatalf("Test vector %d ('%s') does not match generated value '%s'", idx+1, tgtList[idx], s)
|
||||
}
|
||||
}
|
||||
}
|
36
pdsh/dshgroup/funcs_dshgrpgenerator.go
Normal file
36
pdsh/dshgroup/funcs_dshgrpgenerator.go
Normal file
@ -0,0 +1,36 @@
|
||||
package dshgroup
|
||||
|
||||
func (d *DshGrpGenerator) Generate() (yieldFunc func(yield func(host string) (done bool))) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DshGrpGenerator) Hosts() (hostList []string) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DshGrpGenerator) Host() (host string) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DshGrpGenerator) Next() (done bool) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DshGrpGenerator) Reset() {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
16
pdsh/dshgroup/funcs_ptrnparseerr.go
Normal file
16
pdsh/dshgroup/funcs_ptrnparseerr.go
Normal file
@ -0,0 +1,16 @@
|
||||
package dshgroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error conforms a PtrnParseErr to error interface.
|
||||
func (p *PtrnParseErr) Error() (errStr string) {
|
||||
|
||||
errStr = fmt.Sprintf(
|
||||
"Parse error in pattern '%s', position %d rune '%s' (%#x) (in token: %v): %v",
|
||||
p.ptrn, p.pos, string(p.r), p.r, p.inToken, p.err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
90
pdsh/dshgroup/types.go
Normal file
90
pdsh/dshgroup/types.go
Normal file
@ -0,0 +1,90 @@
|
||||
package dshgroup
|
||||
|
||||
// TODO: This... doesn't really have much usefulness, does it?
|
||||
/*
|
||||
type (
|
||||
HostLister interface {
|
||||
// Hosts returns ALL hsots (where applicable) that are considered/generated for a Lister.
|
||||
Hosts() (hosts []string, err error)
|
||||
}
|
||||
)
|
||||
*/
|
||||
|
||||
type (
|
||||
/*
|
||||
DshGroupLister behaves like the host list generator
|
||||
for pdsh(1)'s "dshgroup module options" (the `misc/dshgroup`
|
||||
module for pdsh).
|
||||
*/
|
||||
DshGroupLister struct {
|
||||
/*
|
||||
NoEnv, if true, will *not* use DSHGROUP_PATH (force-defaulting to /etc/dsh/group/,
|
||||
but see NoDefault).
|
||||
*/
|
||||
NoEnv bool
|
||||
/*
|
||||
NoDefault, if true, will *not* add the default path `/etc/dsh/group/`
|
||||
to the search paths.
|
||||
|
||||
If NoDefault is false, this path is only added if DSHGROUP_PATH is not defined
|
||||
(or, if it IS defined, if NoEnv is true).
|
||||
*/
|
||||
NoDefault bool
|
||||
// NoHome, if true, will *not* add the `~/.dsh/group/` path to the search paths.
|
||||
NoHome bool
|
||||
/*
|
||||
ForceLegacy, if true, will disable the PDSH `#include <PATH|GROUP>` modification --
|
||||
treating the source as a traditional DSH group file instead (e.g. `#include ...`
|
||||
is treated as just a comment).
|
||||
*/
|
||||
ForceLegacy bool
|
||||
// StrictWhitespace follows the same behavior as PDSH regarding no whitespace between patterns.
|
||||
StrictWhitespace bool
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
// DshGrpGenerator generates a list of hosts according to the pdsh "dshgroup" module.
|
||||
DshGrpGenerator struct {
|
||||
/*
|
||||
tokens are interleaved with tokenized and indexed *after*;
|
||||
in other words, str = <substr0><token0><substr1><token1>...
|
||||
*/
|
||||
tokens []dshGrpToken
|
||||
// tokenized holds the split original text with tokens removed and split where the tokens occur.
|
||||
tokenized []string
|
||||
// text holds the original pattern.
|
||||
text string
|
||||
}
|
||||
dshGrpToken struct {
|
||||
/*
|
||||
token contains the original range specifier.
|
||||
Tokens may be e.g.:
|
||||
|
||||
* 3: str3
|
||||
* 3-5: str3, str4, str5
|
||||
* 3,5: str3, str5
|
||||
*/
|
||||
token string
|
||||
// subtokens hold a split of the individual range specifiers.
|
||||
subtokens []dshGrpSubtoken
|
||||
}
|
||||
dshGrpSubtoken struct {
|
||||
// start indicates either the single value or the start of the range.
|
||||
start uint
|
||||
// end, if 0 or less than start, indicates a single-value range.
|
||||
end uint
|
||||
// pad, if non-empty, is a string to add to the beginning of each of the generated substrings for this subtoken.
|
||||
pad string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
PtrnParseErr struct {
|
||||
pos uint
|
||||
ptrn string
|
||||
r rune
|
||||
err error
|
||||
inToken bool
|
||||
}
|
||||
)
|
11
pdsh/genders/docs.go
Normal file
11
pdsh/genders/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package genders implements the [misc/genders] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/genders]: https://www.mankier.com/1/pdsh#genders_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/genders.c
|
||||
*/
|
||||
package genders
|
11
pdsh/machines/docs.go
Normal file
11
pdsh/machines/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package machines implements the [misc/machines] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/machines]: https://www.mankier.com/1/pdsh#machines_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/machines.c
|
||||
*/
|
||||
package machines
|
11
pdsh/netgroup/docs.go
Normal file
11
pdsh/netgroup/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package netgroup implements the [misc/netgroup] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/netgroup]: https://www.mankier.com/1/pdsh#netgroup_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/netgroup.c
|
||||
*/
|
||||
package netgroup
|
11
pdsh/nodeupdown/docs.go
Normal file
11
pdsh/nodeupdown/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package nodeupdown implements the [misc/nodeupdown] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/nodeupdown]: https://www.mankier.com/1/pdsh#nodeupdown_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/nodeupdown.c
|
||||
*/
|
||||
package nodeupdown
|
11
pdsh/slurm/docs.go
Normal file
11
pdsh/slurm/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package slurm implements the [misc/slurm] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/slurm]: https://www.mankier.com/1/pdsh#slurm_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/slurm.c
|
||||
*/
|
||||
package slurm
|
11
pdsh/torque/docs.go
Normal file
11
pdsh/torque/docs.go
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Package torque implements the [misc/torque] PDSH module. ([source])
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[misc/torque]: https://www.mankier.com/1/pdsh#torque_module_options
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/torque.c
|
||||
*/
|
||||
package torque
|
60
pdsh/types.go
Normal file
60
pdsh/types.go
Normal file
@ -0,0 +1,60 @@
|
||||
package pdsh
|
||||
|
||||
type (
|
||||
/*
|
||||
Generator is one of the PDSH host generators/iterators offered by this module.
|
||||
|
||||
Note that these generators/iterators are *stateful*, which means they shouldn't
|
||||
(probably; I'm not your dad) be used concurrently (unless you want some hard-to-debug results)
|
||||
and all methods advance the generator - so you probably don't want to call both Generate() and
|
||||
Next()/Host() on the same instance, for example.
|
||||
*/
|
||||
Generator interface {
|
||||
/*
|
||||
Generate provides a Go-native iterator (also called a "RangeFunc" or "range over function type")
|
||||
as found in Go 1.23 onwards.
|
||||
|
||||
See the assocaied blog entry for details: https://go.dev/blog/range-functions
|
||||
|
||||
Essentially it allows for e.g.:
|
||||
|
||||
for host := range (Generator).Generate() {
|
||||
// ...
|
||||
}
|
||||
|
||||
which is the "new standard" approach for iteration.
|
||||
*/
|
||||
Generate() (yieldFunc func(yield func(host string) (done bool)))
|
||||
/*
|
||||
Reset is used to reset a Generator, allowing one to "restart" the generation at the beginning.
|
||||
|
||||
Generators in this module are generally single-use, but can be reset/reused with this method.
|
||||
*/
|
||||
Reset()
|
||||
/*
|
||||
Hosts returns a complete generated hostlist at once if you'd rather not iterate.
|
||||
|
||||
Hosts() *does* perform an iteration in runtime, so the recommendation against concurrency
|
||||
stands, but it calls Reset() when done generating to allow other methods of a Generator to be used.
|
||||
*/
|
||||
Hosts() (hostList []string)
|
||||
/*
|
||||
Next and Host behave like more "traditional" iterators, e.g. like (database/sql).Row.Next().
|
||||
|
||||
Next advances the internal state to the next host, and Host() returns it.
|
||||
*/
|
||||
Next() (done bool)
|
||||
/*
|
||||
Host returns the current host string (or "" if done).
|
||||
|
||||
Be sure to e.g.:
|
||||
|
||||
for (Generator).Next() {
|
||||
host := (Generator).Host()
|
||||
}
|
||||
|
||||
otherwise the Host return value will not change.
|
||||
*/
|
||||
Host() (host string)
|
||||
}
|
||||
)
|
14
pdsh/wcoll/docs.go
Normal file
14
pdsh/wcoll/docs.go
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
Package wcoll implements the "default" [WCOLL] method for PDSH. ([source])
|
||||
|
||||
Be sure to read the [HOSTLIST EXPRESSIONS] section in the MAN page.
|
||||
|
||||
# TODO/WIP/Not Yet Implemented
|
||||
|
||||
This package is not yet complete.
|
||||
|
||||
[WCOLL]: https://www.mankier.com/1/pdsh#Environment_Variables
|
||||
[source]: https://github.com/chaos/pdsh/blob/master/src/pdsh/wcoll.c
|
||||
[HOSTLIST EXPRESSIONS]: https://www.mankier.com/1/pdsh#Hostlist_Expressions
|
||||
*/
|
||||
package wcoll
|
53
types_linux.go
Normal file
53
types_linux.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sysutils
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/unix`
|
||||
)
|
||||
|
||||
/*
|
||||
IDState collects information about the current running process.
|
||||
It should only be used as returned from GetIDState().
|
||||
Its methods WILL return false information if any of these values are altered.
|
||||
|
||||
FSUID/FSGID are not supported.
|
||||
*/
|
||||
type IDState struct {
|
||||
// RUID: Real UID
|
||||
RUID int
|
||||
// EUID: Effective UID
|
||||
EUID int
|
||||
// SUID: Saved Set UID
|
||||
SUID int
|
||||
// RGID: Real GID
|
||||
RGID int
|
||||
// EGID: Effective GID
|
||||
EGID int
|
||||
// SGID: Saved Set GID
|
||||
SGID int
|
||||
// SudoEnvUser is true if SUDO_USER or SUDO_UID is set.
|
||||
SudoEnvUser bool
|
||||
// SudoEnvGroup is true if SUDO_GID is set.
|
||||
SudoEnvGroup bool
|
||||
// SudoEnvCmd is true if SUDO_COMMAND is set.
|
||||
SudoEnvCmd bool
|
||||
// SudoEnvHome is true if SUDO_HOME is set.
|
||||
SudoEnvHome bool
|
||||
// SudoEnvVars is true if any of the "well-known" sudo environment variables are set.
|
||||
SudoEnvVars bool
|
||||
// PPIDUidMatch is true if the parent PID UID matches the current process UID (mismatch usually indicates sudo invocation).
|
||||
PPIDUidMatch bool
|
||||
// PPIDGidMatch is true if the parent PID GID matches the current process GID (mismatch usually indicates sudo invocation).
|
||||
PPIDGidMatch bool
|
||||
// uidsChecked is true if the RUID, EUID, and SUID have been populated. (They will be 0 if unset OR if root.)
|
||||
uidsChecked bool
|
||||
// gidsChecked is true if the RGID, EGID, and SGID have been populated. (They will be 0 if unset OR if root.)
|
||||
gidsChecked bool
|
||||
// sudoChecked is true if the SudoEnvVars is set.
|
||||
sudoChecked bool
|
||||
// ppidUidChecked is true if the PPIDUidMatch is set.
|
||||
ppidUidChecked bool
|
||||
// ppidGidChecked is true if the PPIDGidMatch is set.
|
||||
ppidGidChecked bool
|
||||
// stat holds the stat information for the parent PID.
|
||||
stat *unix.Stat_t
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user