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:
parent
7e6eb82195
commit
d8f76f3b25
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
/ego
|
/fortify
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
83
README.md
83
README.md
@ -1,83 +0,0 @@
|
|||||||
ego (the Go side)
|
|
||||||
=================
|
|
||||||
|
|
||||||
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/ego.svg)](https://pkg.go.dev/git.ophivana.moe/cat/ego)
|
|
||||||
|
|
||||||
> Do all your games need access to your documents, browser history, SSH private keys?
|
|
||||||
>
|
|
||||||
> ... No? Just run `ego steam`!
|
|
||||||
|
|
||||||
**Ego** is a tool to run Linux desktop applications under a different local user. Currently
|
|
||||||
integrates with Wayland, Xorg, PulseAudio and xdg-desktop-portal. You may think of it as `xhost`
|
|
||||||
for Wayland and PulseAudio. This is done using filesystem ACLs and X11 host access control.
|
|
||||||
|
|
||||||
Disclaimer: **DO NOT RUN UNTRUSTED PROGRAMS VIA EGO.** However, using ego is more secure than
|
|
||||||
running applications directly under your primary user.
|
|
||||||
|
|
||||||
Differences
|
|
||||||
-----------
|
|
||||||
* Written in Go
|
|
||||||
* Tracks process states
|
|
||||||
* Cleans up after last process exits
|
|
||||||
* Argv preservation in machinectl mode
|
|
||||||
* Has no dependencies other than the two C libraries
|
|
||||||
|
|
||||||
Manual setup
|
|
||||||
------------
|
|
||||||
Ego aims to come with sane defaults and be easy to set up.
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
* Sudo
|
|
||||||
* A C compiler
|
|
||||||
* [Go](https://go.dev/doc/install)
|
|
||||||
* `libacl.so` library (Debian/Ubuntu: libacl1-dev; Fedora: libacl-devel; Arch: acl)
|
|
||||||
* `libxcb.so` library (Debian/Ubuntu: libxcb1-dev; Fedora: libxcb-devel; Arch: libxcb)
|
|
||||||
|
|
||||||
**Recommended:** (Not needed when using `--sudo` mode, but some desktop functionality may not work).
|
|
||||||
* `machinectl` command (Debian/Ubuntu/Fedora: systemd-container; Arch: systemd)
|
|
||||||
* `xdg-desktop-portal-gtk` (Debian/Ubuntu/Fedora/Arch: xdg-desktop-portal-gtk)
|
|
||||||
|
|
||||||
**Installation:**
|
|
||||||
|
|
||||||
1. Run in repository worktree:
|
|
||||||
|
|
||||||
go build -v -ldflags '-s -w'
|
|
||||||
sudo cp ego /usr/local/bin/
|
|
||||||
|
|
||||||
2. Create local user named "ego": <sup>[1]</sup>
|
|
||||||
|
|
||||||
sudo useradd ego --uid 155 --create-home
|
|
||||||
|
|
||||||
3. That's all, try it:
|
|
||||||
|
|
||||||
ego xdg-open .
|
|
||||||
|
|
||||||
[1] No extra groups are needed by the ego user.
|
|
||||||
UID below 1000 hides this user on the login screen.
|
|
||||||
|
|
||||||
### Avoid password prompt
|
|
||||||
If using "machinectl" mode (default if available), you need the rather new systemd version >=247
|
|
||||||
and polkit >=0.106 to do this securely.
|
|
||||||
|
|
||||||
Create file `/etc/polkit-1/rules.d/50-ego-machinectl.rules`, polkit will automatically load it
|
|
||||||
(replace `$USER` with your own username):
|
|
||||||
|
|
||||||
```js
|
|
||||||
polkit.addRule(function(action, subject) {
|
|
||||||
if (action.id == "org.freedesktop.machine1.host-shell" &&
|
|
||||||
action.lookup("user") == "ego" &&
|
|
||||||
subject.user == "$USER") {
|
|
||||||
return polkit.Result.YES;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
##### sudo mode
|
|
||||||
For sudo, add the following to `/etc/sudoers` (replace `$USER` with your own username):
|
|
||||||
|
|
||||||
$USER ALL=(ego) NOPASSWD:ALL
|
|
||||||
|
|
||||||
Appendix
|
|
||||||
--------
|
|
||||||
Ego is licensed under the MIT License (see the `LICENSE` file).
|
|
||||||
The original Ego was created by Marti Raudsepp under the repository https://github.com/intgr/ego
|
|
41
cli.go
41
cli.go
@ -1,50 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"os"
|
"git.ophivana.moe/cat/fortify/internal/system"
|
||||||
"os/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
userName string
|
userName string
|
||||||
methodFlags [2]bool
|
|
||||||
printVersion bool
|
printVersion bool
|
||||||
mustPulse bool
|
mustPulse bool
|
||||||
|
flagVerbose bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.StringVar(&userName, "u", "ego", "Specify a username")
|
flag.StringVar(&userName, "u", "chronos", "Specify a username")
|
||||||
flag.BoolVar(&methodFlags[0], "sudo", false, "Use 'sudo' to change user")
|
flag.BoolVar(&system.MethodFlags[0], "sudo", false, "Use 'sudo' to change user")
|
||||||
flag.BoolVar(&methodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup")
|
flag.BoolVar(&system.MethodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup")
|
||||||
flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal")
|
flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal")
|
||||||
flag.BoolVar(&verbose, "v", false, "Verbose output")
|
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||||
flag.BoolVar(&printVersion, "V", false, "Print version")
|
flag.BoolVar(&printVersion, "V", false, "Print version")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyArgs() {
|
|
||||||
tryLauncher()
|
|
||||||
tryVersion()
|
|
||||||
tryLicense()
|
|
||||||
|
|
||||||
command = flag.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
ego = u
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("Running as user", ego.Username, "("+ego.Uid+"),", "command:", command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "ego development environment";
|
description = "fortify development environment";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
||||||
@ -30,11 +30,11 @@
|
|||||||
mkShell {
|
mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
(buildGoModule rec {
|
(buildGoModule rec {
|
||||||
pname = "ego";
|
pname = "fortify";
|
||||||
version = "0.0.0-flake";
|
version = "0.0.0-flake";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
vendorHash = null; # we have no dependencies :3
|
vendorHash = null; # we have no Go dependencies :3
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
|
2
go.mod
2
go.mod
@ -1,3 +1,3 @@
|
|||||||
module git.ophivana.moe/cat/ego
|
module git.ophivana.moe/cat/fortify
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -13,88 +13,11 @@ import (
|
|||||||
//#cgo linux LDFLAGS: -lacl
|
//#cgo linux LDFLAGS: -lacl
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
const (
|
|
||||||
aclRead = C.ACL_READ
|
|
||||||
aclWrite = C.ACL_WRITE
|
|
||||||
aclExecute = C.ACL_EXECUTE
|
|
||||||
|
|
||||||
aclTypeDefault = C.ACL_TYPE_DEFAULT
|
|
||||||
aclTypeAccess = C.ACL_TYPE_ACCESS
|
|
||||||
|
|
||||||
aclUndefinedTag = C.ACL_UNDEFINED_TAG
|
|
||||||
aclUserObj = C.ACL_USER_OBJ
|
|
||||||
aclUser = C.ACL_USER
|
|
||||||
aclGroupObj = C.ACL_GROUP_OBJ
|
|
||||||
aclGroup = C.ACL_GROUP
|
|
||||||
aclMask = C.ACL_MASK
|
|
||||||
aclOther = C.ACL_OTHER
|
|
||||||
)
|
|
||||||
|
|
||||||
type acl struct {
|
type acl struct {
|
||||||
val C.acl_t
|
val C.acl_t
|
||||||
freed bool
|
freed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func aclUpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
|
|
||||||
// read acl from file
|
|
||||||
a, err := aclGetFile(path, aclTypeAccess)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// free acl on return if get is successful
|
|
||||||
defer a.free()
|
|
||||||
|
|
||||||
// remove existing entry
|
|
||||||
if err = a.removeEntry(aclUser, 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, aclUser); 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, aclTypeAccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
|
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
|
||||||
p := C.CString(path)
|
p := C.CString(path)
|
||||||
a, err := C.acl_get_file(p, t)
|
a, err := C.acl_get_file(p, t)
|
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
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
@ -10,6 +10,8 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// we unfortunately have to assume there are never races between processes
|
// we unfortunately have to assume there are never races between processes
|
||||||
@ -33,7 +35,7 @@ func init() {
|
|||||||
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
|
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryState() {
|
func Early() {
|
||||||
if !stateActionEarly {
|
if !stateActionEarly {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -52,13 +54,9 @@ func tryState() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRevertPath(p string) {
|
// SaveProcess called after process start, before wait
|
||||||
cleanupCandidate = append(cleanupCandidate, p)
|
func SaveProcess(uid string, cmd *exec.Cmd) error {
|
||||||
}
|
statePath = path.Join(system.V.RunDir, uid, strconv.Itoa(cmd.Process.Pid))
|
||||||
|
|
||||||
// called after process start, before wait
|
|
||||||
func registerProcess(uid string, cmd *exec.Cmd) error {
|
|
||||||
statePath = path.Join(runDir, uid, strconv.Itoa(cmd.Process.Pid))
|
|
||||||
state := launcherState{
|
state := launcherState{
|
||||||
PID: cmd.Process.Pid,
|
PID: cmd.Process.Pid,
|
||||||
Launcher: cmd.Path,
|
Launcher: cmd.Path,
|
||||||
@ -66,7 +64,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error {
|
|||||||
Command: command,
|
Command: command,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Mkdir(path.Join(runDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +84,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error {
|
|||||||
func readLaunchers() ([]*launcherState, error) {
|
func readLaunchers() ([]*launcherState, error) {
|
||||||
var f *os.File
|
var f *os.File
|
||||||
var r []*launcherState
|
var r []*launcherState
|
||||||
launcherPrefix := path.Join(runDir, ego.Uid)
|
launcherPrefix := path.Join(system.V.RunDir, u.Uid)
|
||||||
|
|
||||||
if pl, err := os.ReadDir(launcherPrefix); err != nil {
|
if pl, err := os.ReadDir(launcherPrefix); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -115,48 +113,3 @@ func readLaunchers() ([]*launcherState, error) {
|
|||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func beforeExit() {
|
|
||||||
if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
fmt.Println("Error removing state file:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a, err := readLaunchers(); err != nil {
|
|
||||||
fmt.Println("Error reading active launchers:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
} else if len(a) > 0 {
|
|
||||||
// other launchers are still active
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(a))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("No other launchers active, will clean up")
|
|
||||||
}
|
|
||||||
|
|
||||||
if xcbActionComplete {
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", ego.Username)
|
|
||||||
}
|
|
||||||
if err := changeHosts(xcbHostModeDelete, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
|
|
||||||
fmt.Println("Error removing XHost entry:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, candidate := range cleanupCandidate {
|
|
||||||
if err := aclUpdatePerm(candidate, uid); err != nil {
|
|
||||||
fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
|
|
||||||
}
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", ego.Username, candidate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fatal(msg ...any) {
|
|
||||||
fmt.Println(msg...)
|
|
||||||
beforeExit()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
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
|
||||||
|
}
|
72
launcher.go
72
launcher.go
@ -1,72 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// hidden path for main to act as a launcher
|
|
||||||
egoLauncher = "EGO_LAUNCHER"
|
|
||||||
)
|
|
||||||
|
|
||||||
// hidden launcher path
|
|
||||||
func tryLauncher() {
|
|
||||||
if printVersion {
|
|
||||||
if r, ok := os.LookupEnv(egoLauncher); ok {
|
|
||||||
// egoLauncher variable contains launcher payload
|
|
||||||
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(egoLauncher); err != nil {
|
|
||||||
fmt.Println("Error unsetting launcher payload:", err)
|
|
||||||
// not fatal, do not fail
|
|
||||||
}
|
|
||||||
|
|
||||||
var p string
|
|
||||||
|
|
||||||
if len(argv) > 0 {
|
|
||||||
if p, ok = 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 launcherPayloadEnv() string {
|
|
||||||
r := &bytes.Buffer{}
|
|
||||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
|
||||||
|
|
||||||
if err := gob.NewEncoder(enc).Encode(command); err != nil {
|
|
||||||
fatal("Error encoding launcher payload:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = enc.Close()
|
|
||||||
return egoLauncher + "=" + r.String()
|
|
||||||
}
|
|
338
main.go
338
main.go
@ -6,15 +6,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/acl"
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/app"
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/state"
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/system"
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/util"
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/xcb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "impure"
|
var (
|
||||||
|
Version = "impure"
|
||||||
|
a *app.App
|
||||||
|
)
|
||||||
|
|
||||||
func tryVersion() {
|
func tryVersion() {
|
||||||
if printVersion {
|
if printVersion {
|
||||||
@ -23,25 +30,9 @@ func tryVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
ego *user.User
|
|
||||||
uid int
|
|
||||||
env []string
|
|
||||||
command []string
|
|
||||||
verbose bool
|
|
||||||
runtime string
|
|
||||||
runDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
term = "TERM"
|
term = "TERM"
|
||||||
home = "HOME"
|
|
||||||
sudoAskPass = "SUDO_ASKPASS"
|
|
||||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
|
||||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
|
||||||
display = "DISPLAY"
|
display = "DISPLAY"
|
||||||
pulseServer = "PULSE_SERVER"
|
|
||||||
pulseCookie = "PULSE_COOKIE"
|
|
||||||
|
|
||||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||||
waylandDisplay = "WAYLAND_DISPLAY"
|
waylandDisplay = "WAYLAND_DISPLAY"
|
||||||
@ -49,315 +40,168 @@ const (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
copyArgs()
|
|
||||||
|
|
||||||
if u, err := strconv.Atoi(ego.Uid); err != nil {
|
// launcher payload early exit
|
||||||
// usually unreachable
|
app.Early(printVersion)
|
||||||
panic("ego uid parse")
|
|
||||||
} else {
|
// version/license command early exit
|
||||||
uid = u
|
tryVersion()
|
||||||
|
tryLicense()
|
||||||
|
|
||||||
|
system.Retrieve(flagVerbose)
|
||||||
|
a = app.New(userName, flag.Args())
|
||||||
|
state.Set(*a.User, a.Command(), a.UID())
|
||||||
|
|
||||||
|
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
||||||
|
if err := os.Mkdir(system.V.RunDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||||
|
state.Fatal("Error creating runtime directory:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
// state query command early exit
|
||||||
fatal("Env variable", xdgRuntimeDir, "unset")
|
state.Early()
|
||||||
} else {
|
|
||||||
runtime = r
|
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||||
runDir = path.Join(runtime, "ego")
|
// acl is unnecessary as this directory is world executable
|
||||||
|
if err := os.Mkdir(system.V.Share, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||||
|
state.Fatal("Error creating shared directory:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// state query command
|
// warn about target user home directory ownership
|
||||||
tryState()
|
if stat, err := os.Stat(a.HomeDir); err != nil {
|
||||||
|
if system.V.Verbose {
|
||||||
// Report warning if user home directory does not exist or has wrong ownership
|
|
||||||
if stat, err := os.Stat(ego.HomeDir); err != nil {
|
|
||||||
if verbose {
|
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, fs.ErrPermission):
|
case errors.Is(err, fs.ErrPermission):
|
||||||
fmt.Printf("User %s home directory %s is not accessible", ego.Username, ego.HomeDir)
|
fmt.Printf("User %s home directory %s is not accessible", a.Username, a.HomeDir)
|
||||||
case errors.Is(err, fs.ErrNotExist):
|
case errors.Is(err, fs.ErrNotExist):
|
||||||
fmt.Printf("User %s home directory %s does not exist", ego.Username, ego.HomeDir)
|
fmt.Printf("User %s home directory %s does not exist", a.Username, a.HomeDir)
|
||||||
default:
|
default:
|
||||||
fmt.Printf("Error stat user %s home directory %s: %s", ego.Username, ego.HomeDir, err)
|
fmt.Printf("Error stat user %s home directory %s: %s", a.Username, a.HomeDir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// FreeBSD: not cross-platform
|
// FreeBSD: not cross-platform
|
||||||
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != ego.Uid {
|
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != a.Uid {
|
||||||
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", ego.Username, ego.HomeDir, ego.Uid, u)
|
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", a.Username, a.HomeDir, a.Uid, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add execute perm to runtime dir, e.g. `/run/user/%d`
|
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||||
if s, err := os.Stat(runtime); err != nil {
|
if s, err := os.Stat(system.V.Runtime); err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
fatal("Runtime directory does not exist")
|
state.Fatal("Runtime directory does not exist")
|
||||||
}
|
}
|
||||||
fatal("Error accessing runtime directory:", err)
|
state.Fatal("Error accessing runtime directory:", err)
|
||||||
} else if !s.IsDir() {
|
} else if !s.IsDir() {
|
||||||
fatal(fmt.Sprintf("Path '%s' is not a directory", runtime))
|
state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime))
|
||||||
} else {
|
} else {
|
||||||
if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
|
if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil {
|
||||||
fatal("Error preparing runtime dir:", err)
|
state.Fatal("Error preparing runtime dir:", err)
|
||||||
} else {
|
} else {
|
||||||
registerRevertPath(runtime)
|
state.RegisterRevertPath(system.V.Runtime)
|
||||||
}
|
}
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("Runtime data dir '%s' configured\n", runtime)
|
fmt.Printf("Runtime data dir '%s' configured\n", system.V.Runtime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create runtime dir for Ego itself (e.g. `/run/user/%d/ego`) and make it readable for target
|
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
||||||
if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
||||||
fatal("Error creating Ego runtime dir:", err)
|
|
||||||
}
|
|
||||||
if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil {
|
|
||||||
fatal("Error preparing Ego runtime dir:", err)
|
|
||||||
} else {
|
|
||||||
registerRevertPath(runDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`)
|
|
||||||
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
|
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// add environment variable for new process
|
// add environment variable for new process
|
||||||
env = append(env, waylandDisplay+"="+path.Join(runtime, w))
|
wp := path.Join(system.V.Runtime, w)
|
||||||
wp := path.Join(runtime, w)
|
a.AppendEnv(waylandDisplay, wp)
|
||||||
if err := aclUpdatePerm(wp, uid, aclRead, aclWrite, aclExecute); err != nil {
|
if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
||||||
fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
state.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
||||||
} else {
|
} else {
|
||||||
registerRevertPath(wp)
|
state.RegisterRevertPath(wp)
|
||||||
}
|
}
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("Wayland socket '%s' configured\n", w)
|
fmt.Printf("Wayland socket '%s' configured\n", w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
|
// discovery X11 and grant user permission via the `ChangeHosts` command
|
||||||
if d, ok := os.LookupEnv(display); !ok {
|
if d, ok := os.LookupEnv(display); !ok {
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Println("X11: DISPLAY not set, skipping")
|
fmt.Println("X11: DISPLAY not set, skipping")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// add environment variable for new process
|
// add environment variable for new process
|
||||||
env = append(env, display+"="+d)
|
a.AppendEnv(display, d)
|
||||||
|
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
|
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
|
||||||
}
|
}
|
||||||
if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
|
if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil {
|
||||||
fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
state.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
||||||
} else {
|
} else {
|
||||||
xcbActionComplete = true
|
state.XcbActionComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
|
// ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
|
||||||
pulse := path.Join(runtime, "pulse")
|
pulse := path.Join(system.V.Runtime, "pulse")
|
||||||
pulseS := path.Join(pulse, "native")
|
pulseS := path.Join(pulse, "native")
|
||||||
if s, err := os.Stat(pulse); err != nil {
|
if s, err := os.Stat(pulse); err != nil {
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
fatal("Error accessing PulseAudio directory:", err)
|
state.Fatal("Error accessing PulseAudio directory:", err)
|
||||||
}
|
}
|
||||||
if mustPulse {
|
if mustPulse {
|
||||||
fatal("PulseAudio is unavailable")
|
state.Fatal("PulseAudio is unavailable")
|
||||||
}
|
}
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
|
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// add environment variable for new process
|
// add environment variable for new process
|
||||||
env = append(env, pulseServer+"=unix:"+pulseS)
|
a.AppendEnv(util.PulseServer, "unix:"+pulseS)
|
||||||
if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil {
|
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
|
||||||
fatal("Error preparing PulseAudio:", err)
|
state.Fatal("Error preparing PulseAudio:", err)
|
||||||
} else {
|
} else {
|
||||||
registerRevertPath(pulse)
|
state.RegisterRevertPath(pulse)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native`
|
// ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
||||||
if s, err = os.Stat(pulseS); err != nil {
|
if s, err = os.Stat(pulseS); err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
fatal("PulseAudio directory found but socket does not exist")
|
state.Fatal("PulseAudio directory found but socket does not exist")
|
||||||
}
|
}
|
||||||
fatal("Error accessing PulseAudio socket:", err)
|
state.Fatal("Error accessing PulseAudio socket:", err)
|
||||||
} else {
|
} else {
|
||||||
if m := s.Mode(); m&0o006 != 0o006 {
|
if m := s.Mode(); m&0o006 != 0o006 {
|
||||||
fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish current user's pulse-cookie for target user
|
// Publish current user's pulse-cookie for target user
|
||||||
pulseCookieSource := discoverPulseCookie()
|
pulseCookieSource := util.DiscoverPulseCookie()
|
||||||
env = append(env, pulseCookie+"="+pulseCookieSource)
|
pulseCookieFinal := path.Join(system.V.Share, "pulse-cookie")
|
||||||
pulseCookieFinal := path.Join(runDir, "pulse-cookie")
|
a.AppendEnv(util.PulseCookie, pulseCookieFinal)
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
||||||
}
|
}
|
||||||
if err = copyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
||||||
fatal("Error copying PulseAudio cookie:", err)
|
state.Fatal("Error copying PulseAudio cookie:", err)
|
||||||
}
|
}
|
||||||
if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
|
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
|
||||||
fatal("Error publishing PulseAudio cookie:", err)
|
state.Fatal("Error publishing PulseAudio cookie:", err)
|
||||||
} else {
|
} else {
|
||||||
registerRevertPath(pulseCookieFinal)
|
state.RegisterRevertPath(pulseCookieFinal)
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbose {
|
if system.V.Verbose {
|
||||||
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
|
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass $TERM to launcher
|
// pass $TERM to launcher
|
||||||
if t, ok := os.LookupEnv(term); ok {
|
if t, ok := os.LookupEnv(term); ok {
|
||||||
env = append(env, term+"="+t)
|
a.AppendEnv(term, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := launchBySudo
|
a.Run()
|
||||||
m, b := false, false
|
|
||||||
switch {
|
|
||||||
case methodFlags[0]: // sudo
|
|
||||||
case 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 !sdBooted() {
|
|
||||||
fmt.Println("This system was not booted through systemd")
|
|
||||||
fmt.Println(sudoFallback)
|
|
||||||
} else if tp, ok := which("machinectl"); !ok {
|
|
||||||
fmt.Println("Did not find 'machinectl' in PATH")
|
|
||||||
fmt.Println(sudoFallback)
|
|
||||||
} else {
|
|
||||||
toolPath = tp
|
|
||||||
f = func() []string { return launchByMachineCtl(b) }
|
|
||||||
}
|
|
||||||
} else if tp, ok := which("sudo"); !ok {
|
|
||||||
fatal("Did not find 'sudo' in PATH")
|
|
||||||
} else {
|
|
||||||
toolPath = tp
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(toolPath, f()...)
|
|
||||||
cmd.Env = env
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmd.Dir = runDir
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("Executing:", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
fatal("Error starting process:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := registerProcess(ego.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) {
|
|
||||||
fatal("Error running process:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("Process exited with exit code", r)
|
|
||||||
}
|
|
||||||
beforeExit()
|
|
||||||
os.Exit(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchBySudo() (args []string) {
|
|
||||||
args = make([]string, 0, 4+len(env)+len(command))
|
|
||||||
|
|
||||||
// -Hiu $USER
|
|
||||||
args = append(args, "-Hiu", ego.Username)
|
|
||||||
|
|
||||||
// -A?
|
|
||||||
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
|
||||||
}
|
|
||||||
args = append(args, "-A")
|
|
||||||
}
|
|
||||||
|
|
||||||
// environ
|
|
||||||
args = append(args, env...)
|
|
||||||
|
|
||||||
// -- $@
|
|
||||||
args = append(args, "--")
|
|
||||||
args = append(args, command...)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchByMachineCtl(bare bool) (args []string) {
|
|
||||||
args = make([]string, 0, 9+len(env))
|
|
||||||
|
|
||||||
// shell --uid=$USER
|
|
||||||
args = append(args, "shell", "--uid="+ego.Username)
|
|
||||||
|
|
||||||
// --quiet
|
|
||||||
if !verbose {
|
|
||||||
args = append(args, "--quiet")
|
|
||||||
}
|
|
||||||
|
|
||||||
// environ
|
|
||||||
envQ := make([]string, len(env)+1)
|
|
||||||
for i, e := range env {
|
|
||||||
envQ[i] = "-E" + e
|
|
||||||
}
|
|
||||||
envQ[len(env)] = "-E" + launcherPayloadEnv()
|
|
||||||
args = append(args, envQ...)
|
|
||||||
|
|
||||||
// -- .host
|
|
||||||
args = append(args, "--", ".host")
|
|
||||||
|
|
||||||
// /bin/sh -c
|
|
||||||
if sh, ok := which("sh"); !ok {
|
|
||||||
fatal("Did not find 'sh' in PATH")
|
|
||||||
} else {
|
|
||||||
args = append(args, sh, "-c")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(command) == 0 { // execute shell if command is not provided
|
|
||||||
command = []string{"$SHELL"}
|
|
||||||
}
|
|
||||||
|
|
||||||
innerCommand := strings.Builder{}
|
|
||||||
|
|
||||||
if !bare {
|
|
||||||
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
|
||||||
for _, e := range env {
|
|
||||||
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
|
||||||
}
|
|
||||||
innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if executable, err := os.Executable(); err != nil {
|
|
||||||
fatal("Error reading executable path:", err)
|
|
||||||
} else {
|
|
||||||
innerCommand.WriteString("exec " + executable + " -V")
|
|
||||||
}
|
|
||||||
args = append(args, innerCommand.String())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
100
util.go
100
util.go
@ -1,100 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
systemdCheckPath = "/run/systemd/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
|
||||||
func sdBooted() bool {
|
|
||||||
_, err := os.Stat(systemdCheckPath)
|
|
||||||
if err != nil {
|
|
||||||
if 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try various ways 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) {
|
|
||||||
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) {
|
|
||||||
fatal("Error accessing PulseAudio cookie:", err)
|
|
||||||
// unreachable
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
} else if !s.IsDir() {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
|
||||||
pulseCookie, xdgConfigHome, home))
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
72
x11.go
72
x11.go
@ -1,72 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
//#include <stdlib.h>
|
|
||||||
//#include <xcb/xcb.h>
|
|
||||||
//#cgo linux LDFLAGS: -lxcb
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
const (
|
|
||||||
xcbHostModeInsert = C.XCB_HOST_MODE_INSERT
|
|
||||||
xcbHostModeDelete = C.XCB_HOST_MODE_DELETE
|
|
||||||
|
|
||||||
xcbFamilyInternet = C.XCB_FAMILY_INTERNET
|
|
||||||
xcbFamilyDecnet = C.XCB_FAMILY_DECNET
|
|
||||||
xcbFamilyChaos = C.XCB_FAMILY_CHAOS
|
|
||||||
xcbFamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
|
|
||||||
xcbFamilyInternet6 = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user