mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
Lock pixel format to 32bpp little-endian truecolour and reject other formats
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -80,24 +81,17 @@ const (
|
|||||||
// Distinct-colour cap below which we still prefer Basic+zlib (text,
|
// Distinct-colour cap below which we still prefer Basic+zlib (text,
|
||||||
// UI). Sampled, not exhaustive: cheap to compute, good enough.
|
// UI). Sampled, not exhaustive: cheap to compute, good enough.
|
||||||
tightJPEGMinColors = 64
|
tightJPEGMinColors = 64
|
||||||
|
|
||||||
// Hextile subencoding flags (a bitmask in the first byte of each sub-tile).
|
|
||||||
hextileRaw = 0x01
|
|
||||||
hextileBackgroundSpecified = 0x02
|
|
||||||
hextileForegroundSpecified = 0x04
|
|
||||||
hextileAnySubrects = 0x08
|
|
||||||
hextileSubrectsColoured = 0x10
|
|
||||||
|
|
||||||
// Hextile sub-tile size per RFB spec.
|
|
||||||
hextileSubSize = 16
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverPixelFormat is the default pixel format advertised by the server:
|
// serverPixelFormat is the pixel format the server advertises and requires:
|
||||||
// 32bpp RGBA, big-endian, true-colour, 8 bits per channel.
|
// 32bpp RGBA, little-endian, true-colour, 8 bits per channel at standard
|
||||||
|
// shifts (R=16, G=8, B=0). handleSetPixelFormat rejects any client that
|
||||||
|
// negotiates a different format. Browser-side decoders are little-endian
|
||||||
|
// natively, so advertising little-endian skips a byte-swap on every pixel.
|
||||||
var serverPixelFormat = [16]byte{
|
var serverPixelFormat = [16]byte{
|
||||||
32, // bits-per-pixel
|
32, // bits-per-pixel
|
||||||
24, // depth
|
24, // depth
|
||||||
1, // big-endian-flag
|
0, // big-endian-flag
|
||||||
1, // true-colour-flag
|
1, // true-colour-flag
|
||||||
0, 255, // red-max
|
0, 255, // red-max
|
||||||
0, 255, // green-max
|
0, 255, // green-max
|
||||||
@@ -108,42 +102,48 @@ var serverPixelFormat = [16]byte{
|
|||||||
0, 0, 0, // padding
|
0, 0, 0, // padding
|
||||||
}
|
}
|
||||||
|
|
||||||
// clientPixelFormat holds the negotiated pixel format from the client.
|
// clientPixelFormat holds the negotiated pixel format. Only RGB channel
|
||||||
|
// shifts are tracked: every other field is constrained by the server to
|
||||||
|
// the values in serverPixelFormat (32bpp / little-endian / truecolour /
|
||||||
|
// 8-bit channels) and rejected at SetPixelFormat time if the client tries
|
||||||
|
// to negotiate otherwise.
|
||||||
type clientPixelFormat struct {
|
type clientPixelFormat struct {
|
||||||
bpp uint8
|
rShift uint8
|
||||||
bigEndian uint8
|
gShift uint8
|
||||||
rMax uint16
|
bShift uint8
|
||||||
gMax uint16
|
|
||||||
bMax uint16
|
|
||||||
rShift uint8
|
|
||||||
gShift uint8
|
|
||||||
bShift uint8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultClientPixelFormat() clientPixelFormat {
|
func defaultClientPixelFormat() clientPixelFormat {
|
||||||
return clientPixelFormat{
|
return clientPixelFormat{
|
||||||
bpp: serverPixelFormat[0],
|
rShift: serverPixelFormat[10],
|
||||||
bigEndian: serverPixelFormat[2],
|
gShift: serverPixelFormat[11],
|
||||||
rMax: binary.BigEndian.Uint16(serverPixelFormat[4:6]),
|
bShift: serverPixelFormat[12],
|
||||||
gMax: binary.BigEndian.Uint16(serverPixelFormat[6:8]),
|
|
||||||
bMax: binary.BigEndian.Uint16(serverPixelFormat[8:10]),
|
|
||||||
rShift: serverPixelFormat[10],
|
|
||||||
gShift: serverPixelFormat[11],
|
|
||||||
bShift: serverPixelFormat[12],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePixelFormat(pf []byte) clientPixelFormat {
|
// parsePixelFormat returns the negotiated client pixel format, or an error
|
||||||
return clientPixelFormat{
|
// if the client tried to negotiate an unsupported format. The server only
|
||||||
bpp: pf[0],
|
// supports 32bpp truecolour little-endian with 8-bit channels; arbitrary
|
||||||
bigEndian: pf[2],
|
// shifts within that constraint are allowed because they are cheap to honour.
|
||||||
rMax: binary.BigEndian.Uint16(pf[4:6]),
|
func parsePixelFormat(pf []byte) (clientPixelFormat, error) {
|
||||||
gMax: binary.BigEndian.Uint16(pf[6:8]),
|
bpp := pf[0]
|
||||||
bMax: binary.BigEndian.Uint16(pf[8:10]),
|
bigEndian := pf[2]
|
||||||
rShift: pf[10],
|
trueColour := pf[3]
|
||||||
gShift: pf[11],
|
rMax := binary.BigEndian.Uint16(pf[4:6])
|
||||||
bShift: pf[12],
|
gMax := binary.BigEndian.Uint16(pf[6:8])
|
||||||
|
bMax := binary.BigEndian.Uint16(pf[8:10])
|
||||||
|
if bpp != 32 || bigEndian != 0 || trueColour != 1 ||
|
||||||
|
rMax != 255 || gMax != 255 || bMax != 255 {
|
||||||
|
return clientPixelFormat{}, fmt.Errorf(
|
||||||
|
"unsupported pixel format (bpp=%d be=%d tc=%d rgb-max=%d/%d/%d): "+
|
||||||
|
"server only supports 32bpp truecolour little-endian 8-bit channels",
|
||||||
|
bpp, bigEndian, trueColour, rMax, gMax, bMax)
|
||||||
}
|
}
|
||||||
|
return clientPixelFormat{
|
||||||
|
rShift: pf[10],
|
||||||
|
gShift: pf[11],
|
||||||
|
bShift: pf[12],
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeCopyRectBody emits the per-rect payload for a CopyRect rectangle:
|
// encodeCopyRectBody emits the per-rect payload for a CopyRect rectangle:
|
||||||
@@ -211,10 +211,7 @@ func encodeLastRectBody() []byte {
|
|||||||
// encodeRawRect encodes a framebuffer region as a raw RFB rectangle.
|
// encodeRawRect encodes a framebuffer region as a raw RFB rectangle.
|
||||||
// The returned buffer includes the FramebufferUpdate header (1 rectangle).
|
// The returned buffer includes the FramebufferUpdate header (1 rectangle).
|
||||||
func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte {
|
func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte {
|
||||||
bytesPerPixel := max(int(pf.bpp)/8, 1)
|
buf := make([]byte, 4+12+w*h*4)
|
||||||
|
|
||||||
pixelBytes := w * h * bytesPerPixel
|
|
||||||
buf := make([]byte, 4+12+pixelBytes)
|
|
||||||
|
|
||||||
// FramebufferUpdate header.
|
// FramebufferUpdate header.
|
||||||
buf[0] = serverFramebufferUpdate
|
buf[0] = serverFramebufferUpdate
|
||||||
@@ -228,26 +225,17 @@ func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte
|
|||||||
binary.BigEndian.PutUint16(buf[10:12], uint16(h))
|
binary.BigEndian.PutUint16(buf[10:12], uint16(h))
|
||||||
binary.BigEndian.PutUint32(buf[12:16], uint32(encRaw))
|
binary.BigEndian.PutUint32(buf[12:16], uint32(encRaw))
|
||||||
|
|
||||||
writePixels(buf[16:], img, pf, rect{x, y, w, h}, bytesPerPixel)
|
writePixels(buf[16:], img, pf, rect{x, y, w, h})
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePixels writes a rectangle of img into dst in the client's requested
|
// writePixels writes a rectangle of img into dst as 32bpp little-endian
|
||||||
// pixel format. It fast-paths the common case (32bpp, full 8-bit channels)
|
// pixels at the negotiated RGB shifts. The pixel format is constrained at
|
||||||
// with a tight loop that skips the per-channel *max/255 arithmetic and emits
|
// SetPixelFormat time so we can assume 4 bytes per pixel, 8-bit channels,
|
||||||
// a single uint32 per pixel; the general path handles arbitrary formats.
|
// and little-endian byte order; arbitrary shifts (R/G/B order) are honoured.
|
||||||
func writePixels(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect, bytesPerPixel int) {
|
func writePixels(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect) {
|
||||||
if bytesPerPixel == 4 && pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 {
|
|
||||||
writePixelsFast32(dst, img, pf, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writePixelsGeneric(dst, img, pf, r, bytesPerPixel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePixelsFast32(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect) {
|
|
||||||
stride := img.Stride
|
stride := img.Stride
|
||||||
rShift, gShift, bShift := pf.rShift, pf.gShift, pf.bShift
|
rShift, gShift, bShift := pf.rShift, pf.gShift, pf.bShift
|
||||||
bigEndian := pf.bigEndian != 0
|
|
||||||
off := 0
|
off := 0
|
||||||
for row := r.y; row < r.y+r.h; row++ {
|
for row := r.y; row < r.y+r.h; row++ {
|
||||||
p := row*stride + r.x*4
|
p := row*stride + r.x*4
|
||||||
@@ -255,47 +243,13 @@ func writePixelsFast32(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect
|
|||||||
pixel := (uint32(img.Pix[p]) << rShift) |
|
pixel := (uint32(img.Pix[p]) << rShift) |
|
||||||
(uint32(img.Pix[p+1]) << gShift) |
|
(uint32(img.Pix[p+1]) << gShift) |
|
||||||
(uint32(img.Pix[p+2]) << bShift)
|
(uint32(img.Pix[p+2]) << bShift)
|
||||||
if bigEndian {
|
binary.LittleEndian.PutUint32(dst[off:off+4], pixel)
|
||||||
binary.BigEndian.PutUint32(dst[off:off+4], pixel)
|
|
||||||
} else {
|
|
||||||
binary.LittleEndian.PutUint32(dst[off:off+4], pixel)
|
|
||||||
}
|
|
||||||
p += 4
|
p += 4
|
||||||
off += 4
|
off += 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writePixelsGeneric(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect, bytesPerPixel int) {
|
|
||||||
stride := img.Stride
|
|
||||||
off := 0
|
|
||||||
for row := r.y; row < r.y+r.h; row++ {
|
|
||||||
for col := r.x; col < r.x+r.w; col++ {
|
|
||||||
p := row*stride + col*4
|
|
||||||
rv := uint32(img.Pix[p]) * uint32(pf.rMax) / 255
|
|
||||||
gv := uint32(img.Pix[p+1]) * uint32(pf.gMax) / 255
|
|
||||||
bv := uint32(img.Pix[p+2]) * uint32(pf.bMax) / 255
|
|
||||||
pixel := (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift)
|
|
||||||
emitPixelBytes(dst[off:off+bytesPerPixel], pixel, bytesPerPixel, pf.bigEndian != 0)
|
|
||||||
off += bytesPerPixel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func emitPixelBytes(dst []byte, pixel uint32, bytesPerPixel int, bigEndian bool) {
|
|
||||||
if bigEndian {
|
|
||||||
for i := range bytesPerPixel {
|
|
||||||
dst[i] = byte(pixel >> uint((bytesPerPixel-1-i)*8))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i := range bytesPerPixel {
|
|
||||||
dst[i] = byte(pixel >> uint(i*8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// diffTiles compares two RGBA images and returns a tile-ordered list of
|
// diffTiles compares two RGBA images and returns a tile-ordered list of
|
||||||
// dirty tiles, one entry per tile. Tile order is top-to-bottom, left-to-
|
// dirty tiles, one entry per tile. Tile order is top-to-bottom, left-to-
|
||||||
// right within each row. The caller decides whether to coalesce or hand
|
// right within each row. The caller decides whether to coalesce or hand
|
||||||
@@ -453,33 +407,6 @@ func tileIsUniform(img *image.RGBA, x, y, w, h int) (uint32, bool) {
|
|||||||
return first, true
|
return first, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodePixel packs an RGBA byte triple into the client's requested pixel
|
|
||||||
// format, honouring bpp, channel maxes, shifts and endianness. Returns the
|
|
||||||
// number of bytes written to dst (1..4).
|
|
||||||
func encodePixel(dst []byte, pf clientPixelFormat, r, g, b byte) int {
|
|
||||||
bytesPerPixel := max(int(pf.bpp)/8, 1)
|
|
||||||
var val uint32
|
|
||||||
if pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 {
|
|
||||||
val = (uint32(r) << pf.rShift) | (uint32(g) << pf.gShift) | (uint32(b) << pf.bShift)
|
|
||||||
} else {
|
|
||||||
rv := uint32(r) * uint32(pf.rMax) / 255
|
|
||||||
gv := uint32(g) * uint32(pf.gMax) / 255
|
|
||||||
bv := uint32(b) * uint32(pf.bMax) / 255
|
|
||||||
val = (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift)
|
|
||||||
}
|
|
||||||
if pf.bigEndian != 0 {
|
|
||||||
for i := range bytesPerPixel {
|
|
||||||
dst[i] = byte(val >> uint((bytesPerPixel-1-i)*8))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i := range bytesPerPixel {
|
|
||||||
dst[i] = byte(val >> uint(i*8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bytesPerPixel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// tightState holds the per-session JPEG scratch buffer and reused encoders
|
// tightState holds the per-session JPEG scratch buffer and reused encoders
|
||||||
// so per-rect encoding stays alloc-free in the steady state.
|
// so per-rect encoding stays alloc-free in the steady state.
|
||||||
type tightState struct {
|
type tightState struct {
|
||||||
|
|||||||
@@ -84,26 +84,7 @@ func BenchmarkWritePixels(b *testing.B) {
|
|||||||
b.SetBytes(int64(r.w * r.h * 4))
|
b.SetBytes(int64(r.w * r.h * 4))
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
writePixels(dst, img, pf, rect{0, 0, r.w, r.h}, 4)
|
writePixels(dst, img, pf, rect{0, 0, r.w, r.h})
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkWritePixelsScaled forces the general (non-fast) path by using a
|
|
||||||
// pixel format with non-255 channel maxes.
|
|
||||||
func BenchmarkWritePixelsScaled(b *testing.B) {
|
|
||||||
pf := defaultClientPixelFormat()
|
|
||||||
pf.rMax, pf.gMax, pf.bMax = 31, 63, 31 // 16bpp-ish; exercises the divide path
|
|
||||||
pf.bpp = 16
|
|
||||||
for _, r := range benchRects {
|
|
||||||
img := makeBenchImage(r.w, r.h, 1)
|
|
||||||
dst := make([]byte, r.w*r.h*2)
|
|
||||||
b.Run(r.name, func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(r.w * r.h * 4))
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
writePixels(dst, img, pf, rect{0, 0, r.w, r.h}, 2)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,7 +260,10 @@ func (s *session) handleSetPixelFormat() error {
|
|||||||
if _, err := io.ReadFull(s.conn, buf[:]); err != nil {
|
if _, err := io.ReadFull(s.conn, buf[:]); err != nil {
|
||||||
return fmt.Errorf("read SetPixelFormat: %w", err)
|
return fmt.Errorf("read SetPixelFormat: %w", err)
|
||||||
}
|
}
|
||||||
pf := parsePixelFormat(buf[3:19])
|
pf, err := parsePixelFormat(buf[3:19])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s.encMu.Lock()
|
s.encMu.Lock()
|
||||||
s.pf = pf
|
s.pf = pf
|
||||||
s.encMu.Unlock()
|
s.encMu.Unlock()
|
||||||
@@ -822,11 +825,8 @@ func drainRequests(ch chan fbRequest) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pfIsTightCompatible reports whether the negotiated client pixel format
|
// pfIsTightCompatible reports whether the negotiated client pixel format
|
||||||
// matches Tight's TPIXEL constraint: 32 bpp true colour with 8-bit RGB
|
// matches Tight's TPIXEL constraint: standard RGB shifts (R=16, G=8, B=0).
|
||||||
// channels at standard shifts (R=16, G=8, B=0). For anything else we fall
|
// bpp/endianness/channel-max are already locked at SetPixelFormat time.
|
||||||
// back to Zlib/Hextile/Raw which respect pf in full.
|
|
||||||
func pfIsTightCompatible(pf clientPixelFormat) bool {
|
func pfIsTightCompatible(pf clientPixelFormat) bool {
|
||||||
return pf.bpp == 32 &&
|
return pf.rShift == 16 && pf.gShift == 8 && pf.bShift == 0
|
||||||
pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 &&
|
|
||||||
pf.rShift == 16 && pf.gShift == 8 && pf.bShift == 0
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user