diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 625de0d..62002ac 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -8,7 +8,23 @@ on: jobs: release: runs-on: ubuntu-latest + container: + image: node:16-bookworm-slim steps: + - name: Get dependencies + run: >- + echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list && + apt-get update && + apt-get install -y + git + gcc + pkg-config + libwayland-dev + wayland-protocols/bookworm-backports + libxcb1-dev + libacl1-dev + if: ${{ runner.os == 'Linux' }} + - name: Checkout uses: actions/checkout@v4 with: @@ -19,14 +35,9 @@ jobs: with: go-version: '>=1.23.0' - - name: Get dependencies + - name: Go generate run: >- - apt-get update && - apt-get install -y - gcc - pkg-config - libacl1-dev - if: ${{ runner.os == 'Linux' }} + go generate ./... - name: Build for Linux run: >- diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index f04ddf3..ceb84ff 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -7,7 +7,23 @@ on: jobs: test: runs-on: ubuntu-latest + container: + image: node:16-bookworm-slim steps: + - name: Get dependencies + run: >- + echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list && + apt-get update && + apt-get install -y + git + gcc + pkg-config + libwayland-dev + wayland-protocols/bookworm-backports + libxcb1-dev + libacl1-dev + if: ${{ runner.os == 'Linux' }} + - name: Checkout uses: actions/checkout@v4 with: @@ -18,14 +34,9 @@ jobs: with: go-version: '>=1.23.0' - - name: Get dependencies + - name: Go generate run: >- - apt-get update && - apt-get install -y - gcc - pkg-config - libacl1-dev - if: ${{ runner.os == 'Linux' }} + go generate ./... - name: Run tests run: >- diff --git a/.gitignore b/.gitignore index 4b84d41..d374fc7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ go.work.sum # env file .env .idea -.vscode \ No newline at end of file +.vscode + +# go generate +security-context-v1-protocol.* \ No newline at end of file diff --git a/package.nix b/package.nix index 5e7a59a..af6340a 100644 --- a/package.nix +++ b/package.nix @@ -4,7 +4,11 @@ makeBinaryWrapper, xdg-dbus-proxy, bubblewrap, + pkg-config, acl, + wayland, + wayland-scanner, + wayland-protocols, xorg, }: @@ -41,10 +45,20 @@ buildGoModule rec { buildInputs = [ acl + wayland + wayland-protocols xorg.libxcb ]; - nativeBuildInputs = [ makeBinaryWrapper ]; + nativeBuildInputs = [ + pkg-config + wayland-scanner + makeBinaryWrapper + ]; + + preConfigure = '' + HOME=$(mktemp -d) go generate ./... + ''; postInstall = '' install -D --target-directory=$out/share/zsh/site-functions comp/* diff --git a/wl/c.go b/wl/c.go new file mode 100644 index 0000000..907693b --- /dev/null +++ b/wl/c.go @@ -0,0 +1,111 @@ +package wl + +//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h" +//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c" + +/* +#cgo linux pkg-config: wayland-client +#cgo freebsd openbsd LDFLAGS: -lwayland-client + +#include +#include +#include + +#include +#include +#include + +#include +#include "security-context-v1-protocol.h" + +static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + struct wp_security_context_manager_v1 **out = data; + + if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0) + *out = wl_registry_bind(registry, name, &wp_security_context_manager_v1_interface, 1); +} + +static void registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { } // no-op + +static const struct wl_registry_listener registry_listener = { + .global = registry_handle_global, + .global_remove = registry_handle_global_remove, +}; + +static int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) { + int32_t res = 0; // refer to resErr for meaning + + struct wl_display *display; + display = wl_display_connect_to_fd(fd); + if (!display) { + res = 1; + goto out; + }; + + struct wl_registry *registry; + registry = wl_display_get_registry(display); + + struct wp_security_context_manager_v1 *security_context_manager = NULL; + wl_registry_add_listener(registry, ®istry_listener, &security_context_manager); + int ret; + ret = wl_display_roundtrip(display); + wl_registry_destroy(registry); + if (ret < 0) + goto out; + + if (!security_context_manager) { + res = 2; + goto out; + } + + int listen_fd = -1; + listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listen_fd < 0) + goto out; + + struct sockaddr_un sockaddr = {0}; + sockaddr.sun_family = AF_UNIX; + snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path); + if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) + goto out; + + if (listen(listen_fd, 0) != 0) + goto out; + + struct wp_security_context_v1 *security_context; + security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, sync_fd); + wp_security_context_v1_set_sandbox_engine(security_context, "moe.ophivana.fortify"); + wp_security_context_v1_set_app_id(security_context, app_id); + wp_security_context_v1_set_instance_id(security_context, instance_id); + wp_security_context_v1_commit(security_context); + wp_security_context_v1_destroy(security_context); + if (wl_display_roundtrip(display) < 0) + goto out; + +out: + if (listen_fd >= 0) + close(listen_fd); + if (security_context_manager) + wp_security_context_manager_v1_destroy(security_context_manager); + if (display) + wl_display_disconnect(display); + + free((void *)socket_path); + free((void *)app_id); + free((void *)instance_id); + return res; +} +*/ +import "C" +import "errors" + +var resErr = [...]error{ + 0: nil, + 1: errors.New("wl_display_connect_to_fd() failed"), + 2: errors.New("wp_security_context_v1 not available"), +} + +func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error { + res := C.bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD)) + return resErr[int32(res)] +} diff --git a/wl/conn.go b/wl/conn.go new file mode 100644 index 0000000..a04845c --- /dev/null +++ b/wl/conn.go @@ -0,0 +1,119 @@ +package wl + +import ( + "errors" + "net" + "os" + "runtime" + "sync" + "syscall" +) + +type Conn struct { + conn *net.UnixConn + + done chan struct{} + doneOnce sync.Once + + mu sync.Mutex +} + +// Attach connects Conn to a wayland socket. +func (c *Conn) Attach(p string) (err error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn != nil { + return errors.New("attached") + } + + c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"}) + return +} + +// Close releases resources and closes the connection to the wayland compositor. +func (c *Conn) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.done == nil { + return errors.New("no socket bound") + } + + c.doneOnce.Do(func() { + c.done <- struct{}{} + <-c.done + }) + + // closed by wayland + runtime.SetFinalizer(c.conn, nil) + return nil +} + +func (c *Conn) Bind(p, appID, instanceID string) (*os.File, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn == nil { + return nil, errors.New("not attached") + } + if c.done != nil { + return nil, errors.New("bound") + } + + if rc, err := c.conn.SyscallConn(); err != nil { + // unreachable + return nil, err + } else { + c.done = make(chan struct{}) + return bindRawConn(c.done, rc, p, appID, instanceID) + } +} + +func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID string) (*os.File, error) { + var syncPipe [2]*os.File + + if r, w, err := os.Pipe(); err != nil { + return nil, err + } else { + syncPipe[0] = r + syncPipe[1] = w + } + + setupDone := make(chan error, 1) // does not block with c.done + + go func() { + if err := rc.Control(func(fd uintptr) { + // prevent runtime from closing the read end of sync fd + runtime.SetFinalizer(syncPipe[0], nil) + + // allow the Bind method to return after setup + setupDone <- bind(fd, p, appID, instanceID, syncPipe[0].Fd()) + close(setupDone) + + // keep socket alive until done is requested + <-done + }); err != nil { + setupDone <- err + } + + // notify Close that rc.Control has returned + close(done) + }() + + // return write end of the pipe + return syncPipe[1], <-setupDone +} + +func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error { + // ensure p is available + if f, err := os.Create(p); err != nil { + return err + } else if err = f.Close(); err != nil { + return err + } else if err = os.Remove(p); err != nil { + return err + } + + return bindWaylandFd(p, fd, appID, instanceID, syncFD) +}