package xturbo import ( "encoding/binary" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "golang.org/x/sys/unix" ) const ( authMagicCookie = "MIT-MAGIC-COOKIE-1" familyLocal = 256 // FamilyLocal in Xauthority files familyWild = 0xffff // FamilyWild: matches any address socketDir = "/tmp/.X11-unix/X" ) // Dial connects to a local X server named by a DISPLAY string (":N", ":N.S", // "unix:N"; empty means $DISPLAY). It tries the abstract socket // "@/tmp/.X11-unix/XN" first, then the filesystem socket, and authenticates // with MIT-MAGIC-COOKIE-1 from $XAUTHORITY or ~/.Xauthority when a matching // entry exists. Meant for tests and tools; the XWM uses NewConn. func Dial(display string) (*Conn, error) { if display == "" { display = os.Getenv("DISPLAY") } num, err := parseDisplay(display) if err != nil { return nil, err } fd, err := dialUnix(num) if err != nil { return nil, err } var cookie []byte if path := xauthorityPath(); path != "" { host, _ := os.Hostname() cookie, err = readCookie(path, host, num) if err != nil && !errors.Is(err, os.ErrNotExist) { _ = unix.Close(fd) return nil, err } } if cookie == nil { return newConn(fd, "", nil) } return newConn(fd, authMagicCookie, cookie) } // parseDisplay returns the display number of a local DISPLAY string. The // screen suffix is accepted and ignored (Root is always the first screen). func parseDisplay(display string) (int, error) { host, rest, ok := strings.Cut(display, ":") if !ok || (host != "" && host != "unix") || strings.HasPrefix(rest, ":") { return 0, fmt.Errorf("xturbo: unsupported DISPLAY %q (local :N or unix:N only)", display) } numStr, screen, hasScreen := strings.Cut(rest, ".") num, err := strconv.ParseUint(numStr, 10, 31) if err != nil { return 0, fmt.Errorf("xturbo: bad DISPLAY %q: display number", display) } if hasScreen { if _, err := strconv.ParseUint(screen, 10, 31); err != nil { return 0, fmt.Errorf("xturbo: bad DISPLAY %q: screen number", display) } } return int(num), nil } // dialUnix connects a blocking stream socket to display num, abstract // namespace first (as libxcb on Linux), then the filesystem path. func dialUnix(num int) (int, error) { path := socketDir + strconv.Itoa(num) var firstErr error for _, name := range []string{"@" + path, path} { fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) if err != nil { return -1, os.NewSyscallError("socket", err) } for { err = unix.Connect(fd, &unix.SockaddrUnix{Name: name}) if err != unix.EINTR { break } } if err == nil { return fd, nil } _ = unix.Close(fd) if firstErr == nil { firstErr = err } } return -1, fmt.Errorf("xturbo: connect %s: %w", path, firstErr) } func xauthorityPath() string { if p := os.Getenv("XAUTHORITY"); p != "" { return p } if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".Xauthority") } return "" } // readCookie returns the MIT-MAGIC-COOKIE-1 for a local display from an // Xauthority file: the first entry of family local with address host (or // FamilyWild), display number num (or empty, meaning any). It returns nil, // nil when no entry matches. func readCookie(path, host string, num int) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return findCookie(data, host, num) } func findCookie(data []byte, host string, num int) ([]byte, error) { want := strconv.Itoa(num) for len(data) > 0 { if len(data) < 2 { return nil, malformed("Xauthority entry truncated") } family := binary.BigEndian.Uint16(data) data = data[2:] var fields [4][]byte // address, number, name, data for i := range fields { if len(data) < 2 { return nil, fmt.Errorf("%w: Xauthority entry truncated: %w", ErrMalformed, io.ErrUnexpectedEOF) } n := int(binary.BigEndian.Uint16(data)) if len(data) < 2+n { return nil, fmt.Errorf("%w: Xauthority field truncated: %w", ErrMalformed, io.ErrUnexpectedEOF) } fields[i] = data[2 : 2+n] data = data[2+n:] } addr, number, name, cookie := fields[0], fields[1], fields[2], fields[3] hostOK := family == familyWild || (family == familyLocal && string(addr) == host) numOK := len(number) == 0 || string(number) == want if hostOK && numOK && string(name) == authMagicCookie { return append([]byte(nil), cookie...), nil } } return nil, nil }