diff options
| author | Ophestra Umiker <cat@ophivana.moe> | 2024-09-04 01:20:12 +0900 |
|---|---|---|
| committer | Ophestra Umiker <cat@ophivana.moe> | 2024-09-04 01:20:12 +0900 |
| commit | d8f76f3b2594db4687c0203c4f2be8d3e4ef7740 (patch) | |
| tree | b1d8bfb5250d71094e397f8afa87710de1f1ca2f /internal | |
| parent | 7e6eb82195c650bc34829e5cca2d2296e78c0707 (diff) | |
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>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/acl/c.go | 99 | ||||
| -rw-r--r-- | internal/acl/export.go | 86 | ||||
| -rw-r--r-- | internal/app/builder.go | 13 | ||||
| -rw-r--r-- | internal/app/launch.go | 152 | ||||
| -rw-r--r-- | internal/app/setup.go | 124 | ||||
| -rw-r--r-- | internal/state/exit.go | 68 | ||||
| -rw-r--r-- | internal/state/register.go | 12 | ||||
| -rw-r--r-- | internal/state/track.go | 115 | ||||
| -rw-r--r-- | internal/state/value.go | 21 | ||||
| -rw-r--r-- | internal/system/retrieve.go | 28 | ||||
| -rw-r--r-- | internal/system/value.go | 17 | ||||
| -rw-r--r-- | internal/util/simple.go | 39 | ||||
| -rw-r--r-- | internal/util/std.go | 75 | ||||
| -rw-r--r-- | internal/xcb/c.go | 33 | ||||
| -rw-r--r-- | internal/xcb/export.go | 47 |
15 files changed, 929 insertions, 0 deletions
diff --git a/internal/acl/c.go b/internal/acl/c.go new file mode 100644 index 00000000..89bc0469 --- /dev/null +++ b/internal/acl/c.go @@ -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 +} diff --git a/internal/acl/export.go b/internal/acl/export.go new file mode 100644 index 00000000..aeb8ed96 --- /dev/null +++ b/internal/acl/export.go @@ -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) +} diff --git a/internal/app/builder.go b/internal/app/builder.go new file mode 100644 index 00000000..2bce43b4 --- /dev/null +++ b/internal/app/builder.go @@ -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) +} diff --git a/internal/app/launch.go b/internal/app/launch.go new file mode 100644 index 00000000..c6f7b773 --- /dev/null +++ b/internal/app/launch.go @@ -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 +} diff --git a/internal/app/setup.go b/internal/app/setup.go new file mode 100644 index 00000000..7b5cd2ec --- /dev/null +++ b/internal/app/setup.go @@ -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 +} diff --git a/internal/state/exit.go b/internal/state/exit.go new file mode 100644 index 00000000..e0e2c04a --- /dev/null +++ b/internal/state/exit.go @@ -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) + } + } +} diff --git a/internal/state/register.go b/internal/state/register.go new file mode 100644 index 00000000..2fff565e --- /dev/null +++ b/internal/state/register.go @@ -0,0 +1,12 @@ +package state + +func RegisterRevertPath(p string) { + cleanupCandidate = append(cleanupCandidate, p) +} + +func XcbActionComplete() { + if xcbActionComplete { + Fatal("xcb inserted twice") + } + xcbActionComplete = true +} diff --git a/internal/state/track.go b/internal/state/track.go new file mode 100644 index 00000000..913d684d --- /dev/null +++ b/internal/state/track.go @@ -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 +} diff --git a/internal/state/value.go b/internal/state/value.go new file mode 100644 index 00000000..cb20818c --- /dev/null +++ b/internal/state/value.go @@ -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 +} diff --git a/internal/system/retrieve.go b/internal/system/retrieve.go new file mode 100644 index 00000000..d9f0e3dd --- /dev/null +++ b/internal/system/retrieve.go @@ -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 +} diff --git a/internal/system/value.go b/internal/system/value.go new file mode 100644 index 00000000..aa4da518 --- /dev/null +++ b/internal/system/value.go @@ -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 +) diff --git a/internal/util/simple.go b/internal/util/simple.go new file mode 100644 index 00000000..08ce23f6 --- /dev/null +++ b/internal/util/simple.go @@ -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 +} diff --git a/internal/util/std.go b/internal/util/std.go new file mode 100644 index 00000000..5c37a0d7 --- /dev/null +++ b/internal/util/std.go @@ -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 "" +} diff --git a/internal/xcb/c.go b/internal/xcb/c.go new file mode 100644 index 00000000..8fe59d8c --- /dev/null +++ b/internal/xcb/c.go @@ -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 + } +} diff --git a/internal/xcb/export.go b/internal/xcb/export.go new file mode 100644 index 00000000..72a74131 --- /dev/null +++ b/internal/xcb/export.go @@ -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 +} |
