1package xturbo
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12
13 "golang.org/x/sys/unix"
14)
15
16const (
17 authMagicCookie = "MIT-MAGIC-COOKIE-1"
18 familyLocal = 256 // FamilyLocal in Xauthority files
19 familyWild = 0xffff // FamilyWild: matches any address
20 socketDir = "/tmp/.X11-unix/X"
21)
22
23// Dial connects to a local X server named by a DISPLAY string (":N", ":N.S",
24// "unix:N"; empty means $DISPLAY). It tries the abstract socket
25// "@/tmp/.X11-unix/XN" first, then the filesystem socket, and authenticates
26// with MIT-MAGIC-COOKIE-1 from $XAUTHORITY or ~/.Xauthority when a matching
27// entry exists. Meant for tests and tools; the XWM uses NewConn.
28func Dial(display string) (*Conn, error) {
29 if display == "" {
30 display = os.Getenv("DISPLAY")
31 }
32 num, err := parseDisplay(display)
33 if err != nil {
34 return nil, err
35 }
36 fd, err := dialUnix(num)
37 if err != nil {
38 return nil, err
39 }
40 var cookie []byte
41 if path := xauthorityPath(); path != "" {
42 host, _ := os.Hostname()
43 cookie, err = readCookie(path, host, num)
44 if err != nil && !errors.Is(err, os.ErrNotExist) {
45 _ = unix.Close(fd)
46 return nil, err
47 }
48 }
49 if cookie == nil {
50 return newConn(fd, "", nil)
51 }
52 return newConn(fd, authMagicCookie, cookie)
53}
54
55// parseDisplay returns the display number of a local DISPLAY string. The
56// screen suffix is accepted and ignored (Root is always the first screen).
57func parseDisplay(display string) (int, error) {
58 host, rest, ok := strings.Cut(display, ":")
59 if !ok || (host != "" && host != "unix") || strings.HasPrefix(rest, ":") {
60 return 0, fmt.Errorf("xturbo: unsupported DISPLAY %q (local :N or unix:N only)", display)
61 }
62 numStr, screen, hasScreen := strings.Cut(rest, ".")
63 num, err := strconv.ParseUint(numStr, 10, 31)
64 if err != nil {
65 return 0, fmt.Errorf("xturbo: bad DISPLAY %q: display number", display)
66 }
67 if hasScreen {
68 if _, err := strconv.ParseUint(screen, 10, 31); err != nil {
69 return 0, fmt.Errorf("xturbo: bad DISPLAY %q: screen number", display)
70 }
71 }
72 return int(num), nil
73}
74
75// dialUnix connects a blocking stream socket to display num, abstract
76// namespace first (as libxcb on Linux), then the filesystem path.
77func dialUnix(num int) (int, error) {
78 path := socketDir + strconv.Itoa(num)
79 var firstErr error
80 for _, name := range []string{"@" + path, path} {
81 fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
82 if err != nil {
83 return -1, os.NewSyscallError("socket", err)
84 }
85 for {
86 err = unix.Connect(fd, &unix.SockaddrUnix{Name: name})
87 if err != unix.EINTR {
88 break
89 }
90 }
91 if err == nil {
92 return fd, nil
93 }
94 _ = unix.Close(fd)
95 if firstErr == nil {
96 firstErr = err
97 }
98 }
99 return -1, fmt.Errorf("xturbo: connect %s: %w", path, firstErr)
100}
101
102func xauthorityPath() string {
103 if p := os.Getenv("XAUTHORITY"); p != "" {
104 return p
105 }
106 if home, err := os.UserHomeDir(); err == nil {
107 return filepath.Join(home, ".Xauthority")
108 }
109 return ""
110}
111
112// readCookie returns the MIT-MAGIC-COOKIE-1 for a local display from an
113// Xauthority file: the first entry of family local with address host (or
114// FamilyWild), display number num (or empty, meaning any). It returns nil,
115// nil when no entry matches.
116func readCookie(path, host string, num int) ([]byte, error) {
117 data, err := os.ReadFile(path)
118 if err != nil {
119 return nil, err
120 }
121 return findCookie(data, host, num)
122}
123
124func findCookie(data []byte, host string, num int) ([]byte, error) {
125 want := strconv.Itoa(num)
126 for len(data) > 0 {
127 if len(data) < 2 {
128 return nil, malformed("Xauthority entry truncated")
129 }
130 family := binary.BigEndian.Uint16(data)
131 data = data[2:]
132 var fields [4][]byte // address, number, name, data
133 for i := range fields {
134 if len(data) < 2 {
135 return nil, fmt.Errorf("%w: Xauthority entry truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
136 }
137 n := int(binary.BigEndian.Uint16(data))
138 if len(data) < 2+n {
139 return nil, fmt.Errorf("%w: Xauthority field truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
140 }
141 fields[i] = data[2 : 2+n]
142 data = data[2+n:]
143 }
144 addr, number, name, cookie := fields[0], fields[1], fields[2], fields[3]
145 hostOK := family == familyWild || (family == familyLocal && string(addr) == host)
146 numOK := len(number) == 0 || string(number) == want
147 if hostOK && numOK && string(name) == authMagicCookie {
148 return append([]byte(nil), cookie...), nil
149 }
150 }
151 return nil, nil
152}