rename to fortify and restructure
More sandbox features will be added and this will no longer track ego's features and behaviour. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
99
internal/acl/c.go
Normal file
99
internal/acl/c.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <sys/acl.h>
|
||||
//#include <acl/libacl.h>
|
||||
//#cgo linux LDFLAGS: -lacl
|
||||
import "C"
|
||||
|
||||
type acl struct {
|
||||
val C.acl_t
|
||||
freed bool
|
||||
}
|
||||
|
||||
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
|
||||
p := C.CString(path)
|
||||
a, err := C.acl_get_file(p, t)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
if errors.Is(err, syscall.ENODATA) {
|
||||
err = nil
|
||||
}
|
||||
return &acl{val: a, freed: false}, err
|
||||
}
|
||||
|
||||
func (a *acl) setFile(path string, t C.acl_type_t) error {
|
||||
if C.acl_valid(a.val) != 0 {
|
||||
return fmt.Errorf("invalid acl")
|
||||
}
|
||||
|
||||
p := C.CString(path)
|
||||
_, err := C.acl_set_file(p, t, a.val)
|
||||
C.free(unsafe.Pointer(p))
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *acl) removeEntry(tt C.acl_tag_t, tq int) error {
|
||||
var e C.acl_entry_t
|
||||
|
||||
// get first entry
|
||||
if r, err := C.acl_get_entry(a.val, C.ACL_FIRST_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on acl with no entries
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if r, err := C.acl_get_entry(a.val, C.ACL_NEXT_ENTRY, &e); err != nil {
|
||||
return err
|
||||
} else if r == 0 {
|
||||
// return on drained acl
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
q int
|
||||
t C.acl_tag_t
|
||||
)
|
||||
|
||||
// get current entry tag type
|
||||
if _, err := C.acl_get_tag_type(e, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get current entry qualifier
|
||||
if rq, err := C.acl_get_qualifier(e); err != nil {
|
||||
// neither ACL_USER nor ACL_GROUP
|
||||
if errors.Is(err, syscall.EINVAL) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
} else {
|
||||
q = *(*int)(rq)
|
||||
C.acl_free(rq)
|
||||
}
|
||||
|
||||
// delete on match
|
||||
if t == tt && q == tq {
|
||||
_, err := C.acl_delete_entry(a.val, e)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *acl) free() {
|
||||
if a.freed {
|
||||
panic("acl already freed")
|
||||
}
|
||||
C.acl_free(unsafe.Pointer(a.val))
|
||||
a.freed = true
|
||||
}
|
||||
86
internal/acl/export.go
Normal file
86
internal/acl/export.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package acl
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <sys/acl.h>
|
||||
//#include <acl/libacl.h>
|
||||
//#cgo linux LDFLAGS: -lacl
|
||||
import "C"
|
||||
|
||||
const (
|
||||
Read = C.ACL_READ
|
||||
Write = C.ACL_WRITE
|
||||
Execute = C.ACL_EXECUTE
|
||||
|
||||
TypeDefault = C.ACL_TYPE_DEFAULT
|
||||
TypeAccess = C.ACL_TYPE_ACCESS
|
||||
|
||||
UndefinedTag = C.ACL_UNDEFINED_TAG
|
||||
UserObj = C.ACL_USER_OBJ
|
||||
User = C.ACL_USER
|
||||
GroupObj = C.ACL_GROUP_OBJ
|
||||
Group = C.ACL_GROUP
|
||||
Mask = C.ACL_MASK
|
||||
Other = C.ACL_OTHER
|
||||
)
|
||||
|
||||
func UpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
|
||||
// read acl from file
|
||||
a, err := aclGetFile(path, TypeAccess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// free acl on return if get is successful
|
||||
defer a.free()
|
||||
|
||||
// remove existing entry
|
||||
if err = a.removeEntry(User, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new entry if perms are passed
|
||||
if len(perms) > 0 {
|
||||
// create new acl entry
|
||||
var e C.acl_entry_t
|
||||
if _, err = C.acl_create_entry(&a.val, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get perm set of new entry
|
||||
var p C.acl_permset_t
|
||||
if _, err = C.acl_get_permset(e, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add target perms
|
||||
for _, perm := range perms {
|
||||
if _, err = C.acl_add_perm(p, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set perm set to new entry
|
||||
if _, err = C.acl_set_permset(e, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set user tag to new entry
|
||||
if _, err = C.acl_set_tag_type(e, User); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set qualifier (uid) to new entry
|
||||
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// calculate mask after update
|
||||
if _, err = C.acl_calc_mask(&a.val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write acl to file
|
||||
return a.setFile(path, TypeAccess)
|
||||
}
|
||||
13
internal/app/builder.go
Normal file
13
internal/app/builder.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package app
|
||||
|
||||
func (a *App) Command() []string {
|
||||
return a.command
|
||||
}
|
||||
|
||||
func (a *App) UID() int {
|
||||
return a.uid
|
||||
}
|
||||
|
||||
func (a *App) AppendEnv(k, v string) {
|
||||
a.env = append(a.env, k+"="+v)
|
||||
}
|
||||
152
internal/app/launch.go
Normal file
152
internal/app/launch.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
sudoAskPass = "SUDO_ASKPASS"
|
||||
launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
|
||||
)
|
||||
|
||||
func (a *App) launcherPayloadEnv() string {
|
||||
r := &bytes.Buffer{}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
||||
|
||||
if err := gob.NewEncoder(enc).Encode(a.command); err != nil {
|
||||
state.Fatal("Error encoding launcher payload:", err)
|
||||
}
|
||||
|
||||
_ = enc.Close()
|
||||
return launcherPayload + "=" + r.String()
|
||||
}
|
||||
|
||||
// Early hidden launcher path
|
||||
func Early(printVersion bool) {
|
||||
if printVersion {
|
||||
if r, ok := os.LookupEnv(launcherPayload); ok {
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
|
||||
|
||||
var argv []string
|
||||
if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
|
||||
fmt.Println("Error decoding launcher payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.Unsetenv(launcherPayload); err != nil {
|
||||
fmt.Println("Error unsetting launcher payload:", err)
|
||||
// not fatal, do not fail
|
||||
}
|
||||
|
||||
var p string
|
||||
|
||||
if len(argv) > 0 {
|
||||
if p, ok = util.Which(argv[0]); !ok {
|
||||
fmt.Printf("Did not find '%s' in PATH\n", argv[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if p, ok = os.LookupEnv("SHELL"); !ok {
|
||||
fmt.Println("No command was specified and $SHELL was unset")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := syscall.Exec(p, argv, os.Environ()); err != nil {
|
||||
fmt.Println("Error executing launcher payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// unreachable
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) launchBySudo() (args []string) {
|
||||
args = make([]string, 0, 4+len(a.env)+len(a.command))
|
||||
|
||||
// -Hiu $USER
|
||||
args = append(args, "-Hiu", a.Username)
|
||||
|
||||
// -A?
|
||||
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
||||
if system.V.Verbose {
|
||||
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
||||
}
|
||||
args = append(args, "-A")
|
||||
}
|
||||
|
||||
// environ
|
||||
args = append(args, a.env...)
|
||||
|
||||
// -- $@
|
||||
args = append(args, "--")
|
||||
args = append(args, a.command...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) launchByMachineCtl(bare bool) (args []string) {
|
||||
args = make([]string, 0, 9+len(a.env))
|
||||
|
||||
// shell --uid=$USER
|
||||
args = append(args, "shell", "--uid="+a.Username)
|
||||
|
||||
// --quiet
|
||||
if !system.V.Verbose {
|
||||
args = append(args, "--quiet")
|
||||
}
|
||||
|
||||
// environ
|
||||
envQ := make([]string, len(a.env)+1)
|
||||
for i, e := range a.env {
|
||||
envQ[i] = "-E" + e
|
||||
}
|
||||
envQ[len(a.env)] = "-E" + a.launcherPayloadEnv()
|
||||
args = append(args, envQ...)
|
||||
|
||||
// -- .host
|
||||
args = append(args, "--", ".host")
|
||||
|
||||
// /bin/sh -c
|
||||
if sh, ok := util.Which("sh"); !ok {
|
||||
state.Fatal("Did not find 'sh' in PATH")
|
||||
} else {
|
||||
args = append(args, sh, "-c")
|
||||
}
|
||||
|
||||
if len(a.command) == 0 { // execute shell if command is not provided
|
||||
a.command = []string{"$SHELL"}
|
||||
}
|
||||
|
||||
innerCommand := strings.Builder{}
|
||||
|
||||
if !bare {
|
||||
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
||||
for _, e := range a.env {
|
||||
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
||||
}
|
||||
innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
|
||||
}
|
||||
|
||||
if executable, err := os.Executable(); err != nil {
|
||||
state.Fatal("Error reading executable path:", err)
|
||||
} else {
|
||||
innerCommand.WriteString("exec " + executable + " -V")
|
||||
}
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
}
|
||||
124
internal/app/setup.go
Normal file
124
internal/app/setup.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
uid int
|
||||
env []string
|
||||
command []string
|
||||
|
||||
*user.User
|
||||
}
|
||||
|
||||
func (a *App) Run() {
|
||||
f := a.launchBySudo
|
||||
m, b := false, false
|
||||
switch {
|
||||
case system.MethodFlags[0]: // sudo
|
||||
case system.MethodFlags[1]: // bare
|
||||
m, b = true, true
|
||||
default: // machinectl
|
||||
m, b = true, false
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
|
||||
// dependency checks
|
||||
const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work"
|
||||
if m {
|
||||
if !util.SdBooted() {
|
||||
fmt.Println("This system was not booted through systemd")
|
||||
fmt.Println(sudoFallback)
|
||||
} else if tp, ok := util.Which("machinectl"); !ok {
|
||||
fmt.Println("Did not find 'machinectl' in PATH")
|
||||
fmt.Println(sudoFallback)
|
||||
} else {
|
||||
toolPath = tp
|
||||
f = func() []string { return a.launchByMachineCtl(b) }
|
||||
}
|
||||
} else if tp, ok := util.Which("sudo"); !ok {
|
||||
state.Fatal("Did not find 'sudo' in PATH")
|
||||
} else {
|
||||
toolPath = tp
|
||||
}
|
||||
|
||||
if system.V.Verbose {
|
||||
fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
|
||||
}
|
||||
|
||||
cmd := exec.Command(toolPath, f()...)
|
||||
cmd.Env = a.env
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = system.V.RunDir
|
||||
|
||||
if system.V.Verbose {
|
||||
fmt.Println("Executing:", cmd)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
state.Fatal("Error starting process:", err)
|
||||
}
|
||||
|
||||
if err := state.SaveProcess(a.Uid, cmd); err != nil {
|
||||
// process already started, shouldn't be fatal
|
||||
fmt.Println("Error registering process:", err)
|
||||
}
|
||||
|
||||
var r int
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
state.Fatal("Error running process:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if system.V.Verbose {
|
||||
fmt.Println("Process exited with exit code", r)
|
||||
}
|
||||
state.BeforeExit()
|
||||
os.Exit(r)
|
||||
}
|
||||
|
||||
func New(userName string, args []string) *App {
|
||||
a := &App{command: args}
|
||||
|
||||
if u, err := user.Lookup(userName); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
fmt.Println("unknown user", userName)
|
||||
} else {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.User = u
|
||||
}
|
||||
|
||||
if u, err := strconv.Atoi(a.Uid); err != nil {
|
||||
// usually unreachable
|
||||
panic("uid parse")
|
||||
} else {
|
||||
a.uid = u
|
||||
}
|
||||
|
||||
if system.V.Verbose {
|
||||
fmt.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
68
internal/state/exit.go
Normal file
68
internal/state/exit.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
"git.ophivana.moe/cat/fortify/internal/xcb"
|
||||
)
|
||||
|
||||
func Fatal(msg ...any) {
|
||||
fmt.Println(msg...)
|
||||
BeforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func BeforeExit() {
|
||||
if u == nil {
|
||||
fmt.Println("warn: beforeExit called before app init")
|
||||
return
|
||||
}
|
||||
|
||||
if statePath == "" {
|
||||
if system.V.Verbose {
|
||||
fmt.Println("State path is unset")
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("Error removing state file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if d, err := readLaunchers(); err != nil {
|
||||
fmt.Println("Error reading active launchers:", err)
|
||||
os.Exit(1)
|
||||
} else if len(d) > 0 {
|
||||
// other launchers are still active
|
||||
if system.V.Verbose {
|
||||
fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(d))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if system.V.Verbose {
|
||||
fmt.Println("No other launchers active, will clean up")
|
||||
}
|
||||
|
||||
if xcbActionComplete {
|
||||
if system.V.Verbose {
|
||||
fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", u.Username)
|
||||
}
|
||||
if err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+u.Username); err != nil {
|
||||
fmt.Println("Error removing XHost entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range cleanupCandidate {
|
||||
if err := acl.UpdatePerm(candidate, uid); err != nil {
|
||||
fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
|
||||
}
|
||||
if system.V.Verbose {
|
||||
fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", u.Username, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
internal/state/register.go
Normal file
12
internal/state/register.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package state
|
||||
|
||||
func RegisterRevertPath(p string) {
|
||||
cleanupCandidate = append(cleanupCandidate, p)
|
||||
}
|
||||
|
||||
func XcbActionComplete() {
|
||||
if xcbActionComplete {
|
||||
Fatal("xcb inserted twice")
|
||||
}
|
||||
xcbActionComplete = true
|
||||
}
|
||||
115
internal/state/track.go
Normal file
115
internal/state/track.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
)
|
||||
|
||||
// we unfortunately have to assume there are never races between processes
|
||||
// this and launcher should eventually be replaced by a server process
|
||||
|
||||
var (
|
||||
stateActionEarly bool
|
||||
statePath string
|
||||
cleanupCandidate []string
|
||||
xcbActionComplete bool
|
||||
)
|
||||
|
||||
type launcherState struct {
|
||||
PID int
|
||||
Launcher string
|
||||
Argv []string
|
||||
Command []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
|
||||
}
|
||||
|
||||
func Early() {
|
||||
if !stateActionEarly {
|
||||
return
|
||||
}
|
||||
|
||||
launchers, err := readLaunchers()
|
||||
if err != nil {
|
||||
fmt.Println("Error reading launchers:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("\tPID\tLauncher")
|
||||
for _, state := range launchers {
|
||||
fmt.Printf("\t%d\t%s\nCommand: %s\nArgv: %s\n", state.PID, state.Launcher, state.Command, state.Argv)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// SaveProcess called after process start, before wait
|
||||
func SaveProcess(uid string, cmd *exec.Cmd) error {
|
||||
statePath = path.Join(system.V.RunDir, uid, strconv.Itoa(cmd.Process.Pid))
|
||||
state := launcherState{
|
||||
PID: cmd.Process.Pid,
|
||||
Launcher: cmd.Path,
|
||||
Argv: cmd.Args,
|
||||
Command: command,
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
return gob.NewEncoder(f).Encode(state)
|
||||
}
|
||||
}
|
||||
|
||||
func readLaunchers() ([]*launcherState, error) {
|
||||
var f *os.File
|
||||
var r []*launcherState
|
||||
launcherPrefix := path.Join(system.V.RunDir, u.Uid)
|
||||
|
||||
if pl, err := os.ReadDir(launcherPrefix); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, e := range pl {
|
||||
if err = func() error {
|
||||
if f, err = os.Open(path.Join(launcherPrefix, e.Name())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("foreign state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
var s launcherState
|
||||
r = append(r, &s)
|
||||
return gob.NewDecoder(f).Decode(&s)
|
||||
}
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
21
internal/state/value.go
Normal file
21
internal/state/value.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
)
|
||||
|
||||
var (
|
||||
u *user.User
|
||||
uid int
|
||||
command []string
|
||||
)
|
||||
|
||||
func Set(val user.User, c []string, d int) {
|
||||
if u != nil {
|
||||
panic("state set twice")
|
||||
}
|
||||
|
||||
u = &val
|
||||
command = c
|
||||
uid = d
|
||||
}
|
||||
28
internal/system/retrieve.go
Normal file
28
internal/system/retrieve.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Retrieve(verbose bool) {
|
||||
if V != nil {
|
||||
panic("system info retrieved twice")
|
||||
}
|
||||
|
||||
v := &Values{Share: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())), Verbose: verbose}
|
||||
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fmt.Println("Env variable", xdgRuntimeDir, "unset")
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
v.Runtime = r
|
||||
v.RunDir = path.Join(v.Runtime, "fortify")
|
||||
}
|
||||
|
||||
V = v
|
||||
}
|
||||
17
internal/system/value.go
Normal file
17
internal/system/value.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package system
|
||||
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
)
|
||||
|
||||
type Values struct {
|
||||
Share string
|
||||
Runtime string
|
||||
RunDir string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
var (
|
||||
V *Values
|
||||
MethodFlags [2]bool
|
||||
)
|
||||
39
internal/util/simple.go
Normal file
39
internal/util/simple.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Which(file string) (string, bool) {
|
||||
p, err := exec.LookPath(file)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func CopyFile(dst, src string) error {
|
||||
srcD, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if srcD.Close() != nil {
|
||||
// unreachable
|
||||
panic("src file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if dstD.Close() != nil {
|
||||
// unreachable
|
||||
panic("dst file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(dstD, srcD)
|
||||
return err
|
||||
}
|
||||
75
internal/util/std.go
Normal file
75
internal/util/std.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
)
|
||||
|
||||
const (
|
||||
systemdCheckPath = "/run/systemd/system"
|
||||
|
||||
home = "HOME"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
|
||||
PulseServer = "PULSE_SERVER"
|
||||
PulseCookie = "PULSE_COOKIE"
|
||||
)
|
||||
|
||||
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
||||
func SdBooted() bool {
|
||||
_, err := os.Stat(systemdCheckPath)
|
||||
if err != nil {
|
||||
if system.V.Verbose {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("System not booted through systemd")
|
||||
} else {
|
||||
fmt.Println("Error accessing", systemdCheckPath+":", err.Error())
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie
|
||||
func DiscoverPulseCookie() string {
|
||||
if p, ok := os.LookupEnv(PulseCookie); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
state.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
state.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
state.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
PulseCookie, xdgConfigHome, home))
|
||||
return ""
|
||||
}
|
||||
33
internal/xcb/c.go
Normal file
33
internal/xcb/c.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package xcb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <xcb/xcb.h>
|
||||
//#cgo linux LDFLAGS: -lxcb
|
||||
import "C"
|
||||
|
||||
func xcbHandleConnectionError(c *C.xcb_connection_t) error {
|
||||
if errno := C.xcb_connection_has_error(c); errno != 0 {
|
||||
switch errno {
|
||||
case C.XCB_CONN_ERROR:
|
||||
return errors.New("connection error")
|
||||
case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
|
||||
return errors.New("extension not supported")
|
||||
case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
|
||||
return errors.New("memory not available")
|
||||
case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
|
||||
return errors.New("request length exceeded")
|
||||
case C.XCB_CONN_CLOSED_PARSE_ERR:
|
||||
return errors.New("invalid display string")
|
||||
case C.XCB_CONN_CLOSED_INVALID_SCREEN:
|
||||
return errors.New("server has no screen matching display")
|
||||
default:
|
||||
return errors.New("generic X11 failure")
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
47
internal/xcb/export.go
Normal file
47
internal/xcb/export.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package xcb
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <xcb/xcb.h>
|
||||
//#cgo linux LDFLAGS: -lxcb
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
HostModeInsert = C.XCB_HOST_MODE_INSERT
|
||||
HostModeDelete = C.XCB_HOST_MODE_DELETE
|
||||
|
||||
FamilyInternet = C.XCB_FAMILY_INTERNET
|
||||
FamilyDecnet = C.XCB_FAMILY_DECNET
|
||||
FamilyChaos = C.XCB_FAMILY_CHAOS
|
||||
FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
|
||||
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
|
||||
)
|
||||
|
||||
func ChangeHosts(mode, family C.uint8_t, address string) error {
|
||||
var c *C.xcb_connection_t
|
||||
c = C.xcb_connect(nil, nil)
|
||||
defer C.xcb_disconnect(c)
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := C.CString(address)
|
||||
cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
|
||||
C.free(unsafe.Pointer(addr))
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := C.xcb_request_check(c, cookie)
|
||||
if e != nil {
|
||||
defer C.free(unsafe.Pointer(e))
|
||||
return errors.New("xcb_change_hosts() failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user