2017-06-12 14:11:23 +03:00
|
|
|
package vnc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"encoding/binary"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
2017-06-13 01:52:07 +03:00
|
|
|
"sync"
|
2017-06-12 14:11:23 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var DefaultServerMessages = []ServerMessage{
|
|
|
|
&FramebufferUpdate{},
|
|
|
|
&SetColorMapEntries{},
|
|
|
|
&Bell{},
|
|
|
|
&ServerCutText{},
|
|
|
|
}
|
|
|
|
|
|
|
|
func Connect(ctx context.Context, c net.Conn, cfg *ClientConfig) (*ClientConn, error) {
|
|
|
|
conn, err := NewClientConn(c, cfg)
|
|
|
|
if err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
if err := cfg.VersionHandler(cfg, conn); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
if err := cfg.SecurityHandler(cfg, conn); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
if err := cfg.ClientInitHandler(cfg, conn); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
if err := cfg.ServerInitHandler(cfg, conn); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Conn = (*ClientConn)(nil)
|
|
|
|
|
2017-06-13 01:52:07 +03:00
|
|
|
func (c *ClientConn) Conn() net.Conn {
|
|
|
|
return c.c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) SetProtoVersion(pv string) {
|
|
|
|
c.protocol = pv
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) SetEncodings(encs []EncodingType) error {
|
|
|
|
|
|
|
|
msg := &SetEncodings{
|
|
|
|
MsgType: SetEncodingsMsgType,
|
|
|
|
EncNum: uint16(len(encs)),
|
|
|
|
Encodings: encs,
|
|
|
|
}
|
|
|
|
|
|
|
|
return msg.Write(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) UnreadByte() error {
|
|
|
|
return c.br.UnreadByte()
|
|
|
|
}
|
|
|
|
|
2017-06-12 14:11:23 +03:00
|
|
|
func (c *ClientConn) Flush() error {
|
2017-06-13 01:52:07 +03:00
|
|
|
c.m.Lock()
|
|
|
|
defer c.m.Unlock()
|
2017-06-12 14:11:23 +03:00
|
|
|
return c.bw.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) Close() error {
|
|
|
|
return c.c.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) Read(buf []byte) (int, error) {
|
|
|
|
return c.br.Read(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) Write(buf []byte) (int, error) {
|
2017-06-13 01:52:07 +03:00
|
|
|
c.m.Lock()
|
|
|
|
defer c.m.Unlock()
|
2017-06-12 14:11:23 +03:00
|
|
|
return c.bw.Write(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) ColorMap() *ColorMap {
|
|
|
|
return c.colorMap
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientConn) SetColorMap(cm *ColorMap) {
|
|
|
|
c.colorMap = cm
|
|
|
|
}
|
|
|
|
func (c *ClientConn) DesktopName() string {
|
|
|
|
return c.desktopName
|
|
|
|
}
|
|
|
|
func (c *ClientConn) PixelFormat() *PixelFormat {
|
|
|
|
return c.pixelFormat
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
func (c *ClientConn) SetDesktopName(name string) {
|
|
|
|
c.desktopName = name
|
|
|
|
}
|
|
|
|
func (c *ClientConn) SetPixelFormat(pf *PixelFormat) error {
|
|
|
|
c.pixelFormat = pf
|
|
|
|
return nil
|
|
|
|
}
|
2017-06-12 14:11:23 +03:00
|
|
|
func (c *ClientConn) Encodings() []Encoding {
|
|
|
|
return c.encodings
|
|
|
|
}
|
|
|
|
func (c *ClientConn) Width() uint16 {
|
|
|
|
return c.fbWidth
|
|
|
|
}
|
|
|
|
func (c *ClientConn) Height() uint16 {
|
|
|
|
return c.fbHeight
|
|
|
|
}
|
|
|
|
func (c *ClientConn) Protocol() string {
|
|
|
|
return c.protocol
|
|
|
|
}
|
|
|
|
func (c *ClientConn) SetWidth(w uint16) {
|
|
|
|
c.fbWidth = w
|
|
|
|
}
|
|
|
|
func (c *ClientConn) SetHeight(h uint16) {
|
|
|
|
c.fbHeight = h
|
|
|
|
}
|
|
|
|
|
|
|
|
// The ClientConn type holds client connection information.
|
|
|
|
type ClientConn struct {
|
|
|
|
c net.Conn
|
|
|
|
br *bufio.Reader
|
|
|
|
bw *bufio.Writer
|
|
|
|
cfg *ClientConfig
|
|
|
|
protocol string
|
2017-06-13 01:52:07 +03:00
|
|
|
m sync.Mutex
|
2017-06-12 14:11:23 +03:00
|
|
|
// If the pixel format uses a color map, then this is the color
|
|
|
|
// map that is used. This should not be modified directly, since
|
|
|
|
// the data comes from the server.
|
|
|
|
// Definition in §5 - Representation of Pixel Data.
|
|
|
|
colorMap *ColorMap
|
|
|
|
|
|
|
|
// Name associated with the desktop, sent from the server.
|
|
|
|
desktopName string
|
|
|
|
|
|
|
|
// Encodings supported by the client. This should not be modified
|
|
|
|
// directly. Instead, SetEncodings() should be used.
|
|
|
|
encodings []Encoding
|
|
|
|
|
|
|
|
// Height of the frame buffer in pixels, sent from the server.
|
|
|
|
fbHeight uint16
|
|
|
|
|
|
|
|
// Width of the frame buffer in pixels, sent from the server.
|
|
|
|
fbWidth uint16
|
|
|
|
|
|
|
|
// The pixel format associated with the connection. This shouldn't
|
|
|
|
// be modified. If you wish to set a new pixel format, use the
|
|
|
|
// SetPixelFormat method.
|
|
|
|
pixelFormat *PixelFormat
|
|
|
|
|
|
|
|
quit chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewClientConn(c net.Conn, cfg *ClientConfig) (*ClientConn, error) {
|
|
|
|
if cfg.ServerMessages == nil {
|
|
|
|
return nil, fmt.Errorf("ServerMessages cannel is nil")
|
|
|
|
}
|
|
|
|
if len(cfg.Encodings) == 0 {
|
|
|
|
return nil, fmt.Errorf("client can't handle encodings")
|
|
|
|
}
|
|
|
|
return &ClientConn{
|
|
|
|
c: c,
|
|
|
|
cfg: cfg,
|
|
|
|
br: bufio.NewReader(c),
|
|
|
|
bw: bufio.NewWriter(c),
|
|
|
|
encodings: cfg.Encodings,
|
2017-06-13 01:52:07 +03:00
|
|
|
quit: make(chan struct{}),
|
2017-06-12 14:11:23 +03:00
|
|
|
pixelFormat: cfg.PixelFormat,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClientMessage represents a Client-to-Server RFB message type.
|
|
|
|
type ClientMessageType uint8
|
|
|
|
|
|
|
|
//go:generate stringer -type=ClientMessageType
|
|
|
|
|
|
|
|
// Client-to-Server message types.
|
|
|
|
const (
|
|
|
|
SetPixelFormatMsgType ClientMessageType = iota
|
|
|
|
_
|
|
|
|
SetEncodingsMsgType
|
|
|
|
FramebufferUpdateRequestMsgType
|
|
|
|
KeyEventMsgType
|
|
|
|
PointerEventMsgType
|
|
|
|
ClientCutTextMsgType
|
|
|
|
)
|
|
|
|
|
|
|
|
// SetPixelFormat holds the wire format message.
|
|
|
|
type SetPixelFormat struct {
|
|
|
|
MsgType ClientMessageType
|
|
|
|
_ [3]byte // padding
|
|
|
|
PF PixelFormat // pixel-format
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetPixelFormat) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return SetPixelFormatMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetPixelFormat) Write(c Conn) error {
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.MsgType); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
pf := c.PixelFormat()
|
|
|
|
// Invalidate the color map.
|
|
|
|
if pf.TrueColor != 1 {
|
|
|
|
c.SetColorMap(&ColorMap{})
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetPixelFormat) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
return binary.Read(c, binary.BigEndian, &msg)
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetEncodings holds the wire format message, sans encoding-type field.
|
|
|
|
type SetEncodings struct {
|
|
|
|
MsgType ClientMessageType
|
|
|
|
_ [1]byte // padding
|
|
|
|
EncNum uint16 // number-of-encodings
|
2017-06-13 01:52:07 +03:00
|
|
|
Encodings []EncodingType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetEncodings) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return SetEncodingsMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetEncodings) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Read(c, binary.BigEndian, &msg.MsgType); err != nil {
|
2017-06-12 14:11:23 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
var pad [1]byte
|
|
|
|
if err := binary.Read(c, binary.BigEndian, &pad); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Read(c, binary.BigEndian, &msg.EncNum); err != nil {
|
2017-06-12 14:11:23 +03:00
|
|
|
return err
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
var enc EncodingType
|
2017-06-12 14:11:23 +03:00
|
|
|
for i := uint16(0); i < msg.EncNum; i++ {
|
|
|
|
if err := binary.Read(c, binary.BigEndian, &enc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
msg.Encodings = append(msg.Encodings, enc)
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
c.SetEncodings(msg.Encodings)
|
2017-06-12 14:11:23 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *SetEncodings) Write(c Conn) error {
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.MsgType); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var pad [1]byte
|
|
|
|
if err := binary.Write(c, binary.BigEndian, pad); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if uint16(len(msg.Encodings)) > msg.EncNum {
|
|
|
|
msg.EncNum = uint16(len(msg.Encodings))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.EncNum); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, enc := range msg.Encodings {
|
|
|
|
if err := binary.Write(c, binary.BigEndian, enc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return c.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
// FramebufferUpdateRequest holds the wire format message.
|
|
|
|
type FramebufferUpdateRequest struct {
|
|
|
|
MsgType ClientMessageType
|
|
|
|
Inc uint8 // incremental
|
|
|
|
X, Y uint16 // x-, y-position
|
|
|
|
Width, Height uint16 // width, height
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *FramebufferUpdateRequest) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return FramebufferUpdateRequestMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *FramebufferUpdateRequest) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
return binary.Read(c, binary.BigEndian, &msg)
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *FramebufferUpdateRequest) Write(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Write(c, binary.BigEndian, msg); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.Flush()
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// KeyEvent holds the wire format message.
|
|
|
|
type KeyEvent struct {
|
|
|
|
MsgType ClientMessageType // message-type
|
|
|
|
Down uint8 // down-flag
|
|
|
|
_ [2]byte // padding
|
|
|
|
Key Key // key
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *KeyEvent) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return KeyEventMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *KeyEvent) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
return binary.Read(c, binary.BigEndian, &msg)
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *KeyEvent) Write(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Write(c, binary.BigEndian, msg); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.Flush()
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// PointerEventMessage holds the wire format message.
|
|
|
|
type PointerEvent struct {
|
|
|
|
MsgType ClientMessageType // message-type
|
|
|
|
Mask uint8 // button-mask
|
|
|
|
X, Y uint16 // x-, y-position
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *PointerEvent) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return PointerEventMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *PointerEvent) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
return binary.Read(c, binary.BigEndian, &msg)
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *PointerEvent) Write(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Write(c, binary.BigEndian, msg); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.Flush()
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// ClientCutText holds the wire format message, sans the text field.
|
|
|
|
type ClientCutText struct {
|
|
|
|
MsgType ClientMessageType // message-type
|
|
|
|
_ [3]byte // padding
|
|
|
|
Length uint32 // length
|
|
|
|
Text []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *ClientCutText) Type() ClientMessageType {
|
2017-06-13 01:52:07 +03:00
|
|
|
return ClientCutTextMsgType
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *ClientCutText) Read(c Conn) error {
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Read(c, binary.BigEndian, &msg.MsgType); err != nil {
|
2017-06-12 14:11:23 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var pad [3]byte
|
|
|
|
if err := binary.Read(c, binary.BigEndian, &pad); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-06-13 01:52:07 +03:00
|
|
|
if err := binary.Read(c, binary.BigEndian, &msg.Length); err != nil {
|
2017-06-12 14:11:23 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
text := make([]uint8, msg.Length)
|
|
|
|
if err := binary.Read(c, binary.BigEndian, &text); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
msg.Text = text
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (msg *ClientCutText) Write(c Conn) error {
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.MsgType); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var pad [3]byte
|
|
|
|
if err := binary.Write(c, binary.BigEndian, &pad); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if uint32(len(msg.Text)) > msg.Length {
|
|
|
|
msg.Length = uint32(len(msg.Text))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.Length); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(c, binary.BigEndian, msg.Text); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListenAndHandle listens to a VNC server and handles server messages.
|
|
|
|
func (c *ClientConn) Handle() error {
|
|
|
|
var err error
|
2017-06-13 01:52:07 +03:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(2)
|
2017-06-12 14:11:23 +03:00
|
|
|
defer c.Close()
|
|
|
|
|
|
|
|
serverMessages := make(map[ServerMessageType]ServerMessage)
|
|
|
|
for _, m := range c.cfg.ServerMessages {
|
|
|
|
serverMessages[m.Type()] = m
|
|
|
|
}
|
|
|
|
|
2017-06-13 01:52:07 +03:00
|
|
|
go func() error {
|
|
|
|
defer wg.Done()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case msg := <-c.cfg.ClientMessageCh:
|
|
|
|
if err = msg.Write(c); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
case <-c.quit:
|
|
|
|
return nil
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
}()
|
|
|
|
|
|
|
|
go func() error {
|
|
|
|
defer wg.Done()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-c.quit:
|
|
|
|
return nil
|
|
|
|
default:
|
|
|
|
var messageType ServerMessageType
|
|
|
|
if err = binary.Read(c, binary.BigEndian, &messageType); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := c.UnreadByte(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
msg, ok := serverMessages[messageType]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("unknown message-type: %v", messageType)
|
|
|
|
}
|
|
|
|
if err = msg.Read(c); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if c.cfg.ServerMessageCh == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
c.cfg.ServerMessageCh <- msg
|
2017-06-12 14:11:23 +03:00
|
|
|
}
|
|
|
|
}
|
2017-06-13 01:52:07 +03:00
|
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
fmt.Printf("tttt\n")
|
2017-06-12 14:11:23 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
type ClientHandler func(*ClientConfig, Conn) error
|
|
|
|
|
|
|
|
// A ClientConfig structure is used to configure a ClientConn. After
|
|
|
|
// one has been passed to initialize a connection, it must not be modified.
|
|
|
|
type ClientConfig struct {
|
|
|
|
VersionHandler ClientHandler
|
|
|
|
SecurityHandler ClientHandler
|
2017-06-13 01:52:07 +03:00
|
|
|
SecurityHandlers []SecurityHandler
|
2017-06-12 14:11:23 +03:00
|
|
|
ClientInitHandler ClientHandler
|
|
|
|
ServerInitHandler ClientHandler
|
|
|
|
Encodings []Encoding
|
|
|
|
PixelFormat *PixelFormat
|
|
|
|
ColorMap *ColorMap
|
|
|
|
ClientMessageCh chan ClientMessage
|
|
|
|
ServerMessageCh chan ServerMessage
|
|
|
|
Exclusive bool
|
|
|
|
ServerMessages []ServerMessage
|
|
|
|
}
|