diff --git a/cmd/stagelinq-discover/main.go b/cmd/stagelinq-discover/main.go index 66f2049..a122399 100644 --- a/cmd/stagelinq-discover/main.go +++ b/cmd/stagelinq-discover/main.go @@ -24,6 +24,8 @@ func main() { panic(err) } + listener.Announce() + deadline := time.After(timeout) foundDevices := []*stagelinq.Device{} @@ -35,7 +37,7 @@ discoveryLoop: case <-deadline: break discoveryLoop default: - device, deviceState, err := listener.Discover() + device, deviceState, err := listener.Discover(timeout) if err != nil { log.Printf("WARNING: %s", err.Error()) continue discoveryLoop @@ -52,6 +54,24 @@ discoveryLoop: } foundDevices = append(foundDevices, device) log.Printf("%s %q %q %q", device.IP.String(), device.Name, device.SoftwareName, device.SoftwareVersion) + + // discover provided services + log.Println("\tattempting to connect to this deviceā€¦") + deviceConn, err := device.Connect(listener.Token(), []*stagelinq.Service{}) + if err != nil { + log.Printf("WARNING: %s", err.Error()) + } else { + defer deviceConn.Close() + log.Println("\trequesting device data servicesā€¦") + services, err := deviceConn.RequestServices() + if err != nil { + log.Printf("WARNING: %s", err.Error()) + } else { + for _, service := range services { + log.Printf("\toffers %s at port %d", service.Name, service.Port) + } + } + } } } diff --git a/device.go b/device.go index f46c238..e6a8dea 100644 --- a/device.go +++ b/device.go @@ -17,7 +17,7 @@ const ( // Device presents information about a discovered StagelinQ device on the network. type Device struct { port uint16 - token [16]byte + token Token IP net.IP Name string @@ -25,6 +25,31 @@ type Device struct { SoftwareVersion string } +// dial starts a TCP connection with the device. +func (device *Device) dial() (conn net.Conn, err error) { + ip := device.IP + port := device.port + + conn, err = net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: ip, + Port: int(port), + }) + + return +} + +// Connect starts a new main connection with the device. +// You need to pass the StagelinQ token announced for your own device. +// You also need to pass services you want to provide; if you don't have any, pass an empty array. +func (device *Device) Connect(token Token, offeredServices []*Service) (conn *MainConnection, err error) { + tcpConn, err := device.dial() + if err != nil { + return + } + conn, err = newMainConnection(tcpConn, token, device.token, offeredServices) + return +} + // IsEqual checks if this device has the same address and values as the other given device. func (device *Device) IsEqual(anotherDevice *Device) bool { return device.token == anotherDevice.token && @@ -33,7 +58,7 @@ func (device *Device) IsEqual(anotherDevice *Device) bool { device.SoftwareVersion == anotherDevice.SoftwareVersion } -func newDeviceFromDiscovery(addr *net.UDPAddr, msg *DiscoveryMessage) *Device { +func newDeviceFromDiscovery(addr *net.UDPAddr, msg *discoveryMessage) *Device { return &Device{ port: msg.Port, token: msg.Token, diff --git a/device_conn.go b/device_conn.go deleted file mode 100644 index 30e94c8..0000000 --- a/device_conn.go +++ /dev/null @@ -1,66 +0,0 @@ -package stagelinq - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "net" -) - -// ErrUnknownMessageID indicates that an invalid message was received from the connected device. -var ErrUnknownMessageID = errors.New("unknown message ID") - -type deviceConn struct { - net.Conn -} - -func newDeviceConn(conn net.Conn) *deviceConn { - return &deviceConn{conn} -} - -func (s *deviceConn) Close() error { - return s.Conn.Close() -} - -func (s *deviceConn) WriteMessage(msg message) (err error) { - buf := new(bytes.Buffer) - - // write message id (4 bytes) - if err = binary.Write(buf, binary.BigEndian, msg.id()); err != nil { - return - } - - // write message itself - if err = msg.writeTo(buf); err != nil { - return - } - - // write the whole thing out to the device - _, err = s.Conn.Write(buf.Bytes()) - return -} - -func (s *deviceConn) ReadMessage() (msg message, err error) { - // read message id (4 bytes) - var messageID int32 - if err = binary.Read(s.Conn, binary.BigEndian, &messageID); err != nil { - return - } - - // find associated function that creates message object - messageObjectGeneratorFunction, ok := tcpMessageMap[messageID] - if !ok { - err = ErrUnknownMessageID - err = fmt.Errorf("%s: %x", ErrUnknownMessageID.Error(), messageID) - return - } - - // create message object and decode message from device - msg = messageObjectGeneratorFunction() - if err = msg.readFrom(s.Conn); err != nil { - return - } - - return -} diff --git a/go.mod b/go.mod index f5d7a97..4489f93 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.icedream.tech/icedream/stagelinq-receiver go 1.15 require ( + github.com/hashicorp/go-multierror v1.1.0 github.com/stretchr/testify v1.6.1 golang.org/x/text v0.3.4 ) diff --git a/go.sum b/go.sum index 437c2c5..79e1d83 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= diff --git a/go_hacks_go1.16.go b/go_hacks_go1.16.go new file mode 100644 index 0000000..b2d0e4f --- /dev/null +++ b/go_hacks_go1.16.go @@ -0,0 +1,12 @@ +//+build go1.16 + +package stagelinq + +import ( + "errors" + "net" +) + +func checkErrIsNetClosed(err error) bool { + return errors.Is(err, net.ErrClosed) +} diff --git a/go_hacks_notgo1.16.go b/go_hacks_notgo1.16.go new file mode 100644 index 0000000..219fdfd --- /dev/null +++ b/go_hacks_notgo1.16.go @@ -0,0 +1,7 @@ +//+build !go1.16 + +package stagelinq + +func checkErrIsNetClosed(err error) bool { + return err.Error() == "use of closed network connection" +} diff --git a/listener.go b/listener.go index 865306b..11824d4 100644 --- a/listener.go +++ b/listener.go @@ -46,25 +46,33 @@ type Listener struct { softwareVersion string name string packetConn net.PacketConn - token [16]byte - timeout time.Duration + token Token port uint16 } +// Token returns our token that is being announced to the StagelinQ network. +// Use this token for further communication with services on other devices. +func (l *Listener) Token() Token { + return l.token +} + // Close shuts down the listener. func (l *Listener) Close() error { + // Send two exit messages just to make sure (apps I tested seem to do that) + l.announce(discovererExit) + l.announce(discovererExit) return l.packetConn.Close() } // Announce announces this StagelinQ listener to the network. // This function should be called before actually listening in for devices to allow them to pick up our token for communication immediately. func (l *Listener) Announce() error { - return l.announce(DiscovererHowdy) + return l.announce(discovererHowdy) } -func (l *Listener) announce(action DiscovererMessageAction) (err error) { +func (l *Listener) announce(action discovererMessageAction) (err error) { // TODO - optimization: cache the built message because it will be sent repeatedly? - m := &DiscoveryMessage{ + m := &discoveryMessage{ Source: l.name, SoftwareName: l.softwareName, SoftwareVersion: l.softwareVersion, @@ -85,14 +93,14 @@ func (l *Listener) announce(action DiscovererMessageAction) (err error) { } // Discover listens for any StagelinQ devices announcing to the network. -// If no device is found within the configured timeout or any non-StagelinQ message has been received, nil is returned for the device. +// If no device is found within the given timeout or any non-StagelinQ message has been received, nil is returned for the device. // If a device has been discovered before, the returned device object is not going to be the same as when the device was previously discovered. // Use device.IsEqual for such comparison. -func (l *Listener) Discover() (device *Device, deviceState DeviceState, err error) { +func (l *Listener) Discover(timeout time.Duration) (device *Device, deviceState DeviceState, err error) { b := make([]byte, 8*1024) - if l.timeout != 0 { - l.packetConn.SetReadDeadline(time.Now().Add(l.timeout)) + if timeout != 0 { + l.packetConn.SetReadDeadline(time.Now().Add(timeout)) } n, src, err := l.packetConn.ReadFrom(b) @@ -106,15 +114,9 @@ func (l *Listener) Discover() (device *Device, deviceState DeviceState, err erro return } - // do first bytes match expected magic bytes? - if !bytes.Equal(b[0:4], magicBytes) { - err = ErrInvalidMessageReceived - return - } - // decode message - r := bytes.NewReader(b[4:n]) - m := new(DiscoveryMessage) + r := bytes.NewReader(b) + m := new(discoveryMessage) if err = m.readFrom(r); err != nil { return } @@ -122,9 +124,9 @@ func (l *Listener) Discover() (device *Device, deviceState DeviceState, err erro device = newDeviceFromDiscovery(src.(*net.UDPAddr), m) switch m.Action { - case DiscovererExit: + case discovererExit: deviceState = DeviceLeaving - case DiscovererHowdy: + case discovererHowdy: deviceState = DevicePresent default: err = ErrInvalidDiscovererActionReceived @@ -139,7 +141,7 @@ func Listen() (listener *Listener, err error) { return ListenWithConfiguration(nil) } -var zeroToken = [16]byte{} +var zeroToken = Token{} // ListenWithConfiguration sets up a StagelinQ listener with the given configuration. func ListenWithConfiguration(listenerConfig *ListenerConfiguration) (listener *Listener, err error) { @@ -176,7 +178,6 @@ func ListenWithConfiguration(listenerConfig *ListenerConfiguration) (listener *L packetConn: packetConn, softwareName: listenerConfig.SoftwareName, softwareVersion: listenerConfig.SoftwareVersion, - timeout: listenerConfig.DiscoveryTimeout, token: token, } diff --git a/listener_configuration.go b/listener_configuration.go index 9847926..b6a96ec 100644 --- a/listener_configuration.go +++ b/listener_configuration.go @@ -25,5 +25,5 @@ type ListenerConfiguration struct { SoftwareVersion string // Token is used as part of announcements and main data communication. It is currently recommended to leave this empty. - Token [16]byte + Token Token } diff --git a/main_conn.go b/main_conn.go deleted file mode 100644 index 808ad2b..0000000 --- a/main_conn.go +++ /dev/null @@ -1,66 +0,0 @@ -package stagelinq - -import "net" - -type AnnouncedService struct { - Name string - Port uint16 -} - -type Reference []byte - -type MainConnection struct { - token [16]byte - *deviceConn - - servicesC chan *AnnouncedService - referenceC chan *Reference -} - -// ConnectToDevice starts a StagelinQ connection with the given device. -func ConnectToDevice(listener *Listener, device *Device) (conn *MainConnection, err error) { - ip := device.IP - port := device.port - - tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ - IP: ip, - Port: int(port), - }) - if err != nil { - return - } - - deviceConn := newDeviceConn(tcpConn) - - conn = &MainConnection{ - token: listener.token, - deviceConn: deviceConn, - } - - return -} - -func (conn *MainConnection) Close() (err error) { - err = conn.deviceConn.Close() - return -} - -// RequestServices asks the device to return other TCP ports it is listening on and which services it provides on them. -func (conn *MainConnection) RequestServices() error { - return conn.deviceConn.WriteMessage(&ServicesRequestMessage{ - tokenPrefixedMessage: tokenPrefixedMessage{ - Token: conn.token, - }, - }) -} - -// AnnounceService tells the device about a service we provide. -func (conn *MainConnection) AnnounceService(name string, port uint16) error { - return conn.deviceConn.WriteMessage(&ServiceAnnouncementMessage{ - tokenPrefixedMessage: tokenPrefixedMessage{ - Token: conn.token, - }, - Service: name, - Port: port, - }) -} diff --git a/main_connection.go b/main_connection.go new file mode 100644 index 0000000..44be698 --- /dev/null +++ b/main_connection.go @@ -0,0 +1,178 @@ +package stagelinq + +import ( + "net" + "sync" + "time" +) + +// Service contains information about a data service a device provides. +type Service struct { + Name string + Port uint16 +} + +// MainConnection represents a connection to the main TCP port of a StagelinQ device. +type MainConnection struct { + lock sync.Mutex + + token Token + *messageConnection + + offeredServices []*Service + + servicesC chan *Service + atLeastOneServiceReceived bool + + errorC chan error + + reference int64 +} + +var mainConnectionMessageSet = newDeviceConnMessageSet([]message{ + &serviceAnnouncementMessage{}, + &referenceMessage{}, + &servicesRequestMessage{}, +}) + +// newMainConnection wraps an existing network connection to communicate StagelinQ main connection messages with it. +func newMainConnection(conn net.Conn, token Token, targetToken Token, offeredServices []*Service) (retval *MainConnection, err error) { + msgConn := newMessageConnection(conn, mainConnectionMessageSet) + + mainConn := &MainConnection{ + token: token, + messageConnection: msgConn, + errorC: make(chan error, 1), + offeredServices: offeredServices, + } + + go func() { + for { + <-time.After(250 * time.Millisecond) + + // TODO - we're always returning zero as timestamp here just like SoundSwitch does, we still need to implement the behavior Resolume Arena + mainConn.lock.Lock() + ref := mainConn.reference + mainConn.lock.Unlock() + if err = mainConn.messageConnection.WriteMessage(&referenceMessage{ + tokenPrefixedMessage: tokenPrefixedMessage{ + Token: mainConn.token, + }, + Token2: targetToken, + Reference: ref, + }); err != nil { + return + } + } + }() + + go func() { + var err error + defer func() { + if err != nil { + mainConn.errorC <- err + close(mainConn.errorC) + } + if mainConn.servicesC != nil { + close(mainConn.servicesC) + mainConn.servicesC = nil + } + }() + for { + var msg message + msg, err = mainConn.ReadMessage() + if err != nil { + return + } + + func() { + mainConn.lock.Lock() + defer mainConn.lock.Unlock() + + switch v := msg.(type) { + case *serviceAnnouncementMessage: + if mainConn.servicesC == nil { + err = ErrInvalidMessageReceived + break + } + mainConn.servicesC <- &Service{ + Name: v.Service, + Port: v.Port, + } + case *referenceMessage: + if mainConn.servicesC != nil { + close(mainConn.servicesC) + mainConn.servicesC = nil + } + // TODO - not sure what else to actually do with this information yet + // mainConn.reference = v.Reference + case *servicesRequestMessage: + for _, service := range mainConn.offeredServices { + if err = mainConn.announceService(service.Name, service.Port); err != nil { + return + } + } + } + }() + } + }() + + retval = mainConn + + return +} + +// Close terminates the connection. +func (conn *MainConnection) Close() (err error) { + return conn.conn.Close() +} + +// RequestServices asks the device to return other TCP ports it is listening on and which services it provides on them. +func (conn *MainConnection) RequestServices() (retval []*Service, err error) { + if err = conn.requestServices(); err != nil { + return + } + + conn.lock.Lock() + serviceC := make(chan *Service) + conn.servicesC = serviceC + conn.atLeastOneServiceReceived = false + services := []*Service{} + conn.lock.Unlock() + + for service := range serviceC { + services = append(services, service) + } + select { + case err = <-conn.errorC: + default: + } + + // the message reading loop already will set conn.servicesC back to nil + + retval = services + + return +} + +func (conn *MainConnection) requestServices() (err error) { + if err = conn.messageConnection.WriteMessage(&servicesRequestMessage{ + tokenPrefixedMessage: tokenPrefixedMessage{ + Token: conn.token, + }, + }); err != nil { + return + } + return +} + +// announceService tells the device about a service we provide. +func (conn *MainConnection) announceService(name string, port uint16) error { + return conn.messageConnection.WriteMessage(&serviceAnnouncementMessage{ + tokenPrefixedMessage: tokenPrefixedMessage{ + Token: conn.token, + }, + Service: name, + Port: port, + }) +} diff --git a/message_connection.go b/message_connection.go new file mode 100644 index 0000000..7b51021 --- /dev/null +++ b/message_connection.go @@ -0,0 +1,94 @@ +package stagelinq + +import ( + "bufio" + "bytes" + "net" + "reflect" +) + +type messageSet struct { + messages []reflect.Type +} + +func newDeviceConnMessageSet(messageObjects []message) *messageSet { + messages := make([]reflect.Type, len(messageObjects)) + for i, messageObject := range messageObjects { + // .Elem() because type will be a pointer-to-type but we want to create instances of the type itself later + messages[i] = reflect.TypeOf(messageObject).Elem() + } + return &messageSet{messages} +} + +func (ms *messageSet) Messages() []reflect.Type { + return ms.messages +} + +type messageConnection struct { + conn net.Conn + bufferedReader *bufio.Reader + expectedMessages *messageSet +} + +func newMessageConnection(conn net.Conn, expectedMessages *messageSet) *messageConnection { + if conn == nil { + panic("conn must not be nil") + } + if expectedMessages == nil { + panic("expectedMessages must not be nil") + } + if len(expectedMessages.Messages()) <= 0 { + panic("expectedMessages must not be empty") + } + return &messageConnection{ + conn: conn, + bufferedReader: bufio.NewReader(conn), + expectedMessages: expectedMessages, + } +} + +func (s *messageConnection) WriteMessage(msg message) (err error) { + buf := new(bytes.Buffer) + + // write message parts into buffer + if err = msg.writeTo(buf); err != nil { + return + } + + // write the whole thing out as one message to the device + _, err = s.conn.Write(buf.Bytes()) + + // if err == nil { + // log.Printf("SEND: %s", spew.Sdump(msg)) + // } + + return +} + +func (s *messageConnection) ReadMessage() (msg message, err error) { + var targetMsg message + var ok bool + for _, messageType := range s.expectedMessages.Messages() { + targetMsg = reflect.New(messageType).Interface().(message) + ok, err = targetMsg.checkMatch(s.bufferedReader) + if err != nil { + return + } + if ok { + break + } + } + + if !ok { + err = ErrInvalidMessageReceived + return + } + + err = targetMsg.readFrom(s.bufferedReader) + if err == nil { + msg = targetMsg + // log.Printf("RECV: %s", spew.Sdump(msg)) + } + + return +} diff --git a/device_conn_test.go b/message_connection_test.go similarity index 77% rename from device_conn_test.go rename to message_connection_test.go index 5b916d1..8605774 100644 --- a/device_conn_test.go +++ b/message_connection_test.go @@ -7,17 +7,17 @@ import ( "github.com/stretchr/testify/require" ) -var testToken = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} +var testToken = Token{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} -func Test_DeviceConn_Read(t *testing.T) { +func Test_MessageConnection_Read(t *testing.T) { testMessages := []struct { Message message Bytes []byte }{ { - Message: &ServiceAnnouncementMessage{ + Message: &serviceAnnouncementMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, }, Service: "StateMap", Port: 0xb1d7, @@ -38,9 +38,9 @@ func Test_DeviceConn_Read(t *testing.T) { }, }, { - Message: &ReferenceMessage{ + Message: &referenceMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, }, Reference: 0x000009ed4f310604, }, @@ -60,9 +60,9 @@ func Test_DeviceConn_Read(t *testing.T) { }, }, { - Message: &ServicesRequestMessage{ + Message: &servicesRequestMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, }, }, Bytes: []byte{ @@ -73,9 +73,9 @@ func Test_DeviceConn_Read(t *testing.T) { }, }, { - Message: &ServiceAnnouncementMessage{ + Message: &serviceAnnouncementMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, + Token: Token{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, }, Service: "DirectoryService", Port: 0xe190, @@ -99,9 +99,9 @@ func Test_DeviceConn_Read(t *testing.T) { }, }, { - Message: &ServicesRequestMessage{ + Message: &servicesRequestMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, + Token: Token{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, }, }, Bytes: []byte{ @@ -116,8 +116,8 @@ func Test_DeviceConn_Read(t *testing.T) { }, }, { - Message: &ReferenceMessage{ - Token2: [16]byte{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, + Message: &referenceMessage{ + Token2: Token{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, Reference: 0x000009ed4f310604, }, Bytes: []byte{ @@ -161,17 +161,23 @@ func Test_DeviceConn_Read(t *testing.T) { if err != nil { t.Fatalf("Failed to accept test connection: %s", err.Error()) } - deviceConn := newDeviceConn(conn) + + messageObjects := []message{} + for _, testMessage := range testMessages { + messageObjects = append(messageObjects, testMessage.Message) + } + msgConn := newMessageConnection(conn, newDeviceConnMessageSet(messageObjects)) + for _, expectedMessage := range testMessages { - message, err := deviceConn.ReadMessage() + message, err := msgConn.ReadMessage() require.Nil(t, err) require.Equal(t, expectedMessage.Message, message) } } -func Test_DeviceConn(t *testing.T) { +func Test_MessageConnection(t *testing.T) { testMessages := []message{ - &ServiceAnnouncementMessage{ + &serviceAnnouncementMessage{ tokenPrefixedMessage: tokenPrefixedMessage{testToken}, Service: "test", Port: 0x1234, @@ -190,9 +196,11 @@ func Test_DeviceConn(t *testing.T) { t.Fatalf("Failed to set up test connection: %s", err.Error()) return } - deviceConn := newDeviceConn(conn) + + msgConn := newMessageConnection(conn, newDeviceConnMessageSet(testMessages)) + for _, testMessage := range testMessages { - err := deviceConn.WriteMessage(testMessage) + err := msgConn.WriteMessage(testMessage) require.Nil(t, err) } }() @@ -201,9 +209,9 @@ func Test_DeviceConn(t *testing.T) { if err != nil { t.Fatalf("Failed to accept test connection: %s", err.Error()) } - deviceConn := newDeviceConn(conn) + msgConn := newMessageConnection(conn, newDeviceConnMessageSet(testMessages)) for _, expectedMessage := range testMessages { - message, err := deviceConn.ReadMessage() + message, err := msgConn.ReadMessage() require.Nil(t, err) require.Equal(t, expectedMessage, message) } diff --git a/messages.go b/messages.go index fbb49cd..c26e3ab 100644 --- a/messages.go +++ b/messages.go @@ -1,17 +1,42 @@ package stagelinq import ( + "bufio" + "bytes" "encoding/binary" + "errors" "io" ) -// Token contains the identifying token for a device in the StagelinQ network. +// Token contains the identifying Token for a device in the StagelinQ network. type Token [16]byte type message interface { readFrom(io.Reader) error writeTo(io.Writer) error - id() int32 + + // checkMatch MUST use the Peek method to read any bytes needed to exactly identify whether a message matches. + // It SHOULD not peek more bytes than are necessary to identify the message. + // The method MUST avoid Read to allow other message types to validate the message properly. + checkMatch(*bufio.Reader) (bool, error) +} + +func readMessageID(r io.Reader) (id int32, err error) { + err = binary.Read(r, binary.BigEndian, &id) + return +} + +func peekMessageID(r *bufio.Reader) (id int32, err error) { + b, err := r.Peek(4) + if err != nil { + return + } + return readMessageID(bytes.NewReader(b)) +} + +func writeMessageID(w io.Writer, id int32) (err error) { + err = binary.Write(w, binary.BigEndian, id) + return } type tokenPrefixedMessage struct { @@ -28,17 +53,29 @@ func (m *tokenPrefixedMessage) writeTo(w io.Writer) (err error) { return } -type ServiceAnnouncementMessage struct { +type serviceAnnouncementMessage struct { tokenPrefixedMessage Service string Port uint16 } -func (m *ServiceAnnouncementMessage) id() int32 { - return 0x00000000 +func (m *serviceAnnouncementMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + id, err := peekMessageID(r) + if err != nil { + return + } + ok = id == 0x00000000 + return } -func (m *ServiceAnnouncementMessage) readFrom(r io.Reader) (err error) { +func (m *serviceAnnouncementMessage) readFrom(r io.Reader) (err error) { + messageID, err := readMessageID(r) + if err != nil { + return + } else if messageID != 0x00000000 { + err = ErrInvalidMessageReceived + return + } if err = m.tokenPrefixedMessage.readFrom(r); err != nil { return } @@ -51,7 +88,10 @@ func (m *ServiceAnnouncementMessage) readFrom(r io.Reader) (err error) { return } -func (m *ServiceAnnouncementMessage) writeTo(w io.Writer) (err error) { +func (m *serviceAnnouncementMessage) writeTo(w io.Writer) (err error) { + if err = writeMessageID(w, 0x00000000); err != nil { + return + } if err = m.tokenPrefixedMessage.writeTo(w); err != nil { return } @@ -62,17 +102,29 @@ func (m *ServiceAnnouncementMessage) writeTo(w io.Writer) (err error) { return } -type ReferenceMessage struct { +type referenceMessage struct { tokenPrefixedMessage Token2 Token Reference int64 } -func (m *ReferenceMessage) id() int32 { - return 0x00000001 +func (m *referenceMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + id, err := peekMessageID(r) + if err != nil { + return + } + ok = id == 0x00000001 + return } -func (m *ReferenceMessage) readFrom(r io.Reader) (err error) { +func (m *referenceMessage) readFrom(r io.Reader) (err error) { + messageID, err := readMessageID(r) + if err != nil { + return + } else if messageID != 0x00000001 { + err = ErrInvalidMessageReceived + return + } if err = m.tokenPrefixedMessage.readFrom(r); err != nil { return } @@ -83,7 +135,10 @@ func (m *ReferenceMessage) readFrom(r io.Reader) (err error) { return } -func (m *ReferenceMessage) writeTo(w io.Writer) (err error) { +func (m *referenceMessage) writeTo(w io.Writer) (err error) { + if err = writeMessageID(w, 0x00000001); err != nil { + return + } if err = m.tokenPrefixedMessage.writeTo(w); err != nil { return } @@ -94,41 +149,281 @@ func (m *ReferenceMessage) writeTo(w io.Writer) (err error) { return } -type ServicesRequestMessage struct { +type servicesRequestMessage struct { tokenPrefixedMessage } -func (m *ServicesRequestMessage) id() int32 { - return 0x00000002 +func (m *servicesRequestMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + id, err := peekMessageID(r) + if err != nil { + return + } + ok = id == 0x00000002 + return +} + +func (m *servicesRequestMessage) readFrom(r io.Reader) (err error) { + messageID, err := readMessageID(r) + if err != nil { + return + } else if messageID != 0x00000002 { + err = ErrInvalidMessageReceived + return + } + + err = m.tokenPrefixedMessage.readFrom(r) + return +} + +func (m *servicesRequestMessage) writeTo(w io.Writer) (err error) { + if err = writeMessageID(w, 0x00000002); err != nil { + return + } + + err = m.tokenPrefixedMessage.writeTo(w) + return +} + +// TODO - StateSubscribeMessage.Interval: Check what Interval actually is, it seems to either be 00 00 00 00 (Resolume Arena) or 00 00 00 0a (SoundSwitch) + +var smaaMagicBytes = []byte{0x73, 0x6d, 0x61, 0x61} + +func checkSmaa(r *bufio.Reader, id int32) (ok bool, err error) { + // peek length bytes and smaa magic bytes + b, err := r.Peek(4 + 4 + 4) + if err != nil { + return + } + + // check smaa magic bytes + if ok = bytes.Equal(b[4:8], smaaMagicBytes); !ok { + return + } + + // check id + if int32(binary.BigEndian.Uint32(b[8:12])) != id { + ok = false + } + + return +} + +type stateSubscribeMessage struct { + //Length uint32 + //Unknown []byte = {0x73,0x6d,0x61,0x61} + //Unknown2 []byte = {0x00,0x00,0x07,0xd2} + Name string + Interval uint32 +} + +func (m *stateSubscribeMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + return checkSmaa(r, 0x000007d2) +} + +func (m *stateSubscribeMessage) readFrom(r io.Reader) (err error) { + var expectedLength uint32 + if err = binary.Read(r, binary.BigEndian, &expectedLength); err != nil { + return + } + + // read smaa magic bytes + magicBytes := make([]byte, 4) + if _, err = r.Read(magicBytes); err != nil { + return + } + if !bytes.Equal(magicBytes, smaaMagicBytes) { + err = errors.New("invalid smaa magic bytes") + return + } + + // TODO - figure this out + if _, err = r.Read(magicBytes); err != nil { + return + } + if !bytes.Equal(magicBytes, []byte{0x00, 0x00, 0x07, 0xd2}) { + err = errors.New("invalid post-smaa magic bytes") + return + } + + // read value name + if err = readNetworkString(r, &m.Name); err != nil { + return + } + + // TODO - figure this out + err = binary.Read(r, binary.BigEndian, &m.Interval) + return } -// DiscovererMessageAction is the action taken by a device as part of StagelinQ device discovery. +func (m *stateSubscribeMessage) writeTo(w io.Writer) (err error) { + // write smaa magic bytes + buf := new(bytes.Buffer) + if _, err = buf.Write(smaaMagicBytes); err != nil { + return + } + + // TODO - figure this out + if _, err = buf.Write([]byte{0x00, 0x00, 0x07, 0xd2}); err != nil { + return + } + + // write value name + if err = writeNetworkString(buf, m.Name); err != nil { + return + } + + // TODO - figure this out + if err = binary.Write(buf, binary.BigEndian, m.Interval); err != nil { + return + } + + // send message length over wire + if err = binary.Write(w, binary.BigEndian, uint32(buf.Len())); err != nil { + return + } + + // send actual message over wire + _, err = w.Write(buf.Bytes()) + return +} + +type stateEmitMessage struct { + //Length uint32 + //Unknown []byte = {0x73,0x6d,0x61,0x61} + //Unknown2 []byte = {0x00,0x00,0x00,0x00} + Name string + JSON string +} + +func (m *stateEmitMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + return checkSmaa(r, 0x00000000) +} + +func (m *stateEmitMessage) readFrom(r io.Reader) (err error) { + // read expected message length + var expectedLength uint32 + if err = binary.Read(r, binary.BigEndian, &expectedLength); err != nil { + return + } + + // set up buffer to write message into + msgBytes := make([]byte, int(expectedLength)) + msgBytesOffset := 0 + for msgBytesOffset < int(expectedLength) { + var n int + if n, err = r.Read(msgBytes[msgBytesOffset:]); err != nil { + return + } + msgBytesOffset += n + } + msgReader := bytes.NewReader(msgBytes) + + // read smaa magic bytes + magicBytes := make([]byte, 4) + if _, err = msgReader.Read(magicBytes); err != nil { + return + } + if !bytes.Equal(magicBytes, smaaMagicBytes) { + err = errors.New("invalid smaa magic bytes") + return + } + + // TODO - figure this out + if _, err = msgReader.Read(magicBytes); err != nil { + return + } + if !bytes.Equal(magicBytes, []byte{0x00, 0x00, 0x00, 0x00}) { + err = errors.New("invalid post-smaa magic bytes") + return + } + + // read value name + if err = readNetworkString(msgReader, &m.Name); err != nil { + return + } + + // read value JSON + if err = readNetworkString(msgReader, &m.JSON); err != nil { + return + } + + return +} + +func (m *stateEmitMessage) writeTo(w io.Writer) (err error) { + buf := new(bytes.Buffer) + + // write smaa magic bytes to message buffer + if _, err = buf.Write(smaaMagicBytes); err != nil { + return + } + + // TODO - figure this out + if _, err = buf.Write([]byte{0x00, 0x00, 0x00, 0x00}); err != nil { + return + } + + // write value name to message buffer + if err = writeNetworkString(buf, m.Name); err != nil { + return + } + + // write value JSON to message buffer + if err = writeNetworkString(buf, m.JSON); err != nil { + return + } + + // send message length over wire + if err = binary.Write(w, binary.BigEndian, uint32(buf.Len())); err != nil { + return + } + + // send actual message over wire + _, err = w.Write(buf.Bytes()) + return +} + +// discovererMessageAction is the action taken by a device as part of StagelinQ device discovery. // Possible values are DiscovererHowdy or DiscovererExit. -type DiscovererMessageAction string +type discovererMessageAction string const ( - // DiscovererHowdy is the value set on the Action field of a DiscoveryMessage when a StagelinQ-compatible device announces itself in the network. - DiscovererHowdy DiscovererMessageAction = "DISCOVERER_HOWDY_" + // discovererHowdy is the value set on the Action field of a DiscoveryMessage when a StagelinQ-compatible device announces itself in the network. + discovererHowdy discovererMessageAction = "DISCOVERER_HOWDY_" - // DiscovererExit is the value set on the Action field of a DiscoveryMessage when a StagelinQ-compatible device leaves the network. - DiscovererExit DiscovererMessageAction = "DISCOVERER_EXIT_" + // discovererExit is the value set on the Action field of a DiscoveryMessage when a StagelinQ-compatible device leaves the network. + discovererExit discovererMessageAction = "DISCOVERER_EXIT_" ) -// DiscoveryMessage contains the data carried in the message payload for device trying to handshake the StagelinQ protocol to any other device in the network. -type DiscoveryMessage struct { +// discoveryMessage contains the data carried in the message payload for device trying to handshake the StagelinQ protocol to any other device in the network. +type discoveryMessage struct { tokenPrefixedMessage Source string - Action DiscovererMessageAction + Action discovererMessageAction SoftwareName string SoftwareVersion string Port uint16 } -func (m *DiscoveryMessage) id() int32 { - return 0x61697244 +var discoveryMagic = []byte("airD") + +func (m *discoveryMessage) checkMatch(r *bufio.Reader) (ok bool, err error) { + var readMagic []byte + if readMagic, err = r.Peek(4); err != nil { + return + } + ok = bytes.Equal(readMagic, discoveryMagic) + return } -func (m *DiscoveryMessage) readFrom(r io.Reader) (err error) { +func (m *discoveryMessage) readFrom(r io.Reader) (err error) { + readMagic := make([]byte, 4) + if _, err = r.Read(readMagic); err != nil { + return + } else if !bytes.Equal(readMagic, discoveryMagic) { + err = ErrInvalidMessageReceived + return + } if err = m.tokenPrefixedMessage.readFrom(r); err != nil { return } @@ -139,7 +434,7 @@ func (m *DiscoveryMessage) readFrom(r io.Reader) (err error) { if err = readNetworkString(r, &actionString); err != nil { return } - m.Action = DiscovererMessageAction(actionString) + m.Action = discovererMessageAction(actionString) if err = readNetworkString(r, &m.SoftwareName); err != nil { return } @@ -150,7 +445,10 @@ func (m *DiscoveryMessage) readFrom(r io.Reader) (err error) { return } -func (m *DiscoveryMessage) writeTo(w io.Writer) (err error) { +func (m *discoveryMessage) writeTo(w io.Writer) (err error) { + if _, err = w.Write(discoveryMagic); err != nil { + return + } if err = m.tokenPrefixedMessage.writeTo(w); err != nil { return } @@ -171,13 +469,3 @@ func (m *DiscoveryMessage) writeTo(w io.Writer) (err error) { } return } - -var tcpMessageMap = map[int32]func() message{ - 0x00000000: func() message { return new(ServiceAnnouncementMessage) }, - 0x00000001: func() message { return new(ReferenceMessage) }, - 0x00000002: func() message { return new(ServicesRequestMessage) }, -} - -var udpMessageMap = map[int32]func() message{ - 0x61697244 /* "airD" */ : func() message { return new(DiscoveryMessage) }, -} diff --git a/messages_test.go b/messages_test.go index 9ed40fb..65d6ea6 100644 --- a/messages_test.go +++ b/messages_test.go @@ -1,6 +1,7 @@ package stagelinq import ( + "bufio" "bytes" "testing" @@ -14,7 +15,7 @@ var testMessages = []struct { }{ { Bytes: []byte{ - /*0x61, 0x69, 0x72, 0x44,*/ 0xf4, 0x05, 0xdc, 0x14, + 0x61, 0x69, 0x72, 0x44, 0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x70, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6d, @@ -28,13 +29,13 @@ var testMessages = []struct { 0x00, 0x0a, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x35, 0x00, 0x2e, 0x00, 0x32, 0x84, 0x03, }, - CreateMessage: func() message { return new(DiscoveryMessage) }, - Message: &DiscoveryMessage{ + CreateMessage: func() message { return new(discoveryMessage) }, + Message: &discoveryMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, }, Source: "prime4", - Action: DiscovererHowdy, + Action: discovererHowdy, SoftwareName: "JC11", SoftwareVersion: "1.5.2", Port: 0x8403, @@ -42,17 +43,17 @@ var testMessages = []struct { }, { Bytes: []byte{ - /*0x00, 0x00, 0x00, 0x00,*/ 0x52, 0x3e, 0x67, 0x9d, + 0x00, 0x00, 0x00, 0x00, 0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7, 0x00, 0x00, 0x00, 0x10, 0x00, 0x53, 0x00, 0x74, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x4d, 0x00, 0x61, 0x00, 0x70, 0xe1, 0x96, }, - CreateMessage: func() message { return new(ServiceAnnouncementMessage) }, - Message: &ServiceAnnouncementMessage{ + CreateMessage: func() message { return new(serviceAnnouncementMessage) }, + Message: &serviceAnnouncementMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, + Token: Token{0x52, 0x3e, 0x67, 0x9d, 0xa4, 0x18, 0x4d, 0x1e, 0x83, 0xd0, 0xc7, 0x52, 0xcf, 0xca, 0x8f, 0xf7}, }, Service: "StateMap", Port: 0xe196, @@ -60,39 +61,91 @@ var testMessages = []struct { }, { Bytes: []byte{ - /*0x00, 0x00, 0x00, 0x02,*/ 0xf4, 0x05, 0xdc, 0x14, + 0x00, 0x00, 0x00, 0x02, 0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76, }, - CreateMessage: func() message { return new(ServicesRequestMessage) }, - Message: &ServicesRequestMessage{ + CreateMessage: func() message { return new(servicesRequestMessage) }, + Message: &servicesRequestMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, }, }, }, { Bytes: []byte{ - /*0x00, 0x00, 0x00, 0x01,*/ 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76, 0x00, 0x00, 0x09, 0xed, 0x4f, 0x31, 0x06, 0x04, }, - CreateMessage: func() message { return new(ReferenceMessage) }, - Message: &ReferenceMessage{ + CreateMessage: func() message { return new(referenceMessage) }, + Message: &referenceMessage{ tokenPrefixedMessage: tokenPrefixedMessage{ - Token: [16]byte{}, + Token: Token{}, }, - Token2: [16]byte{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, + Token2: Token{0xf4, 0x05, 0xdc, 0x14, 0x02, 0x23, 0x47, 0xf5, 0x8b, 0x79, 0x2c, 0x8c, 0x49, 0x33, 0x52, 0x76}, Reference: 0x000009ed4f310604, }, }, + // { + // Bytes: []byte{ + // 0x00, 0x00, 0x00, 0x00, 0x06, 0xd2, 0x3b, 0xe4, + // 0x8e, 0xb2, 0x4f, 0xc7, 0x8f, 0x03, 0xc6, 0xcc, + // 0x70, 0x70, 0x30, 0x1b, 0x00, 0x00, 0x00, 0x10, + // 0x00, 0x53, 0x00, 0x74, 0x00, 0x61, 0x00, 0x74, + // 0x00, 0x65, 0x00, 0x4d, 0x00, 0x61, 0x00, 0x70, + // 0xff, 0xbd, + // }, + // }, + { + Bytes: []byte{ + 0x00, 0x00, 0x00, 0x44, 0x73, 0x6d, 0x61, 0x61, + 0x00, 0x00, 0x07, 0xd2, 0x00, 0x00, 0x00, 0x34, + 0x00, 0x2f, 0x00, 0x43, 0x00, 0x6c, 0x00, 0x69, + 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x2f, + 0x00, 0x50, 0x00, 0x72, 0x00, 0x65, 0x00, 0x66, + 0x00, 0x65, 0x00, 0x72, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, 0x2f, + 0x00, 0x50, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x79, + 0x00, 0x65, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00, + }, + CreateMessage: func() message { return new(stateSubscribeMessage) }, + Message: &stateSubscribeMessage{ + Name: ClientPreferencesPlayer, + }, + }, + { + Bytes: []byte{ + 0x00, 0x00, 0x00, 0x72, 0x73, 0x6d, 0x61, 0x61, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, + 0x00, 0x2f, 0x00, 0x43, 0x00, 0x6c, 0x00, 0x69, + 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x2f, + 0x00, 0x50, 0x00, 0x72, 0x00, 0x65, 0x00, 0x66, + 0x00, 0x65, 0x00, 0x72, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, 0x2f, + 0x00, 0x50, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x79, + 0x00, 0x65, 0x00, 0x72, 0x00, 0x00, 0x00, 0x2e, + 0x00, 0x7b, 0x00, 0x22, 0x00, 0x73, 0x00, 0x74, + 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x67, + 0x00, 0x22, 0x00, 0x3a, 0x00, 0x22, 0x00, 0x31, + 0x00, 0x22, 0x00, 0x2c, 0x00, 0x22, 0x00, 0x74, + 0x00, 0x79, 0x00, 0x70, 0x00, 0x65, 0x00, 0x22, + 0x00, 0x3a, 0x00, 0x34, 0x00, 0x7d, + }, + CreateMessage: func() message { return new(stateEmitMessage) }, + Message: &stateEmitMessage{ + Name: ClientPreferencesPlayer, + JSON: `{"string":"1","type":4}`, + }, + }, } func Test_Messages_Read(t *testing.T) { - for _, def := range testMessages { + for i, def := range testMessages { + t.Logf("Message index %d", i) r := bytes.NewReader(def.Bytes) m := def.CreateMessage() err := m.readFrom(r) @@ -102,7 +155,8 @@ func Test_Messages_Read(t *testing.T) { } func Test_Messages_Write(t *testing.T) { - for _, def := range testMessages { + for i, def := range testMessages { + t.Logf("Message index %d", i) buf := new(bytes.Buffer) err := def.Message.writeTo(buf) require.Nil(t, err) @@ -110,3 +164,12 @@ func Test_Messages_Write(t *testing.T) { require.Equal(t, def.Bytes, resultBytes) } } + +func Test_Messages_CheckMatch(t *testing.T) { + for i, def := range testMessages { + t.Logf("Message index %d", i) + ok, err := def.Message.checkMatch(bufio.NewReader(bytes.NewReader(def.Bytes))) + require.Nil(t, err) + require.True(t, ok) + } +}