Merge 5f9aee7852c50d4df9eaddf46f0ffa36d8527d47 into 722264ac773fc5df4f73671ee4fcce79537e93a6
10 files · +1246 -1
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 +272 -0
| @@ -0,0 +1,272 @@ | |||
| 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 | + | // Walk the recorded reply to find the first screen and its first depth | |
| 165 | + | // that carries visuals; every extent is a multiple of 4 bytes. | |
| 166 | + | screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29]) | |
| 167 | + | visualDepth := screen + screenFixedSize | |
| 168 | + | for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 { | |
| 169 | + | visualDepth += depthFixedSize | |
| 170 | + | } | |
| 171 | + | // Screen header present, its allowed depths cut off. | |
| 172 | + | missingDepths := truncateSetup(setup, screen+screenFixedSize) | |
| 173 | + | // A depth header announcing visuals, the visuals cut off. | |
| 174 | + | missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize) | |
| 175 | + | // One visual of the depth present, the rest cut off. | |
| 176 | + | partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize) | |
| 177 | + | // Complete first screen, a second screen declared but absent. | |
| 178 | + | missingScreen := bytes.Clone(setup) | |
| 179 | + | missingScreen[28] = 2 | |
| 180 | + | // Header claims 8 bytes of body, failure reason claims 20. | |
| 181 | + | reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} | |
| 182 | + | // Success header whose length is shorter than the fixed Setup. | |
| 183 | + | shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0} | |
| 184 | + | for _, tc := range []struct { | |
| 185 | + | name string | |
| 186 | + | reply []byte | |
| 187 | + | }{ | |
| 188 | + | {"empty", nil}, | |
| 189 | + | {"truncated header", setup[:5]}, | |
| 190 | + | {"truncated body", setup[:len(setup)-3]}, | |
| 191 | + | {"no screens", noScreens}, | |
| 192 | + | {"bad xid mask", badMask}, | |
| 193 | + | {"protocol major", badVersion}, | |
| 194 | + | {"unknown status", badStatus}, | |
| 195 | + | {"screen past end", screenPast}, | |
| 196 | + | {"missing depths", missingDepths}, | |
| 197 | + | {"missing visuals", missingVisuals}, | |
| 198 | + | {"partial visuals", partialVisuals}, | |
| 199 | + | {"missing subsequent screen", missingScreen}, | |
| 200 | + | {"reason past end", reasonPast}, | |
| 201 | + | {"short success", shortSuccess}, | |
| 202 | + | } { | |
| 203 | + | t.Run(tc.name, func(t *testing.T) { | |
| 204 | + | fd, _ := scriptedServer(t, tc.reply, 3, true) | |
| 205 | + | c, err := NewConn(fd) | |
| 206 | + | if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) { | |
| 207 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |
| 208 | + | } | |
| 209 | + | }) | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and | |
| 214 | + | // rewrites its additional-data length to match. | |
| 215 | + | func truncateSetup(setup []byte, n int) []byte { | |
| 216 | + | p := bytes.Clone(setup[:n]) | |
| 217 | + | binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4)) | |
| 218 | + | return p | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | func TestConnFlushClose(t *testing.T) { | |
| 222 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 223 | + | if err != nil { | |
| 224 | + | t.Fatal(err) | |
| 225 | + | } | |
| 226 | + | server := os.NewFile(uintptr(fds[1]), "server") | |
| 227 | + | defer server.Close() | |
| 228 | + | setup := loadSetup(t, "Success") | |
| 229 | + | go func() { | |
| 230 | + | var req [12]byte | |
| 231 | + | _, _ = io.ReadFull(server, req[:]) | |
| 232 | + | _, _ = server.Write(setup) | |
| 233 | + | }() | |
| 234 | + | c, err := NewConn(fds[0]) | |
| 235 | + | if err != nil { | |
| 236 | + | t.Fatal(err) | |
| 237 | + | } | |
| 238 | + | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | |
| 239 | + | if p == nil || seq != 1 { | |
| 240 | + | t.Fatal("request") | |
| 241 | + | } | |
| 242 | + | if err := c.Flush(); err != nil { | |
| 243 | + | t.Fatal(err) | |
| 244 | + | } | |
| 245 | + | var got [4]byte | |
| 246 | + | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | |
| 247 | + | t.Fatalf("%x %v", got, err) | |
| 248 | + | } | |
| 249 | + | if err := c.Close(); err != nil { | |
| 250 | + | t.Fatal(err) | |
| 251 | + | } | |
| 252 | + | if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) { | |
| 253 | + | t.Fatal("after Close") | |
| 254 | + | } | |
| 255 | + | if p, _ := c.Buf().Request(43, 0, 0); p != nil { | |
| 256 | + | t.Fatal("Buf usable after Close") | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func TestSetupErrorMessage(t *testing.T) { | |
| 261 | + | for _, tc := range []struct { | |
| 262 | + | e SetupError | |
| 263 | + | want string | |
| 264 | + | }{ | |
| 265 | + | {SetupError{0, "no"}, "xturbo: connection setup failed: no"}, | |
| 266 | + | {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"}, | |
| 267 | + | } { | |
| 268 | + | if got := tc.e.Error(); got != tc.want { | |
| 269 | + | t.Errorf("%q, want %q", got, tc.want) | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | } | |
| @@ -0,0 +1,272 @@ | |||||
| 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 | + | // Walk the recorded reply to find the first screen and its first depth | |||
| 165 | + | // that carries visuals; every extent is a multiple of 4 bytes. | |||
| 166 | + | screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29]) | |||
| 167 | + | visualDepth := screen + screenFixedSize | |||
| 168 | + | for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 { | |||
| 169 | + | visualDepth += depthFixedSize | |||
| 170 | + | } | |||
| 171 | + | // Screen header present, its allowed depths cut off. | |||
| 172 | + | missingDepths := truncateSetup(setup, screen+screenFixedSize) | |||
| 173 | + | // A depth header announcing visuals, the visuals cut off. | |||
| 174 | + | missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize) | |||
| 175 | + | // One visual of the depth present, the rest cut off. | |||
| 176 | + | partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize) | |||
| 177 | + | // Complete first screen, a second screen declared but absent. | |||
| 178 | + | missingScreen := bytes.Clone(setup) | |||
| 179 | + | missingScreen[28] = 2 | |||
| 180 | + | // Header claims 8 bytes of body, failure reason claims 20. | |||
| 181 | + | reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} | |||
| 182 | + | // Success header whose length is shorter than the fixed Setup. | |||
| 183 | + | shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0} | |||
| 184 | + | for _, tc := range []struct { | |||
| 185 | + | name string | |||
| 186 | + | reply []byte | |||
| 187 | + | }{ | |||
| 188 | + | {"empty", nil}, | |||
| 189 | + | {"truncated header", setup[:5]}, | |||
| 190 | + | {"truncated body", setup[:len(setup)-3]}, | |||
| 191 | + | {"no screens", noScreens}, | |||
| 192 | + | {"bad xid mask", badMask}, | |||
| 193 | + | {"protocol major", badVersion}, | |||
| 194 | + | {"unknown status", badStatus}, | |||
| 195 | + | {"screen past end", screenPast}, | |||
| 196 | + | {"missing depths", missingDepths}, | |||
| 197 | + | {"missing visuals", missingVisuals}, | |||
| 198 | + | {"partial visuals", partialVisuals}, | |||
| 199 | + | {"missing subsequent screen", missingScreen}, | |||
| 200 | + | {"reason past end", reasonPast}, | |||
| 201 | + | {"short success", shortSuccess}, | |||
| 202 | + | } { | |||
| 203 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 204 | + | fd, _ := scriptedServer(t, tc.reply, 3, true) | |||
| 205 | + | c, err := NewConn(fd) | |||
| 206 | + | if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) { | |||
| 207 | + | t.Fatalf("conn %t, err %v", c != nil, err) | |||
| 208 | + | } | |||
| 209 | + | }) | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and | |||
| 214 | + | // rewrites its additional-data length to match. | |||
| 215 | + | func truncateSetup(setup []byte, n int) []byte { | |||
| 216 | + | p := bytes.Clone(setup[:n]) | |||
| 217 | + | binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4)) | |||
| 218 | + | return p | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | func TestConnFlushClose(t *testing.T) { | |||
| 222 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 223 | + | if err != nil { | |||
| 224 | + | t.Fatal(err) | |||
| 225 | + | } | |||
| 226 | + | server := os.NewFile(uintptr(fds[1]), "server") | |||
| 227 | + | defer server.Close() | |||
| 228 | + | setup := loadSetup(t, "Success") | |||
| 229 | + | go func() { | |||
| 230 | + | var req [12]byte | |||
| 231 | + | _, _ = io.ReadFull(server, req[:]) | |||
| 232 | + | _, _ = server.Write(setup) | |||
| 233 | + | }() | |||
| 234 | + | c, err := NewConn(fds[0]) | |||
| 235 | + | if err != nil { | |||
| 236 | + | t.Fatal(err) | |||
| 237 | + | } | |||
| 238 | + | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | |||
| 239 | + | if p == nil || seq != 1 { | |||
| 240 | + | t.Fatal("request") | |||
| 241 | + | } | |||
| 242 | + | if err := c.Flush(); err != nil { | |||
| 243 | + | t.Fatal(err) | |||
| 244 | + | } | |||
| 245 | + | var got [4]byte | |||
| 246 | + | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | |||
| 247 | + | t.Fatalf("%x %v", got, err) | |||
| 248 | + | } | |||
| 249 | + | if err := c.Close(); err != nil { | |||
| 250 | + | t.Fatal(err) | |||
| 251 | + | } | |||
| 252 | + | if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) { | |||
| 253 | + | t.Fatal("after Close") | |||
| 254 | + | } | |||
| 255 | + | if p, _ := c.Buf().Request(43, 0, 0); p != nil { | |||
| 256 | + | t.Fatal("Buf usable after Close") | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | func TestSetupErrorMessage(t *testing.T) { | |||
| 261 | + | for _, tc := range []struct { | |||
| 262 | + | e SetupError | |||
| 263 | + | want string | |||
| 264 | + | }{ | |||
| 265 | + | {SetupError{0, "no"}, "xturbo: connection setup failed: no"}, | |||
| 266 | + | {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"}, | |||
| 267 | + | } { | |||
| 268 | + | if got := tc.e.Error(); got != tc.want { | |||
| 269 | + | t.Errorf("%q, want %q", got, tc.want) | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | } | |||
A conn_xserver_test.go +161 -0
| @@ -0,0 +1,161 @@ | |||
| 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 | + | // Xvfb may write to -displayfd again (e.g. after a server reset); closing | |
| 35 | + | // the read end early makes that write fail and kills the server. | |
| 36 | + | t.Cleanup(func() { _ = r.Close() }) | |
| 37 | + | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile) | |
| 38 | + | cmd.ExtraFiles = []*os.File{w} | |
| 39 | + | var log bytes.Buffer | |
| 40 | + | cmd.Stderr = &log | |
| 41 | + | if err := cmd.Start(); err != nil { | |
| 42 | + | w.Close() | |
| 43 | + | t.Fatal(err) | |
| 44 | + | } | |
| 45 | + | w.Close() | |
| 46 | + | t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) | |
| 47 | + | _ = r.SetReadDeadline(time.Now().Add(10 * time.Second)) | |
| 48 | + | var buf [32]byte | |
| 49 | + | n, err := r.Read(buf[:]) | |
| 50 | + | if err != nil { | |
| 51 | + | t.Fatalf("Xvfb: %v: %s", err, log.String()) | |
| 52 | + | } | |
| 53 | + | num := strings.TrimSpace(string(buf[:n])) | |
| 54 | + | if _, err := strconv.Atoi(num); err != nil { | |
| 55 | + | t.Fatalf("Xvfb display %q", num) | |
| 56 | + | } | |
| 57 | + | return ":" + num | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // writeXauthority writes one FamilyLocal entry for this host, any display. | |
| 61 | + | func writeXauthority(t *testing.T, path string, cookie []byte) { | |
| 62 | + | t.Helper() | |
| 63 | + | host, err := os.Hostname() | |
| 64 | + | if err != nil { | |
| 65 | + | t.Fatal(err) | |
| 66 | + | } | |
| 67 | + | var b []byte | |
| 68 | + | field := func(s []byte) { | |
| 69 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |
| 70 | + | b = append(b, s...) | |
| 71 | + | } | |
| 72 | + | b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal | |
| 73 | + | field([]byte(host)) | |
| 74 | + | field(nil) // any display number | |
| 75 | + | field([]byte("MIT-MAGIC-COOKIE-1")) | |
| 76 | + | field(cookie) | |
| 77 | + | if err := os.WriteFile(path, b, 0o600); err != nil { | |
| 78 | + | t.Fatal(err) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func TestXserverDial(t *testing.T) { | |
| 83 | + | dir := t.TempDir() | |
| 84 | + | auth := filepath.Join(dir, "Xauthority") | |
| 85 | + | cookie := make([]byte, 16) | |
| 86 | + | _, _ = rand.Read(cookie) | |
| 87 | + | writeXauthority(t, auth, cookie) | |
| 88 | + | display := startXvfb(t, auth) | |
| 89 | + | ||
| 90 | + | // Without a cookie the server refuses the setup: some builds answer Failed, | |
| 91 | + | // others close the connection without a reply. | |
| 92 | + | t.Setenv("XAUTHORITY", filepath.Join(dir, "missing")) | |
| 93 | + | if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) { | |
| 94 | + | t.Fatalf("unauthenticated Dial: %v", err) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | t.Setenv("XAUTHORITY", auth) | |
| 98 | + | c, err := xturbo.Dial(display + ".0") | |
| 99 | + | if err != nil { | |
| 100 | + | t.Fatal(err) | |
| 101 | + | } | |
| 102 | + | defer c.Close() | |
| 103 | + | ||
| 104 | + | // Compare root's own parsing against the generated Setup view instead of | |
| 105 | + | // server-assigned constants (they differ between X server builds). | |
| 106 | + | setup := c.SetupBytes() | |
| 107 | + | if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) { | |
| 108 | + | t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup)) | |
| 109 | + | } | |
| 110 | + | it := xproto.NewSetupIter(setup, 1) | |
| 111 | + | if !it.Next() { | |
| 112 | + | t.Fatal("setup does not parse") | |
| 113 | + | } | |
| 114 | + | sv := it.Value() | |
| 115 | + | if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 { | |
| 116 | + | t.Fatalf("setup size %d status %d", sv.Size(), sv.Status()) | |
| 117 | + | } | |
| 118 | + | screens := sv.Roots() | |
| 119 | + | if !screens.Next() || uint32(screens.Value().Root()) != c.Root() { | |
| 120 | + | t.Fatalf("Root %#x, setup view says otherwise", c.Root()) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // A trivial round trip: GetInputFocus, then read its reply. | |
| 124 | + | seq := xproto.GetInputFocus(c.Buf()) | |
| 125 | + | if seq != 1 { | |
| 126 | + | t.Fatalf("seq %d", seq) | |
| 127 | + | } | |
| 128 | + | if err := c.Flush(); err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | reply := readPacket(t, c.Fd()) | |
| 132 | + | if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) { | |
| 133 | + | t.Fatalf("reply %x", reply) | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // readPacket polls the non-blocking fd and reads one 32-byte packet. | |
| 138 | + | func readPacket(t *testing.T, fd int) [32]byte { | |
| 139 | + | t.Helper() | |
| 140 | + | var p [32]byte | |
| 141 | + | got := 0 | |
| 142 | + | deadline := time.Now().Add(5 * time.Second) | |
| 143 | + | for got < len(p) { | |
| 144 | + | if time.Now().After(deadline) { | |
| 145 | + | t.Fatal("timeout reading reply") | |
| 146 | + | } | |
| 147 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |
| 148 | + | if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR { | |
| 149 | + | t.Fatal(err) | |
| 150 | + | } | |
| 151 | + | n, err := unix.Read(fd, p[got:]) | |
| 152 | + | if err == unix.EAGAIN || err == unix.EINTR { | |
| 153 | + | continue | |
| 154 | + | } | |
| 155 | + | if err != nil || n == 0 { | |
| 156 | + | t.Fatalf("read: %d %v", n, err) | |
| 157 | + | } | |
| 158 | + | got += n | |
| 159 | + | } | |
| 160 | + | return p | |
| 161 | + | } | |
| @@ -0,0 +1,161 @@ | |||||
| 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 | + | // Xvfb may write to -displayfd again (e.g. after a server reset); closing | |||
| 35 | + | // the read end early makes that write fail and kills the server. | |||
| 36 | + | t.Cleanup(func() { _ = r.Close() }) | |||
| 37 | + | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile) | |||
| 38 | + | cmd.ExtraFiles = []*os.File{w} | |||
| 39 | + | var log bytes.Buffer | |||
| 40 | + | cmd.Stderr = &log | |||
| 41 | + | if err := cmd.Start(); err != nil { | |||
| 42 | + | w.Close() | |||
| 43 | + | t.Fatal(err) | |||
| 44 | + | } | |||
| 45 | + | w.Close() | |||
| 46 | + | t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) | |||
| 47 | + | _ = r.SetReadDeadline(time.Now().Add(10 * time.Second)) | |||
| 48 | + | var buf [32]byte | |||
| 49 | + | n, err := r.Read(buf[:]) | |||
| 50 | + | if err != nil { | |||
| 51 | + | t.Fatalf("Xvfb: %v: %s", err, log.String()) | |||
| 52 | + | } | |||
| 53 | + | num := strings.TrimSpace(string(buf[:n])) | |||
| 54 | + | if _, err := strconv.Atoi(num); err != nil { | |||
| 55 | + | t.Fatalf("Xvfb display %q", num) | |||
| 56 | + | } | |||
| 57 | + | return ":" + num | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // writeXauthority writes one FamilyLocal entry for this host, any display. | |||
| 61 | + | func writeXauthority(t *testing.T, path string, cookie []byte) { | |||
| 62 | + | t.Helper() | |||
| 63 | + | host, err := os.Hostname() | |||
| 64 | + | if err != nil { | |||
| 65 | + | t.Fatal(err) | |||
| 66 | + | } | |||
| 67 | + | var b []byte | |||
| 68 | + | field := func(s []byte) { | |||
| 69 | + | b = binary.BigEndian.AppendUint16(b, uint16(len(s))) | |||
| 70 | + | b = append(b, s...) | |||
| 71 | + | } | |||
| 72 | + | b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal | |||
| 73 | + | field([]byte(host)) | |||
| 74 | + | field(nil) // any display number | |||
| 75 | + | field([]byte("MIT-MAGIC-COOKIE-1")) | |||
| 76 | + | field(cookie) | |||
| 77 | + | if err := os.WriteFile(path, b, 0o600); err != nil { | |||
| 78 | + | t.Fatal(err) | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func TestXserverDial(t *testing.T) { | |||
| 83 | + | dir := t.TempDir() | |||
| 84 | + | auth := filepath.Join(dir, "Xauthority") | |||
| 85 | + | cookie := make([]byte, 16) | |||
| 86 | + | _, _ = rand.Read(cookie) | |||
| 87 | + | writeXauthority(t, auth, cookie) | |||
| 88 | + | display := startXvfb(t, auth) | |||
| 89 | + | ||||
| 90 | + | // Without a cookie the server refuses the setup: some builds answer Failed, | |||
| 91 | + | // others close the connection without a reply. | |||
| 92 | + | t.Setenv("XAUTHORITY", filepath.Join(dir, "missing")) | |||
| 93 | + | if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) { | |||
| 94 | + | t.Fatalf("unauthenticated Dial: %v", err) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | t.Setenv("XAUTHORITY", auth) | |||
| 98 | + | c, err := xturbo.Dial(display + ".0") | |||
| 99 | + | if err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | defer c.Close() | |||
| 103 | + | ||||
| 104 | + | // Compare root's own parsing against the generated Setup view instead of | |||
| 105 | + | // server-assigned constants (they differ between X server builds). | |||
| 106 | + | setup := c.SetupBytes() | |||
| 107 | + | if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) { | |||
| 108 | + | t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup)) | |||
| 109 | + | } | |||
| 110 | + | it := xproto.NewSetupIter(setup, 1) | |||
| 111 | + | if !it.Next() { | |||
| 112 | + | t.Fatal("setup does not parse") | |||
| 113 | + | } | |||
| 114 | + | sv := it.Value() | |||
| 115 | + | if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 { | |||
| 116 | + | t.Fatalf("setup size %d status %d", sv.Size(), sv.Status()) | |||
| 117 | + | } | |||
| 118 | + | screens := sv.Roots() | |||
| 119 | + | if !screens.Next() || uint32(screens.Value().Root()) != c.Root() { | |||
| 120 | + | t.Fatalf("Root %#x, setup view says otherwise", c.Root()) | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // A trivial round trip: GetInputFocus, then read its reply. | |||
| 124 | + | seq := xproto.GetInputFocus(c.Buf()) | |||
| 125 | + | if seq != 1 { | |||
| 126 | + | t.Fatalf("seq %d", seq) | |||
| 127 | + | } | |||
| 128 | + | if err := c.Flush(); err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | reply := readPacket(t, c.Fd()) | |||
| 132 | + | if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) { | |||
| 133 | + | t.Fatalf("reply %x", reply) | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // readPacket polls the non-blocking fd and reads one 32-byte packet. | |||
| 138 | + | func readPacket(t *testing.T, fd int) [32]byte { | |||
| 139 | + | t.Helper() | |||
| 140 | + | var p [32]byte | |||
| 141 | + | got := 0 | |||
| 142 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 143 | + | for got < len(p) { | |||
| 144 | + | if time.Now().After(deadline) { | |||
| 145 | + | t.Fatal("timeout reading reply") | |||
| 146 | + | } | |||
| 147 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |||
| 148 | + | if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR { | |||
| 149 | + | t.Fatal(err) | |||
| 150 | + | } | |||
| 151 | + | n, err := unix.Read(fd, p[got:]) | |||
| 152 | + | if err == unix.EAGAIN || err == unix.EINTR { | |||
| 153 | + | continue | |||
| 154 | + | } | |||
| 155 | + | if err != nil || n == 0 { | |||
| 156 | + | t.Fatalf("read: %d %v", n, err) | |||
| 157 | + | } | |||
| 158 | + | got += n | |||
| 159 | + | } | |||
| 160 | + | return p | |||
| 161 | + | } | |||
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 } | |||
M internal/xtest/capture_test.go +3 -1
| @@ -101,7 +101,9 @@ func TestCaptureRequests(t *testing.T) { | |||
| 101 | 101 | if err != nil { | |
| 102 | 102 | t.Fatal(err) | |
| 103 | 103 | } | |
| 104 | - | defer r.Close() | |
| 104 | + | // Xvfb writes to displayfd again after a server reset; keep the read end | |
| 105 | + | // open until the test ends so that write cannot kill the server. | |
| 106 | + | t.Cleanup(func() { _ = r.Close() }) | |
| 105 | 107 | cmd := exec.Command("Xvfb", xvfbArgs()...) | |
| 106 | 108 | cmd.ExtraFiles = []*os.File{w} | |
| 107 | 109 | var log bytes.Buffer | |
| @@ -101,7 +101,9 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 101 | if err != nil { | 101 | if err != nil { | ||
| 102 | t.Fatal(err) | 102 | t.Fatal(err) | ||
| 103 | } | 103 | } | ||
| 104 | - | defer r.Close() | 104 | + | // Xvfb writes to displayfd again after a server reset; keep the read end |
| 105 | + | // open until the test ends so that write cannot kill the server. | |||
| 106 | + | t.Cleanup(func() { _ = r.Close() }) | |||
| 105 | cmd := exec.Command("Xvfb", xvfbArgs()...) | 107 | cmd := exec.Command("Xvfb", xvfbArgs()...) | ||
| 106 | cmd.ExtraFiles = []*os.File{w} | 108 | cmd.ExtraFiles = []*os.File{w} | ||
| 107 | var log bytes.Buffer | 109 | var log bytes.Buffer | ||
A setup.go +178 -0
| @@ -0,0 +1,178 @@ | |||
| 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 | + | depthFixedSize = 8 // fixed part of a DEPTH | |
| 19 | + | visualSize = 24 // VISUALTYPE | |
| 20 | + | protocolMajor = 11 | |
| 21 | + | protocolMinor = 0 | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // setupInfo is what root keeps from a successful setup reply. Everything else | |
| 25 | + | // is parsed by callers from Conn.SetupBytes. | |
| 26 | + | type setupInfo struct { | |
| 27 | + | xidBase, xidMask uint32 | |
| 28 | + | root uint32 | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) } | |
| 32 | + | ||
| 33 | + | // encodeSetupRequest builds the little-endian connection setup request. | |
| 34 | + | func encodeSetupRequest(authName string, authData []byte) []byte { | |
| 35 | + | n, d := pad4(len(authName)), pad4(len(authData)) | |
| 36 | + | p := make([]byte, 12+n+d) | |
| 37 | + | p[0] = 'l' | |
| 38 | + | binary.LittleEndian.PutUint16(p[2:], protocolMajor) | |
| 39 | + | binary.LittleEndian.PutUint16(p[4:], protocolMinor) | |
| 40 | + | binary.LittleEndian.PutUint16(p[6:], uint16(len(authName))) | |
| 41 | + | binary.LittleEndian.PutUint16(p[8:], uint16(len(authData))) | |
| 42 | + | copy(p[12:], authName) | |
| 43 | + | copy(p[12+n:], authData) | |
| 44 | + | return p | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func pad4(n int) int { return (n + 3) &^ 3 } | |
| 48 | + | ||
| 49 | + | // handshake runs the blocking setup exchange on fd and returns the full setup | |
| 50 | + | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |
| 51 | + | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |
| 52 | + | if len(authName) > 0xffff || len(authData) > 0xffff { | |
| 53 | + | return nil, setupInfo{}, ErrBadLength | |
| 54 | + | } | |
| 55 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |
| 56 | + | return nil, setupInfo{}, err | |
| 57 | + | } | |
| 58 | + | var hdr [setupHeaderSize]byte | |
| 59 | + | if err := readFull(fd, hdr[:]); err != nil { | |
| 60 | + | return nil, setupInfo{}, err | |
| 61 | + | } | |
| 62 | + | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |
| 63 | + | copy(reply, hdr[:]) | |
| 64 | + | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |
| 65 | + | return nil, setupInfo{}, err | |
| 66 | + | } | |
| 67 | + | info, err := parseSetup(reply) | |
| 68 | + | if err != nil { | |
| 69 | + | return nil, setupInfo{}, err | |
| 70 | + | } | |
| 71 | + | return reply, info, nil | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // parseSetup validates a complete setup reply (header length already | |
| 75 | + | // honoured) and extracts what root needs. | |
| 76 | + | func parseSetup(p []byte) (setupInfo, error) { | |
| 77 | + | if len(p) < setupHeaderSize { | |
| 78 | + | return setupInfo{}, malformed("short setup reply") | |
| 79 | + | } | |
| 80 | + | switch p[0] { | |
| 81 | + | case setupStatusFailed: | |
| 82 | + | // SetupFailed: reason_len at byte 1, reason after the header. | |
| 83 | + | n := int(p[1]) | |
| 84 | + | if setupHeaderSize+n > len(p) { | |
| 85 | + | return setupInfo{}, malformed("setup failure reason overflows reply") | |
| 86 | + | } | |
| 87 | + | return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])} | |
| 88 | + | case setupStatusAuthenticate: | |
| 89 | + | // SetupAuthenticate: reason fills length*4 bytes, NUL padded. | |
| 90 | + | reason := p[setupHeaderSize:] | |
| 91 | + | for len(reason) > 0 && reason[len(reason)-1] == 0 { | |
| 92 | + | reason = reason[:len(reason)-1] | |
| 93 | + | } | |
| 94 | + | return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)} | |
| 95 | + | case setupStatusSuccess: | |
| 96 | + | default: | |
| 97 | + | return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0])) | |
| 98 | + | } | |
| 99 | + | if len(p) < setupFixedSize { | |
| 100 | + | return setupInfo{}, malformed("short setup reply") | |
| 101 | + | } | |
| 102 | + | if binary.LittleEndian.Uint16(p[2:]) != protocolMajor { | |
| 103 | + | return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:]))) | |
| 104 | + | } | |
| 105 | + | info := setupInfo{ | |
| 106 | + | xidBase: binary.LittleEndian.Uint32(p[12:]), | |
| 107 | + | xidMask: binary.LittleEndian.Uint32(p[16:]), | |
| 108 | + | } | |
| 109 | + | if info.xidMask == 0 || info.xidBase&info.xidMask != 0 { | |
| 110 | + | return setupInfo{}, malformed("resource id base/mask") | |
| 111 | + | } | |
| 112 | + | if p[28] == 0 { | |
| 113 | + | return setupInfo{}, malformed("setup has no screens") | |
| 114 | + | } | |
| 115 | + | off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29]) | |
| 116 | + | if off > len(p) { | |
| 117 | + | return setupInfo{}, malformed("vendor or pixmap formats overflow setup reply") | |
| 118 | + | } | |
| 119 | + | roots := int(p[28]) | |
| 120 | + | for i := range roots { | |
| 121 | + | if len(p)-off < screenFixedSize { | |
| 122 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d overflows setup reply", i)) | |
| 123 | + | } | |
| 124 | + | if i == 0 { | |
| 125 | + | info.root = binary.LittleEndian.Uint32(p[off:]) | |
| 126 | + | } | |
| 127 | + | depths := int(p[off+39]) | |
| 128 | + | off += screenFixedSize | |
| 129 | + | for range depths { | |
| 130 | + | if len(p)-off < depthFixedSize { | |
| 131 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d depth overflows setup reply", i)) | |
| 132 | + | } | |
| 133 | + | visuals := visualSize * int(binary.LittleEndian.Uint16(p[off+2:])) | |
| 134 | + | off += depthFixedSize | |
| 135 | + | if len(p)-off < visuals { | |
| 136 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d visuals overflow setup reply", i)) | |
| 137 | + | } | |
| 138 | + | off += visuals | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | return info, nil | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func writeFull(fd int, p []byte) error { | |
| 145 | + | for len(p) > 0 { | |
| 146 | + | n, err := unix.Write(fd, p) | |
| 147 | + | switch { | |
| 148 | + | case err == unix.EINTR: | |
| 149 | + | continue | |
| 150 | + | case err == unix.EPIPE || err == unix.ECONNRESET: | |
| 151 | + | return ErrClosed | |
| 152 | + | case err != nil: | |
| 153 | + | return os.NewSyscallError("write", err) | |
| 154 | + | case n <= 0: | |
| 155 | + | return io.ErrShortWrite | |
| 156 | + | } | |
| 157 | + | p = p[n:] | |
| 158 | + | } | |
| 159 | + | return nil | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func readFull(fd int, p []byte) error { | |
| 163 | + | for len(p) > 0 { | |
| 164 | + | n, err := unix.Read(fd, p) | |
| 165 | + | switch { | |
| 166 | + | case err == unix.EINTR: | |
| 167 | + | continue | |
| 168 | + | case err == unix.ECONNRESET: | |
| 169 | + | return ErrClosed | |
| 170 | + | case err != nil: | |
| 171 | + | return os.NewSyscallError("read", err) | |
| 172 | + | case n == 0: | |
| 173 | + | return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF) | |
| 174 | + | } | |
| 175 | + | p = p[n:] | |
| 176 | + | } | |
| 177 | + | return nil | |
| 178 | + | } | |
| @@ -0,0 +1,178 @@ | |||||
| 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 | + | depthFixedSize = 8 // fixed part of a DEPTH | |||
| 19 | + | visualSize = 24 // VISUALTYPE | |||
| 20 | + | protocolMajor = 11 | |||
| 21 | + | protocolMinor = 0 | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // setupInfo is what root keeps from a successful setup reply. Everything else | |||
| 25 | + | // is parsed by callers from Conn.SetupBytes. | |||
| 26 | + | type setupInfo struct { | |||
| 27 | + | xidBase, xidMask uint32 | |||
| 28 | + | root uint32 | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) } | |||
| 32 | + | ||||
| 33 | + | // encodeSetupRequest builds the little-endian connection setup request. | |||
| 34 | + | func encodeSetupRequest(authName string, authData []byte) []byte { | |||
| 35 | + | n, d := pad4(len(authName)), pad4(len(authData)) | |||
| 36 | + | p := make([]byte, 12+n+d) | |||
| 37 | + | p[0] = 'l' | |||
| 38 | + | binary.LittleEndian.PutUint16(p[2:], protocolMajor) | |||
| 39 | + | binary.LittleEndian.PutUint16(p[4:], protocolMinor) | |||
| 40 | + | binary.LittleEndian.PutUint16(p[6:], uint16(len(authName))) | |||
| 41 | + | binary.LittleEndian.PutUint16(p[8:], uint16(len(authData))) | |||
| 42 | + | copy(p[12:], authName) | |||
| 43 | + | copy(p[12+n:], authData) | |||
| 44 | + | return p | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func pad4(n int) int { return (n + 3) &^ 3 } | |||
| 48 | + | ||||
| 49 | + | // handshake runs the blocking setup exchange on fd and returns the full setup | |||
| 50 | + | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |||
| 51 | + | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |||
| 52 | + | if len(authName) > 0xffff || len(authData) > 0xffff { | |||
| 53 | + | return nil, setupInfo{}, ErrBadLength | |||
| 54 | + | } | |||
| 55 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |||
| 56 | + | return nil, setupInfo{}, err | |||
| 57 | + | } | |||
| 58 | + | var hdr [setupHeaderSize]byte | |||
| 59 | + | if err := readFull(fd, hdr[:]); err != nil { | |||
| 60 | + | return nil, setupInfo{}, err | |||
| 61 | + | } | |||
| 62 | + | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |||
| 63 | + | copy(reply, hdr[:]) | |||
| 64 | + | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |||
| 65 | + | return nil, setupInfo{}, err | |||
| 66 | + | } | |||
| 67 | + | info, err := parseSetup(reply) | |||
| 68 | + | if err != nil { | |||
| 69 | + | return nil, setupInfo{}, err | |||
| 70 | + | } | |||
| 71 | + | return reply, info, nil | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // parseSetup validates a complete setup reply (header length already | |||
| 75 | + | // honoured) and extracts what root needs. | |||
| 76 | + | func parseSetup(p []byte) (setupInfo, error) { | |||
| 77 | + | if len(p) < setupHeaderSize { | |||
| 78 | + | return setupInfo{}, malformed("short setup reply") | |||
| 79 | + | } | |||
| 80 | + | switch p[0] { | |||
| 81 | + | case setupStatusFailed: | |||
| 82 | + | // SetupFailed: reason_len at byte 1, reason after the header. | |||
| 83 | + | n := int(p[1]) | |||
| 84 | + | if setupHeaderSize+n > len(p) { | |||
| 85 | + | return setupInfo{}, malformed("setup failure reason overflows reply") | |||
| 86 | + | } | |||
| 87 | + | return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])} | |||
| 88 | + | case setupStatusAuthenticate: | |||
| 89 | + | // SetupAuthenticate: reason fills length*4 bytes, NUL padded. | |||
| 90 | + | reason := p[setupHeaderSize:] | |||
| 91 | + | for len(reason) > 0 && reason[len(reason)-1] == 0 { | |||
| 92 | + | reason = reason[:len(reason)-1] | |||
| 93 | + | } | |||
| 94 | + | return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)} | |||
| 95 | + | case setupStatusSuccess: | |||
| 96 | + | default: | |||
| 97 | + | return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0])) | |||
| 98 | + | } | |||
| 99 | + | if len(p) < setupFixedSize { | |||
| 100 | + | return setupInfo{}, malformed("short setup reply") | |||
| 101 | + | } | |||
| 102 | + | if binary.LittleEndian.Uint16(p[2:]) != protocolMajor { | |||
| 103 | + | return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:]))) | |||
| 104 | + | } | |||
| 105 | + | info := setupInfo{ | |||
| 106 | + | xidBase: binary.LittleEndian.Uint32(p[12:]), | |||
| 107 | + | xidMask: binary.LittleEndian.Uint32(p[16:]), | |||
| 108 | + | } | |||
| 109 | + | if info.xidMask == 0 || info.xidBase&info.xidMask != 0 { | |||
| 110 | + | return setupInfo{}, malformed("resource id base/mask") | |||
| 111 | + | } | |||
| 112 | + | if p[28] == 0 { | |||
| 113 | + | return setupInfo{}, malformed("setup has no screens") | |||
| 114 | + | } | |||
| 115 | + | off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29]) | |||
| 116 | + | if off > len(p) { | |||
| 117 | + | return setupInfo{}, malformed("vendor or pixmap formats overflow setup reply") | |||
| 118 | + | } | |||
| 119 | + | roots := int(p[28]) | |||
| 120 | + | for i := range roots { | |||
| 121 | + | if len(p)-off < screenFixedSize { | |||
| 122 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d overflows setup reply", i)) | |||
| 123 | + | } | |||
| 124 | + | if i == 0 { | |||
| 125 | + | info.root = binary.LittleEndian.Uint32(p[off:]) | |||
| 126 | + | } | |||
| 127 | + | depths := int(p[off+39]) | |||
| 128 | + | off += screenFixedSize | |||
| 129 | + | for range depths { | |||
| 130 | + | if len(p)-off < depthFixedSize { | |||
| 131 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d depth overflows setup reply", i)) | |||
| 132 | + | } | |||
| 133 | + | visuals := visualSize * int(binary.LittleEndian.Uint16(p[off+2:])) | |||
| 134 | + | off += depthFixedSize | |||
| 135 | + | if len(p)-off < visuals { | |||
| 136 | + | return setupInfo{}, malformed(fmt.Sprintf("screen %d visuals overflow setup reply", i)) | |||
| 137 | + | } | |||
| 138 | + | off += visuals | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | return info, nil | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func writeFull(fd int, p []byte) error { | |||
| 145 | + | for len(p) > 0 { | |||
| 146 | + | n, err := unix.Write(fd, p) | |||
| 147 | + | switch { | |||
| 148 | + | case err == unix.EINTR: | |||
| 149 | + | continue | |||
| 150 | + | case err == unix.EPIPE || err == unix.ECONNRESET: | |||
| 151 | + | return ErrClosed | |||
| 152 | + | case err != nil: | |||
| 153 | + | return os.NewSyscallError("write", err) | |||
| 154 | + | case n <= 0: | |||
| 155 | + | return io.ErrShortWrite | |||
| 156 | + | } | |||
| 157 | + | p = p[n:] | |||
| 158 | + | } | |||
| 159 | + | return nil | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | func readFull(fd int, p []byte) error { | |||
| 163 | + | for len(p) > 0 { | |||
| 164 | + | n, err := unix.Read(fd, p) | |||
| 165 | + | switch { | |||
| 166 | + | case err == unix.EINTR: | |||
| 167 | + | continue | |||
| 168 | + | case err == unix.ECONNRESET: | |||
| 169 | + | return ErrClosed | |||
| 170 | + | case err != nil: | |||
| 171 | + | return os.NewSyscallError("read", err) | |||
| 172 | + | case n == 0: | |||
| 173 | + | return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF) | |||
| 174 | + | } | |||
| 175 | + | p = p[n:] | |||
| 176 | + | } | |||
| 177 | + | return nil | |||
| 178 | + | } | |||
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 | |||