feat(runtime): add Conn setup handshake, Dial and Xauthority cookies
NewConn takes ownership of a connected stream socket, sends a little-endian protocol 11.0 setup request, reads the full setup reply while blocking and then switches the fd to O_NONBLOCK. Root keeps the reply (SetupBytes), the first screen root and the resource-id base/mask, parsing the fixed fields itself without importing xproto. Failed (0) and Authenticate (2) replies become *SetupError wrapping ErrSetupFailed; truncated or inconsistent replies wrap ErrMalformed. The fd is closed when setup fails. Dial accepts local DISPLAY strings (:N, :N.S, unix:N), connects to the abstract socket and then /tmp/.X11-unix/XN, and sends MIT-MAGIC-COOKIE-1 from $XAUTHORITY or ~/.Xauthority (FamilyLocal + hostname or FamilyWild, matching or empty display number). Conn also exposes Close, Fd, Buf, Ext (empty ExtTable bound to Buf.Ext) and Flush. Tests drive socketpair servers with recorded Xvfb setup replies, table-test Xauthority parsing, and under the xserver tag dial a cookie-protected Xvfb and round-trip GetInputFocus.
- commit
- 106b42bd82362ea4f87c88c8fbea10d865c35d86
- author
- brice · · 2026-10-22 08:17 UTC
- committer
- brice · · 2026-10-22 08:47 UTC
- parent
- 722264a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +1189 -0
A conn.go +100 -0
| @@ -0,0 +1,100 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | ||
| 6 | + | "golang.org/x/sys/unix" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Conn is a client connection to an X server over a stream socket. It is not | |
| 10 | + | // safe for concurrent use: one goroutine owns it. | |
| 11 | + | type Conn struct { | |
| 12 | + | fd int | |
| 13 | + | buf *Buf | |
| 14 | + | ext ExtTable | |
| 15 | + | setup []byte | |
| 16 | + | root uint32 | |
| 17 | + | ||
| 18 | + | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // NewConn takes ownership of fd, a connected stream socket to an X server, | |
| 22 | + | // and runs the connection setup without authorization (e.g. the -wm fd of an | |
| 23 | + | // XWayland the caller spawned). The handshake blocks; afterwards fd is | |
| 24 | + | // switched to O_NONBLOCK. On failure fd is closed. | |
| 25 | + | func NewConn(fd int) (*Conn, error) { | |
| 26 | + | return newConn(fd, "", nil) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func newConn(fd int, authName string, authData []byte) (*Conn, error) { | |
| 30 | + | c, err := setupConn(fd, authName, authData) | |
| 31 | + | if err != nil { | |
| 32 | + | _ = unix.Close(fd) | |
| 33 | + | return nil, err | |
| 34 | + | } | |
| 35 | + | return c, nil | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func setupConn(fd int, authName string, authData []byte) (*Conn, error) { | |
| 39 | + | if err := unix.SetNonblock(fd, false); err != nil { | |
| 40 | + | return nil, os.NewSyscallError("fcntl", err) | |
| 41 | + | } | |
| 42 | + | setup, info, err := handshake(fd, authName, authData) | |
| 43 | + | if err != nil { | |
| 44 | + | return nil, err | |
| 45 | + | } | |
| 46 | + | if err := unix.SetNonblock(fd, true); err != nil { | |
| 47 | + | return nil, os.NewSyscallError("fcntl", err) | |
| 48 | + | } | |
| 49 | + | c := &Conn{ | |
| 50 | + | fd: fd, | |
| 51 | + | buf: NewBuf(0), | |
| 52 | + | setup: setup, | |
| 53 | + | root: info.root, | |
| 54 | + | xidBase: info.xidBase, | |
| 55 | + | xidMask: info.xidMask, | |
| 56 | + | } | |
| 57 | + | c.buf.Ext = &c.ext | |
| 58 | + | return c, nil | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Close closes the socket. Later calls, and Flush after Close, return | |
| 62 | + | // ErrClosed. | |
| 63 | + | func (c *Conn) Close() error { | |
| 64 | + | if c.fd < 0 { | |
| 65 | + | return ErrClosed | |
| 66 | + | } | |
| 67 | + | fd := c.fd | |
| 68 | + | c.fd = -1 | |
| 69 | + | c.buf.Fail(ErrClosed) | |
| 70 | + | if err := unix.Close(fd); err != nil { | |
| 71 | + | return os.NewSyscallError("close", err) | |
| 72 | + | } | |
| 73 | + | return nil | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // Fd returns the socket, for the caller's poll loop; -1 after Close. | |
| 77 | + | func (c *Conn) Fd() int { return c.fd } | |
| 78 | + | ||
| 79 | + | // Buf returns the request encoder bound to this connection. | |
| 80 | + | func (c *Conn) Buf() *Buf { return c.buf } | |
| 81 | + | ||
| 82 | + | // Ext returns the connection's extension table. | |
| 83 | + | func (c *Conn) Ext() *ExtTable { return &c.ext } | |
| 84 | + | ||
| 85 | + | // SetupBytes returns the full setup reply. It is retained for the life of the | |
| 86 | + | // Conn; parse it with the xproto Setup view. Do not modify it. | |
| 87 | + | func (c *Conn) SetupBytes() []byte { return c.setup } | |
| 88 | + | ||
| 89 | + | // Root returns the root window of the first screen. | |
| 90 | + | func (c *Conn) Root() uint32 { return c.root } | |
| 91 | + | ||
| 92 | + | // Flush writes queued requests without blocking. It returns ErrWouldBlock when | |
| 93 | + | // the socket is full (arm writable interest and call again) and the sticky | |
| 94 | + | // Buf error once the connection is unusable. | |
| 95 | + | func (c *Conn) Flush() error { | |
| 96 | + | if c.fd < 0 { | |
| 97 | + | return ErrClosed | |
| 98 | + | } | |
| 99 | + | return c.buf.flush(c.fd) | |
| 100 | + | } | |
| @@ -0,0 +1,100 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | ||||
| 6 | + | "golang.org/x/sys/unix" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Conn is a client connection to an X server over a stream socket. It is not | |||
| 10 | + | // safe for concurrent use: one goroutine owns it. | |||
| 11 | + | type Conn struct { | |||
| 12 | + | fd int | |||
| 13 | + | buf *Buf | |||
| 14 | + | ext ExtTable | |||
| 15 | + | setup []byte | |||
| 16 | + | root uint32 | |||
| 17 | + | ||||
| 18 | + | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // NewConn takes ownership of fd, a connected stream socket to an X server, | |||
| 22 | + | // and runs the connection setup without authorization (e.g. the -wm fd of an | |||
| 23 | + | // XWayland the caller spawned). The handshake blocks; afterwards fd is | |||
| 24 | + | // switched to O_NONBLOCK. On failure fd is closed. | |||
| 25 | + | func NewConn(fd int) (*Conn, error) { | |||
| 26 | + | return newConn(fd, "", nil) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func newConn(fd int, authName string, authData []byte) (*Conn, error) { | |||
| 30 | + | c, err := setupConn(fd, authName, authData) | |||
| 31 | + | if err != nil { | |||
| 32 | + | _ = unix.Close(fd) | |||
| 33 | + | return nil, err | |||
| 34 | + | } | |||
| 35 | + | return c, nil | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func setupConn(fd int, authName string, authData []byte) (*Conn, error) { | |||
| 39 | + | if err := unix.SetNonblock(fd, false); err != nil { | |||
| 40 | + | return nil, os.NewSyscallError("fcntl", err) | |||
| 41 | + | } | |||
| 42 | + | setup, info, err := handshake(fd, authName, authData) | |||
| 43 | + | if err != nil { | |||
| 44 | + | return nil, err | |||
| 45 | + | } | |||
| 46 | + | if err := unix.SetNonblock(fd, true); err != nil { | |||
| 47 | + | return nil, os.NewSyscallError("fcntl", err) | |||
| 48 | + | } | |||
| 49 | + | c := &Conn{ | |||
| 50 | + | fd: fd, | |||
| 51 | + | buf: NewBuf(0), | |||
| 52 | + | setup: setup, | |||
| 53 | + | root: info.root, | |||
| 54 | + | xidBase: info.xidBase, | |||
| 55 | + | xidMask: info.xidMask, | |||
| 56 | + | } | |||
| 57 | + | c.buf.Ext = &c.ext | |||
| 58 | + | return c, nil | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Close closes the socket. Later calls, and Flush after Close, return | |||
| 62 | + | // ErrClosed. | |||
| 63 | + | func (c *Conn) Close() error { | |||
| 64 | + | if c.fd < 0 { | |||
| 65 | + | return ErrClosed | |||
| 66 | + | } | |||
| 67 | + | fd := c.fd | |||
| 68 | + | c.fd = -1 | |||
| 69 | + | c.buf.Fail(ErrClosed) | |||
| 70 | + | if err := unix.Close(fd); err != nil { | |||
| 71 | + | return os.NewSyscallError("close", err) | |||
| 72 | + | } | |||
| 73 | + | return nil | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // Fd returns the socket, for the caller's poll loop; -1 after Close. | |||
| 77 | + | func (c *Conn) Fd() int { return c.fd } | |||
| 78 | + | ||||
| 79 | + | // Buf returns the request encoder bound to this connection. | |||
| 80 | + | func (c *Conn) Buf() *Buf { return c.buf } | |||
| 81 | + | ||||
| 82 | + | // Ext returns the connection's extension table. | |||
| 83 | + | func (c *Conn) Ext() *ExtTable { return &c.ext } | |||
| 84 | + | ||||
| 85 | + | // SetupBytes returns the full setup reply. It is retained for the life of the | |||
| 86 | + | // Conn; parse it with the xproto Setup view. Do not modify it. | |||
| 87 | + | func (c *Conn) SetupBytes() []byte { return c.setup } | |||
| 88 | + | ||||
| 89 | + | // Root returns the root window of the first screen. | |||
| 90 | + | func (c *Conn) Root() uint32 { return c.root } | |||
| 91 | + | ||||
| 92 | + | // Flush writes queued requests without blocking. It returns ErrWouldBlock when | |||
| 93 | + | // the socket is full (arm writable interest and call again) and the sticky | |||
| 94 | + | // Buf error once the connection is unusable. | |||
| 95 | + | func (c *Conn) Flush() error { | |||
| 96 | + | if c.fd < 0 { | |||
| 97 | + | return ErrClosed | |||
| 98 | + | } | |||
| 99 | + | return c.buf.flush(c.fd) | |||
| 100 | + | } | |||
A conn_test.go +244 -0
| @@ -0,0 +1,244 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "errors" | |
| 7 | + | "io" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "testing" | |
| 11 | + | ||
| 12 | + | "bnema.dev/xturbo/internal/xtest" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func loadSetup(t testing.TB, name string) []byte { | |
| 17 | + | t.Helper() | |
| 18 | + | b, err := xtest.LoadHex(filepath.Join("testdata", "setup", name+".setup.hex")) | |
| 19 | + | if err != nil { | |
| 20 | + | t.Fatal(err) | |
| 21 | + | } | |
| 22 | + | return b | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // scriptedServer runs the server side of a socketpair: it reads one setup | |
| 26 | + | // request, reports it on the returned channel, writes reply in chunks of | |
| 27 | + | // chunk bytes (0 = all at once) and, if hangup, closes its end. | |
| 28 | + | func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) { | |
| 29 | + | t.Helper() | |
| 30 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | // Exercise NewConn switching a non-blocking fd back to blocking. | |
| 35 | + | if err := unix.SetNonblock(fds[0], true); err != nil { | |
| 36 | + | t.Fatal(err) | |
| 37 | + | } | |
| 38 | + | server := os.NewFile(uintptr(fds[1]), "server") | |
| 39 | + | ch := make(chan []byte, 1) | |
| 40 | + | done := make(chan struct{}) | |
| 41 | + | t.Cleanup(func() { <-done; server.Close() }) | |
| 42 | + | go func() { | |
| 43 | + | defer close(done) | |
| 44 | + | var hdr [12]byte | |
| 45 | + | if _, err := io.ReadFull(server, hdr[:]); err != nil { | |
| 46 | + | ch <- nil | |
| 47 | + | return | |
| 48 | + | } | |
| 49 | + | n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:]))) | |
| 50 | + | rest := make([]byte, n) | |
| 51 | + | if _, err := io.ReadFull(server, rest); err != nil { | |
| 52 | + | ch <- nil | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | ch <- append(hdr[:], rest...) | |
| 56 | + | for p := reply; len(p) > 0; { | |
| 57 | + | k := len(p) | |
| 58 | + | if chunk > 0 { | |
| 59 | + | k = min(k, chunk) | |
| 60 | + | } | |
| 61 | + | if _, err := server.Write(p[:k]); err != nil { | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | p = p[k:] | |
| 65 | + | } | |
| 66 | + | if hangup { | |
| 67 | + | server.Close() | |
| 68 | + | } | |
| 69 | + | }() | |
| 70 | + | return fds[0], ch | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func TestNewConnSuccess(t *testing.T) { | |
| 74 | + | setup := loadSetup(t, "Success") | |
| 75 | + | fd, req := scriptedServer(t, setup, 7, false) | |
| 76 | + | c, err := NewConn(fd) | |
| 77 | + | if err != nil { | |
| 78 | + | t.Fatal(err) | |
| 79 | + | } | |
| 80 | + | defer c.Close() | |
| 81 | + | got := <-req | |
| 82 | + | want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0} | |
| 83 | + | if !bytes.Equal(got, want) { | |
| 84 | + | t.Fatalf("setup request %x, want %x", got, want) | |
| 85 | + | } | |
| 86 | + | if !bytes.Equal(c.SetupBytes(), setup) { | |
| 87 | + | t.Fatal("SetupBytes differs from the reply") | |
| 88 | + | } | |
| 89 | + | // Expected values are read from the fixture header comment | |
| 90 | + | // (Success.setup.hex), i.e. what that recorded Xvfb assigned. | |
| 91 | + | if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff { | |
| 92 | + | t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask) | |
| 93 | + | } | |
| 94 | + | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 { | |
| 95 | + | t.Fatal("accessors") | |
| 96 | + | } | |
| 97 | + | if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present { | |
| 98 | + | t.Fatal("ExtTable not empty") | |
| 99 | + | } | |
| 100 | + | flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0) | |
| 101 | + | if err != nil || flags&unix.O_NONBLOCK == 0 { | |
| 102 | + | t.Fatalf("fd not O_NONBLOCK after setup: %v", err) | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func TestNewConnAuthRequest(t *testing.T) { | |
| 107 | + | fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false) | |
| 108 | + | cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} | |
| 109 | + | c, err := newConn(fd, authMagicCookie, cookie) | |
| 110 | + | if err != nil { | |
| 111 | + | t.Fatal(err) | |
| 112 | + | } | |
| 113 | + | defer c.Close() | |
| 114 | + | want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...) | |
| 115 | + | want = append(want, cookie...) | |
| 116 | + | if got := <-req; !bytes.Equal(got, want) { | |
| 117 | + | t.Fatalf("setup request %x, want %x", got, want) | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func TestNewConnRefused(t *testing.T) { | |
| 122 | + | authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0} | |
| 123 | + | for _, tc := range []struct { | |
| 124 | + | name string | |
| 125 | + | reply []byte | |
| 126 | + | status uint8 | |
| 127 | + | reason string | |
| 128 | + | }{ | |
| 129 | + | {"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"}, | |
| 130 | + | {"authenticate", authenticate, 2, "nope!"}, | |
| 131 | + | } { | |
| 132 | + | t.Run(tc.name, func(t *testing.T) { | |
| 133 | + | fd, _ := scriptedServer(t, tc.reply, 5, true) | |
| 134 | + | c, err := NewConn(fd) | |
| 135 | + | var se *SetupError | |
| 136 | + | if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) { | |
| 137 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |
| 138 | + | } | |
| 139 | + | if se.Status != tc.status || se.Reason != tc.reason { | |
| 140 | + | t.Fatalf("status %d reason %q", se.Status, se.Reason) | |
| 141 | + | } | |
| 142 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF { | |
| 143 | + | t.Fatalf("fd not closed on failure: %v", err) | |
| 144 | + | } | |
| 145 | + | }) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestNewConnMalformed(t *testing.T) { | |
| 150 | + | setup := loadSetup(t, "Success") | |
| 151 | + | noScreens := bytes.Clone(setup) | |
| 152 | + | noScreens[28] = 0 | |
| 153 | + | badMask := bytes.Clone(setup) | |
| 154 | + | binary.LittleEndian.PutUint32(badMask[16:], 0) | |
| 155 | + | badVersion := bytes.Clone(setup) | |
| 156 | + | badVersion[2] = 10 | |
| 157 | + | badStatus := bytes.Clone(setup) | |
| 158 | + | badStatus[0] = 3 | |
| 159 | + | // Fixed Setup only (length 8 words): one root declared, no SCREEN bytes. | |
| 160 | + | screenPast := bytes.Clone(setup[:40]) | |
| 161 | + | binary.LittleEndian.PutUint16(screenPast[6:], 8) | |
| 162 | + | binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len | |
| 163 | + | screenPast[29] = 0 // pixmap_formats_len | |
| 164 | + | // Header claims 8 bytes of body, failure reason claims 20. | |
| 165 | + | reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} | |
| 166 | + | // Success header whose length is shorter than the fixed Setup. | |
| 167 | + | shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0} | |
| 168 | + | for _, tc := range []struct { | |
| 169 | + | name string | |
| 170 | + | reply []byte | |
| 171 | + | }{ | |
| 172 | + | {"empty", nil}, | |
| 173 | + | {"truncated header", setup[:5]}, | |
| 174 | + | {"truncated body", setup[:len(setup)-3]}, | |
| 175 | + | {"no screens", noScreens}, | |
| 176 | + | {"bad xid mask", badMask}, | |
| 177 | + | {"protocol major", badVersion}, | |
| 178 | + | {"unknown status", badStatus}, | |
| 179 | + | {"screen past end", screenPast}, | |
| 180 | + | {"reason past end", reasonPast}, | |
| 181 | + | {"short success", shortSuccess}, | |
| 182 | + | } { | |
| 183 | + | t.Run(tc.name, func(t *testing.T) { | |
| 184 | + | fd, _ := scriptedServer(t, tc.reply, 3, true) | |
| 185 | + | c, err := NewConn(fd) | |
| 186 | + | if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) { | |
| 187 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |
| 188 | + | } | |
| 189 | + | }) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | func TestConnFlushClose(t *testing.T) { | |
| 194 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 195 | + | if err != nil { | |
| 196 | + | t.Fatal(err) | |
| 197 | + | } | |
| 198 | + | server := os.NewFile(uintptr(fds[1]), "server") | |
| 199 | + | defer server.Close() | |
| 200 | + | setup := loadSetup(t, "Success") | |
| 201 | + | go func() { | |
| 202 | + | var req [12]byte | |
| 203 | + | _, _ = io.ReadFull(server, req[:]) | |
| 204 | + | _, _ = server.Write(setup) | |
| 205 | + | }() | |
| 206 | + | c, err := NewConn(fds[0]) | |
| 207 | + | if err != nil { | |
| 208 | + | t.Fatal(err) | |
| 209 | + | } | |
| 210 | + | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | |
| 211 | + | if p == nil || seq != 1 { | |
| 212 | + | t.Fatal("request") | |
| 213 | + | } | |
| 214 | + | if err := c.Flush(); err != nil { | |
| 215 | + | t.Fatal(err) | |
| 216 | + | } | |
| 217 | + | var got [4]byte | |
| 218 | + | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | |
| 219 | + | t.Fatalf("%x %v", got, err) | |
| 220 | + | } | |
| 221 | + | if err := c.Close(); err != nil { | |
| 222 | + | t.Fatal(err) | |
| 223 | + | } | |
| 224 | + | if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) { | |
| 225 | + | t.Fatal("after Close") | |
| 226 | + | } | |
| 227 | + | if p, _ := c.Buf().Request(43, 0, 0); p != nil { | |
| 228 | + | t.Fatal("Buf usable after Close") | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func TestSetupErrorMessage(t *testing.T) { | |
| 233 | + | for _, tc := range []struct { | |
| 234 | + | e SetupError | |
| 235 | + | want string | |
| 236 | + | }{ | |
| 237 | + | {SetupError{0, "no"}, "xturbo: connection setup failed: no"}, | |
| 238 | + | {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"}, | |
| 239 | + | } { | |
| 240 | + | if got := tc.e.Error(); got != tc.want { | |
| 241 | + | t.Errorf("%q, want %q", got, tc.want) | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | } | |
| @@ -0,0 +1,244 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "errors" | |||
| 7 | + | "io" | |||
| 8 | + | "os" | |||
| 9 | + | "path/filepath" | |||
| 10 | + | "testing" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/xturbo/internal/xtest" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func loadSetup(t testing.TB, name string) []byte { | |||
| 17 | + | t.Helper() | |||
| 18 | + | b, err := xtest.LoadHex(filepath.Join("testdata", "setup", name+".setup.hex")) | |||
| 19 | + | if err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | return b | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // scriptedServer runs the server side of a socketpair: it reads one setup | |||
| 26 | + | // request, reports it on the returned channel, writes reply in chunks of | |||
| 27 | + | // chunk bytes (0 = all at once) and, if hangup, closes its end. | |||
| 28 | + | func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) { | |||
| 29 | + | t.Helper() | |||
| 30 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | // Exercise NewConn switching a non-blocking fd back to blocking. | |||
| 35 | + | if err := unix.SetNonblock(fds[0], true); err != nil { | |||
| 36 | + | t.Fatal(err) | |||
| 37 | + | } | |||
| 38 | + | server := os.NewFile(uintptr(fds[1]), "server") | |||
| 39 | + | ch := make(chan []byte, 1) | |||
| 40 | + | done := make(chan struct{}) | |||
| 41 | + | t.Cleanup(func() { <-done; server.Close() }) | |||
| 42 | + | go func() { | |||
| 43 | + | defer close(done) | |||
| 44 | + | var hdr [12]byte | |||
| 45 | + | if _, err := io.ReadFull(server, hdr[:]); err != nil { | |||
| 46 | + | ch <- nil | |||
| 47 | + | return | |||
| 48 | + | } | |||
| 49 | + | n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:]))) | |||
| 50 | + | rest := make([]byte, n) | |||
| 51 | + | if _, err := io.ReadFull(server, rest); err != nil { | |||
| 52 | + | ch <- nil | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | ch <- append(hdr[:], rest...) | |||
| 56 | + | for p := reply; len(p) > 0; { | |||
| 57 | + | k := len(p) | |||
| 58 | + | if chunk > 0 { | |||
| 59 | + | k = min(k, chunk) | |||
| 60 | + | } | |||
| 61 | + | if _, err := server.Write(p[:k]); err != nil { | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | p = p[k:] | |||
| 65 | + | } | |||
| 66 | + | if hangup { | |||
| 67 | + | server.Close() | |||
| 68 | + | } | |||
| 69 | + | }() | |||
| 70 | + | return fds[0], ch | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestNewConnSuccess(t *testing.T) { | |||
| 74 | + | setup := loadSetup(t, "Success") | |||
| 75 | + | fd, req := scriptedServer(t, setup, 7, false) | |||
| 76 | + | c, err := NewConn(fd) | |||
| 77 | + | if err != nil { | |||
| 78 | + | t.Fatal(err) | |||
| 79 | + | } | |||
| 80 | + | defer c.Close() | |||
| 81 | + | got := <-req | |||
| 82 | + | want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0} | |||
| 83 | + | if !bytes.Equal(got, want) { | |||
| 84 | + | t.Fatalf("setup request %x, want %x", got, want) | |||
| 85 | + | } | |||
| 86 | + | if !bytes.Equal(c.SetupBytes(), setup) { | |||
| 87 | + | t.Fatal("SetupBytes differs from the reply") | |||
| 88 | + | } | |||
| 89 | + | // Expected values are read from the fixture header comment | |||
| 90 | + | // (Success.setup.hex), i.e. what that recorded Xvfb assigned. | |||
| 91 | + | if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff { | |||
| 92 | + | t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask) | |||
| 93 | + | } | |||
| 94 | + | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 { | |||
| 95 | + | t.Fatal("accessors") | |||
| 96 | + | } | |||
| 97 | + | if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present { | |||
| 98 | + | t.Fatal("ExtTable not empty") | |||
| 99 | + | } | |||
| 100 | + | flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0) | |||
| 101 | + | if err != nil || flags&unix.O_NONBLOCK == 0 { | |||
| 102 | + | t.Fatalf("fd not O_NONBLOCK after setup: %v", err) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func TestNewConnAuthRequest(t *testing.T) { | |||
| 107 | + | fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false) | |||
| 108 | + | cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} | |||
| 109 | + | c, err := newConn(fd, authMagicCookie, cookie) | |||
| 110 | + | if err != nil { | |||
| 111 | + | t.Fatal(err) | |||
| 112 | + | } | |||
| 113 | + | defer c.Close() | |||
| 114 | + | want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...) | |||
| 115 | + | want = append(want, cookie...) | |||
| 116 | + | if got := <-req; !bytes.Equal(got, want) { | |||
| 117 | + | t.Fatalf("setup request %x, want %x", got, want) | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func TestNewConnRefused(t *testing.T) { | |||
| 122 | + | authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0} | |||
| 123 | + | for _, tc := range []struct { | |||
| 124 | + | name string | |||
| 125 | + | reply []byte | |||
| 126 | + | status uint8 | |||
| 127 | + | reason string | |||
| 128 | + | }{ | |||
| 129 | + | {"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"}, | |||
| 130 | + | {"authenticate", authenticate, 2, "nope!"}, | |||
| 131 | + | } { | |||
| 132 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 133 | + | fd, _ := scriptedServer(t, tc.reply, 5, true) | |||
| 134 | + | c, err := NewConn(fd) | |||
| 135 | + | var se *SetupError | |||
| 136 | + | if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) { | |||
| 137 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |||
| 138 | + | } | |||
| 139 | + | if se.Status != tc.status || se.Reason != tc.reason { | |||
| 140 | + | t.Fatalf("status %d reason %q", se.Status, se.Reason) | |||
| 141 | + | } | |||
| 142 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF { | |||
| 143 | + | t.Fatalf("fd not closed on failure: %v", err) | |||
| 144 | + | } | |||
| 145 | + | }) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func TestNewConnMalformed(t *testing.T) { | |||
| 150 | + | setup := loadSetup(t, "Success") | |||
| 151 | + | noScreens := bytes.Clone(setup) | |||
| 152 | + | noScreens[28] = 0 | |||
| 153 | + | badMask := bytes.Clone(setup) | |||
| 154 | + | binary.LittleEndian.PutUint32(badMask[16:], 0) | |||
| 155 | + | badVersion := bytes.Clone(setup) | |||
| 156 | + | badVersion[2] = 10 | |||
| 157 | + | badStatus := bytes.Clone(setup) | |||
| 158 | + | badStatus[0] = 3 | |||
| 159 | + | // Fixed Setup only (length 8 words): one root declared, no SCREEN bytes. | |||
| 160 | + | screenPast := bytes.Clone(setup[:40]) | |||
| 161 | + | binary.LittleEndian.PutUint16(screenPast[6:], 8) | |||
| 162 | + | binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len | |||
| 163 | + | screenPast[29] = 0 // pixmap_formats_len | |||
| 164 | + | // Header claims 8 bytes of body, failure reason claims 20. | |||
| 165 | + | reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} | |||
| 166 | + | // Success header whose length is shorter than the fixed Setup. | |||
| 167 | + | shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0} | |||
| 168 | + | for _, tc := range []struct { | |||
| 169 | + | name string | |||
| 170 | + | reply []byte | |||
| 171 | + | }{ | |||
| 172 | + | {"empty", nil}, | |||
| 173 | + | {"truncated header", setup[:5]}, | |||
| 174 | + | {"truncated body", setup[:len(setup)-3]}, | |||
| 175 | + | {"no screens", noScreens}, | |||
| 176 | + | {"bad xid mask", badMask}, | |||
| 177 | + | {"protocol major", badVersion}, | |||
| 178 | + | {"unknown status", badStatus}, | |||
| 179 | + | {"screen past end", screenPast}, | |||
| 180 | + | {"reason past end", reasonPast}, | |||
| 181 | + | {"short success", shortSuccess}, | |||
| 182 | + | } { | |||
| 183 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 184 | + | fd, _ := scriptedServer(t, tc.reply, 3, true) | |||
| 185 | + | c, err := NewConn(fd) | |||
| 186 | + | if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) { | |||
| 187 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |||
| 188 | + | } | |||
| 189 | + | }) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func TestConnFlushClose(t *testing.T) { | |||
| 194 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 195 | + | if err != nil { | |||
| 196 | + | t.Fatal(err) | |||
| 197 | + | } | |||
| 198 | + | server := os.NewFile(uintptr(fds[1]), "server") | |||
| 199 | + | defer server.Close() | |||
| 200 | + | setup := loadSetup(t, "Success") | |||
| 201 | + | go func() { | |||
| 202 | + | var req [12]byte | |||
| 203 | + | _, _ = io.ReadFull(server, req[:]) | |||
| 204 | + | _, _ = server.Write(setup) | |||
| 205 | + | }() | |||
| 206 | + | c, err := NewConn(fds[0]) | |||
| 207 | + | if err != nil { | |||
| 208 | + | t.Fatal(err) | |||
| 209 | + | } | |||
| 210 | + | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | |||
| 211 | + | if p == nil || seq != 1 { | |||
| 212 | + | t.Fatal("request") | |||
| 213 | + | } | |||
| 214 | + | if err := c.Flush(); err != nil { | |||
| 215 | + | t.Fatal(err) | |||
| 216 | + | } | |||
| 217 | + | var got [4]byte | |||
| 218 | + | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | |||
| 219 | + | t.Fatalf("%x %v", got, err) | |||
| 220 | + | } | |||
| 221 | + | if err := c.Close(); err != nil { | |||
| 222 | + | t.Fatal(err) | |||
| 223 | + | } | |||
| 224 | + | if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) { | |||
| 225 | + | t.Fatal("after Close") | |||
| 226 | + | } | |||
| 227 | + | if p, _ := c.Buf().Request(43, 0, 0); p != nil { | |||
| 228 | + | t.Fatal("Buf usable after Close") | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func TestSetupErrorMessage(t *testing.T) { | |||
| 233 | + | for _, tc := range []struct { | |||
| 234 | + | e SetupError | |||
| 235 | + | want string | |||
| 236 | + | }{ | |||
| 237 | + | {SetupError{0, "no"}, "xturbo: connection setup failed: no"}, | |||
| 238 | + | {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"}, | |||
| 239 | + | } { | |||
| 240 | + | if got := tc.e.Error(); got != tc.want { | |||
| 241 | + | t.Errorf("%q, want %q", got, tc.want) | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 244 | + | } | |||
A conn_xserver_test.go +158 -0
| @@ -0,0 +1,158 @@ | |||
| 1 | + | //go:build xserver | |
| 2 | + | ||
| 3 | + | package xturbo_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "crypto/rand" | |
| 8 | + | "encoding/binary" | |
| 9 | + | "errors" | |
| 10 | + | "os" | |
| 11 | + | "os/exec" | |
| 12 | + | "path/filepath" | |
| 13 | + | "strconv" | |
| 14 | + | "strings" | |
| 15 | + | "testing" | |
| 16 | + | "time" | |
| 17 | + | ||
| 18 | + | "bnema.dev/xturbo" | |
| 19 | + | "bnema.dev/xturbo/xproto" | |
| 20 | + | "golang.org/x/sys/unix" | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | // startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the | |
| 24 | + | // MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string. | |
| 25 | + | func startXvfb(t *testing.T, authFile string) string { | |
| 26 | + | t.Helper() | |
| 27 | + | if _, err := exec.LookPath("Xvfb"); err != nil { | |
| 28 | + | t.Skip("Xvfb not installed") | |
| 29 | + | } | |
| 30 | + | r, w, err := os.Pipe() | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | defer r.Close() | |
| 35 | + | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile) | |
| 36 | + | cmd.ExtraFiles = []*os.File{w} | |
| 37 | + | var log bytes.Buffer | |
| 38 | + | cmd.Stderr = &log | |
| 39 | + | if err := cmd.Start(); err != nil { | |
| 40 | + | w.Close() | |
| 41 | + | t.Fatal(err) | |
| 42 | + | } | |
| 43 | + | w.Close() | |
| 44 | + | t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) | |
| 45 | + | _ = r.SetReadDeadline(time.Now().Add(10 * time.Second)) | |
| 46 | + | var buf [32]byte | |
| 47 | + | n, err := r.Read(buf[:]) | |
| 48 | + | if err != nil { | |
| 49 | + | t.Fatalf("Xvfb: %v: %s", err, log.String()) | |
| 50 | + | } | |
| 51 | + | num := strings.TrimSpace(string(buf[:n])) | |
| 52 | + | if _, err := strconv.Atoi(num); err != nil { | |
| 53 | + | t.Fatalf("Xvfb display %q", num) | |
| 54 | + | } | |
| 55 | + | return ":" + num | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // writeXauthority writes one FamilyLocal entry for this host, any display. | |
| 59 | + | func writeXauthority(t *testing.T, path string, cookie []byte) { | |
| 60 | + | t.Helper() | |
| 61 | + | host, err := os.Hostname() | |
| 62 | + | if err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | var b []byte | |
| 66 | + | field := func(s []byte) { | |
| 67 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |
| 68 | + | b = append(b, s...) | |
| 69 | + | } | |
| 70 | + | b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal | |
| 71 | + | field([]byte(host)) | |
| 72 | + | field(nil) // any display number | |
| 73 | + | field([]byte("MIT-MAGIC-COOKIE-1")) | |
| 74 | + | field(cookie) | |
| 75 | + | if err := os.WriteFile(path, b, 0o600); err != nil { | |
| 76 | + | t.Fatal(err) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func TestXserverDial(t *testing.T) { | |
| 81 | + | dir := t.TempDir() | |
| 82 | + | auth := filepath.Join(dir, "Xauthority") | |
| 83 | + | cookie := make([]byte, 16) | |
| 84 | + | _, _ = rand.Read(cookie) | |
| 85 | + | writeXauthority(t, auth, cookie) | |
| 86 | + | display := startXvfb(t, auth) | |
| 87 | + | ||
| 88 | + | // Without a cookie the server refuses the setup. | |
| 89 | + | t.Setenv("XAUTHORITY", filepath.Join(dir, "missing")) | |
| 90 | + | if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) { | |
| 91 | + | t.Fatalf("unauthenticated Dial: %v", err) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | t.Setenv("XAUTHORITY", auth) | |
| 95 | + | c, err := xturbo.Dial(display + ".0") | |
| 96 | + | if err != nil { | |
| 97 | + | t.Fatal(err) | |
| 98 | + | } | |
| 99 | + | defer c.Close() | |
| 100 | + | ||
| 101 | + | // Compare root's own parsing against the generated Setup view instead of | |
| 102 | + | // server-assigned constants (they differ between X server builds). | |
| 103 | + | setup := c.SetupBytes() | |
| 104 | + | if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) { | |
| 105 | + | t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup)) | |
| 106 | + | } | |
| 107 | + | it := xproto.NewSetupIter(setup, 1) | |
| 108 | + | if !it.Next() { | |
| 109 | + | t.Fatal("setup does not parse") | |
| 110 | + | } | |
| 111 | + | sv := it.Value() | |
| 112 | + | if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 { | |
| 113 | + | t.Fatalf("setup size %d status %d", sv.Size(), sv.Status()) | |
| 114 | + | } | |
| 115 | + | screens := sv.Roots() | |
| 116 | + | if !screens.Next() || uint32(screens.Value().Root()) != c.Root() { | |
| 117 | + | t.Fatalf("Root %#x, setup view says otherwise", c.Root()) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // A trivial round trip: GetInputFocus, then read its reply. | |
| 121 | + | seq := xproto.GetInputFocus(c.Buf()) | |
| 122 | + | if seq != 1 { | |
| 123 | + | t.Fatalf("seq %d", seq) | |
| 124 | + | } | |
| 125 | + | if err := c.Flush(); err != nil { | |
| 126 | + | t.Fatal(err) | |
| 127 | + | } | |
| 128 | + | reply := readPacket(t, c.Fd()) | |
| 129 | + | if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) { | |
| 130 | + | t.Fatalf("reply %x", reply) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // readPacket polls the non-blocking fd and reads one 32-byte packet. | |
| 135 | + | func readPacket(t *testing.T, fd int) [32]byte { | |
| 136 | + | t.Helper() | |
| 137 | + | var p [32]byte | |
| 138 | + | got := 0 | |
| 139 | + | deadline := time.Now().Add(5 * time.Second) | |
| 140 | + | for got < len(p) { | |
| 141 | + | if time.Now().After(deadline) { | |
| 142 | + | t.Fatal("timeout reading reply") | |
| 143 | + | } | |
| 144 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |
| 145 | + | if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR { | |
| 146 | + | t.Fatal(err) | |
| 147 | + | } | |
| 148 | + | n, err := unix.Read(fd, p[got:]) | |
| 149 | + | if err == unix.EAGAIN || err == unix.EINTR { | |
| 150 | + | continue | |
| 151 | + | } | |
| 152 | + | if err != nil || n == 0 { | |
| 153 | + | t.Fatalf("read: %d %v", n, err) | |
| 154 | + | } | |
| 155 | + | got += n | |
| 156 | + | } | |
| 157 | + | return p | |
| 158 | + | } | |
| @@ -0,0 +1,158 @@ | |||||
| 1 | + | //go:build xserver | |||
| 2 | + | ||||
| 3 | + | package xturbo_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "crypto/rand" | |||
| 8 | + | "encoding/binary" | |||
| 9 | + | "errors" | |||
| 10 | + | "os" | |||
| 11 | + | "os/exec" | |||
| 12 | + | "path/filepath" | |||
| 13 | + | "strconv" | |||
| 14 | + | "strings" | |||
| 15 | + | "testing" | |||
| 16 | + | "time" | |||
| 17 | + | ||||
| 18 | + | "bnema.dev/xturbo" | |||
| 19 | + | "bnema.dev/xturbo/xproto" | |||
| 20 | + | "golang.org/x/sys/unix" | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | // startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the | |||
| 24 | + | // MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string. | |||
| 25 | + | func startXvfb(t *testing.T, authFile string) string { | |||
| 26 | + | t.Helper() | |||
| 27 | + | if _, err := exec.LookPath("Xvfb"); err != nil { | |||
| 28 | + | t.Skip("Xvfb not installed") | |||
| 29 | + | } | |||
| 30 | + | r, w, err := os.Pipe() | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | defer r.Close() | |||
| 35 | + | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile) | |||
| 36 | + | cmd.ExtraFiles = []*os.File{w} | |||
| 37 | + | var log bytes.Buffer | |||
| 38 | + | cmd.Stderr = &log | |||
| 39 | + | if err := cmd.Start(); err != nil { | |||
| 40 | + | w.Close() | |||
| 41 | + | t.Fatal(err) | |||
| 42 | + | } | |||
| 43 | + | w.Close() | |||
| 44 | + | t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) | |||
| 45 | + | _ = r.SetReadDeadline(time.Now().Add(10 * time.Second)) | |||
| 46 | + | var buf [32]byte | |||
| 47 | + | n, err := r.Read(buf[:]) | |||
| 48 | + | if err != nil { | |||
| 49 | + | t.Fatalf("Xvfb: %v: %s", err, log.String()) | |||
| 50 | + | } | |||
| 51 | + | num := strings.TrimSpace(string(buf[:n])) | |||
| 52 | + | if _, err := strconv.Atoi(num); err != nil { | |||
| 53 | + | t.Fatalf("Xvfb display %q", num) | |||
| 54 | + | } | |||
| 55 | + | return ":" + num | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // writeXauthority writes one FamilyLocal entry for this host, any display. | |||
| 59 | + | func writeXauthority(t *testing.T, path string, cookie []byte) { | |||
| 60 | + | t.Helper() | |||
| 61 | + | host, err := os.Hostname() | |||
| 62 | + | if err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | var b []byte | |||
| 66 | + | field := func(s []byte) { | |||
| 67 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |||
| 68 | + | b = append(b, s...) | |||
| 69 | + | } | |||
| 70 | + | b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal | |||
| 71 | + | field([]byte(host)) | |||
| 72 | + | field(nil) // any display number | |||
| 73 | + | field([]byte("MIT-MAGIC-COOKIE-1")) | |||
| 74 | + | field(cookie) | |||
| 75 | + | if err := os.WriteFile(path, b, 0o600); err != nil { | |||
| 76 | + | t.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func TestXserverDial(t *testing.T) { | |||
| 81 | + | dir := t.TempDir() | |||
| 82 | + | auth := filepath.Join(dir, "Xauthority") | |||
| 83 | + | cookie := make([]byte, 16) | |||
| 84 | + | _, _ = rand.Read(cookie) | |||
| 85 | + | writeXauthority(t, auth, cookie) | |||
| 86 | + | display := startXvfb(t, auth) | |||
| 87 | + | ||||
| 88 | + | // Without a cookie the server refuses the setup. | |||
| 89 | + | t.Setenv("XAUTHORITY", filepath.Join(dir, "missing")) | |||
| 90 | + | if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) { | |||
| 91 | + | t.Fatalf("unauthenticated Dial: %v", err) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | t.Setenv("XAUTHORITY", auth) | |||
| 95 | + | c, err := xturbo.Dial(display + ".0") | |||
| 96 | + | if err != nil { | |||
| 97 | + | t.Fatal(err) | |||
| 98 | + | } | |||
| 99 | + | defer c.Close() | |||
| 100 | + | ||||
| 101 | + | // Compare root's own parsing against the generated Setup view instead of | |||
| 102 | + | // server-assigned constants (they differ between X server builds). | |||
| 103 | + | setup := c.SetupBytes() | |||
| 104 | + | if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) { | |||
| 105 | + | t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup)) | |||
| 106 | + | } | |||
| 107 | + | it := xproto.NewSetupIter(setup, 1) | |||
| 108 | + | if !it.Next() { | |||
| 109 | + | t.Fatal("setup does not parse") | |||
| 110 | + | } | |||
| 111 | + | sv := it.Value() | |||
| 112 | + | if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 { | |||
| 113 | + | t.Fatalf("setup size %d status %d", sv.Size(), sv.Status()) | |||
| 114 | + | } | |||
| 115 | + | screens := sv.Roots() | |||
| 116 | + | if !screens.Next() || uint32(screens.Value().Root()) != c.Root() { | |||
| 117 | + | t.Fatalf("Root %#x, setup view says otherwise", c.Root()) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | // A trivial round trip: GetInputFocus, then read its reply. | |||
| 121 | + | seq := xproto.GetInputFocus(c.Buf()) | |||
| 122 | + | if seq != 1 { | |||
| 123 | + | t.Fatalf("seq %d", seq) | |||
| 124 | + | } | |||
| 125 | + | if err := c.Flush(); err != nil { | |||
| 126 | + | t.Fatal(err) | |||
| 127 | + | } | |||
| 128 | + | reply := readPacket(t, c.Fd()) | |||
| 129 | + | if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) { | |||
| 130 | + | t.Fatalf("reply %x", reply) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // readPacket polls the non-blocking fd and reads one 32-byte packet. | |||
| 135 | + | func readPacket(t *testing.T, fd int) [32]byte { | |||
| 136 | + | t.Helper() | |||
| 137 | + | var p [32]byte | |||
| 138 | + | got := 0 | |||
| 139 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 140 | + | for got < len(p) { | |||
| 141 | + | if time.Now().After(deadline) { | |||
| 142 | + | t.Fatal("timeout reading reply") | |||
| 143 | + | } | |||
| 144 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |||
| 145 | + | if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR { | |||
| 146 | + | t.Fatal(err) | |||
| 147 | + | } | |||
| 148 | + | n, err := unix.Read(fd, p[got:]) | |||
| 149 | + | if err == unix.EAGAIN || err == unix.EINTR { | |||
| 150 | + | continue | |||
| 151 | + | } | |||
| 152 | + | if err != nil || n == 0 { | |||
| 153 | + | t.Fatalf("read: %d %v", n, err) | |||
| 154 | + | } | |||
| 155 | + | got += n | |||
| 156 | + | } | |||
| 157 | + | return p | |||
| 158 | + | } | |||
A dial.go +152 -0
| @@ -0,0 +1,152 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 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 | + | ||
| 16 | + | const ( | |
| 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. | |
| 28 | + | func 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). | |
| 57 | + | func 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. | |
| 77 | + | func 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 | + | ||
| 102 | + | func 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. | |
| 116 | + | func 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 | + | ||
| 124 | + | func 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 | + | } | |
| @@ -0,0 +1,152 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 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 | + | ||||
| 16 | + | const ( | |||
| 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. | |||
| 28 | + | func 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). | |||
| 57 | + | func 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. | |||
| 77 | + | func 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 | + | ||||
| 102 | + | func 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. | |||
| 116 | + | func 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 | + | ||||
| 124 | + | func 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 | + | } | |||
A dial_test.go +124 -0
| @@ -0,0 +1,124 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "errors" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type authEntry struct { | |
| 13 | + | family uint16 | |
| 14 | + | addr, number, name string | |
| 15 | + | data []byte | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func encodeAuth(entries ...authEntry) []byte { | |
| 19 | + | var b []byte | |
| 20 | + | str := func(s []byte) { | |
| 21 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |
| 22 | + | b = append(b, s...) | |
| 23 | + | } | |
| 24 | + | for _, e := range entries { | |
| 25 | + | b = binary.BigEndian.AppendUint16(b, e.family) | |
| 26 | + | str([]byte(e.addr)) | |
| 27 | + | str([]byte(e.number)) | |
| 28 | + | str([]byte(e.name)) | |
| 29 | + | str(e.data) | |
| 30 | + | } | |
| 31 | + | return b | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func TestReadCookie(t *testing.T) { | |
| 35 | + | cookie := []byte("0123456789abcdef") | |
| 36 | + | other := []byte("fedcba9876543210") | |
| 37 | + | local := func(host, num string, data []byte) authEntry { | |
| 38 | + | return authEntry{familyLocal, host, num, authMagicCookie, data} | |
| 39 | + | } | |
| 40 | + | for _, tc := range []struct { | |
| 41 | + | name string | |
| 42 | + | file []byte | |
| 43 | + | want []byte | |
| 44 | + | wantErr error | |
| 45 | + | }{ | |
| 46 | + | {"match", encodeAuth(local("arch", "7", cookie)), cookie, nil}, | |
| 47 | + | {"first match wins", encodeAuth(local("other", "7", other), local("arch", "3", other), local("arch", "7", cookie), local("arch", "7", other)), cookie, nil}, | |
| 48 | + | {"wild family", encodeAuth(authEntry{familyWild, "", "7", authMagicCookie, cookie}), cookie, nil}, | |
| 49 | + | {"empty number matches any", encodeAuth(local("arch", "", cookie)), cookie, nil}, | |
| 50 | + | {"other host", encodeAuth(local("box", "7", cookie)), nil, nil}, | |
| 51 | + | {"other display", encodeAuth(local("arch", "70", cookie)), nil, nil}, | |
| 52 | + | {"other family", encodeAuth(authEntry{0, "arch", "7", authMagicCookie, cookie}), nil, nil}, | |
| 53 | + | {"other protocol", encodeAuth(authEntry{familyLocal, "arch", "7", "XDM-AUTHORIZATION-1", cookie}), nil, nil}, | |
| 54 | + | {"empty file", nil, nil, nil}, | |
| 55 | + | {"truncated family", []byte{1}, nil, ErrMalformed}, | |
| 56 | + | {"truncated field", encodeAuth(local("arch", "7", cookie))[:30], nil, ErrMalformed}, | |
| 57 | + | {"missing fields", encodeAuth(local("arch", "7", cookie))[:10], nil, ErrMalformed}, | |
| 58 | + | } { | |
| 59 | + | t.Run(tc.name, func(t *testing.T) { | |
| 60 | + | path := filepath.Join(t.TempDir(), "Xauthority") | |
| 61 | + | if err := os.WriteFile(path, tc.file, 0o600); err != nil { | |
| 62 | + | t.Fatal(err) | |
| 63 | + | } | |
| 64 | + | got, err := readCookie(path, "arch", 7) | |
| 65 | + | if !errors.Is(err, tc.wantErr) || (tc.wantErr == nil && err != nil) || !bytes.Equal(got, tc.want) { | |
| 66 | + | t.Fatalf("got %q, %v; want %q, %v", got, err, tc.want, tc.wantErr) | |
| 67 | + | } | |
| 68 | + | }) | |
| 69 | + | } | |
| 70 | + | if _, err := readCookie(filepath.Join(t.TempDir(), "missing"), "arch", 7); !errors.Is(err, os.ErrNotExist) { | |
| 71 | + | t.Fatal(err) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func TestXauthorityPath(t *testing.T) { | |
| 76 | + | t.Setenv("XAUTHORITY", "/x/auth") | |
| 77 | + | if got := xauthorityPath(); got != "/x/auth" { | |
| 78 | + | t.Fatal(got) | |
| 79 | + | } | |
| 80 | + | t.Setenv("XAUTHORITY", "") | |
| 81 | + | t.Setenv("HOME", "/home/u") | |
| 82 | + | if got := xauthorityPath(); got != "/home/u/.Xauthority" { | |
| 83 | + | t.Fatal(got) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestParseDisplay(t *testing.T) { | |
| 88 | + | for _, tc := range []struct { | |
| 89 | + | in string | |
| 90 | + | want int | |
| 91 | + | ok bool | |
| 92 | + | }{ | |
| 93 | + | {":0", 0, true}, | |
| 94 | + | {":12", 12, true}, | |
| 95 | + | {":3.1", 3, true}, | |
| 96 | + | {"unix:5", 5, true}, | |
| 97 | + | {"unix:5.0", 5, true}, | |
| 98 | + | {"", 0, false}, | |
| 99 | + | {"0", 0, false}, | |
| 100 | + | {":", 0, false}, | |
| 101 | + | {":x", 0, false}, | |
| 102 | + | {":1.", 0, false}, | |
| 103 | + | {":1.x", 0, false}, | |
| 104 | + | {":-1", 0, false}, | |
| 105 | + | {"localhost:0", 0, false}, | |
| 106 | + | {"host::0", 0, false}, | |
| 107 | + | {"::1:0", 0, false}, | |
| 108 | + | } { | |
| 109 | + | got, err := parseDisplay(tc.in) | |
| 110 | + | if (err == nil) != tc.ok || got != tc.want { | |
| 111 | + | t.Errorf("%q: %d, %v", tc.in, got, err) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestDialNoServer(t *testing.T) { | |
| 117 | + | // Display numbers above any real server: both abstract and path fail. | |
| 118 | + | if _, err := Dial(":2147483000"); err == nil { | |
| 119 | + | t.Fatal("Dial succeeded without a server") | |
| 120 | + | } | |
| 121 | + | if _, err := Dial("tcp/host:0"); err == nil { | |
| 122 | + | t.Fatal("remote DISPLAY accepted") | |
| 123 | + | } | |
| 124 | + | } | |
| @@ -0,0 +1,124 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "errors" | |||
| 7 | + | "os" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type authEntry struct { | |||
| 13 | + | family uint16 | |||
| 14 | + | addr, number, name string | |||
| 15 | + | data []byte | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func encodeAuth(entries ...authEntry) []byte { | |||
| 19 | + | var b []byte | |||
| 20 | + | str := func(s []byte) { | |||
| 21 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |||
| 22 | + | b = append(b, s...) | |||
| 23 | + | } | |||
| 24 | + | for _, e := range entries { | |||
| 25 | + | b = binary.BigEndian.AppendUint16(b, e.family) | |||
| 26 | + | str([]byte(e.addr)) | |||
| 27 | + | str([]byte(e.number)) | |||
| 28 | + | str([]byte(e.name)) | |||
| 29 | + | str(e.data) | |||
| 30 | + | } | |||
| 31 | + | return b | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func TestReadCookie(t *testing.T) { | |||
| 35 | + | cookie := []byte("0123456789abcdef") | |||
| 36 | + | other := []byte("fedcba9876543210") | |||
| 37 | + | local := func(host, num string, data []byte) authEntry { | |||
| 38 | + | return authEntry{familyLocal, host, num, authMagicCookie, data} | |||
| 39 | + | } | |||
| 40 | + | for _, tc := range []struct { | |||
| 41 | + | name string | |||
| 42 | + | file []byte | |||
| 43 | + | want []byte | |||
| 44 | + | wantErr error | |||
| 45 | + | }{ | |||
| 46 | + | {"match", encodeAuth(local("arch", "7", cookie)), cookie, nil}, | |||
| 47 | + | {"first match wins", encodeAuth(local("other", "7", other), local("arch", "3", other), local("arch", "7", cookie), local("arch", "7", other)), cookie, nil}, | |||
| 48 | + | {"wild family", encodeAuth(authEntry{familyWild, "", "7", authMagicCookie, cookie}), cookie, nil}, | |||
| 49 | + | {"empty number matches any", encodeAuth(local("arch", "", cookie)), cookie, nil}, | |||
| 50 | + | {"other host", encodeAuth(local("box", "7", cookie)), nil, nil}, | |||
| 51 | + | {"other display", encodeAuth(local("arch", "70", cookie)), nil, nil}, | |||
| 52 | + | {"other family", encodeAuth(authEntry{0, "arch", "7", authMagicCookie, cookie}), nil, nil}, | |||
| 53 | + | {"other protocol", encodeAuth(authEntry{familyLocal, "arch", "7", "XDM-AUTHORIZATION-1", cookie}), nil, nil}, | |||
| 54 | + | {"empty file", nil, nil, nil}, | |||
| 55 | + | {"truncated family", []byte{1}, nil, ErrMalformed}, | |||
| 56 | + | {"truncated field", encodeAuth(local("arch", "7", cookie))[:30], nil, ErrMalformed}, | |||
| 57 | + | {"missing fields", encodeAuth(local("arch", "7", cookie))[:10], nil, ErrMalformed}, | |||
| 58 | + | } { | |||
| 59 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 60 | + | path := filepath.Join(t.TempDir(), "Xauthority") | |||
| 61 | + | if err := os.WriteFile(path, tc.file, 0o600); err != nil { | |||
| 62 | + | t.Fatal(err) | |||
| 63 | + | } | |||
| 64 | + | got, err := readCookie(path, "arch", 7) | |||
| 65 | + | if !errors.Is(err, tc.wantErr) || (tc.wantErr == nil && err != nil) || !bytes.Equal(got, tc.want) { | |||
| 66 | + | t.Fatalf("got %q, %v; want %q, %v", got, err, tc.want, tc.wantErr) | |||
| 67 | + | } | |||
| 68 | + | }) | |||
| 69 | + | } | |||
| 70 | + | if _, err := readCookie(filepath.Join(t.TempDir(), "missing"), "arch", 7); !errors.Is(err, os.ErrNotExist) { | |||
| 71 | + | t.Fatal(err) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func TestXauthorityPath(t *testing.T) { | |||
| 76 | + | t.Setenv("XAUTHORITY", "/x/auth") | |||
| 77 | + | if got := xauthorityPath(); got != "/x/auth" { | |||
| 78 | + | t.Fatal(got) | |||
| 79 | + | } | |||
| 80 | + | t.Setenv("XAUTHORITY", "") | |||
| 81 | + | t.Setenv("HOME", "/home/u") | |||
| 82 | + | if got := xauthorityPath(); got != "/home/u/.Xauthority" { | |||
| 83 | + | t.Fatal(got) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestParseDisplay(t *testing.T) { | |||
| 88 | + | for _, tc := range []struct { | |||
| 89 | + | in string | |||
| 90 | + | want int | |||
| 91 | + | ok bool | |||
| 92 | + | }{ | |||
| 93 | + | {":0", 0, true}, | |||
| 94 | + | {":12", 12, true}, | |||
| 95 | + | {":3.1", 3, true}, | |||
| 96 | + | {"unix:5", 5, true}, | |||
| 97 | + | {"unix:5.0", 5, true}, | |||
| 98 | + | {"", 0, false}, | |||
| 99 | + | {"0", 0, false}, | |||
| 100 | + | {":", 0, false}, | |||
| 101 | + | {":x", 0, false}, | |||
| 102 | + | {":1.", 0, false}, | |||
| 103 | + | {":1.x", 0, false}, | |||
| 104 | + | {":-1", 0, false}, | |||
| 105 | + | {"localhost:0", 0, false}, | |||
| 106 | + | {"host::0", 0, false}, | |||
| 107 | + | {"::1:0", 0, false}, | |||
| 108 | + | } { | |||
| 109 | + | got, err := parseDisplay(tc.in) | |||
| 110 | + | if (err == nil) != tc.ok || got != tc.want { | |||
| 111 | + | t.Errorf("%q: %d, %v", tc.in, got, err) | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func TestDialNoServer(t *testing.T) { | |||
| 117 | + | // Display numbers above any real server: both abstract and path fail. | |||
| 118 | + | if _, err := Dial(":2147483000"); err == nil { | |||
| 119 | + | t.Fatal("Dial succeeded without a server") | |||
| 120 | + | } | |||
| 121 | + | if _, err := Dial("tcp/host:0"); err == nil { | |||
| 122 | + | t.Fatal("remote DISPLAY accepted") | |||
| 123 | + | } | |||
| 124 | + | } | |||
A errors.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import "errors" | |
| 4 | + | ||
| 5 | + | var ( | |
| 6 | + | // ErrMalformed reports bytes from the server or a local file that do not | |
| 7 | + | // follow the wire format (truncated, impossible lengths). | |
| 8 | + | ErrMalformed = errors.New("xturbo: malformed data") | |
| 9 | + | // ErrSetupFailed is wrapped by SetupError when the server refuses the | |
| 10 | + | // connection. | |
| 11 | + | ErrSetupFailed = errors.New("xturbo: connection setup failed") | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // Setup reply status bytes (xproto Setup*.status). | |
| 15 | + | const ( | |
| 16 | + | setupStatusFailed = 0 | |
| 17 | + | setupStatusSuccess = 1 | |
| 18 | + | setupStatusAuthenticate = 2 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // SetupError is the server's refusal of the connection setup: Status 0 | |
| 22 | + | // (Failed) or 2 (Authenticate), with the server's reason. It wraps | |
| 23 | + | // ErrSetupFailed. | |
| 24 | + | type SetupError struct { | |
| 25 | + | Status uint8 | |
| 26 | + | Reason string | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func (e *SetupError) Error() string { | |
| 30 | + | kind := "failed" | |
| 31 | + | if e.Status == setupStatusAuthenticate { | |
| 32 | + | kind = "authenticate" | |
| 33 | + | } | |
| 34 | + | return "xturbo: connection setup " + kind + ": " + e.Reason | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (e *SetupError) Unwrap() error { return ErrSetupFailed } | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import "errors" | |||
| 4 | + | ||||
| 5 | + | var ( | |||
| 6 | + | // ErrMalformed reports bytes from the server or a local file that do not | |||
| 7 | + | // follow the wire format (truncated, impossible lengths). | |||
| 8 | + | ErrMalformed = errors.New("xturbo: malformed data") | |||
| 9 | + | // ErrSetupFailed is wrapped by SetupError when the server refuses the | |||
| 10 | + | // connection. | |||
| 11 | + | ErrSetupFailed = errors.New("xturbo: connection setup failed") | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // Setup reply status bytes (xproto Setup*.status). | |||
| 15 | + | const ( | |||
| 16 | + | setupStatusFailed = 0 | |||
| 17 | + | setupStatusSuccess = 1 | |||
| 18 | + | setupStatusAuthenticate = 2 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // SetupError is the server's refusal of the connection setup: Status 0 | |||
| 22 | + | // (Failed) or 2 (Authenticate), with the server's reason. It wraps | |||
| 23 | + | // ErrSetupFailed. | |||
| 24 | + | type SetupError struct { | |||
| 25 | + | Status uint8 | |||
| 26 | + | Reason string | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func (e *SetupError) Error() string { | |||
| 30 | + | kind := "failed" | |||
| 31 | + | if e.Status == setupStatusAuthenticate { | |||
| 32 | + | kind = "authenticate" | |||
| 33 | + | } | |||
| 34 | + | return "xturbo: connection setup " + kind + ": " + e.Reason | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func (e *SetupError) Unwrap() error { return ErrSetupFailed } | |||
A setup.go +155 -0
| @@ -0,0 +1,155 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "fmt" | |
| 6 | + | "io" | |
| 7 | + | "os" | |
| 8 | + | ||
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Fixed layout of the xproto Setup reply (offsets from its status byte). | |
| 13 | + | const ( | |
| 14 | + | setupHeaderSize = 8 // status, pad/reason_len, protocol major, minor, length | |
| 15 | + | setupFixedSize = 40 // fixed part of a successful Setup | |
| 16 | + | screenFixedSize = 40 // fixed part of a SCREEN | |
| 17 | + | formatSize = 8 // FORMAT | |
| 18 | + | protocolMajor = 11 | |
| 19 | + | protocolMinor = 0 | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | // setupInfo is what root keeps from a successful setup reply. Everything else | |
| 23 | + | // is parsed by callers from Conn.SetupBytes. | |
| 24 | + | type setupInfo struct { | |
| 25 | + | xidBase, xidMask uint32 | |
| 26 | + | root uint32 | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) } | |
| 30 | + | ||
| 31 | + | // encodeSetupRequest builds the little-endian connection setup request. | |
| 32 | + | func encodeSetupRequest(authName string, authData []byte) []byte { | |
| 33 | + | n, d := pad4(len(authName)), pad4(len(authData)) | |
| 34 | + | p := make([]byte, 12+n+d) | |
| 35 | + | p[0] = 'l' | |
| 36 | + | binary.LittleEndian.PutUint16(p[2:], protocolMajor) | |
| 37 | + | binary.LittleEndian.PutUint16(p[4:], protocolMinor) | |
| 38 | + | binary.LittleEndian.PutUint16(p[6:], uint16(len(authName))) | |
| 39 | + | binary.LittleEndian.PutUint16(p[8:], uint16(len(authData))) | |
| 40 | + | copy(p[12:], authName) | |
| 41 | + | copy(p[12+n:], authData) | |
| 42 | + | return p | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func pad4(n int) int { return (n + 3) &^ 3 } | |
| 46 | + | ||
| 47 | + | // handshake runs the blocking setup exchange on fd and returns the full setup | |
| 48 | + | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |
| 49 | + | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |
| 50 | + | if len(authName) > 0xffff || len(authData) > 0xffff { | |
| 51 | + | return nil, setupInfo{}, ErrBadLength | |
| 52 | + | } | |
| 53 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |
| 54 | + | return nil, setupInfo{}, err | |
| 55 | + | } | |
| 56 | + | var hdr [setupHeaderSize]byte | |
| 57 | + | if err := readFull(fd, hdr[:]); err != nil { | |
| 58 | + | return nil, setupInfo{}, err | |
| 59 | + | } | |
| 60 | + | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |
| 61 | + | copy(reply, hdr[:]) | |
| 62 | + | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |
| 63 | + | return nil, setupInfo{}, err | |
| 64 | + | } | |
| 65 | + | info, err := parseSetup(reply) | |
| 66 | + | if err != nil { | |
| 67 | + | return nil, setupInfo{}, err | |
| 68 | + | } | |
| 69 | + | return reply, info, nil | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // parseSetup validates a complete setup reply (header length already | |
| 73 | + | // honoured) and extracts what root needs. | |
| 74 | + | func parseSetup(p []byte) (setupInfo, error) { | |
| 75 | + | if len(p) < setupHeaderSize { | |
| 76 | + | return setupInfo{}, malformed("short setup reply") | |
| 77 | + | } | |
| 78 | + | switch p[0] { | |
| 79 | + | case setupStatusFailed: | |
| 80 | + | // SetupFailed: reason_len at byte 1, reason after the header. | |
| 81 | + | n := int(p[1]) | |
| 82 | + | if setupHeaderSize+n > len(p) { | |
| 83 | + | return setupInfo{}, malformed("setup failure reason overflows reply") | |
| 84 | + | } | |
| 85 | + | return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])} | |
| 86 | + | case setupStatusAuthenticate: | |
| 87 | + | // SetupAuthenticate: reason fills length*4 bytes, NUL padded. | |
| 88 | + | reason := p[setupHeaderSize:] | |
| 89 | + | for len(reason) > 0 && reason[len(reason)-1] == 0 { | |
| 90 | + | reason = reason[:len(reason)-1] | |
| 91 | + | } | |
| 92 | + | return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)} | |
| 93 | + | case setupStatusSuccess: | |
| 94 | + | default: | |
| 95 | + | return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0])) | |
| 96 | + | } | |
| 97 | + | if len(p) < setupFixedSize { | |
| 98 | + | return setupInfo{}, malformed("short setup reply") | |
| 99 | + | } | |
| 100 | + | if binary.LittleEndian.Uint16(p[2:]) != protocolMajor { | |
| 101 | + | return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:]))) | |
| 102 | + | } | |
| 103 | + | info := setupInfo{ | |
| 104 | + | xidBase: binary.LittleEndian.Uint32(p[12:]), | |
| 105 | + | xidMask: binary.LittleEndian.Uint32(p[16:]), | |
| 106 | + | } | |
| 107 | + | if info.xidMask == 0 || info.xidBase&info.xidMask != 0 { | |
| 108 | + | return setupInfo{}, malformed("resource id base/mask") | |
| 109 | + | } | |
| 110 | + | if p[28] == 0 { | |
| 111 | + | return setupInfo{}, malformed("setup has no screens") | |
| 112 | + | } | |
| 113 | + | off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29]) | |
| 114 | + | if off+screenFixedSize > len(p) { | |
| 115 | + | return setupInfo{}, malformed("first screen overflows setup reply") | |
| 116 | + | } | |
| 117 | + | info.root = binary.LittleEndian.Uint32(p[off:]) | |
| 118 | + | return info, nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func writeFull(fd int, p []byte) error { | |
| 122 | + | for len(p) > 0 { | |
| 123 | + | n, err := unix.Write(fd, p) | |
| 124 | + | switch { | |
| 125 | + | case err == unix.EINTR: | |
| 126 | + | continue | |
| 127 | + | case err == unix.EPIPE || err == unix.ECONNRESET: | |
| 128 | + | return ErrClosed | |
| 129 | + | case err != nil: | |
| 130 | + | return os.NewSyscallError("write", err) | |
| 131 | + | case n <= 0: | |
| 132 | + | return io.ErrShortWrite | |
| 133 | + | } | |
| 134 | + | p = p[n:] | |
| 135 | + | } | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | func readFull(fd int, p []byte) error { | |
| 140 | + | for len(p) > 0 { | |
| 141 | + | n, err := unix.Read(fd, p) | |
| 142 | + | switch { | |
| 143 | + | case err == unix.EINTR: | |
| 144 | + | continue | |
| 145 | + | case err == unix.ECONNRESET: | |
| 146 | + | return ErrClosed | |
| 147 | + | case err != nil: | |
| 148 | + | return os.NewSyscallError("read", err) | |
| 149 | + | case n == 0: | |
| 150 | + | return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF) | |
| 151 | + | } | |
| 152 | + | p = p[n:] | |
| 153 | + | } | |
| 154 | + | return nil | |
| 155 | + | } | |
| @@ -0,0 +1,155 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "fmt" | |||
| 6 | + | "io" | |||
| 7 | + | "os" | |||
| 8 | + | ||||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Fixed layout of the xproto Setup reply (offsets from its status byte). | |||
| 13 | + | const ( | |||
| 14 | + | setupHeaderSize = 8 // status, pad/reason_len, protocol major, minor, length | |||
| 15 | + | setupFixedSize = 40 // fixed part of a successful Setup | |||
| 16 | + | screenFixedSize = 40 // fixed part of a SCREEN | |||
| 17 | + | formatSize = 8 // FORMAT | |||
| 18 | + | protocolMajor = 11 | |||
| 19 | + | protocolMinor = 0 | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | // setupInfo is what root keeps from a successful setup reply. Everything else | |||
| 23 | + | // is parsed by callers from Conn.SetupBytes. | |||
| 24 | + | type setupInfo struct { | |||
| 25 | + | xidBase, xidMask uint32 | |||
| 26 | + | root uint32 | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) } | |||
| 30 | + | ||||
| 31 | + | // encodeSetupRequest builds the little-endian connection setup request. | |||
| 32 | + | func encodeSetupRequest(authName string, authData []byte) []byte { | |||
| 33 | + | n, d := pad4(len(authName)), pad4(len(authData)) | |||
| 34 | + | p := make([]byte, 12+n+d) | |||
| 35 | + | p[0] = 'l' | |||
| 36 | + | binary.LittleEndian.PutUint16(p[2:], protocolMajor) | |||
| 37 | + | binary.LittleEndian.PutUint16(p[4:], protocolMinor) | |||
| 38 | + | binary.LittleEndian.PutUint16(p[6:], uint16(len(authName))) | |||
| 39 | + | binary.LittleEndian.PutUint16(p[8:], uint16(len(authData))) | |||
| 40 | + | copy(p[12:], authName) | |||
| 41 | + | copy(p[12+n:], authData) | |||
| 42 | + | return p | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func pad4(n int) int { return (n + 3) &^ 3 } | |||
| 46 | + | ||||
| 47 | + | // handshake runs the blocking setup exchange on fd and returns the full setup | |||
| 48 | + | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |||
| 49 | + | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |||
| 50 | + | if len(authName) > 0xffff || len(authData) > 0xffff { | |||
| 51 | + | return nil, setupInfo{}, ErrBadLength | |||
| 52 | + | } | |||
| 53 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |||
| 54 | + | return nil, setupInfo{}, err | |||
| 55 | + | } | |||
| 56 | + | var hdr [setupHeaderSize]byte | |||
| 57 | + | if err := readFull(fd, hdr[:]); err != nil { | |||
| 58 | + | return nil, setupInfo{}, err | |||
| 59 | + | } | |||
| 60 | + | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |||
| 61 | + | copy(reply, hdr[:]) | |||
| 62 | + | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |||
| 63 | + | return nil, setupInfo{}, err | |||
| 64 | + | } | |||
| 65 | + | info, err := parseSetup(reply) | |||
| 66 | + | if err != nil { | |||
| 67 | + | return nil, setupInfo{}, err | |||
| 68 | + | } | |||
| 69 | + | return reply, info, nil | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // parseSetup validates a complete setup reply (header length already | |||
| 73 | + | // honoured) and extracts what root needs. | |||
| 74 | + | func parseSetup(p []byte) (setupInfo, error) { | |||
| 75 | + | if len(p) < setupHeaderSize { | |||
| 76 | + | return setupInfo{}, malformed("short setup reply") | |||
| 77 | + | } | |||
| 78 | + | switch p[0] { | |||
| 79 | + | case setupStatusFailed: | |||
| 80 | + | // SetupFailed: reason_len at byte 1, reason after the header. | |||
| 81 | + | n := int(p[1]) | |||
| 82 | + | if setupHeaderSize+n > len(p) { | |||
| 83 | + | return setupInfo{}, malformed("setup failure reason overflows reply") | |||
| 84 | + | } | |||
| 85 | + | return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])} | |||
| 86 | + | case setupStatusAuthenticate: | |||
| 87 | + | // SetupAuthenticate: reason fills length*4 bytes, NUL padded. | |||
| 88 | + | reason := p[setupHeaderSize:] | |||
| 89 | + | for len(reason) > 0 && reason[len(reason)-1] == 0 { | |||
| 90 | + | reason = reason[:len(reason)-1] | |||
| 91 | + | } | |||
| 92 | + | return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)} | |||
| 93 | + | case setupStatusSuccess: | |||
| 94 | + | default: | |||
| 95 | + | return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0])) | |||
| 96 | + | } | |||
| 97 | + | if len(p) < setupFixedSize { | |||
| 98 | + | return setupInfo{}, malformed("short setup reply") | |||
| 99 | + | } | |||
| 100 | + | if binary.LittleEndian.Uint16(p[2:]) != protocolMajor { | |||
| 101 | + | return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:]))) | |||
| 102 | + | } | |||
| 103 | + | info := setupInfo{ | |||
| 104 | + | xidBase: binary.LittleEndian.Uint32(p[12:]), | |||
| 105 | + | xidMask: binary.LittleEndian.Uint32(p[16:]), | |||
| 106 | + | } | |||
| 107 | + | if info.xidMask == 0 || info.xidBase&info.xidMask != 0 { | |||
| 108 | + | return setupInfo{}, malformed("resource id base/mask") | |||
| 109 | + | } | |||
| 110 | + | if p[28] == 0 { | |||
| 111 | + | return setupInfo{}, malformed("setup has no screens") | |||
| 112 | + | } | |||
| 113 | + | off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29]) | |||
| 114 | + | if off+screenFixedSize > len(p) { | |||
| 115 | + | return setupInfo{}, malformed("first screen overflows setup reply") | |||
| 116 | + | } | |||
| 117 | + | info.root = binary.LittleEndian.Uint32(p[off:]) | |||
| 118 | + | return info, nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func writeFull(fd int, p []byte) error { | |||
| 122 | + | for len(p) > 0 { | |||
| 123 | + | n, err := unix.Write(fd, p) | |||
| 124 | + | switch { | |||
| 125 | + | case err == unix.EINTR: | |||
| 126 | + | continue | |||
| 127 | + | case err == unix.EPIPE || err == unix.ECONNRESET: | |||
| 128 | + | return ErrClosed | |||
| 129 | + | case err != nil: | |||
| 130 | + | return os.NewSyscallError("write", err) | |||
| 131 | + | case n <= 0: | |||
| 132 | + | return io.ErrShortWrite | |||
| 133 | + | } | |||
| 134 | + | p = p[n:] | |||
| 135 | + | } | |||
| 136 | + | return nil | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | func readFull(fd int, p []byte) error { | |||
| 140 | + | for len(p) > 0 { | |||
| 141 | + | n, err := unix.Read(fd, p) | |||
| 142 | + | switch { | |||
| 143 | + | case err == unix.EINTR: | |||
| 144 | + | continue | |||
| 145 | + | case err == unix.ECONNRESET: | |||
| 146 | + | return ErrClosed | |||
| 147 | + | case err != nil: | |||
| 148 | + | return os.NewSyscallError("read", err) | |||
| 149 | + | case n == 0: | |||
| 150 | + | return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF) | |||
| 151 | + | } | |||
| 152 | + | p = p[n:] | |||
| 153 | + | } | |||
| 154 | + | return nil | |||
| 155 | + | } | |||
A testdata/setup/Failed.setup.hex +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | # SetupFailed reply (status 0) captured from Xvfb -auth <file> for a client | |
| 2 | + | # sending no authorization. | |
| 3 | + | 00400b0000001000417574686f72697a6174696f6e2072657175697265642c20 | |
| 4 | + | 627574206e6f20617574686f72697a6174696f6e2070726f746f636f6c207370 | |
| 5 | + | 656369666965640a | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | # SetupFailed reply (status 0) captured from Xvfb -auth <file> for a client | |||
| 2 | + | # sending no authorization. | |||
| 3 | + | 00400b0000001000417574686f72697a6174696f6e2072657175697265642c20 | |||
| 4 | + | 627574206e6f20617574686f72697a6174696f6e2070726f746f636f6c207370 | |||
| 5 | + | 656369666965640a | |||
A testdata/setup/Success.setup.hex +214 -0
| @@ -0,0 +1,214 @@ | |||
| 1 | + | # Setup reply (status 1) captured from Xvfb -screen 0 800x600x24 -nolisten tcp -ac, | |
| 2 | + | # little-endian client, protocol 11.0, no authorization. Raw socket capture. | |
| 3 | + | # resource_id_base 0x00200000, mask 0x001fffff, first screen root 0x0000039f. | |
| 4 | + | 01000b0000008f06a1a5b80000002000ffff1f00000100001400ffff01060000 | |
| 5 | + | 202008ff0000000054686520582e4f726720466f756e646174696f6e01012000 | |
| 6 | + | 0000000004082000000000000808200000000000101020000000000018202000 | |
| 7 | + | 0000000020202000000000009f03000020000000ffffff000000000000000000 | |
| 8 | + | 20035802cb009800010001002100000001001806180000010000000021000000 | |
| 9 | + | 040800010000ff0000ff0000ff0000000000000022000000050800010000ff00 | |
| 10 | + | 00ff0000ff0000000000000091020000040800010000ff0000ff0000ff000000 | |
| 11 | + | 0000000092020000040800010000ff0000ff0000ff0000000000000093020000 | |
| 12 | + | 040800010000ff0000ff0000ff0000000000000094020000040800010000ff00 | |
| 13 | + | 00ff0000ff0000000000000095020000040800010000ff0000ff0000ff000000 | |
| 14 | + | 0000000096020000040800010000ff0000ff0000ff0000000000000097020000 | |
| 15 | + | 040800010000ff0000ff0000ff0000000000000098020000040800010000ff00 | |
| 16 | + | 00ff0000ff0000000000000099020000040800010000ff0000ff0000ff000000 | |
| 17 | + | 000000009a020000040800010000ff0000ff0000ff000000000000009b020000 | |
| 18 | + | 040800010000ff0000ff0000ff000000000000009c020000040800010000ff00 | |
| 19 | + | 00ff0000ff000000000000009d020000040800010000ff0000ff0000ff000000 | |
| 20 | + | 000000009e020000040800010000ff0000ff0000ff000000000000009f020000 | |
| 21 | + | 040800010000ff0000ff0000ff00000000000000a0020000040800010000ff00 | |
| 22 | + | 00ff0000ff00000000000000a1020000040800010000ff0000ff0000ff000000 | |
| 23 | + | 00000000a2020000040800010000ff0000ff0000ff00000000000000a3020000 | |
| 24 | + | 040800010000ff0000ff0000ff00000000000000a4020000040800010000ff00 | |
| 25 | + | 00ff0000ff00000000000000a5020000040800010000ff0000ff0000ff000000 | |
| 26 | + | 00000000a6020000040800010000ff0000ff0000ff00000000000000a7020000 | |
| 27 | + | 040800010000ff0000ff0000ff00000000000000a8020000040800010000ff00 | |
| 28 | + | 00ff0000ff00000000000000a9020000040800010000ff0000ff0000ff000000 | |
| 29 | + | 00000000aa020000040800010000ff0000ff0000ff00000000000000ab020000 | |
| 30 | + | 040800010000ff0000ff0000ff00000000000000ac020000040800010000ff00 | |
| 31 | + | 00ff0000ff00000000000000ad020000040800010000ff0000ff0000ff000000 | |
| 32 | + | 00000000ae020000040800010000ff0000ff0000ff00000000000000af020000 | |
| 33 | + | 040800010000ff0000ff0000ff00000000000000b0020000040800010000ff00 | |
| 34 | + | 00ff0000ff00000000000000b1020000040800010000ff0000ff0000ff000000 | |
| 35 | + | 00000000b2020000040800010000ff0000ff0000ff00000000000000b3020000 | |
| 36 | + | 040800010000ff0000ff0000ff00000000000000b4020000040800010000ff00 | |
| 37 | + | 00ff0000ff00000000000000b5020000040800010000ff0000ff0000ff000000 | |
| 38 | + | 00000000b6020000040800010000ff0000ff0000ff00000000000000b7020000 | |
| 39 | + | 040800010000ff0000ff0000ff00000000000000b8020000040800010000ff00 | |
| 40 | + | 00ff0000ff00000000000000b9020000040800010000ff0000ff0000ff000000 | |
| 41 | + | 00000000ba020000040800010000ff0000ff0000ff00000000000000bb020000 | |
| 42 | + | 040800010000ff0000ff0000ff00000000000000bc020000040800010000ff00 | |
| 43 | + | 00ff0000ff00000000000000bd020000040800010000ff0000ff0000ff000000 | |
| 44 | + | 00000000be020000040800010000ff0000ff0000ff00000000000000bf020000 | |
| 45 | + | 040800010000ff0000ff0000ff00000000000000c0020000040800010000ff00 | |
| 46 | + | 00ff0000ff00000000000000c1020000040800010000ff0000ff0000ff000000 | |
| 47 | + | 00000000c2020000040800010000ff0000ff0000ff00000000000000c3020000 | |
| 48 | + | 040800010000ff0000ff0000ff00000000000000c4020000040800010000ff00 | |
| 49 | + | 00ff0000ff00000000000000c5020000040800010000ff0000ff0000ff000000 | |
| 50 | + | 00000000c6020000040800010000ff0000ff0000ff00000000000000c7020000 | |
| 51 | + | 040800010000ff0000ff0000ff00000000000000c8020000040800010000ff00 | |
| 52 | + | 00ff0000ff00000000000000c9020000040800010000ff0000ff0000ff000000 | |
| 53 | + | 00000000ca020000040800010000ff0000ff0000ff00000000000000cb020000 | |
| 54 | + | 040800010000ff0000ff0000ff00000000000000cc020000040800010000ff00 | |
| 55 | + | 00ff0000ff00000000000000cd020000040800010000ff0000ff0000ff000000 | |
| 56 | + | 00000000ce020000040800010000ff0000ff0000ff00000000000000cf020000 | |
| 57 | + | 040800010000ff0000ff0000ff00000000000000d0020000040800010000ff00 | |
| 58 | + | 00ff0000ff00000000000000d1020000040800010000ff0000ff0000ff000000 | |
| 59 | + | 00000000d2020000040800010000ff0000ff0000ff00000000000000d3020000 | |
| 60 | + | 040800010000ff0000ff0000ff00000000000000d4020000040800010000ff00 | |
| 61 | + | 00ff0000ff00000000000000d5020000040800010000ff0000ff0000ff000000 | |
| 62 | + | 00000000d6020000040800010000ff0000ff0000ff00000000000000d7020000 | |
| 63 | + | 040800010000ff0000ff0000ff00000000000000d8020000040800010000ff00 | |
| 64 | + | 00ff0000ff00000000000000d9020000040800010000ff0000ff0000ff000000 | |
| 65 | + | 00000000da020000040800010000ff0000ff0000ff00000000000000db020000 | |
| 66 | + | 040800010000ff0000ff0000ff00000000000000dc020000040800010000ff00 | |
| 67 | + | 00ff0000ff00000000000000dd020000040800010000ff0000ff0000ff000000 | |
| 68 | + | 00000000de020000040800010000ff0000ff0000ff00000000000000df020000 | |
| 69 | + | 040800010000ff0000ff0000ff00000000000000e0020000040800010000ff00 | |
| 70 | + | 00ff0000ff00000000000000e1020000040800010000ff0000ff0000ff000000 | |
| 71 | + | 00000000e2020000040800010000ff0000ff0000ff00000000000000e3020000 | |
| 72 | + | 040800010000ff0000ff0000ff00000000000000e4020000040800010000ff00 | |
| 73 | + | 00ff0000ff00000000000000e5020000040800010000ff0000ff0000ff000000 | |
| 74 | + | 00000000e6020000040800010000ff0000ff0000ff00000000000000e7020000 | |
| 75 | + | 040800010000ff0000ff0000ff00000000000000e8020000040800010000ff00 | |
| 76 | + | 00ff0000ff00000000000000e9020000040800010000ff0000ff0000ff000000 | |
| 77 | + | 00000000ea020000040800010000ff0000ff0000ff00000000000000eb020000 | |
| 78 | + | 040800010000ff0000ff0000ff00000000000000ec020000040800010000ff00 | |
| 79 | + | 00ff0000ff00000000000000ed020000040800010000ff0000ff0000ff000000 | |
| 80 | + | 00000000ee020000040800010000ff0000ff0000ff00000000000000ef020000 | |
| 81 | + | 040800010000ff0000ff0000ff00000000000000f0020000040800010000ff00 | |
| 82 | + | 00ff0000ff00000000000000f1020000040800010000ff0000ff0000ff000000 | |
| 83 | + | 00000000f2020000040800010000ff0000ff0000ff00000000000000f3020000 | |
| 84 | + | 040800010000ff0000ff0000ff00000000000000f4020000040800010000ff00 | |
| 85 | + | 00ff0000ff00000000000000f5020000040800010000ff0000ff0000ff000000 | |
| 86 | + | 00000000f6020000040800010000ff0000ff0000ff00000000000000f7020000 | |
| 87 | + | 040800010000ff0000ff0000ff00000000000000f8020000040800010000ff00 | |
| 88 | + | 00ff0000ff00000000000000f9020000040800010000ff0000ff0000ff000000 | |
| 89 | + | 00000000fa020000040800010000ff0000ff0000ff00000000000000fb020000 | |
| 90 | + | 040800010000ff0000ff0000ff00000000000000fc020000040800010000ff00 | |
| 91 | + | 00ff0000ff00000000000000fd020000040800010000ff0000ff0000ff000000 | |
| 92 | + | 00000000fe020000040800010000ff0000ff0000ff00000000000000ff020000 | |
| 93 | + | 040800010000ff0000ff0000ff0000000000000000030000040800010000ff00 | |
| 94 | + | 00ff0000ff0000000000000001030000040800010000ff0000ff0000ff000000 | |
| 95 | + | 0000000002030000040800010000ff0000ff0000ff0000000000000003030000 | |
| 96 | + | 040800010000ff0000ff0000ff0000000000000004030000040800010000ff00 | |
| 97 | + | 00ff0000ff0000000000000005030000040800010000ff0000ff0000ff000000 | |
| 98 | + | 0000000006030000040800010000ff0000ff0000ff0000000000000007030000 | |
| 99 | + | 040800010000ff0000ff0000ff0000000000000008030000040800010000ff00 | |
| 100 | + | 00ff0000ff0000000000000009030000040800010000ff0000ff0000ff000000 | |
| 101 | + | 000000000a030000040800010000ff0000ff0000ff000000000000000b030000 | |
| 102 | + | 040800010000ff0000ff0000ff000000000000000c030000040800010000ff00 | |
| 103 | + | 00ff0000ff000000000000000d030000040800010000ff0000ff0000ff000000 | |
| 104 | + | 000000000e030000040800010000ff0000ff0000ff000000000000000f030000 | |
| 105 | + | 040800010000ff0000ff0000ff0000000000000010030000050800010000ff00 | |
| 106 | + | 00ff0000ff0000000000000011030000050800010000ff0000ff0000ff000000 | |
| 107 | + | 0000000012030000050800010000ff0000ff0000ff0000000000000013030000 | |
| 108 | + | 050800010000ff0000ff0000ff0000000000000014030000050800010000ff00 | |
| 109 | + | 00ff0000ff0000000000000015030000050800010000ff0000ff0000ff000000 | |
| 110 | + | 0000000016030000050800010000ff0000ff0000ff0000000000000017030000 | |
| 111 | + | 050800010000ff0000ff0000ff0000000000000018030000050800010000ff00 | |
| 112 | + | 00ff0000ff0000000000000019030000050800010000ff0000ff0000ff000000 | |
| 113 | + | 000000001a030000050800010000ff0000ff0000ff000000000000001b030000 | |
| 114 | + | 050800010000ff0000ff0000ff000000000000001c030000050800010000ff00 | |
| 115 | + | 00ff0000ff000000000000001d030000050800010000ff0000ff0000ff000000 | |
| 116 | + | 000000001e030000050800010000ff0000ff0000ff000000000000001f030000 | |
| 117 | + | 050800010000ff0000ff0000ff0000000000000020030000050800010000ff00 | |
| 118 | + | 00ff0000ff0000000000000021030000050800010000ff0000ff0000ff000000 | |
| 119 | + | 0000000022030000050800010000ff0000ff0000ff0000000000000023030000 | |
| 120 | + | 050800010000ff0000ff0000ff0000000000000024030000050800010000ff00 | |
| 121 | + | 00ff0000ff0000000000000025030000050800010000ff0000ff0000ff000000 | |
| 122 | + | 0000000026030000050800010000ff0000ff0000ff0000000000000027030000 | |
| 123 | + | 050800010000ff0000ff0000ff0000000000000028030000050800010000ff00 | |
| 124 | + | 00ff0000ff0000000000000029030000050800010000ff0000ff0000ff000000 | |
| 125 | + | 000000002a030000050800010000ff0000ff0000ff000000000000002b030000 | |
| 126 | + | 050800010000ff0000ff0000ff000000000000002c030000050800010000ff00 | |
| 127 | + | 00ff0000ff000000000000002d030000050800010000ff0000ff0000ff000000 | |
| 128 | + | 000000002e030000050800010000ff0000ff0000ff000000000000002f030000 | |
| 129 | + | 050800010000ff0000ff0000ff0000000000000030030000050800010000ff00 | |
| 130 | + | 00ff0000ff0000000000000031030000050800010000ff0000ff0000ff000000 | |
| 131 | + | 0000000032030000050800010000ff0000ff0000ff0000000000000033030000 | |
| 132 | + | 050800010000ff0000ff0000ff0000000000000034030000050800010000ff00 | |
| 133 | + | 00ff0000ff0000000000000035030000050800010000ff0000ff0000ff000000 | |
| 134 | + | 0000000036030000050800010000ff0000ff0000ff0000000000000037030000 | |
| 135 | + | 050800010000ff0000ff0000ff0000000000000038030000050800010000ff00 | |
| 136 | + | 00ff0000ff0000000000000039030000050800010000ff0000ff0000ff000000 | |
| 137 | + | 000000003a030000050800010000ff0000ff0000ff000000000000003b030000 | |
| 138 | + | 050800010000ff0000ff0000ff000000000000003c030000050800010000ff00 | |
| 139 | + | 00ff0000ff000000000000003d030000050800010000ff0000ff0000ff000000 | |
| 140 | + | 000000003e030000050800010000ff0000ff0000ff000000000000003f030000 | |
| 141 | + | 050800010000ff0000ff0000ff0000000000000040030000050800010000ff00 | |
| 142 | + | 00ff0000ff0000000000000041030000050800010000ff0000ff0000ff000000 | |
| 143 | + | 0000000042030000050800010000ff0000ff0000ff0000000000000043030000 | |
| 144 | + | 050800010000ff0000ff0000ff0000000000000044030000050800010000ff00 | |
| 145 | + | 00ff0000ff0000000000000045030000050800010000ff0000ff0000ff000000 | |
| 146 | + | 0000000046030000050800010000ff0000ff0000ff0000000000000047030000 | |
| 147 | + | 050800010000ff0000ff0000ff0000000000000048030000050800010000ff00 | |
| 148 | + | 00ff0000ff0000000000000049030000050800010000ff0000ff0000ff000000 | |
| 149 | + | 000000004a030000050800010000ff0000ff0000ff000000000000004b030000 | |
| 150 | + | 050800010000ff0000ff0000ff000000000000004c030000050800010000ff00 | |
| 151 | + | 00ff0000ff000000000000004d030000050800010000ff0000ff0000ff000000 | |
| 152 | + | 000000004e030000050800010000ff0000ff0000ff000000000000004f030000 | |
| 153 | + | 050800010000ff0000ff0000ff0000000000000050030000050800010000ff00 | |
| 154 | + | 00ff0000ff0000000000000051030000050800010000ff0000ff0000ff000000 | |
| 155 | + | 0000000052030000050800010000ff0000ff0000ff0000000000000053030000 | |
| 156 | + | 050800010000ff0000ff0000ff0000000000000054030000050800010000ff00 | |
| 157 | + | 00ff0000ff0000000000000055030000050800010000ff0000ff0000ff000000 | |
| 158 | + | 0000000056030000050800010000ff0000ff0000ff0000000000000057030000 | |
| 159 | + | 050800010000ff0000ff0000ff0000000000000058030000050800010000ff00 | |
| 160 | + | 00ff0000ff0000000000000059030000050800010000ff0000ff0000ff000000 | |
| 161 | + | 000000005a030000050800010000ff0000ff0000ff000000000000005b030000 | |
| 162 | + | 050800010000ff0000ff0000ff000000000000005c030000050800010000ff00 | |
| 163 | + | 00ff0000ff000000000000005d030000050800010000ff0000ff0000ff000000 | |
| 164 | + | 000000005e030000050800010000ff0000ff0000ff000000000000005f030000 | |
| 165 | + | 050800010000ff0000ff0000ff0000000000000060030000050800010000ff00 | |
| 166 | + | 00ff0000ff0000000000000061030000050800010000ff0000ff0000ff000000 | |
| 167 | + | 0000000062030000050800010000ff0000ff0000ff0000000000000063030000 | |
| 168 | + | 050800010000ff0000ff0000ff0000000000000064030000050800010000ff00 | |
| 169 | + | 00ff0000ff0000000000000065030000050800010000ff0000ff0000ff000000 | |
| 170 | + | 0000000066030000050800010000ff0000ff0000ff0000000000000067030000 | |
| 171 | + | 050800010000ff0000ff0000ff0000000000000068030000050800010000ff00 | |
| 172 | + | 00ff0000ff0000000000000069030000050800010000ff0000ff0000ff000000 | |
| 173 | + | 000000006a030000050800010000ff0000ff0000ff000000000000006b030000 | |
| 174 | + | 050800010000ff0000ff0000ff000000000000006c030000050800010000ff00 | |
| 175 | + | 00ff0000ff000000000000006d030000050800010000ff0000ff0000ff000000 | |
| 176 | + | 000000006e030000050800010000ff0000ff0000ff000000000000006f030000 | |
| 177 | + | 050800010000ff0000ff0000ff0000000000000070030000050800010000ff00 | |
| 178 | + | 00ff0000ff0000000000000071030000050800010000ff0000ff0000ff000000 | |
| 179 | + | 0000000072030000050800010000ff0000ff0000ff0000000000000073030000 | |
| 180 | + | 050800010000ff0000ff0000ff0000000000000074030000050800010000ff00 | |
| 181 | + | 00ff0000ff0000000000000075030000050800010000ff0000ff0000ff000000 | |
| 182 | + | 0000000076030000050800010000ff0000ff0000ff0000000000000077030000 | |
| 183 | + | 050800010000ff0000ff0000ff0000000000000078030000050800010000ff00 | |
| 184 | + | 00ff0000ff0000000000000079030000050800010000ff0000ff0000ff000000 | |
| 185 | + | 000000007a030000050800010000ff0000ff0000ff000000000000007b030000 | |
| 186 | + | 050800010000ff0000ff0000ff000000000000007c030000050800010000ff00 | |
| 187 | + | 00ff0000ff000000000000007d030000050800010000ff0000ff0000ff000000 | |
| 188 | + | 000000007e030000050800010000ff0000ff0000ff000000000000007f030000 | |
| 189 | + | 050800010000ff0000ff0000ff0000000000000080030000050800010000ff00 | |
| 190 | + | 00ff0000ff0000000000000081030000050800010000ff0000ff0000ff000000 | |
| 191 | + | 0000000082030000050800010000ff0000ff0000ff0000000000000083030000 | |
| 192 | + | 050800010000ff0000ff0000ff0000000000000084030000050800010000ff00 | |
| 193 | + | 00ff0000ff0000000000000085030000050800010000ff0000ff0000ff000000 | |
| 194 | + | 0000000086030000050800010000ff0000ff0000ff0000000000000087030000 | |
| 195 | + | 050800010000ff0000ff0000ff0000000000000088030000050800010000ff00 | |
| 196 | + | 00ff0000ff0000000000000089030000050800010000ff0000ff0000ff000000 | |
| 197 | + | 000000008a030000050800010000ff0000ff0000ff000000000000008b030000 | |
| 198 | + | 050800010000ff0000ff0000ff000000000000008c030000050800010000ff00 | |
| 199 | + | 00ff0000ff000000000000008d030000050800010000ff0000ff0000ff000000 | |
| 200 | + | 000000008e030000050800010000ff0000ff0000ff0000000000000001000000 | |
| 201 | + | 0000000004000000000000000800000000000000100000000000000020001000 | |
| 202 | + | 0000000040000000040800010000ff0000ff0000ff000000000000008f030000 | |
| 203 | + | 040800010000ff0000ff0000ff0000000000000090030000040800010000ff00 | |
| 204 | + | 00ff0000ff0000000000000091030000040800010000ff0000ff0000ff000000 | |
| 205 | + | 0000000092030000040800010000ff0000ff0000ff0000000000000093030000 | |
| 206 | + | 040800010000ff0000ff0000ff0000000000000094030000040800010000ff00 | |
| 207 | + | 00ff0000ff0000000000000095030000040800010000ff0000ff0000ff000000 | |
| 208 | + | 0000000096030000040800010000ff0000ff0000ff0000000000000097030000 | |
| 209 | + | 040800010000ff0000ff0000ff0000000000000098030000040800010000ff00 | |
| 210 | + | 00ff0000ff0000000000000099030000040800010000ff0000ff0000ff000000 | |
| 211 | + | 000000009a030000040800010000ff0000ff0000ff000000000000009b030000 | |
| 212 | + | 040800010000ff0000ff0000ff000000000000009c030000040800010000ff00 | |
| 213 | + | 00ff0000ff000000000000009d030000040800010000ff0000ff0000ff000000 | |
| 214 | + | 00000000 | |
| @@ -0,0 +1,214 @@ | |||||
| 1 | + | # Setup reply (status 1) captured from Xvfb -screen 0 800x600x24 -nolisten tcp -ac, | |||
| 2 | + | # little-endian client, protocol 11.0, no authorization. Raw socket capture. | |||
| 3 | + | # resource_id_base 0x00200000, mask 0x001fffff, first screen root 0x0000039f. | |||
| 4 | + | 01000b0000008f06a1a5b80000002000ffff1f00000100001400ffff01060000 | |||
| 5 | + | 202008ff0000000054686520582e4f726720466f756e646174696f6e01012000 | |||
| 6 | + | 0000000004082000000000000808200000000000101020000000000018202000 | |||
| 7 | + | 0000000020202000000000009f03000020000000ffffff000000000000000000 | |||
| 8 | + | 20035802cb009800010001002100000001001806180000010000000021000000 | |||
| 9 | + | 040800010000ff0000ff0000ff0000000000000022000000050800010000ff00 | |||
| 10 | + | 00ff0000ff0000000000000091020000040800010000ff0000ff0000ff000000 | |||
| 11 | + | 0000000092020000040800010000ff0000ff0000ff0000000000000093020000 | |||
| 12 | + | 040800010000ff0000ff0000ff0000000000000094020000040800010000ff00 | |||
| 13 | + | 00ff0000ff0000000000000095020000040800010000ff0000ff0000ff000000 | |||
| 14 | + | 0000000096020000040800010000ff0000ff0000ff0000000000000097020000 | |||
| 15 | + | 040800010000ff0000ff0000ff0000000000000098020000040800010000ff00 | |||
| 16 | + | 00ff0000ff0000000000000099020000040800010000ff0000ff0000ff000000 | |||
| 17 | + | 000000009a020000040800010000ff0000ff0000ff000000000000009b020000 | |||
| 18 | + | 040800010000ff0000ff0000ff000000000000009c020000040800010000ff00 | |||
| 19 | + | 00ff0000ff000000000000009d020000040800010000ff0000ff0000ff000000 | |||
| 20 | + | 000000009e020000040800010000ff0000ff0000ff000000000000009f020000 | |||
| 21 | + | 040800010000ff0000ff0000ff00000000000000a0020000040800010000ff00 | |||
| 22 | + | 00ff0000ff00000000000000a1020000040800010000ff0000ff0000ff000000 | |||
| 23 | + | 00000000a2020000040800010000ff0000ff0000ff00000000000000a3020000 | |||
| 24 | + | 040800010000ff0000ff0000ff00000000000000a4020000040800010000ff00 | |||
| 25 | + | 00ff0000ff00000000000000a5020000040800010000ff0000ff0000ff000000 | |||
| 26 | + | 00000000a6020000040800010000ff0000ff0000ff00000000000000a7020000 | |||
| 27 | + | 040800010000ff0000ff0000ff00000000000000a8020000040800010000ff00 | |||
| 28 | + | 00ff0000ff00000000000000a9020000040800010000ff0000ff0000ff000000 | |||
| 29 | + | 00000000aa020000040800010000ff0000ff0000ff00000000000000ab020000 | |||
| 30 | + | 040800010000ff0000ff0000ff00000000000000ac020000040800010000ff00 | |||
| 31 | + | 00ff0000ff00000000000000ad020000040800010000ff0000ff0000ff000000 | |||
| 32 | + | 00000000ae020000040800010000ff0000ff0000ff00000000000000af020000 | |||
| 33 | + | 040800010000ff0000ff0000ff00000000000000b0020000040800010000ff00 | |||
| 34 | + | 00ff0000ff00000000000000b1020000040800010000ff0000ff0000ff000000 | |||
| 35 | + | 00000000b2020000040800010000ff0000ff0000ff00000000000000b3020000 | |||
| 36 | + | 040800010000ff0000ff0000ff00000000000000b4020000040800010000ff00 | |||
| 37 | + | 00ff0000ff00000000000000b5020000040800010000ff0000ff0000ff000000 | |||
| 38 | + | 00000000b6020000040800010000ff0000ff0000ff00000000000000b7020000 | |||
| 39 | + | 040800010000ff0000ff0000ff00000000000000b8020000040800010000ff00 | |||
| 40 | + | 00ff0000ff00000000000000b9020000040800010000ff0000ff0000ff000000 | |||
| 41 | + | 00000000ba020000040800010000ff0000ff0000ff00000000000000bb020000 | |||
| 42 | + | 040800010000ff0000ff0000ff00000000000000bc020000040800010000ff00 | |||
| 43 | + | 00ff0000ff00000000000000bd020000040800010000ff0000ff0000ff000000 | |||
| 44 | + | 00000000be020000040800010000ff0000ff0000ff00000000000000bf020000 | |||
| 45 | + | 040800010000ff0000ff0000ff00000000000000c0020000040800010000ff00 | |||
| 46 | + | 00ff0000ff00000000000000c1020000040800010000ff0000ff0000ff000000 | |||
| 47 | + | 00000000c2020000040800010000ff0000ff0000ff00000000000000c3020000 | |||
| 48 | + | 040800010000ff0000ff0000ff00000000000000c4020000040800010000ff00 | |||
| 49 | + | 00ff0000ff00000000000000c5020000040800010000ff0000ff0000ff000000 | |||
| 50 | + | 00000000c6020000040800010000ff0000ff0000ff00000000000000c7020000 | |||
| 51 | + | 040800010000ff0000ff0000ff00000000000000c8020000040800010000ff00 | |||
| 52 | + | 00ff0000ff00000000000000c9020000040800010000ff0000ff0000ff000000 | |||
| 53 | + | 00000000ca020000040800010000ff0000ff0000ff00000000000000cb020000 | |||
| 54 | + | 040800010000ff0000ff0000ff00000000000000cc020000040800010000ff00 | |||
| 55 | + | 00ff0000ff00000000000000cd020000040800010000ff0000ff0000ff000000 | |||
| 56 | + | 00000000ce020000040800010000ff0000ff0000ff00000000000000cf020000 | |||
| 57 | + | 040800010000ff0000ff0000ff00000000000000d0020000040800010000ff00 | |||
| 58 | + | 00ff0000ff00000000000000d1020000040800010000ff0000ff0000ff000000 | |||
| 59 | + | 00000000d2020000040800010000ff0000ff0000ff00000000000000d3020000 | |||
| 60 | + | 040800010000ff0000ff0000ff00000000000000d4020000040800010000ff00 | |||
| 61 | + | 00ff0000ff00000000000000d5020000040800010000ff0000ff0000ff000000 | |||
| 62 | + | 00000000d6020000040800010000ff0000ff0000ff00000000000000d7020000 | |||
| 63 | + | 040800010000ff0000ff0000ff00000000000000d8020000040800010000ff00 | |||
| 64 | + | 00ff0000ff00000000000000d9020000040800010000ff0000ff0000ff000000 | |||
| 65 | + | 00000000da020000040800010000ff0000ff0000ff00000000000000db020000 | |||
| 66 | + | 040800010000ff0000ff0000ff00000000000000dc020000040800010000ff00 | |||
| 67 | + | 00ff0000ff00000000000000dd020000040800010000ff0000ff0000ff000000 | |||
| 68 | + | 00000000de020000040800010000ff0000ff0000ff00000000000000df020000 | |||
| 69 | + | 040800010000ff0000ff0000ff00000000000000e0020000040800010000ff00 | |||
| 70 | + | 00ff0000ff00000000000000e1020000040800010000ff0000ff0000ff000000 | |||
| 71 | + | 00000000e2020000040800010000ff0000ff0000ff00000000000000e3020000 | |||
| 72 | + | 040800010000ff0000ff0000ff00000000000000e4020000040800010000ff00 | |||
| 73 | + | 00ff0000ff00000000000000e5020000040800010000ff0000ff0000ff000000 | |||
| 74 | + | 00000000e6020000040800010000ff0000ff0000ff00000000000000e7020000 | |||
| 75 | + | 040800010000ff0000ff0000ff00000000000000e8020000040800010000ff00 | |||
| 76 | + | 00ff0000ff00000000000000e9020000040800010000ff0000ff0000ff000000 | |||
| 77 | + | 00000000ea020000040800010000ff0000ff0000ff00000000000000eb020000 | |||
| 78 | + | 040800010000ff0000ff0000ff00000000000000ec020000040800010000ff00 | |||
| 79 | + | 00ff0000ff00000000000000ed020000040800010000ff0000ff0000ff000000 | |||
| 80 | + | 00000000ee020000040800010000ff0000ff0000ff00000000000000ef020000 | |||
| 81 | + | 040800010000ff0000ff0000ff00000000000000f0020000040800010000ff00 | |||
| 82 | + | 00ff0000ff00000000000000f1020000040800010000ff0000ff0000ff000000 | |||
| 83 | + | 00000000f2020000040800010000ff0000ff0000ff00000000000000f3020000 | |||
| 84 | + | 040800010000ff0000ff0000ff00000000000000f4020000040800010000ff00 | |||
| 85 | + | 00ff0000ff00000000000000f5020000040800010000ff0000ff0000ff000000 | |||
| 86 | + | 00000000f6020000040800010000ff0000ff0000ff00000000000000f7020000 | |||
| 87 | + | 040800010000ff0000ff0000ff00000000000000f8020000040800010000ff00 | |||
| 88 | + | 00ff0000ff00000000000000f9020000040800010000ff0000ff0000ff000000 | |||
| 89 | + | 00000000fa020000040800010000ff0000ff0000ff00000000000000fb020000 | |||
| 90 | + | 040800010000ff0000ff0000ff00000000000000fc020000040800010000ff00 | |||
| 91 | + | 00ff0000ff00000000000000fd020000040800010000ff0000ff0000ff000000 | |||
| 92 | + | 00000000fe020000040800010000ff0000ff0000ff00000000000000ff020000 | |||
| 93 | + | 040800010000ff0000ff0000ff0000000000000000030000040800010000ff00 | |||
| 94 | + | 00ff0000ff0000000000000001030000040800010000ff0000ff0000ff000000 | |||
| 95 | + | 0000000002030000040800010000ff0000ff0000ff0000000000000003030000 | |||
| 96 | + | 040800010000ff0000ff0000ff0000000000000004030000040800010000ff00 | |||
| 97 | + | 00ff0000ff0000000000000005030000040800010000ff0000ff0000ff000000 | |||
| 98 | + | 0000000006030000040800010000ff0000ff0000ff0000000000000007030000 | |||
| 99 | + | 040800010000ff0000ff0000ff0000000000000008030000040800010000ff00 | |||
| 100 | + | 00ff0000ff0000000000000009030000040800010000ff0000ff0000ff000000 | |||
| 101 | + | 000000000a030000040800010000ff0000ff0000ff000000000000000b030000 | |||
| 102 | + | 040800010000ff0000ff0000ff000000000000000c030000040800010000ff00 | |||
| 103 | + | 00ff0000ff000000000000000d030000040800010000ff0000ff0000ff000000 | |||
| 104 | + | 000000000e030000040800010000ff0000ff0000ff000000000000000f030000 | |||
| 105 | + | 040800010000ff0000ff0000ff0000000000000010030000050800010000ff00 | |||
| 106 | + | 00ff0000ff0000000000000011030000050800010000ff0000ff0000ff000000 | |||
| 107 | + | 0000000012030000050800010000ff0000ff0000ff0000000000000013030000 | |||
| 108 | + | 050800010000ff0000ff0000ff0000000000000014030000050800010000ff00 | |||
| 109 | + | 00ff0000ff0000000000000015030000050800010000ff0000ff0000ff000000 | |||
| 110 | + | 0000000016030000050800010000ff0000ff0000ff0000000000000017030000 | |||
| 111 | + | 050800010000ff0000ff0000ff0000000000000018030000050800010000ff00 | |||
| 112 | + | 00ff0000ff0000000000000019030000050800010000ff0000ff0000ff000000 | |||
| 113 | + | 000000001a030000050800010000ff0000ff0000ff000000000000001b030000 | |||
| 114 | + | 050800010000ff0000ff0000ff000000000000001c030000050800010000ff00 | |||
| 115 | + | 00ff0000ff000000000000001d030000050800010000ff0000ff0000ff000000 | |||
| 116 | + | 000000001e030000050800010000ff0000ff0000ff000000000000001f030000 | |||
| 117 | + | 050800010000ff0000ff0000ff0000000000000020030000050800010000ff00 | |||
| 118 | + | 00ff0000ff0000000000000021030000050800010000ff0000ff0000ff000000 | |||
| 119 | + | 0000000022030000050800010000ff0000ff0000ff0000000000000023030000 | |||
| 120 | + | 050800010000ff0000ff0000ff0000000000000024030000050800010000ff00 | |||
| 121 | + | 00ff0000ff0000000000000025030000050800010000ff0000ff0000ff000000 | |||
| 122 | + | 0000000026030000050800010000ff0000ff0000ff0000000000000027030000 | |||
| 123 | + | 050800010000ff0000ff0000ff0000000000000028030000050800010000ff00 | |||
| 124 | + | 00ff0000ff0000000000000029030000050800010000ff0000ff0000ff000000 | |||
| 125 | + | 000000002a030000050800010000ff0000ff0000ff000000000000002b030000 | |||
| 126 | + | 050800010000ff0000ff0000ff000000000000002c030000050800010000ff00 | |||
| 127 | + | 00ff0000ff000000000000002d030000050800010000ff0000ff0000ff000000 | |||
| 128 | + | 000000002e030000050800010000ff0000ff0000ff000000000000002f030000 | |||
| 129 | + | 050800010000ff0000ff0000ff0000000000000030030000050800010000ff00 | |||
| 130 | + | 00ff0000ff0000000000000031030000050800010000ff0000ff0000ff000000 | |||
| 131 | + | 0000000032030000050800010000ff0000ff0000ff0000000000000033030000 | |||
| 132 | + | 050800010000ff0000ff0000ff0000000000000034030000050800010000ff00 | |||
| 133 | + | 00ff0000ff0000000000000035030000050800010000ff0000ff0000ff000000 | |||
| 134 | + | 0000000036030000050800010000ff0000ff0000ff0000000000000037030000 | |||
| 135 | + | 050800010000ff0000ff0000ff0000000000000038030000050800010000ff00 | |||
| 136 | + | 00ff0000ff0000000000000039030000050800010000ff0000ff0000ff000000 | |||
| 137 | + | 000000003a030000050800010000ff0000ff0000ff000000000000003b030000 | |||
| 138 | + | 050800010000ff0000ff0000ff000000000000003c030000050800010000ff00 | |||
| 139 | + | 00ff0000ff000000000000003d030000050800010000ff0000ff0000ff000000 | |||
| 140 | + | 000000003e030000050800010000ff0000ff0000ff000000000000003f030000 | |||
| 141 | + | 050800010000ff0000ff0000ff0000000000000040030000050800010000ff00 | |||
| 142 | + | 00ff0000ff0000000000000041030000050800010000ff0000ff0000ff000000 | |||
| 143 | + | 0000000042030000050800010000ff0000ff0000ff0000000000000043030000 | |||
| 144 | + | 050800010000ff0000ff0000ff0000000000000044030000050800010000ff00 | |||
| 145 | + | 00ff0000ff0000000000000045030000050800010000ff0000ff0000ff000000 | |||
| 146 | + | 0000000046030000050800010000ff0000ff0000ff0000000000000047030000 | |||
| 147 | + | 050800010000ff0000ff0000ff0000000000000048030000050800010000ff00 | |||
| 148 | + | 00ff0000ff0000000000000049030000050800010000ff0000ff0000ff000000 | |||
| 149 | + | 000000004a030000050800010000ff0000ff0000ff000000000000004b030000 | |||
| 150 | + | 050800010000ff0000ff0000ff000000000000004c030000050800010000ff00 | |||
| 151 | + | 00ff0000ff000000000000004d030000050800010000ff0000ff0000ff000000 | |||
| 152 | + | 000000004e030000050800010000ff0000ff0000ff000000000000004f030000 | |||
| 153 | + | 050800010000ff0000ff0000ff0000000000000050030000050800010000ff00 | |||
| 154 | + | 00ff0000ff0000000000000051030000050800010000ff0000ff0000ff000000 | |||
| 155 | + | 0000000052030000050800010000ff0000ff0000ff0000000000000053030000 | |||
| 156 | + | 050800010000ff0000ff0000ff0000000000000054030000050800010000ff00 | |||
| 157 | + | 00ff0000ff0000000000000055030000050800010000ff0000ff0000ff000000 | |||
| 158 | + | 0000000056030000050800010000ff0000ff0000ff0000000000000057030000 | |||
| 159 | + | 050800010000ff0000ff0000ff0000000000000058030000050800010000ff00 | |||
| 160 | + | 00ff0000ff0000000000000059030000050800010000ff0000ff0000ff000000 | |||
| 161 | + | 000000005a030000050800010000ff0000ff0000ff000000000000005b030000 | |||
| 162 | + | 050800010000ff0000ff0000ff000000000000005c030000050800010000ff00 | |||
| 163 | + | 00ff0000ff000000000000005d030000050800010000ff0000ff0000ff000000 | |||
| 164 | + | 000000005e030000050800010000ff0000ff0000ff000000000000005f030000 | |||
| 165 | + | 050800010000ff0000ff0000ff0000000000000060030000050800010000ff00 | |||
| 166 | + | 00ff0000ff0000000000000061030000050800010000ff0000ff0000ff000000 | |||
| 167 | + | 0000000062030000050800010000ff0000ff0000ff0000000000000063030000 | |||
| 168 | + | 050800010000ff0000ff0000ff0000000000000064030000050800010000ff00 | |||
| 169 | + | 00ff0000ff0000000000000065030000050800010000ff0000ff0000ff000000 | |||
| 170 | + | 0000000066030000050800010000ff0000ff0000ff0000000000000067030000 | |||
| 171 | + | 050800010000ff0000ff0000ff0000000000000068030000050800010000ff00 | |||
| 172 | + | 00ff0000ff0000000000000069030000050800010000ff0000ff0000ff000000 | |||
| 173 | + | 000000006a030000050800010000ff0000ff0000ff000000000000006b030000 | |||
| 174 | + | 050800010000ff0000ff0000ff000000000000006c030000050800010000ff00 | |||
| 175 | + | 00ff0000ff000000000000006d030000050800010000ff0000ff0000ff000000 | |||
| 176 | + | 000000006e030000050800010000ff0000ff0000ff000000000000006f030000 | |||
| 177 | + | 050800010000ff0000ff0000ff0000000000000070030000050800010000ff00 | |||
| 178 | + | 00ff0000ff0000000000000071030000050800010000ff0000ff0000ff000000 | |||
| 179 | + | 0000000072030000050800010000ff0000ff0000ff0000000000000073030000 | |||
| 180 | + | 050800010000ff0000ff0000ff0000000000000074030000050800010000ff00 | |||
| 181 | + | 00ff0000ff0000000000000075030000050800010000ff0000ff0000ff000000 | |||
| 182 | + | 0000000076030000050800010000ff0000ff0000ff0000000000000077030000 | |||
| 183 | + | 050800010000ff0000ff0000ff0000000000000078030000050800010000ff00 | |||
| 184 | + | 00ff0000ff0000000000000079030000050800010000ff0000ff0000ff000000 | |||
| 185 | + | 000000007a030000050800010000ff0000ff0000ff000000000000007b030000 | |||
| 186 | + | 050800010000ff0000ff0000ff000000000000007c030000050800010000ff00 | |||
| 187 | + | 00ff0000ff000000000000007d030000050800010000ff0000ff0000ff000000 | |||
| 188 | + | 000000007e030000050800010000ff0000ff0000ff000000000000007f030000 | |||
| 189 | + | 050800010000ff0000ff0000ff0000000000000080030000050800010000ff00 | |||
| 190 | + | 00ff0000ff0000000000000081030000050800010000ff0000ff0000ff000000 | |||
| 191 | + | 0000000082030000050800010000ff0000ff0000ff0000000000000083030000 | |||
| 192 | + | 050800010000ff0000ff0000ff0000000000000084030000050800010000ff00 | |||
| 193 | + | 00ff0000ff0000000000000085030000050800010000ff0000ff0000ff000000 | |||
| 194 | + | 0000000086030000050800010000ff0000ff0000ff0000000000000087030000 | |||
| 195 | + | 050800010000ff0000ff0000ff0000000000000088030000050800010000ff00 | |||
| 196 | + | 00ff0000ff0000000000000089030000050800010000ff0000ff0000ff000000 | |||
| 197 | + | 000000008a030000050800010000ff0000ff0000ff000000000000008b030000 | |||
| 198 | + | 050800010000ff0000ff0000ff000000000000008c030000050800010000ff00 | |||
| 199 | + | 00ff0000ff000000000000008d030000050800010000ff0000ff0000ff000000 | |||
| 200 | + | 000000008e030000050800010000ff0000ff0000ff0000000000000001000000 | |||
| 201 | + | 0000000004000000000000000800000000000000100000000000000020001000 | |||
| 202 | + | 0000000040000000040800010000ff0000ff0000ff000000000000008f030000 | |||
| 203 | + | 040800010000ff0000ff0000ff0000000000000090030000040800010000ff00 | |||
| 204 | + | 00ff0000ff0000000000000091030000040800010000ff0000ff0000ff000000 | |||
| 205 | + | 0000000092030000040800010000ff0000ff0000ff0000000000000093030000 | |||
| 206 | + | 040800010000ff0000ff0000ff0000000000000094030000040800010000ff00 | |||
| 207 | + | 00ff0000ff0000000000000095030000040800010000ff0000ff0000ff000000 | |||
| 208 | + | 0000000096030000040800010000ff0000ff0000ff0000000000000097030000 | |||
| 209 | + | 040800010000ff0000ff0000ff0000000000000098030000040800010000ff00 | |||
| 210 | + | 00ff0000ff0000000000000099030000040800010000ff0000ff0000ff000000 | |||
| 211 | + | 000000009a030000040800010000ff0000ff0000ff000000000000009b030000 | |||
| 212 | + | 040800010000ff0000ff0000ff000000000000009c030000040800010000ff00 | |||
| 213 | + | 00ff0000ff000000000000009d030000040800010000ff0000ff0000ff000000 | |||
| 214 | + | 00000000 | |||